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>
110 lines
5.6 KiB
TypeScript
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);
|
|
});
|
|
});
|