test(trip): cover publish-toggle failure/lock paths + missing-key 400
Add the three cases the code review flagged as uncovered: - TP7 (R15): a failed POST reverts the switch and surfaces the visible toast. - TP8 (R13): the in-flight lock suppresses a concurrent second submit (exactly one POST fires while the switch is aria-busy/disabled). - TP5 leg: a MISSING published key -> 400 (the array_key_exists branch, distinct from the is_bool branch already covered). All 10 trip-publish specs green (serial, worktree container). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// @ts-check
|
||||
// Tests: TP1, TP1b, TP2–TP6 — the owner trip publish/unpublish toggle on the
|
||||
// Tests: TP1, TP1b, TP2–TP8 — the owner trip publish/unpublish toggle on the
|
||||
// /trips listing (U7). Covers the owner gate, coverless drafts, cache-correct
|
||||
// hide/restore, the active-trip confirm, backend authz, and the home fallback.
|
||||
// hide/restore, the active-trip confirm, backend authz, the home fallback, the
|
||||
// client failure/toast path (R15), and the in-flight double-submit lock (R13).
|
||||
//
|
||||
// Owner identity (doc-review P1): the harness authenticates as GRAV_TEST_USER,
|
||||
// but committed site.yaml sets owner_username: mischa, and EntryScopeGuard is a
|
||||
@@ -252,6 +253,12 @@ test('TP5: publish endpoint enforces 401/403 and rejects a non-boolean body', as
|
||||
r = await page.request.post(url, { data: { published: 'false' } });
|
||||
expect(r.status()).toBe(400);
|
||||
expect(readTripPublished(slug)).toBe('true');
|
||||
|
||||
// Owner, MISSING published key → 400 (the array_key_exists branch, distinct
|
||||
// from the is_bool branch above), frontmatter unchanged.
|
||||
r = await page.request.post(url, { data: {} });
|
||||
expect(r.status()).toBe(400);
|
||||
expect(readTripPublished(slug)).toBe('true');
|
||||
});
|
||||
|
||||
// ── TP6: an unpublished active trip makes home fall back ───────────────────────
|
||||
@@ -282,3 +289,65 @@ test('TP6: an unpublished active trip falls back to between-trips on home', asyn
|
||||
await expect(page.locator('.home-predeparture-divider')).toHaveCount(0);
|
||||
}).toPass({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
// ── TP7: a failed publish reverts the switch and surfaces a visible toast ──────
|
||||
test('TP7: a failed publish reverts the switch and shows a toast (R15)', async ({ page }) => {
|
||||
const slug = `tp7-${Date.now()}`;
|
||||
createFixtureTrip(slug, { published: true });
|
||||
|
||||
await page.goto('/trips');
|
||||
const toggle = toggleFor(page, slug);
|
||||
await expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
// Force the mutation to fail server-side; the request is intercepted so it
|
||||
// never reaches the endpoint (a generic 5xx → generic "couldn't update" copy).
|
||||
await page.route('**/api/v1/trip/*/publish', (route) =>
|
||||
route.fulfill({ status: 500, contentType: 'application/json', body: '{}' }));
|
||||
|
||||
await toggle.click();
|
||||
|
||||
// The switch never flipped (the optimistic flip only happens on success), so
|
||||
// "revert" is just re-enabling it; the visible page-level toast appears.
|
||||
await expect(page.locator('#trip-publish-live')).toBeVisible();
|
||||
await expect(page.locator('#trip-publish-live')).toContainText("Couldn't update");
|
||||
await expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
await expect(toggle).toBeEnabled();
|
||||
// Never persisted (the request was intercepted before the server).
|
||||
expect(readTripPublished(slug)).toBe('true');
|
||||
|
||||
await page.unroute('**/api/v1/trip/*/publish');
|
||||
});
|
||||
|
||||
// ── TP8: the in-flight lock suppresses a concurrent second submit ──────────────
|
||||
test('TP8: the pending lock suppresses a concurrent second submit (R13)', async ({ page }) => {
|
||||
const slug = `tp8-${Date.now()}`;
|
||||
createFixtureTrip(slug, { published: true });
|
||||
|
||||
await page.goto('/trips');
|
||||
const toggle = toggleFor(page, slug);
|
||||
await expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
// Count and DELAY the mutation so the switch stays in-flight while we click
|
||||
// again. Fulfilled locally (204), so the server/disk is never touched.
|
||||
let posts = 0;
|
||||
await page.route('**/api/v1/trip/*/publish', async (route) => {
|
||||
posts += 1;
|
||||
await new Promise((r) => setTimeout(r, 1_000));
|
||||
route.fulfill({ status: 204, body: '' });
|
||||
});
|
||||
|
||||
await toggle.click();
|
||||
// In flight: locked (aria-busy + disabled).
|
||||
await expect(toggle).toHaveAttribute('aria-busy', 'true');
|
||||
await expect(toggle).toBeDisabled();
|
||||
|
||||
// A second click during the in-flight window must NOT fire a second POST.
|
||||
await toggle.click({ force: true });
|
||||
|
||||
// First request settles → optimistic flip + unlock; exactly one POST fired.
|
||||
await expect(toggle).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(toggle).toBeEnabled();
|
||||
expect(posts).toBe(1);
|
||||
|
||||
await page.unroute('**/api/v1/trip/*/publish');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user