Tonight
This commit is contained in:
parent
28323f145e
commit
94f9c0d594
76 changed files with 3238 additions and 1230 deletions
|
|
@ -28,6 +28,14 @@ export interface SceneCtx {
|
|||
cursor: { x: number; y: number };
|
||||
}
|
||||
|
||||
const AI_CLOSEUP_ZOOM_MS = 1400;
|
||||
const RESULTS_ZOOM_OUT_MS = 1500;
|
||||
const EXPORT_ZOOM_OUT_MS = 1100;
|
||||
const PROMPT_TYPING_DELAY_MS = 64;
|
||||
const MAP_ZOOM_WHEEL_STEPS = 18;
|
||||
const MAP_ZOOM_WHEEL_DELTA = -120;
|
||||
const MAP_ZOOM_WHEEL_PAUSE_MS = 70;
|
||||
|
||||
/**
|
||||
* Scene 1: start wide, then zoom into the AI prompt. The AI response is
|
||||
* stubbed, while the map filters and right pane are loaded from the real app.
|
||||
|
|
@ -39,10 +47,15 @@ export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
|||
await showCaption(page, 'Brief: flats or terraces under £450k near central Manchester.');
|
||||
await sleep(180);
|
||||
|
||||
await zoomToAiBox(page, 720);
|
||||
await sleep(760);
|
||||
await zoomToAiBox(page, AI_CLOSEUP_ZOOM_MS);
|
||||
await sleep(AI_CLOSEUP_ZOOM_MS + 160);
|
||||
|
||||
await fakeType(page, '[data-tutorial="ai-filters"] textarea', PROMPT_TEXT, 18);
|
||||
await fakeType(
|
||||
page,
|
||||
'[data-tutorial="ai-filters"] textarea',
|
||||
PROMPT_TEXT,
|
||||
PROMPT_TYPING_DELAY_MS
|
||||
);
|
||||
await sleep(160);
|
||||
const aiResponse = page
|
||||
.waitForResponse(
|
||||
|
|
@ -71,17 +84,16 @@ export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
|||
export async function sceneZoomOutResults(ctx: SceneCtx): Promise<void> {
|
||||
const { page } = ctx;
|
||||
await showCaption(page, 'Now the view lands on central Manchester with the filters already set.');
|
||||
await zoomReset(page, 860);
|
||||
await sleep(980);
|
||||
await zoomReset(page, RESULTS_ZOOM_OUT_MS);
|
||||
await sleep(RESULTS_ZOOM_OUT_MS + 160);
|
||||
await hideCaption(page);
|
||||
await sleep(180);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene 3: drag the right thumb of the AI-applied travel-time slider from
|
||||
* 35 to 20 minutes. The slider has step=1 over 0–120, so the 15-minute
|
||||
* range crosses 15 step boundaries — at our pace each one gets ~20+ recorded
|
||||
* frames, so the thumb reads as a continuous slide rather than incremental.
|
||||
* 35 to 20 minutes. The slider has step=1 over 0–120, so the drag is paced
|
||||
* with real pointer updates instead of jumping the value directly.
|
||||
*
|
||||
* The card we drag (`tt_0`) only exists because the AI filter step inserted
|
||||
* exactly one travel-time entry; if you change the AI stub's count, update
|
||||
|
|
@ -135,17 +147,18 @@ export async function sceneClusterClick(ctx: SceneCtx): Promise<void> {
|
|||
|
||||
await showCaption(page, 'Zoom into the result clusters, then click a promising hexagon.');
|
||||
|
||||
const cluster = {
|
||||
const defaultCluster = {
|
||||
x: 360 + (viewport.width - 360) * 0.35,
|
||||
y: viewport.height * 0.52,
|
||||
};
|
||||
const cluster = await pickMapZoomTarget(ctx, defaultCluster);
|
||||
|
||||
await smoothMove(page, ctx.cursor, cluster, { durationMs: 520 });
|
||||
ctx.cursor = cluster;
|
||||
await sleep(220);
|
||||
|
||||
await zoomMapWithWheel(ctx, cluster);
|
||||
ctx.cursor = await clickVisibleHexagon(ctx);
|
||||
ctx.cursor = await clickVisibleHexagon(ctx, cluster);
|
||||
await sleep(360);
|
||||
await showCaption(
|
||||
page,
|
||||
|
|
@ -155,24 +168,43 @@ export async function sceneClusterClick(ctx: SceneCtx): Promise<void> {
|
|||
await hideCaption(page);
|
||||
}
|
||||
|
||||
async function pickMapZoomTarget(
|
||||
ctx: SceneCtx,
|
||||
fallback: { x: number; y: number }
|
||||
): Promise<{ x: number; y: number }> {
|
||||
const [target] = await ctx.dashboard.visibleHexagonTargets(1).catch(() => []);
|
||||
return target ? { x: target.x, y: target.y } : fallback;
|
||||
}
|
||||
|
||||
async function zoomMapWithWheel(ctx: SceneCtx, target: { x: number; y: number }): Promise<void> {
|
||||
const { page, dashboard } = ctx;
|
||||
const mapVersion = dashboard.getMapDataVersion();
|
||||
await page.mouse.move(target.x, target.y);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await page.mouse.wheel(0, -120);
|
||||
await sleep(95);
|
||||
for (let i = 0; i < MAP_ZOOM_WHEEL_STEPS; i++) {
|
||||
await page.mouse.wheel(0, MAP_ZOOM_WHEEL_DELTA);
|
||||
await sleep(MAP_ZOOM_WHEEL_PAUSE_MS);
|
||||
}
|
||||
await dashboard.waitForMapSettled(mapVersion, 16000);
|
||||
await sleep(260);
|
||||
}
|
||||
|
||||
async function clickVisibleHexagon(ctx: SceneCtx): Promise<{ x: number; y: number }> {
|
||||
const candidates = await ctx.dashboard.visibleHexagonTargets(8);
|
||||
async function clickVisibleHexagon(
|
||||
ctx: SceneCtx,
|
||||
fallbackTarget: { x: number; y: number }
|
||||
): Promise<{ x: number; y: number }> {
|
||||
const candidates = await ctx.dashboard.visibleHexagonTargets(8).catch((error) => {
|
||||
console.log(
|
||||
`[scene] Falling back to direct map click targets: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
return [];
|
||||
});
|
||||
const clickTargets = await addFallbackClickTargets(ctx, candidates, fallbackTarget);
|
||||
const startedAt = ctx.dashboard.getSelectionStatsVersion();
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const target of candidates) {
|
||||
for (const target of clickTargets) {
|
||||
await moveAndClickHexagon(ctx, target);
|
||||
try {
|
||||
await ctx.dashboard.waitForSelectionReady(startedAt, 7000);
|
||||
|
|
@ -192,6 +224,39 @@ async function clickVisibleHexagon(ctx: SceneCtx): Promise<{ x: number; y: numbe
|
|||
);
|
||||
}
|
||||
|
||||
async function addFallbackClickTargets(
|
||||
ctx: SceneCtx,
|
||||
candidates: HexagonClickTarget[],
|
||||
fallbackTarget: { x: number; y: number }
|
||||
): Promise<HexagonClickTarget[]> {
|
||||
const mapBox = await ctx.page.locator('[data-tutorial="map"]').boundingBox();
|
||||
const fallbacks: HexagonClickTarget[] = [
|
||||
{
|
||||
h3: 'direct-target',
|
||||
x: fallbackTarget.x,
|
||||
y: fallbackTarget.y,
|
||||
count: 1,
|
||||
},
|
||||
];
|
||||
|
||||
if (mapBox) {
|
||||
fallbacks.push({
|
||||
h3: 'map-center',
|
||||
x: mapBox.x + mapBox.width / 2,
|
||||
y: mapBox.y + mapBox.height / 2,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
return [...candidates, ...fallbacks].filter((target) => {
|
||||
const key = `${Math.round(target.x / 12)},${Math.round(target.y / 12)}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function moveAndClickHexagon(ctx: SceneCtx, target: HexagonClickTarget): Promise<void> {
|
||||
await smoothMove(ctx.page, ctx.cursor, { x: target.x, y: target.y }, { durationMs: 420 });
|
||||
ctx.cursor = { x: target.x, y: target.y };
|
||||
|
|
@ -204,8 +269,8 @@ export async function sceneExportAndOutro(ctx: SceneCtx): Promise<void> {
|
|||
const { page } = ctx;
|
||||
|
||||
await showCaption(page, 'When the shortlist feels right, export the exact filtered view.');
|
||||
await zoomReset(page, 680);
|
||||
await sleep(520);
|
||||
await zoomReset(page, EXPORT_ZOOM_OUT_MS);
|
||||
await sleep(EXPORT_ZOOM_OUT_MS + 120);
|
||||
|
||||
const exportButton = page.locator('button[title="Export to Excel"]').first();
|
||||
await exportButton.waitFor({ state: 'visible', timeout: 4000 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue