The account modal had accumulated: a profile line, a name form, two collapsed panels for changing email and password, two consent switches, an order table and four controls. The table was the piece that fitted worst, being the only tabular data in a 700px dialog whose body is capped at 70vh. The scroll={{ x: 'max-content' }} already on it was a workaround for being in the wrong container rather than a layout choice.
It moves to /orders, an ordinary page in the same Routes block as /cart and /privacy, rather than another entry in MODAL_ROUTES. Order history is a list you read, like the cart, not a dialog you dismiss. A modal at /account/orders would have been the smaller change and was rejected: it inherits the same width and the same scroll cap, so it moves the table without giving it anything.
The page shell follows Cart.tsx, which is the established shape here: a Layout with a Header carrying Back to Shop and the title, and the same guard sending a signed-out visitor to /login. The account modal keeps a View order history button where the table used to be, because that is where a customer looks for it.
One thing changes rather than moves. The old effect caught a failed load with a toast and left orders as an empty array. The toast faded and the empty table did not, so from then on a customer whose request failed saw exactly what a customer with no orders saw, and the page asserted something false. Loading, failed and empty are now three distinct states, and the failed one carries a Retry: a transient failure would otherwise strand someone on a page that needs a full reload to recover.
OrdersBody sits at module level rather than nested inside Orders(). A function declared inside a component counts toward that component's cognitive complexity, which is what made Customers() hard to bring back under the threshold in #81.
The two assertions in account-modal.spec.ts that looked for the text "Order History" inside the modal are updated to look for the link, not deleted. They were the only coverage that the account view still offers any route to the orders, which is exactly what this change could have silently broken.
Verification, against a real backend and database: five new tests covering the signed-out redirect, the empty state, Back to Shop, the link from My Account, and that the page renders as a page rather than a modal over the storefront - that last one is what would catch /orders being added to MODAL_ROUTES and quietly undoing the change. The full suite goes from 100 to 105 passing with no new failures; the three that fail did so before this branch and fail identically on main. tsc and the production build are clean, ESLint reports no errors.
Closes #121
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
import { test, expect, Page } from './fixtures';
|
|
|
|
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.getByRole('textbox', { name: 'First name' }).fill('Test');
|
|
await page.getByRole('textbox', { name: 'Last name' }).fill('Customer');
|
|
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);
|
|
// The orders table lives at /orders now. What the account view still owes
|
|
// the customer is a way to reach it.
|
|
await expect(modal.getByRole('button', { name: 'View order history' })).toBeVisible();
|
|
// 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.getByRole('button', { name: 'View order history' })).toBeVisible();
|
|
|
|
await closeAccount(page);
|
|
await expect(page).toHaveURL(/\/$/);
|
|
});
|
|
});
|