// 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 }; test.beforeAll(async () => { server = await serveEditor(); }); test.afterAll(async () => { await server.close(); }); async function deviceNamed(page: Page, name: string): Promise { return (await drawing(page)).nodes.find((node) => node.name === name)!.id; } async function tap(page: Page, id: string): Promise { 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 { 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('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(); });