Files
mdg-filius/packages/editor/browser/simulation.spec.ts
T
julian.kiedaischandClaude Opus 5.5 f0e7c73ec7 feat(editor): F4.6c the software installation on a computer's desktop
A new oracle, InstallRun, installs each of Java's programs on new
computers, all of them on one, each installed and removed again, and a
DNS and an e-mail server installed again after their removal; Java saves
the result as a project. A new program is switched off (a computer's
firewall is on), makes the folders and files it lacks (the web server's
start page in FILIUS' language and its picture, /peer2peer,
/mailserver/konten.txt, /dns/hosts), and removing it takes its settings
and leaves its files; a server installed again reads its records or
accounts from them. Java installs in the order of its list and starts
the program at once.

The document's setSoftware does the same, checked against Java's saved
installation. The core knows what a program makes in the files and
installs or removes a program while the simulation runs; the room
watches the document's programs and hands changes to the engine, so a
new command line works at once. The desktop offers Java's dialog with
two lists, arrows, double click and apply changes.

Also: Java's file may name a number it rolled by the result of any
getter (an idref, here a thread's priority); GetterDefaults now
measures the priority of the saved thread classes, which the import
read as 0 before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:03:35 +00:00

440 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.
import { expect, test, type Page } from '@playwright/test';
import { desktopOf, drawing, onDevice, 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();
});
async function deviceNamed(page: Page, name: string): Promise<string> {
return (await drawing(page)).nodes.find((node) => node.name === name)!.id;
}
async function tap(page: Page, id: string): Promise<void> {
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
}
const simulationButton = (page: Page) => page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' });
const designButton = (page: Page) => page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' });
const desktop = (page: Page, id: string) => page.locator(`section[data-desktop="${id}"]`);
/** 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();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
}
test('locks the design while the simulation runs, as Java does, and gives it back', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const router = (await drawing(page)).nodes.find((node) => node.kind === 'router')!;
await tap(page, router.id);
await expect(page.getByRole('complementary', { name: 'Einstellungen' })).toBeVisible();
await expect(designButton(page)).toHaveAttribute('aria-pressed', 'true');
await simulationButton(page).click();
await expect(simulationButton(page)).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await expect(page.getByRole('complementary', { name: 'Einstellungen' })).toHaveCount(0);
await expect(page.locator('[data-palette]')).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Rückgängig' })).toHaveCount(0);
// Java's slider: 90 per cent at first (Verbindung's delay factor 10).
const speed = page.locator('input[name="speed"]');
await expect(speed).toHaveValue('90');
await expect(page.getByTitle('Aktuelle Simulationsgeschwindigkeit')).toHaveText('90%');
await speed.fill('40');
await expect(page.getByTitle('Aktuelle Simulationsgeschwindigkeit')).toHaveText('40%');
// Nothing moves, nothing is chosen, a key removes nothing.
const at = await onDevice(page, router.id);
await page.mouse.move(at.x, at.y);
await page.mouse.down();
await page.mouse.move(at.x + 80, at.y + 60, { steps: 5 });
await page.mouse.up();
await page.keyboard.press('Delete');
const still = (await drawing(page)).nodes.find((node) => node.id === router.id)!;
expect([still.x, still.y]).toEqual([router.x, router.y]);
await expect(page.locator('g.device[data-selected]')).toHaveCount(0);
await designButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'design');
await expect(page.locator('[data-palette]').first()).toBeVisible();
await expect(page.getByRole('button', { name: 'Rückgängig' })).toBeVisible();
await expect(page.locator('input[name="speed"]')).toHaveCount(0);
// The speed stays the room's for the next start.
await simulationButton(page).click();
await expect(page.locator('input[name="speed"]')).toHaveValue('40');
});
test("lights a cable while it carries a frame, in Java's green", async ({ page }) => {
// The notebooks ask for their addresses by DHCP as the simulation starts.
await openEditor(page, server.base, 'einfaches_rechnernetz_komplett.fls');
await simulationButton(page).click();
await expect(page.locator('path[data-link][data-active="true"]').first()).toBeAttached({ timeout: 10_000 });
await expect(page.locator('path[data-link][data-active="true"]').first()).toHaveCSS('stroke', 'rgb(0, 255, 64)');
await designButton(page).click();
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 }) => {
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);
await tap(page, id);
const window = desktop(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();
await window.getByRole('button', { name: 'Netzwerk' }).click();
const values = window.locator('.desktop-network dd');
await expect(window.locator('.desktop-network dt')).toHaveText(['IP-Adresse:', 'Netzmaske:', 'DNS-Server:', 'Gateway:', 'MAC-Adresse:']);
await expect(values.nth(0)).toHaveText('141.99.2.10');
await expect(values.nth(4)).toHaveText(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/);
// A press beside the panel closes it.
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);
await window.getByRole('button', { name: 'Schließen' }).click();
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
// Java's menu: "show desktop".
const at = await onDevice(page, await deviceNamed(page, 'Notebook 1.10'));
await page.mouse.move(at.x, at.y);
await page.mouse.down();
await page.waitForTimeout(700);
await page.mouse.up();
await page.getByRole('menuitem', { name: 'Desktop anzeigen' }).click();
await expect(page.locator('section[data-desktop] h2')).toHaveText('Notebook 1.10 - 141.99.1.10');
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 }) => {
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));
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));
});
test('takes nothing back and keeps no design tool while the simulation runs', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const id = await deviceNamed(page, 'Notebook 1.10');
const at = await onDevice(page, id);
await page.mouse.move(at.x, at.y);
await page.mouse.down();
await page.mouse.move(at.x + 60, at.y + 40, { steps: 5 });
await page.mouse.up();
const moved = (await drawing(page)).nodes.find((node) => node.id === id)!;
await page.locator('[data-palette="cable"]').click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-tool', 'cable');
await start(page);
await page.keyboard.press('Control+z');
const still = (await drawing(page)).nodes.find((node) => node.id === id)!;
expect([still.x, still.y]).toEqual([moved.x, moved.y]);
await designButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-tool', 'select');
// Back in the design, the step can be taken back.
await page.getByRole('button', { name: 'Rückgängig' }).click();
await expect.poll(async () => (await drawing(page)).nodes.find((node) => node.id === id)!.x).not.toBe(moved.x);
});
test("titles a desktop with the address alone where the address is the computer's name, and lights its network icon while its cable carries a frame", async ({ page }) => {
await openEditor(page, server.base, 'einfaches_rechnernetz_komplett.fls');
const id = await deviceNamed(page, 'Rechner 1');
await tap(page, id);
await page.getByRole('complementary', { name: 'Einstellungen' }).getByLabel('IP-Adresse als Name verwenden').check();
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);
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();
await expect(window.locator('.desktop-network')).toBeVisible();
await window.getByRole('button', { name: 'Startbildschirm' }).click();
await expect(window.locator('.desktop-network')).toHaveCount(0);
});
test("closes a desktop whose computer is removed while the design is shown", async ({ page }) => {
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 designButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'design');
await tap(page, id);
await page.keyboard.press('Delete');
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.
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]);
});
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 }) => {
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 box = (await window.boundingBox())!;
const area = (await page.locator('.editor-area').boundingBox())!;
const modes = (await simulationButton(page).boundingBox())!;
expect(box.width).toBeCloseTo(area.width, 0);
expect(box.y).toBeGreaterThanOrEqual(modes.y + modes.height);
expect(box.y + box.height).toBeCloseTo(area.y + area.height, 0);
const home = (await window.getByRole('button', { name: 'Startbildschirm' }).boundingBox())!;
expect(home.height).toBeGreaterThanOrEqual(44);
});
test("offers the command line's keys as buttons for a finger, and types without zooming", async ({ page }) => {
await openEditor(page, server.base, 'internetwork_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);
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)']) {
const box = (await window.getByRole('button', { name, exact: true }).boundingBox())!;
expect(box.height, name).toBeGreaterThanOrEqual(44);
}
await expect(window.locator('input[name="terminalInput"]')).toHaveCSS('font-size', '16px');
await window.getByRole('button', { name: 'ipconfig', exact: true }).tap();
await window.locator('input[name="terminalInput"]').press('Enter');
await expect(window.locator('.terminal-text')).toContainText('IP Adresse . . . : 141.99.1.10');
});
test('offers the installation’s programs as targets for a finger', async ({ page }) => {
await openEditor(page, server.base, 'internetwork_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);
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())!;
expect(first.height).toBeGreaterThanOrEqual(44);
const arrow = (await panel.getByRole('button', { name: 'Installieren' }).boundingBox())!;
expect(arrow.height).toBeGreaterThanOrEqual(44);
await panel.locator('[data-list="available"] [role="option"]').first().tap();
await panel.getByRole('button', { name: 'Installieren' }).tap();
await expect(panel.locator('[data-list="installed"] [role="option"]')).toHaveText(['Befehlszeile', 'Bildbetrachter', 'Webbrowser']);
});
});
/** 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 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');
return { id, window, input: window.locator('input[name="terminalInput"]'), text: window.locator('.terminal-text'), prompt: window.locator('.terminal-prompt') };
}
test("types into a computer's command line as into Java's: the start screen, a line after its prompt, the history, a ping across the routers", async ({ page }) => {
test.setTimeout(60_000);
await openEditor(page, server.base, 'internetwork_komplett.fls');
await start(page);
const { window, input, text, prompt } = await commandLine(page, 'Notebook 1.10');
await expect(text).toContainText("Verwende den Befehl 'help'");
await expect(prompt).toHaveText('/>');
await expect(input).toBeFocused();
await input.fill('ipconfig');
await input.press('Enter');
await expect(text).toContainText('\n/> ipconfig\nIP Adresse . . . : 141.99.1.10');
await expect(input).toHaveValue('');
await input.press('ArrowUp');
await expect(input).toHaveValue('ipconfig');
await input.press('ArrowDown');
await expect(input).toHaveValue('');
// A chip puts its command into the line.
await window.getByRole('button', { name: 'ping', exact: true }).click();
await expect(input).toHaveValue('ping ');
await input.pressSequentially('141.99.5.10');
await input.press('Enter');
// While the ping runs, Java hides the prompt.
await expect(prompt).toHaveCSS('visibility', 'hidden');
await expect(text).toContainText('4 Paket(e) gesendet, 4 Paket(e) empfangen', { timeout: 20_000 });
await expect(prompt).toHaveCSS('visibility', 'visible');
await window.getByRole('button', { name: 'Vorheriger Befehl' }).click();
await expect(input).toHaveValue('ping 141.99.5.10');
});
test('ends a command with Ctrl+C, from the keyboard and from its button', async ({ page }) => {
test.setTimeout(60_000);
await openEditor(page, server.base, 'internetwork_komplett.fls');
await start(page);
const { window, input, text, prompt } = await commandLine(page, 'Notebook 1.10');
await input.fill('ping 141.99.9.9');
await input.press('Enter');
await expect(prompt).toHaveCSS('visibility', 'hidden');
await input.press('Control+c');
await expect(prompt).toHaveCSS('visibility', 'visible', { timeout: 20_000 });
await expect(text).toContainText('Paketstatistik');
await input.fill('ping 141.99.9.9');
await input.press('Enter');
await expect(prompt).toHaveCSS('visibility', 'hidden');
await window.getByRole('button', { name: 'Befehl abbrechen (Strg+C)' }).click();
await expect(prompt).toHaveCSS('visibility', 'visible', { timeout: 20_000 });
});
test('keeps the command line over leaving it, over a return to the design, and exit closes it', async ({ page }) => {
await openEditor(page, server.base, 'internetwork_komplett.fls');
await start(page);
const { id, window, input, text } = await commandLine(page, 'Notebook 1.10');
await input.fill('pwd');
await input.press('Enter');
await expect(text).toContainText('\n/> pwd\n/');
await desktop(page, id).getByRole('button', { name: 'Startbildschirm' }).click();
await expect(window).toHaveCount(0);
await desktop(page, id).locator('[data-program="commandPrompt"]').click();
await expect(text).toContainText('\n/> pwd\n/');
await designButton(page).click();
await start(page);
await expect(text).toContainText('\n/> pwd\n/');
await input.fill('exit');
await input.press('Enter');
await expect(window).toHaveCount(0);
await expect(desktop(page, id).locator('.desktop-icon').first()).toBeVisible();
await desktop(page, id).locator('[data-program="commandPrompt"]').click();
await expect(text).toContainText('\n/> exit\n');
// An exit that was there when the window opened does not close it again: it takes the next line.
await input.fill('ls');
await input.press('Enter');
await expect(text).toContainText('\n/> exit\n\n/> ls\n');
await expect(window).toBeVisible();
});
test("installs and removes programs as Java's installation does, and a program installed runs at once", async ({ page }) => {
await openEditor(page, server.base, 'internetwork_komplett.fls');
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);
await window.locator('[data-program="installation"]').click();
const panel = window.locator('[data-program-window="installation"]');
await expect(panel.getByRole('heading')).toHaveText('Software-Installation');
const installed = panel.locator('[data-list="installed"] [role="option"]');
const available = panel.locator('[data-list="available"] [role="option"]');
await expect(installed).toHaveCount(0);
await expect(available).toHaveText(['Befehlszeile', 'Bildbetrachter', 'Datei-Explorer', 'DNS-Server', 'E-Mail-Programm', 'E-Mail-Server', 'Echo-Server', 'Einfacher Client', 'Firewall', 'Gnutella', 'Text-Editor', 'Webserver', 'Webbrowser']);
// Chosen with a tap each, moved over with the arrow; the list keeps Java's order.
await available.filter({ hasText: 'Webserver' }).click();
await available.filter({ hasText: 'Befehlszeile' }).click();
await expect(available.filter({ hasText: 'Webserver' })).toHaveAttribute('aria-selected', 'true');
// A second tap lets a program go again.
await available.filter({ hasText: 'Gnutella' }).click();
await available.filter({ hasText: 'Gnutella' }).click();
await expect(available.filter({ hasText: 'Gnutella' })).toHaveAttribute('aria-selected', 'false');
await panel.getByRole('button', { name: 'Installieren' }).click();
await expect(installed).toHaveText(['Befehlszeile', 'Webserver']);
// Nothing changes before "apply changes".
expect((await desktopOf(page, id))!.programs).toEqual([]);
await panel.getByRole('button', { name: 'Änderungen annehmen' }).click();
await expect(panel).toHaveCount(0);
await expect(window.locator('.desktop-icon span')).toHaveText(['Software-Installation', 'Befehlszeile', 'Webserver']);
expect([...(await desktopOf(page, id))!.programs].sort()).toEqual(['commandPrompt', 'webServer']);
// The command line runs at once, and sees the web server's files.
await window.locator('[data-program="commandPrompt"]').click();
const input = window.locator('input[name="terminalInput"]');
await input.fill('ls /webserver');
await input.press('Enter');
await expect(window.locator('.terminal-text')).toContainText('index.html');
await window.getByRole('button', { name: 'Startbildschirm' }).click();
// A double click moves a program back; applied, it is gone.
await window.locator('[data-program="installation"]').click();
await installed.filter({ hasText: 'Webserver' }).dblclick();
await expect(installed).toHaveText(['Befehlszeile']);
await panel.getByRole('button', { name: 'Änderungen annehmen' }).click();
await expect(window.locator('.desktop-icon span')).toHaveText(['Software-Installation', 'Befehlszeile']);
expect((await desktopOf(page, id))!.programs).toEqual(['commandPrompt']);
// Taken on and given back before applying: nothing happens.
await window.locator('[data-program="installation"]').click();
await available.filter({ hasText: 'Gnutella' }).dblclick();
await installed.filter({ hasText: 'Gnutella' }).click();
await panel.getByRole('button', { name: 'Entfernen' }).click();
await panel.getByRole('button', { name: 'Änderungen annehmen' }).click();
expect((await desktopOf(page, id))!.programs).toEqual(['commandPrompt']);
});
test('says so when the simulation fails, and goes back to the design', async ({ page }) => {
// FILIUS' modem example: the core has no modems yet (remote links); a ping to the other side fails it.
await openEditor(page, server.base, 'modem_beispiel.fls');
await start(page);
const { input } = await commandLine(page, 'Notebook 0.10');
await input.fill('ping 192.168.0.11');
await input.press('Enter');
await expect(page.getByRole('alert')).toHaveText('Die Simulation ist auf einen Fehler gestoßen und wurde beendet.', { timeout: 20_000 });
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'design');
});
test('speaks English when asked', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls', '&lang=en');
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 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();
});