From 6863a4437faf2cb23ff16963d2e9cc356e8a2d4c Mon Sep 17 00:00:00 2001 From: Sparky <1609870+sparkyfen@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:52:53 -0700 Subject: [PATCH 1/2] test(e2e): wait for both upload responses before asserting the sticker batch (SONA-216) The sticker pack form uploads a batch one file at a time and sets its error state as soon as a file's 422 returns, so the refusal banner renders while the next file's POST is still in flight. The mixed-batch test read the collected /api/upload statuses right after the banner appeared, so under load it saw [422] before the good file's 200 arrived. It failed twice on main's post-merge CI and intermittently in full upload-project runs, while passing alone. Poll for both responses before comparing statuses. Test-only change; the form behaves the same. The two neighbouring pack tests need no change: the refused-only batch has a single file, so its banner cannot precede the last response, and the all-good batch waits for both sticker rows, which only render after both responses. --- tests/e2e/upload.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/e2e/upload.spec.ts b/tests/e2e/upload.spec.ts index 6fdb6d0a..e73f16c3 100644 --- a/tests/e2e/upload.spec.ts +++ b/tests/e2e/upload.spec.ts @@ -1056,6 +1056,14 @@ test('the sticker pack form names the refusal and keeps the good file in the bat await expect(banner).toContainText('e2e-unscrubbable.png'); await expect(banner).toContainText('Export a fresh copy from an image editor'); + // The form uploads one file at a time and renders the banner as soon as the + // refusal comes back, so the banner above can appear while the good file's + // POST is still in flight. Wait for both responses before reading statuses. + // At least two rather than exactly two: a response from a superseded staging + // attempt can land after the reset, and the sorted comparison below names + // what was actually seen instead of timing out here. + await expect.poll(() => statuses.length, { timeout: 10_000 }).toBeGreaterThanOrEqual(2); + // The statuses the server really answered with — one refusal, one success. // Order varies with staging order, so compare sorted. expect([...statuses].sort((a, b) => a - b)).toEqual([200, 422]); From 28e687ac26e257243aa73f20695767c2db7172b6 Mon Sep 17 00:00:00 2001 From: Sparky <1609870+sparkyfen@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:16:06 -0700 Subject: [PATCH 2/2] test(e2e): count only the active staging attempt's upload responses (SONA-216) A hydration retry re-stages the batch and resets the collected statuses, so a late response from the superseded attempt could stand in for one of the current batch's two. Responses are now tagged with the attempt that issued their request, and the poll asks for exactly two. --- tests/e2e/upload.spec.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/e2e/upload.spec.ts b/tests/e2e/upload.spec.ts index e73f16c3..904a0663 100644 --- a/tests/e2e/upload.spec.ts +++ b/tests/e2e/upload.spec.ts @@ -1029,9 +1029,18 @@ test('the sticker pack form names the refusal and keeps the good file in the bat test.setTimeout(60_000); await adminLogin(page, PASSWORD); + // Responses are counted against the staging attempt that issued their + // request: stagePackFiles may re-stage on a hydration retry, and a late + // response from the superseded attempt must not stand in for one of the + // current batch's two. const statuses: number[] = []; + let attempt = 0; + const attemptOf = new WeakMap(); + page.on('request', (r) => { + if (r.method() === 'POST' && new URL(r.url()).pathname === '/api/upload') attemptOf.set(r, attempt); + }); page.on('response', (r) => { - if (new URL(r.url()).pathname === '/api/upload') statuses.push(r.status()); + if (attemptOf.get(r.request()) === attempt) statuses.push(r.status()); }); const uploads = countUploadPosts(page); @@ -1044,6 +1053,7 @@ test('the sticker pack form names the refusal and keeps the good file in the bat ], uploads, () => { + attempt++; statuses.length = 0; } ); @@ -1058,11 +1068,9 @@ test('the sticker pack form names the refusal and keeps the good file in the bat // The form uploads one file at a time and renders the banner as soon as the // refusal comes back, so the banner above can appear while the good file's - // POST is still in flight. Wait for both responses before reading statuses. - // At least two rather than exactly two: a response from a superseded staging - // attempt can land after the reset, and the sorted comparison below names - // what was actually seen instead of timing out here. - await expect.poll(() => statuses.length, { timeout: 10_000 }).toBeGreaterThanOrEqual(2); + // POST is still in flight. Wait for both of this attempt's responses before + // reading statuses. + await expect.poll(() => statuses.length, { timeout: 10_000 }).toBe(2); // The statuses the server really answered with — one refusal, one success. // Order varies with staging order, so compare sorted.