All checks were successful
CD / Build and push images (push) Successful in 2m2s
CI / Lint, typecheck, test (push) Successful in 1m41s
CI / Auth e2e pack (push) Successful in 1m49s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m9s
CD / Promote to Int (push) Successful in 9s
A bubble menu on link selection offers "edit URL", "open in new tab", and "remove link"; Mod-k opens the same editor for the current selection (creating a link if there isn't one yet), and the toolbar button does the same. Invalid protocols (e.g. javascript:) show a localized inline error instead of silently no-oping. Pasting a URL over selected text links it instead of replacing the text. Links always render with target="_blank" so read mode opens them in a new tab by default; edit mode suppresses the resulting navigate-on- click (Mod-click still follows it), since a plain click there should place the cursor instead. Closes #29
177 lines
7.4 KiB
TypeScript
177 lines
7.4 KiB
TypeScript
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<ReturnType<typeof contextForUser>>,
|
|
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<void> {
|
|
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();
|
|
});
|