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>
271 lines
16 KiB
TypeScript
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);
|
|
});
|