Moving a device, moving or resizing a documentation item went into the shared document at every pointer move: 59 updates a second, each leaving about 18 bytes of history (M4 on test-filius). Now the editor keeps the drag while it lasts, shows it itself and tells the others through the awareness (moving, at most every 50 ms, the end at once), and writes it once when it is let go. A drag the browser cancels or a second finger turns into a pinch is written where it is; WebKit sends a second pointerdown for the first finger when another comes, which had lost the drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.1 KiB
TypeScript
83 lines
4.1 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());
|
|
}
|
|
|
|
/** How many changes the document has had since the page opened it. */
|
|
export function documentUpdates(page: Page): Promise<number> {
|
|
return page.evaluate(() => (window as unknown as { filiusDev: { updates: () => number } }).filiusDev.updates());
|
|
}
|
|
|
|
/** 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();
|
|
}
|