No hacks
This commit is contained in:
parent
e3e8a4522e
commit
58bb3cb4f8
9 changed files with 49 additions and 174 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 375 KiB |
Binary file not shown.
|
|
@ -233,7 +233,6 @@ export default memo(function Map({
|
||||||
}, [viewState, dimensions, onViewChange]);
|
}, [viewState, dimensions, onViewChange]);
|
||||||
|
|
||||||
const handleMove = useCallback((evt: { viewState: ViewState }) => {
|
const handleMove = useCallback((evt: { viewState: ViewState }) => {
|
||||||
if (window.__demoRecording) window.__demoMapIdle = false;
|
|
||||||
setInternalViewState((prev) => {
|
setInternalViewState((prev) => {
|
||||||
const next = evt.viewState;
|
const next = evt.viewState;
|
||||||
// Skip re-render when viewport values haven't changed (e.g. container resize
|
// Skip re-render when viewport values haven't changed (e.g. container resize
|
||||||
|
|
@ -254,10 +253,6 @@ export default memo(function Map({
|
||||||
|
|
||||||
const handleIdle = useCallback(() => {
|
const handleIdle = useCallback(() => {
|
||||||
if (screenshotMode) window.__map_idle = true;
|
if (screenshotMode) window.__map_idle = true;
|
||||||
if (window.__demoRecording) {
|
|
||||||
window.__demoMapIdle = true;
|
|
||||||
window.__demoMapIdleVersion = (window.__demoMapIdleVersion ?? 0) + 1;
|
|
||||||
}
|
|
||||||
}, [screenshotMode]);
|
}, [screenshotMode]);
|
||||||
|
|
||||||
const handleFlyTo = useCallback((lat: number, lng: number, zoom: number) => {
|
const handleFlyTo = useCallback((lat: number, lng: number, zoom: number) => {
|
||||||
|
|
|
||||||
|
|
@ -55,18 +55,6 @@ const MapPageSelectionPane = lazy(() =>
|
||||||
const UpgradeModal = lazy(() => import('../ui/UpgradeModal'));
|
const UpgradeModal = lazy(() => import('../ui/UpgradeModal'));
|
||||||
const Joyride = lazy(() => import('react-joyride'));
|
const Joyride = lazy(() => import('react-joyride'));
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__demoRecording?: boolean;
|
|
||||||
__demoOpenBestHexagon?: () => string | null;
|
|
||||||
__demoMapSettled?: boolean;
|
|
||||||
__demoMapSettleVersion?: number;
|
|
||||||
__demoMapIdle?: boolean;
|
|
||||||
__demoMapIdleVersion?: number;
|
|
||||||
__demoSelectionReady?: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function MapFallback() {
|
function MapFallback() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center bg-warm-100 dark:bg-navy-950">
|
<div className="flex h-full w-full items-center justify-center bg-warm-100 dark:bg-navy-950">
|
||||||
|
|
@ -231,10 +219,6 @@ export default function MapPage({
|
||||||
travelTimeEntries: entries,
|
travelTimeEntries: entries,
|
||||||
shareCode,
|
shareCode,
|
||||||
});
|
});
|
||||||
const demoMapHasData = mapData.usePostcodeView
|
|
||||||
? mapData.postcodeData.length > 0
|
|
||||||
: mapData.data.length > 0;
|
|
||||||
|
|
||||||
const handleAiFilterSubmit = useCallback(
|
const handleAiFilterSubmit = useCallback(
|
||||||
async (query: string) => {
|
async (query: string) => {
|
||||||
// Build context from current filters for conversational refinement
|
// Build context from current filters for conversational refinement
|
||||||
|
|
@ -432,48 +416,6 @@ export default function MapPage({
|
||||||
setRightPaneTab(initialTab);
|
setRightPaneTab(initialTab);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!window.__demoRecording) return;
|
|
||||||
void import('./MapPageSelectionPane');
|
|
||||||
void import('./AreaPane');
|
|
||||||
void import('./PropertiesPane');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!window.__demoRecording) return;
|
|
||||||
window.__demoMapSettled = !mapData.loading && demoMapHasData;
|
|
||||||
if (window.__demoMapSettled) {
|
|
||||||
window.__demoMapSettleVersion = (window.__demoMapSettleVersion ?? 0) + 1;
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
window.__demoMapSettled = false;
|
|
||||||
};
|
|
||||||
}, [demoMapHasData, mapData.loading]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!window.__demoRecording) return;
|
|
||||||
window.__demoSelectionReady = Boolean(selectedHexagon && areaStats && !loadingAreaStats);
|
|
||||||
return () => {
|
|
||||||
window.__demoSelectionReady = false;
|
|
||||||
};
|
|
||||||
}, [areaStats, loadingAreaStats, selectedHexagon]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!window.__demoRecording) return;
|
|
||||||
window.__demoOpenBestHexagon = () => {
|
|
||||||
const best = mapData.data.reduce<(typeof mapData.data)[number] | null>((winner, item) => {
|
|
||||||
if (!winner || item.count > winner.count) return item;
|
|
||||||
return winner;
|
|
||||||
}, null);
|
|
||||||
if (!best) return null;
|
|
||||||
handleHexagonClick(best.h3);
|
|
||||||
return best.h3;
|
|
||||||
};
|
|
||||||
return () => {
|
|
||||||
delete window.__demoOpenBestHexagon;
|
|
||||||
};
|
|
||||||
}, [handleHexagonClick, mapData.data]);
|
|
||||||
|
|
||||||
// Navigate to a specific postcode on mount (e.g. from saved properties)
|
// Navigate to a specific postcode on mount (e.g. from saved properties)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialPostcode) return;
|
if (!initialPostcode) return;
|
||||||
|
|
@ -1124,7 +1066,6 @@ export default function MapPage({
|
||||||
onClose={handleCloseSelection}
|
onClose={handleCloseSelection}
|
||||||
renderAreaPane={renderAreaPane}
|
renderAreaPane={renderAreaPane}
|
||||||
renderPropertiesPane={renderPropertiesPane}
|
renderPropertiesPane={renderPropertiesPane}
|
||||||
demoReady={Boolean(areaStats && !loadingAreaStats)}
|
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ interface MapPageSelectionPaneProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
renderAreaPane: () => ReactNode;
|
renderAreaPane: () => ReactNode;
|
||||||
renderPropertiesPane: () => ReactNode;
|
renderPropertiesPane: () => ReactNode;
|
||||||
demoReady?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MapPageSelectionPane({
|
export function MapPageSelectionPane({
|
||||||
|
|
@ -30,12 +29,10 @@ export function MapPageSelectionPane({
|
||||||
onClose,
|
onClose,
|
||||||
renderAreaPane,
|
renderAreaPane,
|
||||||
renderPropertiesPane,
|
renderPropertiesPane,
|
||||||
demoReady = false,
|
|
||||||
}: MapPageSelectionPaneProps) {
|
}: MapPageSelectionPaneProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tutorial="right-pane"
|
data-tutorial="right-pane"
|
||||||
data-demo-ready={demoReady ? 'true' : 'false'}
|
|
||||||
className="flex bg-white dark:bg-navy-950 shadow-lg z-10"
|
className="flex bg-white dark:bg-navy-950 shadow-lg z-10"
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,6 @@ export async function launchRecordingBrowser(): Promise<RecordingBrowser> {
|
||||||
|
|
||||||
async function suppressDevServerNoise(context: BrowserContext) {
|
async function suppressDevServerNoise(context: BrowserContext) {
|
||||||
await context.addInitScript(() => {
|
await context.addInitScript(() => {
|
||||||
(window as typeof window & { __demoRecording?: boolean }).__demoRecording = true;
|
|
||||||
|
|
||||||
const RealWS = window.WebSocket;
|
const RealWS = window.WebSocket;
|
||||||
window.WebSocket = new Proxy(RealWS, {
|
window.WebSocket = new Proxy(RealWS, {
|
||||||
construct(target, args) {
|
construct(target, args) {
|
||||||
|
|
|
||||||
|
|
@ -451,67 +451,3 @@ export async function waitForAnimationFrames(page: Page, frames = 3): Promise<vo
|
||||||
frames
|
frames
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDemoMapSettleVersion(page: Page): Promise<number> {
|
|
||||||
return page.evaluate(
|
|
||||||
() =>
|
|
||||||
(
|
|
||||||
window as typeof window & {
|
|
||||||
__demoMapSettleVersion?: number;
|
|
||||||
}
|
|
||||||
).__demoMapSettleVersion ?? 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForDemoMapSettled(
|
|
||||||
page: Page,
|
|
||||||
timeoutMs = 12000,
|
|
||||||
afterVersion = -1
|
|
||||||
): Promise<void> {
|
|
||||||
await page.waitForFunction(
|
|
||||||
(version) => {
|
|
||||||
const demo = window as typeof window & {
|
|
||||||
__demoMapSettled?: boolean;
|
|
||||||
__demoMapSettleVersion?: number;
|
|
||||||
__demoMapIdle?: boolean;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
demo.__demoMapSettled === true &&
|
|
||||||
demo.__demoMapIdle === true &&
|
|
||||||
(demo.__demoMapSettleVersion ?? 0) > version
|
|
||||||
);
|
|
||||||
},
|
|
||||||
afterVersion,
|
|
||||||
{ timeout: timeoutMs }
|
|
||||||
);
|
|
||||||
await waitForAnimationFrames(page, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForCurrentDemoMapSettled(page: Page, timeoutMs = 12000): Promise<void> {
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => {
|
|
||||||
const demo = window as typeof window & {
|
|
||||||
__demoMapSettled?: boolean;
|
|
||||||
__demoMapIdle?: boolean;
|
|
||||||
};
|
|
||||||
return demo.__demoMapSettled === true && demo.__demoMapIdle === true;
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ timeout: timeoutMs }
|
|
||||||
);
|
|
||||||
await waitForAnimationFrames(page, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function waitForDemoSelectionReady(page: Page, timeoutMs = 12000): Promise<void> {
|
|
||||||
await page.waitForFunction(
|
|
||||||
() =>
|
|
||||||
(
|
|
||||||
window as typeof window & {
|
|
||||||
__demoSelectionReady?: boolean;
|
|
||||||
}
|
|
||||||
).__demoSelectionReady === true,
|
|
||||||
undefined,
|
|
||||||
{ timeout: timeoutMs }
|
|
||||||
);
|
|
||||||
await waitForAnimationFrames(page, 4);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Page } from 'playwright';
|
import type { Page } from 'playwright';
|
||||||
|
import type { DashboardRecorder, HexagonClickTarget } from './dashboard.js';
|
||||||
import {
|
import {
|
||||||
AI_ZOOM_SCALE,
|
AI_ZOOM_SCALE,
|
||||||
BRAND_NAME,
|
BRAND_NAME,
|
||||||
|
|
@ -13,14 +14,9 @@ import {
|
||||||
import {
|
import {
|
||||||
clearVignette,
|
clearVignette,
|
||||||
flashRect,
|
flashRect,
|
||||||
getDemoMapSettleVersion,
|
|
||||||
hideCaption,
|
hideCaption,
|
||||||
showCaption,
|
showCaption,
|
||||||
showOutro,
|
showOutro,
|
||||||
visualClick,
|
|
||||||
waitForDemoMapSettled,
|
|
||||||
waitForCurrentDemoMapSettled,
|
|
||||||
waitForDemoSelectionReady,
|
|
||||||
zoomReset,
|
zoomReset,
|
||||||
zoomTo,
|
zoomTo,
|
||||||
} from './dom.js';
|
} from './dom.js';
|
||||||
|
|
@ -28,6 +24,7 @@ import { fakeType, sleep, smoothMove, smoothDragSliderThumb } from './motion.js'
|
||||||
|
|
||||||
export interface SceneCtx {
|
export interface SceneCtx {
|
||||||
page: Page;
|
page: Page;
|
||||||
|
dashboard: DashboardRecorder;
|
||||||
cursor: { x: number; y: number };
|
cursor: { x: number; y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +33,7 @@ export interface SceneCtx {
|
||||||
* stubbed, while the map filters and right pane are loaded from the real app.
|
* stubbed, while the map filters and right pane are loaded from the real app.
|
||||||
*/
|
*/
|
||||||
export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
||||||
const { page } = ctx;
|
const { page, dashboard } = ctx;
|
||||||
|
|
||||||
await clearVignette(page);
|
await clearVignette(page);
|
||||||
await showCaption(page, 'Brief: flats or terraces under £450k near central Manchester.');
|
await showCaption(page, 'Brief: flats or terraces under £450k near central Manchester.');
|
||||||
|
|
@ -53,13 +50,13 @@ export async function sceneAiCloseUp(ctx: SceneCtx): Promise<void> {
|
||||||
{ timeout: 1800 }
|
{ timeout: 1800 }
|
||||||
)
|
)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
const mapVersion = await getDemoMapSettleVersion(page);
|
const mapVersion = dashboard.getMapDataVersion();
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
document.querySelector<HTMLFormElement>('[data-tutorial="ai-filters"] form')?.requestSubmit();
|
document.querySelector<HTMLFormElement>('[data-tutorial="ai-filters"] form')?.requestSubmit();
|
||||||
});
|
});
|
||||||
await aiResponse;
|
await aiResponse;
|
||||||
await sleep(160);
|
await sleep(160);
|
||||||
await waitForDemoMapSettled(page, 15000, mapVersion);
|
await dashboard.waitForMapSettled(mapVersion, 15000);
|
||||||
await showCaption(page, 'The filters are already live on the map.');
|
await showCaption(page, 'The filters are already live on the map.');
|
||||||
await sleep(560);
|
await sleep(560);
|
||||||
await hideCaption(page);
|
await hideCaption(page);
|
||||||
|
|
@ -91,7 +88,7 @@ export async function sceneZoomOutResults(ctx: SceneCtx): Promise<void> {
|
||||||
* the selector or this scene will time out.
|
* the selector or this scene will time out.
|
||||||
*/
|
*/
|
||||||
export async function sceneTravelTimeSlider(ctx: SceneCtx): Promise<void> {
|
export async function sceneTravelTimeSlider(ctx: SceneCtx): Promise<void> {
|
||||||
const { page } = ctx;
|
const { page, dashboard } = ctx;
|
||||||
await showCaption(
|
await showCaption(
|
||||||
page,
|
page,
|
||||||
`Then tighten the commute: ${TT_DRAG_FROM_MIN} minutes down to ${TT_DRAG_TO_MIN}.`
|
`Then tighten the commute: ${TT_DRAG_FROM_MIN} minutes down to ${TT_DRAG_TO_MIN}.`
|
||||||
|
|
@ -109,7 +106,7 @@ export async function sceneTravelTimeSlider(ctx: SceneCtx): Promise<void> {
|
||||||
|
|
||||||
// Slider goes 0..120, target = 20 → fraction 0.166...
|
// Slider goes 0..120, target = 20 → fraction 0.166...
|
||||||
const toFraction = TT_DRAG_TO_MIN / TT_SLIDER_MAX;
|
const toFraction = TT_DRAG_TO_MIN / TT_SLIDER_MAX;
|
||||||
const mapVersion = await getDemoMapSettleVersion(page);
|
const mapVersion = dashboard.getMapDataVersion();
|
||||||
|
|
||||||
ctx.cursor = await smoothDragSliderThumb(
|
ctx.cursor = await smoothDragSliderThumb(
|
||||||
page,
|
page,
|
||||||
|
|
@ -121,7 +118,7 @@ export async function sceneTravelTimeSlider(ctx: SceneCtx): Promise<void> {
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(220);
|
await sleep(220);
|
||||||
await waitForDemoMapSettled(page, 16000, mapVersion);
|
await dashboard.waitForMapSettled(mapVersion, 16000);
|
||||||
await showCaption(page, 'The map redraws around the areas that still work.');
|
await showCaption(page, 'The map redraws around the areas that still work.');
|
||||||
await sleep(720);
|
await sleep(720);
|
||||||
await hideCaption(page);
|
await hideCaption(page);
|
||||||
|
|
@ -147,13 +144,8 @@ export async function sceneClusterClick(ctx: SceneCtx): Promise<void> {
|
||||||
ctx.cursor = cluster;
|
ctx.cursor = cluster;
|
||||||
await sleep(220);
|
await sleep(220);
|
||||||
|
|
||||||
await zoomMapWithWheel(page, cluster);
|
await zoomMapWithWheel(ctx, cluster);
|
||||||
|
ctx.cursor = await clickVisibleHexagon(ctx);
|
||||||
const clicked = await clickHexagon(page, cluster);
|
|
||||||
ctx.cursor = clicked;
|
|
||||||
await openDemoHexagon(page);
|
|
||||||
await page.locator('[data-tutorial="right-pane"]').waitFor({ state: 'visible', timeout: 5000 });
|
|
||||||
await waitForDemoSelectionReady(page, 16000);
|
|
||||||
await sleep(360);
|
await sleep(360);
|
||||||
await showCaption(
|
await showCaption(
|
||||||
page,
|
page,
|
||||||
|
|
@ -163,35 +155,48 @@ export async function sceneClusterClick(ctx: SceneCtx): Promise<void> {
|
||||||
await hideCaption(page);
|
await hideCaption(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickHexagon(
|
async function zoomMapWithWheel(ctx: SceneCtx, target: { x: number; y: number }): Promise<void> {
|
||||||
page: Page,
|
const { page, dashboard } = ctx;
|
||||||
target: { x: number; y: number }
|
const mapVersion = dashboard.getMapDataVersion();
|
||||||
): Promise<{ x: number; y: number }> {
|
|
||||||
await visualClick(page, target);
|
|
||||||
await sleep(140);
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function zoomMapWithWheel(page: Page, target: { x: number; y: number }): Promise<void> {
|
|
||||||
await page.mouse.move(target.x, target.y);
|
await page.mouse.move(target.x, target.y);
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
await page.mouse.wheel(0, -120);
|
await page.mouse.wheel(0, -120);
|
||||||
await sleep(95);
|
await sleep(95);
|
||||||
}
|
}
|
||||||
await waitForCurrentDemoMapSettled(page, 16000);
|
await dashboard.waitForMapSettled(mapVersion, 16000);
|
||||||
await sleep(260);
|
await sleep(260);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDemoHexagon(page: Page): Promise<void> {
|
async function clickVisibleHexagon(ctx: SceneCtx): Promise<{ x: number; y: number }> {
|
||||||
const selected = await page.evaluate(
|
const candidates = await ctx.dashboard.visibleHexagonTargets(8);
|
||||||
() =>
|
const startedAt = ctx.dashboard.getSelectionStatsVersion();
|
||||||
(
|
let lastError: Error | null = null;
|
||||||
window as typeof window & {
|
|
||||||
__demoOpenBestHexagon?: () => string | null;
|
for (const target of candidates) {
|
||||||
}
|
await moveAndClickHexagon(ctx, target);
|
||||||
).__demoOpenBestHexagon?.() ?? null
|
try {
|
||||||
|
await ctx.dashboard.waitForSelectionReady(startedAt, 7000);
|
||||||
|
return { x: target.x, y: target.y };
|
||||||
|
} catch (error) {
|
||||||
|
if (ctx.dashboard.getSelectionStatsVersion() > startedAt) {
|
||||||
|
return { x: target.x, y: target.y };
|
||||||
|
}
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Could not open a map selection from the visible hexagons${
|
||||||
|
lastError ? `: ${lastError.message}` : ''
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
if (!selected) throw new Error('Could not open a demo hexagon selection');
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
await ctx.page.mouse.click(target.x, target.y);
|
||||||
|
await sleep(140);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Export the current shortlist, then reveal the URL. */
|
/** Export the current shortlist, then reveal the URL. */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Page } from 'playwright';
|
import type { Page } from 'playwright';
|
||||||
import { installCursor, installZoomWrapper, waitForCurrentDemoMapSettled } from './dom.js';
|
import { installCursor, installZoomWrapper } from './dom.js';
|
||||||
|
import { DashboardRecorder } from './dashboard.js';
|
||||||
import { sleep } from './motion.js';
|
import { sleep } from './motion.js';
|
||||||
import { dashboardUrl } from './routes.js';
|
import { dashboardUrl } from './routes.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -18,19 +19,21 @@ export interface TimelineResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareTimeline(page: Page): Promise<SceneCtx> {
|
export async function prepareTimeline(page: Page): Promise<SceneCtx> {
|
||||||
|
const dashboard = new DashboardRecorder(page);
|
||||||
|
const initialMapVersion = dashboard.getMapDataVersion();
|
||||||
await page.goto(dashboardUrl(), { waitUntil: 'domcontentloaded' });
|
await page.goto(dashboardUrl(), { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForLoadState('load', { timeout: 15000 }).catch(() => {});
|
await page.waitForLoadState('load', { timeout: 15000 }).catch(() => {});
|
||||||
await page
|
await page
|
||||||
.locator('[data-tutorial="ai-filters"]')
|
.locator('[data-tutorial="ai-filters"]')
|
||||||
.waitFor({ state: 'visible', timeout: 15000 });
|
.waitFor({ state: 'visible', timeout: 15000 });
|
||||||
await page.locator('canvas').first().waitFor({ state: 'attached', timeout: 15000 });
|
await page.locator('canvas').first().waitFor({ state: 'attached', timeout: 15000 });
|
||||||
await waitForCurrentDemoMapSettled(page, 15000);
|
await dashboard.waitForMapSettled(initialMapVersion, 15000);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 400));
|
await new Promise((r) => setTimeout(r, 400));
|
||||||
await installZoomWrapper(page);
|
await installZoomWrapper(page);
|
||||||
await installCursor(page);
|
await installCursor(page);
|
||||||
|
|
||||||
const ctx: SceneCtx = { page, cursor: { x: 200, y: 240 } };
|
const ctx: SceneCtx = { page, dashboard, cursor: { x: 200, y: 240 } };
|
||||||
await page.mouse.move(ctx.cursor.x, ctx.cursor.y);
|
await page.mouse.move(ctx.cursor.x, ctx.cursor.y);
|
||||||
await prepareAiBox(ctx);
|
await prepareAiBox(ctx);
|
||||||
await sleep(80);
|
await sleep(80);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue