// 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); }); });