diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Experience/DomainsInput/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Experience/DomainsInput/index.tsx index b15f853f2..4b63ebde5 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Experience/DomainsInput/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Experience/DomainsInput/index.tsx @@ -32,12 +32,14 @@ function DomainsInput({ className, values, onChange: rawOnChange, error, placeho const [focusedValueId, setFocusedValueId] = useState>(null); const [currentValue, setCurrentValue] = useState(''); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { setError } = useFormContext(); + const { setError, clearErrors } = useFormContext(); const onChange = (values: Option[]) => { const { values: parsedValues, errorMessage } = domainOptionsParser(values); if (errorMessage) { setError('domains', { type: 'custom', message: errorMessage }); + } else { + clearErrors('domains'); } rawOnChange(parsedValues); }; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx index fd64cb8e0..27b929311 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Experience/index.tsx @@ -97,7 +97,6 @@ function Experience({ data, isDeleted, onUpdated, isDarkModeEnabled }: Props) { watch, setValue, setError, - clearErrors, handleSubmit, register, formState: { defaultValues, isDirty, isSubmitting, errors }, diff --git a/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts b/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts index 12a3c353f..09376a838 100644 --- a/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts +++ b/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts @@ -4,14 +4,24 @@ import { expectModalWithTitle, expectToClickDetailsPageOption, expectConfirmModalAndAct, + expectToSaveChanges, + waitForToast, } from '#src/ui-helpers/index.js'; -import { expectNavigation, appendPathname } from '#src/utils.js'; +import { expectNavigation, appendPathname, dcls, cls } from '#src/utils.js'; import { findModalFooterButton, fillSsoConnectorCreationModal } from './helpers.js'; import { ssoConnectorTestCases } from './sso-connectors-test-cases.js'; await page.setViewport({ width: 1920, height: 1080 }); +const emailDomainInputFieldSelector = [ + 'form', + `${dcls('input')}${cls('multiple')}[role=button]`, + 'input', +].join(' '); + +const emailDomainErrorMessageSelector = ['form', dcls('field'), dcls('errorMessage')].join(' '); + describe('create SSO connectors', () => { const logtoConsoleUrl = new URL(logtoConsoleUrlString); @@ -26,7 +36,7 @@ describe('create SSO connectors', () => { ); await expect(page).toMatchElement( - 'div[class$=main] div[class$=headline] div[class$=titleEllipsis]', + [dcls('main'), dcls('headline'), dcls('titleEllipsis')].join(' '), { text: 'Enterprise SSO', } @@ -37,7 +47,7 @@ describe('create SSO connectors', () => { it('can open create SSO connector modal from table placeholder and create the first SSO connector', async () => { // When no SSO connector is created, use the create button in placeholder. - await expect(page).toClick('table div[class$=placeholder] button span', { + await expect(page).toClick(['table', dcls('placeholder'), 'button', 'span'].join(' '), { text: 'Add enterprise connector', }); @@ -57,9 +67,12 @@ describe('create SSO connectors', () => { await page.waitForNavigation({ waitUntil: 'networkidle0' }); // When there are existing SSO connector(s), use the create button in page header. - await expect(page).toClick('div[class$=main] div[class$=headline] button[type=button] span', { - text: 'Add enterprise connector', - }); + await expect(page).toClick( + [dcls('main'), dcls('headline'), 'button[type=button]', 'span'].join(' '), + { + text: 'Add enterprise connector', + } + ); await expectModalWithTitle(page, 'Add enterprise connector'); @@ -76,9 +89,12 @@ describe('create SSO connectors', () => { await page.waitForNavigation({ waitUntil: 'networkidle0' }); // When there are existing SSO connector(s), use the create button in page header. - await expect(page).toClick('div[class$=main] div[class$=headline] button[type=button] span', { - text: 'Add enterprise connector', - }); + await expect(page).toClick( + [dcls('main'), dcls('headline'), 'button[type=button]', 'span'].join(' '), + { + text: 'Add enterprise connector', + } + ); await expectModalWithTitle(page, 'Add enterprise connector'); @@ -95,7 +111,7 @@ describe('create SSO connectors', () => { // Error message should be shown. await expect(page).toMatchElement( - '.ReactModalPortal div[class$=field] div[class$=errorMessage]', + ['.ReactModalPortal', dcls('field'), dcls('errorMessage')].join(' '), { text: 'Connector name already exists. Please choose a different name.', } @@ -104,7 +120,7 @@ describe('create SSO connectors', () => { await expect(findModalFooterButton(true)).resolves.toBeTruthy(); await expect(page).toFill( - '.ReactModalPortal input[type=text][name=connectorName]', + ['.ReactModalPortal', 'input[type=text][name=connectorName]'].join(' '), `${connectorName} (1)` ); @@ -125,6 +141,108 @@ describe('create SSO connectors', () => { expect(page.url().endsWith('/connection')).toBeTruthy(); }); + it("can go to SSO connector's 'SSO Experience' tab", async () => { + // Navigate to "SSO Experience" tab + await expect(page).toClick(['nav', dcls('item'), dcls('link'), 'a'].join(' '), { + text: 'SSO Experience', + }); + + // Confirm the current path is for "SSO Experience". + expect(page.url().endsWith('/experience')).toBeTruthy(); + // Confirm the current tab is "SSO Experience". + await expect(page).toMatchElement(['nav', dcls('item'), dcls('selected'), 'a'].join(' '), { + text: 'SSO Experience', + }); + }); + + it("can configure SSO connectors's 'SSO Experience' GENERAL setup", async () => { + // Expect to see inline notification alert to configure email domain. + await expect(page).toMatchElement( + ['form', `${dcls('alert')}${cls('inlineNotification')}`, dcls('content')].join(' '), + { + text: 'Add email domain to guide enterprise users to their identity provider for Single Sign-on.', + } + ); + + // Configure email domain + await expect(page).toFill(emailDomainInputFieldSelector, 'svhd.io'); + + // Press enter to add email domain + await page.keyboard.press('Enter'); + + // Input email domain with invalid format + await expect(page).toFill(emailDomainInputFieldSelector, 'abc'); + + // Press space to add email domain + await page.keyboard.press('Space'); + await expect(page).toMatchElement(emailDomainErrorMessageSelector, { + text: 'Invalid domain format.', + }); + + // Input public email domain (e.g., 'gmail.com', 'yahoo.com' etc) + await expect(page).toFill(emailDomainInputFieldSelector, 'gmail.com'); + + // Press tab to add email domain + await page.keyboard.press('Tab'); + await expect(page).toMatchElement(emailDomainErrorMessageSelector, { + text: 'Public email domains are not allowed. Invalid domain format.', + }); + + // Remove last added email domain (which is `gmail.com` in this case). CSS selector can not specify the text content of the component. + await expect(page).toClick( + [ + 'form', + `${dcls('input')}${cls('multiple')}[role=button]`, + `${dcls('info')}${cls('tag')}:last-of-type`, + 'button', + ].join(' ') + ); + + // Error message got updated, since forbidden email domain is removed. + await expect(page).toMatchElement(emailDomainErrorMessageSelector, { + text: 'Invalid domain format.', + }); + await expect(page).toFill(emailDomainInputFieldSelector, 'abc'); + + // Input field blurred to input email domain. + await page.$eval( + // Can not use `emailDomainInputFieldSelector` here since it could break the type inference. + 'form div[class*=input][class*=multiple][role=button] input', + (element: HTMLInputElement) => { + element.blur(); + } + ); + + // Does not allow duplicate email domain. + await expect(page).toMatchElement(emailDomainErrorMessageSelector, { + text: 'There are duplicate domains. Invalid domain format.', + }); + + // Remove last two added email domains (which are two `abc` in this case). + await expect(page).toClick( + [ + 'form', + `${dcls('input')}${cls('multiple')}[role=button]`, + `${dcls('info')}${cls('tag')}:last-of-type`, + 'button', + ].join(' ') + ); + + // Focus on email domain input field component (at this time, the input field is empty). + const inputField = await page.$(emailDomainInputFieldSelector); + await inputField?.focus(); + // Should remove the last input email domain with double backspace when the input box is empty. + await inputField?.press('Backspace'); + await inputField?.press('Backspace'); + + // Since incorrect email domains are removed, error message no longer exists. + const errorMessage = await page.$(emailDomainErrorMessageSelector); + expect(errorMessage).toBeNull(); + + await expectToSaveChanges(page); + await waitForToast(page, { text: 'Saved' }); + }); + it('can delete an SSO connector from details page', async () => { // Delete connector await expectToClickDetailsPageOption(page, 'Delete');