Files
Recordly/tests/ui/cursor-height.spec.ts
webadderall f007984ebc Fix editor previews, zoom controls and recording workflow
Preserve renderer state through mute and export transitions, gate hover previews on a composed frame, and align timeline bounds with authored zoom intervals. Improve settings, cursor and webcam controls, inline recording countdown, project recovery and safe library cleanup.
2026-11-06 18:05:14 +11:00

173 lines
5.6 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installDesktopBridge } from "./bridge";
// Use actual loaded/rasterized artwork: checking the SVG canvas dimensions misses
// transparent margins and the shadow padding that caused the size mismatch.
test("cursor bodies match across styles and preview/export renderers", async ({ page }) => {
await page.route("**/cursor-height-audit.html", (route) =>
route.fulfill({
contentType: "text/html",
body: "<!doctype html><body></body>",
}),
);
await page.goto("/cursor-height-audit.html");
const measurements = await page.evaluate(async () => {
const renderer = await import(
"/src/components/video-editor/videoPlayback/cursorRenderer.ts"
);
const sizing = await import(
"/src/components/video-editor/videoPlayback/uploadedCursorAssets.ts"
);
const { Application } = await import("/tests/ui/pixiAudit.ts");
function bodyHeight(canvas: HTMLCanvasElement) {
const pixels = canvas
.getContext("2d")!
.getImageData(0, 0, canvas.width, canvas.height).data;
let top = canvas.height;
let bottom = -1;
for (let y = 0; y < canvas.height; y++)
for (let x = 0; x < canvas.width; x++) {
if (pixels[(y * canvas.width + x) * 4 + 3] >= 128) {
top = Math.min(top, y);
bottom = Math.max(bottom, y);
}
}
return bottom - top + 1;
}
const app = new Application();
await app.init({ width: 400, height: 400, backgroundAlpha: 0, resolution: 1 });
const results: {
style: string;
target: number;
pixi: number;
canvas: number;
atlas: number;
}[] = [];
for (const style of [
"macos",
"tahoe",
"windows11",
"dot",
"figma",
"bold-outline",
"pixel-hand",
"bee",
"breezex",
"tahoe-inverted",
"catppuccin-pink",
]) {
const atlas = await renderer.buildNativeCursorAtlas(style);
const image = new Image();
image.src = atlas!.dataUrl;
await image.decode();
const entry = atlas!.entries[0];
const tile = document.createElement("canvas");
tile.width = entry.width;
tile.height = entry.height;
tile.getContext("2d")!.drawImage(
image,
entry.x,
entry.y,
entry.width,
entry.height,
0,
0,
tile.width,
tile.height,
);
for (const [width, size] of [
[1920, 28],
[1920, 84],
[960, 84],
]) {
const target = (size * width) / 1920;
const config = {
...renderer.DEFAULT_CURSOR_CONFIG,
style,
dotRadius: size,
clickBounce: 0,
clickEffect: "none" as const,
};
const viewport = { x: 0, y: 0, width, height: 1080 };
const samples = [{ timeMs: 0, cx: 0.5, cy: 0.5, cursorType: "arrow" as const }];
const overlay = new renderer.PixiCursorOverlay(config);
app.stage.addChild(overlay.container);
overlay.update(samples, 0, viewport, true, true);
overlay.container.position.set(200 - width / 2, 200 - 540);
app.render();
const pixi = app.renderer.extract.canvas(app.stage) as HTMLCanvasElement;
const canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 400;
const ctx = canvas.getContext("2d")!;
ctx.translate(200 - width / 2, 200 - 540);
renderer.drawCursorOnCanvas(
ctx,
samples,
0,
viewport,
new renderer.SmoothedCursorState(config),
config,
);
results.push({
style,
target,
pixi: bodyHeight(pixi),
canvas: bodyHeight(canvas),
atlas:
(bodyHeight(tile) / entry.height) *
target *
sizing.getCursorStyleSizeMultiplier(style),
});
overlay.destroy();
}
}
app.destroy(true);
return results;
});
for (const row of measurements) {
expect(Math.abs(row.pixi - row.target), JSON.stringify(row)).toBeLessThanOrEqual(2);
expect(Math.abs(row.canvas - row.target), JSON.stringify(row)).toBeLessThanOrEqual(2);
expect(Math.abs(row.atlas - row.target), JSON.stringify(row)).toBeLessThanOrEqual(1);
expect(Math.abs(row.pixi - row.canvas), JSON.stringify(row)).toBeLessThanOrEqual(1);
}
});
test("picker thumbnails have the same visible height using renderer assets", async ({ page }) => {
await installDesktopBridge(page, "filmstrip.mp4");
await page.goto("/?windowType=editor");
await page.getByRole("radio", { name: "Cursor", exact: true }).click();
const picker = page.getByRole("grid", { name: "Cursor Style", exact: true });
await expect(picker.locator("img:visible")).toHaveCount(8);
await expect(picker.getByRole("row", { name: "Bibata Bee", exact: true })).toHaveCount(0);
const boldOutline = picker.getByRole("row", { name: "Bold Outline", exact: true });
await boldOutline.click();
await expect(boldOutline).toHaveAttribute("aria-selected", "true");
await page.mouse.move(1200, 100);
const heights = await picker.locator("img:visible").evaluateAll(async (images) =>
Promise.all(
images.map(async (element) => {
const image = element as HTMLImageElement;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(image, 0, 0);
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let top = canvas.height;
let bottom = -1;
for (let y = 0; y < canvas.height; y++)
for (let x = 0; x < canvas.width; x++) {
if (data[(y * canvas.width + x) * 4 + 3] >= 128) {
top = Math.min(top, y);
bottom = Math.max(bottom, y);
}
}
return ((bottom - top + 1) / canvas.height) * image.getBoundingClientRect().height;
}),
),
);
for (const height of heights) expect(height).toBeCloseTo(28, 0);
});