import { strToU8, zipSync } from 'fflate'; import { expect, test } from '@playwright/test'; import type { BrowserContext } from '@playwright/test'; import { contextForUser } from './helpers'; /** * Section-style plugins end to end (issue #75): install a `section_style` * fixture plugin, wrap editor content in one of its styles, and verify the * scoped CSS actually applies (computed background) in edit and read mode; * unwrap restores plain content; a disabled plugin leaves the section intact * but neutral. Selectors are language-neutral (classes, not labels) — the UI * language follows the user's locale. */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; const PLUGIN_ID = 'e2e-section-styles'; // An unmistakable probe color no app style uses. const BOXED_BACKGROUND = 'rgb(4, 5, 6)'; function sectionStyleZip(): Buffer { const manifest = { id: PLUGIN_ID, name: 'E2E Section Styles', version: '1.0.0', apiVersion: '1', kind: 'section_style', extensionPoints: [ { type: 'sectionStyle', id: 'boxed', title: { de: 'E2E-Box', en: 'E2E box' } }, ], license: 'MIT', }; const css = `.dt-style-${PLUGIN_ID}-boxed { background: ${BOXED_BACKGROUND}; padding: 4px; }`; return Buffer.from( zipSync({ 'manifest.json': strToU8(JSON.stringify(manifest)), 'styles.css': strToU8(css) }), ); } async function installAsRequired(context: BrowserContext): Promise { // Idempotent per run: demote + remove any previous installation. await context.request.patch(`/api/v1/admin/plugins/${PLUGIN_ID}/mode`, { data: { mode: 'disabled' }, }); await context.request.delete(`/api/v1/admin/plugins/${PLUGIN_ID}`); const installed = await context.request.post('/api/v1/admin/plugins', { multipart: { file: { name: `${PLUGIN_ID}.zip`, mimeType: 'application/zip', buffer: sectionStyleZip() }, }, }); expect(installed.status(), await installed.text()).toBe(201); const mode = await context.request.patch(`/api/v1/admin/plugins/${PLUGIN_ID}/mode`, { data: { mode: 'required' }, }); expect(mode.status(), await mode.text()).toBe(200); } async function createPage( context: BrowserContext, 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 }; } test('wrap, restyle in read mode, unwrap, and neutral fallback when disabled', async ({ browser, }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-admin'); await installAsRequired(context); const { pondSlug, pageSlug } = await createPage(context, `E2E Sections ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); 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('Boxed content'); // Wrap the paragraph in the plugin's style via the toolbar picker. const picker = page.locator('.editor-toolbar__section-select'); await picker.selectOption(`${PLUGIN_ID}/boxed`); const section = content.locator(`.dt-section.dt-style-${PLUGIN_ID}-boxed`); await expect(section).toContainText('Boxed content'); // The plugin stylesheet is linked and its scoped rule applies. await expect(section).toHaveCSS('background-color', BOXED_BACKGROUND); await expect(picker).toHaveValue(`${PLUGIN_ID}/boxed`); // Read mode renders the same styled section. await page.getByRole('button', { name: /view|ansicht|lesen/i }).click(); await expect(section).toHaveCSS('background-color', BOXED_BACKGROUND); // Unwrap leaves the text in place, without the section wrapper. await page.getByRole('button', { name: /edit|bearbeiten/i }).click(); await content.locator('p', { hasText: 'Boxed content' }).click(); await page.locator('.editor-toolbar__section-menu button').click(); await expect(content).toContainText('Boxed content'); await expect(section).toHaveCount(0); // Re-wrap, then disable the plugin: the section node survives with neutral // styling (content intact, class matches no stylesheet). await picker.selectOption(`${PLUGIN_ID}/boxed`); await expect(section).toHaveCSS('background-color', BOXED_BACKGROUND); const disabled = await context.request.patch(`/api/v1/admin/plugins/${PLUGIN_ID}/mode`, { data: { mode: 'disabled' }, }); expect(disabled.status()).toBe(200); await page.reload(); await expect(content.locator(`.dt-section.dt-style-${PLUGIN_ID}-boxed`)).toContainText( 'Boxed content', ); await expect(content.locator(`.dt-section.dt-style-${PLUGIN_ID}-boxed`)).not.toHaveCSS( 'background-color', BOXED_BACKGROUND, ); await context.request.delete(`/api/v1/admin/plugins/${PLUGIN_ID}`); });