Files
mdg-filius/packages/editor/test/bridge.test.ts
T
julian.kiedaischandClaude Opus 5.5 8efbc442f1 feat(editor): a computer's files to and from the person's own files (F8.1)
Beside the dashboard only: Java's import window gets "Aus meinen Dateien"
(D4) — the dashboard is asked over the bridge for a file of at most Java's
150 000 bytes and hands its bytes over — and a file's menu gets "In meine
Dateien speichern" (D5), its bytes handed to the dashboard to save.

- Bridge: chooseFile/chosenFile and saveFile/savedFile, each question with
  its number; an answer from elsewhere, of another version, for no question
  or of the other kind is no answer.
- Whoever reads or finds the project frozen may save, not bring in.
- doc fileBytes: a text as UTF-8, a picture as it is, and "missing" while
  a blob's bytes have not come (read so, the file would be empty).

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

110 lines
5.6 KiB
TypeScript

// The bridge to the dashboard: only the expected origin, only this version,
// and the editor says when it listens.
import { describe, expect, it } from 'vitest';
import { BRIDGE_VERSION, dashboardFiles, startBridge } from '../src/bridge/dashboardBridge.js';
function fakeWindow() {
const posted: [unknown, string, Transferable[]?][] = [];
const listeners = new Set<(event: { origin: string; data: unknown }) => void>();
return {
posted,
window: {
parent: { postMessage: (message: unknown, origin: string, transfer?: Transferable[]) => posted.push(transfer === undefined ? [message, origin] : [message, origin, transfer]) },
addEventListener: (_: 'message', listener: (event: { origin: string; data: unknown }) => void) => listeners.add(listener),
removeEventListener: (_: 'message', listener: (event: { origin: string; data: unknown }) => void) => listeners.delete(listener),
},
send: (origin: string, data: unknown) => {
for (const listener of listeners) listener({ origin, data });
},
listening: () => listeners.size,
};
}
describe('the bridge to the dashboard', () => {
it('says it is ready, to the dashboard only', () => {
const fake = fakeWindow();
startBridge(fake.window, 'https://dashboard.example', { onLocale: () => {}, onTheme: () => {} });
expect(fake.posted).toEqual([[{ v: BRIDGE_VERSION, type: 'ready' }, 'https://dashboard.example']]);
});
it('takes language and colours from the dashboard, and nothing from elsewhere', () => {
const fake = fakeWindow();
const got: unknown[] = [];
const stop = startBridge(fake.window, 'https://dashboard.example', {
onLocale: (locale) => got.push(['locale', locale]),
onTheme: (theme, tokens) => got.push(['theme', theme, tokens]),
});
fake.send('https://evil.example', { v: 1, type: 'setLocale', locale: 'en' });
fake.send('https://dashboard.example', { v: 2, type: 'setLocale', locale: 'en' });
fake.send('https://dashboard.example', { v: 1, type: 'setLocale', locale: 7 });
fake.send('https://dashboard.example', { v: 1, type: 'setTheme', theme: 'pink' });
fake.send('https://dashboard.example', null);
expect(got).toEqual([]);
fake.send('https://dashboard.example', { v: 1, type: 'setLocale', locale: 'en-GB' });
fake.send('https://dashboard.example', { v: 1, type: 'setTheme', theme: 'dark', tokens: { '--accent': '#123456', color: 'red', '--bad': 3 } });
expect(got).toEqual([
['locale', 'en-GB'],
['theme', 'dark', { '--accent': '#123456' }],
]);
stop();
expect(fake.listening()).toBe(0);
});
});
describe("the person's own files over the bridge (F8)", () => {
const DASHBOARD = 'https://dashboard.example';
it('asks for a file with its limit and takes the answer to that question only', async () => {
const fake = fakeWindow();
const files = dashboardFiles(fake.window, DASHBOARD);
const chosen = files.chooseFile(150_000);
expect(fake.posted).toEqual([[{ v: BRIDGE_VERSION, type: 'chooseFile', request: 1, maxBytes: 150_000 }, DASHBOARD, []]]);
const data = new Uint8Array([1, 2, 3]).buffer;
// From elsewhere, of another version, for another question, of the other kind: no answer — the question still waits.
fake.send('https://evil.example', { v: 1, type: 'chosenFile', request: 1, name: 'x.png', data });
fake.send(DASHBOARD, { v: 2, type: 'chosenFile', request: 1, name: 'x.png', data });
fake.send(DASHBOARD, { v: 1, type: 'chosenFile', request: 9, name: 'x.png', data });
fake.send(DASHBOARD, { v: 1, type: 'savedFile', request: 1, result: 'saved', name: 'x.png' });
fake.send(DASHBOARD, { v: 1, type: 'chosenFile', request: 1, name: 'Bild.png', data });
expect(await chosen).toEqual({ name: 'Bild.png', bytes: new Uint8Array([1, 2, 3]) });
});
it('hears a cancelled choice as none', async () => {
const fake = fakeWindow();
const files = dashboardFiles(fake.window, DASHBOARD);
const chosen = files.chooseFile(10);
fake.send(DASHBOARD, { v: 1, type: 'chosenFile', request: 1, cancelled: true });
expect(await chosen).toBeNull();
});
it('hands a copy of the bytes over to save, and keeps its own', async () => {
const fake = fakeWindow();
const files = dashboardFiles(fake.window, DASHBOARD);
const bytes = new Uint8Array([9, 8, 7, 6]).subarray(1, 3);
const saved = files.saveFile('Grüße.txt', bytes);
const [message, origin, transfer] = fake.posted[0]!;
expect(origin).toBe(DASHBOARD);
expect(message).toMatchObject({ v: BRIDGE_VERSION, type: 'saveFile', request: 1, name: 'Grüße.txt' });
const data = (message as { data: ArrayBuffer }).data;
expect([...new Uint8Array(data)]).toEqual([8, 7]);
expect(transfer).toEqual([data]);
expect(data).not.toBe(bytes.buffer);
fake.send(DASHBOARD, { v: 1, type: 'savedFile', request: 1, result: 'saved', name: 'Grüße (2).txt' });
expect(await saved).toEqual({ result: 'saved', name: 'Grüße (2).txt' });
});
it('counts its questions, and an unknown result is a failure', async () => {
const fake = fakeWindow();
const files = dashboardFiles(fake.window, DASHBOARD);
const first = files.saveFile('a.txt', new Uint8Array());
const second = files.saveFile('b.txt', new Uint8Array());
fake.send(DASHBOARD, { v: 1, type: 'savedFile', request: 2, result: 'cancelled' });
fake.send(DASHBOARD, { v: 1, type: 'savedFile', request: 1, result: 'weird' });
expect(await second).toEqual({ result: 'cancelled', name: 'b.txt' });
expect(await first).toEqual({ result: 'failed', name: 'a.txt' });
files.stop();
expect(fake.listening()).toBe(0);
});
});