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
+97 -1
View File
@@ -196,7 +196,7 @@ describe('a simulation room', () => {
expect(reason({ node: 'nobody', app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownNode' });
expect(reason({ node: node('Switch 1').id, app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownApp' });
expect(reason({ node: notebook, app: 'terminal', action: 'paint' })).toMatchObject({ code: 'unknownAction' });
expect(reason({ node: notebook, app: 'webBrowser', action: 'open' })).toMatchObject({ code: 'unknownAction' });
expect(reason({ node: notebook, app: 'emailClient', action: 'send' })).toMatchObject({ code: 'unknownAction' });
expect(reason({ node: notebook, app: 'terminal', action: 'enter', args: { line: 7 } })).toMatchObject({ code: 'badArgs' });
expect(reason({ node: notebook, app: 'terminal', action: 'interrupt' })).toEqual({ v: 1, kind: 'ack', reqId: 'r' });
send({ kind: 'nonsense', reqId: 'q' });
@@ -371,3 +371,99 @@ describe('a simulation room', () => {
});
});
describe("a simulation room's browser and web server windows", () => {
const views = (got: readonly HostMessage[], node: Id, app: string) => ofKind(got, 'view').filter((each) => each.node === node && each.app === app).map((each) => each.view);
it("sends the browser's view and the web server's whole when they change, and none of the browser's lines", () => {
const { node, send, run, got } = setUp();
const notebook = node('Notebook 1.10').id;
const server = node('Webserver 5.10').id;
send({ kind: 'sub', topics: [`app:${notebook}/webBrowser`, `app:${server}/webServer`] });
// Before a start: neither has anything.
expect([views(got, notebook, 'webBrowser'), views(got, server, 'webServer')]).toEqual([[null], [null]]);
send({ kind: 'start' });
run(TICK_MILLISECONDS);
expect(views(got, notebook, 'webBrowser')).toEqual([null, { address: 'http://', loading: null, arrived: false, page: { kind: 'start' } }]);
expect(views(got, server, 'webServer')).toEqual([null, { active: true, useVirtualHosts: false, virtualHosts: [['', ''], ['', ''], ['', ''], ['', ''], ['', '']] }]);
send({ kind: 'input', node: notebook, app: 'webBrowser', action: 'open', args: { address: '141.99.5.10' }, reqId: 'open' });
expect(ofKind(got, 'ack').map((each) => each.reqId)).toEqual(['open']);
expect(views(got, notebook, 'webBrowser').at(-1)).toEqual({ address: 'http://141.99.5.10/', loading: 'http://141.99.5.10/', arrived: false, page: { kind: 'start' } });
run(20_000);
const page = views(got, notebook, 'webBrowser').at(-1) as { arrived: boolean; page: { kind: string; html: string; images: Record<string, unknown> } };
expect(page.arrived).toBe(true);
expect(page.page.kind).toBe('html');
expect(Object.keys(page.page.images).length).toBeGreaterThan(0);
// The browser's lines are its view; the server's go on as lines.
expect(ofKind(got, 'app').some((each) => each.app === 'webBrowser')).toBe(false);
expect(ofKind(got, 'app').filter((each) => each.app === 'webServer').flatMap((each) => each.lines as { kind: string }[]).some((line) => line.kind === 'received')).toBe(true);
// Nothing new: no view sent again.
const before = got.length;
run(1_000);
expect(ofKind(got.slice(before), 'view')).toEqual([]);
// The server's button, check box and table.
send({ kind: 'input', node: server, app: 'webServer', action: 'setActive', args: { active: false }, reqId: 's1' });
send({ kind: 'input', node: server, app: 'webServer', action: 'useVirtualHosts', args: { use: true }, reqId: 's2' });
send({ kind: 'input', node: server, app: 'webServer', action: 'setVirtualHost', args: { row: 4, column: 1, value: 'ordner' }, reqId: 's3' });
expect(views(got, server, 'webServer').at(-1)).toEqual({ active: false, useVirtualHosts: true, virtualHosts: [['', ''], ['', ''], ['', ''], ['', ''], ['', 'ordner']] });
expect(views(got, server, 'webServer')).toHaveLength(5);
// A stop: the browser keeps its page (Java's window does), the server has none; a client subscribing again gets both.
send({ kind: 'stop' });
expect(views(got, server, 'webServer').at(-1)).toBeNull();
const again = got.length;
send({ kind: 'sub', topics: [`app:${notebook}/webBrowser`] });
expect(views(got.slice(again), notebook, 'webBrowser')).toEqual([page]);
});
it('follows the links of a page, and writes the routers’ pages in the language of the start', () => {
const { node, send, run, got, room } = setUp();
const notebook = node('Notebook 1.10').id;
const gateway = docSnapshot(exampleDoc()).deviceConfigurations.get(notebook)!.defaultGateway;
send({ kind: 'sub', topics: [`app:${notebook}/webBrowser`] });
const page = () => (views(got, notebook, 'webBrowser').at(-1) as { address: string; page: { html?: string } });
send({ kind: 'start' });
run(TICK_MILLISECONDS);
send({ kind: 'input', node: notebook, app: 'webBrowser', action: 'open', args: { address: gateway }, reqId: 'r' });
run(20_000);
expect(page().page.html).toContain('Administration des Vermittlungsrechners');
send({ kind: 'input', node: notebook, app: 'webBrowser', action: 'follow', args: { href: 'routes' }, reqId: 'f' });
run(20_000);
expect([page().address, page().page.html]).toEqual([`http://${gateway}/routes`, expect.stringContaining('Routing-/Weiterleitungs-Tabelle')]);
room.stop();
send({ kind: 'start', language: 'en' });
run(TICK_MILLISECONDS);
send({ kind: 'input', node: notebook, app: 'webBrowser', action: 'open', args: { address: gateway }, reqId: 'r' });
run(20_000);
expect(page().page.html).toContain('Router Administration');
});
it('names a wrong action in the windows', () => {
const { node, send, run, got } = setUp();
const notebook = node('Notebook 1.10').id;
const server = node('Webserver 5.10').id;
const dns = node('DNS-Server 2.10').id;
send({ kind: 'start' });
run(TICK_MILLISECONDS);
const cases: [string, string, string, Record<string, unknown>, string][] = [
[notebook, 'webBrowser', 'open', {}, 'badArgs'],
[notebook, 'webBrowser', 'follow', { href: 7 }, 'badArgs'],
[notebook, 'webBrowser', 'follow', { href: 'a', post: 1 }, 'badArgs'],
[notebook, 'webBrowser', 'back', {}, 'unknownAction'],
[dns, 'webBrowser', 'open', { address: 'x' }, 'unknownApp'],
[server, 'webServer', 'setActive', { active: 'yes' }, 'badArgs'],
[server, 'webServer', 'useVirtualHosts', { use: 1 }, 'badArgs'],
[server, 'webServer', 'setVirtualHost', { row: 5, column: 0, value: 'a' }, 'badArgs'],
[server, 'webServer', 'setVirtualHost', { row: -1, column: 0, value: 'a' }, 'badArgs'],
[server, 'webServer', 'setVirtualHost', { row: 1.5, column: 0, value: 'a' }, 'badArgs'],
[server, 'webServer', 'setVirtualHost', { row: 0, column: 2, value: 'a' }, 'badArgs'],
[server, 'webServer', 'setVirtualHost', { row: 0, column: 1, value: 2 }, 'badArgs'],
[server, 'webServer', 'restart', {}, 'unknownAction'],
[notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'],
[server, 'emailServer', 'setActive', { active: true }, 'unknownAction'],
];
for (const [target, app, action, args, code] of cases) {
send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` });
expect(ofKind(got, 'error').at(-1), `${app} ${action} ${JSON.stringify(args)}`).toMatchObject({ code, reqId: `${app}.${action}.${JSON.stringify(args)}` });
}
expect(ofKind(got, 'ack')).toEqual([]);
});
});