From 462f677cdc8dc0e303206108243619207e9e8826 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Fri, 18 Aug 2023 16:46:00 +0800 Subject: [PATCH] test: add ui tests for sign-up and sign-in settings (#4373) --- .../src/tests/ui/sign-in-experience.test.ts | 138 ----- .../ui/sign-in-experience/branding.test.ts | 100 +++ .../tests/ui/sign-in-experience/helpers.ts | 58 ++ .../connector-setup-helpers.ts | 161 +++++ .../sign-up-and-sign-in/happy-path.test.ts | 577 ++++++++++++++++++ .../sign-up-and-sign-in/helpers.ts | 240 ++++++++ .../sign-up-and-sign-in/sad-path.test.ts | 301 +++++++++ .../integration-tests/src/ui-helpers/index.ts | 6 + 8 files changed, 1443 insertions(+), 138 deletions(-) delete mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience.test.ts create mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience/branding.test.ts create mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience/helpers.ts create mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/connector-setup-helpers.ts create mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts create mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/helpers.ts create mode 100644 packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience.test.ts b/packages/integration-tests/src/tests/ui/sign-in-experience.test.ts deleted file mode 100644 index 746f5b588..000000000 --- a/packages/integration-tests/src/tests/ui/sign-in-experience.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; -import { goToAdminConsole, trySaveChanges, waitForToaster } from '#src/ui-helpers/index.js'; -import { appendPathname, expectNavigation } from '#src/utils.js'; - -await page.setViewport({ width: 1920, height: 1080 }); - -const defaultPrimaryColor = '#6139F6'; -const testPrimaryColor = '#5B4D8E'; - -describe('sign-in experience', () => { - const logtoConsoleUrl = new URL(logtoConsoleUrlString); - - beforeAll(async () => { - await goToAdminConsole(); - }); - - it('navigate to sign-in experience page', async () => { - await expectNavigation( - page.goto(appendPathname('/console/sign-in-experience', logtoConsoleUrl).href) - ); - - await expect(page).toMatchElement( - 'div[class$=main] div[class$=container] div[class$=cardTitle] div[class$=titleEllipsis]', - { - text: 'Sign-in experience', - } - ); - - // Start & finish guide - await expect(page).toClick('div[class$=container] div[class$=content] button span', { - text: 'Get Started', - }); - - await expect(page).toClick( - 'div[class$=ReactModalPortal] div[class$=footerContent] > button span', - { - text: 'Done', - } - ); - - // Land on branding tab by default - expect(page.url()).toBe(new URL(`console/sign-in-experience/branding`, logtoConsoleUrl).href); - - // Wait for the branding tab to load - await expect(page).toMatchElement('div[class$=tabContent] div[class$=card] div[class$=title]', { - text: 'BRANDING AREA', - }); - - await expect(page).toMatchElement('div[class$=tabContent] div[class$=card] div[class$=title]', { - text: 'Custom CSS', - }); - }); - - describe('update branding config', () => { - it('update branding config', async () => { - // Enabled dark mode - await expect(page).toClick( - 'form div[class$=field] label[class$=switch]:has(input[name="color.isDarkModeEnabled"])' - ); - - // Update brand color - const brandColorField = await expect(page).toMatchElement( - 'div[class$=field]:has(div[class$=headline] div[class$=title])', - { - text: 'Brand color', - } - ); - - await expect(brandColorField).toClick('div[role=button]'); - - await expect(page).toFill('input[id^=rc-editable-input]', testPrimaryColor); - - // Close the color input - await page.keyboard.press('Escape'); - - // Recalculate dark brand color - await expect(page).toClick('div[class$=darkModeTip] button span', { text: 'Recalculate' }); - - // Wait for the recalculate to finish - await page.waitForTimeout(500); - - // Fill in the custom CSS - await expect(page).toFill( - 'div[class$=editor] textarea', - 'body { background-color: #5B4D8E; }' - ); - - await trySaveChanges(page); - - await waitForToaster(page, { - text: 'Saved', - }); - }); - - it('reset branding config', async () => { - // Reset branding config - const brandColorField = await expect(page).toMatchElement( - 'div[class$=field]:has(div[class$=headline] div[class$=title])', - { - text: 'Brand color', - } - ); - - await expect(brandColorField).toClick('div[role=button]'); - - await expect(page).toFill('input[id^=rc-editable-input]', defaultPrimaryColor); - - // Close the color input - await page.keyboard.press('Escape'); - - // Recalculate dark brand color - await expect(page).toClick('div[class$=darkModeTip] button span', { text: 'Recalculate' }); - - // Wait for the recalculate to finish - await page.waitForTimeout(500); - - // Fill in the custom CSS - await expect(page).toFill('div[class$=editor] textarea', ''); - - await trySaveChanges(page); - - await waitForToaster(page, { - text: 'Saved', - }); - - // Disable dark mode - await expect(page).toClick( - 'form div[class$=field] label[class$=switch]:has(input[name="color.isDarkModeEnabled"])' - ); - - await trySaveChanges(page); - - await waitForToaster(page, { - text: 'Saved', - }); - }); - }); -}); diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/branding.test.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/branding.test.ts new file mode 100644 index 000000000..beaa09fb7 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/branding.test.ts @@ -0,0 +1,100 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { goToAdminConsole } from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { waitForFormCard, expectToSelectColor, expectToSaveSignInExperience } from './helpers.js'; + +const defaultPrimaryColor = '#6139F6'; +const testPrimaryColor = '#5B4D8E'; + +await page.setViewport({ width: 1920, height: 1080 }); + +describe('sign-in experience: branding', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + + beforeAll(async () => { + await goToAdminConsole(); + }); + + it('navigate to sign-in experience page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/sign-in-experience', logtoConsoleUrl).href) + ); + + await expect(page).toMatchElement( + 'div[class$=main] div[class$=container] div[class$=cardTitle] div[class$=titleEllipsis]', + { + text: 'Sign-in experience', + } + ); + + // Start & finish guide + await expect(page).toClick('div[class$=container] div[class$=content] button span', { + text: 'Get Started', + }); + + await expect(page).toClick( + 'div[class$=ReactModalPortal] div[class$=footerContent] > button span', + { + text: 'Done', + } + ); + + // Land on branding tab by default + expect(page.url()).toBe(new URL(`console/sign-in-experience/branding`, logtoConsoleUrl).href); + + // Wait for the branding tab to load + await waitForFormCard(page, 'BRANDING AREA'); + await waitForFormCard(page, 'Custom CSS'); + }); + + it('update branding config', async () => { + // Enabled dark mode + await expect(page).toClick( + 'form div[class$=field] label[class$=switch]:has(input[name="color.isDarkModeEnabled"])' + ); + + // Update brand color + await expectToSelectColor(page, { + field: 'Brand color', + color: testPrimaryColor, + }); + + // Recalculate dark brand color + await expect(page).toClick('div[class$=darkModeTip] button span', { text: 'Recalculate' }); + + // Wait for the recalculate to finish + await page.waitForTimeout(500); + + // Fill in the custom CSS + await expect(page).toFill('div[class$=editor] textarea', 'body { background-color: #5B4D8E; }'); + + await expectToSaveSignInExperience(page); + }); + + it('reset branding config', async () => { + // Reset branding config + await expectToSelectColor(page, { + field: 'Brand color', + color: defaultPrimaryColor, + }); + + // Recalculate dark brand color + await expect(page).toClick('div[class$=darkModeTip] button span', { text: 'Recalculate' }); + + // Wait for the recalculate to finish + await page.waitForTimeout(500); + + // Fill in the custom CSS + await expect(page).toFill('div[class$=editor] textarea', ''); + + await expectToSaveSignInExperience(page); + + // Disable dark mode + await expect(page).toClick( + 'form div[class$=field] label[class$=switch]:has(input[name="color.isDarkModeEnabled"])' + ); + + await expectToSaveSignInExperience(page); + }); +}); diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/helpers.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/helpers.ts new file mode 100644 index 000000000..0c1e056e9 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/helpers.ts @@ -0,0 +1,58 @@ +import { type Page } from 'puppeteer'; + +import { trySaveChanges, expectConfirmModalAndAct, waitForToaster } from '#src/ui-helpers/index.js'; + +export const waitForFormCard = async (page: Page, title: string) => { + await expect(page).toMatchElement('div[class$=tabContent] div[class$=card] div[class$=title]', { + text: title, + }); +}; + +type ExpectToSelectColorOptions = { + field: string; + color: string; +}; + +export const expectToSelectColor = async ( + page: Page, + { field, color }: ExpectToSelectColorOptions +) => { + const colorField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] div[class$=title])', + { + text: field, + } + ); + + await expect(colorField).toClick('div[role=button]'); + + await expect(page).toFill('input[id^=rc-editable-input]', color); + + // Close the color input + await page.keyboard.press('Escape'); +}; + +type ExpectToSaveSignInExperienceOptions = { + needToConfirmChanges?: boolean; +}; + +export const expectToSaveSignInExperience = async ( + page: Page, + options?: ExpectToSaveSignInExperienceOptions +) => { + const { needToConfirmChanges = false } = options ?? {}; + + await trySaveChanges(page); + + if (needToConfirmChanges) { + // Confirm changes + await expectConfirmModalAndAct(page, { + title: 'Reminder', + actionText: 'Confirm', + }); + } + + await waitForToaster(page, { + text: 'Saved', + }); +}; diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/connector-setup-helpers.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/connector-setup-helpers.ts new file mode 100644 index 000000000..4a461ae34 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/connector-setup-helpers.ts @@ -0,0 +1,161 @@ +import { ConnectorType } from '@logto/schemas'; +import { type Page } from 'puppeteer'; + +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { expectToClickDetailsPageOption, waitForToaster } from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { + expectToConfirmConnectorDeletion, + expectToSelectConnector, + waitForConnectorCreationGuide, +} from '../../connectors/helpers.js'; + +const logtoConsoleUrl = new URL(logtoConsoleUrlString); + +type TestConnector = { + factoryId: string; + name: string; + connectorType: ConnectorType; + data: Record; +}; + +export const testSendgridConnector: TestConnector = { + factoryId: 'sendgrid-email-service', + name: 'SendGrid Email', + connectorType: ConnectorType.Email, + data: { + 'formConfig.apiKey': 'api-key', + 'formConfig.fromEmail': 'foo@example.com', + 'formConfig.fromName': 'Logto', + }, +}; + +export const testTwilioConnector: TestConnector = { + factoryId: 'twilio-short-message-service', + name: 'Twilio SMS Service', + connectorType: ConnectorType.Sms, + data: { + 'formConfig.accountSID': 'account-sid', + 'formConfig.authToken': 'auth-token', + 'formConfig.fromMessagingServiceSID': 'from-messaging-service-sid', + }, +}; + +export const testAppleConnector: TestConnector = { + factoryId: 'apple-universal', + name: 'Apple', + connectorType: ConnectorType.Social, + data: { + 'formConfig.clientId': 'client-id', + }, +}; + +export const expectToSetupPasswordlessConnector = async ( + page: Page, + { factoryId, name, connectorType, data }: TestConnector +) => { + if (connectorType === ConnectorType.Social) { + return; + } + + await expectNavigation( + page.goto(appendPathname('/console/connectors/passwordless', logtoConsoleUrl).href) + ); + + const connectorItem = await expect(page).toMatchElement( + 'div[class$=item] div[class$=previewTitle]:has(>div)', + { + text: connectorType === ConnectorType.Email ? 'Email connector' : 'SMS connector', + } + ); + + const setupConnectorButton = await expect(connectorItem).toMatchElement('button span', { + text: 'Set Up', + }); + + await setupConnectorButton.click(); + + await setupConnectorButton.click(); + + await expectToSelectConnector(page, { + factoryId, + connectorType, + }); + + await waitForConnectorCreationGuide(page, name); + await expect(page).toFillForm('.ReactModalPortal form', data); + + await expect(page).toClick('.ReactModalPortal form div[class$=footer] button[type=submit] span', { + text: 'Save and Done', + }); + + await waitForToaster(page, { text: 'Saved' }); +}; + +export const expectToSetupSocialConnector = async ( + page: Page, + { factoryId, name, connectorType, data }: TestConnector +) => { + if (connectorType !== ConnectorType.Social) { + return; + } + + await expectNavigation( + page.goto(appendPathname('/console/connectors/social', logtoConsoleUrl).href) + ); + + await expect(page).toClick('div[class$=headline] button[class$=withIcon] span', { + text: 'Add Social Connector', + }); + + await expectToSelectConnector(page, { + factoryId, + connectorType, + }); + + await waitForConnectorCreationGuide(page, name); + await expect(page).toFillForm('.ReactModalPortal form', data); + + await expect(page).toClick('.ReactModalPortal form div[class$=footer] button[type=submit] span', { + text: 'Save and Done', + }); + + await waitForToaster(page, { text: 'Saved' }); +}; + +export const expectToDeletePasswordlessConnector = async (page: Page, { name }: TestConnector) => { + await expectNavigation( + page.goto(appendPathname('/console/connectors/passwordless', logtoConsoleUrl).href) + ); + + await expect(page).toClick('table tbody tr td div[class$=item] a[class$=title] span', { + text: name, + }); + + await expect(page).toMatchElement('div[class$=header] div[class$=name] span', { + text: name, + }); + + await expectToClickDetailsPageOption(page, 'Delete'); + + await expectToConfirmConnectorDeletion(page); +}; + +export const expectToDeleteSocialConnector = async (page: Page, { name }: TestConnector) => { + await expectNavigation( + page.goto(appendPathname('/console/connectors/social', logtoConsoleUrl).href) + ); + + await expect(page).toClick('table tbody tr td div[class$=item] a[class$=title] span', { + text: name, + }); + + await expect(page).toMatchElement('div[class$=header] div[class$=name] span', { + text: name, + }); + + await expectToClickDetailsPageOption(page, 'Delete'); + + await expectToConfirmConnectorDeletion(page); +}; diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts new file mode 100644 index 000000000..164c3e5f5 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/happy-path.test.ts @@ -0,0 +1,577 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { expectToClickNavTab, goToAdminConsole } from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js'; + +import { + expectToDeletePasswordlessConnector, + expectToSetupPasswordlessConnector, + testSendgridConnector, + testTwilioConnector, + expectToSetupSocialConnector, + testAppleConnector, + expectToDeleteSocialConnector, +} from './connector-setup-helpers.js'; +import { + expectToAddSignInMethod, + expectToAddSocialSignInConnector, + expectToClickSignInMethodAuthnOption, + expectToClickSignUpAuthnOption, + expectToRemoveSignInMethod, + expectToRemoveSocialSignInConnector, + expectToResetSignUpAndSignInConfig, + expectToSelectSignUpIdentifier, + expectToSwapSignInMethodAuthnOption, +} from './helpers.js'; + +await page.setViewport({ width: 1920, height: 1080 }); + +describe('sign-in experience(happy path): sign-up and sign-in', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + + beforeAll(async () => { + await goToAdminConsole(); + // Email connector + await expectToSetupPasswordlessConnector(page, testSendgridConnector); + // SMS connector + await expectToSetupPasswordlessConnector(page, testTwilioConnector); + // Social connector + await expectToSetupSocialConnector(page, testAppleConnector); + }); + + afterAll(async () => { + await expectToDeletePasswordlessConnector(page, testSendgridConnector); + await expectToDeletePasswordlessConnector(page, testTwilioConnector); + await expectToDeleteSocialConnector(page, testAppleConnector); + }); + + it('navigate to sign-in experience page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/sign-in-experience', logtoConsoleUrl).href) + ); + + // Land on branding tab by default + expect(page.url()).toBe(new URL(`console/sign-in-experience/branding`, logtoConsoleUrl).href); + }); + + it('navigate to sign-up and sign-in tab', async () => { + await expectToClickNavTab(page, 'Sign-up and Sign-in'); + + await waitForFormCard(page, 'SIGN UP'); + await waitForFormCard(page, 'SIGN IN'); + await waitForFormCard(page, 'SOCIAL SIGN-IN'); + }); + + describe('email as sign-up identifier (verify only)', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select email as sign-in method and disable password settings for sign-up', async () => { + await expectToSelectSignUpIdentifier(page, 'Email address'); + // Disable password settings for sign-up + await expectToClickSignUpAuthnOption(page, 'Create your password'); + // Username will be added in later tests + await expectToRemoveSignInMethod(page, 'Username'); + + /** + * Sign-in method + * - Email address: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update email sign-in method', async () => { + /** + * Sign-in method + * - Email address: verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + await expectToSwapSignInMethodAuthnOption(page, 'Email address'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('add username sign-in method', async () => { + /** + * Sign-in method + * - Email address: verification code + password + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('add & update phone number sign-in method', async () => { + await expectToAddSignInMethod(page, 'Phone number'); + /** + * Sign-in method + * - Email address: verification code + password + * - Username: password + * - Phone number: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + password + * - Username: password + * - Phone number: verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + password + * - Phone number: verification code + */ + await expectToRemoveSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + password + * - Phone number: password + verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('email as sign-up identifier (password & verify)', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select email as sign-in method and enable password settings for sign-up', async () => { + await expectToSelectSignUpIdentifier(page, 'Email address'); + // Username will be added in later tests + await expectToRemoveSignInMethod(page, 'Username'); + + /** + * Sign-in method + * - Email address: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update email sign-in method', async () => { + /** + * Sign-in method + * - Email address: verification code + password + */ + // Sign-in method: Email address + verification code + password + await expectToSwapSignInMethodAuthnOption(page, 'Email address'); + await expectToSaveSignInExperience(page); + + /** + * Sign-in method + * - Email address: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('add phone number & username as sign-in method', async () => { + /** + * Sign-in method + * - Email address: password + * - Phone number: password + verification code + */ + await expectToAddSignInMethod(page, 'Phone number'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: password + * - Phone number: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: password + * - Phone number: password + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: password + * - Phone number: password + verification code + * - Username: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('phone as sign-up identifier (verify only)', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select email as sign-in method and disable password settings for sign-up', async () => { + await expectToSelectSignUpIdentifier(page, 'Phone number'); + // Disable password settings for sign-up + await expectToClickSignUpAuthnOption(page, 'Create your password'); + // Username will be added in later tests + await expectToRemoveSignInMethod(page, 'Username'); + + /** + * Sign-in method + * - Phone number: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update sign-in methods', async () => { + /** + * Sign-in method + * - Phone number: verification code + password + */ + await expectToSwapSignInMethodAuthnOption(page, 'Phone number'); + await expectToSaveSignInExperience(page); + + /** + * Sign-in method + * - Phone number: verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Phone number: verification code + * - Email address: password + verification code + */ + await expectToAddSignInMethod(page, 'Email address'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Phone number: verification code + * - Email address: verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Phone number: verification code + * - Email address: verification code + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('phone as sign-up identifier (password & verify)', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select email as sign-in method and enable password settings for sign-up', async () => { + await expectToSelectSignUpIdentifier(page, 'Phone number'); + // Username will be added in later tests + await expectToRemoveSignInMethod(page, 'Username'); + + /** + * Sign-in method + * - Phone number: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update sign-in methods', async () => { + /** + * Sign-in method + * - Phone number: verification code + password + */ + await expectToSwapSignInMethodAuthnOption(page, 'Phone number'); + await expectToSaveSignInExperience(page); + + /** + * Sign-in method + * - Phone number: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Phone number: password + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('email or phone as sign-up identifier (verify only)', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select email or phone as sign-up identifier and disable password settings for sign-up', async () => { + await expectToSelectSignUpIdentifier(page, 'Email address or phone number'); + await expectToClickSignUpAuthnOption(page, 'Create your password'); + // Username will be added in later tests + await expectToRemoveSignInMethod(page, 'Username'); + + /** + * Sign-in method + * - Email address: password + verification code + * - Phone number: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update sign-in method configs', async () => { + /** + * Sign-in method + * - Email address: verification code + password + * - Phone number: verification code + password + */ + await expectToSwapSignInMethodAuthnOption(page, 'Email address'); + await expectToSwapSignInMethodAuthnOption(page, 'Phone number'); + await expectToSaveSignInExperience(page); + + /** + * Sign-in method + * - Email address: verification code + * - Phone number: verification code + password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + * - Phone number: verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + * - Phone number: verification code + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('email or phone as sign-up identifier (password & verify)', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select email or phone as sign-up identifier and enable password settings for sign-up', async () => { + await expectToSelectSignUpIdentifier(page, 'Email address or phone number'); + // Username will be added in later tests + await expectToRemoveSignInMethod(page, 'Username'); + + /** + * Sign-in method + * - Email address: password + verification code + * - Phone number: password + verification code + */ + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update sign-in method configs', async () => { + /** + * Sign-in method + * - Email address: verification code + password + * - Phone number: verification code + password + */ + await expectToSwapSignInMethodAuthnOption(page, 'Email address'); + await expectToSwapSignInMethodAuthnOption(page, 'Phone number'); + await expectToSaveSignInExperience(page); + + /** + * Sign-in method + * - Email address: password + * - Phone number: verification code + password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: password + * - Phone number: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: password + * - Phone number: password + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('not applicable as sign-up identifier', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('select not applicable as sign-up identifier', async () => { + await expectToSelectSignUpIdentifier(page, 'Not applicable'); + await expectToRemoveSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('update sign-in methods', async () => { + /** + * Sign-in method + * - Email address: password + verification code + */ + await expectToAddSignInMethod(page, 'Email address', false); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Verification code', + }); + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + * - Phone number: password verification code + */ + await expectToAddSignInMethod(page, 'Phone number'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + * - Phone number: password + */ + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Verification code', + }); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + /** + * Sign-in method + * - Email address: verification code + * - Phone number: password + * - Username: password + */ + await expectToAddSignInMethod(page, 'Username'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('add social sign-in connector', async () => { + await expectToAddSocialSignInConnector(page, 'Apple'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + + // Reset + await expectToRemoveSocialSignInConnector(page, 'Apple'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + }); + + describe('disable user registration', () => { + it('navigate to others tab', async () => { + await expectToClickNavTab(page, 'Others'); + + await waitForFormCard(page, 'TERMS'); + await waitForFormCard(page, 'LANGUAGES'); + await waitForFormCard(page, 'ADVANCED OPTIONS'); + }); + + it('disable user registration', async () => { + const switchSelector = 'label[class$=switch]:has(input[name=createAccountEnabled])'; + await expect(page).toClick(switchSelector); + await expectToSaveSignInExperience(page); + + // Reset + await expect(page).toClick(switchSelector); + await expectToSaveSignInExperience(page); + }); + }); +}); diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/helpers.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/helpers.ts new file mode 100644 index 000000000..a53dfc519 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/helpers.ts @@ -0,0 +1,240 @@ +import { type Page } from 'puppeteer'; + +import { expectToSaveSignInExperience } from '../helpers.js'; + +export const expectToSelectSignUpIdentifier = async (page: Page, identifier: string) => { + const signUpIdentifierField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: 'Sign-up identifier', + } + ); + + await expect(signUpIdentifierField).toClick('div[role=button][class*=select]'); + + // Wait for the dropdown to be rendered in the correct position + await page.waitForTimeout(500); + + await expect(page).toClick( + '.ReactModalPortal div[class$=dropdownContainer] div[role=menuitem] div', + { + text: identifier, + } + ); + + await page.waitForSelector('.ReactModalPortal div[class$=dropdownContainer]', { + hidden: true, + }); + + await expect(signUpIdentifierField).toMatchElement('div[class*=select] div[class$=title] div', { + text: identifier, + }); + + // Wait for the config to update + await page.waitForTimeout(500); +}; + +export const expectToClickSignUpAuthnOption = async (page: Page, option: string) => { + const signUpAuthnSettingsFiled = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: 'Authentication setting for sign-up', + } + ); + + await expect(signUpAuthnSettingsFiled).toClick('div[class$=selections] span[class$=label]', { + text: option, + }); +}; + +export const expectToAddSignInMethod = async (page: Page, method: string, isAddAnother = true) => { + const signInMethodsField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: 'Identifier and authentication settings for sign-in', + } + ); + + // Click Add another + await expect(signInMethodsField).toClick('button span', { + text: isAddAnother ? 'Add Another' : 'Add Sign-in Method', + }); + + // Wait for the dropdown to be rendered in the correct position + await page.waitForTimeout(500); + + await expect(page).toClick('.ReactModalPortal div[class$=dropdownContainer] div[role=menuitem]', { + text: method, + }); + + await page.waitForSelector('.ReactModalPortal div[class$=dropdownContainer]', { + hidden: true, + }); +}; + +type ExpectSignInMethodAuthnOptionOptions = { + method: string; + option: string; +}; + +export const expectToClickSignInMethodAuthnOption = async ( + page: Page, + { method, option }: ExpectSignInMethodAuthnOptionOptions +) => { + const methodItem = await expect(page).toMatchElement( + 'div[class$=signInMethodItem]:has(div[class$=identifier])', + { + text: method, + } + ); + + await expect(methodItem).toClick('div[class*=authentication] span[class$=label]', { + text: option, + }); + + // Wait for the config to update + await page.waitForTimeout(500); +}; + +export const expectToSwapSignInMethodAuthnOption = async (page: Page, method: string) => { + const methodItem = await expect(page).toMatchElement( + 'div[class$=signInMethodItem]:has(div[class$=identifier])', + { + text: method, + } + ); + + await expect(methodItem).toClick('div[class*=authentication] div[class$=swapButton] button'); +}; + +export const expectToRemoveSignInMethod = async (page: Page, method: string) => { + const methodItem = await expect(page).toMatchElement( + 'div[class$=signInMethodItem]:has(div[class$=identifier])', + { + text: method, + } + ); + + await expect(methodItem).toClick('div[class$=anchor] button:last-of-type'); + + // Wait for the config to update + await page.waitForTimeout(500); +}; + +export const expectSignInMethodError = async (page: Page, method: string) => { + await expect(page).toMatchElement( + 'div[class$=signInMethodItem] div[class$=error] div[class$=identifier]', + { + text: method, + } + ); +}; + +type ExpectNotificationOnFiledOptions = { + field: string; + content?: RegExp | string; +}; + +export const expectNotificationInFiled = async ( + page: Page, + { field, content }: ExpectNotificationOnFiledOptions +) => { + const signInMethodsField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: field, + } + ); + + await expect(signInMethodsField).toMatchElement( + 'div[class*=inlineNotification] div[class$=content]', + { + text: content, + } + ); +}; + +export const expectSignUpIdentifierSelectorError = async (page: Page) => { + const signUpIdentifierField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: 'Sign-up identifier', + } + ); + + await expect(signUpIdentifierField).toMatchElement('div[class*=select][class*=error]'); +}; + +export const expectToResetSignUpAndSignInConfig = async (page: Page, needSave = true) => { + // Select 'Email address or phone number' first to ensure the sign-in method contains phone and email + await expectToSelectSignUpIdentifier(page, 'Email address or phone number'); + await expectToSelectSignUpIdentifier(page, 'Username'); + await expectToRemoveSignInMethod(page, 'Email address'); + await expectToRemoveSignInMethod(page, 'Phone number'); + if (needSave) { + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + } +}; + +export const expectToAddSocialSignInConnector = async (page: Page, name: string) => { + const socialSignInField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: 'Social sign-in', + } + ); + + await expect(socialSignInField).toClick('button span', { + text: 'Add Social Connector', + }); + + // Wait for the dropdown to be rendered in the correct position + await page.waitForTimeout(500); + + await expect(page).toClick( + '.ReactModalPortal div[class$=dropdownContainer] div[role=menuitem] span[class$=name]', + { + text: name, + } + ); + + await page.waitForSelector('.ReactModalPortal div[class$=dropdownContainer]', { + hidden: true, + }); +}; + +export const expectToRemoveSocialSignInConnector = async (page: Page, name: string) => { + const socialSignInField = await expect(page).toMatchElement( + 'div[class$=field]:has(div[class$=headline] > div[class$=title])', + { + text: 'Social sign-in', + } + ); + + const connectorItem = await expect(socialSignInField).toMatchElement( + 'div[class$=item]:has(span[class$=name])', + { + text: name, + } + ); + + await expect(connectorItem).toClick('button:last-of-type'); +}; + +type ExpectErrorsOnNavTabOptions = { + tab: string; + error?: RegExp | string; +}; + +export const expectErrorsOnNavTab = async ( + page: Page, + { tab, error }: ExpectErrorsOnNavTabOptions +) => { + const signUpAndSignInTab = await expect(page).toMatchElement('nav div[class$=item]:has(a)', { + text: tab, + }); + + await expect(signUpAndSignInTab).toMatchElement('div[class$=errors]', { + text: error, + }); +}; diff --git a/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts new file mode 100644 index 000000000..2ca9f7602 --- /dev/null +++ b/packages/integration-tests/src/tests/ui/sign-in-experience/sign-up-and-sign-in/sad-path.test.ts @@ -0,0 +1,301 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { expectToClickNavTab, goToAdminConsole, trySaveChanges } from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js'; + +import { + expectToDeletePasswordlessConnector, + expectToSetupPasswordlessConnector, + testSendgridConnector, + testTwilioConnector, +} from './connector-setup-helpers.js'; +import { + expectToSelectSignUpIdentifier, + expectNotificationInFiled, + expectSignUpIdentifierSelectorError, + expectToAddSignInMethod, + expectSignInMethodError, + expectErrorsOnNavTab, + expectToClickSignUpAuthnOption, + expectToClickSignInMethodAuthnOption, + expectToResetSignUpAndSignInConfig, +} from './helpers.js'; + +describe('sign-in experience(sad path): sign-up and sign-in', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + + beforeAll(async () => { + await goToAdminConsole(); + }); + + it('navigate to sign-in experience page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/sign-in-experience', logtoConsoleUrl).href) + ); + // Land on branding tab by default + expect(page.url()).toBe(new URL(`console/sign-in-experience/branding`, logtoConsoleUrl).href); + }); + + it('navigate to sign-up and sign-in tab', async () => { + await expectToClickNavTab(page, 'Sign-up and Sign-in'); + + await waitForFormCard(page, 'SIGN UP'); + await waitForFormCard(page, 'SIGN IN'); + await waitForFormCard(page, 'SOCIAL SIGN-IN'); + }); + + describe('cases that no connector is setup', () => { + describe('email address as sign-up identifier', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page, false); + }); + + it('should fail to setup email as sign-up identifier', async () => { + await expectToSelectSignUpIdentifier(page, 'Email address'); + // Disable password settings for sign-up settings + await expectToClickSignUpAuthnOption(page, 'Create your password'); + + await expectNotificationInFiled(page, { + field: 'Sign-up identifier', + content: /No email connector set-up yet./, + }); + + await trySaveChanges(page); + + await expectSignUpIdentifierSelectorError(page); + + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '1 errors', + }); + }); + + it('should fail to add phone number sign-in method', async () => { + await expectToAddSignInMethod(page, 'Phone number'); + await expectNotificationInFiled(page, { + field: 'Identifier and authentication settings for sign-in', + content: /No SMS connector set-up yet./, + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Phone number'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '2 errors', + }); + + // Disable password option for sign-in method + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Password', + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Phone number'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '2 errors', + }); + }); + }); + + describe('phone number as sign-up identifier', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page, false); + }); + + it('should fail to setup phone number as sign-up identifier', async () => { + await expectToSelectSignUpIdentifier(page, 'Phone number'); + // Disable password settings for sign-up settings + await expectToClickSignUpAuthnOption(page, 'Create your password'); + + await expectNotificationInFiled(page, { + field: 'Sign-up identifier', + content: /No SMS connector set-up yet./, + }); + + await trySaveChanges(page); + + await expectSignUpIdentifierSelectorError(page); + + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '1 errors', + }); + }); + + it('should fail to add email address sign-in method', async () => { + await expectToAddSignInMethod(page, 'Email address'); + await expectNotificationInFiled(page, { + field: 'Identifier and authentication settings for sign-in', + content: /No email connector set-up yet./, + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Email address'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '2 errors', + }); + + // Disable password option for sign-in method + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Email address'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '2 errors', + }); + }); + }); + + describe('social sign-in', () => { + it('should display no social connector notification in social sign-in field', async () => { + await expectNotificationInFiled(page, { + field: 'Social sign-in', + content: /No social connector set-up yet./, + }); + }); + }); + }); + + describe('cases that only Email connector is setup', () => { + beforeAll(async () => { + // Email connector + await expectToSetupPasswordlessConnector(page, testSendgridConnector); + // Nav back to sign-in experience page + await expectNavigation( + page.goto( + appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href + ) + ); + }); + + afterAll(async () => { + await expectToDeletePasswordlessConnector(page, testSendgridConnector); + await expectNavigation( + page.goto( + appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href + ) + ); + }); + + describe('email address as sign-up identifier', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('should setup email as sign-up identifier', async () => { + await expectToSelectSignUpIdentifier(page, 'Email address'); + // Disable password settings for sign-up settings + await expectToClickSignUpAuthnOption(page, 'Create your password'); + + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('should fail to add phone number sign-in method', async () => { + await expectToAddSignInMethod(page, 'Phone number'); + await expectNotificationInFiled(page, { + field: 'Identifier and authentication settings for sign-in', + content: /No SMS connector set-up yet./, + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Phone number'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '1 errors', + }); + + // Disable password option for sign-in method + await expectToClickSignInMethodAuthnOption(page, { + method: 'Phone number', + option: 'Password', + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Phone number'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '1 errors', + }); + }); + }); + }); + + describe('cases that only SMS connector is setup', () => { + beforeAll(async () => { + // SMS connector + await expectToSetupPasswordlessConnector(page, testTwilioConnector); + // Nav back to sign-in experience page + await expectNavigation( + page.goto( + appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href + ) + ); + }); + + afterAll(async () => { + await expectToDeletePasswordlessConnector(page, testTwilioConnector); + await expectNavigation( + page.goto( + appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href + ) + ); + }); + + describe('phone number as sign-up identifier', () => { + afterAll(async () => { + await expectToResetSignUpAndSignInConfig(page); + }); + + it('should setup phone number as sign-up identifier', async () => { + await expectToSelectSignUpIdentifier(page, 'Phone number'); + // Disable password settings for sign-up settings + await expectToClickSignUpAuthnOption(page, 'Create your password'); + await expectToSaveSignInExperience(page, { needToConfirmChanges: true }); + }); + + it('should fail to add email sign-in method', async () => { + await expectToAddSignInMethod(page, 'Email address'); + await expectNotificationInFiled(page, { + field: 'Identifier and authentication settings for sign-in', + content: /No email connector set-up yet./, + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Email address'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '1 errors', + }); + + // Disable password option for sign-in method + await expectToClickSignInMethodAuthnOption(page, { + method: 'Email address', + option: 'Password', + }); + + await trySaveChanges(page); + + await expectSignInMethodError(page, 'Email address'); + await expectErrorsOnNavTab(page, { + tab: 'Sign-up and Sign-in', + error: '1 errors', + }); + }); + }); + }); +}); diff --git a/packages/integration-tests/src/ui-helpers/index.ts b/packages/integration-tests/src/ui-helpers/index.ts index fa969b4c1..75663be36 100644 --- a/packages/integration-tests/src/ui-helpers/index.ts +++ b/packages/integration-tests/src/ui-helpers/index.ts @@ -105,3 +105,9 @@ export const expectConfirmModalAndAct = async ( }); } }; + +export const expectToClickNavTab = async (page: Page, tab: string) => { + await expect(page).toClick('nav div[class$=item] div[class$=link] a', { + text: tab, + }); +};