Files
mdg-filius/packages/editor/browser/sheet.spec.ts
T
julian.kiedaischandClaude Opus 5.5 eff9433673 feat(editor): F4.5b the device sheet for routers and home routers
Measured first: SheetRun now types into FILIUS' router and home router
panels, records five forwarding tables (holeTabelle) and steps in the
panel's table. Java turns a card address that passes the pattern but not
IP.ipCheck (".0.0.1") into null and the router panel throws; an empty
gateway is marked red yet taken; a refused table cell is stored as null;
a home router's DHCP client leaves its DHCP server running. None of the
nulls is adopted. The core's routeTableRows matches all five tables.

- doc: card addresses (the first card mirrored in the device's settings),
  router gateway, RIP, manual routes (a new row once all four cells pass,
  a cell, removal); the sheet reads cards with what their cable joins and
  the whole table
- editor: Java's tabs (general, each card titled by its address, the
  forwarding table greyed out under RIP; general, LAN, WAN on a home
  router); the open card's cable drawn active in Java's green

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

280 lines
15 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 device sheet in Chromium (Playwright): it opens with the device that
// is chosen, shows Java's fields, takes a field when it is left, marks and
// keeps a text its rule refuses, moves a notebook into a wireless network —
// beside the surface on a wide screen, below it on a phone.
import { expect, test, type Page } from '@playwright/test';
import type { HostSheet, RouterSheet } from '@mdg-filius/doc';
import { drawing, onDevice, openEditor, serveEditor, sheetOf } 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 choose(page: Page, id: string): Promise<void> {
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
}
const sheet = (page: Page) => page.getByRole('complementary', { name: 'Einstellungen' });
const field = (page: Page, name: string) => sheet(page).locator(`input[name="${name}"]`);
test("opens with the chosen device, shows Java's fields beside the surface, and closes letting go of it", async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
await expect(sheet(page)).toHaveCount(0);
const id = await deviceNamed(page, 'Notebook 1.10');
await choose(page, id);
await expect(sheet(page)).toBeVisible();
await expect(sheet(page).getByRole('heading')).toHaveText('Notebook');
const values = (await sheetOf(page, id)) as HostSheet;
await expect(field(page, 'name')).toHaveValue('Notebook 1.10');
await expect(field(page, 'macAddress')).toHaveValue(values.macAddress);
await expect(field(page, 'ipAddress')).toHaveValue(values.ipAddress);
await expect(field(page, 'subnetMask')).toHaveValue(values.subnetMask);
await expect(field(page, 'defaultGateway')).toHaveValue(values.defaultGateway);
await expect(sheet(page).getByLabel('Kabelgebunden (LAN)')).toBeChecked();
await expect(sheet(page).getByLabel('DHCP zur Konfiguration verwenden')).not.toBeChecked();
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
const aside = (await sheet(page).boundingBox())!;
expect(aside.x).toBeGreaterThanOrEqual(surface.x + surface.width - 1);
expect(aside.width).toBe(340);
await sheet(page).getByRole('button', { name: 'Schließen' }).click();
await expect(sheet(page)).toHaveCount(0);
await expect(page.locator(`g.device[data-node="${id}"]`)).not.toHaveAttribute('data-selected', 'true');
});
test('marks a refused address while it is typed, keeps it with what still holds, and takes a mended one', async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="pc"]').click();
await expect(sheet(page)).toBeVisible();
const id = (await drawing(page)).nodes[0]!.id;
const ip = field(page, 'ipAddress');
await ip.fill('10.0.0.256');
await expect(ip).toHaveAttribute('aria-invalid', 'true');
await field(page, 'subnetMask').focus();
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin 192.168.0.10.');
await expect(ip).toHaveValue('10.0.0.256');
expect(((await sheetOf(page, id)) as HostSheet).ipAddress).toBe('192.168.0.10');
await ip.fill('010.0.0.5');
await expect(ip).toHaveAttribute('aria-invalid', 'false');
await expect(sheet(page).getByRole('status')).toHaveCount(0);
await ip.press('Enter');
await expect(ip).toHaveValue('10.0.0.5');
expect(((await sheetOf(page, id)) as HostSheet).ipAddress).toBe('10.0.0.5');
// Escape gives a text up; the empty gateway is taken, the empty address is not.
await ip.fill('1.2.3');
await ip.press('Escape');
await expect(ip).toHaveValue('10.0.0.5');
await expect(sheet(page)).toBeVisible();
await field(page, 'defaultGateway').fill('');
await field(page, 'defaultGateway').press('Enter');
await ip.fill('');
await ip.press('Enter');
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin 10.0.0.5.');
});
test('takes a change back as one step, and shows the address as the name', async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="pc"]').click();
const id = (await drawing(page)).nodes[0]!.id;
await field(page, 'ipAddress').fill('10.0.0.9');
await field(page, 'ipAddress').press('Enter');
await sheet(page).getByLabel('IP-Adresse als Name verwenden').check();
await expect.poll(async () => (await drawing(page)).nodes[0]!.label).toBe('10.0.0.9');
await expect(field(page, 'name')).toBeDisabled();
await expect(field(page, 'name')).toHaveValue('10.0.0.9');
await page.getByRole('button', { name: 'Rückgängig' }).click();
await expect.poll(async () => (await drawing(page)).nodes[0]!.label).toBe('Rechner');
await expect(field(page, 'name')).toBeEnabled();
await page.getByRole('button', { name: 'Rückgängig' }).click();
await expect(field(page, 'ipAddress')).toHaveValue('192.168.0.10');
expect(((await sheetOf(page, id)) as HostSheet).ipAddress).toBe('192.168.0.10');
});
test('takes the text of one device when another is chosen, and shows the other', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const first = await deviceNamed(page, 'Notebook 1.10');
const second = await deviceNamed(page, 'Notebook 1.11');
await choose(page, first);
await field(page, 'dnsServer').fill('192.168.1.53');
await choose(page, second);
await expect(sheet(page)).toHaveAttribute('data-device', second);
expect(((await sheetOf(page, first)) as HostSheet).dnsServer).toBe('192.168.1.53');
await expect(field(page, 'name')).toHaveValue('Notebook 1.11');
await expect(field(page, 'dnsServer')).toHaveValue(((await sheetOf(page, second)) as HostSheet).dnsServer);
});
test('offers no text of one device to the next, a refused one neither', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const first = await deviceNamed(page, 'Notebook 1.10');
const second = await deviceNamed(page, 'Notebook 1.11');
await choose(page, first);
await field(page, 'ipAddress').fill('300.0.0.1');
await choose(page, second);
await expect(sheet(page)).toHaveAttribute('data-device', second);
await expect(field(page, 'ipAddress')).toHaveValue(((await sheetOf(page, second)) as HostSheet).ipAddress);
await expect(field(page, 'ipAddress')).toHaveAttribute('aria-invalid', 'false');
await expect(sheet(page).getByRole('status')).toHaveCount(0);
});
test('closes the address fields while DHCP configures the card, as Java does', async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="pc"]').click();
const fields = ['ipAddress', 'subnetMask', 'defaultGateway', 'dnsServer'];
await sheet(page).getByLabel('DHCP zur Konfiguration verwenden').check();
for (const name of fields) await expect(field(page, name)).toBeDisabled();
await expect(field(page, 'name')).toBeEnabled();
await sheet(page).getByLabel('DHCP zur Konfiguration verwenden').uncheck();
for (const name of fields) await expect(field(page, name)).toBeEnabled();
});
test("joins a notebook to a switch's wireless network, drawn as wireless", async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="networkSwitch"]').click();
const hub = (await drawing(page)).nodes[0]!.id;
await field(page, 'ssid').fill('Schule');
await field(page, 'ssid').press('Enter');
await sheet(page).getByLabel('als Wolke darstellen').check();
await expect.poll(async () => (await drawing(page)).nodes[0]!.cloud).toBe(true);
await page.locator('[data-palette="notebook"]').click();
await expect(sheet(page).getByRole('heading')).toHaveText('Notebook');
await expect(sheet(page).locator('select[name="ssid"]')).toBeDisabled();
await sheet(page).getByLabel('Drahtlos (WLAN)').check();
await sheet(page).locator('select[name="ssid"]').selectOption('Schule');
await expect.poll(async () => (await drawing(page)).links.map((link) => [link.targetNode, link.wireless])).toEqual([[hub, true]]);
await sheet(page).getByLabel('Kabelgebunden (LAN)').check();
await expect.poll(async () => (await drawing(page)).links).toEqual([]);
});
test("shows a router's cards in Java's tabs, draws the open card's cable active, and renames a tab by its address", async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const id = (await drawing(page)).nodes.find((node) => node.kind === 'router')!.id;
await choose(page, id);
const values = (await sheetOf(page, id)) as RouterSheet;
const tabs = sheet(page).getByRole('tab');
await expect(tabs).toHaveText(['Allgemein', ...values.interfaces.map((card) => card.ipAddress), 'Weiterleitungstabelle']);
await expect(field(page, 'name')).toHaveValue(values.name);
await expect(sheet(page).getByLabel('IP-Weiterleitung aktivieren')).toBeDisabled();
await expect(sheet(page).getByLabel('IP-Weiterleitung aktivieren')).toBeChecked();
const card = values.interfaces[0]!;
await tabs.nth(1).click();
await expect(sheet(page).locator('.sheet-connected')).toHaveText(`Verbunden mit ${card.connected!.device}`);
await expect(page.locator(`path[data-link="${card.connected!.link}"]`)).toHaveAttribute('data-active', 'true');
// Java's active cable is green (JCablePanel.farbeBlinken).
await expect(page.locator(`path[data-link="${card.connected!.link}"]`)).toHaveCSS('stroke', 'rgb(0, 255, 64)');
await field(page, 'ipAddress').fill('192.168.0.1');
await field(page, 'ipAddress').press('Enter');
await expect(tabs.nth(1)).toHaveText('192.168.0.1');
await tabs.first().click();
await expect(page.locator('path[data-active]')).toHaveCount(0);
await tabs.nth(1).click();
await sheet(page).getByRole('button', { name: 'Schließen' }).click();
await expect(page.locator('path[data-active]')).toHaveCount(0);
});
test("edits a router's forwarding table: a new row once its four cells pass, a cell, the row removed; RIP greys it out", async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="router"]').click();
await page.getByRole('dialog').getByRole('button', { name: '2', exact: true }).click();
const id = (await drawing(page)).nodes[0]!.id;
const tabs = sheet(page).getByRole('tab');
await tabs.last().click();
const rows = sheet(page).locator('.route-table tbody tr');
// Java's own rows: two addresses, two networks (the same twice: both cards are new), localhost.
await expect(rows).toHaveCount(5);
await sheet(page).getByRole('button', { name: 'Neuer Eintrag' }).click();
await expect(sheet(page).getByText('Die Zeile wird übernommen, sobald alle vier Felder gültig sind.')).toBeVisible();
await expect(field(page, 'route-destinationNetwork')).toHaveAttribute('aria-invalid', 'false');
for (const [column, text] of [
['destinationNetwork', '10.0.0.0'],
['subnetMask', '255.0.0.0'],
['gateway', '192.168.0.1'],
] as const) {
await field(page, `route-${column}`).fill(text);
await field(page, `route-${column}`).press('Enter');
}
expect(((await sheetOf(page, id)) as RouterSheet).routes).toEqual([]);
await field(page, 'route-interfaceIPAddress').fill('192.168.0.10');
await field(page, 'route-interfaceIPAddress').press('Enter');
await expect.poll(async () => ((await sheetOf(page, id)) as RouterSheet).routes).toEqual([{ destinationNetwork: '10.0.0.0', subnetMask: '255.0.0.0', gateway: '192.168.0.1', interfaceIPAddress: '192.168.0.10' }]);
await expect(sheet(page).locator('tr[data-manual="0"]')).toHaveAttribute('aria-selected', 'true');
await field(page, 'route-gateway').fill('192.168.0.2');
await field(page, 'route-gateway').press('Enter');
await expect(sheet(page).locator('tr[data-manual="0"] td').nth(2)).toHaveText('192.168.0.2');
await sheet(page).getByLabel('Alle Einträge anzeigen').uncheck();
await expect(rows).toHaveCount(1);
await sheet(page).getByLabel('Alle Einträge anzeigen').check();
await sheet(page).locator('tr[data-manual="0"]').click();
await sheet(page).getByRole('button', { name: 'Eintrag löschen' }).click();
await expect.poll(async () => ((await sheetOf(page, id)) as RouterSheet).routes).toEqual([]);
await expect(rows).toHaveCount(5);
await tabs.first().click();
await sheet(page).getByLabel('Automatisches Routing').check();
await expect(tabs.last()).toBeDisabled();
await sheet(page).getByLabel('Automatisches Routing').uncheck();
await expect(tabs.last()).toBeEnabled();
// RIP switched on while the table is open (here: taken back by undo) closes it.
await tabs.last().click();
await expect(sheet(page).locator('.route-table')).toBeVisible();
await page.getByRole('button', { name: 'Rückgängig' }).click();
await expect(tabs.last()).toBeDisabled();
await expect(tabs.first()).toHaveAttribute('aria-selected', 'true');
await expect(sheet(page).getByLabel('Automatisches Routing')).toBeChecked();
});
test("shows a home router's LAN and WAN tabs, DHCP closing the WAN card's address", async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="gateway"]').click();
const tabs = sheet(page).getByRole('tab');
await expect(tabs).toHaveText(['Allgemein', 'LAN: 192.168.0.10', 'WAN: 42.0.0.10']);
await expect(sheet(page).locator('.sheet-connected')).toHaveCount(0);
await tabs.nth(2).click();
await expect(sheet(page).locator('.sheet-connected')).toHaveText('Keine Verbindung');
await sheet(page).getByLabel('DHCP zur Konfiguration verwenden').check();
await expect(field(page, 'ipAddress')).toBeDisabled();
await expect(field(page, 'subnetMask')).toBeDisabled();
await tabs.nth(1).click();
await expect(field(page, 'ipAddress')).toBeEnabled();
await field(page, 'ipAddress').fill('192.168.7.1');
await field(page, 'ipAddress').press('Enter');
await expect(tabs.nth(1)).toHaveText('LAN: 192.168.7.1');
});
test.describe('on a phone', () => {
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });
test('opens below the surface with a tap, the device still in sight', 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.touchscreen.tap(at.x, at.y);
await expect(sheet(page)).toBeVisible();
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
const aside = (await sheet(page).boundingBox())!;
expect(aside.y).toBeGreaterThanOrEqual(surface.y + surface.height - 1);
expect(aside.height).toBeLessThanOrEqual(844 / 2);
expect(surface.height).toBeGreaterThan(200);
const fieldBox = (await field(page, 'ipAddress').boundingBox())!;
expect(fieldBox.height).toBeGreaterThanOrEqual(44);
});
});
test('speaks English when asked', async ({ page }) => {
await openEditor(page, server.base, 'new', '&lang=en');
await page.locator('[data-palette="networkSwitch"]').click();
const aside = page.getByRole('complementary', { name: 'Settings' });
await expect(aside.getByText('Wi-Fi Name (SSID)')).toBeVisible();
await expect(aside.getByText('Retention Time SAT Entries (Seconds)')).toBeVisible();
await expect(aside.getByLabel('Display as Cloud')).not.toBeChecked();
});