Files
mdg-filius/packages/editor/browser/shared.spec.ts
T
julian.kiedaischandClaude Opus 5.5 f52852493a feat: F5.3b the editor on a project of server F
?project=<uuid> opens the shared document over /sync and the room over
/sim (RemoteSimHost: messages wait for the socket, a lost socket comes
back with what it watched, F's refusal or parting ends it); instead of
the editor, a word why when F refuses or lets go. F tells each
connection its access at once. Who only reads sees everything and
changes nothing: no palette or undo, a locked surface, the sheet's
fields disabled (tabs stay), no starting or stopping; thawed, the
document comes anew. The toolbar names who started the simulation.
Refusals carry no message, so Hocuspocus writes nothing outside F's
JSON log. A browser test runs three people against a real F.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:00:18 +00:00

147 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The editor on a project of a real server F (F5.3b), three people in three
// browsers: Mia and Ben write, Rita only reads. What one moves the others
// see; Rita gets everything shown and nothing to change; Mia starts the
// simulation and it runs for all, named after her; Ben's ping in a command
// line is in Mia's too (one window per computer, as in Java); Rita's typing
// there is refused, and she is told; the project frozen, Ben reads and is
// told, thawed he writes again; his grant ended, he is told and the editor
// goes.
import { expect, test, type Browser, type Page } from '@playwright/test';
import { drag, onDevice, serveEditor } 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();
});
/** A person in their own browser context, signed in, the project open. */
async function person(browser: Browser, project: string, name: string, level: 'read' | 'write' = 'write', user?: string): Promise<Page> {
const context = await browser.newContext();
await f.signIn(context, project, { name, level, ...(user === undefined ? {} : { user }) });
const page = await context.newPage();
await page.goto(`${editor.base}?project=${project}&server=${encodeURIComponent(f.base)}`);
await page.waitForSelector('svg.filius-canvas');
return page;
}
const box = (page: Page, id: string) => page.locator(`g.device[data-node="${id}"]`).getAttribute('data-box');
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}"]`);
async function commandLine(page: Page, id: string) {
const at = await onDevice(page, id);
await page.mouse.click(at.x, at.y);
await desktop(page, id).locator('[data-program="commandPrompt"]').click();
const window = desktop(page, id).locator('[data-program-window="commandPrompt"]');
return { input: window.locator('input[name="terminalInput"]'), text: window.locator('.terminal-text') };
}
test('three people in one project of F: moving, reading only, one simulation for all, a frozen project, an ended grant', async ({ browser }) => {
test.setTimeout(120_000);
const project = await f.project('java-examples/internetwork_komplett.fls');
const benUser = crypto.randomUUID();
const mia = await person(browser, project.id, 'Mia');
const ben = await person(browser, project.id, 'Ben', 'write', benUser);
const rita = await person(browser, project.id, 'Rita', 'read');
const devices = await mia.locator('g.device').count();
expect(devices).toBeGreaterThan(5);
for (const page of [ben, rita]) await expect(page.locator('g.device')).toHaveCount(devices);
// Mia moves a device; Ben and Rita see it where she put it.
const notebook = project.node('Notebook 1.10');
const before = await box(mia, notebook);
const from = await onDevice(mia, notebook);
await drag(mia, from, { x: from.x + 120, y: from.y + 40 });
const moved = await box(mia, notebook);
expect(moved).not.toBe(before);
for (const page of [ben, rita]) await expect(page.locator(`g.device[data-node="${notebook}"]`)).toHaveAttribute('data-box', moved!);
// Rita reads: no palette, no undo, the note; her drag moves nothing; her sheet's fields are disabled; she cannot start.
await expect(note(rita, 'readOnly')).toHaveText('Nur ansehen');
await expect(rita.locator('[data-palette]')).toHaveCount(0);
await expect(rita.getByRole('button', { name: 'Rückgängig' })).toHaveCount(0);
await expect(rita.locator('button[name="simulationMode"]')).toBeDisabled();
const ritaFrom = await onDevice(rita, notebook);
await drag(rita, ritaFrom, { x: ritaFrom.x + 80, y: ritaFrom.y });
await expect(rita.locator(`g.device[data-node="${notebook}"]`)).toHaveAttribute('data-box', moved!);
await rita.mouse.click(ritaFrom.x, ritaFrom.y);
const sheet = rita.getByRole('complementary', { name: 'Einstellungen' });
await expect(sheet).toBeVisible();
await expect(sheet.locator('input[name="ipAddress"]')).toBeDisabled();
await sheet.getByRole('button', { name: 'Schließen' }).click();
await expect(note(mia, 'readOnly')).toHaveCount(0);
// Mia starts: the simulation runs for all, named after her.
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');
}
// Ben pings from the notebook's command line; Mia's command line of it shows the same.
const bens = await commandLine(ben, notebook);
await bens.input.fill('ping 141.99.5.10');
await bens.input.press('Enter');
const mias = await commandLine(mia, notebook);
for (const each of [bens, mias]) await expect(each.text).toContainText('4 Paket(e) gesendet, 4 Paket(e) empfangen', { timeout: 30_000 });
// Rita may look, not type.
const ritas = await commandLine(rita, notebook);
await expect(ritas.text).toContainText('ping 141.99.5.10');
await ritas.input.fill('ipconfig');
await ritas.input.press('Enter');
await expect(rita.getByText('Hier können Sie nur zuschauen.')).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');
// Frozen: Ben reads and is told; thawed, he writes again.
expect((await f.service('POST', `/svc/projects/${project.id}/freeze`, { frozen: true })).status).toBe(200);
await expect(note(ben, 'readOnly')).toHaveText('Eingefroren – nur ansehen');
await expect(ben.locator('[data-palette]')).toHaveCount(0);
expect((await f.service('POST', `/svc/projects/${project.id}/freeze`, { frozen: false })).status).toBe(200);
await expect(note(ben, 'readOnly')).toHaveCount(0);
await expect(ben.locator('[data-palette]').first()).toBeVisible();
const benFrom = await onDevice(ben, notebook);
await drag(ben, benFrom, { x: benFrom.x - 60, y: benFrom.y });
const movedByBen = await box(ben, notebook);
expect(movedByBen).not.toBe(moved);
await expect(mia.locator(`g.device[data-node="${notebook}"]`)).toHaveAttribute('data-box', movedByBen!);
// Ben's grant ends: he is told, and the editor goes.
expect((await f.service('POST', '/svc/revoke', { user: benUser, project: project.id })).status).toBe(200);
await expect(ben.getByRole('alert')).toHaveText('Ihr Zugang zu diesem Projekt wurde beendet.');
await expect(canvas(ben)).toHaveCount(0);
await expect(canvas(mia)).toBeVisible();
});
test('says why when F ends only the room: the editor goes as when the document is let go', async ({ browser }) => {
const project = await f.project('java-examples/zwei_rechnernetze_komplett.fls');
const context = await browser.newContext();
await f.signIn(context, project.id, { name: 'Mia' });
const page = await context.newPage();
await page.routeWebSocket(/\/sim/, (socket) => socket.close({ code: 4403, reason: 'noAccess' }));
await page.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}`);
await expect(page.getByRole('alert')).toHaveText('Sie haben keinen Zugang zu diesem Projekt.');
await context.close();
});
test('says why when F refuses: no session, a project it does not know', async ({ page }) => {
const project = await f.project('java-examples/zwei_rechnernetze_komplett.fls');
await page.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}`);
await expect(page.getByRole('alert')).toHaveText('Die Anmeldung ist abgelaufen. Öffnen Sie das Projekt im Dashboard neu.');
await page.goto(`${editor.base}?project=not-a-project&server=${encodeURIComponent(f.base)}`);
await expect(page.getByRole('alert')).toHaveText('Diese Adresse nennt kein Projekt.');
});