Direct function generation

This commit is contained in:
Mark Tolmacs
2025-08-25 10:41:49 +02:00
parent 4c9ad1a22f
commit 9ad75b8375
4 changed files with 1393 additions and 3140 deletions

View File

@@ -4,55 +4,37 @@ declare global {
}
}
export type PlaybackEvent = (
| {
type: "mouse.move";
x: number;
y: number;
}
| {
type: "mouse.down" | "mouse.up";
button: "left" | "right" | "middle";
}
| {
type: "keyboard.down" | "keyboard.up";
key: string;
}
| {
type: "header";
width: number;
height: number;
localStorage: { [key: string]: string };
}
) & {
delay: number;
};
export class Record {
private static events: PlaybackEvent[] = [];
private static events: string = "";
private static timestamp: number = 0;
public static start() {
Record.events += ` await page.setViewportSize({ width: ${window.innerWidth}, height: ${window.innerHeight} });\n`;
Record.events += `await page.goto("http://localhost:3000");`;
Record.events += `await page.waitForLoadState("load");`;
// Record.events += " const mask = [\n";
// Record.events += ` page.getByRole("button", { name: "Share" }),\n`;
// Record.events += ` page.getByTitle("Library").locator("div"),\n`;
// Record.events += " ];\n";
// capture a snapshot of localStorage (if available) to include in the header
const lsSnapshot: { [key: string]: string } = {};
Record.events += " await page.evaluate(() => {\n";
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key != null) {
lsSnapshot[key] = localStorage.getItem(key) ?? "";
const value = JSON.stringify(localStorage.getItem(key));
Record.events += ` localStorage.getItem("${key}");\n`;
Record.events += ` localStorage.setItem("${key}", ${value});\n`;
}
}
} catch {}
Record.events += " });\n";
Record.events += " await page.reload();\n";
Record.events += ` await page.waitForLoadState("load");\n`;
Record.events = [
{
type: "header",
width: window.innerWidth,
height: window.innerHeight,
localStorage: lsSnapshot,
delay: 0,
},
];
// Set up the events
Record.timestamp = performance.now();
window.addEventListener("mousemove", this.onMouseMove);
@@ -60,17 +42,6 @@ export class Record {
window.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
try {
const canvases = Array.from(
document.querySelectorAll("canvas"),
) as HTMLCanvasElement[];
for (const c of canvases) {
c.addEventListener("mousemove", this.onMouseMove);
c.addEventListener("mousedown", this.onMouseDown);
c.addEventListener("mouseup", this.onMouseUp);
}
} catch {}
}
public static stop() {
@@ -79,18 +50,6 @@ export class Record {
window.removeEventListener("mouseup", this.onMouseUp);
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
// Remove listeners from any canvases we attached to
try {
const canvases = Array.from(
document.querySelectorAll("canvas"),
) as HTMLCanvasElement[];
for (const c of canvases) {
c.removeEventListener("mousemove", this.onMouseMove);
c.removeEventListener("mousedown", this.onMouseDown);
c.removeEventListener("mouseup", this.onMouseUp);
}
} catch {}
}
/// Displays a window as an absolutely positioned DIV with the generated
@@ -107,7 +66,7 @@ export class Record {
div.style.zIndex = "10000";
const pre = document.createElement("pre");
pre.textContent = JSON.stringify(this.events, null, 2);
pre.textContent = this.events;
// avoid overlap with the close button and limit height for large event dumps
pre.style.marginTop = "18px";
pre.style.maxHeight = "60vh";
@@ -183,12 +142,10 @@ export class Record {
const delay = now - Record.timestamp;
Record.timestamp = now;
Record.events.push({
type: "mouse.move",
x: event.clientX,
y: event.clientY,
delay,
});
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
Record.events += ` await page.mouse.move(${event.clientX}, ${event.clientY});\n`;
}
private static onMouseDown(event: MouseEvent) {
@@ -196,12 +153,12 @@ export class Record {
const delay = now - Record.timestamp;
Record.timestamp = now;
Record.events.push({
type: "mouse.down",
button:
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right",
delay,
});
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
const button =
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right";
Record.events += ` await page.mouse.down({ button: "${button}" });\n`;
}
private static onMouseUp(event: MouseEvent) {
@@ -209,12 +166,15 @@ export class Record {
const delay = now - Record.timestamp;
Record.timestamp = now;
Record.events.push({
type: "mouse.up",
button:
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right",
delay,
});
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
const button =
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right";
Record.events += ` await page.mouse.down({ button: "${button}" });\n`;
Record.events +=
" await expect(page).toHaveScreenshot({ maxDiffPixels: 100 });\n";
}
private static onKeyDown(event: KeyboardEvent) {
@@ -222,11 +182,10 @@ export class Record {
const delay = now - Record.timestamp;
Record.timestamp = now;
Record.events.push({
type: "keyboard.down",
key: event.key,
delay,
});
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
Record.events += ` await page.keyboard.down("${event.key}");\n`;
}
private static onKeyUp(event: KeyboardEvent) {
@@ -234,11 +193,13 @@ export class Record {
const delay = now - Record.timestamp;
Record.timestamp = now;
Record.events.push({
type: "keyboard.up",
key: event.key,
delay,
});
if (delay > 0) {
Record.events += ` await page.waitForTimeout(${delay});\n`;
}
Record.events += ` await page.keyboard.up("${event.key}");\n`;
Record.events +=
" await expect(page).toHaveScreenshot({ maxDiffPixels: 100 });\n";
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,77 +0,0 @@
import { expect, type Page } from "@playwright/test";
import type { PlaybackEvent } from "../../record";
export async function playbackEvents(page: Page, events: PlaybackEvent[]) {
const mask = [
page.getByRole("button", { name: "Share" }),
page.getByTitle("Library").locator("div"),
];
let width = 0;
let height = 0;
page.setDefaultTimeout(100000);
for (const event of events) {
// Handle header event specially: set viewport and localStorage before playback
if (event.type === "header") {
// set viewport to recorded size
await page.setViewportSize({ width: event.width, height: event.height });
width = event.width;
height = event.height;
// apply localStorage snapshot in page context
if (event.localStorage) {
await page.evaluate((ls: Record<string, string>) => {
try {
for (const k in ls) {
if (Object.prototype.hasOwnProperty.call(ls, k)) {
localStorage.setItem(k, ls[k]);
}
}
} catch {}
}, event.localStorage as Record<string, string>);
}
await page.reload();
await page.waitForLoadState("load");
// header has no further action
continue;
}
// wait the recorded delay before dispatching the event
const ms = Math.max(0, Math.round((event as any).delay ?? 0));
if (ms > 0) {
await page.waitForTimeout(ms);
}
switch (event.type) {
case "mouse.move":
// Simulate mouse movement
if (event.x < 0 || event.x > width || event.y < 0 || event.y > height) {
break;
}
await page.mouse.move(event.x, event.y);
break;
case "mouse.down":
// Simulate mouse button down
await page.mouse.down({ button: event.button });
break;
case "mouse.up":
// Simulate mouse button up
await page.mouse.up({ button: event.button });
await expect(page).toHaveScreenshot({ maxDiffPixels: 100, mask });
break;
case "keyboard.down":
// Simulate key down
await page.keyboard.down(event.key);
break;
case "keyboard.up":
// Simulate key up
await page.keyboard.up(event.key);
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask,
});
break;
}
}
}