"Grundeinstellungen" and "Statische Adresszuweisung" (Java's words) become the dashboard's segmented control: one frame, equal shares, the chosen one in the accent. Measured: "Grundeinstellungen" is 138 px at 14 px and a half of the 340 px sheet holds 139 (157 bold). So the words wrap at their spaces inside a segment, the chosen one is not bold (its width would jump), and where a segment's longest word with its padding no longer fits its share — a phone, another system font — the segments stack, still one control. Decided by measuring the words in the segments' font, not by a breakpoint that would hold for one font only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
601 lines
36 KiB
TypeScript
601 lines
36 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 { HomeRouterSheet, 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 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("sets up a PC's DHCP server on its own page: base settings as Java determines them, static assignments, back to the fields", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
await page.locator('[data-palette="pc"]').click();
|
||
const id = (await drawing(page)).nodes[0]!.id;
|
||
await sheet(page).getByRole('button', { name: 'DHCP-Server einrichten' }).click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('DHCP-Server einrichten');
|
||
await expect(field(page, 'ipAddress')).toBeHidden();
|
||
await expect(field(page, 'dhcpGateway')).toHaveValue('0.0.0.0');
|
||
await expect(field(page, 'dhcpGateway')).toHaveAttribute('readonly', '');
|
||
await expect(field(page, 'dhcpSubnetMask')).toHaveValue('255.255.255.0');
|
||
await field(page, 'lowerBound').fill('192.168.0.300');
|
||
await field(page, 'lowerBound').press('Enter');
|
||
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin 0.0.0.0.');
|
||
await field(page, 'lowerBound').fill('192.168.000.100');
|
||
await field(page, 'lowerBound').press('Enter');
|
||
await expect(field(page, 'lowerBound')).toHaveValue('192.168.0.100');
|
||
await sheet(page).getByLabel('Manuelle Einstellungen').check();
|
||
await expect(field(page, 'dhcpGateway')).not.toHaveAttribute('readonly', '');
|
||
await field(page, 'dhcpGateway').fill('192.168.0.1');
|
||
await field(page, 'dhcpGateway').press('Enter');
|
||
await sheet(page).getByLabel('DHCP aktivieren').check();
|
||
await expect
|
||
.poll(async () => ((await sheetOf(page, id)) as HostSheet).dhcpServer)
|
||
.toMatchObject({ isActive: true, useOwnSettings: true, gateway: '192.168.0.1', lowerBoundIPAddress: '192.168.0.100' });
|
||
|
||
await sheet(page).getByRole('tab', { name: 'Statische Adresszuweisung' }).click();
|
||
await sheet(page).getByRole('button', { name: 'Hinzufügen' }).click();
|
||
await field(page, 'static-macAddress').fill('aa:bb:cc:dd:ee:01');
|
||
await field(page, 'static-macAddress').press('Enter');
|
||
await field(page, 'static-ipAddress').fill('192.168.0.50');
|
||
await field(page, 'static-ipAddress').press('Enter');
|
||
await expect(sheet(page).locator('table[data-rows="static"] tr[data-manual="0"] td').first()).toHaveText('AA:BB:CC:DD:EE:01');
|
||
await sheet(page).getByRole('button', { name: 'Hinzufügen' }).click();
|
||
await field(page, 'static-macAddress').fill('AA:BB:CC:DD:EE:01');
|
||
await field(page, 'static-macAddress').press('Enter');
|
||
await field(page, 'static-ipAddress').fill('192.168.0.51');
|
||
await field(page, 'static-ipAddress').press('Enter');
|
||
await expect(sheet(page).getByRole('alert')).toHaveText('Diese MAC-Adresse hat schon eine feste Adresse.');
|
||
expect(((await sheetOf(page, id)) as HostSheet).dhcpServer.staticAssignments).toHaveLength(1);
|
||
await sheet(page).locator('table[data-rows="static"] tr[data-manual="0"]').click();
|
||
await sheet(page).getByRole('button', { name: 'Entfernen' }).click();
|
||
await expect.poll(async () => ((await sheetOf(page, id)) as HostSheet).dhcpServer.staticAssignments).toEqual([]);
|
||
|
||
// Another device starts at its fields; back at the first, its page is closed too.
|
||
await page.locator('[data-palette="pc"]').click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('Rechner');
|
||
await expect(field(page, 'ipAddress')).toBeVisible();
|
||
await choose(page, id);
|
||
await expect(sheet(page)).toHaveAttribute('data-device', id);
|
||
await sheet(page).getByRole('button', { name: 'DHCP-Server einrichten' }).click();
|
||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('Rechner');
|
||
await expect(field(page, 'ipAddress')).toBeVisible();
|
||
await sheet(page).getByLabel('DHCP zur Konfiguration verwenden').check();
|
||
await expect(sheet(page).getByRole('button', { name: 'DHCP-Server einrichten' })).toBeDisabled();
|
||
});
|
||
|
||
test("keeps a home router's port forwarding on its own page, and shows its DHCP server with the LAN address as gateway", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
await page.locator('[data-palette="gateway"]').click();
|
||
const id = (await drawing(page)).nodes[0]!.id;
|
||
await sheet(page).getByRole('button', { name: 'Portfreigaben' }).click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('Portfreigaben');
|
||
await sheet(page).getByRole('button', { name: 'Neuer Eintrag' }).click();
|
||
await sheet(page).locator('select[name="forwarding-protocolValue"]').selectOption('UDP');
|
||
for (const [column, text] of [
|
||
['publicPortValue', '53'],
|
||
['lanIPAddress', '192.168.0.20'],
|
||
['lanPortValue', '5353'],
|
||
] as const) {
|
||
await field(page, `forwarding-${column}`).fill(text);
|
||
await field(page, `forwarding-${column}`).press('Enter');
|
||
}
|
||
await expect
|
||
.poll(async () => ((await sheetOf(page, id)) as HomeRouterSheet).portForwarding)
|
||
.toEqual([{ protocolValue: 'UDP', publicPortValue: '53', lanIPAddress: '192.168.0.20', lanPortValue: '5353', forwarded: true }]);
|
||
await field(page, 'forwarding-lanPortValue').fill('05353');
|
||
await field(page, 'forwarding-lanPortValue').press('Enter');
|
||
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin 5353.');
|
||
await sheet(page).locator('select[name="forwarding-protocolValue"]').selectOption('TCP');
|
||
await expect.poll(async () => ((await sheetOf(page, id)) as HomeRouterSheet).portForwarding[0]!.protocolValue).toBe('TCP');
|
||
await sheet(page).getByRole('button', { name: 'Eintrag löschen' }).click();
|
||
await expect.poll(async () => ((await sheetOf(page, id)) as HomeRouterSheet).portForwarding).toEqual([]);
|
||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||
|
||
await sheet(page).getByRole('tab', { name: 'LAN: 192.168.0.10' }).click();
|
||
await sheet(page).getByRole('button', { name: 'DHCP-Server einrichten' }).click();
|
||
await expect(field(page, 'dhcpGateway')).toHaveValue('192.168.0.10');
|
||
await expect(field(page, 'dhcpGateway')).toHaveAttribute('readonly', '');
|
||
await expect(field(page, 'dhcpDnsServer')).not.toHaveAttribute('readonly', '');
|
||
await expect(sheet(page).getByLabel('Manuelle Einstellungen')).toHaveCount(0);
|
||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||
// Back on the tab it was opened from, as Java's panel stays behind its dialog.
|
||
await expect(sheet(page).getByRole('tab', { name: 'LAN: 192.168.0.10' })).toHaveAttribute('aria-selected', 'true');
|
||
});
|
||
|
||
test("sets up a router's firewall on its own page: the switches, the default, rules edited, moved and removed", 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 firewall = async () => ((await sheetOf(page, id)) as RouterSheet).firewall;
|
||
await sheet(page).getByRole('button', { name: 'Firewall einrichten' }).click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('Firewall einrichten');
|
||
await expect(sheet(page).getByRole('tab')).toHaveText(['Allgemein', 'Firewall-Regeln']);
|
||
await sheet(page).getByLabel('Firewall aktivieren').check();
|
||
await sheet(page).getByLabel('nur SYN-Pakete verwerfen').uncheck();
|
||
await expect.poll(firewall).toMatchObject({ isActive: true, filterSynSegmentsOnly: false });
|
||
|
||
await sheet(page).getByRole('tab', { name: 'Firewall-Regeln' }).click();
|
||
await sheet(page).locator('select[name="defaultPolicy"]').selectOption('accept');
|
||
await sheet(page).getByRole('button', { name: 'Neue Regel' }).click();
|
||
const rows = sheet(page).locator('table[data-rows="firewall"] tbody tr');
|
||
await expect(rows.first().locator('td')).toHaveText(['1', '', '', '', '', 'TCP', '', 'verwerfen']);
|
||
await expect(sheet(page).getByRole('button', { name: 'rauf' })).toBeDisabled();
|
||
await field(page, 'firewall-sourceIPAddress').fill('abc');
|
||
await field(page, 'firewall-sourceIPAddress').press('Enter');
|
||
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin (leer).');
|
||
await field(page, 'firewall-sourceIPAddress').fill('10.0.0.0');
|
||
await field(page, 'firewall-sourceIPAddress').press('Enter');
|
||
await field(page, 'firewall-sourceSubnetMask').fill('255.0.255.0');
|
||
await field(page, 'firewall-sourceSubnetMask').press('Enter');
|
||
await expect(field(page, 'firewall-sourceSubnetMask')).toHaveAttribute('aria-invalid', 'true');
|
||
await field(page, 'firewall-sourceSubnetMask').fill('255.0.0.0');
|
||
await field(page, 'firewall-sourceSubnetMask').press('Enter');
|
||
await field(page, 'firewall-port').fill('80');
|
||
await field(page, 'firewall-port').press('Enter');
|
||
await sheet(page).locator('select[name="firewall-protocol"]').selectOption('udp');
|
||
await sheet(page).locator('select[name="firewall-action"]').selectOption('accept');
|
||
await expect(rows.first().locator('td')).toHaveText(['1', '10.0.0.0', '255.0.0.0', '', '', 'UDP', '80', 'akzeptieren']);
|
||
|
||
await sheet(page).getByRole('button', { name: 'Neue Regel' }).click();
|
||
await field(page, 'firewall-destinationIPAddress').fill('192.168.0.1');
|
||
await field(page, 'firewall-destinationIPAddress').press('Enter');
|
||
await expect(sheet(page).getByRole('button', { name: 'runter' })).toBeDisabled();
|
||
await sheet(page).getByRole('button', { name: 'rauf' }).click();
|
||
await expect.poll(async () => (await firewall()).rules.map((rule) => rule.destinationIPAddress)).toEqual(['192.168.0.1', '']);
|
||
await expect(rows.first()).toHaveAttribute('aria-selected', 'true');
|
||
await sheet(page).getByRole('button', { name: 'Regel löschen' }).click();
|
||
await expect.poll(async () => (await firewall()).rules.map((rule) => rule.sourceIPAddress)).toEqual(['10.0.0.0']);
|
||
// Java keeps a row chosen: the one that took the removed one's place.
|
||
await expect(rows.first()).toHaveAttribute('aria-selected', 'true');
|
||
await expect(field(page, 'firewall-sourceIPAddress')).toHaveValue('10.0.0.0');
|
||
await expect(await firewall()).toMatchObject({ defaultPolicy: 'accept' });
|
||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('Vermittlungsrechner');
|
||
});
|
||
|
||
test("manages a router's connections on its own page: cables swapped with two taps, cards added and removed, the last kept", 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 joined = async () => ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.device ?? '');
|
||
const values = (await sheetOf(page, id)) as RouterSheet;
|
||
const [one, two] = await joined();
|
||
const cables = (await drawing(page)).links.length;
|
||
await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click();
|
||
await expect(sheet(page).getByRole('heading', { level: 2 })).toHaveText('Verbindungen verwalten');
|
||
const items = sheet(page).locator('ol.connections li');
|
||
const port = (index: number) => items.nth(index).getByRole('button');
|
||
await expect(items.locator('.connection-local')).toHaveText(values.interfaces.map((card, index) => `NIC ${index + 1}: ${card.ipAddress}`));
|
||
await expect(items.locator('.connection-remote')).toHaveText([one!, two!]);
|
||
// Java's colours: green for a plugged card, yellow for the marked one, red for a free one.
|
||
await expect(port(0)).toHaveCSS('background-color', 'rgb(0, 255, 0)');
|
||
await port(0).click();
|
||
await expect(port(0)).toHaveAttribute('aria-pressed', 'true');
|
||
await expect(port(0)).toHaveCSS('background-color', 'rgb(255, 255, 0)');
|
||
await port(0).click();
|
||
await expect(port(0)).toHaveAttribute('aria-pressed', 'false');
|
||
await port(0).click();
|
||
await port(1).click();
|
||
await expect.poll(joined).toEqual([two, one]);
|
||
await expect(items.nth(0)).toHaveAttribute('data-state', 'plugged');
|
||
|
||
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||
await expect(items).toHaveCount(3);
|
||
await expect(items.nth(2).locator('.connection-local')).toHaveText('NIC 3: 192.168.0.10');
|
||
await expect(port(2)).toHaveCSS('background-color', 'rgb(255, 0, 0)');
|
||
await expect(port(2)).toHaveAccessibleName('NIC 3: 192.168.0.10, Keine Verbindung');
|
||
// A free card takes the marked card's cable.
|
||
await port(0).click();
|
||
await port(2).click();
|
||
await expect.poll(joined).toEqual(['', one, two]);
|
||
await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
|
||
await expect.poll(joined).toEqual(['', one]);
|
||
expect((await drawing(page)).links).toHaveLength(cables - 1);
|
||
|
||
for (let count = 2; count < 8; count++) await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||
await expect(items).toHaveCount(8);
|
||
await expect(sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' })).toBeDisabled();
|
||
await expect(sheet(page).getByText('Maximal 8 Schnittstellen erlaubt!')).toBeVisible();
|
||
for (let count = 8; count > 1; count--) await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
|
||
await expect(items).toHaveCount(1);
|
||
await expect(sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' })).toBeDisabled();
|
||
await expect(sheet(page).getByText('Eine Schnittstelle bleibt immer.')).toBeVisible();
|
||
|
||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||
const tabs = sheet(page).getByRole('tab');
|
||
await expect(tabs).toHaveText(['Allgemein', values.interfaces[0]!.ipAddress, 'Weiterleitungstabelle']);
|
||
// Undone, the second card comes back with its cable; a card's tab whose card goes again shows the general tab.
|
||
await page.keyboard.press('Control+z');
|
||
await expect(tabs).toHaveCount(4);
|
||
await tabs.nth(2).click();
|
||
await expect(sheet(page).locator('.sheet-connected')).toHaveText(`Verbunden mit ${one}`);
|
||
await page.keyboard.press('Control+Shift+z');
|
||
await expect(tabs).toHaveCount(3);
|
||
await expect(tabs.first()).toHaveAttribute('aria-selected', 'true');
|
||
await expect(field(page, 'name')).toHaveValue(values.name);
|
||
});
|
||
|
||
test("names a router's computer by its address, and lets go of a marked card when cards change", async ({ page }) => {
|
||
await openEditor(page, server.base, 'internetwork_komplett.fls');
|
||
const routers = (await drawing(page)).nodes.filter((node) => node.kind === 'router');
|
||
let id = '';
|
||
for (const each of routers) if (((await sheetOf(page, each.id)) as RouterSheet).interfaces.some((card) => card.connected?.address)) id = each.id;
|
||
await choose(page, id);
|
||
const values = (await sheetOf(page, id)) as RouterSheet;
|
||
await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click();
|
||
const items = sheet(page).locator('ol.connections li');
|
||
const port = (index: number) => items.nth(index).getByRole('button');
|
||
// Java: a computer's or a router's name with the address of its card, a switch's name alone.
|
||
await expect(items.locator('.connection-remote')).toHaveText(
|
||
values.interfaces.flatMap((card) => (card.connected === null ? [] : [card.connected.address === null ? card.connected.device : `${card.connected.device} (${card.connected.address})`])),
|
||
);
|
||
expect(values.interfaces.some((card) => card.connected?.address !== null && card.connected?.address !== undefined)).toBe(true);
|
||
const pressed = () => sheet(page).locator('.connection-port[aria-pressed="true"]');
|
||
await port(0).click();
|
||
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||
await expect(pressed()).toHaveCount(0);
|
||
await port(0).click();
|
||
await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
|
||
await expect(pressed()).toHaveCount(0);
|
||
// A marked card undone away is marked no more: the next tap marks, it swaps nothing.
|
||
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||
const count = values.interfaces.length + 1;
|
||
await port(count - 1).click();
|
||
await page.keyboard.press('Control+z');
|
||
await expect(items).toHaveCount(count - 1);
|
||
const before = ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null);
|
||
await port(0).click();
|
||
await expect(port(0)).toHaveAttribute('aria-pressed', 'true');
|
||
expect(((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null)).toEqual(before);
|
||
});
|
||
|
||
test("opens a home router's firewall from its general tab", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
await page.locator('[data-palette="gateway"]').click();
|
||
await sheet(page).getByRole('button', { name: 'Firewall einrichten' }).click();
|
||
await expect(sheet(page).getByRole('heading')).toHaveText('Firewall einrichten');
|
||
await expect(sheet(page).getByLabel('Firewall aktivieren')).not.toBeChecked();
|
||
});
|
||
|
||
/** The DHCP page's segmented control: its frame, its two segments' boxes, and how they stand. */
|
||
async function dhcpSegments(page: Page) {
|
||
await page.locator('[data-palette="pc"]').click();
|
||
await sheet(page).getByRole('button', { name: 'DHCP-Server einrichten' }).click();
|
||
const control = sheet(page).getByRole('tablist');
|
||
await expect(control).toHaveClass('sheet-segments');
|
||
const boxes = await Promise.all([0, 1].map(async (index) => (await control.getByRole('tab').nth(index).boundingBox())!));
|
||
return { control, boxes };
|
||
}
|
||
|
||
test("shows a DHCP server's two pages as a segmented control: side by side in the sheet, the long word wrapped, nothing cut off", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
const { control, boxes } = await dhcpSegments(page);
|
||
await expect(control).not.toHaveAttribute('data-stacked', 'true');
|
||
const [basic, assignments] = boxes as [{ x: number; y: number; width: number; height: number }, { x: number; y: number; width: number; height: number }];
|
||
// One row, equal shares, equal heights — „Statische Adresszuweisung" on two lines inside its 44 px.
|
||
expect(Math.round(basic.y)).toBe(Math.round(assignments.y));
|
||
expect(Math.abs(basic.width - assignments.width)).toBeLessThanOrEqual(1);
|
||
expect(basic.height).toBe(assignments.height);
|
||
expect(basic.height).toBeGreaterThanOrEqual(44);
|
||
// No word runs over its segment's edge.
|
||
for (const tab of await control.getByRole('tab').all()) {
|
||
expect(await tab.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||
}
|
||
// The chosen one in the accent, the other quiet; a choice moves it.
|
||
const tabs = control.getByRole('tab');
|
||
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'true');
|
||
await tabs.nth(1).click();
|
||
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true');
|
||
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'false');
|
||
// Not bold when chosen: the width does not jump.
|
||
expect(Math.round((await tabs.nth(1).boundingBox())!.width)).toBe(Math.round(assignments.width));
|
||
});
|
||
|
||
test("stacks the DHCP server's segments exactly where a segment's longest word, with its padding, no longer fits its share of the row", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
const { control } = await dhcpSegments(page);
|
||
// What one segment needs: its longest word in the segments' font, and its padding.
|
||
const need = await control.evaluate((list) => {
|
||
const segments = [...list.querySelectorAll<HTMLElement>('[role="tab"]')];
|
||
const style = getComputedStyle(segments[0]!);
|
||
const measure = document.createElement('canvas').getContext('2d')!;
|
||
measure.font = style.font;
|
||
const words = segments.flatMap((segment) => (segment.textContent ?? '').split(/\s+/));
|
||
return Math.max(...words.map((word) => measure.measureText(word).width)) + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
||
});
|
||
// Two shares and the line between them, inside the frame; a little under and a little over.
|
||
const inside = async (width: number) => control.evaluate((list, value) => { list.style.boxSizing = 'content-box'; list.style.width = `${value}px`; }, width);
|
||
await inside(2 * need + 1 - 0.6);
|
||
await expect(control).toHaveAttribute('data-stacked', 'true');
|
||
await inside(2 * need + 1 + 0.6);
|
||
await expect(control).not.toHaveAttribute('data-stacked', 'true');
|
||
});
|
||
|
||
test.describe('on a phone', () => {
|
||
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });
|
||
|
||
test("stacks the DHCP server's segments where a word no longer fits its share — still one control", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
const { control, boxes } = await dhcpSegments(page);
|
||
await expect(control).toHaveAttribute('data-stacked', 'true');
|
||
await expect(control).toHaveAttribute('aria-orientation', 'vertical');
|
||
const [basic, assignments] = boxes as [{ x: number; y: number; width: number; height: number }, { x: number; y: number; width: number; height: number }];
|
||
expect(Math.round(basic.x)).toBe(Math.round(assignments.x));
|
||
expect(assignments.y).toBeGreaterThanOrEqual(basic.y + basic.height - 1);
|
||
for (const tab of await control.getByRole('tab').all()) {
|
||
expect(await tab.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(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();
|
||
});
|