| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- import type { Page } from "@playwright/test"
- export type NavigationMilestoneSample = {
- observedAtMs: number
- milestones: Record<string, boolean>
- }
- 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<string, { selector: string; visible?: boolean }>
- navigate: () => Promise<void>
- },
- ) {
- await page.evaluate(
- ({ triggerSelector, milestones }) => {
- const samples: NavigationMilestoneSample[] = []
- const streaks = new Map<string, number>()
- const marked = new Set<string>()
- let started: number | undefined
- let running = true
- const visible = (selector: string) =>
- [...document.querySelectorAll<HTMLElement>(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 }
- }
|