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>
This commit is contained in:
@@ -2,15 +2,15 @@
|
||||
// and the editor says when it listens.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { BRIDGE_VERSION, startBridge } from '../src/bridge/dashboardBridge.js';
|
||||
import { BRIDGE_VERSION, dashboardFiles, startBridge } from '../src/bridge/dashboardBridge.js';
|
||||
|
||||
function fakeWindow() {
|
||||
const posted: [unknown, string][] = [];
|
||||
const posted: [unknown, string, Transferable[]?][] = [];
|
||||
const listeners = new Set<(event: { origin: string; data: unknown }) => void>();
|
||||
return {
|
||||
posted,
|
||||
window: {
|
||||
parent: { postMessage: (message: unknown, origin: string) => posted.push([message, origin]) },
|
||||
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),
|
||||
},
|
||||
@@ -51,3 +51,59 @@ describe('the bridge to the dashboard', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user