Files
redefined-designs/frontend/tests/e2e/resend-verification.spec.ts
T
synAdminandClaude Opus 5 218be6d298
Linting / lint (pull_request) Successful in 3m2s
SonarQube Analysis / sonarqube (pull_request) Failing after 24m53s
test(e2e): make the resend allowance failure say why (#257)
The test asserted on toasts, and toasts were the wrong instrument twice over. It failed roughly one full-suite run in three with a strict mode violation — getByText(/already sent several/) resolving to three refusal toasts where one was expected — and two investigations could not establish the mechanism.

The second investigation corrected the arithmetic the first depended on: antd toasts auto-dismiss, so the number visible at the moment of an assertion is a lower bound on how many refusals happened rather than a count. Three visible refusals is equally consistent with four where the first had already faded. That removed the only evidence anyone had for the original theory that the customer's bucket held two hits before the test clicked anything, which left the issue with a symptom and no way to read it.

So this asserts the sequence of response statuses instead. Toasts are a lossy, timing-dependent rendering of the thing the test is actually about, and the responses are the behaviour itself. A failure now reports what happened: four 429s means the bucket really did carry hits from somewhere else, while more than four entries means the UI sent more requests than there were clicks. Either reading identifies the mechanism from one failing run, where before it needed a temporary probe re-added and the suite run until it failed again.

Each click now waits for its own response. The previous "await expect(resend).toBeEnabled()" looked like pacing but was a no-op, since the button is never disabled, so four requests raced. Removing that ordering means a failure cannot be blamed on it. The copy assertion stays, because it is what the test is named for, but scoped with .first() so strict mode does not treat several identical toasts as ambiguous.

This does not fix the underlying flake, and is not meant to. The issue asks for the mechanism to be found before a fix is attempted rather than guessed at, and nothing here changes the limiter or the store.

Verified by typecheck and lint only. The e2e suite needs a database and a browser this machine cannot run, so whether this passes is for CI to say — the same gap that let the integration regression through earlier today.

Refs #257

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:48:33 -05:00

92 lines
4.2 KiB
TypeScript

import { test, expect } from './fixtures';
test.describe('Resending your verification email', () => {
test('the account page offers it while the address is unverified', async ({
customer,
accountModal
}) => {
await accountModal.open();
await expect(accountModal.dialog.getByText('Email not verified')).toBeVisible();
await expect(accountModal.resendVerificationButton).toBeVisible();
});
test('confirms when it has sent', async ({ page, customer, accountModal }) => {
await accountModal.open();
await accountModal.resendVerificationButton.click();
await expect(page.getByText('Check your inbox, and your spam folder.')).toBeVisible();
});
// The message the customer gets on the fourth attempt is the point of the
// limiter's copy: it says the mail probably did send and where to look,
// rather than only that a limit exists.
//
// ## Why this asserts on responses rather than on what is on screen (#257)
//
// This test failed roughly one full-suite run in three, never in isolation,
// with a strict-mode violation: `getByText(/already sent several/)` resolved
// to three refusal toasts where one was expected. Two investigations could
// not establish why, and the second corrected the arithmetic the first
// relied on — antd toasts auto-dismiss, so **the number visible at the moment
// of an assertion is a lower bound on how many refusals happened, not a
// count**. Three visible refusals is equally consistent with four where the
// first had already faded.
//
// That makes toast-counting the wrong instrument twice over: it is
// timing-dependent, and `toBeVisible()` on a multi-match locator fails in
// strict mode even when the behaviour was correct. The responses are the
// behaviour; the toasts are a lossy rendering of it.
//
// So the sequence of statuses is recorded and asserted directly. If this
// fails again it now says which failure it is, which is exactly what #257
// could not determine: `[429, 429, 429, 429]` means the customer's bucket
// already held hits before the test clicked anything, while more than four
// entries means the UI sent more requests than there were clicks. Either
// answer identifies the mechanism from a single failing run, rather than
// needing a temporary probe re-added and the suite run until it fails again.
test('says something useful once the allowance runs out', async ({
page,
customer,
accountModal
}) => {
const isResend = (url: string) =>
new URL(url).pathname === '/api/customers/resend-verification';
// Recorded from the first navigation onward, deliberately: a request the
// test did not make is one of the explanations still open, and a recorder
// installed after the clicks could not see it.
const statuses: number[] = [];
page.on('response', (res) => {
if (isResend(res.url())) statuses.push(res.status());
});
await accountModal.open();
const resend = accountModal.resendVerificationButton;
// Each click waits for its own response before the next. The previous
// `await expect(resend).toBeEnabled()` looked like pacing but was a no-op —
// the button is never disabled, so it returned immediately and left four
// requests racing. Waiting on the response removes concurrency as a
// variable, so a failure here cannot be explained by ordering.
for (let i = 0; i < 4; i++) {
await Promise.all([
page.waitForResponse((res) => isResend(res.url())),
resend.click()
]);
}
// The allowance is three an hour, so three sends and one refusal. The
// message names the customer because a bucket carrying hits from somewhere
// else is the open question, and the address is what identifies whose.
expect(statuses, `resend responses for ${customer.email}`).toEqual([204, 204, 204, 429]);
// The copy itself still matters — it is what this test is named for. Scoped
// with `.first()` because strict mode treats several identical toasts as
// ambiguous, and how many are still on screen is a timing detail this
// assertion should not depend on.
await expect(page.getByText(/already sent several/).first()).toBeVisible();
});
});