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
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>
90 lines
3.8 KiB
TypeScript
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();
|
|
});
|