/login, /register and /forgot-password rendered bare cards with no site header. They linked to each other and nowhere else, so a customer who clicked Log in from the storefront and changed their mind had no way back except the browser's back button. All four auth routes are now modals over the page the customer was already on, reusing the backdrop-location arrangement from #51: a direct visit or a link from an email opens over the storefront, so closing always lands somewhere real. They remain real routes, because /reset-password links to /login and customers may have bookmarks. Signing in or registering now returns the customer to the page behind, signed in, rather than moving them to /account. Someone who signs in while browsing wants to carry on browsing, and this is already how the cart and favorites prompts behave when they resume an interrupted action. The larger half of this is removing the duplication. Signing in existed twice — as these routes and again inside the prompt shown when a signed-out visitor adds to the cart or favorites something — and the two had already drifted. There were three different wordings of the marketing consent in circulation: the register page's, a shorter one in the prompt, and the string the server actually stores. The server keeps that text verbatim so the consent record says what the customer saw, which none of the three did. Both callers now render one shared AuthForm whose checkbox is the exact string the server records, and a test asserts that wording so it cannot drift again silently. Steps within the auth flow replace rather than push, so switching between tabs or stepping to password recovery leaves the whole detour as a single history entry and closing returns to where it started instead of walking back through every tab that was looked at. The privacy policy link opens in a new tab: following it in place would discard a part-filled signup form, and /privacy still has no way back of its own until #52. Test changes follow from the destination change rather than being incidental. Nineteen assertions across seven specs waited for /account after signing in; they now assert the header shows a signed-in customer, which is the condition actually being waited for. Modal submits are scoped to their dialog, because the storefront behind now offers a Log in button of its own and an unscoped locator matched both. Assertions that follow a server round-trip were given a realistic timeout — the 5s default is too tight for a bcrypt hash plus re-rendering the storefront behind the modal. Verified with 83 end-to-end tests, all passing, and type checking clean. No backend changes. Closes #50
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
import { test, expect, Page } from '@playwright/test';
|
|
|
|
const PASSWORD = 'supersecret123';
|
|
|
|
const uniqueEmail = () => `account-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}@example.com`;
|
|
|
|
// Registering closes the auth modal and returns to the storefront, signed in.
|
|
// Returns the address so a test can assert the right account is shown.
|
|
//
|
|
// The wait is generous because registration is a bcrypt round-trip rather than
|
|
// a render: about half a second unloaded, and past Playwright's 5s default when
|
|
// the suite's workers all register at once.
|
|
async function registerCustomer(page: Page): Promise<string> {
|
|
const email = uniqueEmail();
|
|
await page.goto('/register');
|
|
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
|
await page.getByLabel('Password').fill(PASSWORD);
|
|
await page.getByRole('button', { name: 'Create account' }).click();
|
|
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 });
|
|
return email;
|
|
}
|
|
|
|
const accountModal = (page: Page) => page.getByRole('dialog', { name: 'My Account' });
|
|
|
|
async function closeAccount(page: Page) {
|
|
await accountModal(page).getByRole('button', { name: 'Close' }).click();
|
|
await expect(accountModal(page)).toBeHidden();
|
|
}
|
|
|
|
test.describe('My Account opens as a modal', () => {
|
|
test('opens over the storefront and closes back to it, filters and all', async ({ page }) => {
|
|
await registerCustomer(page);
|
|
|
|
// A filtered view, to prove closing restores where the customer actually
|
|
// was rather than a bare storefront.
|
|
await page.goto('/?max_price=50000');
|
|
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: 'My Account' }).click();
|
|
await expect(accountModal(page)).toBeVisible();
|
|
// Still a real, linkable URL rather than hidden view state.
|
|
await expect(page).toHaveURL(/\/account/);
|
|
|
|
await closeAccount(page);
|
|
await expect(page).toHaveURL(/max_price=50000/);
|
|
});
|
|
|
|
test('the browser back button closes it, the same as the close control', async ({ page }) => {
|
|
await registerCustomer(page);
|
|
|
|
await page.goto('/?max_price=50000');
|
|
await page.getByRole('button', { name: 'My Account' }).click();
|
|
await expect(accountModal(page)).toBeVisible();
|
|
|
|
await page.goBack();
|
|
|
|
await expect(accountModal(page)).toBeHidden();
|
|
await expect(page).toHaveURL(/max_price=50000/);
|
|
});
|
|
|
|
test('a direct visit renders the storefront behind it, so closing lands somewhere real', async ({ page }) => {
|
|
await registerCustomer(page);
|
|
|
|
// A bookmark, or the link in a verification email. There is no page behind
|
|
// in this case, which is what used to make /account a dead end.
|
|
await page.goto('/account');
|
|
|
|
await expect(accountModal(page)).toBeVisible();
|
|
await expect(page.getByRole('heading', { name: 'Redefined Designs' })).toBeVisible();
|
|
|
|
await closeAccount(page);
|
|
await expect(page).toHaveURL(/\/$/);
|
|
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible();
|
|
});
|
|
|
|
test('survives a reload, since it is a route rather than view state', async ({ page }) => {
|
|
const email = await registerCustomer(page);
|
|
|
|
await page.goto('/?max_price=50000');
|
|
await page.getByRole('button', { name: 'My Account' }).click();
|
|
await expect(accountModal(page)).toBeVisible();
|
|
|
|
await page.reload();
|
|
|
|
await expect(accountModal(page)).toBeVisible();
|
|
await expect(accountModal(page)).toContainText(email);
|
|
});
|
|
|
|
test('shows the signed-in account and its settings', async ({ page }) => {
|
|
const email = await registerCustomer(page);
|
|
|
|
await page.goto('/account');
|
|
|
|
const modal = accountModal(page);
|
|
await expect(modal).toContainText(email);
|
|
await expect(modal).toContainText('Order History');
|
|
// Scoped to the modal: the storefront behind it has a theme switch of its
|
|
// own, so an unscoped switch locator would be ambiguous.
|
|
await expect(modal.getByRole('switch')).toHaveCount(2);
|
|
});
|
|
|
|
test('deleting the account does not leave the page behind it looking signed in', async ({ page }) => {
|
|
await registerCustomer(page);
|
|
await page.goto('/account');
|
|
|
|
await accountModal(page).getByRole('button', { name: 'Delete my account' }).click();
|
|
await page.getByRole('dialog', { name: 'Delete your account?' })
|
|
.getByRole('button', { name: 'Delete my account' }).click();
|
|
|
|
// The storefront is rendered behind the modal, so a session left in place
|
|
// would visibly go on offering My Account for an account that is gone.
|
|
await expect(page.getByRole('button', { name: 'Sign up' })).toBeVisible();
|
|
await expect(page.getByRole('button', { name: 'My Account' })).toBeHidden();
|
|
await expect(page).toHaveURL(/\/$/);
|
|
});
|
|
|
|
test('stays usable on a phone, with the close control in reach', async ({ page }) => {
|
|
await registerCustomer(page);
|
|
await page.setViewportSize({ width: 390, height: 664 });
|
|
await page.goto('/account');
|
|
|
|
const modal = accountModal(page);
|
|
await expect(modal).toBeVisible();
|
|
// The view is taller than the viewport, so the body scrolls rather than
|
|
// pushing the title and close control off-screen.
|
|
await expect(modal.getByRole('button', { name: 'Close' })).toBeInViewport();
|
|
await expect(modal.getByText('Order History')).toBeVisible();
|
|
|
|
await closeAccount(page);
|
|
await expect(page).toHaveURL(/\/$/);
|
|
});
|
|
});
|