feat(editor): a modem offers a code or joins one, and says under it how it stands (K4, K5)

The modem's sheet chooses how it finds its partner: the other modem on its
port in this project, as FILIUS' file pairs them; another project's, by
offering this modem's code (shown large, with a copy button) or by joining
a code typed in as one reads it off the board. In the action mode each
modem says under it how it stands — connected, waiting, taken, no second
modem on the port, cable missing, on the server only.

Accepted in three browsers on a real F, each in a copy of FILIUS' modem
example: Ben's notebook pings the computer behind Mia's access point; Rita
joins the same code, reads that it is taken, and gets it once Ben stops.
FILIUS' modem example runs in the page's own room as well.

The .fls export writes an offering modem as Java's server and a joining
one as its client; the code has no place there and is named as lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:34:54 +00:00
co-authored by Claude Opus 5.5
parent 7b943d3d96
commit 87c09c6c9d
12 changed files with 355 additions and 34 deletions
+111
View File
@@ -0,0 +1,111 @@
// Two projects' networks joined by their modems (FILIUS_MODEM_PLAN K5), on a
// real server F, three people in three browsers, each in a copy of FILIUS'
// modem example. Mia's access point offers a connection and shows its code;
// Ben's accessing modem joins it with the code as typed off the board. Each
// modem says under it how it stands. Ben's notebook pings 192.168.0.11 —
// the computer behind Mia's access point, for the one behind Ben's own is cut
// off: his access point has no partner left on its port. Rita joins the same
// code and reads that it is taken, until Ben stops.
import { expect, test, type Browser, type Page } from '@playwright/test';
import { onDevice, serveEditor, showDesktop } from './editorPage.js';
import { startServerF, type ServerF } from './serverF.js';
let editor: { readonly base: string; readonly close: () => Promise<void> };
let f: ServerF;
test.beforeAll(async () => {
editor = await serveEditor();
f = await startServerF(editor.base);
});
test.afterAll(async () => {
await f?.close();
await editor?.close();
});
const ACCESS_POINT = 'Modem (Zugangspunkt)';
const ACCESSING = 'Modem (Zugreifend)';
/** A person in their own browser, signed in, in a fresh copy of the modem example. */
async function person(browser: Browser, name: string): Promise<{ readonly page: Page; readonly node: (name: string) => string }> {
const project = await f.project('java-examples/modem_beispiel.fls');
const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'] });
await f.signIn(context, project.id, { name });
const page = await context.newPage();
await page.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}`);
await page.waitForSelector('svg.filius-canvas');
return { page, node: project.node };
}
const sheet = (page: Page) => page.getByRole('complementary', { name: 'Einstellungen' });
const note = (page: Page, id: string) => page.locator(`g.device[data-node="${id}"] text[data-note]`);
async function choose(page: Page, id: string): Promise<void> {
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
await expect(sheet(page)).toBeVisible();
}
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');
}
test("joins two projects' networks through their modems: a code offered, a code typed, a ping across", async ({ browser }) => {
test.setTimeout(120_000);
const mia = await person(browser, 'Mia');
const ben = await person(browser, 'Ben');
const rita = await person(browser, 'Rita');
// Mia's access point offers: its sheet shows the code to pass on, and copies it.
await choose(mia.page, mia.node(ACCESS_POINT));
await sheet(mia.page).getByRole('radio', { name: 'Mit einem anderen Projekt: Verbindung anbieten' }).check();
const code = (await sheet(mia.page).locator('output.modem-code').textContent())!;
expect(code).toMatch(/^[A-HJ-NP-Z2-9]{3}-[A-HJ-NP-Z2-9]{3}$/);
await sheet(mia.page).getByRole('button', { name: 'Code kopieren' }).click();
await expect(sheet(mia.page).getByRole('button', { name: 'Kopiert ✓' })).toBeVisible();
await expect(mia.page.getByRole('alert')).toHaveCount(0);
expect(await mia.page.evaluate(() => navigator.clipboard.readText())).toBe(code);
await mia.page.screenshot({ path: 'test-results/modem-offer.png' });
// Ben's accessing modem joins, the code typed as one reads it off the board.
for (const person of [ben, rita]) {
await choose(person.page, person.node(ACCESSING));
await sheet(person.page).getByRole('radio', { name: 'Mit einem anderen Projekt: mit Code verbinden' }).check();
const field = sheet(person.page).locator('input[name="modemCode"]');
await field.fill(code.toLowerCase().replace('-', ' '));
await field.press('Enter');
await expect(field).toHaveValue(code);
}
await ben.page.screenshot({ path: 'test-results/modem-join.png' });
await start(mia.page);
await expect(note(mia.page, mia.node(ACCESS_POINT))).toHaveText('wartet auf Gegenstelle');
await expect(note(mia.page, mia.node(ACCESSING))).toHaveText('kein zweites Modem mit diesem Port');
await start(ben.page);
await expect(note(ben.page, ben.node(ACCESSING))).toHaveText('verbunden');
await expect(note(mia.page, mia.node(ACCESS_POINT))).toHaveText('verbunden');
await expect(note(mia.page, mia.node(ACCESS_POINT))).toHaveAttribute('data-note', 'good');
await mia.page.screenshot({ path: 'test-results/modem-connected.png' });
// Ben's notebook pings the computer behind Mia's access point.
const notebook = ben.node('Notebook 0.10');
await showDesktop(ben.page, notebook);
const desktop = ben.page.locator(`section[data-desktop="${notebook}"]`);
await desktop.locator('[data-program="commandPrompt"]').click();
const input = desktop.locator('[data-program-window="commandPrompt"] input[name="terminalInput"]');
await input.fill('ping 192.168.0.11');
await input.press('Enter');
await expect(desktop.locator('.terminal-text')).toContainText('4 Paket(e) gesendet, 4 Paket(e) empfangen', { timeout: 30_000 });
// Rita wants the same code: taken while Ben has it, hers once he stops.
await start(rita.page);
await expect(note(rita.page, rita.node(ACCESSING))).toHaveText('Code schon belegt');
await expect(note(rita.page, rita.node(ACCESSING))).toHaveAttribute('data-note', 'trouble');
await ben.page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click();
await expect(note(rita.page, rita.node(ACCESSING))).toHaveText('verbunden');
await expect(note(mia.page, mia.node(ACCESS_POINT))).toHaveText('verbunden');
// In the design there is nothing to say.
await expect(ben.page.locator('text[data-note]')).toHaveCount(0);
});