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>
This commit is contained in:
2026-10-10 21:01:41 +00:00
co-authored by Claude Opus 5.5
parent 317c0d9602
commit 12b26f4abe
23 changed files with 531 additions and 165 deletions
+22 -1
View File
@@ -3,7 +3,7 @@
// 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
// 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.
@@ -164,6 +164,27 @@ test("shows a card's frames as Java's data exchange window does, with their deta
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();
+1
View File
@@ -49,6 +49,7 @@ test('the editor from F itself: two people, one simulation, the bundle packed',
await expect(ben.page.locator('g.device')).toHaveCount(await mia.page.locator('g.device').count());
await mia.page.locator('button[name="simulationMode"]').click();
await expect(ben.page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await ben.page.getByRole('button', { name: 'Eigenschaften' }).click();
await expect(ben.page.locator('[data-note="startedBy"]')).toHaveText('Simulation gestartet von Mia');
expect([...mia.encodings.values()]).toEqual(['br']);
});
+16 -1
View File
@@ -46,6 +46,7 @@ const box = (page: Page, id: string) => page.locator(`g.device[data-node="${id}"
const canvas = (page: Page) => page.locator('svg.filius-canvas');
const desktop = (page: Page, id: string) => page.locator(`section[data-desktop="${id}"]`);
const note = (page: Page, kind: string) => page.locator(`[data-note="${kind}"]`);
const properties = (page: Page) => page.getByRole('complementary', { name: 'Eigenschaften' });
async function commandLine(page: Page, id: string) {
await showDesktop(page, id);
@@ -117,8 +118,12 @@ test('three people in one project of F: moving, reading only, one simulation for
await mia.locator('button[name="simulationMode"]').click();
for (const page of [mia, ben, rita]) {
await expect(canvas(page)).toHaveAttribute('data-mode', 'simulation');
await expect(note(page, 'startedBy')).toHaveText('Simulation gestartet von Mia');
await page.getByRole('button', { name: 'Eigenschaften' }).click();
await expect(properties(page).locator('[data-note="startedBy"]')).toHaveText('Simulation gestartet von Mia');
}
// Rita may look: neither start anew nor drop.
await expect(properties(rita).getByRole('button')).toHaveText(['×']);
for (const page of [mia, ben, rita]) await properties(page).getByRole('button', { name: 'Schließen' }).click();
// Ben pings from the notebook's command line; Mia's command line of it shows the same.
const bens = await commandLine(ben, notebook);
@@ -141,6 +146,16 @@ test('three people in one project of F: moving, reading only, one simulation for
await ritas.input.press('Enter');
await expect(rita.getByText('Hier können Sie nur zuschauen.')).toBeVisible();
// Ben starts it anew, for everyone: the command line begins anew in each, named after him.
await ben.getByRole('button', { name: 'Eigenschaften' }).click();
await properties(ben).getByRole('button', { name: 'Simulation neu starten' }).click();
await properties(ben).getByRole('button', { name: 'Neu starten' }).click();
for (const each of [bens, mias, ritas]) await expect(each.text).not.toContainText('ping 141.99.5.10');
await expect(properties(ben).locator('[data-note="startedBy"]')).toHaveText('Simulation gestartet von Ben');
await mia.getByRole('button', { name: 'Eigenschaften' }).click();
await expect(properties(mia).locator('[data-note="startedBy"]')).toHaveText('Simulation gestartet von Ben');
await expect(properties(mia).getByText('Noch keine verworfenen Pakete.')).toBeVisible();
// Ben stops it, for everyone.
await ben.locator('button[name="designMode"]').click();
for (const page of [mia, ben, rita]) await expect(canvas(page)).toHaveAttribute('data-mode', 'design');
+35 -9
View File
@@ -2,8 +2,10 @@
// (F4.6g5), on FILIUS' example nat_with_mapped_port: a tap on a home router
// shows its NAT table (the port forwarding's entry, then a ping's), a tap on
// a switch its table (the addresses a ping taught it), each reset; and
// "Pakete verwerfen" held while a ping runs: the ping loses answers, the
// list shows the dropped frames in Java's words.
// "Pakete verwerfen" held while a ping runs, in the simulation's
// „Eigenschaften“: the ping loses answers, the list shows the dropped frames
// in Java's words; „Simulation neu starten“ empties the command line and the
// list and keeps the server's file.
import { expect, test, type Locator, type Page } from '@playwright/test';
import { drawing, openEditor, serveEditor, showDesktop, showFromMenu } from './editorPage.js';
@@ -80,7 +82,10 @@ test('shows the tables of a home router and a switch, and drops while “Pakete
await desktop.locator('[data-program="commandPrompt"]').click();
await input.fill('ping 141.99.4.10');
await input.press('Enter');
const hold = page.getByRole('button', { name: 'Pakete verwerfen' });
await page.getByRole('button', { name: 'Eigenschaften' }).click();
const properties = page.getByRole('complementary', { name: 'Eigenschaften' });
await expect(properties.getByText('Noch keine verworfenen Pakete.')).toBeVisible();
const hold = properties.getByRole('button', { name: 'Pakete verwerfen' });
const box = (await hold.boundingBox())!;
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
@@ -91,19 +96,40 @@ test('shows the tables of a home router and a switch, and drops while “Pakete
// The second ping's statistics: an answer lost.
await expect(desktop.locator('.terminal-text')).toContainText(/Paketstatistik ---[^]*Paketstatistik ---\n4 Paket\(e\) gesendet, [0-3] Paket\(e\) empfangen/, { timeout: 30_000 });
await expect(desktop.locator('.terminal-text')).toContainText('-- Timeout!');
const toggle = page.getByRole('button', { name: /^Verworfen: [1-9]/ });
await toggle.click();
await expect(page.getByLabel('Verworfene Pakete')).toContainText(/\d\d:\d\d:\d\d\.\d\d\d : \[src=[0-9A-F:]+, dest=[0-9A-F:]+, type=0x8/);
await expect(properties.getByRole('heading', { name: /^Verworfene Pakete \([1-9]\d*\)$/ })).toBeVisible();
await expect(properties.getByLabel('Verworfene Pakete', { exact: true })).toContainText(/\d\d:\d\d:\d\d\.\d\d\d : \[src=[0-9A-F:]+, dest=[0-9A-F:]+, type=0x8/);
// „Simulation neu starten“: asked first — cancelled, nothing happens; confirmed, the command line and the
// dropped frames begin anew, the simulation runs, and what the server saved is still there.
await input.fill('touch bleibt.txt');
await input.press('Enter');
await properties.getByRole('button', { name: 'Simulation neu starten' }).click();
await expect(properties.getByText('Die Simulation für alle neu starten? Alle Protokolle und Datenpakete gehen verloren.')).toBeVisible();
await properties.getByRole('button', { name: 'Abbrechen' }).click();
await expect(properties.getByLabel('Verworfene Pakete', { exact: true })).toBeVisible();
await expect(desktop.locator('.terminal-text')).toContainText('ping 141.99.4.10');
await properties.getByRole('button', { name: 'Simulation neu starten' }).click();
await properties.getByRole('button', { name: 'Neu starten' }).click();
await expect(desktop.locator('.terminal-text')).not.toContainText('ping 141.99.4.10');
await expect(properties.getByText('Noch keine verworfenen Pakete.')).toBeVisible();
await expect(properties.getByRole('heading', { name: 'Verworfene Pakete (0)' })).toBeVisible();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await input.fill('ls');
await input.press('Enter');
await expect(desktop.locator('.terminal-text')).toContainText('bleibt.txt');
});
test('keeps “Pakete verwerfen” in reach on a phone', async ({ page }) => {
test('keeps “Pakete verwerfen” in reach on a phone, in „Eigenschaften“ over the whole editor', async ({ page }) => {
await page.setViewportSize({ width: 360, height: 740 });
await openEditor(page, server.base, 'nat_with_mapped_port.fls');
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
await expect(page.getByRole('button', { name: 'Pakete verwerfen' })).toBeInViewport({ ratio: 1 });
await expect(page.getByRole('button', { name: /^Verworfen: / })).toBeInViewport({ ratio: 1 });
// Every part of the toolbar within the screen (the dev page's own fields above the editor are wider; they are not the editor's).
const rights = await page.locator('.editor-toolbar > *').evaluateAll((parts) => parts.map((part) => part.getBoundingClientRect().right));
expect(Math.max(...rights)).toBeLessThanOrEqual(360);
await page.getByRole('button', { name: 'Eigenschaften' }).click();
const properties = page.getByRole('complementary', { name: 'Eigenschaften' });
expect(await properties.boundingBox()).toEqual(await page.locator('.editor-root').boundingBox());
await expect(properties.getByRole('button', { name: 'Pakete verwerfen' })).toBeInViewport({ ratio: 1 });
await expect(properties.getByRole('button', { name: 'Simulation neu starten' })).toBeInViewport({ ratio: 1 });
});