import { expect, test } from '@playwright/test'; import type { Page } from '@playwright/test'; import { contextForUser } from './helpers'; /** * Markdown copy/paste/export pack (issue #30). 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; pageId: 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, pageId: page.id }; } async function enterEditMode(page: Page): Promise { await page.getByRole('button', { name: /edit|bearbeiten/i }).click(); await expect(page.locator('.ProseMirror')).toHaveAttribute('contenteditable', 'true'); } test('copying a heading+list selection yields Markdown on the clipboard', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); await context.grantPermissions(['clipboard-read', 'clipboard-write']); const { pondSlug, pageSlug } = await createPage(context, `E2E MD Copy ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); await page.locator('.ProseMirror').click(); await page.keyboard.type('# Title'); await page.keyboard.press('Enter'); await page.keyboard.type('one'); await page.keyboard.press('Enter'); await page.keyboard.type('two'); // Turn the second/third lines into a bullet list, then copy everything. await page.keyboard.press('Shift+Home'); await page.keyboard.press('Shift+ArrowUp'); await page.getByRole('button', { name: /bullet list|aufzählungsliste/i }).click(); await page.keyboard.press('ControlOrMeta+a'); await page.keyboard.press('ControlOrMeta+c'); const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toContain('- one'); expect(clipboardText).toContain('- two'); await context.close(); }); test('pasting a Markdown document into an empty page recreates structure', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E MD Paste ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); await page.locator('.ProseMirror').click(); await page.evaluate(() => { const el = document.querySelector('.ProseMirror'); const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', '# Welcome\n\n- alpha\n- beta\n\n1. first\n2. second\n'); el!.dispatchEvent( new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true }), ); }); const content = page.locator('.ProseMirror'); await expect(content.locator('h1')).toHaveText('Welcome'); await expect(content.locator('ul li')).toHaveCount(2); await expect(content.locator('ol li')).toHaveCount(2); await context.close(); }); test('a plain-text paste is not mangled into rich structure', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E MD Plain ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); await page.locator('.ProseMirror').click(); const plainText = "Just a *reminder* to buy milk - don't forget!"; await page.evaluate((text) => { const el = document.querySelector('.ProseMirror'); const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', text); el!.dispatchEvent( new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true }), ); }, plainText); const content = page.locator('.ProseMirror'); await expect(content).toContainText(plainText); await expect(content.locator('ul, ol, h1, h2, h3, h4')).toHaveCount(0); await context.close(); }); test('a Markdown table pasted with code-editor styling HTML becomes a table (issue #339)', async ({ browser, }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E MD TablePaste ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); await page.locator('.ProseMirror').click(); // VS Code (copyWithSyntaxHighlighting) ships the plain text a second time // as styled div/span HTML — exactly the flavor that used to shadow the // Markdown conversion. await page.evaluate(() => { const el = document.querySelector('.ProseMirror'); const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', '| A | B |\n| --- | --- |\n| 1 | 2 |'); dataTransfer.setData( 'text/html', '
' + '
| A | B |
' + '
| --- | --- |
' + '
| 1 | 2 |
', ); el!.dispatchEvent( new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true }), ); }); const content = page.locator('.ProseMirror'); await expect(content.locator('table')).toHaveCount(1); await expect(content.locator('th').first()).toHaveText('A'); await expect(content.locator('td').first()).toHaveText('1'); await context.close(); }); test('a Markdown table pasted into a code block stays verbatim text (issue #339)', async ({ browser, }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E MD CodePaste ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); await page.locator('.ProseMirror').click(); await page.getByRole('button', { name: /code block|codeblock/i }).click(); await page.evaluate(() => { const el = document.querySelector('.ProseMirror'); const dataTransfer = new DataTransfer(); dataTransfer.setData('text/plain', '| A | B |\n| --- | --- |\n| 1 | 2 |'); el!.dispatchEvent( new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true }), ); }); const content = page.locator('.ProseMirror'); await expect(content.locator('table')).toHaveCount(0); await expect(content.locator('pre')).toContainText('| A | B |'); await context.close(); }); test('typing a Markdown table header plus separator creates a table (issue #339)', async ({ browser, }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E MD TableType ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); const content = page.locator('.ProseMirror'); await content.click(); await page.keyboard.type('| Name | Rolle |'); await page.keyboard.press('Enter'); await page.keyboard.type('| --- | --- |'); await expect(content).toContainText('| --- | --- |'); await page.keyboard.press('Enter'); await expect(content.locator('table')).toHaveCount(1); await expect(content.locator('th').first()).toHaveText('Name'); await expect(content).not.toContainText('| --- | --- |'); // The cursor lands in the table; Tab from the last header cell appends the // first body row (#338), so typing continues seamlessly. await page.keyboard.type('x'); await expect(content.locator('th').first()).toContainText('x'); await context.close(); }); test('page menu downloads the page as Markdown matching its content', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug, pageId } = await createPage(context, `E2E MD Export ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await enterEditMode(page); await page.locator('.ProseMirror').click(); await page.keyboard.type('export me please'); // There is no "saved" status since #36 (collab autosaves continuously) — // poll the export endpoint until the typed content has been persisted. await expect .poll( async () => (await context.request.get(`/api/v1/pages/${pageId}/export/markdown`)).text(), { timeout: 10000 }, ) .toBe('export me please'); const exported = await context.request.get(`/api/v1/pages/${pageId}/export/markdown`); expect(exported.headers()['content-disposition']).toContain(`${pageSlug}.md`); expect(await exported.text()).toBe('export me please'); await context.close(); });