import { Locator, Page, expect } from '@playwright/test'; /** * The register / log in form. * * It is a modal wherever it appears — over the storefront at /register and * /login, and over whatever the customer was doing when they clicked Add to * Cart while signed out. One object covers all three, because it is one * component (`AuthForm`, shared by `AuthRouteModal` and `AuthPromptModal`). * * Locators are scoped to the dialog rather than the page. The storefront behind * the modal has a "Log in" button of its own — the one that opened this — so an * unscoped `getByRole('button', { name: 'Log in' })` matches two things and * fails on strict mode, which is a confusing way to learn that the modal is a * modal. */ export class AuthModal { readonly registerDialog: Locator; readonly logInDialog: Locator; readonly email: Locator; readonly firstName: Locator; readonly lastName: Locator; readonly password: Locator; readonly marketingConsent: Locator; readonly analyticsConsent: Locator; readonly createAccountButton: Locator; readonly createAccountTab: Locator; readonly logInTab: Locator; readonly forgotPasswordButton: Locator; constructor(private readonly page: Page) { this.registerDialog = page.getByRole('dialog', { name: 'Create an account' }); this.logInDialog = page.getByRole('dialog', { name: 'Log in' }); this.email = page.getByRole('textbox', { name: 'Email' }); this.firstName = page.getByRole('textbox', { name: 'First name' }); this.lastName = page.getByRole('textbox', { name: 'Last name' }); this.password = page.getByLabel('Password'); // Named rather than "the checkbox on the form". There are two consents now // and they are deliberately separate (#56), so an unscoped checkbox locator // is ambiguous in strict mode — which is how this broke. Matched on the // opening words of each sentence so the locator survives the wording being // revised, which it has been once already. this.marketingConsent = page.getByRole('checkbox', { name: /^I want to receive occasional emails/ }); this.analyticsConsent = page.getByRole('checkbox', { name: /^I agree that what I browse and buy/ }); this.createAccountButton = page.getByRole('button', { name: 'Create account' }); this.createAccountTab = page.getByRole('tab', { name: 'Create Account' }); this.logInTab = page.getByRole('tab', { name: 'Log In' }); this.forgotPasswordButton = page.getByRole('button', { name: 'Forgot password?' }); } /** The submit button inside the log in dialog, not the one that opened it. */ get submitLogInButton(): Locator { return this.logInDialog.getByRole('button', { name: 'Log in' }); } /** Closes whichever of the two dialogs is open. */ get closeButton(): Locator { return this.page.getByRole('dialog').getByRole('button', { name: 'Close' }); } /** * Signs in through the modal, wherever it was opened from. * * Scoped to the dialog throughout: the storefront behind it has its own * "Log in" button — the one that opened this — and an unscoped fill or click * matches both. */ async logIn(email: string, password: string): Promise { await this.logInDialog.getByRole('textbox', { name: 'Email' }).fill(email); await this.logInDialog.getByLabel('Password').fill(password); await this.submitLogInButton.click(); } async gotoRegister(): Promise { await this.page.goto('/register'); } async gotoLogIn(): Promise { await this.page.goto('/login'); } /** Fills the registration form without submitting, for tests about validation. */ async fillRegistration(details: { email: string; password: string; firstName?: string; lastName?: string; }): Promise { await this.email.fill(details.email); if (details.firstName !== undefined) await this.firstName.fill(details.firstName); if (details.lastName !== undefined) await this.lastName.fill(details.lastName); await this.password.fill(details.password); } async submitRegistration(): Promise { await this.createAccountButton.click(); } async fillCredentials(email: string, password: string): Promise { await this.email.fill(email); await this.password.fill(password); } async submitLogIn(): Promise { await this.submitLogInButton.click(); } /** * Waits for the modal to close, which is what registering or logging in does. * * The dialog going away is the completion of the action rather than an * assertion about it: a test that carries on while the modal is still over * the page clicks the modal's backdrop instead of what it meant to. */ async waitForDismissal(): Promise { await expect(this.registerDialog).toHaveCount(0); await expect(this.logInDialog).toHaveCount(0); } }