// The data exchange window in Chromium (F4.7), on browser/fixtures/web-pages.fls: // Java's menu offers a computer's desktop and each card's data exchange with // its address; the browser's card while it fetches a page — a row per frame // in its layer's colour, the chosen frame's details, the table as a text // file named after the tab —, a router's two cards beside it, "Tabellen // löschen", "Automatisches Scrollen" off, a restart emptying it, a tab closed, the window gone with // the design and back with the next start without the tab of a computer // removed; and on a phone. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { expect, test, type Page } from '@playwright/test'; import { drawing, onDevice, serveEditor, showDesktop } from './editorPage.js'; let server: { readonly base: string; readonly close: () => Promise }; test.beforeAll(async () => { server = await serveEditor(); }); test.afterAll(async () => { await server.close(); }); const FIXTURE = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/web-pages.fls'); async function openPages(page: Page): Promise { await page.goto(`${server.base}?example=new`); await page.waitForSelector('svg.filius-canvas'); await page.locator('input[type="file"]').setInputFiles(FIXTURE); await expect.poll(async () => (await drawing(page).catch(() => ({ nodes: [] }))).nodes.length).toBe(4); } async function start(page: Page): Promise { await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click(); await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation'); } const idOf = async (page: Page, name: string) => (await drawing(page)).nodes.find((node) => node.name === name)!.id; /** Java's menu of a device in the simulation, by a long press. */ async function menuOf(page: Page, name: string) { const at = await onDevice(page, await idOf(page, name)); await page.mouse.move(at.x, at.y); await page.mouse.down(); await page.waitForTimeout(700); await page.mouse.up(); return page.getByRole('menuitem'); } test("shows a card's frames as Java's data exchange window does, with their details, as a text file, and empties every card's", async ({ page }) => { test.setTimeout(120_000); await openPages(page); await start(page); // Java's menu: the desktop, then the card's data exchange with its address. await expect(await menuOf(page, 'Browser')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (192.168.0.10)']); await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (192.168.0.10)' }).click(); const window = page.locator('section[data-capture]'); await expect(window.locator('h2')).toHaveText('Datenaustausch'); await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10']); await expect(window.locator('th')).toHaveText(['Nr.', 'Zeit', 'Quelle', 'Ziel', 'Protokoll', 'Schicht', 'Bemerkungen / Details']); await expect(window.locator('tbody tr')).toHaveCount(0); // Closed, it keeps its tab (Java hides its dialog). await window.locator('.simulation-sheet__close').click(); await expect(window).toHaveCount(0); // The browser fetches the server's page: a row per frame, its highest layer's, in its colour. const desktop = await showDesktop(page, await idOf(page, 'Browser')); await desktop.locator('[data-program="webBrowser"]').click(); const address = desktop.getByRole('textbox', { name: 'Adresse' }); await address.fill('192.168.0.20'); await address.press('Enter'); await expect(desktop.locator('.program-titlebar h3')).toHaveText('Seiten & Formulare', { timeout: 30_000 }); await desktop.locator('.simulation-sheet__close').click(); await (await menuOf(page, 'Browser')).filter({ hasText: 'Datenaustausch' }).click(); await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10']); const rows = window.locator('tbody tr'); await expect(rows.first().locator('td')).toHaveText(['1', /^\d\d:\d\d:\d\d\.\d\d\d$/, '192.168.0.10', '192.168.0.20', 'ARP', 'Vermittlung', /^Suche nach MAC für 192\.168\.0\.20 \[op=REQUEST/]); await expect(rows.first()).toHaveClass(/capture-internet/); const get = rows.filter({ hasText: 'GET / HTTP/1.1' }).first(); await expect(get.locator('td').nth(4)).toHaveText('HTTP'); await expect(get.locator('td').nth(5)).toHaveText('Anwendung'); await expect(get.locator('td').nth(2)).toHaveText(/^192\.168\.0\.10:\d{5}$/); await expect(get).toHaveClass(/capture-application/); // Java's colours (LayerColorVar): application #FFD966, transport #F4B183. expect(await get.evaluate((row) => getComputedStyle(row).backgroundColor)).toBe('rgb(255, 217, 102)'); const syn = rows.filter({ hasText: /^.*SYN, SEQ: [\d.]+$/ }).first(); await expect(syn).toHaveClass(/capture-transport/); expect(await syn.evaluate((row) => getComputedStyle(row).backgroundColor)).toBe('rgb(244, 177, 131)'); const numbers = await rows.locator('td:first-child').allTextContents(); expect(numbers).toEqual(numbers.map((_, index) => String(index + 1))); // A line break in a comment shows as a space (LauscherTableCellRenderer). expect( await get .locator('td') .nth(6) .evaluate((cell) => cell.textContent), ).toMatch(/^GET \/ HTTP\/1\.1 Host: 192\.168\.0\.20 /); // "Automatisches Scrollen" (on): the table at its end. const scroller = window.locator('.capture-table'); const scrolled = () => scroller.evaluate((element) => ({ overflows: element.scrollHeight > element.clientHeight + 20, atEnd: element.scrollTop + element.clientHeight >= element.scrollHeight - 2, top: element.scrollTop, })); await expect.poll(scrolled).toMatchObject({ overflows: true, atEnd: true }); // The request chosen: white on black, its details below, every layer. await get.click(); await expect(get).toHaveAttribute('aria-selected', 'true'); expect(await get.evaluate((row) => [getComputedStyle(row).backgroundColor, getComputedStyle(row).color])).toEqual(['rgb(0, 0, 0)', 'rgb(255, 255, 255)']); const details = window.getByRole('list', { name: 'Bemerkungen / Details' }).locator('li'); await expect(details.first()).toHaveText(/^Nr\.: \d+ \/ Zeit: \d\d:\d\d:\d\d\.\d\d\d$/); const lines = await details.allTextContents(); expect(lines.filter((line) => ['Netzzugang', 'Vermittlung', 'Transport', 'Anwendung'].includes(line))).toEqual(['Netzzugang', 'Vermittlung', 'Transport', 'Anwendung']); expect(lines).toContain('Protokoll: HTTP'); expect(lines.some((line) => line.startsWith('Bemerkungen / Details (') && /^[^(]+\(\d+ Bytes\): $/.test(line))).toBe(true); expect(lines.some((line) => line.startsWith('GET / HTTP/1.1\n'))).toBe(true); // The table as Java's text file, named after the tab. const [download] = await Promise.all([page.waitForEvent('download'), window.getByRole('button', { name: 'Als Textdatei exportieren' }).click()]); expect(download.suggestedFilename()).toBe('Browser - 192.168.0.10.txt'); const text = readFileSync((await download.path())!, 'utf8'); expect(text.split('\r\n').slice(0, 3)).toEqual([ `+${'='.repeat(12)}${`+${'='.repeat(22)}`.repeat(5)}+${'='.repeat(42)}+`, `| Nr. | Zeit | Quelle | Ziel | Protokoll | Schicht | ${'Bemerkungen / Details'.padEnd(40)} | `, `+${'='.repeat(12)}${`+${'='.repeat(22)}`.repeat(5)}+${'='.repeat(42)}+`, ]); expect(text).toContain('| 1 | '); expect(text.match(/^\+-/gm)?.length).toBe(await rows.count()); // A router's cards: one tab each, the one chosen in front. await window.locator('.simulation-sheet__close').click(); await expect(await menuOf(page, 'Router')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']); await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (192.168.0.1)' }).click(); await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10', 'Router - 192.168.0.1']); await expect(window.getByRole('tab', { selected: true })).toHaveText('Router - 192.168.0.1'); await expect(window.locator('th')).toHaveCount(7); // The router's card heard the ARP broadcast. await expect(rows.first().locator('td').nth(4)).toHaveText('ARP'); // "Tabellen löschen": every card's list, numbered anew. await window.getByRole('button', { name: 'Tabellen löschen' }).click(); await expect(rows).toHaveCount(0); await window.getByRole('tab', { name: 'Browser - 192.168.0.10' }).click(); await expect(rows).toHaveCount(0); await expect(window.getByRole('list', { name: 'Bemerkungen / Details' }).locator('li')).toHaveCount(0); // Without "Automatisches Scrollen", fetched again (its tab kept over the window closed): the table stays at its start, nothing chosen. await window.getByLabel('Automatisches Scrollen').uncheck(); await showDesktop(page, await idOf(page, 'Browser')); await desktop.locator('[data-program="webBrowser"]').click(); await address.fill('192.168.0.20'); await address.press('Enter'); await page.waitForTimeout(6_000); await desktop.locator('.simulation-sheet__close').click(); await (await menuOf(page, 'Browser')).filter({ hasText: 'Datenaustausch' }).click(); await expect(window.getByLabel('Automatisches Scrollen')).not.toBeChecked(); await expect.poll(() => rows.count()).toBeGreaterThan(15); await expect(rows.first().locator('td').first()).toHaveText('1'); await expect(window.locator('tbody tr[aria-selected="true"]')).toHaveCount(0); expect(await scrolled()).toMatchObject({ overflows: true, top: 0 }); // Started anew from „Eigenschaften“: the table begins empty, and the frame chosen before is chosen in none of the new ones. await rows.nth(2).click(); await expect(rows.nth(2)).toHaveAttribute('aria-selected', 'true'); await page.getByRole('button', { name: 'Eigenschaften' }).click(); const properties = page.getByRole('complementary', { name: 'Eigenschaften' }); await properties.getByRole('button', { name: 'Simulation neu starten' }).click(); await properties.getByRole('button', { name: 'Neu starten' }).click(); await expect(rows).toHaveCount(0); await properties.getByRole('button', { name: 'Schließen' }).click(); await window.locator('.simulation-sheet__close').click(); await showDesktop(page, await idOf(page, 'Browser')); await desktop.locator('[data-program="webBrowser"]').click(); await address.fill('192.168.0.20'); await address.press('Enter'); await page.waitForTimeout(6_000); await desktop.locator('.simulation-sheet__close').click(); await (await menuOf(page, 'Browser')).filter({ hasText: 'Datenaustausch' }).click(); await expect.poll(() => rows.count()).toBeGreaterThan(3); await expect(rows.first().locator('td').first()).toHaveText('1'); await expect(window.locator('tbody tr[aria-selected="true"]')).toHaveCount(0); // A third tab; a tab closed beside the chosen one leaves it chosen. await window.locator('.simulation-sheet__close').click(); await (await menuOf(page, 'Server')).filter({ hasText: 'Datenaustausch' }).click(); await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10', 'Router - 192.168.0.1', 'Server - 192.168.0.20']); await window.getByRole('tab', { name: 'Browser - 192.168.0.10' }).click(); await window.getByRole('button', { name: 'Reiter schließen: Router - 192.168.0.1' }).click(); await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10', 'Server - 192.168.0.20']); await expect(window.getByRole('tab', { selected: true })).toHaveText('Browser - 192.168.0.10'); // The window goes with the design and comes back with the next start; the tab of a computer removed in the design is gone. await page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click(); await expect(window).toHaveCount(0); const server = await onDevice(page, await idOf(page, 'Server')); await page.mouse.click(server.x, server.y); await page.keyboard.press('Delete'); await expect.poll(async () => (await drawing(page)).nodes.length).toBe(3); await start(page); await expect(window.getByRole('tab')).toHaveText(['Browser - 192.168.0.10']); // Its last tab closed, the window shows nothing. await window.getByRole('button', { name: 'Reiter schließen: Browser - 192.168.0.10' }).click(); await expect(window).toHaveCount(0); }); test('chooses a device with a tap in the simulation, its three-dot button as in the design, and opens nothing by itself', async ({ page }) => { await openPages(page); await start(page); const button = page.locator('.device-menu-button'); const sheet = page.locator('aside[aria-label="Einstellungen"]'); // A router: the button opens the same menu as the long press. const router = await onDevice(page, await idOf(page, 'Router')); await page.mouse.click(router.x, router.y); await expect(page.locator(`g.device[data-node="${await idOf(page, 'Router')}"]`)).toHaveAttribute('data-selected', 'true'); await button.click(); await expect(page.getByRole('menuitem')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']); await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (10.0.0.1)' }).click(); await expect(page.locator('section[data-capture]').getByRole('tab', { selected: true })).toHaveText('Router - 10.0.0.1'); await page.locator('section[data-capture] .simulation-sheet__close').click(); // A computer: the tap opens no desktop (Java's click does), it only chooses; its settings stay closed, the design is locked. const browser = await idOf(page, 'Browser'); const at = await onDevice(page, browser); await page.mouse.click(at.x, at.y); await expect(button).toBeVisible(); await expect(page.locator('section[data-desktop]')).toHaveCount(0); await expect(sheet).toHaveCount(0); await button.click(); await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (192.168.0.10)']); await page.keyboard.press('Escape'); // A switch: the tap opens no table either; its menu does. const lanSwitch = (await drawing(page)).nodes.find((node) => node.kind === 'networkSwitch')!.id; const hub = await onDevice(page, lanSwitch); await page.mouse.click(hub.x, hub.y); await expect(page.locator(`g.device[data-node="${lanSwitch}"]`)).toHaveAttribute('data-selected', 'true'); await expect(page.locator('section[data-viewer]')).toHaveCount(0); await button.click(); await expect(page.getByRole('menuitem')).toHaveText(['SAT Tabelle anzeigen']); await page.getByRole('menuitem', { name: 'SAT Tabelle anzeigen' }).click(); await expect(page.locator(`section[data-viewer="${lanSwitch}"]`)).toBeVisible(); await page.locator('.simulation-sheet__close').click(); // A tap beside lets go. await page.mouse.click(at.x, at.y); await expect(button).toBeVisible(); const surface = (await page.locator('svg.filius-canvas').boundingBox())!; await page.mouse.click(surface.x + surface.width - 30, surface.y + surface.height - 80); await expect(button).toHaveCount(0); // Back in the design nothing stays chosen. await page.mouse.click(at.x, at.y); await page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click(); await expect(button).toHaveCount(0); await expect(sheet).toHaveCount(0); }); test('fills a phone with the data exchange window, its bar in reach', async ({ page }) => { await page.setViewportSize({ width: 360, height: 740 }); await openPages(page); await start(page); await (await menuOf(page, 'Browser')).filter({ hasText: 'Datenaustausch' }).click(); const window = page.locator('section[data-capture]'); for (const name of ['Tabellen löschen', 'Als Textdatei exportieren']) await expect(window.getByRole('button', { name })).toBeInViewport({ ratio: 1 }); await expect(window.getByLabel('Automatisches Scrollen')).toBeChecked(); const box = (await window.boundingBox())!; expect(box.x).toBeGreaterThanOrEqual(0); expect(box.x + box.width).toBeLessThanOrEqual(360); });