import { expect, test } from '@playwright/test'; import type { Page } from '@playwright/test'; import { contextForUser } from './helpers'; /** * Link editing pack (issue #29). Runs against the local dev stack (api + * web); no Mailpit needed. */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; async function createPage( context: Awaited>, title: string, ): Promise<{ pondSlug: string; pageSlug: string }> { const ponds = await context.request.get('/api/v1/ponds'); const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal'); const created = await context.request.post(`/api/v1/ponds/${pond.id}/pages`, { data: { title }, }); const page = await created.json(); return { pondSlug: pond.slug, pageSlug: page.slug }; } async function enterEditModeWithText(page: Page, text: string): Promise { await page.getByRole('button', { name: /edit|bearbeiten/i }).click(); const content = page.locator('.ProseMirror'); await expect(content).toHaveAttribute('contenteditable', 'true'); await content.click(); await page.keyboard.type(text); await page.keyboard.press('ControlOrMeta+a'); } test('toolbar link button links the selection', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link Toolbar ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'dorfteich docs'); await page.getByRole('button', { name: /add link|link hinzufügen/i }).click(); await page.getByLabel(/link url|link-url/i).fill('https://example.org'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await expect(page.locator('.ProseMirror a[href="https://example.org"]')).toHaveText( 'dorfteich docs', ); await context.close(); }); test('rejects invalid link protocols with a localized hint', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link Invalid ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'danger zone'); await page.getByRole('button', { name: /add link|link hinzufügen/i }).click(); await page.getByLabel(/link url|link-url/i).fill('javascript:alert(1)'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await expect(page.locator('.editor-link-form__error')).toBeVisible(); await expect(page.locator('.ProseMirror a')).toHaveCount(0); await context.close(); }); test('Mod-k edits the URL of an existing link', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link ModK ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'edit me'); await page.getByRole('button', { name: /add link|link hinzufügen/i }).click(); await page.getByLabel(/link url|link-url/i).fill('https://example.org/old'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await expect(page.locator('.ProseMirror a[href="https://example.org/old"]')).toBeVisible(); // Select the link text again, then edit it via the keyboard shortcut. await page.keyboard.press('ControlOrMeta+a'); await page.keyboard.press('ControlOrMeta+k'); const urlInput = page.getByLabel(/link url|link-url/i); await expect(urlInput).toHaveValue('https://example.org/old'); await urlInput.fill('https://example.org/new'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await expect(page.locator('.ProseMirror a[href="https://example.org/new"]')).toBeVisible(); await context.close(); }); test('bubble menu removes a link', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link Remove ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'remove me'); await page.getByRole('button', { name: /add link|link hinzufügen/i }).click(); await page.getByLabel(/link url|link-url/i).fill('https://example.org'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await expect(page.locator('.ProseMirror a')).toBeVisible(); await page.locator('.ProseMirror a').click(); await page.getByRole('button', { name: /remove link|link entfernen/i }).click(); await expect(page.locator('.ProseMirror a')).toHaveCount(0); await context.close(); }); test('bubble menu "open in new tab" opens the link href', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link OpenTab ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'open me'); await page.getByRole('button', { name: /add link|link hinzufügen/i }).click(); await page.getByLabel(/link url|link-url/i).fill('https://example.org/open-me'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await page.locator('.ProseMirror a').click(); const [popup] = await Promise.all([ context.waitForEvent('page'), page.getByRole('button', { name: /open in new tab|in neuem tab öffnen/i }).click(), ]); await popup.waitForLoadState(); expect(popup.url()).toBe('https://example.org/open-me'); await context.close(); }); test('read mode links open in a new tab by default', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link ReadMode ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'read mode link'); await page.getByRole('button', { name: /add link|link hinzufügen/i }).click(); await page.getByLabel(/link url|link-url/i).fill('https://example.org'); await page.getByRole('button', { name: /apply|übernehmen/i }).click(); await page.getByRole('button', { name: /read|lesen/i }).click(); await expect(page.locator('.ProseMirror a')).toHaveAttribute('target', '_blank'); await context.close(); }); test('pasting a URL over a selection links the selected text', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Link Paste ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditModeWithText(page, 'wrap this'); await page.evaluate(() => { const el = document.querySelector('.ProseMirror'); const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', 'https://example.org/pasted'); el!.dispatchEvent( new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true }), ); }); await expect(page.locator('.ProseMirror a[href="https://example.org/pasted"]')).toHaveText( 'wrap this', ); await context.close(); });