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