File size: 3,188 Bytes
3e05655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import type { Page } from "@playwright/test"
import { summarizeFirstNavigation, type FirstNavigationSample } from "./first-navigation-metrics"

type FirstNavigationProbe = {
  samples: FirstNavigationSample[]
  stop: () => void
}

export async function measureFirstNavigation(
  page: Page,
  input: {
    href: string
    destinationPath: string
    sourceSelector: string
    destinationSelector: string
    contentSelector: string
    navigate: () => Promise<void>
  },
) {
  await page.evaluate(
    ({ href, destinationPath, sourceSelector, destinationSelector, contentSelector }) => {
      const samples: FirstNavigationSample[] = []
      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
            samples.push({
              observedAtMs: performance.now() - started,
              source: visible(sourceSelector),
              destination: `${location.pathname}${location.search}` === destinationPath && visible(destinationSelector),
              content: visible(contentSelector),
              pathname: `${location.pathname}${location.search}`,
              center: document.elementFromPoint(innerWidth / 2, innerHeight / 2)?.textContent?.slice(0, 80),
            })
            sample()
          }, 0)
        })
      }
      document.addEventListener(
        "click",
        (event) => {
          const link = event.target instanceof Element ? event.target.closest("a") : undefined
          if (link?.getAttribute("href") !== href) return
          started = performance.now()
          sample()
        },
        { capture: true, once: true },
      )
      ;(window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe = {
        samples,
        stop: () => {
          running = false
        },
      }
    },
    {
      href: input.href,
      destinationPath: input.destinationPath,
      sourceSelector: input.sourceSelector,
      destinationSelector: input.destinationSelector,
      contentSelector: input.contentSelector,
    },
  )
  await input.navigate()
  await page.waitForFunction(() => {
    const samples = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe
      ?.samples
    if (!samples) return false
    return samples.length >= 3 && samples.slice(-3).every((sample) => sample.destination && !sample.source)
  })
  const samples = await page.evaluate(() => {
    const probe = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe!
    probe.stop()
    return probe.samples
  })
  return { summary: summarizeFirstNavigation(samples), samples }
}