feat(editor): the simulation opens sheets, not windows in the window

A tap on a device in the simulation now only chooses it; its three-dot
button (or a long press) opens what Java's click opened: the desktop, and
newly a switch's SAT table and a home router's NAT table, beside each
card's data exchange.

The desktop, the tables and the data exchange are no windows in the
window any more but the simulation's sheet (SimulationSheet), after the
dashboard's AppSheet: below the toolbar to the bottom, a head with icon,
title, who else has it open, see-through and close. One at a time — it
covers the surface. It stays over a return to the design with the program
open on a desktop; a sheet whose device was removed closes. See-through
fades the body (0.55) and keeps the head; the choice is kept in the
browser. Java opens these as movable windows, several at once, with a
click; named in CHANGES.md.

Tests open through the menu (showFromMenu, showDesktop in editorPage.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 17:18:25 +00:00
co-authored by Claude Opus 5.5
parent 1593726447
commit 16d9cc2095
26 changed files with 585 additions and 497 deletions
+22 -21
View File
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, serveEditor } from './editorPage.js';
import { drawing, onDevice, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -62,20 +62,18 @@ test("shows a card's frames as Java's data exchange window does, with their deta
await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10']);
await expect(window.locator('th')).toHaveText(['Nr.', 'Zeit', 'Quelle', 'Ziel', 'Protokoll', 'Schicht', 'Bemerkungen / Details']);
await expect(window.locator('tbody tr')).toHaveCount(0);
// Closed, it keeps its tab (Java hides its dialog); it lies over the devices.
await window.locator('.desktop-close').click();
// Closed, it keeps its tab (Java hides its dialog).
await window.locator('.simulation-sheet__close').click();
await expect(window).toHaveCount(0);
// The browser fetches the server's page: a row per frame, its highest layer's, in its colour.
const at = await onDevice(page, await idOf(page, 'Browser'));
await page.mouse.click(at.x, at.y);
const desktop = page.locator('section[data-desktop]');
const desktop = await showDesktop(page, await idOf(page, 'Browser'));
await desktop.locator('[data-program="webBrowser"]').click();
const address = desktop.getByRole('textbox', { name: 'Adresse' });
await address.fill('192.168.0.20');
await address.press('Enter');
await expect(desktop.locator('.program-titlebar h3')).toHaveText('Seiten & Formulare', { timeout: 30_000 });
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
await (await menuOf(page, 'Browser')).filter({ hasText: 'Datenaustausch' }).click();
await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10']);
const rows = window.locator('tbody tr');
@@ -135,7 +133,7 @@ test("shows a card's frames as Java's data exchange window does, with their deta
expect(text.match(/^\+-/gm)?.length).toBe(await rows.count());
// A router's cards: one tab each, the one chosen in front.
await window.locator('.desktop-close').click();
await window.locator('.simulation-sheet__close').click();
await expect(await menuOf(page, 'Router')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']);
await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (192.168.0.1)' }).click();
await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10', 'Router - 192.168.0.1']);
@@ -153,13 +151,12 @@ test("shows a card's frames as Java's data exchange window does, with their deta
// Without "Automatisches Scrollen", fetched again (its tab kept over the window closed): the table stays at its start, nothing chosen.
await window.getByLabel('Automatisches Scrollen').uncheck();
await window.locator('.desktop-close').click();
await page.mouse.click(at.x, at.y);
await showDesktop(page, await idOf(page, 'Browser'));
await desktop.locator('[data-program="webBrowser"]').click();
await address.fill('192.168.0.20');
await address.press('Enter');
await page.waitForTimeout(6_000);
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
await (await menuOf(page, 'Browser')).filter({ hasText: 'Datenaustausch' }).click();
await expect(window.getByLabel('Automatisches Scrollen')).not.toBeChecked();
await expect.poll(() => rows.count()).toBeGreaterThan(15);
@@ -168,7 +165,7 @@ test("shows a card's frames as Java's data exchange window does, with their deta
expect(await scrolled()).toMatchObject({ overflows: true, top: 0 });
// A third tab; a tab closed beside the chosen one leaves it chosen.
await window.locator('.desktop-close').click();
await window.locator('.simulation-sheet__close').click();
await (await menuOf(page, 'Server')).filter({ hasText: 'Datenaustausch' }).click();
await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10', 'Router - 192.168.0.1', 'Server - 192.168.0.20']);
await window.getByRole('tab', { name: 'Browser - 192.168.0.10' }).click();
@@ -190,12 +187,12 @@ test("shows a card's frames as Java's data exchange window does, with their deta
await expect(window).toHaveCount(0);
});
test('chooses a device with a tap in the simulation, its three-dot button as in the design — only where it has a menu', async ({ page }) => {
test('chooses a device with a tap in the simulation, its three-dot button as in the design, and opens nothing by itself', async ({ page }) => {
await openPages(page);
await start(page);
const button = page.locator('.device-menu-button');
const sheet = page.locator('aside[aria-label="Einstellungen"]');
// A router: Java's tap does nothing else; the button opens the same menu as the long press.
// A router: the button opens the same menu as the long press.
const router = await onDevice(page, await idOf(page, 'Router'));
await page.mouse.click(router.x, router.y);
await expect(page.locator(`g.device[data-node="${await idOf(page, 'Router')}"]`)).toHaveAttribute('data-selected', 'true');
@@ -203,25 +200,29 @@ test('chooses a device with a tap in the simulation, its three-dot button as in
await expect(page.getByRole('menuitem')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']);
await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (10.0.0.1)' }).click();
await expect(page.locator('section[data-capture]').getByRole('tab', { selected: true })).toHaveText('Router - 10.0.0.1');
await page.locator('section[data-capture] .desktop-close').click();
// A computer: the tap still opens its desktop (Java), and chooses it; its settings stay closed, the design is locked.
await page.locator('section[data-capture] .simulation-sheet__close').click();
// A computer: the tap opens no desktop (Java's click does), it only chooses; its settings stay closed, the design is locked.
const browser = await idOf(page, 'Browser');
const at = await onDevice(page, browser);
await page.mouse.click(at.x, at.y);
await expect(page.locator(`section[data-desktop="${browser}"]`)).toBeVisible();
await expect(button).toBeVisible();
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
await expect(sheet).toHaveCount(0);
await page.locator(`section[data-desktop="${browser}"] .desktop-close`).click();
await button.click();
await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (192.168.0.10)']);
await page.keyboard.press('Escape');
// A switch has no menu in the simulation, so no button; a tap beside lets go.
// A switch: the tap opens no table either; its menu does.
const lanSwitch = (await drawing(page)).nodes.find((node) => node.kind === 'networkSwitch')!.id;
const hub = await onDevice(page, lanSwitch);
await page.mouse.click(hub.x, hub.y);
await expect(page.locator(`g.device[data-node="${lanSwitch}"]`)).toHaveAttribute('data-selected', 'true');
await expect(button).toHaveCount(0);
await page.locator(`section[data-viewer="${lanSwitch}"] .desktop-close`).click();
await expect(page.locator('section[data-viewer]')).toHaveCount(0);
await button.click();
await expect(page.getByRole('menuitem')).toHaveText(['SAT Tabelle anzeigen']);
await page.getByRole('menuitem', { name: 'SAT Tabelle anzeigen' }).click();
await expect(page.locator(`section[data-viewer="${lanSwitch}"]`)).toBeVisible();
await page.locator('.simulation-sheet__close').click();
// A tap beside lets go.
await page.mouse.click(at.x, at.y);
await expect(button).toBeVisible();
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
+4 -6
View File
@@ -5,7 +5,7 @@
// server stopped; and another computer's command line resolving the new name.
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
import { drawing, onDevice, openEditor, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -19,9 +19,7 @@ test.afterAll(async () => {
async function desktopOf(page: Page, name: string) {
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
return page.locator(`section[data-desktop="${id}"]`);
return showDesktop(page, id);
}
test('keeps the records as Java’s DNS window does, and the server answers with them', async ({ page }) => {
@@ -65,7 +63,7 @@ test('keeps the records as Java’s DNS window does, and the server answers with
await expect(cell).toHaveValue('10.0.0.2');
// A computer whose DNS server it is resolves the new name (the desktop closed, which covered it).
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
const web = await desktopOf(page, 'Web-Server');
await web.locator('[data-program="installation"]').click();
const installation = web.locator('[data-program-window="installation"]');
@@ -77,7 +75,7 @@ test('keeps the records as Java’s DNS window does, and the server answers with
await input.fill('host neu.schule.de');
await input.press('Enter');
await expect(web.locator('.terminal-text')).toContainText('10.0.0.2', { timeout: 30_000 });
await web.locator('.desktop-close').click();
await web.locator('.simulation-sheet__close').click();
await desktopOf(page, 'DNS-Server');
await desktop.locator('[data-program="dnsServer"]').click();
+3 -5
View File
@@ -7,7 +7,7 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, serveEditor } from './editorPage.js';
import { drawing, onDevice, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -33,9 +33,7 @@ async function openPages(page: Page): Promise<void> {
/** A computer's desktop opened with a tap, a program installed on it, and its window opened. */
async function install(page: Page, name: string, program: string, label: string) {
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
const desktop = page.locator(`section[data-desktop="${id}"]`);
const desktop = await showDesktop(page, id);
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: label }).click();
@@ -63,7 +61,7 @@ test('echoes a message from the generic client, as Java’s windows do', async (
await expect(port).toHaveAttribute('readonly', '');
const serverLog = echo.window.getByLabel('Ausgabe');
await expect(serverLog).toHaveText('Annahme von Verbindungsanfragen gestartet\n');
await echo.desktop.locator('.desktop-close').click();
await echo.desktop.locator('.simulation-sheet__close').click();
const client = await install(page, 'Browser', 'simpleClient', 'Einfacher Client');
const send = client.window.getByRole('button', { name: 'Senden' });
+23
View File
@@ -68,6 +68,29 @@ export async function onDevice(page: Page, id: string, across = 0.5, down = 0.4)
return { x: box.x + box.width * across, y: box.y + box.height * down };
}
/**
* In the simulation: a tap chooses the device, its three-dot button opens its
* menu, the item opens its sheet — a desktop, a table, the data exchange. A
* sheet already open covers the surface; it is closed first. With `touch`,
* by finger.
*/
export async function showFromMenu(page: Page, id: string, item: string | RegExp, touch = false): Promise<void> {
const open = page.locator('.simulation-sheet__close');
if ((await open.count()) > 0) await (touch ? open.tap() : open.click());
const at = await onDevice(page, id);
await (touch ? page.touchscreen.tap(at.x, at.y) : page.mouse.click(at.x, at.y));
const button = page.locator('.device-menu-button');
await (touch ? button.tap() : button.click());
const entry = page.getByRole('menuitem', { name: item, exact: true });
await (touch ? entry.tap() : entry.click());
}
/** A computer's desktop as the simulation's sheet (showFromMenu), in either language; its section. */
export async function showDesktop(page: Page, id: string, touch = false) {
await showFromMenu(page, id, /^(Desktop anzeigen|Show desktop)$/, touch);
return page.locator(`section[data-desktop="${id}"]`);
}
/** The surface's scale. */
export async function scaleOf(page: Page): Promise<number> {
return Number(await page.locator('[data-zoom]').getAttribute('data-zoom'));
+2 -4
View File
@@ -9,7 +9,7 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, serveEditor } from './editorPage.js';
import { drawing, onDevice, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -38,9 +38,7 @@ test('makes, copies, pastes, renames, deletes and imports as Java’s explorer d
test.setTimeout(120_000);
await openPages(page);
const id = (await drawing(page)).nodes.find((node) => node.name === 'Server')!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
const desktop = page.locator(`section[data-desktop="${id}"]`);
const desktop = await showDesktop(page, id);
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
const available = installation.locator('[data-list="available"] [role="option"]');
+6 -8
View File
@@ -6,7 +6,7 @@
// changed), the rule chosen and removed.
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
import { drawing, onDevice, openEditor, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -20,9 +20,7 @@ test.afterAll(async () => {
async function desktopOf(page: Page, name: string) {
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
return page.locator(`section[data-desktop="${id}"]`);
return showDesktop(page, id);
}
async function install(page: Page, desktop: ReturnType<Page['locator']>, program: string) {
@@ -65,7 +63,7 @@ test('keeps a computer’s firewall as Java’s window does, and the firewall le
await expect(port).toHaveValue('');
// A notebook's browser is let through by the rule; the log says so.
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
const notebook = await desktopOf(page, 'Rechner 1');
await notebook.locator('[data-program="webBrowser"]').click();
const browser = notebook.locator('[data-program-window="webBrowser"]');
@@ -75,12 +73,12 @@ test('keeps a computer’s firewall as Java’s window does, and the firewall le
await expect(browser.frameLocator('iframe[title="Seite"]').locator('body')).not.toBeEmpty({ timeout: 30_000 });
// ICMP no longer allowed: the notebook's ping is dropped.
await notebook.locator('.desktop-close').click();
await notebook.locator('.simulation-sheet__close').click();
await desktopOf(page, 'Web-Server');
await desktop.locator('[data-program="firewall"]').click();
await icmp.click();
await expect(icmp).not.toBeChecked();
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
await desktopOf(page, 'Rechner 1');
await install(page, notebook, 'Befehlszeile');
await notebook.locator('[data-program="commandPrompt"]').click();
@@ -88,7 +86,7 @@ test('keeps a computer’s firewall as Java’s window does, and the firewall le
await input.fill('ping 141.99.50.231');
await input.press('Enter');
await expect(notebook.locator('.terminal-text')).toContainText('0 Paket(e) empfangen', { timeout: 60_000 });
await notebook.locator('.desktop-close').click();
await notebook.locator('.simulation-sheet__close').click();
await desktopOf(page, 'Web-Server');
await desktop.locator('[data-program="firewall"]').click();
await window.getByRole('tab', { name: 'Log' }).click();
+3 -5
View File
@@ -6,7 +6,7 @@
// network left.
import { expect, test, type Locator, type Page } from '@playwright/test';
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
import { drawing, onDevice, openEditor, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -24,9 +24,7 @@ async function idOf(page: Page, name: string) {
async function desktopOf(page: Page, name: string) {
const id = await idOf(page, name);
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
return page.locator(`section[data-desktop="${id}"]`);
return showDesktop(page, id);
}
async function install(desktop: Locator, program: string) {
@@ -51,7 +49,7 @@ test('joins, searches and downloads as Java’s Gnutella window does', async ({
(id) => (window as unknown as { filiusDev: { writeFile: (id: string, path: string, text: string) => void } }).filiusDev.writeFile(id, '/peer2peer/lied.txt', 'La la la'),
otherId,
);
await other.locator('.desktop-close').click();
await other.locator('.simulation-sheet__close').click();
const desktop = await desktopOf(page, 'Rechner 1');
await install(desktop, 'Gnutella');
+2 -4
View File
@@ -5,7 +5,7 @@
// its question before it removes one, its domain only while it is off.
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
import { drawing, onDevice, openEditor, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -25,9 +25,7 @@ async function start(page: Page): Promise<void> {
/** Opens a computer's desktop with a tap and a program's window on it. */
async function openProgram(page: Page, name: string, program: string) {
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
const desktop = page.locator(`section[data-desktop="${id}"]`);
const desktop = await showDesktop(page, id);
await desktop.locator(`[data-program="${program}"]`).click();
return { desktop, window: desktop.locator(`[data-program-window="${program}"]`) };
}
+3 -6
View File
@@ -15,7 +15,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { expect, test, type Browser, type Page } from '@playwright/test';
import { BLOB_THRESHOLD_BYTES } from '@mdg-filius/doc';
import { drag, onDevice, serveEditor } from './editorPage.js';
import { drag, onDevice, serveEditor, showDesktop } from './editorPage.js';
import { colorOf } from '../src/remote/presence.js';
import { startServerF, type ServerF } from './serverF.js';
@@ -48,8 +48,7 @@ const desktop = (page: Page, id: string) => page.locator(`section[data-desktop="
const note = (page: Page, kind: string) => page.locator(`[data-note="${kind}"]`);
async function commandLine(page: Page, id: string) {
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
await showDesktop(page, id);
await desktop(page, id).locator('[data-program="commandPrompt"]').click();
const window = desktop(page, id).locator('[data-program-window="commandPrompt"]');
return { input: window.locator('input[name="terminalInput"]'), text: window.locator('.terminal-text') };
@@ -206,9 +205,7 @@ test('takes a large picture one person imports to F as a blob, and shows it to a
await mia.locator('button[name="simulationMode"]').click();
for (const page of [mia, ben]) await expect(canvas(page)).toHaveAttribute('data-mode', 'simulation');
const open = async (page: Page) => {
const at = await onDevice(page, notebook);
await page.mouse.click(at.x, at.y);
return desktop(page, notebook);
return showDesktop(page, notebook);
};
const miaDesktop = await open(mia);
await miaDesktop.locator('[data-program="installation"]').click();
+82 -54
View File
@@ -1,10 +1,11 @@
// The simulation in Chromium (F4.6a): Java's mode buttons, the design locked
// while it runs, the speed, the cables that carry frames, a computer's
// desktop opened with a tap — Java's title, its programs in Java's order, its
// network as it runs — and desktops kept over a return to the design.
// desktop opened from its menu — Java's title, its programs in Java's order,
// its network as it runs — as the simulation's sheet, one at a time, below
// the toolbar, see-through on request, kept over a return to the design.
import { expect, test, type Page } from '@playwright/test';
import { desktopOf, drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
import { expect, test, type Locator, type Page } from '@playwright/test';
import { desktopOf, drawing, onDevice, openEditor, serveEditor, showDesktop, showFromMenu } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -29,6 +30,9 @@ const simulationButton = (page: Page) => page.getByRole('button', { name: 'Wechs
const designButton = (page: Page) => page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' });
const desktop = (page: Page, id: string) => page.locator(`section[data-desktop="${id}"]`);
/** Once the sheet has come out of the toolbar (its 140 ms), where it rests. */
const settled = (sheet: Locator) => sheet.evaluate((element) => Promise.all(element.getAnimations().map((animation) => animation.finished)));
/** Starts the simulation and waits until the room says it runs (it answers from its worker). */
async function start(page: Page): Promise<void> {
await simulationButton(page).click();
@@ -86,13 +90,16 @@ test("lights a cable while it carries a frame, in Java's green", async ({ page }
await expect(page.locator('path[data-link][data-active]')).toHaveCount(0);
});
test("opens a computer's desktop with a tap: Java's title, its programs in Java's order, its network as it runs", async ({ page }) => {
test("opens a computer's desktop from its menu: Java's title, its programs in Java's order, its network as it runs", async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const id = await deviceNamed(page, 'DNS-Server 2.10');
expect((await desktopOf(page, id))!.programs).toHaveLength(3);
await start(page);
// A tap only chooses the computer (Java's click opens its desktop): its three-dot button is there, no desktop.
await tap(page, id);
const window = desktop(page, id);
await expect(page.locator('.device-menu-button')).toBeVisible();
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
const window = await showDesktop(page, id);
await expect(window.getByRole('heading')).toHaveText('DNS-Server 2.10 - 141.99.2.10');
await expect(window.locator('.desktop-icon span')).toHaveText(['Software-Installation', 'Datei-Explorer', 'DNS-Server', 'Webserver']);
await expect(window.getByRole('button', { name: 'Startbildschirm' })).toBeVisible();
@@ -106,11 +113,13 @@ test("opens a computer's desktop with a tap: Java's title, its programs in Java'
await window.locator('.desktop-info-layer').click({ position: { x: 20, y: 20 } });
await expect(window.locator('.desktop-network')).toHaveCount(0);
// A switch has no desktop; its long-press menu has nothing either.
await tap(page, await deviceNamed(page, 'Switch 1'));
await expect(page.locator('section[data-desktop]')).toHaveCount(1);
// A switch has no desktop: its menu offers its table, nothing more (it has no address for a data exchange).
await window.getByRole('button', { name: 'Schließen' }).click();
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
await tap(page, await deviceNamed(page, 'Switch 1'));
await page.locator('.device-menu-button').click();
await expect(page.getByRole('menuitem')).toHaveText(['SAT Tabelle anzeigen']);
await page.keyboard.press('Escape');
// Java's menu: "show desktop".
const at = await onDevice(page, await deviceNamed(page, 'Notebook 1.10'));
@@ -123,36 +132,50 @@ test("opens a computer's desktop with a tap: Java's title, its programs in Java'
await expect(page.locator('section[data-desktop] .desktop-icon span')).toHaveText(['Software-Installation', 'Webbrowser']);
});
test('moves a desktop by its title bar, brings the one touched to the front, and keeps them over a return to the design', async ({ page }) => {
test('opens one sheet at a time below the toolbar, see-through on request, kept with its program over a return to the design', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const [first, second] = [await deviceNamed(page, 'Notebook 1.10'), await deviceNamed(page, 'Notebook 1.11')];
await start(page);
await tap(page, first);
// Moved by its title bar into the half of the page the second notebook is not in.
const at = await onDevice(page, second);
const width = page.viewportSize()!.width;
const bar = (await desktop(page, first).locator('.desktop-titlebar h2').boundingBox())!;
const before = (await desktop(page, first).boundingBox())!;
const step = { x: (at.x < width / 2 ? width - 650 : 0) - before.x, y: 30 };
await page.mouse.move(bar.x + 10, bar.y + 10);
await page.mouse.down();
await page.mouse.move(bar.x + 10 + step.x, bar.y + 10 + step.y, { steps: 8 });
await page.mouse.up();
const after = (await desktop(page, first).boundingBox())!;
expect([Math.round(after.x - before.x), Math.round(after.y - before.y)]).toEqual([Math.round(step.x), step.y]);
await tap(page, second);
await expect(page.locator('section[data-desktop]')).toHaveCount(2);
const z = async (id: string) => Number(await desktop(page, id).evaluate((element) => getComputedStyle(element).zIndex));
expect(await z(second)).toBeGreaterThan(await z(first));
await desktop(page, first).locator('.desktop-titlebar h2').click();
expect(await z(first)).toBeGreaterThan(await z(second));
const sheet = await showDesktop(page, first);
await settled(sheet);
// The dashboard's sheet: from below the toolbar to the bottom, the area's width — no window in the window.
const area = (await page.locator('.editor-area').boundingBox())!;
const toolbar = (await page.locator('.editor-toolbar').boundingBox())!;
const box = (await sheet.boundingBox())!;
expect(box.y).toBeGreaterThanOrEqual(toolbar.y + toolbar.height);
expect(box.x).toBeCloseTo(area.x, 0);
expect(box.width).toBeCloseTo(area.width, 0);
expect(box.y + box.height).toBeCloseTo(area.y + area.height, 0);
// Its program stays open over a return to the design.
await sheet.locator('[data-program="installation"]').click();
await designButton(page).click();
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
await simulationButton(page).click();
await expect(page.locator('section[data-desktop]')).toHaveCount(2);
expect(Math.round((await desktop(page, first).boundingBox())!.x)).toBe(Math.round(after.x));
await expect(sheet.locator('[data-program-window="installation"]')).toBeVisible();
// Closed (it covers the surface), another opens: one at a time.
await showDesktop(page, second);
await expect(page.locator('section[data-desktop]')).toHaveCount(1);
await expect(desktop(page, second)).toBeVisible();
// „Durchsichtig“: the network shows through; the choice holds for the next sheet and is kept in this browser.
const seeThrough = desktop(page, second).getByRole('button', { name: 'Durchsichtig' });
await expect(seeThrough).toHaveAttribute('aria-pressed', 'false');
const body = (id: string) => desktop(page, id).locator('.simulation-sheet__body');
await expect(body(second)).toHaveCSS('opacity', '1');
await seeThrough.click();
await expect(seeThrough).toHaveAttribute('aria-pressed', 'true');
await expect(body(second)).toHaveCSS('opacity', '0.55');
// Only the body fades: the sheet itself — and so its head, with title and buttons — stays as it is (opacity is not inherited, so the sheet is asked).
await expect(desktop(page, second)).toHaveCSS('opacity', '1');
expect(await page.evaluate(() => window.localStorage.getItem('filius.sheetSeeThrough'))).toBe('true');
// The next sheet is see-through too; the first desktop, closed in between, begins at its home screen.
await showDesktop(page, first);
await expect(desktop(page, first)).toHaveAttribute('data-see-through', 'true');
await expect(desktop(page, first).locator('[data-program-window]')).toHaveCount(0);
await desktop(page, first).getByRole('button', { name: 'Durchsichtig' }).click();
await expect(body(first)).toHaveCSS('opacity', '1');
});
test('takes nothing back and keeps no design tool while the simulation runs', async ({ page }) => {
@@ -185,8 +208,7 @@ test("titles a desktop with the address alone where the address is the computer'
await page.getByRole('complementary', { name: 'Einstellungen' }).getByRole('button', { name: 'Schließen' }).click();
// The notebooks ask for their addresses by DHCP as the simulation starts; the switch passes their broadcasts on to Rechner 1.
await start(page);
await tap(page, id);
const window = desktop(page, id);
const window = await showDesktop(page, id);
await expect(window.getByRole('heading')).toHaveText('192.168.0.11');
await expect(window.locator('.desktop-network-icon[data-active="true"]')).toBeAttached({ timeout: 10_000 });
await window.getByRole('button', { name: 'Netzwerk' }).click();
@@ -199,7 +221,7 @@ test("closes a desktop whose computer is removed while the design is shown", asy
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const id = await deviceNamed(page, 'Notebook 1.10');
await start(page);
await tap(page, id);
await showDesktop(page, id);
await designButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'design');
await tap(page, id);
@@ -207,27 +229,36 @@ test("closes a desktop whose computer is removed while the design is shown", asy
await simulationButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
// The next desktop takes the first place: the gone one is not counted.
// The next desktop opens as ever.
const next = await deviceNamed(page, 'Notebook 1.11');
await tap(page, next);
const area = (await page.locator('.editor-area').boundingBox())!;
const box = (await desktop(page, next).boundingBox())!;
expect([Math.round(box.x - area.x), Math.round(box.y - area.y)]).toEqual([24, 76]);
await showDesktop(page, next);
await expect(page.locator('section[data-desktop]')).toHaveCount(1);
// So does a switch's table whose switch is removed (a table would otherwise stand there empty, under no name).
const hub = await deviceNamed(page, 'Switch 1');
await showFromMenu(page, hub, 'SAT Tabelle anzeigen');
await expect(page.locator(`section[data-viewer="${hub}"]`)).toBeVisible();
await designButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'design');
await tap(page, hub);
await page.keyboard.press('Delete');
await expect.poll(async () => (await drawing(page)).nodes.some((node) => node.id === hub)).toBe(false);
await simulationButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await expect(page.locator('section[data-viewer]')).toHaveCount(0);
});
test.describe('on a phone', () => {
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });
test('fills the surface with a desktop opened by a tap, below the mode buttons', async ({ page }) => {
test('fills the surface with a desktop opened from its menu, below the mode buttons', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
await simulationButton(page).tap();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await page.getByRole('button', { name: 'Alles zeigen' }).tap();
const id = await deviceNamed(page, 'Notebook 1.10');
const at = await onDevice(page, id);
await page.touchscreen.tap(at.x, at.y);
const window = desktop(page, id);
await expect(window).toBeVisible();
const window = await showDesktop(page, id, true);
await settled(window);
const box = (await window.boundingBox())!;
const area = (await page.locator('.editor-area').boundingBox())!;
const modes = (await simulationButton(page).boundingBox())!;
@@ -244,8 +275,7 @@ test.describe('on a phone', () => {
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await page.getByRole('button', { name: 'Alles zeigen' }).tap();
const id = await deviceNamed(page, 'Notebook 1.10');
const at = await onDevice(page, id);
await page.touchscreen.tap(at.x, at.y);
await showDesktop(page, id, true);
await desktop(page, id).locator('[data-program="commandPrompt"]').tap();
const window = desktop(page, id).locator('[data-program-window="commandPrompt"]');
for (const name of ['ipconfig', 'Vorheriger Befehl', 'Befehl abbrechen (Strg+C)']) {
@@ -264,8 +294,7 @@ test.describe('on a phone', () => {
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await page.getByRole('button', { name: 'Alles zeigen' }).tap();
const id = await deviceNamed(page, 'Notebook 1.10');
const at = await onDevice(page, id);
await page.touchscreen.tap(at.x, at.y);
await showDesktop(page, id, true);
await desktop(page, id).locator('[data-program="installation"]').tap();
const panel = desktop(page, id).locator('[data-program-window="installation"]');
const first = (await panel.locator('[data-list="available"] [role="option"]').first().boundingBox())!;
@@ -281,7 +310,7 @@ test.describe('on a phone', () => {
/** Opens a computer's command line on its desktop. */
async function commandLine(page: Page, name: string) {
const id = await deviceNamed(page, name);
await tap(page, id);
await showDesktop(page, id);
await desktop(page, id).locator('[data-program="commandPrompt"]').click();
const window = desktop(page, id).locator('[data-program-window="commandPrompt"]');
await expect(window.getByRole('heading')).toHaveText('Befehlszeile');
@@ -398,8 +427,7 @@ test("installs and removes programs as Java's installation does, and a program i
const id = await deviceNamed(page, 'Rechner 3.10');
expect((await desktopOf(page, id))!.programs).toEqual([]);
await start(page);
await tap(page, id);
const window = desktop(page, id);
const window = await showDesktop(page, id);
await window.locator('[data-program="installation"]').click();
const panel = window.locator('[data-program-window="installation"]');
await expect(panel.getByRole('heading')).toHaveText('Software-Installation');
@@ -464,7 +492,7 @@ test('speaks English when asked', async ({ page }) => {
await page.getByRole('button', { name: 'Change to Simulation Mode' }).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
const id = await deviceNamed(page, 'DNS-Server 2.10');
await tap(page, id);
await showDesktop(page, id);
await expect(desktop(page, id).locator('.desktop-icon span')).toHaveText(['Software Installation', 'DNS server', 'File explorer', 'Webserver']);
await expect(desktop(page, id).getByRole('button', { name: 'Home Screen' })).toBeVisible();
});
+4 -6
View File
@@ -9,7 +9,7 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, serveEditor } from './editorPage.js';
import { drawing, onDevice, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -40,9 +40,7 @@ async function openPages(page: Page): Promise<void> {
/** A computer's desktop, opened with a tap. */
async function desktopOf(page: Page, name: string) {
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
return page.locator(`section[data-desktop="${id}"]`);
return showDesktop(page, id);
}
test('writes a page into the web server, which serves it while the simulation runs, and changes it there', async ({ page }) => {
@@ -88,7 +86,7 @@ test('writes a page into the web server, which serves it while the simulation ru
// The other computer's browser fetches it from the running web server (the desktops closed in between, which covered the computers).
const fetchPage = async (expected: string) => {
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
const browserDesktop = await desktopOf(page, 'Browser');
await browserDesktop.locator('[data-program="webBrowser"]').click();
const browser = browserDesktop.locator('[data-program-window="webBrowser"]');
@@ -96,7 +94,7 @@ test('writes a page into the web server, which serves it while the simulation ru
await address.fill('192.168.0.20/neu.html');
await address.press('Enter');
await expect(browser.frameLocator('iframe[title="Seite"]').locator('body')).toHaveText(expected, { timeout: 30_000 });
await browserDesktop.locator('.desktop-close').click();
await browserDesktop.locator('.simulation-sheet__close').click();
await desktopOf(page, 'Server');
await desktop.locator('[data-program="textEditor"]').click();
};
+3
View File
@@ -104,7 +104,10 @@ test('builds FILIUS\' „zwei Rechnernetze“ by finger and pings across (the ac
// The simulation; the command line installed on the first computer; a ping across the router.
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).tap();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
// A tap chooses the computer; its three-dot button opens its menu, „Desktop anzeigen“ its desktop as the sheet.
await finger.tap(await onDevice(page, pcA));
await page.getByRole('button', { name: 'Aktionen' }).tap();
await page.getByRole('menuitem', { name: 'Desktop anzeigen' }).tap();
const desktop = page.locator(`section[data-desktop="${pcA}"]`);
await desktop.locator('[data-program="installation"]').tap();
const installation = desktop.locator('[data-program-window="installation"]');
+10 -15
View File
@@ -6,7 +6,7 @@
// list shows the dropped frames in Java's words.
import { expect, test, type Locator, type Page } from '@playwright/test';
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
import { drawing, openEditor, serveEditor, showDesktop, showFromMenu } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -18,11 +18,6 @@ test.afterAll(async () => {
await server.close();
});
async function tap(page: Page, id: string) {
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
}
async function install(desktop: Locator, program: string) {
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
@@ -41,17 +36,17 @@ test('shows the tables of a home router and a switch, and drops while “Pakete
// The home router with the port forwarding: its entry, with Java's columns.
const gateway = idOf('Heimrouter Vincent');
await tap(page, gateway);
await showFromMenu(page, gateway, 'NAT-Tabelle anzeigen');
const nat = page.locator(`section[data-viewer="${gateway}"]`);
await expect(nat.locator('h2')).toHaveText('NAT-Tabelle Heimrouter Vincent');
await expect(nat.locator('th')).toHaveText(['Protokoll', 'Portfreigabe', 'WAN-Adresse', 'LAN-Adresse', 'LAN-Port', 'Letzte Aktualisierung']);
await expect(nat.locator('tbody tr')).toHaveCount(1);
await expect(nat.locator('tbody tr').first().locator('td')).toHaveText(['TCP', '80', '*', '192.168.0.10', '80', '-']);
await nat.locator('.desktop-close').click();
await nat.locator('.simulation-sheet__close').click();
// The web server behind it pings out.
const web = idOf('Webserver - 192.168.0.10');
await tap(page, web);
await showDesktop(page, web);
const desktop = page.locator(`section[data-desktop="${web}"]`);
await install(desktop, 'Befehlszeile');
await desktop.locator('[data-program="commandPrompt"]').click();
@@ -59,29 +54,29 @@ test('shows the tables of a home router and a switch, and drops while “Pakete
await input.fill('ping 141.99.4.10');
await input.press('Enter');
await expect(desktop.locator('.terminal-text')).toContainText('4 Paket(e) empfangen', { timeout: 30_000 });
await desktop.locator('.desktop-close').click();
await desktop.locator('.simulation-sheet__close').click();
await tap(page, gateway);
await showFromMenu(page, gateway, 'NAT-Tabelle anzeigen');
await expect(nat.locator('tbody tr')).toHaveCount(2);
await expect(nat.locator('tbody tr').nth(1).locator('td').nth(0)).toHaveText('ICMP');
await expect(nat.locator('tbody tr').nth(1).locator('td').nth(5)).toHaveText(/^\d\d:\d\d:\d\d$/);
await nat.getByRole('button', { name: 'Tabelle zurücksetzen' }).click();
await expect(nat.locator('tbody tr')).toHaveCount(1);
await nat.locator('.desktop-close').click();
await nat.locator('.simulation-sheet__close').click();
// The switch the web server hangs on: the addresses the ping taught it.
const link = (await drawing(page)).links.find((each) => each.sourceNode === web || each.targetNode === web)!;
const lanSwitch = link.sourceNode === web ? link.targetNode : link.sourceNode;
await tap(page, lanSwitch);
await showFromMenu(page, lanSwitch, 'SAT Tabelle anzeigen');
const sat = page.locator(`section[data-viewer="${lanSwitch}"]`);
await expect(sat.locator('th')).toHaveText(['MAC', 'Port', 'Letzte Aktualisierung']);
await expect(sat.locator('tbody tr').first().locator('td')).toHaveText([/^[0-9A-F:]{17}$/, /^Port \d+$/, /^\d\d:\d\d:\d\d$/]);
await sat.getByRole('button', { name: 'Tabelle löschen' }).click();
await expect(sat.locator('tbody tr')).toHaveCount(0);
await sat.locator('.desktop-close').click();
await sat.locator('.simulation-sheet__close').click();
// "Pakete verwerfen" held while a ping runs: answers lost, the frames listed.
await tap(page, web);
await showDesktop(page, web);
await desktop.locator('[data-program="commandPrompt"]').click();
await input.fill('ping 141.99.4.10');
await input.press('Enter');
+2 -4
View File
@@ -8,7 +8,7 @@
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Page } from '@playwright/test';
import { drawing, onDevice, serveEditor } from './editorPage.js';
import { drawing, onDevice, serveEditor, showDesktop } from './editorPage.js';
let server: { readonly base: string; readonly close: () => Promise<void> };
@@ -37,9 +37,7 @@ async function start(page: Page, name = 'Wechselt in den Aktionsmodus'): Promise
/** Opens a computer's desktop with a tap and a program's window on it. */
async function openProgram(page: Page, name: string, program: string) {
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
const desktop = page.locator(`section[data-desktop="${id}"]`);
const desktop = await showDesktop(page, id);
await desktop.locator(`[data-program="${program}"]`).click();
const window = desktop.locator(`[data-program-window="${program}"]`);
return { desktop, window, title: window.locator('.program-titlebar h3') };