alright
This commit is contained in:
parent
c645b0f1d4
commit
39ef5c6646
79 changed files with 5660 additions and 2199 deletions
|
|
@ -101,7 +101,7 @@ async function runCue(
|
|||
videoTimeMs: cursor.ms + leadInMs,
|
||||
durationMs: measuredAudioMs,
|
||||
});
|
||||
await showCaption(ctx.page, cue.text);
|
||||
await showCaption(ctx.page, cue.text, cue.captionPlacement);
|
||||
|
||||
const during = cue.during ?? [];
|
||||
const declaredSum = during.reduce((s, a) => s + a.durationMs, 0);
|
||||
|
|
@ -184,7 +184,36 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
|
|||
return;
|
||||
}
|
||||
case 'click': {
|
||||
const to = await resolveTarget(ctx, step.target);
|
||||
const selectionVersion = ctx.dashboard.getSelectionStatsVersion();
|
||||
const candidates =
|
||||
step.target.kind === 'hexagon' && step.waitForSelectionReady
|
||||
? await ctx.dashboard.visibleHexagonTargets(4)
|
||||
: [await resolveTarget(ctx, step.target)];
|
||||
let lastError: unknown = null;
|
||||
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
const to = candidates[i];
|
||||
const moveMs = Math.max(120, Math.round(step.durationMs * 0.7));
|
||||
await smoothMove(ctx.page, ctx.cursor, to, { durationMs: moveMs });
|
||||
ctx.cursor = to;
|
||||
await ctx.page.mouse.click(to.x, to.y);
|
||||
if (!step.waitForSelectionReady) return;
|
||||
|
||||
try {
|
||||
await ctx.dashboard.waitForSelectionReady(
|
||||
selectionVersion,
|
||||
Math.min(step.timeoutMs ?? 12000, i === candidates.length - 1 ? 12000 : 4000)
|
||||
);
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
}
|
||||
throw lastError ?? new Error('Click did not open the selection pane');
|
||||
}
|
||||
case 'clickIfVisible': {
|
||||
const to = await tryResolveTarget(ctx, step.target, step.timeoutMs ?? 700);
|
||||
if (!to) return;
|
||||
const moveMs = Math.max(120, Math.round(step.durationMs * 0.7));
|
||||
await smoothMove(ctx.page, ctx.cursor, to, { durationMs: moveMs });
|
||||
ctx.cursor = to;
|
||||
|
|
@ -196,16 +225,109 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
|
|||
return;
|
||||
case 'mapZoom': {
|
||||
const point = await resolveTarget(ctx, step.target);
|
||||
await ctx.page.mouse.move(point.x, point.y);
|
||||
const perStepMs = Math.floor(step.durationMs / Math.max(1, step.steps));
|
||||
const mapVersion = ctx.dashboard.getMapDataVersion();
|
||||
const delta = step.direction === 'out' ? -MAP_ZOOM_WHEEL_DELTA : MAP_ZOOM_WHEEL_DELTA;
|
||||
for (let i = 0; i < step.steps; i++) {
|
||||
await ctx.page.mouse.wheel(0, delta);
|
||||
if (perStepMs > 0) await sleep(perStepMs);
|
||||
const handled = await ctx.page.evaluate(
|
||||
async ({ x, y, steps, durationMs, direction }) => {
|
||||
const root = document.querySelector('.maplibregl-map') as HTMLElement | null;
|
||||
const fiberKey = root
|
||||
? Object.getOwnPropertyNames(root).find((key) => key.startsWith('__reactFiber$'))
|
||||
: undefined;
|
||||
let fiber = fiberKey ? (root as unknown as Record<string, unknown>)[fiberKey] : null;
|
||||
let mapRef: unknown = null;
|
||||
while (fiber && typeof fiber === 'object') {
|
||||
const maybeFiber = fiber as {
|
||||
ref?: { current?: unknown };
|
||||
return?: unknown;
|
||||
};
|
||||
const current = maybeFiber.ref?.current;
|
||||
if (
|
||||
current &&
|
||||
typeof current === 'object' &&
|
||||
typeof (current as { getMap?: unknown }).getMap === 'function'
|
||||
) {
|
||||
mapRef = current;
|
||||
break;
|
||||
}
|
||||
fiber = maybeFiber.return ?? null;
|
||||
}
|
||||
|
||||
const map = (mapRef as { getMap?: () => unknown } | null)?.getMap?.();
|
||||
if (!map || typeof map !== 'object') return false;
|
||||
const mapApi = map as {
|
||||
getCanvas: () => HTMLCanvasElement;
|
||||
getZoom: () => number;
|
||||
getMinZoom?: () => number;
|
||||
getMaxZoom?: () => number;
|
||||
unproject: (point: [number, number]) => unknown;
|
||||
zoomTo: (
|
||||
zoom: number,
|
||||
options: { around?: unknown; duration?: number; essential?: boolean }
|
||||
) => void;
|
||||
};
|
||||
if (
|
||||
typeof mapApi.getCanvas !== 'function' ||
|
||||
typeof mapApi.getZoom !== 'function' ||
|
||||
typeof mapApi.unproject !== 'function' ||
|
||||
typeof mapApi.zoomTo !== 'function'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = mapApi.getCanvas().getBoundingClientRect();
|
||||
const around = mapApi.unproject([x - rect.left, y - rect.top]);
|
||||
const sign = direction === 'out' ? -1 : 1;
|
||||
const zoomDelta = Math.max(0.25, Math.min(5.2, steps * 0.28)) * sign;
|
||||
const minZoom = mapApi.getMinZoom?.() ?? 0;
|
||||
const maxZoom = mapApi.getMaxZoom?.() ?? 22;
|
||||
const targetZoom = Math.max(minZoom, Math.min(maxZoom, mapApi.getZoom() + zoomDelta));
|
||||
mapApi.zoomTo(targetZoom, { around, duration: durationMs, essential: true });
|
||||
await new Promise((resolve) => window.setTimeout(resolve, durationMs));
|
||||
return true;
|
||||
},
|
||||
{
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
steps: step.steps,
|
||||
durationMs: step.durationMs,
|
||||
direction: step.direction,
|
||||
}
|
||||
);
|
||||
|
||||
if (!handled) {
|
||||
const perStepMs = Math.floor(step.durationMs / Math.max(1, step.steps));
|
||||
await ctx.page.evaluate(
|
||||
async ({ x, y, steps, durationMs, delta }) => {
|
||||
const wait = (ms: number) =>
|
||||
new Promise<void>((resolve) => window.setTimeout(resolve, ms));
|
||||
const perStep = Math.floor(durationMs / Math.max(1, steps));
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const target = document.elementFromPoint(x, y) ?? document.querySelector('canvas');
|
||||
target?.dispatchEvent(
|
||||
new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
deltaY: delta,
|
||||
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
|
||||
view: window,
|
||||
})
|
||||
);
|
||||
if (perStep > 0) await wait(perStep);
|
||||
}
|
||||
},
|
||||
{ x: point.x, y: point.y, steps: step.steps, durationMs: step.durationMs, delta }
|
||||
);
|
||||
if (perStepMs > 0) await sleep(0);
|
||||
}
|
||||
if (step.waitForMapSettled) {
|
||||
await ctx.dashboard.waitForMapSettled(mapVersion, step.timeoutMs ?? 12000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'dragSlider':
|
||||
case 'dragSlider': {
|
||||
const mapVersion = ctx.dashboard.getMapDataVersion();
|
||||
ctx.cursor = await smoothDragSliderThumb(
|
||||
ctx.page,
|
||||
step.thumbSelector,
|
||||
|
|
@ -214,12 +336,21 @@ async function runActivity(ctx: ScriptCtx, step: Activity): Promise<void> {
|
|||
step.toFraction,
|
||||
step.durationMs
|
||||
);
|
||||
if (step.waitForMapSettled) {
|
||||
await ctx.dashboard.waitForMapSettled(mapVersion, step.timeoutMs ?? 12000);
|
||||
}
|
||||
return;
|
||||
case 'submitForm':
|
||||
}
|
||||
case 'submitForm': {
|
||||
const mapVersion = ctx.dashboard.getMapDataVersion();
|
||||
await ctx.page.evaluate((selector) => {
|
||||
document.querySelector<HTMLFormElement>(selector)?.requestSubmit();
|
||||
}, step.formSelector);
|
||||
if (step.waitForMapSettled) {
|
||||
await ctx.dashboard.waitForMapSettled(mapVersion, step.timeoutMs ?? 12000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'showOutro':
|
||||
await showOutro(ctx.page, step.brand, step.tagline, step.url);
|
||||
return;
|
||||
|
|
@ -269,6 +400,30 @@ async function resolveTarget(
|
|||
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
}
|
||||
|
||||
async function tryResolveTarget(
|
||||
ctx: ScriptCtx,
|
||||
target: Target,
|
||||
timeoutMs: number
|
||||
): Promise<{ x: number; y: number } | null> {
|
||||
if (target.kind !== 'element') {
|
||||
try {
|
||||
return await resolveTarget(ctx, target);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const locator = ctx.page.locator(target.selector).first();
|
||||
try {
|
||||
await locator.waitFor({ state: 'visible', timeout: timeoutMs });
|
||||
const box = await locator.boundingBox({ timeout: timeoutMs });
|
||||
if (!box) return null;
|
||||
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load synth's measured cue durations. Falls back to a worst-case estimate
|
||||
* if the manifest is missing — that path is only used for ``--no-audio``
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue