import type { Page } from "@playwright/test" export type NavigationMilestoneSample = { observedAtMs: number milestones: Record } export function summarizeNavigationMilestones(samples: NavigationMilestoneSample[]) { const names = Object.keys(samples[0]?.milestones ?? {}) const summarize = (matches: (sample: NavigationMilestoneSample) => boolean) => { const first = samples.find(matches) const stable = samples.findIndex( (sample, index) => index + 2 < samples.length && matches(sample) && matches(samples[index + 1]!) && matches(samples[index + 2]!), ) return { firstObservedMs: first?.observedAtMs ?? null, stableObservedMs: stable === -1 ? null : samples[stable + 2]!.observedAtMs, } } return { samples: samples.length, milestones: Object.fromEntries( names.map((name) => [name, summarize((sample) => sample.milestones[name] === true)]), ), all: summarize((sample) => names.every((name) => sample.milestones[name] === true)), } } type NavigationMilestoneProbe = { samples: NavigationMilestoneSample[] stop: () => void } export async function measureNavigationMilestones( page: Page, input: { triggerSelector: string milestones: Record navigate: () => Promise }, ) { await page.evaluate( ({ triggerSelector, milestones }) => { const samples: NavigationMilestoneSample[] = [] const streaks = new Map() const marked = new Set() let started: number | undefined let running = true const visible = (selector: string) => [...document.querySelectorAll(selector)].some((element) => { const rect = element.getBoundingClientRect() const style = getComputedStyle(element) return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" }) const sample = () => { if (!running || started === undefined) return requestAnimationFrame(() => { setTimeout(() => { if (!running || started === undefined) return const current = Object.fromEntries( Object.entries(milestones).map(([name, milestone]) => [ name, milestone.visible === false ? !document.querySelector(milestone.selector) : visible(milestone.selector), ]), ) samples.push({ observedAtMs: performance.now() - started, milestones: current, }) Object.entries(current).forEach(([name, value]) => { if (!value) { streaks.set(name, 0) return } if (!marked.has(`${name}.first`)) { performance.mark(`opencode.navigation.${name}.first`) marked.add(`${name}.first`) } const streak = (streaks.get(name) ?? 0) + 1 streaks.set(name, streak) if (streak === 3) performance.mark(`opencode.navigation.${name}.stable`) }) const all = Object.values(current).every(Boolean) const allStreak = all ? (streaks.get("all") ?? 0) + 1 : 0 streaks.set("all", allStreak) if (all && !marked.has("all.first")) { performance.mark("opencode.navigation.all.first") marked.add("all.first") } if (allStreak === 3) performance.mark("opencode.navigation.all.stable") sample() }, 0) }) } document.addEventListener( "click", (event) => { if (!(event.target instanceof Element) || !event.target.closest(triggerSelector)) return started = performance.now() performance.mark("opencode.navigation.click") sample() }, { capture: true, once: true }, ) ;(window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones = { samples, stop: () => { running = false }, } }, { triggerSelector: input.triggerSelector, milestones: input.milestones }, ) await input.navigate() await page.waitForFunction(() => { const samples = (window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones ?.samples if (!samples || samples.length < 3) return false return samples.slice(-3).every((sample) => Object.values(sample.milestones).every(Boolean)) }) const samples = await page.evaluate(() => { const probe = (window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones! probe.stop() return probe.samples }) return { summary: summarizeNavigationMilestones(samples), samples } }