first-navigation-probe.ts 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import type { Page } from "@playwright/test"
  2. import { summarizeFirstNavigation, type FirstNavigationSample } from "./first-navigation-metrics"
  3. type FirstNavigationProbe = {
  4. samples: FirstNavigationSample[]
  5. stop: () => void
  6. }
  7. export async function measureFirstNavigation(
  8. page: Page,
  9. input: {
  10. href: string
  11. destinationPath: string
  12. sourceSelector: string
  13. destinationSelector: string
  14. contentSelector: string
  15. navigate: () => Promise<void>
  16. },
  17. ) {
  18. await page.evaluate(
  19. ({ href, destinationPath, sourceSelector, destinationSelector, contentSelector }) => {
  20. const samples: FirstNavigationSample[] = []
  21. let started: number | undefined
  22. let running = true
  23. const visible = (selector: string) =>
  24. [...document.querySelectorAll<HTMLElement>(selector)].some((element) => {
  25. const rect = element.getBoundingClientRect()
  26. const style = getComputedStyle(element)
  27. return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"
  28. })
  29. const sample = () => {
  30. if (!running || started === undefined) return
  31. requestAnimationFrame(() => {
  32. setTimeout(() => {
  33. if (!running || started === undefined) return
  34. samples.push({
  35. observedAtMs: performance.now() - started,
  36. source: visible(sourceSelector),
  37. destination: `${location.pathname}${location.search}` === destinationPath && visible(destinationSelector),
  38. content: visible(contentSelector),
  39. pathname: `${location.pathname}${location.search}`,
  40. center: document.elementFromPoint(innerWidth / 2, innerHeight / 2)?.textContent?.slice(0, 80),
  41. })
  42. sample()
  43. }, 0)
  44. })
  45. }
  46. document.addEventListener(
  47. "click",
  48. (event) => {
  49. const link = event.target instanceof Element ? event.target.closest("a") : undefined
  50. if (link?.getAttribute("href") !== href) return
  51. started = performance.now()
  52. sample()
  53. },
  54. { capture: true, once: true },
  55. )
  56. ;(window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe = {
  57. samples,
  58. stop: () => {
  59. running = false
  60. },
  61. }
  62. },
  63. {
  64. href: input.href,
  65. destinationPath: input.destinationPath,
  66. sourceSelector: input.sourceSelector,
  67. destinationSelector: input.destinationSelector,
  68. contentSelector: input.contentSelector,
  69. },
  70. )
  71. await input.navigate()
  72. await page.waitForFunction(() => {
  73. const samples = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe
  74. ?.samples
  75. if (!samples) return false
  76. return samples.length >= 3 && samples.slice(-3).every((sample) => sample.destination && !sample.source)
  77. })
  78. const samples = await page.evaluate(() => {
  79. const probe = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe!
  80. probe.stop()
  81. return probe.samples
  82. })
  83. return { summary: summarizeFirstNavigation(samples), samples }
  84. }