feat: F4.6g1 the echo server and the generic client windows
Oracle EchoWindowRun uses Java's echo server and client windows as a person does; SaveAfterRunRun echo saves what Java keeps after the run. The room takes setActive with a port, connect/disconnect/send, sends both views and writes the server's port and the client's target back. The editor's windows show Java's buttons, fields and lines; a port that is no port is marked and not handed over (Java's button throws). Core fix: an empty message (one empty segment with the push, as Java's erstelleSegmente makes) counted as a repeat and never arrived. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// The echo server and the generic client on two computers' desktops in
|
||||
// Chromium (F4.6g1), on browser/fixtures/web-pages.fls: installed while the
|
||||
// simulation runs; the server started on a port typed (one that is no port
|
||||
// marked and not handed over), the client connected, a message sent and
|
||||
// echoed, the fields locked while connected, the server's lines, "Trennen".
|
||||
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { drawing, onDevice, serveEditor } from './editorPage.js';
|
||||
|
||||
let server: { readonly base: string; readonly close: () => Promise<void> };
|
||||
|
||||
test.beforeAll(async () => {
|
||||
server = await serveEditor();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
const FIXTURE = join(dirname(fileURLToPath(import.meta.url)), 'fixtures/web-pages.fls');
|
||||
|
||||
async function openPages(page: Page): Promise<void> {
|
||||
await page.goto(`${server.base}?example=new`);
|
||||
await page.waitForSelector('svg.filius-canvas');
|
||||
await page.locator('input[type="file"]').first().setInputFiles(FIXTURE);
|
||||
await expect.poll(async () => (await drawing(page).catch(() => ({ nodes: [] }))).nodes.length).toBe(4);
|
||||
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
|
||||
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||
}
|
||||
|
||||
/** A computer's desktop opened with a tap, a program installed on it, and its window opened. */
|
||||
async function install(page: Page, name: string, program: string, label: string) {
|
||||
const id = (await drawing(page)).nodes.find((node) => node.name === name)!.id;
|
||||
const at = await onDevice(page, id);
|
||||
await page.mouse.click(at.x, at.y);
|
||||
const desktop = page.locator(`section[data-desktop="${id}"]`);
|
||||
await desktop.locator('[data-program="installation"]').click();
|
||||
const installation = desktop.locator('[data-program-window="installation"]');
|
||||
await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: label }).click();
|
||||
await installation.getByRole('button', { name: 'Installieren' }).click();
|
||||
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||
await desktop.locator(`[data-program="${program}"]`).click();
|
||||
return { desktop, window: desktop.locator(`[data-program-window="${program}"]`) };
|
||||
}
|
||||
|
||||
test('echoes a message from the generic client, as Java’s windows do', async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await openPages(page);
|
||||
const echo = await install(page, 'Server', 'echoServer', 'Echo-Server');
|
||||
await expect(echo.window.locator('.program-titlebar h3')).toHaveText('Echo-Server');
|
||||
const port = echo.window.getByRole('textbox', { name: 'Port:' });
|
||||
await expect(port).toHaveValue('55555');
|
||||
// A port that is no port: marked, the button does nothing.
|
||||
await port.fill('abc');
|
||||
await expect(port).toHaveAttribute('aria-invalid', 'true');
|
||||
await echo.window.getByRole('button', { name: 'Starten' }).click();
|
||||
await expect(echo.window.getByRole('button', { name: 'Starten' })).toBeVisible();
|
||||
await port.fill('7777');
|
||||
await echo.window.getByRole('button', { name: 'Starten' }).click();
|
||||
await expect(echo.window.getByRole('button', { name: 'Anhalten' })).toBeVisible();
|
||||
await expect(port).toHaveAttribute('readonly', '');
|
||||
const serverLog = echo.window.getByLabel('Ausgabe');
|
||||
await expect(serverLog).toHaveText('Annahme von Verbindungsanfragen gestartet\n');
|
||||
await echo.desktop.locator('.desktop-close').click();
|
||||
|
||||
const client = await install(page, 'Browser', 'simpleClient', 'Einfacher Client');
|
||||
const send = client.window.getByRole('button', { name: 'Senden' });
|
||||
await expect(send).toBeDisabled();
|
||||
await client.window.getByRole('textbox', { name: 'Server-Adresse:' }).fill('192.168.0.20');
|
||||
await client.window.getByRole('textbox', { name: 'Server-Port:' }).fill('7777');
|
||||
await client.window.getByRole('button', { name: 'Verbinden' }).click();
|
||||
await expect(client.window.getByRole('button', { name: 'Trennen' })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(client.window.getByRole('textbox', { name: 'Server-Adresse:' })).toHaveAttribute('readonly', '');
|
||||
const output = client.window.getByLabel('Ausgabe');
|
||||
await expect(output).toHaveText('Verbindung hergestellt\n');
|
||||
await client.window.getByRole('textbox', { name: 'Nachricht:' }).fill('Hallo Echo');
|
||||
await send.click();
|
||||
await expect(client.window.getByRole('textbox', { name: 'Nachricht:' })).toHaveValue('');
|
||||
await expect(output).toHaveText('Verbindung hergestellt\n<<Hallo Echo\n>>Hallo Echo\n', { timeout: 30_000 });
|
||||
await client.window.getByRole('button', { name: 'Trennen' }).click();
|
||||
await expect(client.window.getByRole('button', { name: 'Verbinden' })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(output).toContainText('Verbindung getrennt');
|
||||
await expect(send).toBeDisabled();
|
||||
});
|
||||
@@ -13,8 +13,8 @@
|
||||
// icon opens its window over the desktop (ProgramFrame), the home button
|
||||
// leaves it; so far the command line (F4.6b), the web browser and the web
|
||||
// server (F4.6d), the e-mail program and server (F4.6e), the text editor
|
||||
// (F4.6f2), the file explorer and the image viewer (F4.6f3) have one, the
|
||||
// others follow. The browser's window takes its title from its page, the
|
||||
// (F4.6f2), the file explorer and the image viewer (F4.6f3), the echo
|
||||
// server and the generic client (F4.6g1) have one, the others follow. The browser's window takes its title from its page, the
|
||||
// text editor's from its open file, the image viewer's from its picture, as
|
||||
// Java's do.
|
||||
|
||||
@@ -38,6 +38,7 @@ import { InstallationPanel } from './InstallationPanel.js';
|
||||
import { TerminalPanel } from './TerminalPanel.js';
|
||||
import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js';
|
||||
import { FileExplorerPanel } from './FileExplorerPanel.js';
|
||||
import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js';
|
||||
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
|
||||
import { WebServerPanel } from './WebServerPanel.js';
|
||||
import './desktop.css';
|
||||
@@ -68,7 +69,7 @@ export interface DesktopWindowProps {
|
||||
export type DesktopWindowName = DesktopProgram | 'installation';
|
||||
|
||||
/** The programs whose windows are built so far. */
|
||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer']);
|
||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient']);
|
||||
|
||||
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
|
||||
function useDesktopView(doc: Y.Doc, id: string) {
|
||||
@@ -183,6 +184,8 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
{shown === 'textEditor' ? <TextEditorPanel doc={doc} node={id} /> : null}
|
||||
{shown === 'fileExplorer' ? <FileExplorerPanel doc={doc} node={id} /> : null}
|
||||
{shown === 'imageViewer' ? <ImageViewerPanel doc={doc} node={id} /> : null}
|
||||
{shown === 'echoServer' ? <EchoServerPanel node={id} simulation={simulation} /> : null}
|
||||
{shown === 'simpleClient' ? <SimpleClientPanel node={id} simulation={simulation} /> : null}
|
||||
</ProgramFrame>
|
||||
)}
|
||||
{info ? (
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// A computer's echo server and generic client, after Java's
|
||||
// GUIApplicationServerBausteinWindow and GUIApplicationClientBausteinWindow
|
||||
// (measured with tools/java-oracle's EchoWindowRun). The server: its start
|
||||
// button ("Starten"/"Anhalten") with the port beside it, which can be typed
|
||||
// into only while it does not run, and its lines. The client: the server's
|
||||
// address and port (typed into only while it is not connected; they show the
|
||||
// program's again when it changes, as Java's window sets them from it),
|
||||
// "Verbinden"/"Trennen", the message and "Senden" (only while connected; the
|
||||
// message is emptied once it is sent) and the lines. The programs are the
|
||||
// room's: the panels show their views and lines and hand over what is done.
|
||||
//
|
||||
// Better than Java: a port that is no port (Java's Integer.parseInt throws,
|
||||
// and the button does nothing) is marked red, and the button hands it not
|
||||
// over.
|
||||
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import type { LogLine, ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import { portOfText, type EchoServerView, type SimpleClientView } from '@mdg-filius/protocol';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Simulation } from '../simulation/useSimulation.js';
|
||||
import { serverLine, simpleClientLine } from './serverLines.js';
|
||||
|
||||
const NO_LINES: readonly LogLine<never>[] = [];
|
||||
|
||||
/** A program's lines as text, the newest in sight as Java's text area scrolls. */
|
||||
function useLines<Entry>(simulation: Simulation, topic: `app:${string}`, line: (entry: Entry) => string) {
|
||||
const lines = (simulation.apps.get(topic as never)?.lines as readonly LogLine<Entry>[] | undefined) ?? NO_LINES;
|
||||
const text = lines.map((each) => `${line(each)}\n`).join('');
|
||||
const element = useRef<HTMLPreElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (element.current !== null) element.current.scrollTop = element.current.scrollHeight;
|
||||
}, [text]);
|
||||
return { text, element };
|
||||
}
|
||||
|
||||
export function EchoServerPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) {
|
||||
const t = useTranslate();
|
||||
const topic = `app:${node}/echoServer` as const;
|
||||
const { watch, act } = simulation;
|
||||
useEffect(() => watch([topic]), [watch, topic]);
|
||||
const view = simulation.views.get(topic) as EchoServerView | null | undefined;
|
||||
const [port, setPort] = useState(view === null || view === undefined ? '' : String(view.port));
|
||||
useEffect(() => {
|
||||
if (view !== null && view !== undefined) setPort(String(view.port));
|
||||
}, [view]);
|
||||
const { text, element } = useLines<ServerLogEntry>(simulation, topic, (entry) => serverLine(entry, t));
|
||||
if (view === null || view === undefined) return <div className="web-server" />;
|
||||
const typed = portOfText(port);
|
||||
const press = () => {
|
||||
if (view.active) act(node, 'echoServer', 'setActive', { active: false });
|
||||
else if (typed !== undefined) act(node, 'echoServer', 'setActive', { active: true, port: typed });
|
||||
};
|
||||
return (
|
||||
<div className="web-server echo-server">
|
||||
<div className="web-server-bar">
|
||||
<button type="button" onClick={press}>
|
||||
{t(view.active ? 'echoServer.stop' : 'echoServer.start')}
|
||||
</button>
|
||||
<label>
|
||||
{t('echoServer.port')}
|
||||
<input
|
||||
value={port}
|
||||
inputMode="numeric"
|
||||
readOnly={view.active}
|
||||
aria-invalid={typed === undefined ? 'true' : undefined}
|
||||
onChange={(event) => setPort(event.target.value)}
|
||||
onKeyDown={(event) => event.key === 'Enter' && press()}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<pre className="web-server-log" ref={element} aria-label={t('echoServer.log')} tabIndex={0}>
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SimpleClientPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) {
|
||||
const t = useTranslate();
|
||||
const topic = `app:${node}/simpleClient` as const;
|
||||
const { watch, act } = simulation;
|
||||
useEffect(() => watch([topic]), [watch, topic]);
|
||||
const view = simulation.views.get(topic) as SimpleClientView | null | undefined;
|
||||
const [address, setAddress] = useState(view?.address ?? '');
|
||||
const [port, setPort] = useState(view === null || view === undefined ? '' : String(view.port));
|
||||
const [message, setMessage] = useState('');
|
||||
useEffect(() => {
|
||||
if (view === null || view === undefined) return;
|
||||
setAddress(view.address);
|
||||
setPort(String(view.port));
|
||||
}, [view]);
|
||||
const { text, element } = useLines<SimpleClientLogEntry>(simulation, topic, (entry) => simpleClientLine(entry, t));
|
||||
if (view === null || view === undefined) return <div className="web-server" />;
|
||||
const typed = portOfText(port);
|
||||
const connect = () => {
|
||||
if (view.connected) act(node, 'simpleClient', 'disconnect');
|
||||
else if (typed !== undefined) act(node, 'simpleClient', 'connect', { address, port: typed });
|
||||
};
|
||||
const send = () => {
|
||||
act(node, 'simpleClient', 'send', { text: message });
|
||||
setMessage('');
|
||||
};
|
||||
return (
|
||||
<div className="web-server simple-client">
|
||||
<label className="simple-client-field">
|
||||
<span>{t('simpleClient.address')}</span>
|
||||
<input value={address} readOnly={view.connected} autoCapitalize="off" autoCorrect="off" spellCheck={false} onChange={(event) => setAddress(event.target.value)} />
|
||||
</label>
|
||||
<label className="simple-client-field">
|
||||
<span>{t('simpleClient.port')}</span>
|
||||
<input value={port} inputMode="numeric" readOnly={view.connected} aria-invalid={typed === undefined ? 'true' : undefined} onChange={(event) => setPort(event.target.value)} />
|
||||
</label>
|
||||
<div className="web-server-bar">
|
||||
<button type="button" onClick={connect}>
|
||||
{t(view.connected ? 'simpleClient.disconnect' : 'simpleClient.connect')}
|
||||
</button>
|
||||
</div>
|
||||
<label className="simple-client-message">
|
||||
<span>{t('simpleClient.message')}</span>
|
||||
<textarea value={message} onChange={(event) => setMessage(event.target.value)} />
|
||||
</label>
|
||||
<div className="web-server-bar">
|
||||
<button type="button" disabled={!view.connected} onClick={send}>
|
||||
{t('simpleClient.send')}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="web-server-log" ref={element} aria-label={t('simpleClient.output')} tabIndex={0}>
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1026,3 +1026,60 @@
|
||||
.image-viewer-picture img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* The echo server and the generic client (GUIApplicationServerBausteinWindow,
|
||||
* GUIApplicationClientBausteinWindow), on the web server's look. */
|
||||
.echo-server input,
|
||||
.simple-client input,
|
||||
.simple-client textarea {
|
||||
min-height: 32px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #a0a0a0;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
font: 16px Dialog, sans-serif;
|
||||
}
|
||||
|
||||
.echo-server input {
|
||||
width: 8em;
|
||||
}
|
||||
|
||||
.echo-server [aria-invalid='true'],
|
||||
.simple-client [aria-invalid='true'] {
|
||||
border-color: #ff0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.echo-server input:read-only,
|
||||
.simple-client input:read-only {
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
.simple-client-field,
|
||||
.simple-client-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.simple-client-field span {
|
||||
flex: none;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.simple-client-field input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.simple-client-message {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.simple-client-message textarea {
|
||||
min-height: 3.5em;
|
||||
padding: 4px 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// A server's lines as Java's server windows write them (ServerAnwendung,
|
||||
// ServerMitarbeiter, TCPSocket's failures), in the person's language: one
|
||||
// line each, as Java appends them to its window's text area.
|
||||
// ServerMitarbeiter, TCPSocket's failures), and the generic client's as its
|
||||
// window writes them (ClientBaustein, Socket, TCPSocket), in the person's
|
||||
// language: one line each (a failed connection two), as Java appends them to
|
||||
// its window's text area.
|
||||
|
||||
import type { ServerLogEntry } from '@mdg-filius/core';
|
||||
import type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import type { Translate } from '../i18n/i18n.js';
|
||||
|
||||
export function serverLine(entry: ServerLogEntry, t: Translate): string {
|
||||
@@ -23,3 +25,23 @@ export function serverLine(entry: ServerLogEntry, t: Translate): string {
|
||||
return t(`server.sendFailed.${entry.failure}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function simpleClientLine(entry: SimpleClientLogEntry, t: Translate): string {
|
||||
switch (entry.kind) {
|
||||
case 'connected':
|
||||
case 'disconnected':
|
||||
case 'notConnected':
|
||||
case 'receiveFailed':
|
||||
return t(`simpleClient.${entry.kind}`);
|
||||
case 'connectFailed':
|
||||
return `${t('simpleClient.notConnected')}\n${t(`simpleClient.failure.${entry.failure}`)}`;
|
||||
case 'connectionClosed':
|
||||
return t('simpleClient.connectionClosed', { address: `${entry.address}:${entry.port}` });
|
||||
case 'received':
|
||||
return `>>${entry.text}`;
|
||||
case 'sent':
|
||||
return `<<${entry.text}`;
|
||||
case 'sendFailed':
|
||||
return t(`server.sendFailed.${entry.failure}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
"dhcp.static": "Statische Adresszuweisung",
|
||||
"dhcp.subnetMask": "Netzmaske",
|
||||
"dhcp.upperBound": "Adress-Obergrenze",
|
||||
"echoServer.log": "Ausgabe",
|
||||
"echoServer.port": "Port:",
|
||||
"echoServer.start": "Starten",
|
||||
"echoServer.stop": "Anhalten",
|
||||
"emailClient.account": "Konto einrichten",
|
||||
"emailClient.accountHint": "E-Mail-Konto verwalten",
|
||||
"emailClient.accountInvalid": "Die Angaben sind entweder fehlerhaft oder unvollständig!",
|
||||
@@ -293,6 +297,21 @@
|
||||
"sheet.title": "Einstellungen",
|
||||
"sheet.wireless": "Drahtlos (WLAN)",
|
||||
"sim.failed": "Die Simulation ist auf einen Fehler gestoßen und wurde beendet.",
|
||||
"simpleClient.address": "Server-Adresse:",
|
||||
"simpleClient.connect": "Verbinden",
|
||||
"simpleClient.connected": "Verbindung hergestellt",
|
||||
"simpleClient.connectionClosed": "Socket zu {address} getrennt",
|
||||
"simpleClient.disconnect": "Trennen",
|
||||
"simpleClient.disconnected": "Verbindung getrennt",
|
||||
"simpleClient.failure.failed": "Aktives Öffnen des Sockets fehlgeschlagen",
|
||||
"simpleClient.failure.timedOut": "Zeitüberschreitung beim Verbindungsaufbau",
|
||||
"simpleClient.failure.unresolved": "Ziel konnte nicht bestimmt werden!",
|
||||
"simpleClient.message": "Nachricht:",
|
||||
"simpleClient.notConnected": "Fehler beim Verbindungsaufbau",
|
||||
"simpleClient.output": "Ausgabe",
|
||||
"simpleClient.port": "Server-Port:",
|
||||
"simpleClient.receiveFailed": "Keine Verbindung hergestellt!",
|
||||
"simpleClient.send": "Senden",
|
||||
"terminal.commands": "Befehle",
|
||||
"terminal.ctrlC": "Strg+C",
|
||||
"terminal.input": "Befehl eingeben",
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
"dhcp.static": "Static Address Assignment",
|
||||
"dhcp.subnetMask": "Netmask",
|
||||
"dhcp.upperBound": "Upper bound of address",
|
||||
"echoServer.log": "Output",
|
||||
"echoServer.port": "Port:",
|
||||
"echoServer.start": "Start",
|
||||
"echoServer.stop": "Stop",
|
||||
"emailClient.account": "Account",
|
||||
"emailClient.accountHint": "Manage email account",
|
||||
"emailClient.accountInvalid": "Wrong or incomplete data!",
|
||||
@@ -293,6 +297,21 @@
|
||||
"sheet.title": "Settings",
|
||||
"sheet.wireless": "Wireless (Wi-Fi)",
|
||||
"sim.failed": "The simulation ran into an error and was stopped.",
|
||||
"simpleClient.address": "Server address:",
|
||||
"simpleClient.connect": "Connect",
|
||||
"simpleClient.connected": "Connection established",
|
||||
"simpleClient.connectionClosed": "Socket to {address} disconnected",
|
||||
"simpleClient.disconnect": "Disconnect",
|
||||
"simpleClient.disconnected": "Disconnected",
|
||||
"simpleClient.failure.failed": "Active opening of socket failed.",
|
||||
"simpleClient.failure.timedOut": "Timeout while connecting",
|
||||
"simpleClient.failure.unresolved": "Target does not exist!",
|
||||
"simpleClient.message": "Message:",
|
||||
"simpleClient.notConnected": "Error while connecting",
|
||||
"simpleClient.output": "Output",
|
||||
"simpleClient.port": "Server port:",
|
||||
"simpleClient.receiveFailed": "No connection established!",
|
||||
"simpleClient.send": "Send",
|
||||
"terminal.commands": "Commands",
|
||||
"terminal.ctrlC": "Ctrl+C",
|
||||
"terminal.input": "Enter a command",
|
||||
|
||||
@@ -45,6 +45,22 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||
'explorer.notImported': 'sw_fileexplorer_msg1',
|
||||
'explorer.tooLarge': 'sw_fileexplorer_msg3',
|
||||
'viewer.open': 'imageviewer_msg2',
|
||||
'echoServer.port': 'serverbaustein_msg1',
|
||||
'echoServer.stop': 'serverbaustein_msg2',
|
||||
'echoServer.start': 'serverbaustein_msg3',
|
||||
'simpleClient.address': 'clientbaustein_msg1',
|
||||
'simpleClient.port': 'clientbaustein_msg2',
|
||||
'simpleClient.connect': 'clientbaustein_msg3',
|
||||
'simpleClient.message': 'clientbaustein_msg4',
|
||||
'simpleClient.send': 'clientbaustein_msg5',
|
||||
'simpleClient.disconnect': 'clientbaustein_msg6',
|
||||
'simpleClient.connected': 'sw_clientbaustein_msg2',
|
||||
'simpleClient.disconnected': 'sw_clientbaustein_msg3',
|
||||
'simpleClient.notConnected': 'sw_clientbaustein_msg4',
|
||||
'simpleClient.receiveFailed': 'sw_tcpsocket_msg9',
|
||||
'simpleClient.failure.unresolved': 'sw_socket_msg1',
|
||||
'simpleClient.failure.timedOut': 'sw_tcpsocket_msg2',
|
||||
'simpleClient.failure.failed': 'sw_tcpsocket_msg3',
|
||||
'textEditor.new': 'texteditor_msg3',
|
||||
'textEditor.open': 'texteditor_msg4',
|
||||
'textEditor.save': 'texteditor_msg5',
|
||||
@@ -269,6 +285,7 @@ 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'],
|
||||
'simpleClient.connectionClosed': ['sw_clientbaustein_msg5', '{address}', 'sw_clientbaustein_msg6'],
|
||||
'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'],
|
||||
'emailServer.created': ['emailserver_msg22', '{name}', 'emailserver_msg23'],
|
||||
'mail.stored': ['sw_smtpmitarbeiter_msg12', '{name}', 'sw_smtpmitarbeiter_msg13'],
|
||||
|
||||
@@ -6,8 +6,8 @@ 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 type { ServerLogEntry, SimpleClientLogEntry } from '@mdg-filius/core';
|
||||
import { serverLine, simpleClientLine } 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 }[] };
|
||||
@@ -39,3 +39,42 @@ describe("a server's lines", () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// The echo windows' lines against Java's (packages/sim/test/echoWindow.java.json, EchoWindowRun).
|
||||
const echo = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../sim/test/echoWindow.java.json'), 'utf8')) as {
|
||||
steps: { step: string; server: { log: string }; client: { output: string } }[];
|
||||
};
|
||||
|
||||
describe("the echo windows' lines", () => {
|
||||
it("are Java's, line by line, the server's and the client's", () => {
|
||||
const last = echo.steps.at(-1)!;
|
||||
const [, port] = /Verbindung zu 192\.168\.0\.20:(\d+) hergestellt/.exec(last.server.log)!;
|
||||
const server: ServerLogEntry[] = [
|
||||
{ kind: 'acceptingStarted' },
|
||||
{ kind: 'connectionAccepted', address: '192.168.0.20', port: Number(port) },
|
||||
{ kind: 'received', text: 'Zeile 1\nZeile 2' },
|
||||
{ kind: 'sent', text: 'Zeile 1\nZeile 2' },
|
||||
{ kind: 'acceptingStopped' },
|
||||
{ kind: 'connectionClosed', address: '192.168.0.20', port: Number(port) },
|
||||
{ kind: 'serverSocketClosed' },
|
||||
];
|
||||
for (const entry of server) expect(last.server.log, entry.kind).toContain(`${serverLine(entry, t)}\n`);
|
||||
const client: SimpleClientLogEntry[] = [
|
||||
{ kind: 'connected' },
|
||||
{ kind: 'sent', text: 'Hallo Echo' },
|
||||
{ kind: 'received', text: 'Hallo Echo' },
|
||||
{ kind: 'connectionClosed', address: '192.168.0.30', port: 55555 },
|
||||
{ kind: 'connectFailed', failure: 'timedOut' },
|
||||
{ kind: 'disconnected' },
|
||||
{ kind: 'connectFailed', failure: 'unresolved' },
|
||||
];
|
||||
for (const entry of client) expect(last.client.output, entry.kind).toContain(`${simpleClientLine(entry, t)}\n`);
|
||||
// Not in this story, in Java's words all the same (sw_clientbaustein_msg4, sw_tcpsocket_msg3, msg9, msg6).
|
||||
expect([
|
||||
simpleClientLine({ kind: 'notConnected' }, t),
|
||||
simpleClientLine({ kind: 'connectFailed', failure: 'failed' }, t),
|
||||
simpleClientLine({ kind: 'receiveFailed' }, t),
|
||||
simpleClientLine({ kind: 'sendFailed', failure: 'notConnected' }, t),
|
||||
]).toEqual(['Fehler beim Verbindungsaufbau', 'Fehler beim Verbindungsaufbau\nAktives Öffnen des Sockets fehlgeschlagen', 'Keine Verbindung hergestellt!', 'Verbindung nicht aufgebaut!']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user