Skip to content

Commit 00ef69d

Browse files
committed
test(studio): add full-frame and smaller object-fit images to the edit accuracy grid
1 parent 78e8492 commit 00ef69d

3 files changed

Lines changed: 82 additions & 9 deletions

File tree

‎packages/studio/tests/e2e/edit-accuracy/case.mjs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const RESIZE_BY = 60;
2727
const ROTATE_BY = (25 * Math.PI) / 180;
2828
const CROP_BY = 40;
2929
const NUDGES = 5;
30+
// Where a hand drag's stray hover move landed, from the press, in composition px (the saved translate it caused).
31+
const STRAY_BY = [-611, 213.6];
3032
const ZOOM_SENSITIVITY = 0.007; // previewZoom.ts: one wheel unit scales zoom by exp(0.007)
3133

3234
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -570,6 +572,26 @@ export async function controlDrag(browser, gesture) {
570572
}
571573
}
572574

575+
// A hand drag gets no-button hover moves from Chromium mid-drag after a layout change; a gesture must ignore them.
576+
// Sent as the PointerEvent the pressed element receives: a CDP move with no buttons ended the drag unsaved.
577+
async function strayMove(ctx, press, [x, y]) {
578+
await ctx.page.evaluate(
579+
([px, py], init) =>
580+
document.elementFromPoint(px, py)?.dispatchEvent(new PointerEvent("pointermove", init)),
581+
press,
582+
{
583+
clientX: x,
584+
clientY: y,
585+
buttons: 0,
586+
pointerId: 1,
587+
pointerType: "mouse",
588+
isPrimary: true,
589+
bubbles: true,
590+
},
591+
);
592+
await nextFrame(ctx.page, 2);
593+
}
594+
573595
/** `route`, given the press point, replaces the gesture's straight path; a `{ pause }` entry holds still. */
574596
// fallow-ignore-next-line complexity
575597
export async function pointerGesture(ctx, gesture, pre, route) {
@@ -603,6 +625,8 @@ export async function pointerGesture(ctx, gesture, pre, route) {
603625
}
604626
const rec = await recording(ctx.page, false);
605627
const smooth = smoothness(rec);
628+
if (ctx.stray)
629+
await strayMove(ctx, press, pre.map.toScreen([0, 1].map((i) => pressComp[i] + STRAY_BY[i])));
606630
await ctx.page.mouse.up();
607631
const lastQuad = gesture === "crop" ? last.m.outline : last.m.visible;
608632
return {
@@ -645,7 +669,7 @@ async function nudgeGesture(ctx, pre) {
645669
export async function inStudio({ browser, spec, dir, files, url, evidence }, drive) {
646670
const context = await browser.createBrowserContext();
647671
const page = await context.newPage();
648-
const ctx = { page, dir, files, handles: null };
672+
const ctx = { page, dir, files, handles: null, stray: spec.stray };
649673
const consoleErrors = [];
650674
page.on("pageerror", (e) => consoleErrors.push(e.message));
651675
evidence.shots = {};

‎packages/studio/tests/e2e/edit-accuracy/grid.mjs‎

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,30 @@ const product = (axes) =>
3131
const caseId = (c) =>
3232
[c.gesture, c.gsap, c.placement, `r${c.rotation}`, c.nesting, `z${c.zoom}`].join("-");
3333

34+
// An <img> as Studio places a dropped picture, full-frame or smaller, at gsap none (one tween for contrast).
35+
// Each pointer gesture also gets the stray no-button move a hand drag meets (case.mjs strayMove).
36+
// The id's placement token is letters only, so `-img(contain|cover)` selects them.
37+
function imageCases() {
38+
const fitted = product({
39+
fit: ["contain", "cover"],
40+
size: ["full", "small"],
41+
nesting: AXES.nesting,
42+
gesture: GESTURES,
43+
}).map((c) => ({ ...c, gsap: "none" }));
44+
return fitted
45+
.concat({ fit: "contain", size: "full", nesting: "root", gesture: "move", gsap: "tween" })
46+
.map((c) => ({ ...c, placement: `img${c.fit}${c.size}`, rotation: 0, zoom: 100, stray: true }))
47+
.map((c) => ({ id: caseId(c), ...c }));
48+
}
49+
3450
/** `pr` is a smaller slice for CI; its final size is still an open decision. */
3551
export function buildGrid(kind = "full") {
3652
return (
3753
product({ ...AXES, gesture: GESTURES })
3854
// xPercent only exists through GSAP on the target itself.
3955
.filter((c) => c.placement !== "xpercent" || !["none", "idle"].includes(c.gsap))
4056
.map((c) => ({ id: caseId(c), ...c, other: c.gsap === "idle" }))
41-
.concat(dragCases())
57+
.concat(dragCases(), imageCases())
4258
.filter((c) => kind !== "pr" || (c.zoom === 100 && c.nesting === "root"))
4359
);
4460
}
@@ -59,6 +75,32 @@ const OTHER_PLACEMENT_CSS = {
5975
center: "left: 75%; top: 75%; translate: -50% -50%;",
6076
};
6177

78+
// A flat 4:3 picture in the target colour: contain letterboxes it in a 16:9 box and cover crops it.
79+
const IMAGE_SRC =
80+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAAEElEQVR42mP4cEABjhhwcgCyFhXBc/l8wQAAAABJRU5ErkJggg==";
81+
82+
/** The target's composition frame in composition px: nothing outside it reaches the producer frame. */
83+
export function frameOf(spec) {
84+
const h = spec.nesting === "nested" ? NESTED_HOST : { left: 0, top: 0, ...COMPOSITION };
85+
return { left: h.left, top: h.top, right: h.left + h.width, bottom: h.top + h.height };
86+
}
87+
88+
// fallow-ignore-next-line complexity
89+
function targetBox(spec) {
90+
if (!spec.fit)
91+
return `${PLACEMENT_CSS[spec.placement]} width: ${TARGET.width}px; height: ${TARGET.height}px;`;
92+
const f = frameOf(spec);
93+
const [w, h] = spec.size === "full" ? [f.right - f.left, f.bottom - f.top] : [640, 360];
94+
const at = spec.size === "full" ? [0, 0] : [640, 360];
95+
// The box colour fills contain's letterbox, so the producer's pixel box is the element box the bench measures.
96+
return `left: ${at[0]}px; top: ${at[1]}px; width: ${w}px; height: ${h}px; object-fit: ${spec.fit};`;
97+
}
98+
99+
const targetEl = (spec, attrs) =>
100+
spec.fit
101+
? `<img id="target"${attrs} src="${IMAGE_SRC}" alt="" />`
102+
: `<div id="target"${attrs}>${targetText(spec)}</div>`;
103+
62104
const TEXT = "Edit accuracy bench";
63105
const targetText = (spec) => (spec.text ? TEXT : "");
64106

@@ -69,7 +111,7 @@ function targetCss(spec) {
69111
: "";
70112
// White text reads as full coverage in the render's luminance box, so it leaves that metric alone.
71113
const text = spec.text ? " color: #ffffff; font: 28px/1.2 sans-serif;" : "";
72-
return `#target { position: absolute; ${PLACEMENT_CSS[spec.placement]} width: ${TARGET.width}px; height: ${TARGET.height}px; background: ${TARGET.color};${rotate}${text} }${other}`;
114+
return `#target { position: absolute; ${targetBox(spec)} background: ${TARGET.color};${rotate}${text} }${other}`;
73115
}
74116

75117
// fallow-ignore-next-line complexity
@@ -99,7 +141,7 @@ function rootHtml(spec) {
99141
const nested = spec.nesting === "nested";
100142
const body = nested
101143
? `<div id="scene-sub" data-composition-id="sub" data-composition-src="compositions/sub.html" data-start="0" data-duration="4" data-track-index="1" style="position: absolute; left: ${NESTED_HOST.left}px; top: ${NESTED_HOST.top}px; width: ${NESTED_HOST.width}px; height: ${NESTED_HOST.height}px; overflow: hidden"></div>`
102-
: `<div id="target" class="clip" data-start="0" data-duration="4" data-track-index="1">${targetText(spec)}</div>${spec.other ? `\n <div id="other" class="clip" data-start="0" data-duration="4" data-track-index="2"></div>` : ""}`;
144+
: `${targetEl(spec, ' class="clip" data-start="0" data-duration="4" data-track-index="1"')}${spec.other ? `\n <div id="other" class="clip" data-start="0" data-duration="4" data-track-index="2"></div>` : ""}`;
103145
const script = spec.gsap === "none" ? "" : timelineScript("main", nested ? [] : gsapLines(spec));
104146
return `<!doctype html>
105147
<html lang="en">
@@ -125,7 +167,7 @@ function subHtml(spec) {
125167
const script = spec.gsap === "none" ? "" : timelineScript("sub", gsapLines(spec));
126168
return `<template id="sub-template">
127169
<div id="sub" data-composition-id="sub" data-width="${NESTED_HOST.width}" data-height="${NESTED_HOST.height}">
128-
<div id="target">${targetText(spec)}</div>${spec.other ? `\n <div id="other"></div>` : ""}
170+
${targetEl(spec, "")}${spec.other ? `\n <div id="other"></div>` : ""}
129171
<style>
130172
#sub { position: relative; width: ${NESTED_HOST.width}px; height: ${NESTED_HOST.height}px; background: ${BACKGROUND}; overflow: hidden; }
131173
${targetCss(spec)}

‎packages/studio/tests/e2e/edit-accuracy/run.mjs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { parseArgs } from "node:util";
1111
import { execSync, spawn } from "node:child_process";
1212
import puppeteer from "puppeteer-core";
1313
import { resolveHeadlessShellPath } from "../../../../engine/src/index.ts";
14-
import { buildGrid, writeFixture } from "./grid.mjs";
14+
import { buildGrid, frameOf, writeFixture } from "./grid.mjs";
1515
import { killServers, runCase, startServer, stopServer } from "./case.mjs";
1616
import { runSequence } from "./sequences.mjs";
1717
import { METRICS, score, writeReport } from "./report.mjs";
@@ -70,9 +70,16 @@ const errorResult = (error, log) => ({
7070

7171
const liveRoots = new Set();
7272

73+
const clipTo = (b, f) => ({
74+
left: Math.max(b.left, f.left),
75+
top: Math.max(b.top, f.top),
76+
right: Math.min(b.right, f.right),
77+
bottom: Math.min(b.bottom, f.bottom),
78+
});
79+
7380
/** Render drift: the reloaded preview's visible box against the target's pixel box in a producer frame. */
74-
async function withRender(dir, decoder, { reloaded, ...measured }, evidence) {
75-
const expected = aabb(reloaded.visible);
81+
async function withRender(spec, dir, decoder, { reloaded, ...measured }, evidence) {
82+
const expected = clipTo(aabb(reloaded.visible), frameOf(spec));
7683
const render = await renderBox(dir, decoder).catch((error) => ({ error }));
7784
// A producer failure fails render alone; the case's other metrics still count.
7885
if (render.error)
@@ -117,7 +124,7 @@ async function runOne(spec, browser, decoder, port) {
117124
});
118125
await stopServer(server);
119126
server = null;
120-
result = await withRender(dir, decoder, measured, evidence);
127+
result = await withRender(spec, dir, decoder, measured, evidence);
121128
} catch (error) {
122129
result = errorResult(error, log);
123130
} finally {

0 commit comments

Comments
 (0)