A button at the top of the palette and the documentation's bar folds it to a slim column, as the dashboard's own bar does; one state for both, kept in the browser. Up to 639 px the device sheet covers the whole editor, over the bar and the mode buttons, instead of sharing half the height below the surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
621 lines
37 KiB
TypeScript
621 lines
37 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, over the whole editor 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("sets the DHCP server's segments side by side in the sheet that covers the phone, and stacks them on a small one — still one control", async ({ page }) => {
|
||
await openEditor(page, server.base, 'new');
|
||
const { control } = await dhcpSegments(page);
|
||
await expect(control).not.toHaveAttribute('data-stacked', 'true');
|
||
// 320 px: the share of „Grundeinstellungen" falls below the word.
|
||
await page.setViewportSize({ width: 320, height: 700 });
|
||
await expect(control).toHaveAttribute('data-stacked', 'true');
|
||
const boxes = await Promise.all([0, 1].map(async (index) => (await control.getByRole('tab').nth(index).boundingBox())!));
|
||
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('covers the whole editor with a tap, the bar and the mode buttons too, and gives the surface back when closed', 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();
|
||
expect(await sheet(page).boundingBox()).toEqual(await page.locator('.editor-root').boundingBox());
|
||
// What lies under it does not take the finger: the palette, the mode buttons.
|
||
for (const under of [page.locator('[data-palette="pc"]'), page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' })]) {
|
||
const box = (await under.boundingBox())!;
|
||
const top = await page.evaluate(([x, y]) => Boolean(document.elementFromPoint(x!, y!)?.closest('.device-sheet')), [box.x + box.width / 2, box.y + box.height / 2]);
|
||
expect(top).toBe(true);
|
||
}
|
||
const fieldBox = (await field(page, 'ipAddress').boundingBox())!;
|
||
expect(fieldBox.height).toBeGreaterThanOrEqual(44);
|
||
await sheet(page).getByRole('button', { name: 'Schließen' }).tap();
|
||
await expect(sheet(page)).toHaveCount(0);
|
||
await expect(page.locator('svg.filius-canvas')).toBeVisible();
|
||
});
|
||
});
|
||
|
||
test('covers the editor from the phone width down, 639 px, and stands beside the surface from 640', async ({ page }) => {
|
||
await page.setViewportSize({ width: 640, height: 800 });
|
||
await openEditor(page, server.base, 'new');
|
||
await page.locator('[data-palette="pc"]').click();
|
||
await expect(sheet(page)).toBeVisible();
|
||
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||
expect((await sheet(page).boundingBox())!.x).toBeGreaterThanOrEqual(surface.x + surface.width - 1);
|
||
await page.setViewportSize({ width: 639, height: 800 });
|
||
await expect.poll(async () => (await sheet(page).boundingBox())!.width).toBe(639);
|
||
});
|
||
|
||
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();
|
||
});
|