mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
feat(cli): tell authors in check why motion blur is off (#5992)
Blur that turns itself off is silent in the output, so an author who asked for data-motion-blur="on" cannot tell. Have hyperframes check read the runtime's decision through probe() and the reason codes from the previous change. check reports motion_blur_off as a warning with a fix hint for a root that asked for blur and got none (an iframe in the root gets its own hint), motion_blur_no_movers when nothing moves enough to blur, and motion_blur_unverified when the check browser has no html-in-canvas to decide with. The console echo of the same message is dropped so a finding is not shown twice. check asks the runtime which root wants blur instead of reading attributes itself, so data-root="true" is honoured, and after probing it suspends blur painting for the rest of the session: its audits read plain DOM, which removes the roughly 10x slowdown of auditing a wrapped composition. It never sets the page's off switch, which would change the probe's answer. Tests drive a finding from the page to the check output, with its fix and without its echo. Folded review fixes: R9 (check hint), R12, R13 (check half), T6. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
38f6e01c2b
commit
4371981b74
@@ -1380,3 +1380,156 @@ describe("component output membership", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("motion blur findings in the check output (T6)", () => {
|
||||
type Handler = (message: ReturnType<typeof fakeConsoleMessage>) => void;
|
||||
const ACCELERATED = {
|
||||
state: "off",
|
||||
reason: "a WebGL canvas cannot be captured",
|
||||
reasonCode: "accelerated-canvas",
|
||||
movers: 0,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(window, "__hfMotionBlur");
|
||||
});
|
||||
|
||||
async function checkWith(
|
||||
report: object,
|
||||
options: { cores?: number; echo?: [string, string][]; mount?: () => void } = {},
|
||||
) {
|
||||
vi.mocked(os.availableParallelism).mockReturnValue(options.cores ?? 1);
|
||||
mountCanvasFixture(`<p id="copy" data-layout-name="copy">Hi</p>`);
|
||||
options.mount?.();
|
||||
const handlers: Handler[] = [];
|
||||
const log = (type: string, text: string) =>
|
||||
handlers.forEach((handler) => handler(fakeConsoleMessage(type, text)));
|
||||
Reflect.set(window, "__hfMotionBlur", {
|
||||
probe: () => {
|
||||
for (const [type, text] of options.echo ?? []) log(type, text);
|
||||
return report;
|
||||
},
|
||||
suspend: vi.fn(),
|
||||
});
|
||||
const browser = Object.assign(Object.create(null), {
|
||||
close: vi.fn(async () => undefined),
|
||||
process: () => ({ exitCode: 0, signalCode: null }),
|
||||
});
|
||||
let opened = 0;
|
||||
vi.mocked(openSettledCompositionPage).mockImplementation(
|
||||
async (_html: string, _url: string, settle: OpenSettledCompositionPageOptions) => {
|
||||
const page = fakePage();
|
||||
const first = ++opened === 1;
|
||||
page.on = vi.fn((event: string, handler: Handler) => {
|
||||
if (event === "console" && first) handlers.push(handler);
|
||||
});
|
||||
settle.onBrowser?.(browser);
|
||||
await settle.beforeNavigate?.(page);
|
||||
return { page, browser, renderReadyTimedOut: false };
|
||||
},
|
||||
);
|
||||
const result = await runBrowserCheck(
|
||||
PROJECT,
|
||||
{ ...DEFAULT_CHECK_OPTIONS, contrast: false },
|
||||
{ kind: "none" },
|
||||
runAuditGrid,
|
||||
);
|
||||
return { findings: result.runtimeFindings, pages: opened };
|
||||
}
|
||||
const blurCodes = (findings: { code: string }[]) =>
|
||||
findings.map((finding) => finding.code).filter((code) => code.startsWith("motion_blur"));
|
||||
|
||||
it("carries a blur finding, its fix and its anchor into the output", async () => {
|
||||
const { findings } = await checkWith(ACCELERATED);
|
||||
const [finding, ...rest] = findings.filter(({ code }) => code === "motion_blur_off");
|
||||
expect(rest).toEqual([]);
|
||||
expect(finding).toMatchObject({
|
||||
severity: "warning",
|
||||
message: expect.stringContaining("a WebGL canvas cannot be captured"),
|
||||
selector: expect.any(String),
|
||||
fixHint: expect.stringMatching(/WebGL\/WebGPU.*sub-composition/),
|
||||
});
|
||||
});
|
||||
|
||||
it("anchors the finding on the root the runtime picks, not the first composition (W4)", async () => {
|
||||
const mount = (): void => {
|
||||
document.body.innerHTML = `
|
||||
<div id="aux" data-composition-id="aux" data-duration="4"></div>
|
||||
<div id="real" data-composition-id="main" data-root="true" data-duration="10"></div>`;
|
||||
const rect = { x: 3, y: 4, width: 640, height: 360 } as DOMRect;
|
||||
const real = document.getElementById("real");
|
||||
if (real) real.getBoundingClientRect = () => rect;
|
||||
};
|
||||
const { findings } = await checkWith(ACCELERATED, { mount });
|
||||
expect(findings.find(({ code }) => code === "motion_blur_off")).toMatchObject({
|
||||
selector: '[data-composition-id][data-root="true"]',
|
||||
dataAttributes: expect.objectContaining({
|
||||
"data-root": "true",
|
||||
"data-composition-id": "main",
|
||||
}),
|
||||
bbox: { x: 3, y: 4, width: 640, height: 360 },
|
||||
});
|
||||
});
|
||||
|
||||
it("still carries it, once, when a second Chrome shares the sampling", async () => {
|
||||
const { findings, pages } = await checkWith(ACCELERATED, { cores: 4 });
|
||||
expect(pages).toBe(2);
|
||||
expect(blurCodes(findings)).toEqual(["motion_blur_off"]);
|
||||
});
|
||||
|
||||
it("keeps an unrelated console warning and drops the runtime's duplicate of the finding", async () => {
|
||||
const { findings } = await checkWith(ACCELERATED, {
|
||||
echo: [
|
||||
["warn", "[motion-blur] off: a WebGL canvas cannot be captured"],
|
||||
["warn", "your own warning"],
|
||||
],
|
||||
});
|
||||
const warnings = findings.filter(({ code }) => code === "console_warning");
|
||||
expect(warnings.map(({ message }) => message)).toEqual(["your own warning"]);
|
||||
expect(blurCodes(findings)).toEqual(["motion_blur_off"]);
|
||||
});
|
||||
|
||||
it("keeps the runtime's off-line when there is no finding to replace it", async () => {
|
||||
const { findings } = await checkWith(
|
||||
{ state: "on", reason: null, reasonCode: null, movers: 2 },
|
||||
{ echo: [["warn", "[motion-blur] off: later, for a reason the probe did not see"]] },
|
||||
);
|
||||
expect(findings.map(({ code }) => code)).toContain("console_warning");
|
||||
expect(blurCodes(findings)).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not drop an error that merely starts like the echo", async () => {
|
||||
const { findings } = await checkWith(ACCELERATED, {
|
||||
echo: [["error", "[motion-blur] off: thrown by the page itself"]],
|
||||
});
|
||||
expect(findings.map(({ code }) => code)).toContain("console_error");
|
||||
});
|
||||
|
||||
it("says nothing moves only when the report says no mover, not when it says some", async () => {
|
||||
const still = await checkWith({ state: "on", reason: null, reasonCode: null, movers: 0 });
|
||||
expect(blurCodes(still.findings)).toEqual(["motion_blur_no_movers"]);
|
||||
const moving = await checkWith({ state: "on", reason: null, reasonCode: null, movers: 3 });
|
||||
expect(blurCodes(moving.findings)).toEqual([]);
|
||||
const off = await checkWith(ACCELERATED);
|
||||
expect(blurCodes(off.findings)).toEqual(["motion_blur_off"]);
|
||||
});
|
||||
|
||||
it("treats a report without a mover count as no movers", async () => {
|
||||
const { findings } = await checkWith({ state: "on", reason: null, reasonCode: null });
|
||||
expect(blurCodes(findings)).toEqual(["motion_blur_no_movers"]);
|
||||
});
|
||||
|
||||
it("adds nothing for a page that never opted in", async () => {
|
||||
vi.mocked(os.availableParallelism).mockReturnValue(1);
|
||||
mountCanvasFixture();
|
||||
const page = fakePage();
|
||||
installSessionMock(page);
|
||||
const result = await runBrowserCheck(
|
||||
PROJECT,
|
||||
{ ...DEFAULT_CHECK_OPTIONS, samples: 1, contrast: false },
|
||||
{ kind: "none" },
|
||||
runAuditGrid,
|
||||
);
|
||||
expect(blurCodes(result.runtimeFindings)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CheckDeadline } from "./checkDeadline.js";
|
||||
import { BrowserCleanupError, closeManagedBrowser } from "../browser/launch.js";
|
||||
import { collectMotionBlurDrafts, dropMotionBlurConsoleEcho } from "./checkMotionBlur.js";
|
||||
import { checkSnapshotDirectory } from "./checkSnapshotDirectory.js";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
@@ -81,6 +82,7 @@ interface RuntimeDraft {
|
||||
line?: number;
|
||||
count?: number;
|
||||
abortedImage?: boolean;
|
||||
fixHint?: string;
|
||||
}
|
||||
|
||||
type RuntimeStamp = Pick<RuntimeDraft, "time" | "timeRange">;
|
||||
@@ -297,6 +299,8 @@ export async function runBrowserCheck(
|
||||
const message = tweenTailMessage(tail);
|
||||
drafts.push({ code: "gsap_tween_tail", severity: "warning", message, time: tail.later.end });
|
||||
}
|
||||
const blur = await collectMotionBlurDrafts(page);
|
||||
drafts.push(...blur);
|
||||
const sampleLog = sampleWindow(drafts);
|
||||
const driver = createPageDriver(page, clock, sampleLog);
|
||||
const result = await runGrid(driver, options, motion, helpers);
|
||||
@@ -308,7 +312,7 @@ export async function runBrowserCheck(
|
||||
...result,
|
||||
timings: { ...result.timings, launchSettleMs },
|
||||
runtimeFindings: dropFailedProxyEchoes(
|
||||
keepBrokenImageAborts(merged, broken),
|
||||
dropMotionBlurConsoleEcho(keepBrokenImageAborts(merged, broken), blur),
|
||||
failedPaths,
|
||||
).map((draft) => runtimeFinding(draft, rootAnchor)),
|
||||
};
|
||||
@@ -1352,20 +1356,23 @@ function anchorsInPage(values: AnchorRequest[]): CheckAnchor[] {
|
||||
}
|
||||
|
||||
async function resolveRootAnchor(page: Page): Promise<CheckAnchor> {
|
||||
const selector = await page.evaluate(
|
||||
(explicit) => (document.querySelector(explicit) ? explicit : "[data-composition-id]"),
|
||||
'[data-composition-id][data-root="true"]',
|
||||
);
|
||||
const anchors = await resolveAnchors(page, [
|
||||
{ selector: "[data-composition-id]", time: 0, bbox: await compositionBbox(page) },
|
||||
{ selector, time: 0, bbox: await compositionBbox(page, selector) },
|
||||
]);
|
||||
return anchors[0] ?? fallbackAnchor(undefined);
|
||||
}
|
||||
|
||||
async function compositionBbox(page: Page): Promise<CheckBbox> {
|
||||
return page.evaluate(() => {
|
||||
const element = document.querySelector("[data-composition-id]");
|
||||
const rect = element?.getBoundingClientRect();
|
||||
async function compositionBbox(page: Page, selector: string): Promise<CheckBbox> {
|
||||
return page.evaluate((root) => {
|
||||
const rect = document.querySelector(root)?.getBoundingClientRect();
|
||||
return rect
|
||||
? { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
||||
: { x: 0, y: 0, width: 0, height: 0 };
|
||||
});
|
||||
}, selector);
|
||||
}
|
||||
|
||||
async function collectContrast(
|
||||
@@ -1835,6 +1842,7 @@ function runtimeFinding(draft: RuntimeDraft, root: CheckAnchor): CheckFinding {
|
||||
...(draft.timeRange ? { timeRange: draft.timeRange } : {}),
|
||||
url: draft.url,
|
||||
line: draft.line,
|
||||
...(draft.fixHint ? { fixHint: draft.fixHint } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { readMotionBlurProbe } from "./checkMotionBlur.js";
|
||||
|
||||
const report = { state: "off", reason: "why", reasonCode: "root-transform", movers: 3 };
|
||||
|
||||
function page(html: string, withApi = true, order: string[] = []) {
|
||||
document.body.innerHTML = html;
|
||||
const probe = vi.fn(() => {
|
||||
order.push("probe");
|
||||
return report;
|
||||
});
|
||||
const suspend = vi.fn(() => order.push("suspend"));
|
||||
if (withApi) Reflect.set(window, "__hfMotionBlur", { probe, suspend });
|
||||
return Object.assign(probe, { suspend });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(window, "__hfMotionBlur");
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
const MAIN = '<div data-composition-id="main" data-motion-blur="on"></div>';
|
||||
const TWO_COMPOSITIONS =
|
||||
'<div data-composition-id="empty"></div>' +
|
||||
'<div data-composition-id="main" data-root="true" data-motion-blur="on"></div>';
|
||||
|
||||
describe("readMotionBlurProbe", () => {
|
||||
it("calls the runtime probe and returns its report", () => {
|
||||
const probe = page(MAIN);
|
||||
expect(readMotionBlurProbe()).toEqual(report);
|
||||
expect(probe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("asks the runtime, not the first composition's attribute, which root wants blur", () => {
|
||||
const probe = page(TWO_COMPOSITIONS);
|
||||
expect(readMotionBlurProbe()).toEqual(report);
|
||||
expect(probe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("suspends blur painting right after the probe, so the audits read plain DOM", () => {
|
||||
const order: string[] = [];
|
||||
page(MAIN, true, order);
|
||||
readMotionBlurProbe();
|
||||
expect(order).toEqual(["probe", "suspend"]);
|
||||
});
|
||||
|
||||
it("still reads the probe from a runtime that has no suspend", () => {
|
||||
document.body.innerHTML = MAIN;
|
||||
Reflect.set(window, "__hfMotionBlur", { probe: () => report });
|
||||
expect(readMotionBlurProbe()).toEqual(report);
|
||||
});
|
||||
|
||||
it("takes the mover count from the report, and reads a missing one as none", () => {
|
||||
Reflect.set(window, "__hfMotionBlur", { probe: () => ({ state: "on", movers: 7 }) });
|
||||
expect(readMotionBlurProbe()).toMatchObject({ movers: 7 });
|
||||
Reflect.set(window, "__hfMotionBlur", { probe: () => ({ state: "on" }) });
|
||||
expect(readMotionBlurProbe()).toMatchObject({ state: "on", movers: 0, reason: null });
|
||||
});
|
||||
|
||||
it("reads nothing when the runtime installed no instance, as for a composition that never opted in", () => {
|
||||
page('<div data-composition-id="main"></div>', false);
|
||||
expect(readMotionBlurProbe()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
type MotionBlurProbe,
|
||||
dropMotionBlurConsoleEcho,
|
||||
motionBlurDrafts,
|
||||
} from "./checkMotionBlur.js";
|
||||
|
||||
const off = (reasonCode: string | null, reason = "because"): MotionBlurProbe => ({
|
||||
state: "off",
|
||||
reason,
|
||||
reasonCode,
|
||||
movers: 0,
|
||||
});
|
||||
|
||||
describe("motionBlurDrafts", () => {
|
||||
it("says nothing without an opt-in to read, or when blur works", () => {
|
||||
expect(motionBlurDrafts(null)).toEqual([]);
|
||||
expect(motionBlurDrafts({ state: "on", reason: null, reasonCode: null, movers: 2 })).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(motionBlurDrafts({ state: "on", reason: null, reasonCode: null, movers: 1 })).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"accelerated-canvas",
|
||||
/in sub-compositions too.*render that effect separately.*drop data-motion-blur/,
|
||||
],
|
||||
["backface-visibility", /mirrored.*3D flip/],
|
||||
["iframe-in-root", /<iframe>, <object>, <embed> or custom element out of the composition root/],
|
||||
["root-transform", /child element instead of the composition root/],
|
||||
["root-opacity", /child element instead of the composition root/],
|
||||
["root-blend-mode", /mix-blend-mode on a child element/],
|
||||
["nested-capture-canvas", /inner <canvas layoutsubtree>/],
|
||||
["something-new", /drop data-motion-blur/],
|
||||
])("warns with a concrete fix for %s", (code, hint) => {
|
||||
const [finding, ...rest] = motionBlurDrafts(off(code, "the reason text"));
|
||||
expect(rest).toEqual([]);
|
||||
expect(finding).toMatchObject({
|
||||
code: "motion_blur_off",
|
||||
severity: "warning",
|
||||
message: 'data-motion-blur="on" is set but blur is off: the reason text',
|
||||
});
|
||||
expect(finding?.fixHint).toMatch(hint);
|
||||
});
|
||||
|
||||
it("only informs, once, when the browser cannot run html-in-canvas", () => {
|
||||
const drafts = motionBlurDrafts(off("unsupported-browser", "html-in-canvas is unavailable"));
|
||||
expect(drafts).toHaveLength(1);
|
||||
expect(drafts[0]).toMatchObject({ code: "motion_blur_unverified", severity: "info" });
|
||||
expect(drafts[0]?.message).toContain("could not be evaluated in this browser");
|
||||
});
|
||||
|
||||
it("informs when blur is on but nothing moves", () => {
|
||||
expect(
|
||||
motionBlurDrafts({ state: "on", reason: null, reasonCode: null, movers: 0 }),
|
||||
).toMatchObject([{ code: "motion_blur_no_movers", severity: "info" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dropMotionBlurConsoleEcho", () => {
|
||||
const echo = { code: "console_warning", message: "[motion-blur] off: because" };
|
||||
const other = { code: "console_warning", message: "something else" };
|
||||
const found = motionBlurDrafts(off("root-transform"));
|
||||
|
||||
it("drops the runtime's own off-line once a finding says it", () => {
|
||||
expect(dropMotionBlurConsoleEcho([echo, other], found)).toEqual([other]);
|
||||
});
|
||||
|
||||
it("leaves every console warning alone when there is no finding", () => {
|
||||
expect(dropMotionBlurConsoleEcho([echo, other], [])).toEqual([echo, other]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { Page } from "puppeteer-core";
|
||||
import type { CheckSeverity } from "./checkTypes.js";
|
||||
|
||||
/** What the core runtime's `__hfMotionBlur.probe()` reports for a root that asked for blur. */
|
||||
export interface MotionBlurProbe {
|
||||
state: "on" | "off";
|
||||
reason: string | null;
|
||||
reasonCode: string | null;
|
||||
movers: number;
|
||||
}
|
||||
|
||||
export interface MotionBlurDraft {
|
||||
code: string;
|
||||
severity: CheckSeverity;
|
||||
message: string;
|
||||
fixHint: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
const OFF_HINTS: Record<string, string> = {
|
||||
"accelerated-canvas":
|
||||
"html-in-canvas captures WebGL/WebGPU frozen, in sub-compositions too: render that effect separately or flatten it out of the page, or drop data-motion-blur on this composition",
|
||||
"backface-visibility":
|
||||
"backface-visibility: hidden inside a 3D transform is drawn mirrored; avoid the 3D flip or drop motion blur",
|
||||
"iframe-in-root":
|
||||
"move the <iframe>, <object>, <embed> or custom element out of the composition root (each is re-initialised when blur wraps the root), or drop motion blur",
|
||||
"root-transform": "animate a child element instead of the composition root, or drop motion blur",
|
||||
"root-opacity": "animate a child element instead of the composition root, or drop motion blur",
|
||||
"root-blend-mode":
|
||||
"set mix-blend-mode on a child element instead of the composition root, or drop motion blur",
|
||||
"nested-capture-canvas":
|
||||
"remove the inner <canvas layoutsubtree> / content=drawable, or drop motion blur",
|
||||
};
|
||||
const GENERIC_OFF_HINT = "remove what the reason names, or drop data-motion-blur";
|
||||
|
||||
const ASKED = 'data-motion-blur="on" is set but';
|
||||
|
||||
/** The findings for one probe; null (no opt-in, or nothing to read) gives none. */
|
||||
export function motionBlurDrafts(probe: MotionBlurProbe | null): MotionBlurDraft[] {
|
||||
if (!probe) return [];
|
||||
if (probe.reasonCode === "unsupported-browser") {
|
||||
return [
|
||||
{
|
||||
code: "motion_blur_unverified",
|
||||
severity: "info",
|
||||
message: `${ASKED} blur could not be evaluated in this browser`,
|
||||
fixHint:
|
||||
"the check browser has no html-in-canvas (drawElementImage); render once to see whether blur applies",
|
||||
time: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (probe.state === "off") {
|
||||
return [
|
||||
{
|
||||
code: "motion_blur_off",
|
||||
severity: "warning",
|
||||
message: `${ASKED} blur is off: ${probe.reason ?? "unknown reason"}`,
|
||||
fixHint: OFF_HINTS[probe.reasonCode ?? ""] ?? GENERIC_OFF_HINT,
|
||||
time: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (probe.movers === 0) {
|
||||
return [
|
||||
{
|
||||
code: "motion_blur_no_movers",
|
||||
severity: "info",
|
||||
message: `${ASKED} nothing in the composition moves fast enough to blur, so it has no effect`,
|
||||
fixHint:
|
||||
"animate an element's transform or position by 2+ px per frame, or drop data-motion-blur",
|
||||
time: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Serialized into the page; the guards must stay one function.
|
||||
// fallow-ignore-next-line complexity
|
||||
export function readMotionBlurProbe(): MotionBlurProbe | null {
|
||||
const api: unknown = Reflect.get(window, "__hfMotionBlur");
|
||||
if (typeof api !== "object" || api === null) return null;
|
||||
const probe: unknown = Reflect.get(api, "probe");
|
||||
if (typeof probe !== "function") return null;
|
||||
const report: unknown = Reflect.apply(probe, api, []);
|
||||
if (typeof report !== "object" || report === null) return null;
|
||||
const reason: unknown = Reflect.get(report, "reason");
|
||||
const code: unknown = Reflect.get(report, "reasonCode");
|
||||
const movers: unknown = Reflect.get(report, "movers");
|
||||
const suspend: unknown = Reflect.get(api, "suspend");
|
||||
if (typeof suspend === "function") Reflect.apply(suspend, api, []);
|
||||
return {
|
||||
state: Reflect.get(report, "state") === "off" ? "off" : "on",
|
||||
reason: typeof reason === "string" ? reason : null,
|
||||
reasonCode: typeof code === "string" ? code : null,
|
||||
movers: typeof movers === "number" ? movers : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** Ask the page to decide blur now. The runtime exposes the global only for a root that asked for blur. */
|
||||
export async function collectMotionBlurDrafts(page: Page): Promise<MotionBlurDraft[]> {
|
||||
const probe = await page.evaluate(readMotionBlurProbe).catch(() => null);
|
||||
return motionBlurDrafts(probe);
|
||||
}
|
||||
|
||||
/** The runtime also warns on the console when a scan turns blur off; the finding already says it. */
|
||||
export function dropMotionBlurConsoleEcho<T extends { code: string; message: string }>(
|
||||
drafts: T[],
|
||||
found: MotionBlurDraft[],
|
||||
): T[] {
|
||||
if (found.length === 0) return drafts;
|
||||
return drafts.filter(
|
||||
(d) => !(d.code === "console_warning" && d.message.startsWith("[motion-blur] off:")),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user