diff --git a/packages/tests-e2e/src/connect/models/BaseModel.ts b/packages/tests-e2e/src/connect/models/BaseModel.ts index 996225a3c..b68e29351 100644 --- a/packages/tests-e2e/src/connect/models/BaseModel.ts +++ b/packages/tests-e2e/src/connect/models/BaseModel.ts @@ -1,7 +1,7 @@ -import type { Page } from '@playwright/test'; +import { Page } from '@playwright/test'; -import { ScreenNames } from '../utils/Constants'; -import { expectScreen } from '../utils/ExpectScreen'; +import { ErrorTexts, ScreenNames } from '../utils/Constants'; +import { expectScreen, expectError } from '../utils/ExpectScreen'; import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; import { AppendModel } from './AppendModel'; import { HomeModel } from './HomeModel'; @@ -53,6 +53,10 @@ export class BaseModel { return expectScreen(this.page, screenName); } + expectError(message: ErrorTexts) { + return expectError(this.page, message); + } + async createUser(invited: boolean, append: boolean) { this.email = await this.signup.autofillCredentials(); await this.signup.submit(); diff --git a/packages/tests-e2e/src/connect/models/LoginModel.ts b/packages/tests-e2e/src/connect/models/LoginModel.ts index eed6e05c3..e31a921b7 100644 --- a/packages/tests-e2e/src/connect/models/LoginModel.ts +++ b/packages/tests-e2e/src/connect/models/LoginModel.ts @@ -1,7 +1,9 @@ -import { expect } from '@playwright/test'; import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; +import { expectScreen } from '../utils/ExpectScreen'; +import { ScreenNames } from '../utils/Constants'; export class LoginModel { page: Page; @@ -12,16 +14,36 @@ export class LoginModel { this.authenticator = authenticator; } - submitPasskeyButton(): Promise { + submitPasskeyButton(complete: boolean) { const operationTrigger = () => this.page.locator('.cb-passkey-button').click(); - return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + if (complete) { + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } else { + return this.authenticator.startAndCancelPasskeyOperation(operationTrigger, () => + expectScreen(this.page, ScreenNames.PasskeyError1), + ); + } } - removePasskeyButton(): Promise { + removePasskeyButton() { return this.page.locator('.cb-switch').click(); } - async submitEmail(email: string, withPasskey: boolean): Promise { + async repeatedlyFailPasskeyInput() { + const operationTrigger1 = () => this.page.getByRole('button', { name: 'Continue' }).click(); + await this.authenticator.startAndCancelPasskeyOperation(operationTrigger1, () => + expectScreen(this.page, ScreenNames.PasskeyError2), + ); + + const operationTrigger2 = () => this.page.getByRole('button', { name: 'Try again' }).click(); + await this.authenticator.startAndCancelPasskeyOperation(operationTrigger2, () => this.page.waitForTimeout(100)); + await this.authenticator.startAndCancelPasskeyOperation(operationTrigger2, () => this.page.waitForTimeout(100)); + await this.authenticator.startAndCancelPasskeyOperation(operationTrigger2, () => + expectScreen(this.page, ScreenNames.InitLoginFallback), + ); + } + + async submitEmail(email: string, withPasskey: boolean) { await this.page.getByLabel('Email address').fill(email); if (withPasskey) { const operationTrigger = () => this.page.getByRole('button', { name: 'Login' }).click(); @@ -31,7 +53,7 @@ export class LoginModel { } } - async submitFallbackCredentials(email: string, password: string, emailAutofilled = false): Promise { + async submitFallbackCredentials(email: string, password: string, emailAutofilled = false) { if (emailAutofilled) { await expect(this.page.getByPlaceholder('Email')).toHaveValue(email); } else { diff --git a/packages/tests-e2e/src/connect/models/PasskeyListModel.ts b/packages/tests-e2e/src/connect/models/PasskeyListModel.ts index 52f47392e..eab308022 100644 --- a/packages/tests-e2e/src/connect/models/PasskeyListModel.ts +++ b/packages/tests-e2e/src/connect/models/PasskeyListModel.ts @@ -16,20 +16,23 @@ export class PasskeyListModel { return expect(this.page.locator('.cb-passkey-list-item')).toHaveCount(n); } - async deletePasskey(index: number) { + async deletePasskey(index: number): Promise { await this.page.locator('.cb-passkey-list-item-delete-icon').nth(index).click(); await this.page.getByRole('button', { name: 'Delete' }).click(); } - async appendPasskey(complete: boolean) { - const operationTrigger = () => this.page.getByRole('button', { name: 'Add a passkey' }).click(); + appendPasskey(complete: boolean): Promise { + const operationTrigger: () => Promise = (): Promise => + this.page.getByRole('button', { name: 'Add a passkey' }).click(); if (complete) { - await this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); } else { - await this.authenticator.startAndCancelPasskeyOperation(operationTrigger, () => - expect(this.page.locator('.cb-notification-text')).toHaveText( - 'You have cancelled setting up your passkey. Please try again.', - ), + return this.authenticator.startAndCancelPasskeyOperation( + operationTrigger, + (): Promise => + expect(this.page.locator('.cb-notification-text')).toHaveText( + 'You have cancelled setting up your passkey. Please try again.', + ), ); } } diff --git a/packages/tests-e2e/src/connect/scenarios/hooks.ts b/packages/tests-e2e/src/connect/scenarios/hooks.ts index eaa287ded..8abea9824 100644 --- a/packages/tests-e2e/src/connect/scenarios/hooks.ts +++ b/packages/tests-e2e/src/connect/scenarios/hooks.ts @@ -24,16 +24,16 @@ export function setupVirtualAuthenticator( }); } -// export function loadInvitationToken( -// test: TestType< -// PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, -// PlaywrightWorkerArgs & PlaywrightWorkerOptions -// >, -// ) { -// test.beforeEach(async ({ model }) => { -// await model.loadInvitationToken(); -// }); -// } +export function loadInvitationToken( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.loadInvitationToken(); + }); +} export function loadSignup( test: TestType< diff --git a/packages/tests-e2e/src/connect/scenarios/login.spec.ts b/packages/tests-e2e/src/connect/scenarios/login.spec.ts index 230229727..8dda5d79d 100644 --- a/packages/tests-e2e/src/connect/scenarios/login.spec.ts +++ b/packages/tests-e2e/src/connect/scenarios/login.spec.ts @@ -1,6 +1,6 @@ import { test } from '../fixtures/BaseTest'; -import { password, ScreenNames } from '../utils/Constants'; -import { setupUser, setupVirtualAuthenticator } from './hooks'; +import { ErrorTexts, password, ScreenNames } from '../utils/Constants'; +import { loadInvitationToken, setupUser, setupVirtualAuthenticator } from './hooks'; test.describe('login component (without invitation token)', () => { setupUser(test, false); @@ -41,7 +41,7 @@ test.describe('login component (with invitation token, with passkeys)', () => { await model.home.logout(); await model.expectScreen(ScreenNames.InitLoginOneTap); - await model.login.submitPasskeyButton(); + await model.login.submitPasskeyButton(true); await model.expectScreen(ScreenNames.Home); }); @@ -55,4 +55,33 @@ test.describe('login component (with invitation token, with passkeys)', () => { await model.login.submitEmail(model.email, true); await model.expectScreen(ScreenNames.Home); }); + + test('attempt login with repeated failed passkey input', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.login.submitPasskeyButton(false); + await model.login.repeatedlyFailPasskeyInput(); + }); +}); + +test.describe('login component (input validation)', () => { + setupVirtualAuthenticator(test); + loadInvitationToken(test); + + test('attempt login with incomplete credentials', async ({ model }) => { + await model.loadLogin(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail('', false); + await model.expectError(ErrorTexts.EmptyEmail); + }); + + test('attempt login with unknown credentials', async ({ model }) => { + await model.loadLogin(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail('unknown-email@corbado.com', false); + await model.expectError(ErrorTexts.UnknownEmail); + }); }); diff --git a/packages/tests-e2e/src/connect/utils/Constants.ts b/packages/tests-e2e/src/connect/utils/Constants.ts index 3b3a444d4..29159a1f3 100644 --- a/packages/tests-e2e/src/connect/utils/Constants.ts +++ b/packages/tests-e2e/src/connect/utils/Constants.ts @@ -7,6 +7,14 @@ export enum ScreenNames { PasskeyAppended, Home, PasskeyList, + PasskeyError1, + PasskeyError2, +} + +export enum ErrorTexts { + EmptyEmail = 'Enter your email address.', + UnknownEmail = 'There is no account registered to that email address.', + CancelledPasskey = 'You have cancelled setting up your passkey. Please try again.', } export const phone = '+4915121609839'; diff --git a/packages/tests-e2e/src/connect/utils/ExpectScreen.ts b/packages/tests-e2e/src/connect/utils/ExpectScreen.ts index a27179fb8..0befd2de2 100644 --- a/packages/tests-e2e/src/connect/utils/ExpectScreen.ts +++ b/packages/tests-e2e/src/connect/utils/ExpectScreen.ts @@ -1,9 +1,9 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import { ScreenNames } from './Constants'; +import { ErrorTexts, ScreenNames } from './Constants'; -export const expectScreen = async (page: Page, screenName: ScreenNames) => { +export const expectScreen = async (page: Page, screenName: ScreenNames): Promise => { switch (screenName) { case ScreenNames.InitSignup: await expect(page.locator('div.font-bold.text-xl')).toHaveText('Signup'); @@ -41,7 +41,23 @@ export const expectScreen = async (page: Page, screenName: ScreenNames) => { await expect(page.locator('.cb-connect-passkey-list')).toBeVisible(); return; + case ScreenNames.PasskeyError1: + await expect(page.locator('.cb-connect-container').locator('.cb-login-header')).toContainText( + 'Use your passkey to confirm it’s really you', + ); + return; + + case ScreenNames.PasskeyError2: + await expect(page.locator('.cb-connect-container').locator('.cb-login-header')).toContainText( + 'Something went wrong!', + ); + return; + default: throw new Error('Invalid screen'); } }; + +export const expectError = (page: Page, message: ErrorTexts): Promise => { + return expect(page.locator('.cb-notification-text')).toHaveText(message); +};