Files
mdg-filius/packages/editor/browser/dashboardFiles.spec.ts
T
julian.kiedaischandClaude Opus 5.5 739f8aab3a test(editor): F8's acceptance in the browser — a picture of one's own files on the simulated web server
Mia brings a picture between the blob threshold and Java's limit from her
files into Webserver 5.10's /webserver; it lies on F under its SHA-256;
Ben, in a browser of his own, fetches http://141.99.5.10/logo.png on
Notebook 1.10 and gets the same bytes; saved back, the same bytes go out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:19 +00:00

206 lines
13 KiB
TypeScript

// The person's own files over the bridge (F8): the page is its own parent
// here, and a little script stands in for the dashboard — it answers
// "chooseFile" with a picture and "saveFile" with a new name, and keeps what
// it was asked. Mia, who writes, brings the picture into a computer's file
// explorer and takes it out again; Rita, who only reads, may take a file out,
// but bring nothing in.
import { createHash } from 'node:crypto';
import { mkdtempSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { expect, test, type Browser, type Page } from '@playwright/test';
import { BLOB_THRESHOLD_BYTES } from '@mdg-filius/doc';
import { IMPORT_MAXIMUM_BYTES } from '@mdg-filius/core';
import { 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 PICTURE = [...readFileSync(fileURLToPath(new URL('../src/assets/desktop/datei.png', import.meta.url)))];
/** A person with the editor embedded as the dashboard embeds it: `parent` is the page itself, and it answers. */
async function embedded(browser: Browser, project: string, name: string, level: 'read' | 'write', picture: number[] = PICTURE, pictureName = 'Logo Schule.png'): Promise<Page> {
const context = await browser.newContext();
await f.signIn(context, project, { name, level });
const page = await context.newPage();
await page.addInitScript(
([picture, pictureName]: [number[], string]) => {
const asked: unknown[] = [];
(window as unknown as { asked: unknown[] }).asked = asked;
window.addEventListener('message', (event) => {
const data = event.data as { type?: string; request?: number; name?: string; data?: ArrayBuffer; maxBytes?: number };
if (data?.type === 'chooseFile') {
asked.push({ type: data.type, maxBytes: data.maxBytes });
const bytes = new Uint8Array(picture).buffer;
window.postMessage({ v: 1, type: 'chosenFile', request: data.request, name: pictureName, data: bytes }, window.location.origin, [bytes]);
}
if (data?.type === 'saveFile') {
asked.push({ type: data.type, name: data.name, bytes: [...new Uint8Array(data.data!)] });
window.postMessage({ v: 1, type: 'savedFile', request: data.request, result: 'saved', name: data.name!.replace('.png', ' (2).png') }, window.location.origin);
}
});
},
[picture, pictureName] as [number[], string],
);
const origin = new URL(editor.base).origin;
await page.goto(`${editor.base}?project=${project}&server=${encodeURIComponent(f.base)}&parent=${encodeURIComponent(origin)}`);
await page.waitForSelector('svg.filius-canvas');
return page;
}
const asked = (page: Page) => page.evaluate(() => (window as unknown as { asked: unknown[] }).asked);
test("brings a picture of the person's own files into a computer and takes it out again; who only reads takes out only", async ({ browser }) => {
test.setTimeout(120_000);
const project = await f.project('java-examples/internetwork_komplett.fls');
const notebook = project.node('Notebook 1.10');
const mia = await embedded(browser, project.id, 'Mia', 'write');
await mia.locator('button[name="simulationMode"]').click();
await expect(mia.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
const desktop = await showDesktop(mia, notebook);
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: 'Datei-Explorer' }).click();
await installation.getByRole('button', { name: 'Installieren' }).click();
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
await desktop.locator('[data-program="fileExplorer"]').click();
const explorer = desktop.locator('[data-program-window="fileExplorer"]');
const toolbar = explorer.getByRole('toolbar', { name: 'Aktionen' });
// D4: Java's import window, the file from her own files — with Java's limit.
await toolbar.getByRole('button', { name: 'Importieren' }).click();
const importing = explorer.getByRole('dialog', { name: 'Datei importieren' });
await importing.getByRole('button', { name: 'Aus meinen Dateien …' }).click();
await expect(importing.locator('input[readonly]')).toHaveValue('Logo Schule.png');
await importing.getByRole('button', { name: 'Datei importieren' }).click();
await expect(importing.getByRole('status')).toHaveText('Datei erfolgreich importiert!');
expect(await asked(mia)).toEqual([{ type: 'chooseFile', maxBytes: 150_000 }]);
await importing.getByRole('button', { name: 'Schließen' }).click();
// D5: nothing chosen, nothing to save; the file chosen, its bytes go out as they came in.
const save = toolbar.getByRole('button', { name: 'In meine Dateien speichern …' });
await expect(save).toBeDisabled();
await explorer.locator('.file-explorer-entries button', { hasText: 'Logo Schule.png' }).click();
await save.click();
await expect(explorer.getByRole('alert')).toHaveText('„Logo Schule (2).png“ steht jetzt in deinen Dateien.');
expect((await asked(mia))[1]).toEqual({ type: 'saveFile', name: 'Logo Schule.png', bytes: PICTURE });
// The entry's menu has it too — after Java's own items.
await explorer.locator('.file-explorer-entries button', { hasText: 'Logo Schule.png' }).click({ button: 'right' });
await expect(explorer.getByRole('menuitem').last()).toHaveText('In meine Dateien speichern …');
await mia.keyboard.press('Escape');
// Rita reads: the explorer of the running simulation, saving yes, bringing in no.
const rita = await embedded(browser, project.id, 'Rita', 'read');
await expect(rita.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
const ritaDesktop = await showDesktop(rita, notebook);
await ritaDesktop.locator('[data-program="fileExplorer"]').click();
const ritaExplorer = ritaDesktop.locator('[data-program-window="fileExplorer"]');
const ritaBar = ritaExplorer.getByRole('toolbar', { name: 'Aktionen' });
await ritaExplorer.locator('.file-explorer-entries button', { hasText: 'Logo Schule.png' }).click();
await expect(ritaBar.getByRole('button', { name: 'In meine Dateien speichern …' })).toBeEnabled();
await ritaBar.getByRole('button', { name: 'Importieren' }).click();
await expect(ritaExplorer.getByRole('dialog', { name: 'Datei importieren' }).getByRole('button', { name: 'Datei wählen' })).toBeVisible();
await expect(ritaExplorer.getByRole('button', { name: 'Aus meinen Dateien …' })).toHaveCount(0);
await mia.context().close();
await rita.context().close();
});
test('offers neither way on its own, without a dashboard', async ({ browser }) => {
const project = await f.project('java-examples/internetwork_komplett.fls');
const context = await browser.newContext();
await f.signIn(context, project.id, { name: 'Mia' });
const page = await context.newPage();
await page.goto(`${editor.base}?project=${project.id}&server=${encodeURIComponent(f.base)}`);
await page.waitForSelector('svg.filius-canvas');
await page.locator('button[name="simulationMode"]').click();
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
const desktop = await showDesktop(page, project.node('Notebook 1.10'));
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: 'Datei-Explorer' }).click();
await installation.getByRole('button', { name: 'Installieren' }).click();
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
await desktop.locator('[data-program="fileExplorer"]').click();
const explorer = desktop.locator('[data-program-window="fileExplorer"]');
await expect(explorer.getByRole('button', { name: 'In meine Dateien speichern …' })).toHaveCount(0);
await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'Importieren' }).click();
await expect(explorer.getByRole('button', { name: 'Aus meinen Dateien …' })).toHaveCount(0);
await context.close();
});
/** Installs programs on a computer's desktop (Java's installation window). */
async function install(desktop: ReturnType<Page['locator']>, programs: readonly string[]): Promise<void> {
await desktop.locator('[data-program="installation"]').click();
const installation = desktop.locator('[data-program-window="installation"]');
for (const program of programs) await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: program }).click();
await installation.getByRole('button', { name: 'Installieren' }).click();
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
}
test("F8's acceptance: a picture of one's own files on the simulated web server, fetched in another computer's browser, and back into the files", async ({ browser }) => {
test.setTimeout(180_000);
const project = await f.project('java-examples/internetwork_komplett.fls');
// A picture between the blob threshold and Java's limit: it goes to F as a blob.
const shooter = await browser.newPage({ viewport: { width: 480, height: 360 } });
await shooter.setContent('<body style="margin:0;background:linear-gradient(45deg,#2e8540,#f5c542,#1d4ed8)"><h1 style="font:64px serif;color:white">MDG Logo</h1></body>');
const file = join(mkdtempSync(join(tmpdir(), 'filius-f8-')), 'logo.png');
await shooter.screenshot({ path: file });
await shooter.close();
const picture = [...readFileSync(file)];
expect(picture.length).toBeGreaterThan(BLOB_THRESHOLD_BYTES);
expect(picture.length).toBeLessThanOrEqual(IMPORT_MAXIMUM_BYTES);
const mia = await embedded(browser, project.id, 'Mia', 'write', picture, 'logo.png');
await mia.locator('button[name="simulationMode"]').click();
await expect(mia.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
const server = await showDesktop(mia, project.node('Webserver 5.10'));
await install(server, ['Datei-Explorer']);
await server.locator('[data-program="fileExplorer"]').click();
const explorer = server.locator('[data-program-window="fileExplorer"]');
await explorer.locator('.file-explorer-tree button[data-path="/webserver"]').click();
await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'Importieren' }).click();
const importing = explorer.getByRole('dialog', { name: 'Datei importieren' });
await importing.getByRole('button', { name: 'Aus meinen Dateien …' }).click();
await importing.getByRole('button', { name: 'Datei importieren' }).click();
await expect(importing.getByRole('status')).toHaveText('Datei erfolgreich importiert!');
await importing.getByRole('button', { name: 'Schließen' }).click();
await expect(explorer.locator('.file-explorer-entries button', { hasText: 'logo.png' })).toBeVisible();
// On F under its SHA-256, as large as it came.
await expect.poll(() => f.blobSize(createHash('sha256').update(Buffer.from(picture)).digest('hex'))).toBe(picture.length);
// Ben, in a browser of his own, on another computer of the network: the web server hands the picture out.
const ben = await embedded(browser, project.id, 'Ben', 'write');
await expect(ben.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
const notebook = await showDesktop(ben, project.node('Notebook 1.10'));
await notebook.locator('[data-program="webBrowser"]').click();
const web = notebook.locator('[data-program-window="webBrowser"]');
const address = web.getByRole('textbox', { name: 'Adresse' });
await address.fill('141.99.5.10/logo.png');
await address.press('Enter');
const shown = web.frameLocator('iframe[title="Seite"]').locator('img');
await expect(shown).toHaveAttribute('src', /^data:image\/png;base64,/, { timeout: 60_000 });
const src = await shown.getAttribute('src');
expect([...Buffer.from(src!.slice(src!.indexOf(',') + 1), 'base64')]).toEqual(picture);
// And back into the files: the bytes as they came in.
await explorer.locator('.file-explorer-entries button', { hasText: 'logo.png' }).click();
await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'In meine Dateien speichern …' }).click();
await expect(explorer.getByRole('alert')).toHaveText('„logo (2).png“ steht jetzt in deinen Dateien.');
expect((await asked(mia)).at(-1)).toEqual({ type: 'saveFile', name: 'logo.png', bytes: picture });
await mia.context().close();
await ben.context().close();
});