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>
280 lines
15 KiB
TypeScript
280 lines
15 KiB
TypeScript
// 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();
|
||
});
|