125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
import type { Page } from 'playwright';
|
|
import {
|
|
PROMPT_TEXT,
|
|
DRAG_FILTER_NAME,
|
|
DRAG_TO_FRACTION,
|
|
} from './config.js';
|
|
import {
|
|
clearVignette,
|
|
hideCaption,
|
|
showCaption,
|
|
showOutro,
|
|
} from './dom.js';
|
|
import { fakeType, sleep, smoothMove, smoothDragSliderThumb } from './motion.js';
|
|
|
|
export interface SceneCtx {
|
|
page: Page;
|
|
cursor: { x: number; y: number };
|
|
}
|
|
|
|
/** Cold open. Vignette fades; cursor parks at a "natural" rest position. */
|
|
export async function sceneColdOpen(ctx: SceneCtx): Promise<void> {
|
|
await clearVignette(ctx.page);
|
|
await ctx.page.mouse.move(ctx.cursor.x, ctx.cursor.y);
|
|
await sleep(1100);
|
|
}
|
|
|
|
/**
|
|
* AI prompt scene: click the collapsed AI box, type the prompt, submit,
|
|
* watch the (stubbed) response apply.
|
|
*/
|
|
export async function sceneAiPrompt(ctx: SceneCtx): Promise<void> {
|
|
const { page } = ctx;
|
|
|
|
await showCaption(page, 'Describe the area you want.');
|
|
|
|
const aiButton = page.locator('[data-tutorial="ai-filters"] button').first();
|
|
const btnBox = await aiButton.boundingBox();
|
|
if (!btnBox) throw new Error('AI button not found');
|
|
|
|
const target = { x: btnBox.x + btnBox.width / 2, y: btnBox.y + btnBox.height / 2 };
|
|
await smoothMove(page, ctx.cursor, target, { durationMs: 400 });
|
|
ctx.cursor = target;
|
|
|
|
await page.mouse.click(target.x, target.y);
|
|
|
|
const textarea = page.locator('[data-tutorial="ai-filters"] textarea');
|
|
await textarea.waitFor({ state: 'visible', timeout: 3000 });
|
|
await sleep(120);
|
|
|
|
const taBox = await textarea.boundingBox();
|
|
if (taBox) {
|
|
const into = { x: taBox.x + 30, y: taBox.y + taBox.height / 2 };
|
|
await smoothMove(page, ctx.cursor, into, { durationMs: 220 });
|
|
ctx.cursor = into;
|
|
}
|
|
|
|
// fakeType runs the typing animation inside the browser to avoid CDP
|
|
// round-trip overhead per keystroke (which can quadruple total typing time).
|
|
await fakeType(page, '[data-tutorial="ai-filters"] textarea', PROMPT_TEXT, 35);
|
|
await sleep(180);
|
|
|
|
await page.keyboard.press('Enter');
|
|
await sleep(700);
|
|
await hideCaption(page);
|
|
await sleep(150);
|
|
}
|
|
|
|
/**
|
|
* Slider scene: pan to a numeric filter's right thumb and drag it inward.
|
|
* The whole point: the user sees the map react in real time to a human action,
|
|
* driving home that AI sets a starting point but you stay in control.
|
|
*/
|
|
export async function sceneSliderControl(ctx: SceneCtx): Promise<void> {
|
|
const { page } = ctx;
|
|
await showCaption(page, 'You stay in control.');
|
|
|
|
const card = page.locator(`[data-filter-name="${DRAG_FILTER_NAME}"]`);
|
|
await card.waitFor({ state: 'visible', timeout: 3000 });
|
|
await card.scrollIntoViewIfNeeded();
|
|
await sleep(120);
|
|
|
|
const thumbSelector = `[data-filter-name="${DRAG_FILTER_NAME}"] [role="slider"] >> nth=1`;
|
|
const trackSelector = `[data-filter-name="${DRAG_FILTER_NAME}"] [data-orientation="horizontal"] >> nth=0`;
|
|
|
|
ctx.cursor = await smoothDragSliderThumb(
|
|
page,
|
|
thumbSelector,
|
|
trackSelector,
|
|
ctx.cursor,
|
|
DRAG_TO_FRACTION,
|
|
1100
|
|
);
|
|
|
|
await sleep(550);
|
|
await hideCaption(page);
|
|
await sleep(150);
|
|
}
|
|
|
|
/** Property reveal: click a postcode on the map to open the side pane with charts. */
|
|
export async function scenePropertyReveal(ctx: SceneCtx): Promise<void> {
|
|
const { page } = ctx;
|
|
const viewport = page.viewportSize() ?? { width: 1920, height: 1080 };
|
|
|
|
const target = {
|
|
x: 360 + (viewport.width - 360) * 0.55,
|
|
y: viewport.height * 0.5,
|
|
};
|
|
|
|
await smoothMove(page, ctx.cursor, target, { durationMs: 500 });
|
|
ctx.cursor = target;
|
|
|
|
await page.mouse.click(target.x, target.y);
|
|
await sleep(1300);
|
|
}
|
|
|
|
/** Outro: full-screen logo card with brand + URL. */
|
|
export async function sceneOutro(ctx: SceneCtx): Promise<void> {
|
|
await showOutro(
|
|
ctx.page,
|
|
'Perfect Postcodes',
|
|
'Find where you actually want to live.',
|
|
'perfectpostcodes.com'
|
|
);
|
|
await sleep(1800);
|
|
}
|