Files
mdg-filius/tools/live/acceptance.ts
T
julian.kiedaischandClaude Opus 5.5 16d9cc2095 feat(editor): the simulation opens sheets, not windows in the window
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>
2026-10-10 17:18:25 +00:00

160 lines
9.2 KiB
TypeScript

// Acceptance of F5 on a live server F (plan §F5): two real browsers in one
// fresh project. At the same time both move a device; one places a computer,
// the other cables it while the first moves another; then one starts the
// simulation, the other makes a file and pings in a computer's command line,
// the first sees that same command line and types in it too; the second
// stops. In the end Mia's drawing, Ben's drawing and the document F stored
// are the same, and the file made in the simulation is in the document.
// Every run makes its own project and deletes it (Scratch's lesson 9).
//
// npx vite-node tools/live/acceptance.ts
import { randomUUID } from 'node:crypto';
import { chromium, type Browser, type Page } from '@playwright/test';
import { nodeFiles, readProject } from '@mdg-filius/doc';
import { drag, onDevice, showDesktop } from '../../packages/editor/browser/editorPage.js';
import { cleanUp, joinDoc, seededProject, signIn, signInAddress } from './lib.js';
const results: [string, boolean, string][] = [];
const check = (name: string, ok: boolean, detail = '') => {
results.push([name, ok, detail]);
console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : ` (${detail})`}`);
};
async function eventually(name: string, test: () => Promise<boolean>, ms = 10_000, detail?: () => Promise<string>) {
for (const end = Date.now() + ms; ;) {
if (await test().catch(() => false)) return check(name, true);
if (Date.now() > end) return check(name, false, detail === undefined ? 'timed out' : await detail());
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
const boxes = (page: Page) => page.locator('g.device[data-node]').evaluateAll((all) => Object.fromEntries(all.map((each) => [each.getAttribute('data-node'), each.getAttribute('data-box')])));
const linkCount = (page: Page) => page.locator('path[data-link]').count();
const canvas = (page: Page) => page.locator('svg.filius-canvas');
async function person(browser: Browser, project: string, name: string, user: string): Promise<Page> {
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
const page = await context.newPage();
page.on('pageerror', (error) => check(`${name}: no page error`, false, error.message));
await page.goto(signInAddress(project, { name, user }));
await page.waitForSelector('svg.filius-canvas');
pages.push(page);
return page;
}
async function commandLine(page: Page, id: string) {
const desktop = await showDesktop(page, id);
await desktop.locator('[data-program="commandPrompt"]').click();
const window = desktop.locator('[data-program-window="commandPrompt"]');
return { input: window.locator('input[name="terminalInput"]'), text: window.locator('.terminal-text') };
}
const browser = await chromium.launch();
const pages: Page[] = [];
try {
const { project, example } = await seededProject('java-examples/internetwork_komplett.fls');
const node = (name: string) => example.graph.nodes.find((each) => each.displayName === name)!.id;
// A computer's address is its device's setting (a router's are its cards').
const ipOf = (name: string) => example.deviceConfigurations.get(node(name))!.ipAddress;
const [miaUser, benUser] = [randomUUID(), randomUUID()];
const mia = await person(browser, project, 'Mia', miaUser);
const ben = await person(browser, project, 'Ben', benUser);
await eventually('Mia and Ben see each other', async () => (await mia.locator(`[data-person="${benUser}"]`).isVisible()) && (await ben.locator(`[data-person="${miaUser}"]`).isVisible()));
const devices = await mia.locator('g.device').count();
const links = await linkCount(mia);
// Both move a device at the same time.
const before = await boxes(mia);
const shift = async (page: Page, id: string, dx: number, dy: number) => {
const from = await onDevice(page, id);
await drag(page, from, { x: from.x + dx, y: from.y + dy });
};
// Onto free ground: a device moved onto another would take the next click meant for the one below.
await Promise.all([shift(mia, node('Rechner 3.10'), -50, 60), shift(ben, node('Notebook 4.10'), 80, 60)]);
await eventually('both moves arrive at both', async () => {
const [m, b] = [await boxes(mia), await boxes(ben)];
return JSON.stringify(m) === JSON.stringify(b) && m[node('Rechner 3.10')] !== before[node('Rechner 3.10')] && m[node('Notebook 4.10')] !== before[node('Notebook 4.10')];
});
// Ben places a computer; Mia cables it to Switch 1 while Ben moves the web server.
const known = new Set(Object.keys(await boxes(ben)));
const palette = (await ben.locator('[data-palette="pc"]').boundingBox())!;
const surface = (await canvas(ben).boundingBox())!;
await drag(ben, { x: palette.x + palette.width / 2, y: palette.y + palette.height / 2 }, { x: surface.x + 330, y: surface.y + 230 });
await ben.keyboard.press('Escape');
let placed = '';
await eventually('the computer Ben placed is at Mia too', async () => {
placed = Object.keys(await boxes(ben)).find((id) => !known.has(id)) ?? '';
return placed !== '' && (await mia.locator(`g.device[data-node="${placed}"]`).count()) === 1;
});
await mia.locator('[data-palette="cable"]').click();
await Promise.all([(async () => drag(mia, await onDevice(mia, placed), await onDevice(mia, node('Switch 1'))))(), shift(ben, node('Webserver 5.10'), 70, -40)]);
await mia.locator('[data-palette="cable"]').click();
await eventually(
'the cable and the move arrive at both',
async () => (await linkCount(mia)) === links + 1 && (await linkCount(ben)) === links + 1 && JSON.stringify(await boxes(mia)) === JSON.stringify(await boxes(ben)),
);
// Mia starts; the simulation runs for both, named after her.
await mia.locator('button[name="simulationMode"]').click();
await eventually(
'the simulation runs for both, started by Mia',
async () => (await canvas(ben).getAttribute('data-mode')) === 'simulation' && (await ben.locator('[data-note="startedBy"]').textContent()) === 'Simulation gestartet von Mia',
);
// Ben makes a file and pings in Notebook 1.10's command line; Mia sees the same and types in it too.
const notebook = node('Notebook 1.10');
const benLine = await commandLine(ben, notebook);
await benLine.input.fill('touch acceptance.txt');
await benLine.input.press('Enter');
await benLine.input.fill(`ping ${ipOf('Webserver 5.10')}`);
await benLine.input.press('Enter');
const miaLine = await commandLine(mia, notebook);
await eventually(
"Ben's ping is in Mia's command line",
async () => /4 Paket\(e\) empfangen/.test((await miaLine.text.textContent()) ?? ''),
20_000,
async () => ((await miaLine.text.textContent()) ?? '').slice(-200),
);
await miaLine.input.fill('ls');
await miaLine.input.press('Enter');
await eventually("Mia's ls, with Ben's file, is in Ben's command line", async () => /acceptance\.txt/.test((await benLine.text.textContent()) ?? ''));
// Ben stops: both are back in design mode.
await ben.locator('button[name="designMode"]').click();
await eventually('both are back in design mode', async () => (await canvas(mia).getAttribute('data-mode')) === 'design' && (await canvas(ben).getAttribute('data-mode')) === 'design');
// The same everywhere: both drawings and the document F stored.
const [m, b] = [await boxes(mia), await boxes(ben)];
check("Mia's drawing is Ben's", JSON.stringify(m) === JSON.stringify(b));
check('one more device and one more cable', Object.keys(m).length === devices + 1 && (await linkCount(mia)) === links + 1, `${Object.keys(m).length} devices, ${await linkCount(mia)} cables`);
await new Promise((resolve) => setTimeout(resolve, 2000));
const stored = await joinDoc(project, await signIn(project, { name: 'Prüfung' }));
const read = readProject(stored.doc);
check("F's document has Mia's devices", read.graph.nodes.length === Object.keys(m).length && read.graph.nodes.every((each) => m[each.id] !== undefined), `${read.graph.nodes.length} devices`);
check("F's document has Mia's cables", read.graph.links.length === links + 1, `${read.graph.links.length} cables`);
const moved = ['Rechner 3.10', 'Notebook 4.10', 'Webserver 5.10'].map((name) => {
const was = example.graph.nodes.find((each) => each.displayName === name)!.position;
const now = read.graph.nodes.find((each) => each.id === node(name))!.position;
return now.x !== was.x || now.y !== was.y;
});
check("F's document has all three moves", moved.every(Boolean), JSON.stringify(moved));
check(
"the file Ben made in the simulation is in F's document",
nodeFiles(stored.doc, notebook)
?.allEntries()
.some((each) => each.path.endsWith('/acceptance.txt')) === true,
);
stored.close();
} catch (error) {
check('ran to the end', false, error instanceof Error ? error.message.split('\n')[0]! : String(error));
// What each saw when it stopped, for the one who looks next.
for (const [index, page] of pages.entries()) await page.screenshot({ path: `acceptance-${index === 0 ? 'mia' : 'ben'}.png` }).catch(() => undefined);
} finally {
await browser.close();
await cleanUp();
}
const failed = results.filter(([, ok]) => !ok).length;
console.log(failed === 0 ? `\nF5 accepted: ${results.length} checks` : `\n${failed} of ${results.length} checks failed`);
process.exit(failed === 0 ? 0 : 1);