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>
185 lines
9.8 KiB
TypeScript
185 lines
9.8 KiB
TypeScript
// A device's files in the document: read as a file system, changed through
|
|
// the core's operations, a text file's Y.Text found in any case, and the
|
|
// document's files into a running engine's — what differs only.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import * as Y from 'yjs';
|
|
import { saveTextAs, VirtualFileSystem } from '@mdg-filius/core';
|
|
import { addDevice, attachBlobs, BLOB_THRESHOLD_BYTES, changeFiles, emptyProject, fileBytes, fileText, memoryBlobSide, nodeFiles, readFilesInto, ROOTS, writeProject } from '../src/index.js';
|
|
|
|
function setUp() {
|
|
const doc = new Y.Doc();
|
|
writeProject(doc, emptyProject());
|
|
let counter = 0;
|
|
addDevice(doc, { kind: 'pc', id: 'pc', name: 'PC', x: 0, y: 0, newId: () => `port-${++counter}` }, 'me');
|
|
const files = () => doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>> | undefined;
|
|
return { doc, files };
|
|
}
|
|
|
|
const paths = (fileSystem: VirtualFileSystem) => fileSystem.allEntries().map((entry) => [entry.path, entry.content.kind === 'text' ? entry.content.text : entry.content.kind]);
|
|
|
|
describe("a device's files in the document", () => {
|
|
it('are changed through the core, written with the origin, what changed only', () => {
|
|
const { doc, files } = setUp();
|
|
expect(paths(nodeFiles(doc, 'pc')!)).toEqual([['/', 'directory']]);
|
|
expect(nodeFiles(doc, 'none')).toBeUndefined();
|
|
const origins: unknown[] = [];
|
|
doc.on('update', (_update: Uint8Array, origin: unknown) => origins.push(origin));
|
|
expect(changeFiles(doc, 'pc', 'me', (fileSystem) => saveTextAs(fileSystem, '/', 'a.txt', 'Anfang Ende'))).toBe('saved');
|
|
expect(origins).toEqual(['me']);
|
|
expect((files()!.get('/a.txt')!.get('text') as Y.Text).toString()).toBe('Anfang Ende');
|
|
// Nothing changed: nothing written.
|
|
expect(changeFiles(doc, 'pc', 'me', (fileSystem) => fileSystem.contains('/a.txt'))).toBe(true);
|
|
expect(origins).toHaveLength(1);
|
|
expect(changeFiles(doc, 'none', 'me', () => 'ran')).toBeUndefined();
|
|
});
|
|
|
|
it("finds a text file's Y.Text in any case, and none for a folder, an image or what is not there", () => {
|
|
const { doc } = setUp();
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.writeTextFile('/Notiz.txt', 'x');
|
|
fileSystem.createDirectory('/ordner');
|
|
fileSystem.writeImageFile('/bild.png', new Uint8Array([1]), 'image/png');
|
|
});
|
|
const text = fileText(doc, 'pc', '/Notiz.txt');
|
|
expect(text?.toString()).toBe('x');
|
|
expect(fileText(doc, 'pc', '/NOTIZ.TXT')).toBe(text);
|
|
expect([fileText(doc, 'pc', '/ordner'), fileText(doc, 'pc', '/bild.png'), fileText(doc, 'pc', '/fehlt.txt'), fileText(doc, 'none', '/Notiz.txt')]).toEqual([
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
]);
|
|
const empty = new Y.Doc();
|
|
writeProject(empty, emptyProject());
|
|
addDevice(empty, { kind: 'pc', id: 'pc', name: 'PC', x: 0, y: 0, newId: () => 'port' }, 'me');
|
|
expect(fileText(empty, 'pc', '/Notiz.txt')).toBeUndefined();
|
|
});
|
|
|
|
it('give a file its bytes to take out: a text as UTF-8, a picture as it is, in any case; nothing for a folder or what is not there', () => {
|
|
const { doc } = setUp();
|
|
const picture = new Uint8Array([137, 80, 78, 71]);
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.writeTextFile('/Grüße.txt', 'Hallo Jörg\r\n');
|
|
fileSystem.createDirectory('/ordner');
|
|
fileSystem.writeImageFile('/ordner/Bild.png', picture, 'image/png');
|
|
});
|
|
expect(fileBytes(doc, 'pc', '/Grüße.txt')).toEqual({ name: 'Grüße.txt', bytes: new TextEncoder().encode('Hallo Jörg\r\n') });
|
|
expect(fileBytes(doc, 'pc', '/ORDNER/bild.PNG')).toEqual({ name: 'Bild.png', bytes: picture });
|
|
expect([fileBytes(doc, 'pc', '/ordner'), fileBytes(doc, 'pc', '/fehlt.txt'), fileBytes(doc, 'none', '/Grüße.txt')]).toEqual(['notFile', 'notFile', 'notFile']);
|
|
});
|
|
|
|
it("say a blob is missing until its bytes have come — read so, the file would be empty", () => {
|
|
const big = new Uint8Array(BLOB_THRESHOLD_BYTES + 5).map((_, index) => index % 251);
|
|
const { doc } = setUp();
|
|
attachBlobs(doc, memoryBlobSide());
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => fileSystem.writeBinaryFile('/gross.bin', big));
|
|
expect(fileBytes(doc, 'pc', '/gross.bin')).toEqual({ name: 'gross.bin', bytes: big });
|
|
// Another browser: the document, but not the bytes yet.
|
|
const there = new Y.Doc();
|
|
const side = memoryBlobSide();
|
|
attachBlobs(there, side);
|
|
Y.applyUpdate(there, Y.encodeStateAsUpdate(doc));
|
|
expect(fileBytes(there, 'pc', '/gross.bin')).toBe('missing');
|
|
side.arrived(new Map([[(doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('files') as Y.Map<Y.Map<unknown>>).get('/gross.bin')!.get('blob') as string, big]]));
|
|
expect(fileBytes(there, 'pc', '/gross.bin')).toEqual({ name: 'gross.bin', bytes: big });
|
|
});
|
|
|
|
it("go into a running engine's file system, what differs only: new ones after the others, changed ones in their place, gone ones removed", () => {
|
|
const { doc } = setUp();
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.createDirectory('/ordner');
|
|
fileSystem.writeTextFile('/ordner/a.txt', 'a');
|
|
fileSystem.writeTextFile('/b.txt', 'b');
|
|
fileSystem.writeTextFile('/Gross.txt', 'G');
|
|
fileSystem.writeImageFile('/bild.png', new Uint8Array([1]), 'image/png');
|
|
fileSystem.writeBinaryFile('/daten.bin', new Uint8Array([2]));
|
|
});
|
|
const engine = nodeFiles(doc, 'pc')!;
|
|
engine.writeTextFile('/eigen.txt', 'the engine wrote it');
|
|
// Alike but for the engine's own file: it goes, the others stay as they are.
|
|
expect(readFilesInto(doc, 'pc', engine)).toBe(true);
|
|
expect(paths(engine)).toEqual(paths(nodeFiles(doc, 'pc')!));
|
|
const revision = engine.revision;
|
|
expect(readFilesInto(doc, 'pc', engine)).toBe(false);
|
|
expect(engine.revision).toBe(revision);
|
|
// A text changed, an image, a binary file, a folder turned into a file and a file into a folder, a new one.
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.writeTextFile('/b.txt', 'b2');
|
|
fileSystem.writeImageFile('/bild.png', new Uint8Array([3]), 'image/png');
|
|
fileSystem.writeBinaryFile('/daten.bin', new Uint8Array([2]), 'application/octet-stream');
|
|
fileSystem.deleteItem('/ordner', true);
|
|
fileSystem.writeTextFile('/ordner', 'now a file');
|
|
fileSystem.deleteItem('/b.txt');
|
|
fileSystem.createDirectory('/b.txt');
|
|
fileSystem.writeTextFile('/neu.txt', 'neu');
|
|
});
|
|
expect(readFilesInto(doc, 'pc', engine)).toBe(true);
|
|
expect(paths(engine)).toEqual(paths(nodeFiles(doc, 'pc')!));
|
|
expect(
|
|
engine
|
|
.entriesInCreationOrder('/')
|
|
.map((entry) => entry.path)
|
|
.at(-1),
|
|
).toBe('/neu.txt');
|
|
expect(engine.imageFile('/bild.png').data).toEqual(new Uint8Array([3]));
|
|
expect(engine.entry('/daten.bin').content).toEqual({ kind: 'binary', data: new Uint8Array([2]), mediaType: 'application/octet-stream' });
|
|
expect(readFilesInto(doc, 'none', engine)).toBe(false);
|
|
// A file in a new folder; then a folder with all in it removed; a name changed only in case comes with its spelling.
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.createDirectory('/neu');
|
|
fileSystem.createDirectory('/neu/tiefer');
|
|
fileSystem.writeTextFile('/neu/tiefer/c.txt', 'c');
|
|
});
|
|
expect(readFilesInto(doc, 'pc', engine)).toBe(true);
|
|
expect(engine.textFile('/neu/tiefer/c.txt')).toBe('c');
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.deleteItem('/neu', true);
|
|
fileSystem.renameInPlace('/neu.txt', 'NEU.txt');
|
|
});
|
|
expect(readFilesInto(doc, 'pc', engine)).toBe(true);
|
|
expect(paths(engine)).toEqual(paths(nodeFiles(doc, 'pc')!));
|
|
expect(engine.contains('/neu')).toBe(false);
|
|
expect(engine.entry('/neu.txt').path).toBe('/NEU.txt');
|
|
// What stayed keeps its place, a name in capitals too.
|
|
const order = (fileSystem: VirtualFileSystem) => fileSystem.entriesInCreationOrder('/').map((entry) => entry.path);
|
|
expect(order(engine)).toEqual(order(nodeFiles(doc, 'pc')!));
|
|
expect(order(engine).indexOf('/Gross.txt')).toBeLessThan(order(engine).indexOf('/NEU.txt'));
|
|
// So also in a folder: a file renamed there keeps its place, in the document and in the engine.
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => {
|
|
fileSystem.createDirectory('/tief');
|
|
fileSystem.writeTextFile('/tief/x', 'x');
|
|
fileSystem.writeTextFile('/tief/y.txt', 'y');
|
|
});
|
|
readFilesInto(doc, 'pc', engine);
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => fileSystem.renameInPlace('/tief/x', 'x.txt'));
|
|
expect(nodeFiles(doc, 'pc')!.entriesInCreationOrder('/tief').map((entry) => entry.path)).toEqual(['/tief/x.txt', '/tief/y.txt']);
|
|
readFilesInto(doc, 'pc', engine);
|
|
expect(engine.entriesInCreationOrder('/tief').map((entry) => entry.path)).toEqual(['/tief/x.txt', '/tief/y.txt']);
|
|
});
|
|
|
|
it('leave out what the file system refuses, as the editor draws it', () => {
|
|
const { doc, files } = setUp();
|
|
changeFiles(doc, 'pc', 'me', (fileSystem) => fileSystem.writeTextFile('/a.txt', 'a'));
|
|
// Two people gave a name at once in different case: one file, the first name, the later text (as fileSystemOf reads it); one in a folder not there is left out.
|
|
doc.transact(() => {
|
|
const entry = new Y.Map<unknown>();
|
|
entry.set('order', 5);
|
|
entry.set('kind', 'text');
|
|
entry.set('text', new Y.Text('A'));
|
|
files()!.set('/A.TXT', entry);
|
|
const lost = new Y.Map<unknown>();
|
|
lost.set('order', 6);
|
|
lost.set('kind', 'text');
|
|
lost.set('text', new Y.Text('lost'));
|
|
files()!.set('/fehlt/x.txt', lost);
|
|
});
|
|
const engine = new VirtualFileSystem();
|
|
expect(readFilesInto(doc, 'pc', engine)).toBe(true);
|
|
expect(paths(engine)).toEqual([
|
|
['/', 'directory'],
|
|
['/a.txt', 'A'],
|
|
]);
|
|
});
|
|
});
|