Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
* @file frames.trackMarkers.interaction.browser.spec.ts
* @input Real Chromium/WebKit in the interaction projects
* @output Proof that one `trackMarkers` decode for a marker and a control
* finds, frame by frame, what one `trackMarker` decode per color finds.
* @position Why a busy proof may track its subject and its control in one
* decode of each frame.
*/

import {expect, test} from '@astryxdesign/a11y-spec/playwright';
import {
CONTROL_RGB,
MARKER_RGB,
trackMarker,
trackMarkers,
type CastFrame,
} from './index';

test('one decode for a marker and a control finds what one decode per color finds', async ({
page,
}) => {
// Frames drawn in the page: the marker and the control apart, touching,
// and each one missing from a frame.
const width = 120;
const height = 80;
const scenes: {
marker: [number, number, number, number] | null;
control: [number, number, number, number] | null;
}[] = [
{marker: [10, 10, 20, 20], control: [70, 40, 16, 16]},
{marker: [30, 12, 20, 20], control: [50, 12, 16, 16]},
{marker: [50, 14, 20, 20], control: null},
{marker: null, control: [60, 30, 16, 16]},
];
const drawn = await page.evaluate(
({size, all, marker, control}) => {
const canvas = document.createElement('canvas');
canvas.width = size.width;
canvas.height = size.height;
const context = canvas.getContext('2d');
if (context == null) {
throw new Error('no 2d context');
}
return all.map(scene => {
context.fillStyle = '#fff';
context.fillRect(0, 0, size.width, size.height);
if (scene.marker != null) {
context.fillStyle = `rgb(${marker.join(',')})`;
context.fillRect(...scene.marker);
}
if (scene.control != null) {
context.fillStyle = `rgb(${control.join(',')})`;
context.fillRect(...scene.control);
}
return canvas
.toDataURL('image/png')
.slice('data:image/png;base64,'.length);
});
},
{
size: {width, height},
all: scenes,
marker: [...MARKER_RGB],
control: [...CONTROL_RGB],
},
);
const frames: CastFrame[] = drawn.map((data, index) => ({
t: 1_000 + index * 16,
data,
format: 'png',
viewportWidth: width,
viewportHeight: height,
}));
const [marker, control] = await trackMarkers(page, frames, {
colors: [MARKER_RGB, CONTROL_RGB],
});
expect(marker).toEqual(await trackMarker(page, frames));
expect(control).toEqual(await trackMarker(page, frames, {rgb: CONTROL_RGB}));
// Each decode found the boxes drawn, so equal results are not two misses.
expect(marker.map(f => f.box?.x ?? null)).toEqual(
scenes.map(scene => scene.marker?.[0] ?? null),
);
expect(control.map(f => f.box?.x ?? null)).toEqual(
scenes.map(scene => scene.control?.[0] ?? null),
);
// Only the default marker carries the cast's step, in one decode as in two.
expect(marker[0].step).toEqual({x: 1, y: 1});
expect(control.map(f => f.step ?? null)).toEqual(scenes.map(() => null));
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
drag,
frames,
markElement,
MARKER_RGB,
openStory,
pinch,
pooledBusyProof,
Expand All @@ -38,7 +39,7 @@ import {
type TouchDriver,
type TouchScript,
touchStoryboard,
trackMarker,
trackMarkers,
waitForAnimation,
waitForRest,
withStorybook,
Expand Down Expand Up @@ -576,6 +577,13 @@ async function recordTrackRuns(
/** The slowed tempo the busy page-settle proofs play the viewer at. */
const SETTLE_TEMPO = 5;

/**
* The part of a busy page-settle test's time its pool leaves free, ms. It
* covers the browser's setup before the test's own code and the work after
* the pool. It also covers a settle a little slower than the ones before it.
*/
const POOL_RESERVE_MS = 12_000;

/** The middle value, or the mean of the two middle ones. */
function median(values: ReadonlyArray<number>): number {
const sorted = [...values].sort((a, b) => a - b);
Expand Down Expand Up @@ -683,20 +691,43 @@ async function pooledSettleProof(
{
start = 'shown',
onFirst,
deadline,
}: {
start?: 'shown' | 'after-lift';
onFirst?: (
castFrames: Parameters<typeof castStoryboard>[1],
tracked: BusyRun['tracked'],
block: BlockedWindow,
) => Promise<void>;
/**
* When the pool must be done, epoch ms. A settle that would not fit
* before it is not started: the pool fails by name.
*/
deadline?: number;
} = {},
): Promise<PooledSettles> {
const trackRuns = await recordTrackRuns(g.track);
const runs: BusyRun[] = [];
const blocks: BlockedWindow[] = [];
const settles: TrackRun[] = [];
// How long each settle took, its first storyboard left out.
const settleMs: number[] = [];
for (const direction of toward) {
// A settle takes about as long as the ones before it. When the slowest
// so far no longer fits, the machine is too slow for the pool. The run
// then fails by name, and the test's timeout never ends it.
if (deadline != null && settleMs.length > 0) {
const slowest = Math.max(...settleMs);
const left = deadline - Date.now();
if (slowest > left) {
const seconds = (ms: number) => (ms / 1000).toFixed(1);
throw new Error(
`pooledSettleProof: too slow for the pool: ${settleMs.length} of ${toward.length} settles took ${seconds(settleMs.reduce((a, b) => a + b, 0))} s, up to ${seconds(slowest)} s each, and ${seconds(Math.max(0, left))} s of the test's time remain`,
);
}
}
const settleStarted = Date.now();
let storyboardMs = 0;
// The tracked photo keeps its left edge on screen as the page settles.
// Toward the next page it is the next photo, which slides in from the
// right. Back, it is the current one, which slides out to the right. A box
Expand Down Expand Up @@ -739,10 +770,13 @@ async function pooledSettleProof(
if (settle == null) {
throw new Error('no settle recorded');
}
const tracked = await trackMarker(page, castFrames);
// One decode of each frame finds the photo's marker and the control.
const [tracked, control] = await trackMarkers(page, castFrames, {
colors: [MARKER_RGB, CONTROL_RGB],
});
runs.push({
tracked,
control: await trackMarker(page, castFrames, {rgb: CONTROL_RGB}),
control,
window: {
from: afterInFlightFrame(tracked, block.from),
to: Math.min(block.to, settle.from + settle.curveMs),
Expand All @@ -751,10 +785,13 @@ async function pooledSettleProof(
blocks.push(block);
settles.push(settle);
if (runs.length === 1) {
const storyboardStarted = Date.now();
await onFirst?.(castFrames, tracked, block);
storyboardMs = Date.now() - storyboardStarted;
}
await waitForRest(g.track);
await unmark();
settleMs.push(Date.now() - settleStarted - storyboardMs);
}
// The settle slows at its end. Near the end a 60 Hz frame moves less than
// one cast step, so the cast shows plateaus there. Judging stops 12 cast
Expand Down Expand Up @@ -805,6 +842,11 @@ async function settleBlockLags(
test('the page settle keeps advancing every frame while the main thread is blocked', async ({
page,
}, testInfo) => {
// With no test timeout (0, as under --debug) the pool has no deadline.
const deadline =
testInfo.timeout > 0
? Date.now() + testInfo.timeout - POOL_RESERVE_MS
: undefined;
const g = await openGallery(page);
// A compositor-only control animation in the same windows: what the
// compositor presented while the main thread spun, on this machine.
Expand All @@ -822,6 +864,7 @@ test('the page settle keeps advancing every frame while the main thread is block
g,
['next', 'next', 'next', 'previous', 'previous', 'previous'],
{
deadline,
onFirst: async (castFrames, tracked, block) => {
const film = await castStoryboard(page, castFrames, {
name: 'lightbox-page-settle-busy-main-thread',
Expand Down
Loading