dorfteich/apps/web/e2e/create-missing-page.spec.ts
Claude Fable 5 64e21e9f94
Some checks failed
CD / Build and push images (push) Successful in 4m20s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Failing after 4m38s
CI / Auth e2e pack (push) Has been skipped
CI / Import/export fidelity gate (push) Has been skipped
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m20s
CD / Promote to Int (push) Has been cancelled
Offer creating the page on the not-found screen (#115)
Following a phantom wikilink now ends with a way out instead of a dead
end: when the pond resolved and the page 404s as plain not_found, the
error screen offers creating the page in place. Title = the URL slug
(the PhantomPagesView mechanic), so every wikilink pointing at the
address resolves; the invalidated page query then mounts the editor on
the same URL. The affordance is deliberately ungated like the sidebar's
new-page button — the client cannot tell 'never existed' from 'not
readable' (#60), and a reader's POST surfaces as the regular 403
banner. The page_trashed branch (#31) is untouched.

Rides along: PhantomPagesView now also invalidates ['pond-links'] —
the graph views kept showing a just-created target as a phantom.

e2e pack create-missing-page.spec.ts (CI wiring lands with #119):
author a phantom link, follow it, create, backlink proves resolution;
reader path asserts the 403 banner and no editor mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:45:27 +02:00

90 lines
3.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { contextForUser } from './helpers';
/**
* Create-from-not-found pack (issue #115): following a phantom wikilink dead-
* ends on the not-found screen, which offers creating the page in place —
* title = the slug, so every link pointing at the address resolves (#47).
* Readers hit the 403 banner instead (the affordance is ungated by design).
* Wired into CI with the vault-import pack (#119).
*/
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
async function insertWikilink(page: import('@playwright/test').Page, query: string): Promise<void> {
const body = page.locator('.editor-content .ProseMirror');
await body.click();
await page.keyboard.type(`[[${query}`);
await expect(page.locator('.wikilink-suggest')).toBeVisible();
await page.keyboard.press('Enter');
}
test('not-found screen creates the missing page in place', async ({ browser }) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const ponds = await context.request.get('/api/v1/ponds');
const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal');
const ts = Date.now();
const source = await (
await context.request.post(`/api/v1/ponds/${pond.id}/pages`, {
data: { title: `NF Source ${ts}` },
})
).json();
const page = await context.newPage();
// Author a phantom wikilink, then follow it in read mode.
await page.goto(`/p/${pond.slug}/${source.slug}`);
await page.locator('.editor-page__mode-toggle').click();
await insertWikilink(page, `nf-ghost-${ts}`);
await page.locator('.editor-page__mode-toggle').click(); // back to read mode
await page.locator('.editor-content a.wikilink', { hasText: `nf-ghost-${ts}` }).click();
// Dead end shows the message AND the way out.
await expect(page).toHaveURL(new RegExp(`/p/${pond.slug}/nf-ghost-${ts}$`));
await expect(page.locator('.form-banner--error, [role="alert"]').first()).toBeVisible();
const createButton = page.locator('.create-missing-page__button');
await expect(createButton).toBeVisible();
// Create → the editor mounts on the same URL.
await createButton.click();
await expect(page.locator('.editor-content .ProseMirror')).toBeVisible();
await expect(page).toHaveURL(new RegExp(`/p/${pond.slug}/nf-ghost-${ts}$`));
// The source's backlink panel proves the link resolved (id-based).
await page.reload();
await expect(page.locator('.backlinks')).toBeVisible();
await expect(page.locator('.backlinks__link', { hasText: `NF Source ${ts}` })).toBeVisible();
await context.close();
});
test('a viewer without edit rights hits the 403 banner instead', async ({ browser }) => {
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
const viewer = await contextForUser(browser, BASE_URL, 'fixture-viewer');
const viewerId = (await (await viewer.request.get('/api/v1/auth/me')).json()).id as string;
const ts = Date.now();
const pond = await (
await owner.request.post('/api/v1/ponds', { data: { name: `NF Pond ${ts}` } })
).json();
await owner.request.post(`/api/v1/ponds/${pond.id}/grants`, {
data: {
subjectType: 'user',
subjectId: viewerId,
role: 'reader',
scopeType: 'pond',
effect: 'allow',
},
});
const page = await viewer.newPage();
await page.goto(`/p/${pond.slug}/does-not-exist-${ts}`);
const createButton = page.locator('.create-missing-page__button');
await expect(createButton).toBeVisible();
await createButton.click();
// The POST 403s for a reader; the banner appears, no editor mounts.
await expect(page.locator('.create-missing-page .form-banner--error')).toBeVisible();
await expect(page.locator('.editor-content .ProseMirror')).toHaveCount(0);
await owner.close();
await viewer.close();
});