F hands out the editor's page and bundle (editor/ beside main.mjs, or FILIUS_EDITOR_DIR): GET and HEAD only, one wall against leaving the folder, F's own routes first, assets/ cached a year, the rest asked for again; the build lays .br and .gz beside the files and F sends the better one the browser takes (bundle 695 KB to 212 KB). deploy/Dockerfile: two stages, Node 24, no node_modules, not root. deploy/docker-compose.yml with an env file per environment: the file sets the stack's name, volume, image tag and port (test 8081, production 8080). Measured with both stacks side by side behind nginx and Chromium. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.9 KiB
TypeScript
78 lines
3.9 KiB
TypeScript
// What the editor's browser tests share: the built page served once per file,
|
|
// and reading the surface (its transform, a device's place on the screen, the
|
|
// document's drawing through the development page's filiusDev).
|
|
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import type { Page } from '@playwright/test';
|
|
import { build, preview, type PreviewServer } from 'vite';
|
|
import type { DesktopView, DeviceSheet, TopologyView } from '@mdg-filius/doc';
|
|
|
|
const config = join(dirname(fileURLToPath(import.meta.url)), '../vite.config.ts');
|
|
|
|
/** The editor built into `outDir`, as the image's build builds it. */
|
|
export async function buildEditor(outDir: string): Promise<void> {
|
|
await build({ configFile: config, logLevel: 'silent', build: { outDir, emptyOutDir: true } });
|
|
}
|
|
|
|
export async function serveEditor(): Promise<{ readonly base: string; readonly close: () => Promise<void> }> {
|
|
await build({ configFile: config, logLevel: 'silent' });
|
|
const server: PreviewServer = await preview({ configFile: config, preview: { port: 0 }, logLevel: 'silent' });
|
|
return {
|
|
base: server.resolvedUrls!.local[0]!,
|
|
close: () => new Promise<void>((resolve) => server.httpServer.close(() => resolve())),
|
|
};
|
|
}
|
|
|
|
export async function openEditor(page: Page, base: string, example: string, query = ''): Promise<void> {
|
|
await page.goto(`${base}?example=${example}${query}`);
|
|
await page.waitForSelector('svg.filius-canvas');
|
|
await page.waitForFunction(() => (window as unknown as { filiusDev?: unknown }).filiusDev !== undefined);
|
|
}
|
|
|
|
/** The document's drawing. */
|
|
export function drawing(page: Page): Promise<TopologyView> {
|
|
return page.evaluate(() => (window as unknown as { filiusDev: { view: () => TopologyView } }).filiusDev.view());
|
|
}
|
|
|
|
/** What the device sheet reads of a device. */
|
|
export function sheetOf(page: Page, id: string): Promise<DeviceSheet | undefined> {
|
|
return page.evaluate((each) => (window as unknown as { filiusDev: { sheet: (id: string) => DeviceSheet | undefined } }).filiusDev.sheet(each), id);
|
|
}
|
|
|
|
/** What a computer's desktop reads of the document. */
|
|
export function desktopOf(page: Page, id: string): Promise<DesktopView | undefined> {
|
|
return page.evaluate((each) => (window as unknown as { filiusDev: { desktop: (id: string) => DesktopView | undefined } }).filiusDev.desktop(each), id);
|
|
}
|
|
|
|
/** A device's button as data-box says it (surface pixels). */
|
|
export async function boxOf(page: Page, id: string): Promise<{ x: number; y: number; width: number; height: number }> {
|
|
const [x, y, width, height] = (await page.locator(`g.device[data-node="${id}"]`).getAttribute('data-box'))!.split(' ').map(Number) as [number, number, number, number];
|
|
return { x, y, width, height };
|
|
}
|
|
|
|
/** Where a device's icon's middle is on the screen. */
|
|
export async function iconCentre(page: Page, id: string): Promise<{ x: number; y: number }> {
|
|
const box = (await page.locator(`g.device[data-node="${id}"] image`).boundingBox())!;
|
|
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
}
|
|
|
|
/** A point of a device's button on the screen, by its share of the button's width and height. */
|
|
export async function onDevice(page: Page, id: string, across = 0.5, down = 0.4): Promise<{ x: number; y: number }> {
|
|
const box = (await page.locator(`g.device[data-node="${id}"] > rect`).first().boundingBox())!;
|
|
return { x: box.x + box.width * across, y: box.y + box.height * down };
|
|
}
|
|
|
|
/** The surface's scale. */
|
|
export async function scaleOf(page: Page): Promise<number> {
|
|
return Number(await page.locator('[data-zoom]').getAttribute('data-zoom'));
|
|
}
|
|
|
|
/** Drags with the mouse from one point to another in steps. */
|
|
export async function drag(page: Page, from: { x: number; y: number }, to: { x: number; y: number }): Promise<void> {
|
|
await page.mouse.move(from.x, from.y);
|
|
await page.mouse.down();
|
|
await page.mouse.move(to.x, to.y, { steps: 8 });
|
|
await page.mouse.up();
|
|
}
|