Files
mdg-filius/packages/editor/browser/capture.spec.ts
T
julian.kiedaischandClaude Opus 5.5 12b26f4abe feat(editor): the simulation's properties in a sheet of their own, with a restart for everyone
Who started the simulation, "Pakete verwerfen" and the dropped packets move
from the toolbar into a properties sheet beside the surface. New in it: restart
the simulation, after asking. The room learns a restart message: stopped as by
stop, every capture, dropped frame and program window emptied, started anew
and named after who restarted; refused for readers and frozen projects, logged,
and waiting for asked blobs like a start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:01:41 +00:00

271 lines
16 KiB
TypeScript

// 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<void> };
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<void> {
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<void> {
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);
});