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
|
// @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
|
// /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,
|
// Owner identity (doc-review P1): the harness authenticates as GRAV_TEST_USER,
|
||||||
// but committed site.yaml sets owner_username: mischa, and EntryScopeGuard is a
|
// 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' } });
|
r = await page.request.post(url, { data: { published: 'false' } });
|
||||||
expect(r.status()).toBe(400);
|
expect(r.status()).toBe(400);
|
||||||
expect(readTripPublished(slug)).toBe('true');
|
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 ───────────────────────
|
// ── 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);
|
await expect(page.locator('.home-predeparture-divider')).toHaveCount(0);
|
||||||
}).toPass({ timeout: 15_000 });
|
}).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