Files
mdg-filius/packages/editor/browser/simulation.spec.ts
T
julian.kiedaischandClaude Opus 5.5 87c09c6c9d feat(editor): a modem offers a code or joins one, and says under it how it stands (K4, K5)
The modem's sheet chooses how it finds its partner: the other modem on its
port in this project, as FILIUS' file pairs them; another project's, by
offering this modem's code (shown large, with a copy button) or by joining
a code typed in as one reads it off the board. In the action mode each
modem says under it how it stands — connected, waiting, taken, no second
modem on the port, cable missing, on the server only.

Accepted in three browsers on a real F, each in a copy of FILIUS' modem
example: Ben's notebook pings the computer behind Mia's access point; Rita
joins the same code, reads that it is taken, and gets it once Ben stops.
FILIUS' modem example runs in the page's own room as well.

The .fls export writes an offering modem as Java's server and a joining
one as its client; the code has no place there and is named as lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:34:54 +00:00

500 lines
28 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 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 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> };
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}"]`);
/** 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();
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 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 opens the computer's menu (Java's click opens its desktop, its menu holds it), no desktop by itself, no three-dot button.
await tap(page, id);
await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (141.99.2.10)']);
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
await expect(page.locator('.device-menu-button')).toHaveCount(0);
await page.keyboard.press('Escape');
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();
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 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 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'));
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('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);
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(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 }) => {
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);
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();
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 showDesktop(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 opens as ever.
const next = await deviceNamed(page, 'Notebook 1.11');
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 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 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())!;
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');
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)']) {
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');
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())!;
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 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');
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('keeps what a run made in the files over a return to the design, as Java\'s project keeps it', async ({ page }) => {
await openEditor(page, server.base, 'internetwork_komplett.fls');
await start(page);
const { input, text } = await commandLine(page, 'Notebook 1.10');
await input.fill('mkdir ordner');
await input.press('Enter');
await input.fill('ls');
await input.press('Enter');
await expect(text).toContainText('ordner');
await designButton(page).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'design');
await start(page);
await input.fill('cls');
await input.press('Enter');
await input.fill('ls');
await input.press('Enter');
await expect(text).toContainText('\n/> ls\n');
await expect(text).toContainText('ordner');
// Removed in the run, it is gone from the project too.
await input.fill('rm ordner');
await input.press('Enter');
await designButton(page).click();
await start(page);
await input.fill('cls');
await input.press('Enter');
await input.fill('ls');
await input.press('Enter');
await expect(text).toContainText('\n/> ls\n');
await expect(text).not.toContainText('ordner');
});
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);
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');
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("runs FILIUS' modem example: both modems say they are connected, a ping goes through the pair", async ({ page }) => {
await openEditor(page, server.base, 'modem_beispiel.fls');
await start(page);
for (const name of ['Modem (Zugreifend)', 'Modem (Zugangspunkt)'])
await expect(page.locator(`g.device[data-node="${await deviceNamed(page, name)}"] text[data-note]`)).toHaveText('verbunden');
const { input, text } = await commandLine(page, 'Notebook 0.10');
await input.fill('ping 192.168.0.11');
await input.press('Enter');
await expect(text).toContainText('4 Paket(e) gesendet, 4 Paket(e) empfangen', { timeout: 30_000 });
});
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 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();
});