Java's third mode button, the sidebar's text and structure field (dropped with its middle under the pointer), items over the network that are moved, resized by Java's rule, typed into on their Y.Text, coloured and set in size and style from Java's menu, deleted; Java's paint per mode, measured in F4.8a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
10 KiB
TypeScript
170 lines
10 KiB
TypeScript
// 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<void> };
|
||
|
||
test.beforeAll(async () => {
|
||
server = await serveEditor();
|
||
});
|
||
|
||
test.afterAll(async () => {
|
||
await server.close();
|
||
});
|
||
|
||
const newDoc = async (page: Page, before: ReadonlySet<string>) => {
|
||
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);
|
||
});
|