feat: F4.6f2 the text editor on the document's Y.Text, files both ways while the simulation runs
Oracle TextEditorWindowRun uses Java's text editor window and its file dialog as a person does. The open file's text is its Y.Text in the document (plan 7.3): typed is in the file at once, for everyone and for the running engine. Core openText/saveTextAs without Java's mistakes (lost saves after save-as onto a file or a removed file, files named 'ordner/d.txt' or ''); doc files.ts (changeFiles, fileText, nodeFiles, readFilesInto); the room takes a person's files into the engine at once and writes the engine's files back at the tick they changed; the editor's window, Java's dialog, the text area following others' changes with the caret in place, its own undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
import { VirtualFileSystem as VirtualFileSystemOf, type Id, type VirtualFileSystem } from '@mdg-filius/core';
|
||||
import { createUndoManager, readProject, ROOTS, setSoftware, SIM_ORIGIN, writeProject, writeRunState } from '@mdg-filius/doc';
|
||||
import { addDevice, changeFiles, createUndoManager, emptyProject, fileText, readProject, ROOTS, setSoftware, SIM_ORIGIN, writeProject, writeRunState } from '@mdg-filius/doc';
|
||||
import { openProjectArchive, type ImportedProject } from '@mdg-filius/fls';
|
||||
import type { HostMessage } from '@mdg-filius/protocol';
|
||||
import { SimulationRoom, TICK_MILLISECONDS, WRITE_BACK_MILLISECONDS } from '../src/index.js';
|
||||
@@ -39,7 +39,9 @@ function setUp(file: string, writeBackMilliseconds?: number) {
|
||||
room.tick();
|
||||
}
|
||||
};
|
||||
return { doc, room, got, id, act, run, send: (message: object) => room.receive(client, { v: 1, ...message }) };
|
||||
/** What the room sent of a window since the last call, as text. */
|
||||
const sent = (app: string) => JSON.stringify(got.splice(0).filter((message) => message.kind === 'app' && message.app === app));
|
||||
return { doc, room, got, id, act, run, sent, send: (message: object) => room.receive(client, { v: 1, ...message }) };
|
||||
}
|
||||
|
||||
/** A computer's files: text as it is, the rest by kind. */
|
||||
@@ -240,3 +242,86 @@ describe("the room's write-back", () => {
|
||||
expect(readProject(doc).webServerConfigurations.has(nodeId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('files both ways while the simulation runs', () => {
|
||||
it("takes what a person writes into the document at once, and writes the engine's files at the tick they changed", () => {
|
||||
const { doc, id, act, run, send, sent } = setUp('email_komplett.fls', 60_000);
|
||||
const one = id('Rechner 1');
|
||||
setSoftware(doc, one, { install: ['commandPrompt'], remove: [] }, 'de', 'me');
|
||||
send({ kind: 'sub', topics: [`app:${one}/terminal`] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
// A file saved by a person (the text editor), then typed into: the command line reads it.
|
||||
changeFiles(doc, one, 'me', (files) => files.writeTextFile('/notiz.txt', 'Hallo'));
|
||||
act('Rechner 1', 'terminal', 'enter', { line: 'cat notiz.txt' });
|
||||
expect(sent('terminal')).toContain('Hallo');
|
||||
doc.transact(() => fileText(doc, one, '/notiz.txt')!.insert(5, ' Welt'), 'me');
|
||||
act('Rechner 1', 'terminal', 'enter', { line: 'cat notiz.txt' });
|
||||
expect(sent('terminal')).toContain('Hallo Welt');
|
||||
// Removed by a person: gone for the command line.
|
||||
changeFiles(doc, one, 'me', (files) => files.deleteItem('/notiz.txt'));
|
||||
act('Rechner 1', 'terminal', 'enter', { line: 'cat notiz.txt' });
|
||||
expect(sent('terminal')).not.toContain('Hallo');
|
||||
|
||||
// A mail taken by the server changes its accounts file in a tick: in the document then, the settings a minute later.
|
||||
const server = id('Mail-Server (filius.de)');
|
||||
const accountsFile = () => fileText(doc, server, '/mailserver/konten.txt')!.toString();
|
||||
const before = accountsFile();
|
||||
const mailboxes = () => readProject(doc).emailServerConfigurations.get(server)!.accounts.map((account) => account.mailbox.length);
|
||||
const settings = mailboxes();
|
||||
act('Rechner 1', 'emailClient', 'setAccount', ACCOUNT('rechner1', '1'));
|
||||
act('Rechner 1', 'emailClient', 'send', { to: 'rechner2@filius.de', cc: '', bcc: '', subject: 'Im Lauf', text: 'Text' });
|
||||
const afterAction = accountsFile();
|
||||
let tick = 0;
|
||||
while (accountsFile() === afterAction && tick++ < 50) run(TICK_MILLISECONDS);
|
||||
expect(accountsFile()).not.toBe(before);
|
||||
expect(accountsFile()).toContain('Im Lauf');
|
||||
expect(mailboxes()).toEqual(settings);
|
||||
|
||||
// A person types into the file the engine just wrote: both stay.
|
||||
doc.transact(() => fileText(doc, server, '/mailserver/konten.txt')!.insert(0, '# '), 'me');
|
||||
expect(accountsFile().startsWith('# ')).toBe(true);
|
||||
expect(accountsFile()).toContain('Im Lauf');
|
||||
});
|
||||
|
||||
it('takes the first file of a computer that had none, leaves out one whose engine has no files, and writes nothing back of what it took', () => {
|
||||
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');
|
||||
addDevice(doc, { kind: 'pc', id: 'bare', name: 'Bare', x: 100, y: 0, newId: () => `port-${++counter}` }, 'me');
|
||||
const programs = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.get('programs') as Y.Map<Y.Map<unknown>>;
|
||||
const entry = new Y.Map<unknown>();
|
||||
entry.set('active', true);
|
||||
programs.set('commandPrompt', entry);
|
||||
// Two computers without files (a new one has a few).
|
||||
for (const id of ['pc', 'bare']) doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get(id)!.delete('files');
|
||||
let now = 0;
|
||||
const room = new SimulationRoom(doc, { now: () => now, seed: () => 7 });
|
||||
const got: HostMessage[] = [];
|
||||
const client = room.connect((message) => got.push(message));
|
||||
const send = (fields: object) => room.receive(client, { v: 1, ...fields });
|
||||
const run = (milliseconds: number) => {
|
||||
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
|
||||
now += TICK_MILLISECONDS;
|
||||
room.tick();
|
||||
}
|
||||
};
|
||||
send({ kind: 'sub', topics: ['app:pc/terminal'] });
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
expect(doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('pc')!.has('files')).toBe(false);
|
||||
const writes: unknown[] = [];
|
||||
doc.on('afterTransaction', (transaction: Y.Transaction) => transaction.origin === SIM_ORIGIN && transaction.changed.size > 0 && writes.push(transaction));
|
||||
changeFiles(doc, 'pc', 'me', (files) => files.writeTextFile('/erste.txt', 'Erste Datei'));
|
||||
// A computer whose programs use no files: its file waits for the next start, the simulation runs on.
|
||||
changeFiles(doc, 'bare', 'me', (files) => files.writeTextFile('/x.txt', 'x'));
|
||||
run(3 * WRITE_BACK_MILLISECONDS);
|
||||
expect(room.currentMode).toBe('simulation');
|
||||
expect(got.filter((message) => message.kind === 'error')).toEqual([]);
|
||||
// What it took it does not write back: the document has it.
|
||||
expect(writes).toHaveLength(0);
|
||||
send({ kind: 'input', node: 'pc', app: 'terminal', action: 'enter', args: { line: 'cat erste.txt' }, reqId: 'cat' });
|
||||
expect(JSON.stringify(got)).toContain('Erste Datei');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user