feat(editor): F4.6d the web browser and web server on a computer's desktop

The oracle BrowserWindowRun uses Java's browser and web server windows as a
person does; the room keeps a browser window per computer and sends a
program window's view, the start names the person's language. The editor
shows the page in a sandboxed frame, links and forms go to the room, the web
server has its button, virtual hosts and lines. Better than Java: relative
images and links count from the page's folder, a mark stays in the page, the
error page from Java's template, the virtual hosts' file with complete rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:09:05 +00:00
co-authored by Claude Opus 5.5
parent f0e7c73ec7
commit aa54e5400e
31 changed files with 2773 additions and 55 deletions
+45
View File
@@ -122,6 +122,20 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
'network.dnsServer': 'network_msg2',
'network.gateway': 'network_msg3',
'network.macAddress': 'network_msg9',
'browser.go': 'webbrowser_msg2',
'server.start': 'webserver_msg1',
'server.stop': 'webserver_msg2',
'webServer.virtualHosts': 'webserver_msg3',
'webServer.hostName': 'webserver_msg4',
'webServer.folder': 'webserver_msg5',
'webServer.useVirtualHosts': 'webserver_msg6',
'server.acceptingStopped': 'sw_serveranwendung_msg1',
'server.acceptingStarted': 'sw_serveranwendung_msg2',
'server.serverSocketClosed': 'sw_serveranwendung_msg3',
'server.serverSocketFailed': 'sw_serveranwendung_msg6',
'server.sendFailed.notConnected': 'sw_tcpsocket_msg6',
'server.sendFailed.connectionLost': 'sw_tcpsocket_msg7',
'server.sendFailed.timedOut': 'sw_tcpsocket_msg8',
};
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
@@ -156,6 +170,20 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
],
};
/** Java's words around a value, as its programs join them („Verbindung zu“ + address + „hergestellt“). */
const FROM_JAVA_JOINED: Readonly<Record<string, readonly [string, string, string]>> = {
'server.connectionAccepted': ['sw_serveranwendung_msg4', '{address}', 'sw_serveranwendung_msg5'],
'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'],
};
/** Java's page for a server it cannot reach (sw_webbrowser_msg1): its title and its heading. */
const FROM_JAVA_PAGE: Readonly<Record<string, readonly [string, RegExp]>> = {
'browser.unreachableTitle': ['sw_webbrowser_msg1', /<title>(.*)<\/title>/],
'browser.unreachable': ['sw_webbrowser_msg1', /<h1>(.*)<\/h1>/],
};
const templates = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/tmpl');
describe('the catalogs', () => {
it('have the same keys in German and English, each with a text', () => {
expect(Object.keys(CATALOGS.en).sort()).toEqual(Object.keys(CATALOGS.de).sort());
@@ -184,5 +212,22 @@ describe('the catalogs', () => {
expect(CATALOGS.de[key], key).toBe(mend(de.get(javaKey)!));
expect(CATALOGS.en[key], key).toBe(mend(en.get(javaKey)!));
}
for (const [key, [before, value, after]] of Object.entries(FROM_JAVA_JOINED)) {
expect(CATALOGS.de[key], key).toBe(`${de.get(before)} ${value} ${de.get(after)}`);
expect(CATALOGS.en[key], key).toBe(`${en.get(before)} ${value} ${en.get(after)}`);
}
for (const [key, [javaKey, part]] of Object.entries(FROM_JAVA_PAGE)) {
expect(CATALOGS.de[key], key).toBe(part.exec(de.get(javaKey)!)![1]);
expect(CATALOGS.en[key], key).toBe(part.exec(en.get(javaKey)!)![1]);
}
});
it.skipIf(!existsSync(templates))("write the browser's error page as Java's template does (tmpl/http_fehler)", () => {
for (const [language, file] of [['de', 'http_fehler_de_DE.txt'], ['en', 'http_fehler_en_GB.txt']] as const) {
const template = readFileSync(join(templates, file), 'utf8').replaceAll(':meldung:', '{message}').replaceAll(':code:', '{code}');
expect(CATALOGS[language]['browser.errorTitle']).toBe(/<title>(.*)<\/title>/.exec(template)![1]);
expect(CATALOGS[language]['browser.errorText']).toBe(/<p>(.*)<\/p>/.exec(template)![1]!.trim());
expect(/<h1>(.*)<\/h1>/.exec(template)![1]).toBe('{message}');
}
});
});
+41
View File
@@ -0,0 +1,41 @@
// A server's lines as Java's web server window wrote them
// (packages/sim/test/browserWindow.java.json, BrowserWindowRun): each kind
// the window got there, in Java's words.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import type { ServerLogEntry } from '@mdg-filius/core';
import { serverLine } from '../src/desktop/serverLines.js';
import { translate } from '../src/i18n/i18n.js';
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../sim/test/browserWindow.java.json'), 'utf8')) as { steps: { step: string; log?: string }[] };
const log = java.steps.find((step) => step.step === 'server:stop')!.log!;
const t = (key: string, values?: Readonly<Record<string, string | number>>) => translate('de', key, values);
describe("a server's lines", () => {
it("are Java's, line by line", () => {
const [, address, port] = /Verbindung zu ([\d.]+):(\d+) hergestellt/.exec(log)!;
const request = 'POST /ziel.html HTTP/1.1\nHost: www.filius.de\n\nname=Fil+ius&c=1&s=y&t=%C3%BC%26%3D&ok=Gut';
const answer = 'HTTP/1.1 200 OK\nContent-type: text/html\n\n<html><head><title>Ziel</title></head><body>Ziel</body></html>';
const entries: ServerLogEntry[] = [
{ kind: 'acceptingStarted' },
{ kind: 'connectionAccepted', address: address!, port: Number(port) },
{ kind: 'received', text: request },
{ kind: 'sent', text: answer },
{ kind: 'connectionClosed', address: address!, port: Number(port) },
{ kind: 'acceptingStopped' },
{ kind: 'serverSocketClosed' },
];
for (const entry of entries) expect(log, entry.kind).toContain(`${serverLine(entry, t)}\n`);
expect(log.startsWith(`${serverLine({ kind: 'acceptingStarted' }, t)}\n`)).toBe(true);
});
it('name a failed send and a port that is taken', () => {
expect(serverLine({ kind: 'sendFailed', failure: 'timedOut' }, t)).toBe('Zeitüberschreitung während des Versands einer Nachricht');
expect(serverLine({ kind: 'sendFailed', failure: 'notConnected' }, (key) => translate('en', key))).toBe('Connection could not be established!');
expect(serverLine({ kind: 'serverSocketFailed' }, t)).toBe('Server-Socket konnte nicht geöffnet werden.');
expect(serverLine({ kind: 'connectionAccepted', address: '1.2.3.4', port: 5 }, (key, values) => translate('en', key, values))).toBe('Connection to 1.2.3.4:5 established');
});
});
@@ -0,0 +1,92 @@
// The project the browser tests open for the web browser and web server
// (browser/fixtures/web-pages.fls), made here with the document's commands:
// a computer "Browser" (192.168.0.10) with a web browser, a computer
// "Server" (192.168.0.20) whose web server runs with the pages below — links,
// a mark in the page, a page in a folder, a GET and a POST form, an image
// there and one missing, a script and a handler that must not run —, and a
// router (192.168.0.1) for its pages, all on one switch. Compared on each run;
// written anew with FILIUS_WRITE_FIXTURES=1.
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { installationFiles, type VirtualFileSystem } from '@mdg-filius/core';
import { addDevice, connect, docArchive, emptyProject, fileEntry, readProject, ROOTS, setHostAddress, setInterfaceAddress, setSoftware, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
const fixture = join(dirname(fileURLToPath(import.meta.url)), '../browser/fixtures/web-pages.fls');
/** The server's pages besides those its installation brings (index.html replaced). */
export const WEB_PAGES: Readonly<Record<string, string>> = {
'/webserver/index.html':
'<html><head><title>Seiten &amp; Formulare</title><script>document.title = "Skript"</script></head>' +
'<body onload="document.body.dataset.ran = \'yes\'"><h1 id="oben">Start</h1>' +
'<img src="splashscreen-mini.png" alt="Bild"><img src="fehlt.png" alt="Fehlt">' +
'<p><a href="zwei.html">Zwei</a> <a href="#unten">Unten</a> <a href="ordner/drei.html">Drei</a></p>' +
'<form action="ziel.html" method="get"><input name="q" value="a b"><input type="submit" name="los" value="Los"></form>' +
'<form action="ziel.html" method="post"><input name="name" value="Fil ius"><input type="checkbox" name="c" value="1" checked>' +
'<input type="checkbox" name="d" value="1"><select name="s"><option>x</option><option selected value="y">Y</option></select>' +
'<textarea name="t">&uuml;&amp;=</textarea><input type="submit" name="ok" value="Gut"></form>' +
'<p id="unten" style="margin-top: 3000px">Unten</p></body></html>',
'/webserver/zwei.html': '<html><head><title>Zwei</title></head><body><a href="index.html">Start</a></body></html>',
'/webserver/ziel.html': '<html><head><title>Ziel</title></head><body>Ziel</body></html>',
'/webserver/ordner/drei.html': '<html><head><title>Drei</title></head><body><a href="vier.html">Vier</a></body></html>',
'/webserver/ordner/vier.html': '<html><head><title>Vier im Ordner</title></head><body>Vier</body></html>',
};
function webPagesDoc(): Y.Doc {
const doc = new Y.Doc();
writeProject(doc, emptyProject());
let ids = 0;
const newId = () => `port-${++ids}`;
addDevice(doc, { kind: 'pc', id: 'browser', name: 'Browser', x: 60, y: 60, newId }, 'me');
addDevice(doc, { kind: 'pc', id: 'server', name: 'Server', x: 360, y: 60, newId }, 'me');
addDevice(doc, { kind: 'router', id: 'router', name: 'Router', x: 210, y: 260, newId }, 'me');
addDevice(doc, { kind: 'networkSwitch', id: 'switch', name: 'Switch', x: 210, y: 140, newId }, 'me');
for (const [node, link] of [['browser', 'cable-1'], ['server', 'cable-2'], ['router', 'cable-3']] as const) connect(doc, { node }, { node: 'switch' }, link, 'me');
setHostAddress(doc, 'browser', 'ipAddress', '192.168.0.10', 'me');
setHostAddress(doc, 'server', 'ipAddress', '192.168.0.20', 'me');
// The router's second card, unplugged, away from the computers' network (its own address would be 192.168.0.10, the browser's).
const routerPorts = (doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('router')!.get('ports') as Y.Array<Y.Map<unknown>>).map((port) => port.get('id') as string);
setInterfaceAddress(doc, 'router', routerPorts[0]!, 'ipAddress', '192.168.0.1', 'me');
setInterfaceAddress(doc, 'router', routerPorts[1]!, 'ipAddress', '10.0.0.1', 'me');
setSoftware(doc, 'browser', { install: ['webBrowser'], remove: [] }, 'de', 'me');
setSoftware(doc, 'server', { install: ['webServer'], remove: [] }, 'de', 'me');
const server = doc.getMap<Y.Map<unknown>>(ROOTS.nodes).get('server')!;
(server.get('programs') as Y.Map<Y.Map<unknown>>).get('webServer')!.set('active', true);
const files = server.get('files') as Y.Map<Y.Map<unknown>>;
let order = files.size;
files.set('/webserver/ordner', fileEntry(order++, { kind: 'directory' }));
for (const [path, text] of Object.entries(WEB_PAGES)) {
if (files.has(path)) (files.get(path)!.get('text') as Y.Text).delete(0, (files.get(path)!.get('text') as Y.Text).length);
if (files.has(path)) (files.get(path)!.get('text') as Y.Text).insert(0, text);
else files.set(path, fileEntry(order++, { kind: 'text', text }));
}
return doc;
}
/** A computer's files, text as it is, the rest by kind. */
function filesOf(files: VirtualFileSystem | undefined): Record<string, string> {
return Object.fromEntries((files?.allEntries() ?? []).map((entry) => [entry.path, entry.content.kind === 'text' ? entry.content.text : entry.content.kind]));
}
describe("the browser tests' project with web pages", () => {
it('is made from the pages above', () => {
const made = docArchive(webPagesDoc()).archive;
if (process.env.FILIUS_WRITE_FIXTURES === '1' || !existsSync(fixture)) writeFileSync(fixture, made);
let ids = 0;
const open = (archive: Uint8Array) => openProjectArchive(archive, () => `id-${++ids}`);
const [saved, fresh] = [open(new Uint8Array(readFileSync(fixture))), open(made)];
const server = (project: typeof saved) => project.graph.nodes.find((node) => node.displayName === 'Server')!.id;
expect(filesOf(saved.virtualFileSystems.get(server(saved)))).toEqual(filesOf(fresh.virtualFileSystems.get(server(fresh))));
expect(filesOf(saved.virtualFileSystems.get(server(saved)))).toMatchObject(WEB_PAGES);
expect(saved.activePrograms.get(server(saved))).toEqual(new Set(['webServer']));
expect(saved.graph.nodes.map((node) => node.displayName).sort()).toEqual(['Browser', 'Router', 'Server', 'Switch']);
expect(saved.graph.links).toHaveLength(3);
// The installation's picture is there for the start page's image.
expect(installationFiles('webServer', 'de').map((file) => file.path)).toContain('/webserver/splashscreen-mini.png');
expect(readProject(webPagesDoc()).installedPrograms.get('browser')).toEqual(new Set(['webBrowser']));
});
});