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:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,343 @@
|
||||
// The room's browser window and the web server's window against Java's
|
||||
// (tools/java-oracle BrowserWindowRun) on web-lan's network: the same
|
||||
// addresses typed, links followed and form buttons pressed, the same address
|
||||
// line, title, page and requests after each; the server's start button, its
|
||||
// virtual hosts' check box and table. Named where the window is better than
|
||||
// Java's:
|
||||
// - an HTTP error shows the browser's error page for its status (Java's
|
||||
// never finds its template and shows "Fehler"),
|
||||
// - a page's relative image counts from the page's folder (Java's from the
|
||||
// server's root),
|
||||
// - a mark in the page (#oben) stays in the page (Java loads the start page),
|
||||
// - the virtual hosts' file holds the complete rows only (Java's holds
|
||||
// every row and, read again, moves a cell of an incomplete row into the
|
||||
// row before).
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { httpStatusText, installationFiles, NetworkRuntimeEngine, VirtualFileSystem, withSnapshot, type WebServer } from '@mdg-filius/core';
|
||||
import { BROWSER_START, browserTitle, htmlTitle, type BrowserPage } from '@mdg-filius/protocol';
|
||||
import { snapshotOf, type Scenario } from '../../core/test/scenario.js';
|
||||
import { BrowserWindow, browserPage } from '../src/browserWindow.js';
|
||||
|
||||
interface JavaBrowserStep {
|
||||
readonly step: string;
|
||||
readonly address: string;
|
||||
readonly title: string;
|
||||
readonly pages: readonly string[];
|
||||
readonly requests: readonly string[];
|
||||
}
|
||||
|
||||
interface JavaServerStep {
|
||||
readonly step: string;
|
||||
readonly button: string;
|
||||
readonly virtualHosts: boolean;
|
||||
readonly serverVirtualHosts: boolean;
|
||||
readonly table: readonly (readonly [string, string])[];
|
||||
readonly vhostsFile: string | null;
|
||||
readonly log: string;
|
||||
}
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const java = JSON.parse(readFileSync(join(here, 'browserWindow.java.json'), 'utf8')) as { readonly steps: readonly (JavaBrowserStep | JavaServerStep)[] };
|
||||
const scenario = JSON.parse(readFileSync(join(here, '../../core/test/scenarios/web-lan.scenario.json'), 'utf8')) as Scenario;
|
||||
const BROWSER = '00000000-0000-0000-0000-000000000201';
|
||||
const SERVER = '00000000-0000-0000-0000-000000000203';
|
||||
|
||||
/** The pages BrowserWindowRun gives the server, besides those it brings. */
|
||||
const PAGES: Record<string, string> = {
|
||||
'/webserver/links.html':
|
||||
'<html><head><title>Links & mehr</title></head><body>\n' +
|
||||
'<a href="ziel.html">rel</a> <a href="/text.txt">abs</a> <a href="http://192.168.0.30/">host</a>\n' +
|
||||
'<a href="ordner/drei.html">folder</a> <a href="ziel.html?a=1&b=2">query</a> <a href="#oben">anchor</a>\n' +
|
||||
'<form action="ziel.html" method="get"><input name="q" value="a b"><input type="submit" name="los" value="Los"></form>\n' +
|
||||
'<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">ü&=</textarea><input type="submit" name="ok" value="Gut"></form>\n' +
|
||||
'</body></html>',
|
||||
'/webserver/ziel.html': '<html><head><title>Ziel</title></head><body>Ziel</body></html>',
|
||||
'/webserver/ohne.html': '<html><body>Ohne Titel</body></html>',
|
||||
'/webserver/text.txt': 'Nur Text\nzwei Zeilen',
|
||||
'/webserver/ordner/drei.html': '<html><head><title>Drei</title></head><body><a href="vier.html">vier</a><img src="bild.png"></body></html>',
|
||||
'/webserver/ordner/vier.html': '<html><body>Vier im Ordner</body></html>',
|
||||
'/webserver/vier.html': '<html><body>Vier oben</body></html>',
|
||||
};
|
||||
|
||||
/** What the form buttons send, as Java's window sends it (and the editor's panel, browserPanel.test.ts). */
|
||||
const FORMS: Record<string, readonly [string, string?]> = {
|
||||
Los: ['ziel.html?q=a+b&los=Los'],
|
||||
Gut: ['ziel.html', 'name=Fil+ius&c=1&s=y&t=%C3%BC%26%3D&ok=Gut'],
|
||||
};
|
||||
|
||||
function serverFiles(): VirtualFileSystem {
|
||||
const files = new VirtualFileSystem();
|
||||
for (const file of installationFiles('webServer', 'de')) {
|
||||
if (file.content.kind === 'directory') files.createDirectory(file.path);
|
||||
else if (file.content.kind === 'text') files.writeTextFile(file.path, file.content.text);
|
||||
else if (file.content.kind === 'image') files.writeImageFile(file.path, file.content.data, file.content.mediaType);
|
||||
}
|
||||
files.writeImageFile('/webserver/bild.png', files.imageFile('/webserver/splashscreen-mini.png').data, 'image/png');
|
||||
files.createDirectory('/webserver/ordner');
|
||||
for (const [path, text] of Object.entries(PAGES)) files.writeTextFile(path, text);
|
||||
return files;
|
||||
}
|
||||
|
||||
function setUp(files: VirtualFileSystem = serverFiles()) {
|
||||
const engine = new NetworkRuntimeEngine(scenario.seed);
|
||||
const snapshot = withSnapshot(snapshotOf(scenario), {
|
||||
webServersByNodeId: new Map([[SERVER, { port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: false }]]),
|
||||
webBrowserNodeIds: new Set([BROWSER]),
|
||||
fileSystemsByNodeId: new Map([[SERVER, files]]),
|
||||
});
|
||||
engine.handle({ type: 'start', snapshot, seed: scenario.seed, initialTimeMilliseconds: 0 });
|
||||
const server = engine.webServer(SERVER)!;
|
||||
const window = new BrowserWindow(engine.webBrowser(BROWSER)!);
|
||||
let requestsSeen = 0;
|
||||
/** The requests the server got since the last call (its ">>" lines). */
|
||||
const requests = () => {
|
||||
const received = server.log.all.filter((line) => line.kind === 'received').map((line) => (line.kind === 'received' ? line.text : ''));
|
||||
const fresh = received.slice(requestsSeen);
|
||||
requestsSeen = received.length;
|
||||
return fresh;
|
||||
};
|
||||
/** As the oracle waits: until a page came and nothing happened for 2.5 s, at most 25 s. */
|
||||
const settle = () => {
|
||||
const browserLog = engine.webBrowser(BROWSER)!.log;
|
||||
const start = browserLog.all.length;
|
||||
const until = engine.state.currentTimeMilliseconds + 25_000;
|
||||
let known = -1;
|
||||
let quiet = engine.state.currentTimeMilliseconds;
|
||||
while (engine.state.currentTimeMilliseconds < until) {
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 100 });
|
||||
const count = browserLog.all.length + server.log.all.length;
|
||||
const page = browserLog.all.slice(start).some((line) => line.kind !== 'image');
|
||||
if (count !== known) {
|
||||
known = count;
|
||||
quiet = engine.state.currentTimeMilliseconds;
|
||||
} else if (page && engine.state.currentTimeMilliseconds - quiet > 2_500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
window.pull();
|
||||
};
|
||||
return { engine, server, window, requests, settle };
|
||||
}
|
||||
|
||||
/** The title as the editor's German window shows it, an HTML page's as written (Java's way; the editor decodes it). */
|
||||
const pageTitle = (page: BrowserPage) =>
|
||||
page.kind === 'html' ? htmlTitle(page.html) : page.kind === 'unreachable' ? 'Fehler' : page.kind === 'error' ? `Fehler ${httpStatusText(page.statusCode)} (${page.statusCode})` : undefined;
|
||||
|
||||
/** Java's last page of a step, as the window's page. */
|
||||
function javaPage(pages: readonly string[]): BrowserPage | undefined {
|
||||
const page = pages.filter((each) => each !== 'update').at(-1);
|
||||
if (page === undefined) return undefined;
|
||||
const [, status, type, body] = /^(\d+) (\S+)\n([\s\S]*)$/.exec(page)!;
|
||||
if (status === '0') return { kind: 'unreachable' };
|
||||
if (status !== '200') return { kind: 'error', statusCode: Number(status) };
|
||||
return type === 'text/html' ? { kind: 'html', html: body!, images: {} } : { kind: 'text', text: body! };
|
||||
}
|
||||
|
||||
/** Where Java errs, per step: its record corrected, as the window does it. */
|
||||
const JAVA_BUGS: Record<string, (step: JavaBrowserStep) => JavaBrowserStep> = {
|
||||
// Java's error page never finds its template: its title is "Fehler".
|
||||
'type:http://www.filius.de/nichts.html': (step) => ({ ...step, title: 'Fehler Not Found (404)' }),
|
||||
// The image beside the page in its folder, which is not there (Java asks for the root's).
|
||||
'follow:ordner/drei.html': (step) => ({ ...step, pages: step.pages.filter((page) => page !== 'update'), requests: [step.requests[0]!, 'GET ordner/bild.png HTTP/1.1\nHost: www.filius.de\n'] }),
|
||||
// A mark in the page: nothing loads (Java loads the host's start page).
|
||||
'follow:#oben': (step) => ({ ...step, address: 'http://www.filius.de/links.html', title: 'Links & mehr', pages: [], requests: [] }),
|
||||
};
|
||||
|
||||
const isServerStep = (step: JavaBrowserStep | JavaServerStep): step is JavaServerStep => step.step.startsWith('server:');
|
||||
|
||||
describe("the browser's window, against Java's", () => {
|
||||
it('shows the address line, title and page Java shows, and sends the requests Java sends', () => {
|
||||
const { server, window, requests, settle } = setUp();
|
||||
let page: BrowserPage = { kind: 'start' };
|
||||
for (const recorded of java.steps) {
|
||||
if (isServerStep(recorded)) {
|
||||
if (recorded.step === 'server:start') server.setActive(true);
|
||||
continue;
|
||||
}
|
||||
const step = JAVA_BUGS[recorded.step]?.(recorded) ?? recorded;
|
||||
const [action, argument] = [step.step.slice(0, step.step.indexOf(':')), step.step.slice(step.step.indexOf(':') + 1).replace(/ \(server off\)$/, '')];
|
||||
if (action === 'type') window.open(argument);
|
||||
if (action === 'follow' || action === 'press') {
|
||||
const [href, post] = action === 'follow' ? [argument] : FORMS[argument]!;
|
||||
window.follow(href, post);
|
||||
}
|
||||
if (step.step !== 'open') settle();
|
||||
expect(window.view.address, step.step).toBe(step.address);
|
||||
expect(browserTitle(window.view, 'Webbrowser', pageTitle), step.step).toBe(step.title);
|
||||
page = javaPage(step.pages) ?? page;
|
||||
const shown = window.view.page;
|
||||
expect(shown.kind === 'html' ? { ...shown, images: {} } : shown, step.step).toEqual(page);
|
||||
// An image that came: Java tells its window ("update").
|
||||
const images = shown.kind === 'html' ? Object.keys(shown.images).length : 0;
|
||||
if (step.pages.includes('update') && step.step !== 'open') expect(images, step.step).toBeGreaterThan(0);
|
||||
expect(requests(), step.step).toEqual(step.requests);
|
||||
// Java's oracle loads the links page anew after each link, without a step.
|
||||
if (action === 'follow' || step.step === 'press:Los') {
|
||||
window.open('www.filius.de/links.html');
|
||||
settle();
|
||||
requests();
|
||||
page = { kind: 'html', html: PAGES['/webserver/links.html']!, images: {} };
|
||||
}
|
||||
}
|
||||
expect(window.view.page).toMatchObject({ kind: 'html', html: PAGES['/webserver/ziel.html'] });
|
||||
});
|
||||
|
||||
it('shows its start page with the program as its title before it loads anything', () => {
|
||||
const { window } = setUp();
|
||||
expect(window.view).toBe(BROWSER_START);
|
||||
const open = java.steps.find((step) => step.step === 'open') as JavaBrowserStep;
|
||||
expect([window.view.address, browserTitle(window.view, 'Webbrowser', pageTitle)]).toEqual([open.address, open.title]);
|
||||
});
|
||||
|
||||
it("shows a page's images from their data, the images of a page in a folder from beside it", () => {
|
||||
const files = serverFiles();
|
||||
files.writeImageFile('/webserver/ordner/bild.png', new Uint8Array([1, 2, 3]), 'image/png');
|
||||
const { server, window, settle, requests } = setUp(files);
|
||||
server.setActive(true);
|
||||
window.open('192.168.0.30');
|
||||
settle();
|
||||
expect(window.view.page).toMatchObject({ kind: 'html', images: { 'splashscreen-mini.png': { contentType: 'image/png' } } });
|
||||
window.open('www.filius.de/ordner/drei.html');
|
||||
settle();
|
||||
requests();
|
||||
expect(window.view.page).toMatchObject({ kind: 'html', images: { 'bild.png': { contentType: 'image/png', data: 'AQID' } } });
|
||||
// Its link, also from beside it.
|
||||
window.follow('vier.html');
|
||||
settle();
|
||||
expect([window.view.address, window.view.page]).toEqual(['http://www.filius.de/ordner/vier.html', { kind: 'html', html: PAGES['/webserver/ordner/vier.html'], images: {} }]);
|
||||
});
|
||||
|
||||
it('shows plain text as text and an image as an image (Java shows both as HTML)', () => {
|
||||
const { server, window, settle } = setUp();
|
||||
server.setActive(true);
|
||||
window.open('www.filius.de/text.txt');
|
||||
settle();
|
||||
expect(window.view.page).toEqual({ kind: 'text', text: 'Nur Text\nzwei Zeilen' });
|
||||
window.open('www.filius.de/bild.png');
|
||||
settle();
|
||||
expect(window.view.page).toMatchObject({ kind: 'image', image: { contentType: 'image/png' } });
|
||||
expect((window.view.page as { image: { data: string } }).image.data).toMatch(/^iVBOR[\w+/=]+$/);
|
||||
});
|
||||
|
||||
it('follows no link without a page, nor one of another scheme, and keeps its page over a new program', () => {
|
||||
const { engine, server, window, settle, requests } = setUp();
|
||||
window.follow('ziel.html');
|
||||
settle();
|
||||
expect(window.view).toBe(BROWSER_START);
|
||||
server.setActive(true);
|
||||
window.open('www.filius.de/links.html');
|
||||
settle();
|
||||
requests();
|
||||
const before = window.view;
|
||||
window.follow('mailto:anna@filius.de');
|
||||
window.follow('ftp://www.filius.de/');
|
||||
settle();
|
||||
expect([window.view, requests()]).toEqual([before, []]);
|
||||
// A new start's program (Java's window outlives the stop): the page stays, the new program's lines are read.
|
||||
engine.handle({ type: 'stop' });
|
||||
engine.handle({ type: 'start', snapshot: engine.state.topologySnapshot, seed: scenario.seed, initialTimeMilliseconds: engine.state.currentTimeMilliseconds });
|
||||
window.attach(engine.webBrowser(BROWSER)!);
|
||||
expect(window.view).toBe(before);
|
||||
engine.webServer(SERVER)!.setActive(true);
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 1_000 });
|
||||
// The new program writes its page before the window reads it again: read from its first line.
|
||||
window.follow('ziel.html');
|
||||
settle();
|
||||
expect(engine.webBrowser(BROWSER)!.log.all.map((line) => line.kind)).toEqual(['page']);
|
||||
expect(window.view.page).toEqual({ kind: 'html', html: PAGES['/webserver/ziel.html'], images: {} });
|
||||
});
|
||||
|
||||
it("writes the browser's last host into the address line for an address without one, as Java's window does", () => {
|
||||
const { server, window, settle } = setUp();
|
||||
server.setActive(true);
|
||||
window.open('www.filius.de/links.html');
|
||||
settle();
|
||||
window.open('http:///ziel.html?x=1');
|
||||
expect(window.view.address).toBe('http://www.filius.de/ziel.html');
|
||||
settle();
|
||||
expect(window.view.page).toEqual({ kind: 'html', html: PAGES['/webserver/ziel.html'], images: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe("the web server's window, against Java's", () => {
|
||||
const steps = java.steps.filter(isServerStep);
|
||||
const step = (name: string) => steps.find((each) => each.step === `server:${name}`)!;
|
||||
const table = (server: WebServer) => server.virtualHostTable.map(([host, folder]) => [host, folder]);
|
||||
const vhosts = (files: VirtualFileSystem) => (files.contains('/www.conf/vhosts') ? files.textFile('/www.conf/vhosts') : null);
|
||||
|
||||
it('starts and stops, switches the virtual hosts and writes their table as Java does', () => {
|
||||
const files = serverFiles();
|
||||
const { server } = setUp(files);
|
||||
const state = () => ({ active: server.isActive, virtualHosts: server.useVirtualHosts, table: table(server), vhostsFile: vhosts(files) });
|
||||
const javaState = (each: JavaServerStep) => ({ active: each.button === 'Anhalten', virtualHosts: each.virtualHosts, table: each.table, vhostsFile: each.vhostsFile });
|
||||
expect(state()).toEqual(javaState(step('open')));
|
||||
server.setActive(true);
|
||||
expect(state()).toEqual(javaState(step('start')));
|
||||
server.setUseVirtualHosts(true);
|
||||
expect(state()).toEqual(javaState(step('virtual hosts on')));
|
||||
for (const [row, column, value] of [
|
||||
[0, 0, 'seite.filius.de'],
|
||||
[0, 1, 'vhost1'],
|
||||
[1, 0, 'nurhost.filius.de'],
|
||||
[2, 1, 'nurordner'],
|
||||
[3, 0, 'andere.filius.de'],
|
||||
[3, 1, 'vhost2'],
|
||||
] as const) {
|
||||
server.setVirtualHost(row, column, value);
|
||||
}
|
||||
// The complete rows only (Java writes every row, its incomplete ones as empty lines).
|
||||
expect(state()).toEqual({ ...javaState(step('rows written')), vhostsFile: 'seite.filius.de\nvhost1\nandere.filius.de\nvhost2\n' });
|
||||
expect(step('rows written').vhostsFile).toBe('seite.filius.de\nvhost1\nnurhost.filius.de\n\n\nnurordner\nandere.filius.de\nvhost2\n\n\n');
|
||||
});
|
||||
|
||||
it('reads its table again at the next start as it wrote it (Java moves the incomplete rows’ cells)', () => {
|
||||
const files = serverFiles();
|
||||
const { server } = setUp(files);
|
||||
server.setUseVirtualHosts(true);
|
||||
for (const [row, column, value] of [
|
||||
[0, 0, 'seite.filius.de'],
|
||||
[0, 1, 'vhost1'],
|
||||
[1, 0, 'nurhost.filius.de'],
|
||||
[2, 1, 'nurordner'],
|
||||
[3, 0, 'andere.filius.de'],
|
||||
[3, 1, 'vhost2'],
|
||||
] as const) {
|
||||
server.setVirtualHost(row, column, value);
|
||||
}
|
||||
const again = setUp(files).server;
|
||||
// A file of virtual hosts switches them on, as in Java.
|
||||
expect(again.useVirtualHosts).toBe(step('restart').serverVirtualHosts);
|
||||
expect(table(again)).toEqual([['seite.filius.de', 'vhost1'], ['andere.filius.de', 'vhost2'], ['', ''], ['', ''], ['', '']]);
|
||||
expect(step('restart').table).toEqual([['seite.filius.de', 'vhost1'], ['nurhost.filius.de', 'nurordner'], ['andere.filius.de', 'vhost2'], ['', ''], ['', '']]);
|
||||
// Switched off, they are on again at the next start while the file is there, as in Java (until the window's setting is kept, F4.6f).
|
||||
again.setUseVirtualHosts(false);
|
||||
expect([again.useVirtualHosts, setUp(files).server.useVirtualHosts]).toEqual([step('virtual hosts off').serverVirtualHosts, step('restart after off').serverVirtualHosts]);
|
||||
});
|
||||
|
||||
it("writes Java's lines into its window when it starts and stops", () => {
|
||||
const { engine, server } = setUp();
|
||||
server.setActive(true);
|
||||
expect(server.log.all.map((line) => line.kind)).toEqual(['acceptingStarted']);
|
||||
expect(step('start').log).toBe('Annahme von Verbindungsanfragen gestartet\n');
|
||||
server.setActive(false);
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 200 });
|
||||
expect(server.log.all.map((line) => line.kind).slice(-2)).toEqual(['acceptingStopped', 'serverSocketClosed']);
|
||||
expect(step('stop').log.endsWith('Annahme von Verbindungsanfragen beendet\nServer-Socket geschlossen\n')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the page for what came', () => {
|
||||
it("is the error page for any status but 200, else by the content type in any case", () => {
|
||||
for (const statusCode of [0, 201, 301, 400, 404, 500, 501]) expect(browserPage(statusCode, 'text/html', '<p>x</p>')).toEqual({ kind: 'error', statusCode });
|
||||
expect(browserPage(200, 'TEXT/HTML', undefined)).toEqual({ kind: 'html', html: '', images: {} });
|
||||
expect(browserPage(200, 'image/GIF', 'R0lG\nODlh')).toEqual({ kind: 'image', image: { contentType: 'image/gif', data: 'R0lGODlh' } });
|
||||
expect(browserPage(200, 'image/jpeg', 'x')).toEqual({ kind: 'text', text: 'x' });
|
||||
expect(browserPage(200, undefined, undefined)).toEqual({ kind: 'text', text: '' });
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user