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 { 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(); });