// The device sheet in Chromium (Playwright): it opens with the device that // is chosen, shows Java's fields, takes a field when it is left, marks and // keeps a text its rule refuses, moves a notebook into a wireless network — // beside the surface on a wide screen, below it on a phone. import { expect, test, type Page } from '@playwright/test'; import type { HostSheet, RouterSheet } from '@mdg-filius/doc'; import { drawing, onDevice, openEditor, serveEditor, sheetOf } from './editorPage.js'; let server: { readonly base: string; readonly close: () => Promise }; 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 vier Felder gültig sind.')).toBeVisible(); await expect(field(page, 'route-destinationNetwork')).toHaveAttribute('aria-invalid', 'false'); for (const [column, text] of [ ['destinationNetwork', '10.0.0.0'], ['subnetMask', '255.0.0.0'], ['gateway', '192.168.0.1'], ] as const) { await field(page, `route-${column}`).fill(text); await field(page, `route-${column}`).press('Enter'); } expect(((await sheetOf(page, id)) as RouterSheet).routes).toEqual([]); await field(page, 'route-interfaceIPAddress').fill('192.168.0.10'); await field(page, 'route-interfaceIPAddress').press('Enter'); await expect.poll(async () => ((await sheetOf(page, id)) as RouterSheet).routes).toEqual([{ destinationNetwork: '10.0.0.0', subnetMask: '255.0.0.0', gateway: '192.168.0.1', interfaceIPAddress: '192.168.0.10' }]); await expect(sheet(page).locator('tr[data-manual="0"]')).toHaveAttribute('aria-selected', 'true'); await field(page, 'route-gateway').fill('192.168.0.2'); await field(page, 'route-gateway').press('Enter'); await expect(sheet(page).locator('tr[data-manual="0"] td').nth(2)).toHaveText('192.168.0.2'); await sheet(page).getByLabel('Alle Einträge anzeigen').uncheck(); await expect(rows).toHaveCount(1); await sheet(page).getByLabel('Alle Einträge anzeigen').check(); await sheet(page).locator('tr[data-manual="0"]').click(); await sheet(page).getByRole('button', { name: 'Eintrag löschen' }).click(); await expect.poll(async () => ((await sheetOf(page, id)) as RouterSheet).routes).toEqual([]); await expect(rows).toHaveCount(5); await tabs.first().click(); await sheet(page).getByLabel('Automatisches Routing').check(); await expect(tabs.last()).toBeDisabled(); await sheet(page).getByLabel('Automatisches Routing').uncheck(); await expect(tabs.last()).toBeEnabled(); // RIP switched on while the table is open (here: taken back by undo) closes it. await tabs.last().click(); await expect(sheet(page).locator('.route-table')).toBeVisible(); await page.getByRole('button', { name: 'Rückgängig' }).click(); await expect(tabs.last()).toBeDisabled(); await expect(tabs.first()).toHaveAttribute('aria-selected', 'true'); await expect(sheet(page).getByLabel('Automatisches Routing')).toBeChecked(); }); test("shows a home router's LAN and WAN tabs, DHCP closing the WAN card's address", async ({ page }) => { await openEditor(page, server.base, 'new'); await page.locator('[data-palette="gateway"]').click(); const tabs = sheet(page).getByRole('tab'); await expect(tabs).toHaveText(['Allgemein', 'LAN: 192.168.0.10', 'WAN: 42.0.0.10']); await expect(sheet(page).locator('.sheet-connected')).toHaveCount(0); await tabs.nth(2).click(); await expect(sheet(page).locator('.sheet-connected')).toHaveText('Keine Verbindung'); await sheet(page).getByLabel('DHCP zur Konfiguration verwenden').check(); await expect(field(page, 'ipAddress')).toBeDisabled(); await expect(field(page, 'subnetMask')).toBeDisabled(); await tabs.nth(1).click(); await expect(field(page, 'ipAddress')).toBeEnabled(); await field(page, 'ipAddress').fill('192.168.7.1'); await field(page, 'ipAddress').press('Enter'); await expect(tabs.nth(1)).toHaveText('LAN: 192.168.7.1'); }); test.describe('on a phone', () => { test.use({ viewport: { width: 390, height: 844 }, hasTouch: true }); test('opens below the surface with a tap, the device still in sight', async ({ page }) => { await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls'); const id = await deviceNamed(page, 'Notebook 1.10'); const at = await onDevice(page, id); await page.touchscreen.tap(at.x, at.y); await expect(sheet(page)).toBeVisible(); const surface = (await page.locator('svg.filius-canvas').boundingBox())!; const aside = (await sheet(page).boundingBox())!; expect(aside.y).toBeGreaterThanOrEqual(surface.y + surface.height - 1); expect(aside.height).toBeLessThanOrEqual(844 / 2); expect(surface.height).toBeGreaterThan(200); const fieldBox = (await field(page, 'ipAddress').boundingBox())!; expect(fieldBox.height).toBeGreaterThanOrEqual(44); }); }); test('speaks English when asked', async ({ page }) => { await openEditor(page, server.base, 'new', '&lang=en'); await page.locator('[data-palette="networkSwitch"]').click(); const aside = page.getByRole('complementary', { name: 'Settings' }); await expect(aside.getByText('Wi-Fi Name (SSID)')).toBeVisible(); await expect(aside.getByText('Retention Time SAT Entries (Seconds)')).toBeVisible(); await expect(aside.getByLabel('Display as Cloud')).not.toBeChecked(); });