[FIX] Adjusted form validation to disallow duplicated emails (#27037)
parent
91844b220a
commit
462b774b27
@ -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"]'); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue