Guards the page-tree-index staleness fix. DEL1 only checked optimistic DOM removal + disk; DEL4 reloads and asserts the server no longer renders the card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
101 lines
5.2 KiB
JavaScript
101 lines
5.2 KiB
JavaScript
// @ts-check
|
||
// Tests: DEL1–DEL3 — the owner entry-delete flow (feed-actions.js + the
|
||
// entry-actions `deleteEntry` route). Delete is a two-step inline confirm on a
|
||
// feed card: Delete → Cancel / Confirm delete → DELETE /api/v1/entry/<slug>.
|
||
//
|
||
// This flow — a destructive, owner-only action — had zero automated coverage.
|
||
// - DEL1: full happy path — the card vanishes AND the folder leaves disk.
|
||
// - DEL2: Cancel is a real escape hatch — nothing is deleted.
|
||
// - DEL3: a failed DELETE keeps the card and surfaces the inline error.
|
||
const { test, expect } = require('@playwright/test');
|
||
const {
|
||
createPhotoEntry, cleanupEntry, findEntry, ACTIVE_TRIP_URL,
|
||
} = require('../helpers');
|
||
|
||
const created = [];
|
||
// cleanupEntry is a no-op when the entry was already deleted by the test.
|
||
test.afterAll(() => created.forEach(cleanupEntry));
|
||
|
||
// ── DEL1: happy delete removes the card and the folder ────────────────────────
|
||
test('DEL1: owner deletes an entry — the card disappears and the folder is removed', async ({ page }) => {
|
||
const tag = `del1-${Date.now()}`;
|
||
await createPhotoEntry(page, tag, { created, content: `Delete-flow fixture ${tag}. Safe to delete.` });
|
||
|
||
await page.goto(ACTIVE_TRIP_URL);
|
||
const card = page.locator('.journal-post', { hasText: tag });
|
||
await expect(card).toHaveCount(1);
|
||
// The owner-only controls must be present — this also asserts the auth gate.
|
||
await card.locator('[data-delete-start]').click();
|
||
await card.locator('[data-delete-confirm]').click();
|
||
|
||
await expect(page.locator('.journal-post', { hasText: tag }))
|
||
.toHaveCount(0, { timeout: 15_000 });
|
||
await expect.poll(() => findEntry(tag), { timeout: 15_000 }).toBeNull();
|
||
});
|
||
|
||
// ── DEL4: a deleted entry stays gone after a full page reload ─────────────────
|
||
// Regression for the page-tree-index staleness bug: deleteEntry did
|
||
// cache.deleteAll() but not Cache::invalidateCache(), so with
|
||
// cache.check.method: folder the deleted child lingered in the pages index and
|
||
// the SERVER re-rendered the (now image-less) card on the next load — even
|
||
// though its folder was gone from disk. DEL1 only checks the optimistic DOM
|
||
// removal + disk, so it missed this. Here we reload and assert the server no
|
||
// longer emits the card.
|
||
test('DEL4: a deleted entry is absent from the feed after a fresh page load', async ({ page }) => {
|
||
const tag = `del4-${Date.now()}`;
|
||
await createPhotoEntry(page, tag, { created, content: `Delete-flow fixture ${tag}. Safe to delete.` });
|
||
|
||
await page.goto(ACTIVE_TRIP_URL);
|
||
const card = page.locator('.journal-post', { hasText: tag });
|
||
await expect(card).toHaveCount(1);
|
||
await card.locator('[data-delete-start]').click();
|
||
await card.locator('[data-delete-confirm]').click();
|
||
await expect(page.locator('.journal-post', { hasText: tag })).toHaveCount(0, { timeout: 15_000 });
|
||
await expect.poll(() => findEntry(tag), { timeout: 15_000 }).toBeNull();
|
||
|
||
// The real test: a fresh server render must not resurrect the entry.
|
||
await page.goto(ACTIVE_TRIP_URL);
|
||
await expect(page.locator('.journal-post', { hasText: tag })).toHaveCount(0);
|
||
});
|
||
|
||
// ── DEL2: Cancel keeps the entry ──────────────────────────────────────────────
|
||
test('DEL2: cancelling the confirm step keeps the entry on the page and on disk', async ({ page }) => {
|
||
const tag = `del2-${Date.now()}`;
|
||
await createPhotoEntry(page, tag, { created, content: `Delete-flow fixture ${tag}. Safe to delete.` });
|
||
|
||
await page.goto(ACTIVE_TRIP_URL);
|
||
const card = page.locator('.journal-post', { hasText: tag });
|
||
await expect(card).toHaveCount(1);
|
||
|
||
await card.locator('[data-delete-start]').click();
|
||
await expect(card.locator('.entry-delete-confirm')).toBeVisible();
|
||
await card.locator('[data-delete-cancel]').click();
|
||
await expect(card.locator('.entry-delete-confirm')).toBeHidden();
|
||
|
||
await expect(card).toHaveCount(1);
|
||
expect(findEntry(tag), 'a cancelled delete must not remove the folder').not.toBeNull();
|
||
});
|
||
|
||
// ── DEL3: a failed DELETE keeps the card and shows the inline error ───────────
|
||
test('DEL3: a failed delete keeps the card and surfaces the inline error', async ({ page }) => {
|
||
const tag = `del3-${Date.now()}`;
|
||
await createPhotoEntry(page, tag, { created, content: `Delete-flow fixture ${tag}. Safe to delete.` });
|
||
|
||
await page.goto(ACTIVE_TRIP_URL);
|
||
// Force the delete request to fail after the confirm.
|
||
await page.route('**/api/v1/entry/**', (route) => {
|
||
if (route.request().method() === 'DELETE') return route.fulfill({ status: 500, body: '' });
|
||
return route.continue();
|
||
});
|
||
|
||
const card = page.locator('.journal-post', { hasText: tag });
|
||
await expect(card).toHaveCount(1);
|
||
await card.locator('[data-delete-start]').click();
|
||
await card.locator('[data-delete-confirm]').click();
|
||
|
||
await expect(card.locator('.entry-delete-msg'))
|
||
.toContainText('Could not delete', { timeout: 15_000 });
|
||
await expect(card).toHaveCount(1); // the card survives a failed delete
|
||
expect(findEntry(tag), 'a failed delete must not remove the folder').not.toBeNull();
|
||
});
|