PR #317 was merged while its run was still failing, so four failures landed on main: one integration and three end-to-end. All of them are consequences of #56, and none could have been caught on the dev machine, which has no database and no browser to run those suites with. The schema mirror was never regenerated. The migration added three columns to customers and src/db-kysely/schema.ts still described the table without them, which is the drift guard from #305 doing exactly what it exists for. Hand-edited to match what kysely-codegen emits — alphabetical, and Generated on the column that has a default — because regenerating properly needs a live database. The other three are the same mistake three times: a control addressed by position, and the position moved. An unscoped getByRole('checkbox') became ambiguous once the register form had two consents. A toHaveCount(2) on the account modal's switches became three. And favoriteAlertsSwitch was getByRole('switch').last(), which did not error when a switch was appended below it — it silently retargeted, toggled analytics consent instead of favourite alerts, and then failed on a text assertion in favorites.spec.ts, naming neither the file nor the control actually at fault. The reason position was ever used is that antd's Switch renders a bare role="switch" with no accessible name; the adjacent Text is a sibling, not a label. So each one now carries an explicit aria-label and is addressed by it. That is what makes them addressable from a test, and it is what a screen reader needed regardless — the fix and the accessibility improvement are the same change. The count assertion stays, but alongside naming each switch, because a count on its own would pass if two of them were swapped for each other. Two coverage gaps closed while here, both properties the compliance work in #56 depends on and neither previously asserted anywhere a customer could see: the analytics checkbox is unchecked on the register form, and the account toggle is off for a new customer. Quebec's Law 25 s.8.1 requires profiling to start off, the integration suite asserts the server half of that, and nothing asserted the half rendered on screen. The fourth Playwright entry, the logged-out header surviving a reload, is reported flaky rather than failed and passed on retry. Left alone; it is unrelated to #56 and #257 covers flakes in this suite. Verified: backend tsc clean, frontend tsc against the test config clean, production build green, both lint suites 0 errors, 478 unit tests passing. The integration and e2e suites still cannot run here, so whether this actually clears run 875's failures is for CI to say — which is the same gap that produced them. Closes #320 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
8.3 KiB
TypeScript
Executable File
228 lines
8.3 KiB
TypeScript
Executable File
import { test, expect, PASSWORD, uniqueEmail } from './fixtures';
|
|
|
|
test.describe('Customer accounts', () => {
|
|
// Both names are required from anyone new so every email has a first name to
|
|
// greet with (#106). The server refuses without them; this is the form
|
|
// refusing first, so nobody gets a round trip to find out.
|
|
test('will not submit a registration without both names', async ({ authModal, header }) => {
|
|
await authModal.gotoRegister();
|
|
await authModal.fillRegistration({ email: uniqueEmail(), password: PASSWORD });
|
|
await authModal.submitRegistration();
|
|
|
|
await expect(authModal.registerDialog.getByText('First name is required')).toBeVisible();
|
|
await expect(authModal.registerDialog.getByText('Last name is required')).toBeVisible();
|
|
// Still on the form rather than signed in.
|
|
await expect(header.myAccountButton).toHaveCount(0);
|
|
});
|
|
|
|
test('marketing consent checkbox is unchecked by default', async ({ authModal }) => {
|
|
await authModal.gotoRegister();
|
|
await expect(authModal.marketingConsent).not.toBeChecked();
|
|
});
|
|
|
|
// A second, separate consent (#56). Unchecked for the same reason as the one
|
|
// above, and asserted separately because the two must be independently
|
|
// refusable — a single control covering both is the bundling GDPR treats as
|
|
// invalid, and Law 25 requires this one to start off.
|
|
test('analytics consent checkbox is unchecked by default', async ({ authModal }) => {
|
|
await authModal.gotoRegister();
|
|
await expect(authModal.analyticsConsent).not.toBeChecked();
|
|
});
|
|
|
|
test('the consent label is the exact wording the server records', async ({ authModal }) => {
|
|
await authModal.gotoRegister();
|
|
|
|
// The stored consent text is kept verbatim so the record says what the
|
|
// customer actually saw. Three different wordings were in circulation
|
|
// before the sign-in form was shared between the routes and the cart
|
|
// prompt, and none of them matched what was stored.
|
|
const consent =
|
|
'I want to receive occasional emails about new one-of-a-kind items from Redefined Designs. I can unsubscribe at any time.';
|
|
await expect(authModal.registerDialog).toContainText(consent);
|
|
|
|
// The analytics consent is a second, separate sentence and a second
|
|
// checkbox (#56). Asserted here for the same reason as the one above — it
|
|
// is stored verbatim, so the rendered label parting from the stored string
|
|
// defeats the record — and because the two being separate is the thing
|
|
// that makes the consent granular. Folding them back into one control
|
|
// would still pass the assertion above and would still be wrong.
|
|
const analytics =
|
|
'I agree that what I browse and buy on this site may be shared with Brevo, the service that sends our emails, so that what they contain is relevant to me. This is optional, separate from receiving the emails themselves, and I can turn it off at any time.';
|
|
await expect(authModal.registerDialog).toContainText(analytics);
|
|
});
|
|
|
|
// Drives the form rather than taking the `customer` fixture: this test is
|
|
// about registering, so the thing under test has to be the thing exercised.
|
|
test('registering signs the customer in and returns them where they were', async ({
|
|
page,
|
|
authModal,
|
|
header,
|
|
accountModal
|
|
}) => {
|
|
const email = uniqueEmail();
|
|
await authModal.gotoRegister();
|
|
await authModal.fillRegistration({
|
|
email,
|
|
password: PASSWORD,
|
|
firstName: 'Test',
|
|
lastName: 'Customer'
|
|
});
|
|
await authModal.submitRegistration();
|
|
await header.waitForSignedIn();
|
|
|
|
// Back on the storefront, signed in — not moved to the account page.
|
|
await expect(page).toHaveURL(/\/$/);
|
|
await accountModal.open();
|
|
await expect(accountModal.emailText(email)).toBeVisible();
|
|
});
|
|
|
|
test('rejects login with the wrong password', async ({ page, customer, accountModal, authModal, header }) => {
|
|
await accountModal.openAndLogOut();
|
|
await expect(header.logInButton).toBeVisible();
|
|
|
|
await authModal.gotoLogIn();
|
|
await authModal.logIn(customer.email, 'wrong-password');
|
|
|
|
await expect(page.getByText('invalid email or password')).toBeVisible();
|
|
});
|
|
|
|
test('logging out returns to the home page and resets the header', async ({
|
|
page,
|
|
customer,
|
|
accountModal,
|
|
header
|
|
}) => {
|
|
await accountModal.openAndLogOut();
|
|
|
|
// A server round-trip followed by re-rendering the storefront behind the
|
|
// modal, so the 5s default is too tight when workers run concurrently.
|
|
await expect(page).toHaveURL(/\/$/, { timeout: 20000 });
|
|
await expect(header.logInButton).toBeVisible();
|
|
await expect(header.signUpButton).toBeVisible();
|
|
await expect(header.myAccountButton).toBeHidden();
|
|
});
|
|
|
|
test('the logged-out header survives a reload', async ({ page, customer, accountModal, header }) => {
|
|
await accountModal.openAndLogOut();
|
|
await expect(header.logInButton).toBeVisible();
|
|
|
|
// Proves the server session was actually destroyed, rather than the header
|
|
// merely being repainted from stale client state.
|
|
await page.reload();
|
|
await expect(header.logInButton).toBeVisible();
|
|
await expect(header.myAccountButton).toBeHidden();
|
|
});
|
|
|
|
test('logging out does not leave the account page on the back stack', async ({
|
|
page,
|
|
customer,
|
|
accountModal
|
|
}) => {
|
|
await accountModal.openAndLogOut();
|
|
await expect(page).toHaveURL(/\/$/, { timeout: 20000 });
|
|
|
|
await page.goBack();
|
|
await expect(page).not.toHaveURL(/\/account/);
|
|
});
|
|
|
|
test('a failed logout says so instead of appearing to succeed', async ({
|
|
page,
|
|
customer,
|
|
accountModal
|
|
}) => {
|
|
await accountModal.open();
|
|
|
|
await page.route('**/api/customers/logout', (route) =>
|
|
route.fulfill({ status: 500, contentType: 'application/json', body: '{"error":"internal error"}' })
|
|
);
|
|
|
|
await accountModal.logOut();
|
|
|
|
// The session cookie is still valid, so pretending to be logged out would
|
|
// silently log the customer back in on their next reload.
|
|
await expect(page.getByText(/couldn't log out/i)).toBeVisible();
|
|
await expect(page).toHaveURL(/\/account/);
|
|
});
|
|
});
|
|
|
|
test.describe('Auth routes are not dead ends', () => {
|
|
test('opening Log in from the header closes back to where browsing left off', async ({
|
|
page,
|
|
header,
|
|
authModal
|
|
}) => {
|
|
await page.goto('/?max_price=50000');
|
|
await header.logInButton.click();
|
|
|
|
await expect(authModal.logInDialog).toBeVisible();
|
|
await expect(page).toHaveURL(/\/login/);
|
|
|
|
await authModal.closeButton.click();
|
|
|
|
await expect(authModal.logInDialog).toBeHidden();
|
|
await expect(page).toHaveURL(/max_price=50000/);
|
|
});
|
|
|
|
test('a direct visit opens over the storefront rather than a blank page', async ({
|
|
authModal,
|
|
header
|
|
}) => {
|
|
await authModal.gotoLogIn();
|
|
|
|
await expect(authModal.logInDialog).toBeVisible();
|
|
await expect(header.siteTitle).toBeVisible();
|
|
});
|
|
|
|
test('switching between sign in and sign up keeps one history entry', async ({
|
|
page,
|
|
header,
|
|
authModal
|
|
}) => {
|
|
await page.goto('/?max_price=50000');
|
|
await header.logInButton.click();
|
|
|
|
await authModal.createAccountTab.click();
|
|
await expect(page).toHaveURL(/\/register/);
|
|
await authModal.logInTab.click();
|
|
await expect(page).toHaveURL(/\/login/);
|
|
|
|
// Back returns to browsing rather than walking through each tab visited.
|
|
await page.goBack();
|
|
await expect(page).toHaveURL(/max_price=50000/);
|
|
await expect(authModal.logInDialog).toBeHidden();
|
|
});
|
|
|
|
test('signing in from the header returns to the page behind, signed in', async ({
|
|
page,
|
|
customer,
|
|
accountModal,
|
|
header,
|
|
authModal
|
|
}) => {
|
|
await accountModal.openAndLogOut();
|
|
await expect(header.logInButton).toBeVisible();
|
|
|
|
await page.goto('/?max_price=50000');
|
|
await header.logInButton.click();
|
|
await authModal.logIn(customer.email, customer.password);
|
|
|
|
await header.waitForSignedIn();
|
|
await expect(page).toHaveURL(/max_price=50000/);
|
|
});
|
|
|
|
test('reaching password recovery from the login form keeps a way back', async ({
|
|
page,
|
|
authModal,
|
|
passwordReset
|
|
}) => {
|
|
await authModal.gotoLogIn();
|
|
await authModal.forgotPasswordButton.click();
|
|
|
|
await expect(passwordReset.requestDialog).toBeVisible();
|
|
await expect(page).toHaveURL(/\/forgot-password/);
|
|
|
|
await passwordReset.signInButton.click();
|
|
await expect(authModal.logInDialog).toBeVisible();
|
|
});
|
|
});
|