[FIX] Adjusted form validation to disallow duplicated emails (#27037)

pull/27167/head
Aleksander Nicacio da Silva 3 years ago committed by GitHub
parent 91844b220a
commit 462b774b27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      apps/meteor/client/components/CustomFieldsForm.js
  2. 7
      apps/meteor/client/sidebar/sections/OmnichannelSection.tsx
  3. 255
      apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js
  4. 262
      apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx
  5. 226
      apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts
  6. 34
      apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts
  7. 13
      apps/meteor/tests/e2e/page-objects/omnichannel-info.ts
  8. 45
      apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts
  9. 25
      apps/meteor/tests/e2e/page-objects/omnichannel-section.ts
  10. 12
      apps/meteor/tests/e2e/utils/test.ts

@ -113,7 +113,7 @@ const CustomFieldsAssembler = ({ formValues, formHandlers, customFields, ...prop
});
export default function CustomFieldsForm({
jsonCustomFields = undefined,
jsonCustomFields = {},
customFieldsData,
setCustomFieldsData,
onLoadFields = () => {},
@ -129,7 +129,7 @@ export default function CustomFieldsForm({
}
});
const hasCustomFields = Boolean(Object.values(customFields).length);
const hasCustomFields = useMemo(() => Object.values(customFields).length > 0, [customFields]);
const defaultFields = useMemo(
() =>
Object.entries(customFields).reduce((data, [key, value]) => {
@ -142,11 +142,14 @@ export default function CustomFieldsForm({
const { values, handlers } = useForm({ ...defaultFields, ...customFieldsData });
useEffect(() => {
onLoadFields(hasCustomFields);
onLoadFields?.(hasCustomFields);
}, [onLoadFields, hasCustomFields]);
useEffect(() => {
if (hasCustomFields) {
setCustomFieldsData(values);
}
}, [hasCustomFields, onLoadFields, setCustomFieldsData, values]);
}, [hasCustomFields, setCustomFieldsData, values]);
if (!hasCustomFields) {
return null;

@ -41,7 +41,12 @@ const OmnichannelSection = (props: typeof Box): ReactElement => {
{isCallEnabled && <OmnichannelCallToggle />}
<OmnichannelLivechatToggle />
{hasPermissionToSeeContactCenter && (
<Sidebar.TopBar.Action data-tooltip={t('Contact_Center')} icon='address-book' onClick={(): void => handleRoute('directory')} />
<Sidebar.TopBar.Action
data-tooltip={t('Contact_Center')}
aria-label={t('Contact_Center')}
icon='address-book'
onClick={(): void => handleRoute('directory')}
/>
)}
{isCallReady && <OmniChannelCallDialPad />}
</Sidebar.TopBar.Actions>

@ -1,255 +0,0 @@
import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import React, { useState, useMemo, useEffect } from 'react';
import { hasAtLeastOnePermission } from '../../../../../../app/authorization/client';
import { validateEmail } from '../../../../../../lib/emailValidator';
import CustomFieldsForm from '../../../../../components/CustomFieldsForm';
import VerticalBar from '../../../../../components/VerticalBar';
import { AsyncStatePhase } from '../../../../../hooks/useAsyncState';
import { useComponentDidUpdate } from '../../../../../hooks/useComponentDidUpdate';
import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { useForm } from '../../../../../hooks/useForm';
import { createToken } from '../../../../../lib/utils/createToken';
import { useFormsSubscription } from '../../../additionalForms';
import { FormSkeleton } from '../../Skeleton';
const initialValues = {
token: '',
name: '',
email: '',
phone: '',
username: '',
};
const getInitialValues = (data) => {
if (!data) {
return initialValues;
}
const {
contact: { name, token, phone, visitorEmails, livechatData, contactManager },
} = data;
return {
token: token ?? '',
name: name ?? '',
email: visitorEmails ? visitorEmails[0].address : '',
phone: phone ? phone[0].phoneNumber : '',
livechatData: livechatData ?? {},
username: contactManager?.username ?? '',
};
};
function ContactNewEdit({ id, data, close }) {
const t = useTranslation();
const canViewCustomFields = () => hasAtLeastOnePermission(['view-livechat-room-customfields', 'edit-livechat-room-customfields']);
const initialValue = getInitialValues(data);
const { username: initialUsername } = initialValue;
const { values, handlers, hasUnsavedChanges: hasUnsavedChangesContact } = useForm(initialValue);
const eeForms = useFormsSubscription();
const { useContactManager = () => {} } = eeForms;
const ContactManager = useContactManager();
const { handleName, handleEmail, handlePhone, handleUsername } = handlers;
const { token, name, email, phone, username } = values;
const {
values: valueCustom,
handlers: handleValueCustom,
hasUnsavedChanges: hasUnsavedChangesCustomFields,
} = useForm({
livechatData: values.livechatData,
});
const { handleLivechatData } = handleValueCustom;
const { livechatData } = valueCustom;
const [nameError, setNameError] = useState();
const [emailError, setEmailError] = useState();
const [phoneError, setPhoneError] = useState();
const [customFieldsError, setCustomFieldsError] = useState([]);
const [userId, setUserId] = useState('no-agent-selected');
const { value: allCustomFields, phase: state } = useEndpointData('/v1/livechat/custom-fields');
const jsonConverterToValidFormat = (customFields) => {
const jsonObj = {};
customFields.forEach(({ _id, label, visibility, options, scope, defaultValue, required }) => {
(visibility === 'visible') & (scope === 'visitor') &&
(jsonObj[_id] = {
label,
type: options ? 'select' : 'text',
required,
defaultValue,
options: options && options.split(',').map((item) => item.trim()),
});
});
return jsonObj;
};
const jsonCustomField = useMemo(
() => (allCustomFields && allCustomFields.customFields ? jsonConverterToValidFormat(allCustomFields.customFields) : {}),
[allCustomFields],
);
const saveContact = useEndpoint('POST', '/v1/omnichannel/contact');
const emailAlreadyExistsAction = useEndpoint('GET', '/v1/omnichannel/contact.search');
const phoneAlreadyExistsAction = useEndpoint('GET', '/v1/omnichannel/contact.search');
const getUserData = useEndpoint('GET', '/v1/users.info');
const checkEmailExists = useMutableCallback(async () => {
if (!validateEmail(email)) {
return;
}
const { contact } = await emailAlreadyExistsAction({ email });
if (!contact || (id && contact._id === id)) {
return setEmailError(null);
}
setEmailError(t('Email_already_exists'));
});
const checkPhoneExists = useMutableCallback(async () => {
if (!phone) {
return;
}
const { contact } = await phoneAlreadyExistsAction({ phone });
if (!contact || (id && contact._id === id)) {
return setPhoneError(null);
}
setPhoneError(t('Phone_already_exists'));
});
const dispatchToastMessage = useToastMessageDispatch();
useComponentDidUpdate(() => {
setNameError(!name ? t('The_field_is_required', t('Name')) : '');
}, [t, name]);
useComponentDidUpdate(() => {
setEmailError(email && !validateEmail(email) ? t('Validate_email_address') : null);
}, [t, email]);
useComponentDidUpdate(() => {
!phone && setPhoneError(null);
}, [phone]);
useEffect(() => {
if (!initialUsername) {
return;
}
getUserData({ username: initialUsername }).then(({ user }) => {
setUserId(user._id);
});
}, [getUserData, initialUsername]);
const handleContactManagerChange = useMutableCallback(async (userId) => {
setUserId(userId);
if (userId === 'no-agent-selected') {
handleUsername('');
return;
}
getUserData({ userId }).then(({ user }) => {
handleUsername(user.username);
});
});
const handleSave = useMutableCallback(async (e) => {
e.preventDefault();
let error = false;
if (!name) {
setNameError(t('The_field_is_required', 'name'));
error = true;
}
if (email && !validateEmail(email)) {
setEmailError(t('Validate_email_address'));
error = true;
}
if (error) {
return;
}
const payload = {
name,
phone,
email,
customFields: livechatData || {},
token: token || createToken(),
...(username && { contactManager: { username } }),
...(id && { _id: id }),
};
try {
await saveContact(payload);
dispatchToastMessage({ type: 'success', message: t('Saved') });
close();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
const formIsValid =
(hasUnsavedChangesContact || hasUnsavedChangesCustomFields) && name && !emailError && !phoneError && customFieldsError.length === 0;
if ([state].includes(AsyncStatePhase.LOADING)) {
return <FormSkeleton />;
}
return (
<>
<VerticalBar.ScrollableContent is='form'>
<Field>
<Field.Label>{t('Name')}*</Field.Label>
<Field.Row>
<TextInput error={nameError} flexGrow={1} value={name} onChange={handleName} />
</Field.Row>
<Field.Error>{nameError}</Field.Error>
</Field>
<Field>
<Field.Label>{t('Email')}</Field.Label>
<Field.Row>
<TextInput onBlur={checkEmailExists} error={emailError} flexGrow={1} value={email} onChange={handleEmail} />
</Field.Row>
<Field.Error>{t(emailError)}</Field.Error>
</Field>
<Field>
<Field.Label>{t('Phone')}</Field.Label>
<Field.Row>
<TextInput onBlur={checkPhoneExists} error={phoneError} flexGrow={1} value={phone} onChange={handlePhone} />
</Field.Row>
<Field.Error>{t(phoneError)}</Field.Error>
</Field>
{canViewCustomFields() && allCustomFields && (
<CustomFieldsForm
jsonCustomFields={jsonCustomField}
customFieldsData={livechatData}
setCustomFieldsData={handleLivechatData}
setCustomFieldsError={setCustomFieldsError}
/>
)}
{ContactManager && <ContactManager value={userId} handler={handleContactManagerChange} />}
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup stretch>
<Button flexGrow={1} onClick={close}>
{t('Cancel')}
</Button>
<Button mie='none' flexGrow={1} onClick={handleSave} disabled={!formIsValid} primary>
{t('Save')}
</Button>
</ButtonGroup>
</VerticalBar.Footer>
</>
);
}
export default ContactNewEdit;

@ -0,0 +1,262 @@
import { ILivechatCustomField, ILivechatVisitor, Serialized } from '@rocket.chat/core-typings';
import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React, { useState, useEffect, ReactElement } from 'react';
import { useController, useForm } from 'react-hook-form';
import { debounce } from 'underscore';
import { hasAtLeastOnePermission } from '../../../../../../app/authorization/client';
import { validateEmail } from '../../../../../../lib/emailValidator';
import CustomFieldsForm from '../../../../../components/CustomFieldsForm';
import VerticalBar from '../../../../../components/VerticalBar';
import { createToken } from '../../../../../lib/utils/createToken';
import { useFormsSubscription } from '../../../additionalForms';
import { FormSkeleton } from '../../Skeleton';
type ContactNewEditProps = {
id: string;
data: { contact: Serialized<ILivechatVisitor> | null };
close(): void;
};
type ContactFormData = {
token: string;
name: string;
email: string;
phone: string;
username: string;
customFields: Record<any, any>;
};
type CustomFieldsMetadata = Record<
string,
{
label: string;
type: 'select' | 'text';
required?: boolean;
defaultValue?: unknown;
options?: string[];
}
>;
const DEFAULT_VALUES = {
token: '',
name: '',
email: '',
phone: '',
username: '',
customFields: {},
};
const getInitialValues = (data: ContactNewEditProps['data']): ContactFormData => {
if (!data) {
return DEFAULT_VALUES;
}
const { name, token, phone, visitorEmails, livechatData, contactManager } = data.contact ?? {};
return {
token: token ?? '',
name: name ?? '',
email: visitorEmails ? visitorEmails[0].address : '',
phone: phone ? phone[0].phoneNumber : '',
customFields: livechatData ?? {},
username: contactManager?.username ?? '',
};
};
const formatCustomFieldsMetadata = (customFields: Serialized<ILivechatCustomField>[]): CustomFieldsMetadata =>
customFields
.filter(({ visibility, scope }) => visibility === 'visible' && scope === 'visitor')
.reduce((obj, { _id, label, options, defaultValue, required }) => {
obj[_id] = {
label,
type: options ? 'select' : 'text',
required,
defaultValue,
options: options?.split(',').map((item) => item.trim()),
};
return obj;
}, {} as CustomFieldsMetadata);
export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement => {
const t = useTranslation();
const canViewCustomFields = (): boolean =>
hasAtLeastOnePermission(['view-livechat-room-customfields', 'edit-livechat-room-customfields']);
const { useContactManager } = useFormsSubscription();
const ContactManager = useContactManager?.();
const [userId, setUserId] = useState('no-agent-selected');
const dispatchToastMessage = useToastMessageDispatch();
const saveContact = useEndpoint('POST', '/v1/omnichannel/contact');
const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search');
const getUserData = useEndpoint('GET', '/v1/users.info');
const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields');
const { data: customFieldsMetadata = {}, isLoading: isLoadingCustomFields } = useQuery(['contact-json-custom-fields'], async () => {
const rawFields = await getCustomFields();
return formatCustomFieldsMetadata(rawFields?.customFields);
});
const initialValue = getInitialValues(data);
const { username: initialUsername } = initialValue;
const {
register,
formState: { errors, isValid: isFormValid, isDirty },
control,
setValue,
getValues,
handleSubmit,
trigger,
} = useForm<ContactFormData>({
mode: 'onSubmit',
reValidateMode: 'onSubmit',
defaultValues: initialValue,
});
const {
field: { onChange: handleLivechatData, value: customFields },
} = useController({
name: 'customFields',
control,
});
const [customFieldsErrors, setCustomFieldsErrors] = useState([]);
const isValid = isDirty && isFormValid && customFieldsErrors.length === 0;
useEffect(() => {
if (!initialUsername) {
return;
}
getUserData({ username: initialUsername }).then(({ user }) => {
setUserId(user._id);
});
}, [getUserData, initialUsername]);
const isEmailValid = async (email: string): Promise<boolean | string> => {
if (email === initialValue.email) {
return true;
}
if (!validateEmail(email)) {
return t('error-invalid-email-address');
}
const { contact } = await getContactBy({ email });
return !contact || contact._id === id || t('Email_already_exists');
};
const isPhoneValid = async (phone: string): Promise<boolean | string> => {
if (!phone || initialValue.phone === phone) {
return true;
}
const { contact } = await getContactBy({ phone });
return !contact || contact._id === id || t('Phone_already_exists');
};
const isNameValid = (v: string): string | boolean => (!v.trim() ? t('The_field_is_required', t('Name')) : true);
const handleContactManagerChange = async (userId: string): Promise<void> => {
setUserId(userId);
if (userId === 'no-agent-selected') {
setValue('username', '');
return;
}
const { user } = await getUserData({ userId });
setValue('username', user.username || '');
};
const validate = (fieldName: keyof ContactFormData): (() => void) => debounce(() => trigger(fieldName), 500);
const handleSave = async (): Promise<void> => {
const { name, phone, email, customFields, username, token } = getValues();
const payload = {
name,
phone,
email,
customFields,
token: token || createToken(),
...(username && { contactManager: { username } }),
...(id && { _id: id }),
};
try {
await saveContact(payload);
dispatchToastMessage({ type: 'success', message: t('Saved') });
close();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
if (isLoadingCustomFields) {
return <FormSkeleton />;
}
return (
<>
<VerticalBar.ScrollableContent is='form'>
<Field>
<Field.Label>{t('Name')}*</Field.Label>
<Field.Row>
<TextInput {...register('name', { validate: isNameValid })} error={errors.name?.message} flexGrow={1} />
</Field.Row>
<Field.Error>{errors.name?.message}</Field.Error>
</Field>
<Field>
<Field.Label>{t('Email')}</Field.Label>
<Field.Row>
<TextInput
{...register('email', { validate: isEmailValid, onChange: validate('email') })}
error={errors.email?.message}
flexGrow={1}
/>
</Field.Row>
<Field.Error>{errors.email?.message}</Field.Error>
</Field>
<Field>
<Field.Label>{t('Phone')}</Field.Label>
<Field.Row>
<TextInput
{...register('phone', { validate: isPhoneValid, onChange: validate('phone') })}
error={errors.phone?.message}
flexGrow={1}
/>
</Field.Row>
<Field.Error>{errors.phone?.message}</Field.Error>
</Field>
{canViewCustomFields() && customFields && (
<CustomFieldsForm
jsonCustomFields={customFieldsMetadata}
customFieldsData={customFields}
setCustomFieldsData={handleLivechatData}
setCustomFieldsError={setCustomFieldsErrors}
/>
)}
{ContactManager && <ContactManager value={userId} handler={handleContactManagerChange} />}
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup stretch>
<Button flexGrow={1} onClick={close}>
{t('Cancel')}
</Button>
<Button mie='none' type='submit' onClick={handleSubmit(handleSave)} flexGrow={1} disabled={!isValid} primary>
{t('Save')}
</Button>
</ButtonGroup>
</VerticalBar.Footer>
</>
);
};
export default ContactNewEdit;

@ -0,0 +1,226 @@
import faker from '@faker-js/faker';
import { OmnichannelContacts } from './page-objects/omnichannel-contacts-list';
import { OmnichannelSection } from './page-objects/omnichannel-section';
import { test, expect } from './utils/test';
import { createToken } from '../../client/lib/utils/createToken';
const createContact = (generateToken = false) => ({
id: null,
name: `${faker.name.firstName()} ${faker.name.lastName()}`,
email: faker.internet.email().toLowerCase(),
phone: faker.phone.phoneNumber('+############'),
token: generateToken ? createToken() : null,
customFields: {},
});
const NEW_CONTACT = createContact();
const EDIT_CONTACT = createContact();
const EXISTING_CONTACT = createContact(true);
const URL = {
contactCenter: '/omnichannel-directory/contacts',
newContact: '/omnichannel-directory/contacts/new',
get editContact() {
return `${this.contactCenter}/edit/${NEW_CONTACT.id}`;
},
get contactInfo() {
return `${this.contactCenter}/info/${NEW_CONTACT.id}`;
},
};
const ERROR = {
nameRequired: 'The field Name is required.',
invalidEmail: 'Invalid email address',
existingEmail: 'Email already exists',
existingPhone: 'Phone already exists',
};
test.use({ storageState: 'admin-session.json' });
test.describe('Omnichannel Contact Center', () => {
let poContacts: OmnichannelContacts;
let poOmniSection: OmnichannelSection;
test.beforeAll(async ({ api }) => {
// Add a contact
const { id: _, ...data } = EXISTING_CONTACT;
await api.post('/omnichannel/contact', data);
});
test.afterAll(async ({ api }) => {
// Remove added contacts
await api.delete('/livechat/visitor', { token: EXISTING_CONTACT.token });
await api.delete('/livechat/visitor', { token: NEW_CONTACT.token });
});
test.beforeEach(async ({ page }) => {
poContacts = new OmnichannelContacts(page);
poOmniSection = new OmnichannelSection(page);
});
test.afterEach(async ({ api }) => {
await api
.get('/omnichannel/contact.search', { phone: NEW_CONTACT.phone })
.then((res) => res.json())
.then((res) => {
NEW_CONTACT.token = res.contact?.token;
NEW_CONTACT.id = res.contact?._id;
});
});
test.beforeEach(async ({ page }) => {
await page.goto('/');
await poOmniSection.btnContactCenter.click();
await page.waitForURL(URL.contactCenter);
});
test('Add new contact', async ({ page }) => {
await test.step('cancel button', async () => {
await poContacts.btnNewContact.click();
await page.waitForURL(URL.newContact);
await expect(poContacts.newContact.newContactTitle).toBeVisible();
await poContacts.newContact.btnCancel.click();
await page.waitForURL(URL.contactCenter);
await expect(poContacts.newContact.newContactTitle).not.toBeVisible();
});
await test.step('open contextual bar', async () => {
await poContacts.btnNewContact.click();
await page.waitForURL(URL.newContact);
await expect(poContacts.newContact.newContactTitle).toBeVisible();
await expect(poContacts.newContact.btnSave).toBeDisabled();
});
await test.step('input name', async () => {
await poContacts.newContact.inputName.type(NEW_CONTACT.name);
});
await test.step('validate email format', async () => {
await poContacts.newContact.inputEmail.type('invalidemail');
await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).toBeVisible();
});
await test.step('validate existing email', async () => {
await poContacts.newContact.inputEmail.selectText();
await poContacts.newContact.inputEmail.type(EXISTING_CONTACT.email);
await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).toBeVisible();
await expect(poContacts.newContact.btnSave).toBeDisabled();
});
await test.step('input email', async () => {
await poContacts.newContact.inputEmail.selectText();
await poContacts.newContact.inputEmail.type(NEW_CONTACT.email);
await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).not.toBeVisible();
await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).not.toBeVisible();
});
await test.step('validate existing phone ', async () => {
await poContacts.newContact.inputPhone.type(EXISTING_CONTACT.phone);
await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).toBeVisible();
await expect(poContacts.newContact.btnSave).toBeDisabled();
});
await test.step('input phone ', async () => {
await poContacts.newContact.inputPhone.selectText();
await poContacts.newContact.inputPhone.type(NEW_CONTACT.phone);
await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).not.toBeVisible();
});
await test.step('save new contact ', async () => {
await expect(poContacts.newContact.btnSave).toBeEnabled();
await poContacts.newContact.btnSave.click();
await page.waitForURL(URL.contactCenter);
await expect(poContacts.toastSuccess).toBeVisible();
await poContacts.inputSearch.type(NEW_CONTACT.name);
await expect(poContacts.findRowByName(NEW_CONTACT.name)).toBeVisible();
});
});
test('Edit new contact', async ({ page }) => {
await test.step('search contact and open contextual bar', async () => {
await poContacts.inputSearch.type(NEW_CONTACT.name);
const row = poContacts.findRowByName(NEW_CONTACT.name);
await expect(row).toBeVisible();
await row.click();
await page.waitForURL(URL.contactInfo);
});
await test.step('cancel button', async () => {
await poContacts.contactInfo.btnEdit.click();
await page.waitForURL(URL.editContact);
await poContacts.contactInfo.btnCancel.click();
await page.waitForURL(URL.contactInfo);
});
await test.step('edit contact', async () => {
poContacts.contactInfo.btnEdit.click();
await page.waitForURL(URL.editContact);
});
await test.step('initial values', async () => {
await expect(poContacts.contactInfo.inputName).toHaveValue(NEW_CONTACT.name);
await expect(poContacts.contactInfo.inputEmail).toHaveValue(NEW_CONTACT.email);
await expect(poContacts.contactInfo.inputPhone).toHaveValue(NEW_CONTACT.phone);
});
await test.step('validate email format', async () => {
await poContacts.contactInfo.inputEmail.selectText();
await poContacts.contactInfo.inputEmail.type('invalidemail');
await expect(poContacts.contactInfo.errorMessage(ERROR.invalidEmail)).toBeVisible();
});
await test.step('validate existing email', async () => {
await poContacts.contactInfo.inputEmail.selectText();
await poContacts.contactInfo.inputEmail.type(EXISTING_CONTACT.email);
await expect(poContacts.contactInfo.errorMessage(ERROR.existingEmail)).toBeVisible();
await expect(poContacts.contactInfo.btnSave).toBeDisabled();
});
await test.step('input email', async () => {
await poContacts.contactInfo.inputEmail.selectText();
await poContacts.contactInfo.inputEmail.type(EDIT_CONTACT.email);
await expect(poContacts.contactInfo.errorMessage(ERROR.invalidEmail)).not.toBeVisible();
await expect(poContacts.contactInfo.errorMessage(ERROR.existingEmail)).not.toBeVisible();
await expect(poContacts.contactInfo.btnSave).toBeEnabled();
});
await test.step('validate existing phone ', async () => {
await poContacts.contactInfo.inputPhone.selectText();
await poContacts.contactInfo.inputPhone.type(EXISTING_CONTACT.phone);
await expect(poContacts.contactInfo.errorMessage(ERROR.existingPhone)).toBeVisible();
await expect(poContacts.contactInfo.btnSave).toBeDisabled();
});
await test.step('input phone ', async () => {
await poContacts.contactInfo.inputPhone.selectText();
await poContacts.contactInfo.inputPhone.type(EDIT_CONTACT.phone);
await expect(poContacts.contactInfo.errorMessage(ERROR.existingPhone)).not.toBeVisible();
await expect(poContacts.contactInfo.btnSave).toBeEnabled();
});
await test.step('validate name is required', async () => {
await poContacts.contactInfo.inputName.selectText();
await poContacts.contactInfo.inputName.type(' ');
await expect(poContacts.contactInfo.btnSave).toBeEnabled();
await poContacts.contactInfo.btnSave.click();
await expect(poContacts.contactInfo.errorMessage(ERROR.nameRequired)).toBeVisible();
});
await test.step('edit name', async () => {
await poContacts.contactInfo.inputName.selectText();
await poContacts.contactInfo.inputName.type(EDIT_CONTACT.name);
});
await test.step('save new contact ', async () => {
await poContacts.contactInfo.btnSave.click();
await expect(poContacts.toastSuccess).toBeVisible();
await poContacts.inputSearch.selectText();
await poContacts.inputSearch.type(EDIT_CONTACT.name);
await expect(poContacts.findRowByName(EDIT_CONTACT.name)).toBeVisible();
});
});
});

@ -0,0 +1,34 @@
import type { Locator, Page } from '@playwright/test';
import { OmnichannelContactInfo } from './omnichannel-info';
import { OmnichannelManageContact } from './omnichannel-manage-contact';
export class OmnichannelContacts {
private readonly page: Page;
readonly newContact: OmnichannelManageContact;
readonly contactInfo: OmnichannelContactInfo;
constructor(page: Page) {
this.page = page;
this.newContact = new OmnichannelManageContact(page);
this.contactInfo = new OmnichannelContactInfo(page);
}
get btnNewContact(): Locator {
return this.page.locator('button >> text="New Contact"');
}
get inputSearch(): Locator {
return this.page.locator('input[placeholder="Search"]');
}
findRowByName(contactName: string) {
return this.page.locator(`td >> text="${contactName}"`);
}
get toastSuccess(): Locator {
return this.page.locator('.rcx-toastbar.rcx-toastbar--success');
}
}

@ -0,0 +1,13 @@
import type { Locator } from '@playwright/test';
import { OmnichannelManageContact } from './omnichannel-manage-contact';
export class OmnichannelContactInfo extends OmnichannelManageContact {
get btnEdit(): Locator {
return this.page.locator('role=button[name="Edit"]');
}
get btnCall(): Locator {
return this.page.locator('role=button[name=Call"]');
}
}

@ -0,0 +1,45 @@
import type { Locator, Page } from '@playwright/test';
export class OmnichannelManageContact {
protected readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get newContactTitle(): Locator {
return this.page.locator('h3 >> text="New Contact"');
}
get editContactTitle(): Locator {
return this.page.locator('h3 >> text="Edit Contact Profile"');
}
get inputName(): Locator {
return this.page.locator('input[name=name]');
}
get inputEmail(): Locator {
return this.page.locator('input[name=email]');
}
get inputPhone(): Locator {
return this.page.locator('input[name=phone]');
}
get inputContactManager(): Locator {
return this.page.locator('input[name=contactManager]');
}
get btnSave(): Locator {
return this.page.locator('button >> text="Save"');
}
get btnCancel(): Locator {
return this.page.locator('button >> text="Cancel"');
}
errorMessage(message: string): Locator {
return this.page.locator(`.rcx-field__error >> text="${message}"`);
}
}

@ -0,0 +1,25 @@
import type { Locator, Page } from '@playwright/test';
export class OmnichannelSection {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get element(): Locator {
return this.page.locator('div[data-qa-id="omncSection"]');
}
get btnVoipToggle(): Locator {
return this.page.locator('role=button[name="Enable/Disable VoIP"]');
}
get btnDialpad(): Locator {
return this.page.locator('role=button[name="Open Dialpad"]');
}
get btnContactCenter(): Locator {
return this.page.locator('role=button[name="Contact Center"]');
}
}

@ -13,10 +13,10 @@ export type AnyObj = { [key: string]: any };
export type BaseTest = {
api: {
get(uri: string, prefix?: string): Promise<APIResponse>;
get(uri: string, params?: AnyObj, prefix?: string): Promise<APIResponse>;
post(uri: string, data: AnyObj, prefix?: string): Promise<APIResponse>;
put(uri: string, data: AnyObj, prefix?: string): Promise<APIResponse>;
delete(uri: string, prefix?: string): Promise<APIResponse>;
delete(uri: string, params?: AnyObj, prefix?: string): Promise<APIResponse>;
};
};
@ -61,8 +61,8 @@ export const test = baseTest.extend<BaseTest>({
};
await use({
get(uri: string, prefix = API_PREFIX) {
return request.get(BASE_URL + prefix + uri, { headers });
get(uri: string, params?: AnyObj, prefix = API_PREFIX) {
return request.get(BASE_URL + prefix + uri, { headers, params });
},
post(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.post(BASE_URL + prefix + uri, { headers, data });
@ -70,8 +70,8 @@ export const test = baseTest.extend<BaseTest>({
put(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.put(BASE_URL + prefix + uri, { headers, data });
},
delete(uri: string, prefix = API_PREFIX) {
return request.delete(BASE_URL + prefix + uri, { headers });
delete(uri: string, params?: AnyObj, prefix = API_PREFIX) {
return request.delete(BASE_URL + prefix + uri, { headers, params });
},
});
},

Loading…
Cancel
Save