A tap on a device in the simulation now only chooses it; its three-dot button (or a long press) opens what Java's click opened: the desktop, and newly a switch's SAT table and a home router's NAT table, beside each card's data exchange. The desktop, the tables and the data exchange are no windows in the window any more but the simulation's sheet (SimulationSheet), after the dashboard's AppSheet: below the toolbar to the bottom, a head with icon, title, who else has it open, see-through and close. One at a time — it covers the surface. It stays over a return to the design with the program open on a desktop; a sheet whose device was removed closes. See-through fades the body (0.55) and keeps the head; the choice is kept in the browser. Java opens these as movable windows, several at once, with a click; named in CHANGES.md. Tests open through the menu (showFromMenu, showDesktop in editorPage.ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
250 lines
14 KiB
TypeScript
250 lines
14 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 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 });
|
|
|
|
// 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);
|
|
});
|