// The documentation mode in Chromium (F4.8b), on FILIUS' example // nat_with_mapped_port, which has documentation: Java's third mode button; // the sidebar's rectangle dragged onto the surface (its middle under the // pointer), moved, resized to Java's smallest size, coloured from Java's // list; the sidebar's text tapped into the view, typed into, made large and // italic; Java's paint in the documentation mode and in the design; Delete; // a simulation started ends the mode; and a phone. import { expect, test, type Page } from '@playwright/test'; import { drag, drawing, openEditor, serveEditor } from './editorPage.js'; let server: { readonly base: string; readonly close: () => Promise }; test.beforeAll(async () => { server = await serveEditor(); }); test.afterAll(async () => { await server.close(); }); const newDoc = async (page: Page, before: ReadonlySet) => { await expect.poll(async () => (await drawing(page)).docs.filter((item) => !before.has(item.id)).length).toBe(1); return (await drawing(page)).docs.find((item) => !before.has(item.id))!; }; const docOf = async (page: Page, id: string) => (await drawing(page)).docs.find((item) => item.id === id); const fill = (page: Page, id: string) => page.locator(`g[data-doc="${id}"] .doc-rectangle`).evaluate((rect) => getComputedStyle(rect).fill); async function longPress(page: Page, at: { x: number; y: number }) { await page.mouse.move(at.x, at.y); await page.mouse.down(); await page.waitForTimeout(700); await page.mouse.up(); } test('makes, moves, resizes, colours and types documentation as Java does, and paints it per mode', async ({ page }) => { test.setTimeout(120_000); await openEditor(page, server.base, 'nat_with_mapped_port.fls'); const canvas = page.locator('svg.filius-canvas'); const existing = new Set((await drawing(page)).docs.map((item) => item.id)); expect(existing.size).toBeGreaterThan(0); // Java's order: documentation, design, simulation. expect(await page.locator('.editor-toolbar button[name$="Mode"]').evaluateAll((buttons) => buttons.map((each) => each.getAttribute('name')))).toEqual([ 'documentationMode', 'designMode', 'simulationMode', ]); await page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' }).click(); await expect(canvas).toHaveAttribute('data-mode', 'documentation'); await expect(page.getByRole('navigation', { name: 'Komponenten' })).toHaveCount(0); const sidebar = page.getByRole('navigation', { name: 'Dokumentation' }); await expect(sidebar.getByRole('button')).toHaveText(['Text', 'Gliederungsfläche']); // A rectangle dragged onto the surface: 100 × 50, its middle where it was dropped, grey, with the focus. const surface = (await canvas.boundingBox())!; const drop = { x: surface.x + surface.width * 0.6, y: surface.y + surface.height * 0.3 }; const button = (await sidebar.getByRole('button', { name: 'Gliederungsfläche' }).boundingBox())!; await drag(page, { x: button.x + button.width / 2, y: button.y + button.height / 2 }, drop); const rectangle = await newDoc(page, existing); expect([rectangle.kind, rectangle.width, rectangle.height]).toEqual(['rectangle', 100, 50]); const focusBox = (await page.locator(`g[data-doc="${rectangle.id}"] .doc-focus`).boundingBox())!; expect(Math.abs(focusBox.x + focusBox.width / 2 - drop.x)).toBeLessThan(2); expect(Math.abs(focusBox.y + focusBox.height / 2 - drop.y)).toBeLessThan(2); // Java's paint in the documentation mode: grey at an opacity of 200. expect(await fill(page, rectangle.id)).toBe('rgba(204, 204, 204, 0.784)'); // Moved by its middle, resized by its corner to Java's smallest (20 × 20). await drag(page, drop, { x: drop.x + 40, y: drop.y + 30 }); await expect.poll(async () => (await docOf(page, rectangle.id))!.x - rectangle.x).toBeGreaterThan(20); const moved = (await docOf(page, rectangle.id))!; const corner = (await page.locator(`g[data-doc="${rectangle.id}"] .doc-focus`).boundingBox())!; await drag(page, { x: corner.x + corner.width - 2, y: corner.y + corner.height - 2 }, { x: corner.x - 50, y: corner.y - 50 }); await expect .poll(async () => { const item = (await docOf(page, rectangle.id))!; return [item.x, item.y, item.width, item.height]; }) .toEqual([moved.x, moved.y, 20, 20]); // Its menu: only the colour (Java's twelve) and „Löschen“; a colour chosen. const small = (await page.locator(`g[data-doc="${rectangle.id}"] .doc-focus`).boundingBox())!; await longPress(page, { x: small.x + small.width / 2, y: small.y + small.height / 2 }); const menu = page.getByRole('menu'); await expect(menu.getByRole('group')).toHaveCount(1); await expect(menu.getByRole('group', { name: 'Farbe' }).getByRole('menuitemradio')).toHaveCount(12); await expect(menu.getByRole('menuitemradio', { name: 'Farbe 2' })).toHaveAttribute('aria-checked', 'true'); await menu.getByRole('menuitemradio', { name: 'Farbe 5' }).click(); await expect.poll(async () => (await docOf(page, rectangle.id))!.color).toEqual({ red: 1, green: 201 / 255, blue: 201 / 255, alpha: 1 }); // A text tapped from the sidebar: in the middle of the view, typed into at once. const withRectangle = new Set((await drawing(page)).docs.map((item) => item.id)); await sidebar.getByRole('button', { name: 'Text' }).click(); const text = await newDoc(page, withRectangle); const field = page.getByRole('textbox', { name: 'Text' }); await expect(field).toBeFocused(); await page.keyboard.type('Netz A'); await page.keyboard.press('Enter'); await page.keyboard.type('zweite Zeile'); await expect.poll(async () => (await docOf(page, text.id))!.text).toBe('Netz A\nzweite Zeile'); // Its menu: size, style, Java's fifteen colours; „Groß“, then „Kursiv“. const textFocus = (await page.locator(`g[data-doc="${text.id}"] .doc-focus`).boundingBox())!; const band = { x: textFocus.x + textFocus.width / 2, y: textFocus.y + textFocus.height - 8 }; await longPress(page, band); await expect(menu.getByRole('group')).toHaveText([/^Schriftgröße/, /^Schriftstil/, /^Farbe/]); await expect(menu.getByRole('group', { name: 'Farbe' }).getByRole('menuitemradio')).toHaveCount(15); await expect(menu.getByRole('group', { name: 'Schriftgröße' }).getByRole('menuitemradio', { checked: true })).toHaveAccessibleName('Normal'); await menu.getByRole('menuitemradio', { name: 'Groß', exact: true }).click(); await longPress(page, band); await menu.getByRole('group', { name: 'Schriftstil' }).getByRole('menuitemradio', { name: 'Kursiv' }).click(); await expect .poll(async () => { const item = (await docOf(page, text.id))!; return [item.size, item.italic, item.bold]; }) .toEqual([18, true, false]); await expect(field).toHaveCSS('font-style', 'italic'); // Escape ends the focus: the text drawn, with Java's grey border in the documentation mode. await field.press('Escape'); await expect(field).toHaveCount(0); const words = page.locator(`g[data-doc="${text.id}"] .doc-text`); await expect(words).toHaveText('Netz A\nzweite Zeile'); await expect(words).toHaveCSS('border-top-color', 'rgb(179, 179, 179)'); await expect(words).toHaveCSS('font-size', '18px'); await expect(words).toHaveCSS('font-style', 'italic'); // A rectangle without the focus is moved at once (Java gives it the focus with the press). const before = (await docOf(page, rectangle.id))!; const unfocused = (await page.locator(`g[data-doc="${rectangle.id}"] .doc-rectangle`).boundingBox())!; await expect(page.locator(`g[data-doc="${rectangle.id}"]`)).not.toHaveAttribute('data-focused', 'true'); await drag(page, { x: unfocused.x + unfocused.width / 2, y: unfocused.y + unfocused.height / 2 }, { x: unfocused.x + unfocused.width / 2 + 30, y: unfocused.y + unfocused.height / 2 }); await expect.poll(async () => (await docOf(page, rectangle.id))!.x - before.x).toBeGreaterThan(10); // The design: the rectangle opaque, a fifth of the way to white, below the devices; the text without its border; documentation is not chosen. await page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click(); await expect(canvas).toHaveAttribute('data-mode', 'design'); expect(await fill(page, rectangle.id)).toBe('rgb(255, 212, 212)'); await expect(page.locator(`g[data-doc="${rectangle.id}"] .doc-rectangle`)).toHaveCSS('stroke', 'rgb(204, 160, 160)'); await expect(words).toHaveCSS('border-top-style', 'none'); const wordsBox = (await words.boundingBox())!; await page.mouse.click(wordsBox.x + 4, wordsBox.y + 4); await expect(page.locator('[data-focused="true"]')).toHaveCount(0); await expect(page.getByRole('navigation', { name: 'Komponenten' })).toBeVisible(); // Back in the documentation mode: Delete removes the focused rectangle. await page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' }).click(); const rectangleBox = (await page.locator(`g[data-doc="${rectangle.id}"] .doc-rectangle`).boundingBox())!; await page.mouse.click(rectangleBox.x + rectangleBox.width / 2, rectangleBox.y + rectangleBox.height / 2); await expect(page.locator(`g[data-doc="${rectangle.id}"]`)).toHaveAttribute('data-focused', 'true'); await page.keyboard.press('Delete'); await expect.poll(async () => (await docOf(page, rectangle.id)) === undefined).toBe(true); // A simulation started ends the documentation mode, as Java's does. await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click(); await expect(canvas).toHaveAttribute('data-mode', 'simulation'); await expect(sidebar).toHaveCount(0); await page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' }).click(); await expect(canvas).toHaveAttribute('data-mode', 'documentation'); await expect(page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' })).toHaveAttribute('aria-pressed', 'true'); }); test('keeps the documentation mode within reach on a phone', async ({ page }) => { await page.setViewportSize({ width: 360, height: 740 }); await openEditor(page, server.base, 'nat_with_mapped_port.fls'); await page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' }).click(); await expect(page.getByRole('button', { name: 'Gliederungsfläche' })).toBeInViewport({ ratio: 1 }); const rights = await page.locator('.editor-toolbar > *').evaluateAll((parts) => parts.map((part) => part.getBoundingClientRect().right)); expect(Math.max(...rights)).toBeLessThanOrEqual(360); });