// 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, changeFiles, emptyProject, fileText, 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>(ROOTS.nodes).get('pc')!.get('files') as Y.Map> | 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("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(); 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(); 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'], ]); }); });