ReportRun reads Java's report back with iText: every piece of text with its place, font, size and colour. The editor lays the report out by iText's rules (A4, leading 1.5, loose chunks in the current line, tables 80 % wide with padding 2, fixed traffic columns, page breaks) and draws it with pdf-lib in the PDF's own Helvetica, widths without kerning as iText measures. All device pages of Internet_Komplett, the first page and the data exchange of einfaches_rechnernetz_komplett match Java piece by piece; Java's iText reads our PDF with the same text in the same places. The sidebar gains Java's "Report"; the frames come from the room by a short subscription. Not adopted: true/false for DHCP (Java's own Ja/Nein), hash table orders, an image running off the page, the report writing back into the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
4.4 KiB
TypeScript
80 lines
4.4 KiB
TypeScript
// FILIUS' „Report“ in Chromium (F4.8d), on FILIUS' example
|
|
// „einfaches_rechnernetz_komplett.fls“, run for a moment (its notebooks ask for
|
|
// addresses by DHCP at once), then in the documentation mode the report
|
|
// saved under the project's name — a PDF with Java's title, the overview's
|
|
// image, every computer's configuration and the data exchange of each card
|
|
// that saw frames, with the rows' words as the data exchange window has
|
|
// them.
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { expect, test } from '@playwright/test';
|
|
import { decodePDFRawStream, PDFArray, PDFDocument, PDFName, PDFRawStream } from 'pdf-lib';
|
|
import { openEditor, 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();
|
|
});
|
|
|
|
/** Each page's words, as pdf-lib writes them (hex strings shown with Tj, in Windows-1252). */
|
|
async function pageTexts(bytes: Uint8Array): Promise<string[][]> {
|
|
const pdf = await PDFDocument.load(bytes);
|
|
return pdf.getPages().map((page) => {
|
|
const contents = page.node.get(PDFName.of('Contents'));
|
|
const streams = contents instanceof PDFArray ? contents.asArray() : [contents];
|
|
const words: string[] = [];
|
|
for (const reference of streams) {
|
|
const stream = pdf.context.lookup(reference);
|
|
if (!(stream instanceof PDFRawStream)) continue;
|
|
const operators = new TextDecoder('latin1').decode(decodePDFRawStream(stream).decode());
|
|
for (const [, hex] of operators.matchAll(/<([0-9A-Fa-f]*)>\s*Tj/g))
|
|
words.push(new TextDecoder('windows-1252').decode(Uint8Array.from(hex!.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16))));
|
|
}
|
|
return words;
|
|
});
|
|
}
|
|
|
|
test("saves Java's report of the project, with the data exchange so far", async ({ page }) => {
|
|
test.setTimeout(90_000);
|
|
await openEditor(page, server.base, 'einfaches_rechnernetz_komplett.fls');
|
|
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).click();
|
|
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
|
// A cable lights up: frames went over it.
|
|
await expect(page.locator('path[data-active="true"]').first()).toBeAttached({ timeout: 20_000 });
|
|
await page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' }).click();
|
|
const button = page.getByRole('navigation', { name: 'Dokumentation' }).getByRole('button', { name: 'Report' });
|
|
await expect(button).toHaveAttribute('title', 'Report als PDF-Dokument erstellen');
|
|
const saved = page.waitForEvent('download');
|
|
await button.click();
|
|
const download = await saved;
|
|
expect(download.suggestedFilename()).toBe('einfaches_rechnernetz_komplett.pdf');
|
|
await expect(button).toBeEnabled();
|
|
|
|
const bytes = new Uint8Array(readFileSync((await download.path())!));
|
|
const pdf = await PDFDocument.load(bytes);
|
|
expect(pdf.getTitle()).toBe('Filius-Dokumentation: einfaches_rechnernetz_komplett');
|
|
expect(pdf.getPage(0).getSize()).toEqual({ width: 595, height: 842 });
|
|
const pages = await pageTexts(bytes);
|
|
// The first page: the title, the project's name, the overview with the image.
|
|
expect(pages[0]).toEqual(['Filius-Dokumentation: einfaches_rechnernetz_komplett', 'einfaches_rechnernetz_komplett.fls', '1. ', 'Übersicht']);
|
|
expect(new TextDecoder('latin1').decode(bytes)).toContain('/Subtype /Image');
|
|
// Then the devices: every computer and notebook, none of the switches.
|
|
const words = pages.flat();
|
|
expect(words.filter((word) => /^2\.\d+\. $/.test(word)).length).toBe(9);
|
|
expect(words).toContain('Server');
|
|
expect(words).not.toContain('Switch 1');
|
|
expect(words).toEqual(expect.arrayContaining(['Installiert: ', 'Webserver, Text-Editor, Datei-Explorer, Bildbetrachter, DNS-Server, Befehlszeile', 'www.mein-server.de.', 'Nein']));
|
|
// Then the data exchange: each card that saw frames, under its device's name and address.
|
|
const traffic = pages.findIndex((texts) => texts.includes('Datenaustausch'));
|
|
expect(traffic).toBeGreaterThan(1);
|
|
const rest = pages.slice(traffic).flat();
|
|
expect(rest).toEqual(expect.arrayContaining(['3.1. ', 'Nr.', 'Bemerkungen / Details', 'DHCP', 'Anwendung']));
|
|
expect(rest.some((word) => /^Notebook \d - /.test(word) || /^Server - 192\.168\.0\.100$/.test(word))).toBe(true);
|
|
expect(rest.some((word) => word.startsWith('DHCPDISCOVER'))).toBe(true);
|
|
});
|