@jitsusama/agentic-harness.core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (974) hide show
  1. package/README.md +52 -0
  2. package/dist/advisor/charter.d.ts +14 -0
  3. package/dist/advisor/charter.js +48 -0
  4. package/dist/advisor/findings.d.ts +43 -0
  5. package/dist/advisor/findings.js +82 -0
  6. package/dist/advisor/index.d.ts +24 -0
  7. package/dist/advisor/index.js +23 -0
  8. package/dist/advisor/settings.d.ts +16 -0
  9. package/dist/advisor/settings.js +30 -0
  10. package/dist/advisor/substantive.d.ts +14 -0
  11. package/dist/advisor/substantive.js +33 -0
  12. package/dist/advisor/tools.d.ts +38 -0
  13. package/dist/advisor/tools.js +202 -0
  14. package/dist/attribution/commit-hook.d.ts +53 -0
  15. package/dist/attribution/commit-hook.js +133 -0
  16. package/dist/attribution/gh-footer.d.ts +27 -0
  17. package/dist/attribution/gh-footer.js +80 -0
  18. package/dist/attribution/index.d.ts +11 -0
  19. package/dist/attribution/index.js +11 -0
  20. package/dist/attribution/model-name.d.ts +11 -0
  21. package/dist/attribution/model-name.js +26 -0
  22. package/dist/bin/attribution.d.ts +36 -0
  23. package/dist/bin/attribution.js +82 -0
  24. package/dist/bin/cli.d.ts +14 -0
  25. package/dist/bin/cli.js +236 -0
  26. package/dist/bin/gate-deps.d.ts +23 -0
  27. package/dist/bin/gate-deps.js +61 -0
  28. package/dist/bin/guardians.d.ts +51 -0
  29. package/dist/bin/guardians.js +174 -0
  30. package/dist/bin/memory.d.ts +16 -0
  31. package/dist/bin/memory.js +81 -0
  32. package/dist/bin/notes.d.ts +30 -0
  33. package/dist/bin/notes.js +67 -0
  34. package/dist/bin/process-exec.d.ts +14 -0
  35. package/dist/bin/process-exec.js +31 -0
  36. package/dist/bin/quest.d.ts +38 -0
  37. package/dist/bin/quest.js +187 -0
  38. package/dist/bin/slack-auth.d.ts +49 -0
  39. package/dist/bin/slack-auth.js +107 -0
  40. package/dist/clock/index.d.ts +46 -0
  41. package/dist/clock/index.js +72 -0
  42. package/dist/command/cwd.d.ts +21 -0
  43. package/dist/command/cwd.js +60 -0
  44. package/dist/command/edit.d.ts +14 -0
  45. package/dist/command/edit.js +22 -0
  46. package/dist/command/flags.d.ts +29 -0
  47. package/dist/command/flags.js +111 -0
  48. package/dist/command/index.d.ts +16 -0
  49. package/dist/command/index.js +12 -0
  50. package/dist/command/tokenize.d.ts +7 -0
  51. package/dist/command/tokenize.js +315 -0
  52. package/dist/command/types.d.ts +66 -0
  53. package/dist/command/types.js +9 -0
  54. package/dist/completion/index.d.ts +17 -0
  55. package/dist/completion/index.js +16 -0
  56. package/dist/completion/investigate.d.ts +63 -0
  57. package/dist/completion/investigate.js +198 -0
  58. package/dist/completion/resolve.d.ts +29 -0
  59. package/dist/completion/resolve.js +42 -0
  60. package/dist/completion/side.d.ts +51 -0
  61. package/dist/completion/side.js +93 -0
  62. package/dist/completion/types.d.ts +84 -0
  63. package/dist/completion/types.js +14 -0
  64. package/dist/exec/exec.d.ts +38 -0
  65. package/dist/exec/exec.js +51 -0
  66. package/dist/exec/index.d.ts +8 -0
  67. package/dist/exec/index.js +8 -0
  68. package/dist/gate/decision.d.ts +52 -0
  69. package/dist/gate/decision.js +68 -0
  70. package/dist/gate/deps.d.ts +14 -0
  71. package/dist/gate/deps.js +1 -0
  72. package/dist/gate/index.d.ts +2 -0
  73. package/dist/gate/index.js +1 -0
  74. package/dist/git-cli/index.d.ts +47 -0
  75. package/dist/git-cli/index.js +127 -0
  76. package/dist/github-cli/command-spec.d.ts +14 -0
  77. package/dist/github-cli/command-spec.js +18 -0
  78. package/dist/github-cli/index.d.ts +71 -0
  79. package/dist/github-cli/index.js +188 -0
  80. package/dist/google/apis/calendar.d.ts +64 -0
  81. package/dist/google/apis/calendar.js +297 -0
  82. package/dist/google/apis/docs.d.ts +29 -0
  83. package/dist/google/apis/docs.js +154 -0
  84. package/dist/google/apis/drive.d.ts +46 -0
  85. package/dist/google/apis/drive.js +189 -0
  86. package/dist/google/apis/gmail.d.ts +67 -0
  87. package/dist/google/apis/gmail.js +374 -0
  88. package/dist/google/apis/index.d.ts +10 -0
  89. package/dist/google/apis/index.js +10 -0
  90. package/dist/google/apis/sheets.d.ts +16 -0
  91. package/dist/google/apis/sheets.js +54 -0
  92. package/dist/google/apis/slides.d.ts +16 -0
  93. package/dist/google/apis/slides.js +40 -0
  94. package/dist/google/auth/browser.d.ts +5 -0
  95. package/dist/google/auth/browser.js +27 -0
  96. package/dist/google/auth/credentials.d.ts +26 -0
  97. package/dist/google/auth/credentials.js +69 -0
  98. package/dist/google/auth/errors.d.ts +15 -0
  99. package/dist/google/auth/errors.js +26 -0
  100. package/dist/google/auth/index.d.ts +12 -0
  101. package/dist/google/auth/index.js +12 -0
  102. package/dist/google/auth/oauth-app.d.ts +15 -0
  103. package/dist/google/auth/oauth-app.js +20 -0
  104. package/dist/google/auth/oauth.d.ts +46 -0
  105. package/dist/google/auth/oauth.js +146 -0
  106. package/dist/google/auth/server.d.ts +13 -0
  107. package/dist/google/auth/server.js +70 -0
  108. package/dist/google/auth/setup-instructions.d.ts +16 -0
  109. package/dist/google/auth/setup-instructions.js +24 -0
  110. package/dist/google/auth/store.d.ts +19 -0
  111. package/dist/google/auth/store.js +27 -0
  112. package/dist/google/index.d.ts +13 -0
  113. package/dist/google/index.js +12 -0
  114. package/dist/google/paths.d.ts +17 -0
  115. package/dist/google/paths.js +28 -0
  116. package/dist/google/renderers/calendar.d.ts +23 -0
  117. package/dist/google/renderers/calendar.js +312 -0
  118. package/dist/google/renderers/drive.d.ts +31 -0
  119. package/dist/google/renderers/drive.js +214 -0
  120. package/dist/google/renderers/email.d.ts +16 -0
  121. package/dist/google/renderers/email.js +130 -0
  122. package/dist/google/renderers/index.d.ts +8 -0
  123. package/dist/google/renderers/index.js +8 -0
  124. package/dist/google/types.d.ts +143 -0
  125. package/dist/google/types.js +27 -0
  126. package/dist/governance/distill.d.ts +31 -0
  127. package/dist/governance/distill.js +78 -0
  128. package/dist/governance/index.d.ts +11 -0
  129. package/dist/governance/index.js +10 -0
  130. package/dist/governance/render.d.ts +13 -0
  131. package/dist/governance/render.js +23 -0
  132. package/dist/governance/store.d.ts +32 -0
  133. package/dist/governance/store.js +102 -0
  134. package/dist/governance/types.d.ts +19 -0
  135. package/dist/governance/types.js +9 -0
  136. package/dist/guardian/index.d.ts +7 -0
  137. package/dist/guardian/index.js +7 -0
  138. package/dist/guardian/types.d.ts +37 -0
  139. package/dist/guardian/types.js +12 -0
  140. package/dist/internal/github/cli.d.ts +40 -0
  141. package/dist/internal/github/cli.js +102 -0
  142. package/dist/internal/guardian/commit-file.d.ts +7 -0
  143. package/dist/internal/guardian/commit-file.js +36 -0
  144. package/dist/internal/guardian/commit-format.d.ts +35 -0
  145. package/dist/internal/guardian/commit-format.js +63 -0
  146. package/dist/internal/guardian/commit-shell.d.ts +38 -0
  147. package/dist/internal/guardian/commit-shell.js +134 -0
  148. package/dist/internal/guardian/history-gate.d.ts +41 -0
  149. package/dist/internal/guardian/history-gate.js +76 -0
  150. package/dist/internal/guardian/prose-gate.d.ts +31 -0
  151. package/dist/internal/guardian/prose-gate.js +50 -0
  152. package/dist/internal/guardian/redirect-gate.d.ts +41 -0
  153. package/dist/internal/guardian/redirect-gate.js +114 -0
  154. package/dist/internal/guardian/section-gate.d.ts +20 -0
  155. package/dist/internal/guardian/section-gate.js +36 -0
  156. package/dist/internal/guardian/title-gate.d.ts +20 -0
  157. package/dist/internal/guardian/title-gate.js +36 -0
  158. package/dist/internal/json-array.d.ts +11 -0
  159. package/dist/internal/json-array.js +40 -0
  160. package/dist/internal/lsp/registry.d.ts +12 -0
  161. package/dist/internal/lsp/registry.js +16 -0
  162. package/dist/internal/notes/discovery.d.ts +59 -0
  163. package/dist/internal/notes/discovery.js +91 -0
  164. package/dist/internal/notes/frontmatter.d.ts +52 -0
  165. package/dist/internal/notes/frontmatter.js +163 -0
  166. package/dist/internal/notes/id.d.ts +42 -0
  167. package/dist/internal/notes/id.js +80 -0
  168. package/dist/internal/notes/io.d.ts +25 -0
  169. package/dist/internal/notes/io.js +51 -0
  170. package/dist/internal/notes/sanitize.d.ts +19 -0
  171. package/dist/internal/notes/sanitize.js +35 -0
  172. package/dist/internal/process-global.d.ts +20 -0
  173. package/dist/internal/process-global.js +28 -0
  174. package/dist/internal/quest/alias-index.d.ts +61 -0
  175. package/dist/internal/quest/alias-index.js +69 -0
  176. package/dist/internal/quest/append-journey.d.ts +22 -0
  177. package/dist/internal/quest/append-journey.js +63 -0
  178. package/dist/internal/quest/bash-write.d.ts +49 -0
  179. package/dist/internal/quest/bash-write.js +208 -0
  180. package/dist/internal/quest/dates.d.ts +14 -0
  181. package/dist/internal/quest/dates.js +20 -0
  182. package/dist/internal/quest/discovery.d.ts +96 -0
  183. package/dist/internal/quest/discovery.js +354 -0
  184. package/dist/internal/quest/fields.d.ts +37 -0
  185. package/dist/internal/quest/fields.js +94 -0
  186. package/dist/internal/quest/frontmatter.d.ts +88 -0
  187. package/dist/internal/quest/frontmatter.js +630 -0
  188. package/dist/internal/quest/git-signals.d.ts +53 -0
  189. package/dist/internal/quest/git-signals.js +192 -0
  190. package/dist/internal/quest/id.d.ts +50 -0
  191. package/dist/internal/quest/id.js +116 -0
  192. package/dist/internal/quest/io.d.ts +54 -0
  193. package/dist/internal/quest/io.js +233 -0
  194. package/dist/internal/quest/mentions.d.ts +45 -0
  195. package/dist/internal/quest/mentions.js +70 -0
  196. package/dist/internal/quest/mutate.d.ts +38 -0
  197. package/dist/internal/quest/mutate.js +83 -0
  198. package/dist/internal/quest/process-liveness.d.ts +98 -0
  199. package/dist/internal/quest/process-liveness.js +216 -0
  200. package/dist/internal/quest/quest-doc.d.ts +116 -0
  201. package/dist/internal/quest/quest-doc.js +239 -0
  202. package/dist/internal/quest/ranking.d.ts +48 -0
  203. package/dist/internal/quest/ranking.js +114 -0
  204. package/dist/internal/quest/reopen.d.ts +65 -0
  205. package/dist/internal/quest/reopen.js +113 -0
  206. package/dist/internal/quest/resolve-cwd.d.ts +32 -0
  207. package/dist/internal/quest/resolve-cwd.js +89 -0
  208. package/dist/internal/quest/sanitize.d.ts +42 -0
  209. package/dist/internal/quest/sanitize.js +69 -0
  210. package/dist/internal/quest/scaffold.d.ts +59 -0
  211. package/dist/internal/quest/scaffold.js +169 -0
  212. package/dist/internal/quest/scratch.d.ts +27 -0
  213. package/dist/internal/quest/scratch.js +106 -0
  214. package/dist/internal/quest/session-liveness.d.ts +125 -0
  215. package/dist/internal/quest/session-liveness.js +386 -0
  216. package/dist/internal/quest/session-ownership.d.ts +22 -0
  217. package/dist/internal/quest/session-ownership.js +100 -0
  218. package/dist/internal/quest/session-registry.d.ts +221 -0
  219. package/dist/internal/quest/session-registry.js +266 -0
  220. package/dist/internal/quest/status.d.ts +10 -0
  221. package/dist/internal/quest/status.js +15 -0
  222. package/dist/internal/quest/structural-journal.d.ts +41 -0
  223. package/dist/internal/quest/structural-journal.js +67 -0
  224. package/dist/internal/quest/structural.d.ts +43 -0
  225. package/dist/internal/quest/structural.js +77 -0
  226. package/dist/internal/quest/toc.d.ts +28 -0
  227. package/dist/internal/quest/toc.js +90 -0
  228. package/dist/internal/quest/trees.d.ts +53 -0
  229. package/dist/internal/quest/trees.js +145 -0
  230. package/dist/internal/quest/url-fetchers.d.ts +50 -0
  231. package/dist/internal/quest/url-fetchers.js +97 -0
  232. package/dist/internal/quest/write-classifier.d.ts +46 -0
  233. package/dist/internal/quest/write-classifier.js +59 -0
  234. package/dist/internal/refs/builtins.d.ts +32 -0
  235. package/dist/internal/refs/builtins.js +179 -0
  236. package/dist/internal/refs/registry.d.ts +25 -0
  237. package/dist/internal/refs/registry.js +42 -0
  238. package/dist/internal/registry/global-symbol-registry.d.ts +37 -0
  239. package/dist/internal/registry/global-symbol-registry.js +55 -0
  240. package/dist/internal/review/registry.d.ts +12 -0
  241. package/dist/internal/review/registry.js +16 -0
  242. package/dist/internal/sqlite/db.d.ts +16 -0
  243. package/dist/internal/sqlite/db.js +26 -0
  244. package/dist/internal/terminal/builtins.d.ts +8 -0
  245. package/dist/internal/terminal/builtins.js +14 -0
  246. package/dist/internal/terminal/drivers/fallback.d.ts +8 -0
  247. package/dist/internal/terminal/drivers/fallback.js +31 -0
  248. package/dist/internal/terminal/drivers/shared.d.ts +39 -0
  249. package/dist/internal/terminal/drivers/shared.js +51 -0
  250. package/dist/internal/terminal/drivers/tmux.d.ts +14 -0
  251. package/dist/internal/terminal/drivers/tmux.js +88 -0
  252. package/dist/internal/terminal/drivers/wezterm.d.ts +35 -0
  253. package/dist/internal/terminal/drivers/wezterm.js +175 -0
  254. package/dist/internal/terminal/registry.d.ts +12 -0
  255. package/dist/internal/terminal/registry.js +16 -0
  256. package/dist/internal/tree/builtins.d.ts +8 -0
  257. package/dist/internal/tree/builtins.js +10 -0
  258. package/dist/internal/tree/providers/git-worktree.d.ts +20 -0
  259. package/dist/internal/tree/providers/git-worktree.js +279 -0
  260. package/dist/internal/tree/registry.d.ts +12 -0
  261. package/dist/internal/tree/registry.js +16 -0
  262. package/dist/lsp/config.d.ts +77 -0
  263. package/dist/lsp/config.js +154 -0
  264. package/dist/lsp/external.d.ts +13 -0
  265. package/dist/lsp/external.js +52 -0
  266. package/dist/lsp/format.d.ts +11 -0
  267. package/dist/lsp/format.js +54 -0
  268. package/dist/lsp/index.d.ts +17 -0
  269. package/dist/lsp/index.js +16 -0
  270. package/dist/lsp/offsets.d.ts +22 -0
  271. package/dist/lsp/offsets.js +51 -0
  272. package/dist/lsp/registry.d.ts +28 -0
  273. package/dist/lsp/registry.js +45 -0
  274. package/dist/lsp/standalone/backend.d.ts +42 -0
  275. package/dist/lsp/standalone/backend.js +150 -0
  276. package/dist/lsp/standalone/document.d.ts +45 -0
  277. package/dist/lsp/standalone/document.js +81 -0
  278. package/dist/lsp/standalone/server.d.ts +84 -0
  279. package/dist/lsp/standalone/server.js +521 -0
  280. package/dist/lsp/types.d.ts +119 -0
  281. package/dist/lsp/types.js +13 -0
  282. package/dist/memory/db.d.ts +15 -0
  283. package/dist/memory/db.js +25 -0
  284. package/dist/memory/index.d.ts +12 -0
  285. package/dist/memory/index.js +11 -0
  286. package/dist/memory/paths.d.ts +19 -0
  287. package/dist/memory/paths.js +33 -0
  288. package/dist/memory/scope.d.ts +14 -0
  289. package/dist/memory/scope.js +22 -0
  290. package/dist/memory/store.d.ts +12 -0
  291. package/dist/memory/store.js +146 -0
  292. package/dist/memory/types.d.ts +80 -0
  293. package/dist/memory/types.js +6 -0
  294. package/dist/notes/types.d.ts +50 -0
  295. package/dist/notes/types.js +23 -0
  296. package/dist/notes/verbs/create.d.ts +5 -0
  297. package/dist/notes/verbs/create.js +55 -0
  298. package/dist/notes/verbs/mutate.d.ts +8 -0
  299. package/dist/notes/verbs/mutate.js +152 -0
  300. package/dist/notes/verbs/queries.d.ts +15 -0
  301. package/dist/notes/verbs/queries.js +116 -0
  302. package/dist/notes/verbs/reindex.d.ts +12 -0
  303. package/dist/notes/verbs/reindex.js +35 -0
  304. package/dist/notes/verbs/shared.d.ts +37 -0
  305. package/dist/notes/verbs/shared.js +29 -0
  306. package/dist/observability/index.d.ts +13 -0
  307. package/dist/observability/index.js +12 -0
  308. package/dist/observability/recorder.d.ts +50 -0
  309. package/dist/observability/recorder.js +77 -0
  310. package/dist/observability/store.d.ts +22 -0
  311. package/dist/observability/store.js +213 -0
  312. package/dist/observability/types.d.ts +82 -0
  313. package/dist/observability/types.js +1 -0
  314. package/dist/process/index.d.ts +9 -0
  315. package/dist/process/index.js +9 -0
  316. package/dist/process/process.d.ts +95 -0
  317. package/dist/process/process.js +153 -0
  318. package/dist/prose/block.d.ts +9 -0
  319. package/dist/prose/block.js +43 -0
  320. package/dist/prose/detect.d.ts +50 -0
  321. package/dist/prose/detect.js +217 -0
  322. package/dist/prose/gate.d.ts +14 -0
  323. package/dist/prose/gate.js +21 -0
  324. package/dist/prose/index.d.ts +4 -0
  325. package/dist/prose/index.js +4 -0
  326. package/dist/prose/wrap.d.ts +9 -0
  327. package/dist/prose/wrap.js +47 -0
  328. package/dist/quest/actions.d.ts +20 -0
  329. package/dist/quest/actions.js +107 -0
  330. package/dist/quest/config.d.ts +79 -0
  331. package/dist/quest/config.js +83 -0
  332. package/dist/quest/index.d.ts +29 -0
  333. package/dist/quest/index.js +28 -0
  334. package/dist/quest/lifecycle.d.ts +365 -0
  335. package/dist/quest/lifecycle.js +900 -0
  336. package/dist/quest/lookup.d.ts +243 -0
  337. package/dist/quest/lookup.js +496 -0
  338. package/dist/quest/machine.d.ts +48 -0
  339. package/dist/quest/machine.js +96 -0
  340. package/dist/quest/render-rows.d.ts +166 -0
  341. package/dist/quest/render-rows.js +214 -0
  342. package/dist/quest/state.d.ts +111 -0
  343. package/dist/quest/state.js +46 -0
  344. package/dist/quest/types.d.ts +224 -0
  345. package/dist/quest/types.js +10 -0
  346. package/dist/quest/verbs/alias.d.ts +9 -0
  347. package/dist/quest/verbs/alias.js +129 -0
  348. package/dist/quest/verbs/lifecycle.d.ts +36 -0
  349. package/dist/quest/verbs/lifecycle.js +407 -0
  350. package/dist/quest/verbs/queries.d.ts +24 -0
  351. package/dist/quest/verbs/queries.js +241 -0
  352. package/dist/quest/verbs/reorder.d.ts +15 -0
  353. package/dist/quest/verbs/reorder.js +85 -0
  354. package/dist/quest/verbs/shared.d.ts +63 -0
  355. package/dist/quest/verbs/shared.js +35 -0
  356. package/dist/quest/verbs/stage.d.ts +39 -0
  357. package/dist/quest/verbs/stage.js +506 -0
  358. package/dist/quest/verbs/structural.d.ts +23 -0
  359. package/dist/quest/verbs/structural.js +310 -0
  360. package/dist/quest/verbs/tree-ops.d.ts +20 -0
  361. package/dist/quest/verbs/tree-ops.js +289 -0
  362. package/dist/refs/index.d.ts +13 -0
  363. package/dist/refs/index.js +12 -0
  364. package/dist/refs/lookup.d.ts +53 -0
  365. package/dist/refs/lookup.js +102 -0
  366. package/dist/refs/register.d.ts +34 -0
  367. package/dist/refs/register.js +44 -0
  368. package/dist/refs/types.d.ts +81 -0
  369. package/dist/refs/types.js +17 -0
  370. package/dist/remote/index.d.ts +9 -0
  371. package/dist/remote/index.js +9 -0
  372. package/dist/remote/name.d.ts +22 -0
  373. package/dist/remote/name.js +30 -0
  374. package/dist/result/cite.d.ts +84 -0
  375. package/dist/result/cite.js +127 -0
  376. package/dist/result/counts.d.ts +15 -0
  377. package/dist/result/counts.js +17 -0
  378. package/dist/result/details.d.ts +61 -0
  379. package/dist/result/details.js +121 -0
  380. package/dist/result/digest.d.ts +37 -0
  381. package/dist/result/digest.js +227 -0
  382. package/dist/result/follow.d.ts +29 -0
  383. package/dist/result/follow.js +37 -0
  384. package/dist/result/index.d.ts +26 -0
  385. package/dist/result/index.js +26 -0
  386. package/dist/result/listing.d.ts +62 -0
  387. package/dist/result/listing.js +72 -0
  388. package/dist/result/location.d.ts +67 -0
  389. package/dist/result/location.js +125 -0
  390. package/dist/result/query.d.ts +67 -0
  391. package/dist/result/query.js +144 -0
  392. package/dist/result/spill.d.ts +41 -0
  393. package/dist/result/spill.js +121 -0
  394. package/dist/result/store.d.ts +83 -0
  395. package/dist/result/store.js +214 -0
  396. package/dist/result/view.d.ts +31 -0
  397. package/dist/result/view.js +38 -0
  398. package/dist/review/anchor.d.ts +84 -0
  399. package/dist/review/anchor.js +89 -0
  400. package/dist/review/ask/agents.d.ts +80 -0
  401. package/dist/review/ask/agents.js +296 -0
  402. package/dist/review/ask/anchorable.d.ts +35 -0
  403. package/dist/review/ask/anchorable.js +76 -0
  404. package/dist/review/ask/answer.d.ts +54 -0
  405. package/dist/review/ask/answer.js +209 -0
  406. package/dist/review/ask/audit.d.ts +71 -0
  407. package/dist/review/ask/audit.js +158 -0
  408. package/dist/review/ask/collect.d.ts +41 -0
  409. package/dist/review/ask/collect.js +82 -0
  410. package/dist/review/ask/council.d.ts +290 -0
  411. package/dist/review/ask/council.js +386 -0
  412. package/dist/review/ask/critique.d.ts +69 -0
  413. package/dist/review/ask/critique.js +157 -0
  414. package/dist/review/ask/harvest.d.ts +119 -0
  415. package/dist/review/ask/harvest.js +409 -0
  416. package/dist/review/ask/identity.d.ts +93 -0
  417. package/dist/review/ask/identity.js +134 -0
  418. package/dist/review/ask/judge.d.ts +37 -0
  419. package/dist/review/ask/judge.js +140 -0
  420. package/dist/review/ask/persona.d.ts +84 -0
  421. package/dist/review/ask/persona.js +127 -0
  422. package/dist/review/ask/progress.d.ts +117 -0
  423. package/dist/review/ask/progress.js +137 -0
  424. package/dist/review/ask/prompt.d.ts +131 -0
  425. package/dist/review/ask/prompt.js +242 -0
  426. package/dist/review/ask/roster.d.ts +94 -0
  427. package/dist/review/ask/roster.js +338 -0
  428. package/dist/review/ask/run.d.ts +435 -0
  429. package/dist/review/ask/run.js +340 -0
  430. package/dist/review/ask/span.d.ts +70 -0
  431. package/dist/review/ask/span.js +166 -0
  432. package/dist/review/ask/stack-round.d.ts +66 -0
  433. package/dist/review/ask/stack-round.js +125 -0
  434. package/dist/review/ask/start.d.ts +48 -0
  435. package/dist/review/ask/start.js +118 -0
  436. package/dist/review/ask/store.d.ts +83 -0
  437. package/dist/review/ask/store.js +226 -0
  438. package/dist/review/ask/wire.d.ts +89 -0
  439. package/dist/review/ask/wire.js +204 -0
  440. package/dist/review/attach.d.ts +161 -0
  441. package/dist/review/attach.js +357 -0
  442. package/dist/review/authoring.d.ts +84 -0
  443. package/dist/review/authoring.js +203 -0
  444. package/dist/review/backed.d.ts +90 -0
  445. package/dist/review/backed.js +168 -0
  446. package/dist/review/bind.d.ts +75 -0
  447. package/dist/review/bind.js +223 -0
  448. package/dist/review/budget.d.ts +133 -0
  449. package/dist/review/budget.js +278 -0
  450. package/dist/review/capabilities.d.ts +217 -0
  451. package/dist/review/capabilities.js +46 -0
  452. package/dist/review/change.d.ts +155 -0
  453. package/dist/review/change.js +15 -0
  454. package/dist/review/checks.d.ts +56 -0
  455. package/dist/review/checks.js +11 -0
  456. package/dist/review/config.d.ts +71 -0
  457. package/dist/review/config.js +125 -0
  458. package/dist/review/conventions.d.ts +35 -0
  459. package/dist/review/conventions.js +58 -0
  460. package/dist/review/conversation.d.ts +98 -0
  461. package/dist/review/conversation.js +16 -0
  462. package/dist/review/decided.d.ts +58 -0
  463. package/dist/review/decided.js +67 -0
  464. package/dist/review/diff.d.ts +116 -0
  465. package/dist/review/diff.js +237 -0
  466. package/dist/review/draft/continuation.d.ts +23 -0
  467. package/dist/review/draft/continuation.js +32 -0
  468. package/dist/review/draft/fanout.d.ts +57 -0
  469. package/dist/review/draft/fanout.js +46 -0
  470. package/dist/review/draft/handle.d.ts +55 -0
  471. package/dist/review/draft/handle.js +110 -0
  472. package/dist/review/draft/plan.d.ts +112 -0
  473. package/dist/review/draft/plan.js +343 -0
  474. package/dist/review/draft/publish.d.ts +38 -0
  475. package/dist/review/draft/publish.js +118 -0
  476. package/dist/review/draft/render.d.ts +32 -0
  477. package/dist/review/draft/render.js +100 -0
  478. package/dist/review/draft/state.d.ts +98 -0
  479. package/dist/review/draft/state.js +83 -0
  480. package/dist/review/draft/store.d.ts +33 -0
  481. package/dist/review/draft/store.js +100 -0
  482. package/dist/review/elsewhere.d.ts +27 -0
  483. package/dist/review/elsewhere.js +39 -0
  484. package/dist/review/engine.d.ts +106 -0
  485. package/dist/review/engine.js +243 -0
  486. package/dist/review/events.d.ts +59 -0
  487. package/dist/review/events.js +35 -0
  488. package/dist/review/failed.d.ts +46 -0
  489. package/dist/review/failed.js +56 -0
  490. package/dist/review/finding.d.ts +81 -0
  491. package/dist/review/finding.js +85 -0
  492. package/dist/review/fix.d.ts +147 -0
  493. package/dist/review/fix.js +175 -0
  494. package/dist/review/followup.d.ts +84 -0
  495. package/dist/review/followup.js +123 -0
  496. package/dist/review/index.d.ts +134 -0
  497. package/dist/review/index.js +80 -0
  498. package/dist/review/keys.d.ts +23 -0
  499. package/dist/review/keys.js +40 -0
  500. package/dist/review/landing.d.ts +37 -0
  501. package/dist/review/landing.js +68 -0
  502. package/dist/review/lenses.d.ts +132 -0
  503. package/dist/review/lenses.js +430 -0
  504. package/dist/review/propose-from.d.ts +76 -0
  505. package/dist/review/propose-from.js +86 -0
  506. package/dist/review/provider.d.ts +306 -0
  507. package/dist/review/provider.js +16 -0
  508. package/dist/review/providers/git/index.d.ts +23 -0
  509. package/dist/review/providers/git/index.js +214 -0
  510. package/dist/review/providers/github/authoring.d.ts +22 -0
  511. package/dist/review/providers/github/authoring.js +350 -0
  512. package/dist/review/providers/github/claims.d.ts +49 -0
  513. package/dist/review/providers/github/claims.js +136 -0
  514. package/dist/review/providers/github/conversation.d.ts +18 -0
  515. package/dist/review/providers/github/conversation.js +455 -0
  516. package/dist/review/providers/github/fields.d.ts +30 -0
  517. package/dist/review/providers/github/fields.js +47 -0
  518. package/dist/review/providers/github/index.d.ts +15 -0
  519. package/dist/review/providers/github/index.js +110 -0
  520. package/dist/review/providers/github/proposals.d.ts +14 -0
  521. package/dist/review/providers/github/proposals.js +298 -0
  522. package/dist/review/providers/github/queue.d.ts +46 -0
  523. package/dist/review/providers/github/queue.js +122 -0
  524. package/dist/review/providers/github/stacking.d.ts +20 -0
  525. package/dist/review/providers/github/stacking.js +107 -0
  526. package/dist/review/queue.d.ts +43 -0
  527. package/dist/review/queue.js +56 -0
  528. package/dist/review/reactable.d.ts +93 -0
  529. package/dist/review/reactable.js +140 -0
  530. package/dist/review/register.d.ts +52 -0
  531. package/dist/review/register.js +53 -0
  532. package/dist/review/resolve.d.ts +58 -0
  533. package/dist/review/resolve.js +150 -0
  534. package/dist/review/retarget.d.ts +73 -0
  535. package/dist/review/retarget.js +82 -0
  536. package/dist/review/revisited.d.ts +77 -0
  537. package/dist/review/revisited.js +121 -0
  538. package/dist/review/stack.d.ts +110 -0
  539. package/dist/review/stack.js +52 -0
  540. package/dist/sections/block.d.ts +9 -0
  541. package/dist/sections/block.js +42 -0
  542. package/dist/sections/detect.d.ts +31 -0
  543. package/dist/sections/detect.js +67 -0
  544. package/dist/sections/gate.d.ts +21 -0
  545. package/dist/sections/gate.js +22 -0
  546. package/dist/sections/index.d.ts +5 -0
  547. package/dist/sections/index.js +5 -0
  548. package/dist/sections/sanctioned.d.ts +13 -0
  549. package/dist/sections/sanctioned.js +21 -0
  550. package/dist/shell/index.d.ts +8 -0
  551. package/dist/shell/index.js +8 -0
  552. package/dist/shell/parse.d.ts +125 -0
  553. package/dist/shell/parse.js +298 -0
  554. package/dist/slack/api/channels.d.ts +12 -0
  555. package/dist/slack/api/channels.js +28 -0
  556. package/dist/slack/api/client.d.ts +94 -0
  557. package/dist/slack/api/client.js +272 -0
  558. package/dist/slack/api/files.d.ts +95 -0
  559. package/dist/slack/api/files.js +223 -0
  560. package/dist/slack/api/index.d.ts +11 -0
  561. package/dist/slack/api/index.js +11 -0
  562. package/dist/slack/api/messages.d.ts +98 -0
  563. package/dist/slack/api/messages.js +292 -0
  564. package/dist/slack/api/reactions.d.ts +47 -0
  565. package/dist/slack/api/reactions.js +50 -0
  566. package/dist/slack/api/resolve-conversations.d.ts +43 -0
  567. package/dist/slack/api/resolve-conversations.js +130 -0
  568. package/dist/slack/api/resolve-messages.d.ts +29 -0
  569. package/dist/slack/api/resolve-messages.js +93 -0
  570. package/dist/slack/api/resolve-users.d.ts +27 -0
  571. package/dist/slack/api/resolve-users.js +73 -0
  572. package/dist/slack/api/search.d.ts +86 -0
  573. package/dist/slack/api/search.js +186 -0
  574. package/dist/slack/api/users.d.ts +12 -0
  575. package/dist/slack/api/users.js +33 -0
  576. package/dist/slack/auth/browser-extract.d.ts +33 -0
  577. package/dist/slack/auth/browser-extract.js +201 -0
  578. package/dist/slack/auth/browser.d.ts +5 -0
  579. package/dist/slack/auth/browser.js +25 -0
  580. package/dist/slack/auth/credentials.d.ts +14 -0
  581. package/dist/slack/auth/credentials.js +24 -0
  582. package/dist/slack/auth/errors.d.ts +9 -0
  583. package/dist/slack/auth/errors.js +19 -0
  584. package/dist/slack/auth/extract.d.ts +23 -0
  585. package/dist/slack/auth/extract.js +44 -0
  586. package/dist/slack/auth/index.d.ts +12 -0
  587. package/dist/slack/auth/index.js +11 -0
  588. package/dist/slack/auth/oauth-app.d.ts +11 -0
  589. package/dist/slack/auth/oauth-app.js +20 -0
  590. package/dist/slack/auth/oauth.d.ts +31 -0
  591. package/dist/slack/auth/oauth.js +82 -0
  592. package/dist/slack/auth/server.d.ts +14 -0
  593. package/dist/slack/auth/server.js +75 -0
  594. package/dist/slack/auth/store.d.ts +17 -0
  595. package/dist/slack/auth/store.js +27 -0
  596. package/dist/slack/block-message.d.ts +9 -0
  597. package/dist/slack/block-message.js +30 -0
  598. package/dist/slack/blocks.d.ts +94 -0
  599. package/dist/slack/blocks.js +765 -0
  600. package/dist/slack/content-gate.d.ts +12 -0
  601. package/dist/slack/content-gate.js +22 -0
  602. package/dist/slack/detect.d.ts +31 -0
  603. package/dist/slack/detect.js +125 -0
  604. package/dist/slack/index.d.ts +18 -0
  605. package/dist/slack/index.js +17 -0
  606. package/dist/slack/paths.d.ts +28 -0
  607. package/dist/slack/paths.js +46 -0
  608. package/dist/slack/renderers/channel.d.ts +6 -0
  609. package/dist/slack/renderers/channel.js +27 -0
  610. package/dist/slack/renderers/index.d.ts +9 -0
  611. package/dist/slack/renderers/index.js +9 -0
  612. package/dist/slack/renderers/message.d.ts +21 -0
  613. package/dist/slack/renderers/message.js +214 -0
  614. package/dist/slack/renderers/reactions.d.ts +8 -0
  615. package/dist/slack/renderers/reactions.js +43 -0
  616. package/dist/slack/renderers/search.d.ts +6 -0
  617. package/dist/slack/renderers/search.js +39 -0
  618. package/dist/slack/renderers/user.d.ts +6 -0
  619. package/dist/slack/renderers/user.js +32 -0
  620. package/dist/slack/resolvers/cache.d.ts +29 -0
  621. package/dist/slack/resolvers/cache.js +70 -0
  622. package/dist/slack/resolvers/conversation-cache.d.ts +42 -0
  623. package/dist/slack/resolvers/conversation-cache.js +128 -0
  624. package/dist/slack/resolvers/conversation.d.ts +44 -0
  625. package/dist/slack/resolvers/conversation.js +229 -0
  626. package/dist/slack/resolvers/index.d.ts +8 -0
  627. package/dist/slack/resolvers/index.js +8 -0
  628. package/dist/slack/resolvers/target.d.ts +19 -0
  629. package/dist/slack/resolvers/target.js +45 -0
  630. package/dist/slack/resolvers/url.d.ts +20 -0
  631. package/dist/slack/resolvers/url.js +22 -0
  632. package/dist/slack/resolvers/user.d.ts +54 -0
  633. package/dist/slack/resolvers/user.js +134 -0
  634. package/dist/slack/types.d.ts +180 -0
  635. package/dist/slack/types.js +13 -0
  636. package/dist/tdd/attest.d.ts +21 -0
  637. package/dist/tdd/attest.js +21 -0
  638. package/dist/tdd/discipline.d.ts +11 -0
  639. package/dist/tdd/discipline.js +28 -0
  640. package/dist/tdd/index.d.ts +14 -0
  641. package/dist/tdd/index.js +13 -0
  642. package/dist/tdd/machine.d.ts +48 -0
  643. package/dist/tdd/machine.js +139 -0
  644. package/dist/tdd/presentation.d.ts +34 -0
  645. package/dist/tdd/presentation.js +38 -0
  646. package/dist/tdd/queries.d.ts +10 -0
  647. package/dist/tdd/queries.js +22 -0
  648. package/dist/terminal/index.d.ts +12 -0
  649. package/dist/terminal/index.js +11 -0
  650. package/dist/terminal/register.d.ts +21 -0
  651. package/dist/terminal/register.js +31 -0
  652. package/dist/terminal/resolve.d.ts +63 -0
  653. package/dist/terminal/resolve.js +117 -0
  654. package/dist/terminal/types.d.ts +134 -0
  655. package/dist/terminal/types.js +19 -0
  656. package/dist/thinking/index.d.ts +20 -0
  657. package/dist/thinking/index.js +27 -0
  658. package/dist/title/block.d.ts +9 -0
  659. package/dist/title/block.js +32 -0
  660. package/dist/title/detect.d.ts +25 -0
  661. package/dist/title/detect.js +154 -0
  662. package/dist/title/gate.d.ts +19 -0
  663. package/dist/title/gate.js +21 -0
  664. package/dist/title/index.d.ts +3 -0
  665. package/dist/title/index.js +3 -0
  666. package/dist/tree/index.d.ts +18 -0
  667. package/dist/tree/index.js +17 -0
  668. package/dist/tree/register.d.ts +21 -0
  669. package/dist/tree/register.js +31 -0
  670. package/dist/tree/resolve.d.ts +20 -0
  671. package/dist/tree/resolve.js +31 -0
  672. package/dist/tree/types.d.ts +83 -0
  673. package/dist/tree/types.js +12 -0
  674. package/dist/ui/count.d.ts +29 -0
  675. package/dist/ui/count.js +35 -0
  676. package/dist/ui/path.d.ts +25 -0
  677. package/dist/ui/path.js +40 -0
  678. package/dist/verify/index.d.ts +17 -0
  679. package/dist/verify/index.js +14 -0
  680. package/dist/verify/project.d.ts +21 -0
  681. package/dist/verify/project.js +49 -0
  682. package/dist/verify/resolve.d.ts +26 -0
  683. package/dist/verify/resolve.js +27 -0
  684. package/dist/verify/run.d.ts +31 -0
  685. package/dist/verify/run.js +145 -0
  686. package/dist/web/a11y/announcements.d.ts +67 -0
  687. package/dist/web/a11y/announcements.js +116 -0
  688. package/dist/web/a11y/budget.d.ts +75 -0
  689. package/dist/web/a11y/budget.js +97 -0
  690. package/dist/web/a11y/focus.d.ts +37 -0
  691. package/dist/web/a11y/focus.js +106 -0
  692. package/dist/web/a11y/index.d.ts +19 -0
  693. package/dist/web/a11y/index.js +25 -0
  694. package/dist/web/a11y/naming.d.ts +23 -0
  695. package/dist/web/a11y/naming.js +55 -0
  696. package/dist/web/a11y/outline.d.ts +23 -0
  697. package/dist/web/a11y/outline.js +42 -0
  698. package/dist/web/a11y/reading.d.ts +12 -0
  699. package/dist/web/a11y/reading.js +101 -0
  700. package/dist/web/a11y/scope.d.ts +23 -0
  701. package/dist/web/a11y/scope.js +99 -0
  702. package/dist/web/a11y/states.d.ts +22 -0
  703. package/dist/web/a11y/states.js +53 -0
  704. package/dist/web/a11y/tree.d.ts +124 -0
  705. package/dist/web/a11y/tree.js +269 -0
  706. package/dist/web/a11y/walk.d.ts +225 -0
  707. package/dist/web/a11y/walk.js +568 -0
  708. package/dist/web/a11y/walkprobe.d.ts +44 -0
  709. package/dist/web/a11y/walkprobe.js +365 -0
  710. package/dist/web/audit/axe.d.ts +219 -0
  711. package/dist/web/audit/axe.js +322 -0
  712. package/dist/web/audit/behind.d.ts +63 -0
  713. package/dist/web/audit/behind.js +142 -0
  714. package/dist/web/audit/capture.d.ts +43 -0
  715. package/dist/web/audit/capture.js +185 -0
  716. package/dist/web/audit/colour.d.ts +85 -0
  717. package/dist/web/audit/colour.js +189 -0
  718. package/dist/web/audit/contrast.d.ts +74 -0
  719. package/dist/web/audit/contrast.js +104 -0
  720. package/dist/web/audit/health.d.ts +27 -0
  721. package/dist/web/audit/health.js +57 -0
  722. package/dist/web/audit/index.d.ts +22 -0
  723. package/dist/web/audit/index.js +22 -0
  724. package/dist/web/audit/pair.d.ts +55 -0
  725. package/dist/web/audit/pair.js +157 -0
  726. package/dist/web/audit/probe.d.ts +31 -0
  727. package/dist/web/audit/probe.js +189 -0
  728. package/dist/web/audit/report.d.ts +22 -0
  729. package/dist/web/audit/report.js +250 -0
  730. package/dist/web/audit/structure.d.ts +126 -0
  731. package/dist/web/audit/structure.js +595 -0
  732. package/dist/web/audit/sweep.d.ts +66 -0
  733. package/dist/web/audit/sweep.js +117 -0
  734. package/dist/web/audit/target.d.ts +89 -0
  735. package/dist/web/audit/target.js +168 -0
  736. package/dist/web/audit/verdict.d.ts +48 -0
  737. package/dist/web/audit/verdict.js +52 -0
  738. package/dist/web/audit/visual.d.ts +161 -0
  739. package/dist/web/audit/visual.js +297 -0
  740. package/dist/web/browser.d.ts +213 -0
  741. package/dist/web/browser.js +927 -0
  742. package/dist/web/compare/images.d.ts +45 -0
  743. package/dist/web/compare/images.js +101 -0
  744. package/dist/web/compare/index.d.ts +15 -0
  745. package/dist/web/compare/index.js +15 -0
  746. package/dist/web/compare/provenance.d.ts +48 -0
  747. package/dist/web/compare/provenance.js +86 -0
  748. package/dist/web/compare/regions.d.ts +78 -0
  749. package/dist/web/compare/regions.js +211 -0
  750. package/dist/web/cookies/extract.d.ts +26 -0
  751. package/dist/web/cookies/extract.js +167 -0
  752. package/dist/web/cookies/index.d.ts +8 -0
  753. package/dist/web/cookies/index.js +7 -0
  754. package/dist/web/cookies/inject.d.ts +13 -0
  755. package/dist/web/cookies/inject.js +27 -0
  756. package/dist/web/cookies/setup.d.ts +31 -0
  757. package/dist/web/cookies/setup.js +94 -0
  758. package/dist/web/design/index.d.ts +9 -0
  759. package/dist/web/design/index.js +9 -0
  760. package/dist/web/design/inventory.d.ts +126 -0
  761. package/dist/web/design/inventory.js +324 -0
  762. package/dist/web/design/probe.d.ts +24 -0
  763. package/dist/web/design/probe.js +88 -0
  764. package/dist/web/element/actionable.d.ts +34 -0
  765. package/dist/web/element/actionable.js +48 -0
  766. package/dist/web/element/animations.d.ts +48 -0
  767. package/dist/web/element/animations.js +74 -0
  768. package/dist/web/element/box.d.ts +53 -0
  769. package/dist/web/element/box.js +58 -0
  770. package/dist/web/element/hover.d.ts +75 -0
  771. package/dist/web/element/hover.js +172 -0
  772. package/dist/web/element/index.d.ts +17 -0
  773. package/dist/web/element/index.js +17 -0
  774. package/dist/web/element/listeners.d.ts +72 -0
  775. package/dist/web/element/listeners.js +98 -0
  776. package/dist/web/element/measure.d.ts +40 -0
  777. package/dist/web/element/measure.js +118 -0
  778. package/dist/web/element/probes.d.ts +108 -0
  779. package/dist/web/element/probes.js +195 -0
  780. package/dist/web/element/pseudo.d.ts +51 -0
  781. package/dist/web/element/pseudo.js +96 -0
  782. package/dist/web/element/view.d.ts +30 -0
  783. package/dist/web/element/view.js +117 -0
  784. package/dist/web/element/visibility.d.ts +42 -0
  785. package/dist/web/element/visibility.js +67 -0
  786. package/dist/web/envelope/artifacts.d.ts +23 -0
  787. package/dist/web/envelope/artifacts.js +45 -0
  788. package/dist/web/envelope/index.d.ts +9 -0
  789. package/dist/web/envelope/index.js +9 -0
  790. package/dist/web/envelope/naming.d.ts +23 -0
  791. package/dist/web/envelope/naming.js +43 -0
  792. package/dist/web/envelope/paged.d.ts +42 -0
  793. package/dist/web/envelope/paged.js +99 -0
  794. package/dist/web/envelope/sink.d.ts +43 -0
  795. package/dist/web/envelope/sink.js +67 -0
  796. package/dist/web/environment/devices.d.ts +65 -0
  797. package/dist/web/environment/devices.js +82 -0
  798. package/dist/web/environment/emulation.d.ts +124 -0
  799. package/dist/web/environment/emulation.js +258 -0
  800. package/dist/web/environment/index.d.ts +15 -0
  801. package/dist/web/environment/index.js +15 -0
  802. package/dist/web/environment/probes.d.ts +18 -0
  803. package/dist/web/environment/probes.js +40 -0
  804. package/dist/web/environment/shaping.d.ts +90 -0
  805. package/dist/web/environment/shaping.js +141 -0
  806. package/dist/web/environment/status.d.ts +51 -0
  807. package/dist/web/environment/status.js +109 -0
  808. package/dist/web/environment/storage.d.ts +55 -0
  809. package/dist/web/environment/storage.js +142 -0
  810. package/dist/web/environment/tabs.d.ts +32 -0
  811. package/dist/web/environment/tabs.js +47 -0
  812. package/dist/web/environment/view.d.ts +10 -0
  813. package/dist/web/environment/view.js +42 -0
  814. package/dist/web/evaluate/index.d.ts +6 -0
  815. package/dist/web/evaluate/index.js +6 -0
  816. package/dist/web/evaluate/outcome.d.ts +74 -0
  817. package/dist/web/evaluate/outcome.js +91 -0
  818. package/dist/web/evaluate/probe.d.ts +31 -0
  819. package/dist/web/evaluate/probe.js +303 -0
  820. package/dist/web/index.d.ts +47 -0
  821. package/dist/web/index.js +47 -0
  822. package/dist/web/input/index.d.ts +11 -0
  823. package/dist/web/input/index.js +11 -0
  824. package/dist/web/input/keys.d.ts +48 -0
  825. package/dist/web/input/keys.js +189 -0
  826. package/dist/web/input/pointer.d.ts +56 -0
  827. package/dist/web/input/pointer.js +77 -0
  828. package/dist/web/input/touch.d.ts +61 -0
  829. package/dist/web/input/touch.js +83 -0
  830. package/dist/web/mermaid.d.ts +69 -0
  831. package/dist/web/mermaid.js +223 -0
  832. package/dist/web/paths.d.ts +16 -0
  833. package/dist/web/paths.js +27 -0
  834. package/dist/web/perf/heap.d.ts +42 -0
  835. package/dist/web/perf/heap.js +73 -0
  836. package/dist/web/perf/index.d.ts +14 -0
  837. package/dist/web/perf/index.js +14 -0
  838. package/dist/web/perf/layers.d.ts +98 -0
  839. package/dist/web/perf/layers.js +165 -0
  840. package/dist/web/perf/probe.d.ts +26 -0
  841. package/dist/web/perf/probe.js +123 -0
  842. package/dist/web/perf/profile.d.ts +53 -0
  843. package/dist/web/perf/profile.js +97 -0
  844. package/dist/web/perf/trace.d.ts +152 -0
  845. package/dist/web/perf/trace.js +429 -0
  846. package/dist/web/perf/view.d.ts +6 -0
  847. package/dist/web/perf/view.js +86 -0
  848. package/dist/web/perf/vitals.d.ts +141 -0
  849. package/dist/web/perf/vitals.js +175 -0
  850. package/dist/web/reader.d.ts +131 -0
  851. package/dist/web/reader.js +340 -0
  852. package/dist/web/screenshot.d.ts +50 -0
  853. package/dist/web/screenshot.js +108 -0
  854. package/dist/web/search.d.ts +49 -0
  855. package/dist/web/search.js +137 -0
  856. package/dist/web/session/artifacts.d.ts +32 -0
  857. package/dist/web/session/artifacts.js +47 -0
  858. package/dist/web/session/emulation.d.ts +118 -0
  859. package/dist/web/session/emulation.js +307 -0
  860. package/dist/web/session/recording.d.ts +51 -0
  861. package/dist/web/session/recording.js +64 -0
  862. package/dist/web/session/settling.d.ts +42 -0
  863. package/dist/web/session/settling.js +104 -0
  864. package/dist/web/session/shaping.d.ts +48 -0
  865. package/dist/web/session/shaping.js +113 -0
  866. package/dist/web/session/sourcemaps.d.ts +64 -0
  867. package/dist/web/session/sourcemaps.js +135 -0
  868. package/dist/web/session/telemetry.d.ts +219 -0
  869. package/dist/web/session/telemetry.js +486 -0
  870. package/dist/web/session/wires.d.ts +22 -0
  871. package/dist/web/session/wires.js +11 -0
  872. package/dist/web/session.d.ts +1153 -0
  873. package/dist/web/session.js +3400 -0
  874. package/dist/web/snapshot/deep.d.ts +36 -0
  875. package/dist/web/snapshot/deep.js +91 -0
  876. package/dist/web/snapshot/flatten.d.ts +101 -0
  877. package/dist/web/snapshot/flatten.js +131 -0
  878. package/dist/web/snapshot/index.d.ts +8 -0
  879. package/dist/web/snapshot/index.js +10 -0
  880. package/dist/web/snapshot/presented.d.ts +32 -0
  881. package/dist/web/snapshot/presented.js +74 -0
  882. package/dist/web/snapshot/query.d.ts +67 -0
  883. package/dist/web/snapshot/query.js +136 -0
  884. package/dist/web/sourcemap/index.d.ts +9 -0
  885. package/dist/web/sourcemap/index.js +9 -0
  886. package/dist/web/sourcemap/mappings.d.ts +111 -0
  887. package/dist/web/sourcemap/mappings.js +218 -0
  888. package/dist/web/styles/capture.d.ts +49 -0
  889. package/dist/web/styles/capture.js +85 -0
  890. package/dist/web/styles/cascade.d.ts +107 -0
  891. package/dist/web/styles/cascade.js +275 -0
  892. package/dist/web/styles/curation.d.ts +45 -0
  893. package/dist/web/styles/curation.js +282 -0
  894. package/dist/web/styles/index.d.ts +10 -0
  895. package/dist/web/styles/index.js +10 -0
  896. package/dist/web/target/index.d.ts +7 -0
  897. package/dist/web/target/index.js +7 -0
  898. package/dist/web/target/refusals.d.ts +59 -0
  899. package/dist/web/target/refusals.js +305 -0
  900. package/dist/web/target/target.d.ts +59 -0
  901. package/dist/web/target/target.js +94 -0
  902. package/dist/web/telemetry/buffer.d.ts +35 -0
  903. package/dist/web/telemetry/buffer.js +72 -0
  904. package/dist/web/telemetry/console.d.ts +127 -0
  905. package/dist/web/telemetry/console.js +217 -0
  906. package/dist/web/telemetry/dialogs.d.ts +41 -0
  907. package/dist/web/telemetry/dialogs.js +38 -0
  908. package/dist/web/telemetry/downloads.d.ts +42 -0
  909. package/dist/web/telemetry/downloads.js +65 -0
  910. package/dist/web/telemetry/failure.d.ts +31 -0
  911. package/dist/web/telemetry/failure.js +72 -0
  912. package/dist/web/telemetry/har.d.ts +97 -0
  913. package/dist/web/telemetry/har.js +227 -0
  914. package/dist/web/telemetry/index.d.ts +18 -0
  915. package/dist/web/telemetry/index.js +18 -0
  916. package/dist/web/telemetry/lifecycle.d.ts +69 -0
  917. package/dist/web/telemetry/lifecycle.js +120 -0
  918. package/dist/web/telemetry/network.d.ts +152 -0
  919. package/dist/web/telemetry/network.js +130 -0
  920. package/dist/web/telemetry/requests.d.ts +31 -0
  921. package/dist/web/telemetry/requests.js +119 -0
  922. package/dist/web/telemetry/sockets.d.ts +56 -0
  923. package/dist/web/telemetry/sockets.js +101 -0
  924. package/dist/web/telemetry/view.d.ts +16 -0
  925. package/dist/web/telemetry/view.js +125 -0
  926. package/dist/web/wait/conditions.d.ts +87 -0
  927. package/dist/web/wait/conditions.js +100 -0
  928. package/dist/web/wait/index.d.ts +6 -0
  929. package/dist/web/wait/index.js +6 -0
  930. package/dist/web/wait/settle.d.ts +54 -0
  931. package/dist/web/wait/settle.js +84 -0
  932. package/dist/work/ask.d.ts +45 -0
  933. package/dist/work/ask.js +85 -0
  934. package/dist/work/author.d.ts +61 -0
  935. package/dist/work/author.js +144 -0
  936. package/dist/work/broker.d.ts +151 -0
  937. package/dist/work/broker.js +165 -0
  938. package/dist/work/events.d.ts +98 -0
  939. package/dist/work/events.js +57 -0
  940. package/dist/work/history.d.ts +69 -0
  941. package/dist/work/history.js +174 -0
  942. package/dist/work/index.d.ts +49 -0
  943. package/dist/work/index.js +33 -0
  944. package/dist/work/inplay.d.ts +43 -0
  945. package/dist/work/inplay.js +42 -0
  946. package/dist/work/memory.d.ts +84 -0
  947. package/dist/work/memory.js +203 -0
  948. package/dist/work/objection.d.ts +92 -0
  949. package/dist/work/objection.js +72 -0
  950. package/dist/work/provider.d.ts +79 -0
  951. package/dist/work/provider.js +45 -0
  952. package/dist/work/providers/git.d.ts +29 -0
  953. package/dist/work/providers/git.js +87 -0
  954. package/dist/work/publish.d.ts +53 -0
  955. package/dist/work/publish.js +139 -0
  956. package/dist/work/rebase.d.ts +78 -0
  957. package/dist/work/rebase.js +261 -0
  958. package/dist/work/reclaim.d.ts +56 -0
  959. package/dist/work/reclaim.js +52 -0
  960. package/dist/work/register.d.ts +29 -0
  961. package/dist/work/register.js +41 -0
  962. package/dist/work/stack.d.ts +126 -0
  963. package/dist/work/stack.js +243 -0
  964. package/dist/work/stacks.d.ts +156 -0
  965. package/dist/work/stacks.js +524 -0
  966. package/dist/work/survey.d.ts +52 -0
  967. package/dist/work/survey.js +58 -0
  968. package/dist/work/tidy.d.ts +147 -0
  969. package/dist/work/tidy.js +136 -0
  970. package/dist/work/tree.d.ts +120 -0
  971. package/dist/work/tree.js +69 -0
  972. package/dist/work/unattended.d.ts +40 -0
  973. package/dist/work/unattended.js +57 -0
  974. package/package.json +145 -0
@@ -0,0 +1,3400 @@
1
+ /**
2
+ * A browser session: one tab, driven by observe-then-act.
3
+ *
4
+ * observe renders the page's accessibility tree as a
5
+ * role-and-name outline (plus optional screenshot and
6
+ * readable text); act targets an element the way the model
7
+ * named it, role plus accessible name, disambiguated by
8
+ * container or ordinal, using the browser's own accessibility
9
+ * matching. Opaque node handles never reach the model.
10
+ *
11
+ * web_read is a one-shot over a session; the browser drive
12
+ * tool holds one open across tool calls. Same code path,
13
+ * different lifetime.
14
+ */
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import * as path from "node:path";
17
+ // The key table is reached through the driver's declared
18
+ // internal subpath rather than its barrel, which marks the
19
+ // table private and strips it from the public types. Copying
20
+ // the 255 entries instead would mean maintaining a second
21
+ // opinion about which keys exist, and being wrong separately.
22
+ import { _keyDefinitions } from "puppeteer-core/internal/common/USKeyboardLayout.js";
23
+ import { FOCUS_PROBE, normalizeAxTree, renderAxOutline, renderReading, scopeTree, spliceFrames, subtreeAt, WALK_COLLECT, WALK_READ, WALK_REMEMBER, WALK_RESTORE, } from "./a11y/index.js";
24
+ import { newContextPage } from "./browser.js";
25
+ import { compareImages, readPng } from "./compare/images.js";
26
+ import { describeDrift, parse as parseProvenance, sidecarFor, stringify, } from "./compare/provenance.js";
27
+ import { injectCookies, isSetUp } from "./cookies/index.js";
28
+ import { ANCESTORS_PROBE, ANIMATIONS_PROBE, CONTENT_PROBE, diffStyles, foldHover, HIDE_TEXT, HOVER_SCAN, judgeActionability, judgeVisibility, MAX_HOVER_CANDIDATES, measureBetween, normalizeAnimations, normalizeBoxModel, normalizeListeners, OCCLUDER_PROBE, OWN_TEXT_PROBE, PAINTING_ELEMENT_PROBE, SELECT_TEXT_PROBE, SETTLE_PROBE, sameBox, } from "./element/index.js";
29
+ import { DIR_MODE, FILE_MODE, pathComponent } from "./envelope/index.js";
30
+ import { captureState, chooseTab, matchesPattern, } from "./environment/index.js";
31
+ import { parseChords, } from "./input/index.js";
32
+ import { defaultBrowserDataRoot } from "./paths.js";
33
+ import { inFlight, isIdle, } from "./wait/index.js";
34
+ /**
35
+ * The styles a snapshot carries by default.
36
+ *
37
+ * Enough to answer why something is not where it should be,
38
+ * without dragging the whole computed style of every node into
39
+ * the answer.
40
+ */
41
+ const SNAPSHOT_STYLES = [
42
+ "display",
43
+ "visibility",
44
+ "opacity",
45
+ "position",
46
+ "color",
47
+ "background-color",
48
+ "font-size",
49
+ ];
50
+ /** Extra stops beyond two full passes, to show a cycle clearly. */
51
+ const WALK_SLACK = 4;
52
+ /**
53
+ * How far the walk goes when nobody said, however large the page.
54
+ *
55
+ * A ceiling on the default rather than on the caller: asked for
56
+ * more stops than this, the walk takes them.
57
+ */
58
+ const MAX_WALK_STOPS = 400;
59
+ /** How many trailing stops to inspect for being stuck. */
60
+ const WALK_STUCK_SAMPLE = 4;
61
+ /** What a stop outside the candidates has for a style. */
62
+ const BLANK_STYLE = {
63
+ outlineStyle: "none",
64
+ outlineWidth: "0px",
65
+ outlineColor: "",
66
+ boxShadow: "none",
67
+ backgroundColor: "",
68
+ borderColor: "",
69
+ color: "",
70
+ };
71
+ /** How long a wait runs before it gives up and says so. */
72
+ const DEFAULT_WAIT_MS = 10_000;
73
+ /** How often a wait looks again. */
74
+ const WAIT_POLL_MS = 100;
75
+ /**
76
+ * Every key the driver knows how to send.
77
+ *
78
+ * The chord parser is given this rather than keeping its own
79
+ * copy, so there is one table and it is the one that will
80
+ * actually be used to dispatch.
81
+ */
82
+ const KNOWN_KEYS = new Set(Object.keys(_keyDefinitions));
83
+ import { readFile } from "node:fs/promises";
84
+ import { createRequire } from "node:module";
85
+ import { buildStructure, enabledRules, foldBehind, foldPair, parseRgb, readAxeRun, TARGET_CAPTURE, visualCaptureSource, } from "./audit/index.js";
86
+ import { inventorySource } from "./design/index.js";
87
+ import { describeThrow, evaluationSource, } from "./evaluate/index.js";
88
+ import { categoriesFor, compareHeap, foldLayers, foldProfile, foldTrace, LAYER_SETTLE_MS, nameNode, observerBootstrap, readVitalsSource, } from "./perf/index.js";
89
+ import { captureTiles } from "./screenshot.js";
90
+ import { ArtifactLedger } from "./session/artifacts.js";
91
+ import { EmulationController } from "./session/emulation.js";
92
+ import { claimRecording, describeRecording, recordingInProgress, releaseRecording, } from "./session/recording.js";
93
+ import { PageSettler } from "./session/settling.js";
94
+ import { NetworkShaper } from "./session/shaping.js";
95
+ import { SourceMapStore } from "./session/sourcemaps.js";
96
+ import { SessionTelemetry } from "./session/telemetry.js";
97
+ import { flattenSnapshot, } from "./snapshot/index.js";
98
+ import { asCall, COMPUTED_STYLE_PROBE, curateStyles, INITIALS_PROBE, normalizeCascade, SHORTHAND_PROPERTIES, traceProperty, } from "./styles/index.js";
99
+ import { ambiguityRefusal, notFoundRefusal, resolveTarget, } from "./target/index.js";
100
+ import { diedWithTheTab, failureText, requestStatus, toHar, } from "./telemetry/index.js";
101
+ /**
102
+ * How long to let a state settle before reading it.
103
+ *
104
+ * Forcing a state starts whatever transition the page declared,
105
+ * so a reading taken at once catches the resting values and
106
+ * reports that nothing changed. The wait is on the browser's
107
+ * own promise that its animations have finished; this only caps
108
+ * a page that keeps starting new ones.
109
+ */
110
+ const SETTLE_CAP_MS = 2000;
111
+ /**
112
+ * How long to wait for an element to become ready, and how
113
+ * often to look. A page that animates a dialog in takes a few
114
+ * hundred milliseconds, and waiting is far cheaper than a click
115
+ * that silently misses.
116
+ */
117
+ const READY_BUDGET_MS = 2000;
118
+ const READY_POLL_MS = 100;
119
+ /**
120
+ * How long to give Chrome to announce a crash that has already
121
+ * broken a call, and how often to look for the announcement.
122
+ *
123
+ * Only spent on a call that has just failed with the tab dying
124
+ * underneath it, so the wait is never on the healthy path. The
125
+ * announcement arrives in milliseconds; the budget is generous
126
+ * because the alternative to waiting is losing the call.
127
+ */
128
+ const CRASH_NOTICE_BUDGET_MS = 2000;
129
+ const CRASH_NOTICE_POLL_MS = 25;
130
+ /**
131
+ * Whether an action arrives by pointer.
132
+ *
133
+ * A pointer has to reach the element's centre unobstructed and
134
+ * on screen. Everything else addresses the element directly and
135
+ * would succeed where a click could not.
136
+ */
137
+ /** Whether claiming the recording permit came back as a refusal. */
138
+ function isRecordingRefused(value) {
139
+ return typeof value === "object" && value !== null && "refusal" in value;
140
+ }
141
+ function usesPointer(action) {
142
+ return action.kind === "click" || action.kind === "hover";
143
+ }
144
+ /**
145
+ * What this action needs, which for one action is unusually
146
+ * little.
147
+ *
148
+ * Choosing a file happens in the operating system's dialog, and
149
+ * the input that receives it is nearly always hidden with a
150
+ * styled label over it, which is the recommended way to build
151
+ * one. So an upload asks neither to be seen nor to be reachable
152
+ * by a pointer: it asks that the input exists and takes files.
153
+ *
154
+ * It also does without the driver's handle. The aria selector
155
+ * that produces one does not offer a node it considers hidden,
156
+ * so for the ordinary hidden input the two matchers disagree,
157
+ * our own outline finds it and the selector does not, and the
158
+ * act refuses something it had just listed. The file is set
159
+ * through the protocol against the node the outline resolved.
160
+ */
161
+ function needsOf(action) {
162
+ if (action.kind === "upload") {
163
+ return { pointer: false, sight: false, handle: false };
164
+ }
165
+ return { pointer: usesPointer(action), sight: true, handle: true };
166
+ }
167
+ /** Chrome cookie injection was asked for but is not set up. */
168
+ export class CookieSetupNeeded extends Error {
169
+ constructor() {
170
+ super("Chrome cookie injection is not set up. Run Chrome cookie " +
171
+ "setup, or open the session without cookies.");
172
+ this.name = "CookieSetupNeeded";
173
+ }
174
+ }
175
+ /** A driveable browser session over a single tab. */
176
+ /**
177
+ * Where axe-core's bundle sits on disk.
178
+ *
179
+ * Resolved through node rather than assembled from a relative
180
+ * path, so it keeps working wherever the package is installed
181
+ * and however the dependency is hoisted.
182
+ */
183
+ function axeSource() {
184
+ return createRequire(import.meta.url).resolve("axe-core/axe.min.js");
185
+ }
186
+ export class BrowserSession {
187
+ name;
188
+ page;
189
+ cdp;
190
+ context;
191
+ options;
192
+ /** What every property computes to untouched, read once. */
193
+ initialStyles;
194
+ /** The recovery in flight, so nothing reads a tab mid-replacement. */
195
+ recovery;
196
+ /** The live view collaborators drive the browser through. */
197
+ wires = {
198
+ page: () => this.page,
199
+ cdp: () => this.cdp,
200
+ context: () => this.context,
201
+ ready: () => this.ready(),
202
+ };
203
+ /** The maps the page has declared, for authored positions. */
204
+ sourceMaps = new SourceMapStore(this.wires);
205
+ /** What this session pretends to be, and its keeper. */
206
+ emulation = new EmulationController(this.wires, {
207
+ settle: () => this.settlePage(),
208
+ });
209
+ /** The rules and throttle this session's network answers to. */
210
+ shaper = new NetworkShaper(this.wires);
211
+ /** The disk side of this session: sink, stamps and ledger. */
212
+ artifacts = new ArtifactLedger();
213
+ /** The wait between a change and an honest reading of it. */
214
+ settler = new PageSettler(this.wires, () => this.requests());
215
+ /** Everything this session overhears, and its buffers. */
216
+ telemetry = new SessionTelemetry(this.wires, {
217
+ onCrash: () => {
218
+ this.recovery = this.recover();
219
+ },
220
+ downloadDir: () => this.artifacts.sink().dir,
221
+ keep: (path) => {
222
+ this.artifacts.keep(path);
223
+ },
224
+ });
225
+ constructor(name,
226
+ // The page and its protocol channel are replaced wholesale
227
+ // when the tab crashes, so neither can be readonly.
228
+ page, cdp, context, options) {
229
+ this.name = name;
230
+ this.page = page;
231
+ this.cdp = cdp;
232
+ this.context = context;
233
+ this.options = options;
234
+ }
235
+ /** Wall clock time of the last operation. See ready(). */
236
+ usedAt = Date.now();
237
+ /**
238
+ * Open a fresh session in a browser context of its own, so
239
+ * its cookies, storage and cache belong to it alone.
240
+ */
241
+ static async open(name, options = {}) {
242
+ if (options.cookies && !isSetUp())
243
+ throw new CookieSetupNeeded();
244
+ const { page, context } = await newContextPage();
245
+ try {
246
+ const cdp = await page.createCDPSession();
247
+ await cdp.send("Accessibility.enable");
248
+ // The element domain reads boxes and the cascade, which
249
+ // need the DOM and CSS agents running.
250
+ await cdp.send("DOM.enable");
251
+ await cdp.send("CSS.enable");
252
+ const session = new BrowserSession(name, page, cdp, context, options);
253
+ await session.telemetry.listenForAnnouncements();
254
+ await session.telemetry.listenForLogs();
255
+ await session.telemetry.listenForRequests();
256
+ await session.telemetry.listenForDialogs();
257
+ await session.telemetry.listenForLifecycle();
258
+ await session.telemetry.listenForDownloads();
259
+ await session.sourceMaps.listen();
260
+ await session.watchVitals();
261
+ return session;
262
+ }
263
+ catch (err) {
264
+ // Do not leak the context if the CDP channel could not be
265
+ // set up; close it before surfacing the failure.
266
+ await context.close().catch(() => { });
267
+ throw err;
268
+ }
269
+ }
270
+ /** Decide how dialogs will be answered from here on. */
271
+ setDialogPolicy(policy) {
272
+ this.telemetry.setDialogPolicy(policy);
273
+ }
274
+ /** How dialogs are currently answered. */
275
+ get dialogs() {
276
+ return this.telemetry.dialogs;
277
+ }
278
+ /** Every request the page has made. */
279
+ requests() {
280
+ return this.telemetry.requests();
281
+ }
282
+ /**
283
+ * Write a capture out as an HTTP Archive.
284
+ *
285
+ * Bodies are fetched for the requests being exported, since an
286
+ * archive without them answers far fewer questions than one
287
+ * with them, and an export is already an explicit ask.
288
+ */
289
+ async exportHar(requests) {
290
+ const bodies = new Map();
291
+ for (const request of requests) {
292
+ const fetched = await this.bodyOf(request.id);
293
+ if (fetched && fetched.body.length > 0)
294
+ bodies.set(request.id, fetched);
295
+ }
296
+ const path = this.artifacts
297
+ .sink()
298
+ .writeText(`capture-${this.artifacts.nextArchive()}.har`, JSON.stringify(toHar(requests, { bodies }), null, 2));
299
+ return this.artifacts.keep(path);
300
+ }
301
+ /**
302
+ * The body of one recorded reply, fetched on demand.
303
+ *
304
+ * Bodies are not captured as they stream past: most are never
305
+ * asked for, and holding every one would cost far more memory
306
+ * than the rest of the session put together. Chrome keeps them
307
+ * for the current document, so this asks only when asked.
308
+ */
309
+ async bodyOf(requestId) {
310
+ return this.telemetry.bodyOf(requestId);
311
+ }
312
+ /**
313
+ * What the page has said, from a cursor onward.
314
+ *
315
+ * The buffer survives navigation on purpose: a message logged
316
+ * just before a redirect is often the one that explains it.
317
+ */
318
+ logs(since) {
319
+ return this.telemetry.logs(since);
320
+ }
321
+ /** Navigate the tab to a URL and wait for the network to settle. */
322
+ async navigate(url) {
323
+ await this.ready();
324
+ // Cookies are per-domain, so they are injected against the
325
+ // URL we are about to visit rather than once at open.
326
+ if (this.options.cookies)
327
+ await injectCookies(this.page, url);
328
+ // A navigation that never arrives is an answer, not an
329
+ // exception. Offline, or against a host that does not resolve,
330
+ // this threw a raw driver error straight past a doc that
331
+ // promises the opposite two lines below, and the caller got a
332
+ // stack trace where it had asked a question. What the network
333
+ // said is the useful part, so it is returned.
334
+ let response = null;
335
+ try {
336
+ response = await this.page.goto(url, { waitUntil: "networkidle2" });
337
+ }
338
+ catch (error) {
339
+ const failure = failureText(error);
340
+ // The tab can die under this very call, and Chrome tells the
341
+ // driver the frame is detached before it announces the crash
342
+ // that detached it. Returning here would hand the caller a
343
+ // driver error for a session that is about to be perfectly
344
+ // healthy, and leave it parked on the replacement's blank
345
+ // page: measured, a crash made a session permanently
346
+ // unnavigable while every later call reported success.
347
+ if (!diedWithTheTab(failure) || !(await this.awaitReplacement())) {
348
+ return { failure };
349
+ }
350
+ try {
351
+ response = await this.page.goto(url, { waitUntil: "networkidle2" });
352
+ }
353
+ catch (afterRecovery) {
354
+ return { failure: failureText(afterRecovery) };
355
+ }
356
+ }
357
+ await this.emulation.reassert();
358
+ // Chrome's idle heuristics fire before a client-rendered app
359
+ // has anything on screen, so this waits for the DOM as well.
360
+ // Without it, navigating to a real application answered with an
361
+ // outline whose only content was "Loading page".
362
+ await this.settlePage();
363
+ // The status was thrown away, so a 404 or a 500 read as a
364
+ // successful arrival and every later check judged the error
365
+ // page. Returned rather than thrown: an error page is
366
+ // sometimes exactly what a caller means to look at, and the
367
+ // answer should say which one they got.
368
+ const status = response?.status();
369
+ return status === undefined ? {} : { status };
370
+ }
371
+ /** Where the session currently is. */
372
+ get url() {
373
+ return this.page.url();
374
+ }
375
+ /**
376
+ * Fetch the page again.
377
+ *
378
+ * ignoreCache is the useful default for a developer, who
379
+ * reloads to see a change they just made. A reload that served
380
+ * the old file back would be the one thing it must not do.
381
+ */
382
+ async reload() {
383
+ await this.ready();
384
+ try {
385
+ await this.page.reload({ waitUntil: "networkidle2" });
386
+ }
387
+ catch (error) {
388
+ // Same reasoning as navigate: reloading with no network is a
389
+ // thing a tester does deliberately.
390
+ return { failure: failureText(error) };
391
+ }
392
+ await this.emulation.reassert();
393
+ await this.settlePage();
394
+ return {};
395
+ }
396
+ /**
397
+ * Wait for the page to stop changing, so a read that follows
398
+ * describes where the page ended up rather than where it was.
399
+ *
400
+ * Returns what it found instead of throwing on a page that never
401
+ * settles: something that animates or polls for ever is still
402
+ * worth reading, as long as the answer does not pretend it was
403
+ * final.
404
+ */
405
+ async settlePage(budgetMs) {
406
+ return this.settler.settle(budgetMs);
407
+ }
408
+ /** What the last settle saw, for a reader that wants to say so. */
409
+ get settledLast() {
410
+ return this.settler.lastSeen;
411
+ }
412
+ /**
413
+ * Step through the history the session accumulated.
414
+ *
415
+ * Refuses rather than doing nothing at either end. Silently
416
+ * staying put looks identical to a page that ignored the
417
+ * request, and the caller then has no way to tell whether
418
+ * going back was possible at all.
419
+ *
420
+ * The refusal is driven by the thrown error, not by a null
421
+ * return. Puppeteer returns null whenever a navigation has no
422
+ * HTTP response, which includes about:blank and same-document
423
+ * moves, so reading null as failure reported a successful step
424
+ * back to about:blank as an impossible one. An absent entry is
425
+ * what throws.
426
+ */
427
+ async step(direction) {
428
+ await this.ready();
429
+ try {
430
+ if (direction === "back") {
431
+ await this.page.goBack({ waitUntil: "networkidle2" });
432
+ }
433
+ else {
434
+ await this.page.goForward({ waitUntil: "networkidle2" });
435
+ }
436
+ await this.emulation.reassert();
437
+ await this.settlePage();
438
+ }
439
+ catch (error) {
440
+ // Not every failure here is an empty history. Stepping back
441
+ // with the network off fails too, and this used to answer
442
+ // that there was nothing behind the page: a confident, wrong
443
+ // account of the session's own history, which is the kind of
444
+ // lie that sends someone looking in the wrong place.
445
+ const said = failureText(error);
446
+ if (!/History entry/i.test(said)) {
447
+ return {
448
+ ok: false,
449
+ refusal: `Going ${direction} did not arrive: ${said}. The page ` +
450
+ "is where it was. Read requests to see what the network " +
451
+ "did.",
452
+ };
453
+ }
454
+ return {
455
+ ok: false,
456
+ refusal: direction === "back"
457
+ ? "There is nothing behind this page in the session's " +
458
+ "history. It is where the session started."
459
+ : "There is nothing ahead of this page. Going forward " +
460
+ "only works after going back.",
461
+ };
462
+ }
463
+ return { ok: true, url: this.page.url() };
464
+ }
465
+ /**
466
+ * Render the page's accessibility outline, plus url and
467
+ * title. A scope narrows the tree before it is rendered; with
468
+ * none, the whole page is read.
469
+ */
470
+ async observe(scope = {}, form = "outline") {
471
+ const tree = await this.axTree();
472
+ return await this.describe(scopeTree(tree, scope), form);
473
+ }
474
+ /**
475
+ * Read one branch of the page, named the way the caller names
476
+ * anything else. Unlike a whole-page read this can fail, so
477
+ * it reports what would have worked instead.
478
+ */
479
+ async observeWithin(target, scope = {}, form = "outline") {
480
+ const tree = await this.axTree();
481
+ const resolution = resolveTarget(tree, target);
482
+ if (resolution.kind === "ambiguous") {
483
+ return { ok: false, refusal: ambiguityRefusal(tree, target) };
484
+ }
485
+ if (resolution.kind === "notFound") {
486
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
487
+ }
488
+ const branch = subtreeAt(tree, resolution.backendDomId);
489
+ if (!branch) {
490
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
491
+ }
492
+ return {
493
+ ok: true,
494
+ observation: await this.describe(scopeTree(branch, scope), form),
495
+ };
496
+ }
497
+ /** Wrap a tree as an observation of where the session is. */
498
+ async describe(tree, form) {
499
+ return {
500
+ url: this.page.url(),
501
+ title: await this.page.title(),
502
+ outline: form === "reading" ? renderReading(tree) : renderAxOutline(tree),
503
+ tree,
504
+ };
505
+ }
506
+ /** Perform an action, resolving semantic targets against the a11y tree. */
507
+ async act(action) {
508
+ if (action.kind === "navigate") {
509
+ await this.navigate(action.url);
510
+ return { ok: true };
511
+ }
512
+ // Readiness is established before targeting, because an
513
+ // element that has not appeared yet is the commonest reason
514
+ // an action is early rather than wrong.
515
+ const needs = needsOf(action);
516
+ const ready = await this.awaitReady(action.target, needs.pointer, needs.sight);
517
+ if (!ready.ready) {
518
+ // Never showing up and never existing look the same from
519
+ // here, and only one of them has a spelling suggestion
520
+ // worth making, so ask the targeting to explain itself.
521
+ const missing = await this.resolve(action.target);
522
+ if (!missing.ok)
523
+ return missing;
524
+ await missing.element.dispose();
525
+ return { ok: false, blocked: ready };
526
+ }
527
+ if (!needs.handle) {
528
+ await this.perform(action, undefined, ready.backendDomId);
529
+ if (ready.waitedMs > 0)
530
+ return { ok: true, waitedMs: ready.waitedMs };
531
+ return { ok: true };
532
+ }
533
+ const handle = await this.resolve(action.target);
534
+ if (!handle.ok)
535
+ return handle;
536
+ await this.perform(action, handle.element, ready.backendDomId);
537
+ await handle.element.dispose();
538
+ if (ready.waitedMs > 0)
539
+ return { ok: true, waitedMs: ready.waitedMs };
540
+ return { ok: true };
541
+ }
542
+ /**
543
+ * Tell each frame of a stack where it was written.
544
+ *
545
+ * A frame with no map keeps its generated position rather than
546
+ * being dropped: half a stack is worse than a raw one.
547
+ */
548
+ async resolveFrames(frames) {
549
+ return this.sourceMaps.resolveFrames(frames);
550
+ }
551
+ /**
552
+ * Replace the dead tab with a live one.
553
+ *
554
+ * The context survives a tab crash and can still make pages,
555
+ * so the session keeps its identity, its cookies and every
556
+ * buffer it has filled; only the tab is new. Watchers have to
557
+ * be reinstalled because they were bound to a protocol channel
558
+ * that no longer answers.
559
+ */
560
+ async recover() {
561
+ // The dead page will not answer, but closing it does work,
562
+ // and leaving it would leak a renderer for the session's life.
563
+ await this.page.close().catch(() => { });
564
+ await this.bindTo(await this.context.newPage());
565
+ }
566
+ /**
567
+ * Point the session at a tab and make it whole again.
568
+ *
569
+ * Every watcher is bound to one tab's protocol channel, so a
570
+ * session that changes tabs without reinstalling them goes on
571
+ * answering while quietly deaf to everything the new tab does.
572
+ * That is the same wound a crash leaves, which is why recovering
573
+ * from a crash and switching tabs are one piece of code: two
574
+ * copies would drift, and the half that drifted would fail
575
+ * silently in exactly this way.
576
+ */
577
+ async bindTo(page) {
578
+ const cdp = await page.createCDPSession();
579
+ this.page = page;
580
+ this.cdp = cdp;
581
+ await cdp.send("Accessibility.enable");
582
+ await cdp.send("DOM.enable");
583
+ await cdp.send("CSS.enable");
584
+ await this.telemetry.listenForAnnouncements();
585
+ await this.telemetry.listenForLogs();
586
+ await this.telemetry.listenForRequests();
587
+ await this.telemetry.listenForDialogs();
588
+ // Every listener open() installs has to come back, or the
589
+ // session goes on answering while quietly deaf to part of
590
+ // what the page does. Downloads and source maps were the two
591
+ // that did not, so after a crash a file the page handed back
592
+ // went unrecorded and a cascade trace lost its authored
593
+ // positions, with nothing anywhere saying why.
594
+ await this.telemetry.listenForDownloads();
595
+ await this.sourceMaps.listen();
596
+ // A fresh tab knows nothing of what the old one was
597
+ // pretending, so the standing intent is put back before
598
+ // anything reads from it.
599
+ await this.emulation.apply();
600
+ const { frameTree } = await cdp.send("Page.getFrameTree");
601
+ this.telemetry.readoptLifecycle(frameTree.frame.id);
602
+ }
603
+ /**
604
+ * Wait for any recovery to finish.
605
+ *
606
+ * Called before anything that touches the tab, so a read that
607
+ * arrives during a crash waits for the replacement instead of
608
+ * hanging on a renderer that is never coming back.
609
+ */
610
+ /**
611
+ * Wait for the replacement tab, when one is coming.
612
+ *
613
+ * The crash announcement that starts a recovery can arrive
614
+ * after the failure it caused, so asking whether a recovery is
615
+ * under way immediately gets the wrong answer. This gives the
616
+ * announcement its moment and then waits the recovery out,
617
+ * reporting whether there was one, so a caller that died for
618
+ * some other reason is not left waiting on a tab nobody is
619
+ * replacing.
620
+ */
621
+ async awaitReplacement() {
622
+ const deadline = Date.now() + CRASH_NOTICE_BUDGET_MS;
623
+ while (!this.recovery && Date.now() < deadline) {
624
+ await new Promise((wake) => setTimeout(wake, CRASH_NOTICE_POLL_MS));
625
+ }
626
+ if (!this.recovery)
627
+ return false;
628
+ await this.recovery;
629
+ return true;
630
+ }
631
+ async ready() {
632
+ // Every operation passes through here, which makes it the one
633
+ // honest place to record that the session is being used. The
634
+ // registry reaps on idleness, and it used to measure that
635
+ // from when a call started rather than from the last thing
636
+ // the session did, so a health sweep or a long wait could
637
+ // have the browser closed out from under it mid-call.
638
+ this.usedAt = Date.now();
639
+ if (this.recovery)
640
+ await this.recovery;
641
+ }
642
+ /** When this session last did anything, for the idle reaper. */
643
+ get lastUsedAt() {
644
+ return this.usedAt;
645
+ }
646
+ /** Files the page has handed back. */
647
+ downloads() {
648
+ return this.telemetry.downloads();
649
+ }
650
+ /**
651
+ * Record what the page's JavaScript is doing, for a while.
652
+ *
653
+ * A long task says three seconds went somewhere. This says
654
+ * where. The profiler is started, the page is left alone for
655
+ * the window asked for, and the samples are folded into time
656
+ * per function.
657
+ *
658
+ * Nothing is done to the page in between on purpose: whatever
659
+ * is being profiled has to be the page's own work, and a probe
660
+ * running during the window would appear in its own results.
661
+ */
662
+ async profile(forMs) {
663
+ await this.ready();
664
+ await this.cdp.send("Profiler.enable");
665
+ // A thousand microseconds is Chrome's own default. Sampling
666
+ // faster buys precision the reader cannot use and costs the
667
+ // page time that then shows up in its own profile.
668
+ await this.cdp.send("Profiler.setSamplingInterval", { interval: 1000 });
669
+ await this.cdp.send("Profiler.start");
670
+ await new Promise((wake) => setTimeout(wake, forMs));
671
+ const { profile } = await this.cdp.send("Profiler.stop");
672
+ await this.cdp.send("Profiler.disable");
673
+ return foldProfile(profile);
674
+ }
675
+ /**
676
+ * Record a trace around some work, and say what it cost.
677
+ *
678
+ * The work runs inside the recording rather than before it,
679
+ * because a trace only holds what happened while it was on.
680
+ * Measured: a timer installed before the recording starts has
681
+ * no install event, so its lateness cannot be explained and a
682
+ * fetch begun earlier has no url. Tracing after the fact is
683
+ * not a weaker answer, it is no answer.
684
+ *
685
+ * The work's own result is handed back whatever the recording
686
+ * did, so a refused permit or a protocol failure costs the
687
+ * caller the trace and never the action.
688
+ */
689
+ async recordWhile(profiles, work) {
690
+ await this.ready();
691
+ const permit = claimRecording(this.name, profiles);
692
+ if (isRecordingRefused(permit)) {
693
+ return { result: await work(), refusal: permit.refusal };
694
+ }
695
+ const events = [];
696
+ const collect = (batch) => {
697
+ if (batch.value)
698
+ events.push(...batch.value);
699
+ };
700
+ this.cdp.on("Tracing.dataCollected", collect);
701
+ let started = false;
702
+ try {
703
+ await this.cdp.send("Tracing.start", {
704
+ transferMode: "ReportEvents",
705
+ traceConfig: {
706
+ recordMode: "recordContinuously",
707
+ includedCategories: [...categoriesFor(profiles)],
708
+ },
709
+ });
710
+ started = true;
711
+ }
712
+ catch (error) {
713
+ this.cdp.off("Tracing.dataCollected", collect);
714
+ releaseRecording();
715
+ return {
716
+ result: await work(),
717
+ refusal: `The browser refused to start tracing: ${String(error)}`,
718
+ };
719
+ }
720
+ // The action runs whatever the recording does next, and the
721
+ // permit is given back on every path out of here.
722
+ try {
723
+ const result = await work();
724
+ const delivered = new Promise((resolve) => {
725
+ this.cdp.once("Tracing.tracingComplete", () => resolve());
726
+ });
727
+ await this.cdp.send("Tracing.end");
728
+ await delivered;
729
+ return {
730
+ result,
731
+ trace: foldTrace(events, {
732
+ frames: await this.ownFrameIds(),
733
+ profiles,
734
+ }),
735
+ };
736
+ }
737
+ finally {
738
+ this.cdp.off("Tracing.dataCollected", collect);
739
+ if (started)
740
+ releaseRecording();
741
+ }
742
+ }
743
+ /**
744
+ * Every frame id this session owns, its own and its children's.
745
+ *
746
+ * This is what separates our work from another session's in a
747
+ * recording that necessarily contains both.
748
+ */
749
+ async ownFrameIds() {
750
+ const ids = new Set();
751
+ try {
752
+ const { frameTree } = (await this.cdp.send("Page.getFrameTree"));
753
+ const queue = [frameTree];
754
+ while (queue.length > 0) {
755
+ const node = queue.shift();
756
+ if (node === undefined)
757
+ continue;
758
+ ids.add(node.frame.id);
759
+ queue.push(...(node.childFrames ?? []));
760
+ }
761
+ }
762
+ catch {
763
+ // Without a frame tree nothing can be attributed, which the
764
+ // fold reports as unattributed rather than guessing.
765
+ }
766
+ return ids;
767
+ }
768
+ /** Every websocket conversation the page has held. */
769
+ sockets() {
770
+ return this.telemetry.sockets();
771
+ }
772
+ /**
773
+ * Measure what the page is holding, against the last measurement.
774
+ *
775
+ * A collection is forced first unless refused, because a heap
776
+ * that grew means nothing while uncollected garbage is still in
777
+ * it. The previous reading is kept on the session so a caller
778
+ * can measure, do the thing they suspect, and measure again
779
+ * without having to carry a number between calls.
780
+ */
781
+ async heap(collect = true) {
782
+ await this.ready();
783
+ if (collect) {
784
+ // Chrome exposes this on two domains and neither is
785
+ // guaranteed enabled. Failing to collect is not worth
786
+ // failing the read over, but it does change what the
787
+ // reading means, which is what `collected` carries.
788
+ try {
789
+ await this.cdp.send("HeapProfiler.collectGarbage");
790
+ }
791
+ catch {
792
+ collect = false;
793
+ }
794
+ }
795
+ // The domain answers with an empty list until it is enabled,
796
+ // which reads as a heap of zero that never changes: a leak
797
+ // check that always says everything is fine. Enabling is
798
+ // idempotent, so it costs nothing to do it on every read.
799
+ // Measured: without this the domain answers with every metric
800
+ // at zero rather than refusing, which reads as a page holding
801
+ // no memory that never changes. A leak check that always says
802
+ // everything is fine is worse than no leak check. Enabling is
803
+ // idempotent, so it costs nothing to do on every read.
804
+ await this.cdp.send("Performance.enable");
805
+ const { metrics } = await this.cdp.send("Performance.getMetrics");
806
+ const read = (named) => metrics.find((metric) => metric.name === named)?.value ?? 0;
807
+ const reading = {
808
+ usedBytes: read("JSHeapUsedSize"),
809
+ totalBytes: read("JSHeapTotalSize"),
810
+ collected: collect,
811
+ at: Date.now(),
812
+ };
813
+ const compared = compareHeap(reading, this.lastHeap);
814
+ this.lastHeap = reading;
815
+ return compared;
816
+ }
817
+ /** The previous heap reading, for the next comparison. */
818
+ lastHeap;
819
+ /**
820
+ * What the compositor is holding for this page, and why.
821
+ *
822
+ * LayerTree pushes nothing when it is enabled: the tree arrives
823
+ * only on layerTreeDidChange, and a page that has finished
824
+ * settling has no change left to report. Measured across seven
825
+ * candidates, a throwaway screenshot is the cheapest way to make
826
+ * Chrome recompose and hand the tree over, and the only one that
827
+ * alters nothing about the page. Scrolling moves the page,
828
+ * overriding device metrics disturbs an emulation the caller may
829
+ * have set, and toggling a style writes to the DOM.
830
+ *
831
+ * The domain goes off again afterwards, so a read leaves no
832
+ * instrumentation running behind it.
833
+ */
834
+ /**
835
+ * Judge text against the background actually behind its glyphs.
836
+ *
837
+ * For the case axe hands back as needing a person: text over a
838
+ * gradient, a photograph, or anything else with no single
839
+ * background colour. Two shots of the same region, the second with
840
+ * the text hidden, give both the glyph mask and what lies under it.
841
+ *
842
+ * Hiding the text is a paint-only change, so nothing moves between
843
+ * the shots and the subtraction stays aligned. The inline style is
844
+ * put back in a finally, including the case where it had one of its
845
+ * own to begin with.
846
+ */
847
+ async contrastBehind(target, bar = "AA") {
848
+ await this.ready();
849
+ const tree = await this.axTree();
850
+ const resolution = resolveTarget(tree, target);
851
+ if (resolution.kind === "notFound") {
852
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
853
+ }
854
+ if (resolution.kind === "ambiguous") {
855
+ return { ok: false, refusal: ambiguityRefusal(tree, target) };
856
+ }
857
+ const backendNodeId = resolution.backendDomId;
858
+ const named = await this.objectFor(backendNodeId);
859
+ if (!named) {
860
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
861
+ }
862
+ // Naming a paragraph's text means naming its StaticText, since
863
+ // a paragraph has no accessible name, and that resolves to a
864
+ // text node. The colour and the hiding both belong to the
865
+ // element painting it; the capture below still uses the named
866
+ // node, whose box is the tighter one.
867
+ const objectId = await this.paintingElement(named);
868
+ try {
869
+ const styles = await this.stylesOf(objectId);
870
+ // No fallback here on purpose. parseRgb returns undefined for a
871
+ // colour it cannot read, and the fold treats that as undecidable;
872
+ // substituting black would answer confidently about a colour
873
+ // nobody read.
874
+ const colour = parseRgb(styles?.color ?? "");
875
+ const sizing = {
876
+ fontSizePx: Number.parseFloat(styles?.["font-size"] ?? "16") || 16,
877
+ fontWeight: Number.parseFloat(styles?.["font-weight"] ?? "400") || 400,
878
+ };
879
+ const first = await this.capture(backendNodeId, {});
880
+ await this.hideText(objectId, true);
881
+ try {
882
+ const second = await this.capture(backendNodeId, {});
883
+ const withText = readPng(readFileSync(first.paths[0] ?? ""));
884
+ const bare = readPng(readFileSync(second.paths[0] ?? ""));
885
+ return {
886
+ ok: true,
887
+ report: foldBehind({
888
+ withText,
889
+ bare,
890
+ textColour: colour
891
+ ? { r: colour.r, g: colour.g, b: colour.b }
892
+ : undefined,
893
+ sizing,
894
+ bar,
895
+ }),
896
+ };
897
+ }
898
+ finally {
899
+ await this.hideText(objectId, false);
900
+ }
901
+ }
902
+ finally {
903
+ await this.release(objectId);
904
+ // Promoting a text node makes a second handle, and the node
905
+ // that was named is still ours to let go of.
906
+ if (named !== objectId)
907
+ await this.release(named);
908
+ }
909
+ }
910
+ /**
911
+ * Judge the contrast between two elements the caller named.
912
+ *
913
+ * Distinct from contrastBehind, which subtracts pixels to read the
914
+ * background under one element's glyphs. This compares two
915
+ * elements' stated colours, which is the only way to ask about a
916
+ * boundary: an icon against its card, a border against the page.
917
+ * Both sides are read from the computed style rather than the
918
+ * screen, so nothing is captured and nothing moves.
919
+ */
920
+ async contrastPair(one, other, bar = "AA") {
921
+ await this.ready();
922
+ const tree = await this.axTree();
923
+ const sides = [];
924
+ for (const target of [one, other]) {
925
+ const resolution = resolveTarget(tree, target);
926
+ if (resolution.kind === "notFound") {
927
+ return { ok: false, target, refusal: notFoundRefusal(tree, target) };
928
+ }
929
+ if (resolution.kind === "ambiguous") {
930
+ return { ok: false, target, refusal: ambiguityRefusal(tree, target) };
931
+ }
932
+ const named = await this.objectFor(resolution.backendDomId);
933
+ if (!named) {
934
+ return { ok: false, target, refusal: notFoundRefusal(tree, target) };
935
+ }
936
+ // Either side may be named as StaticText, which resolves to a
937
+ // text node: no computed style to read, and no child text
938
+ // nodes of its own, so it would also be judged as carrying no
939
+ // text and drawn against the wrong criterion.
940
+ const objectId = await this.paintingElement(named);
941
+ try {
942
+ sides.push(await this.paintedSide(objectId));
943
+ }
944
+ finally {
945
+ await this.release(objectId);
946
+ if (named !== objectId)
947
+ await this.release(named);
948
+ }
949
+ }
950
+ const [first, second] = sides;
951
+ if (!first || !second) {
952
+ // Only reachable if a side went missing after resolving, and
953
+ // the loop above returns for anything it could name, so the
954
+ // one still unaccounted for is the second.
955
+ return {
956
+ ok: false,
957
+ target: other,
958
+ refusal: notFoundRefusal(tree, other),
959
+ };
960
+ }
961
+ return { ok: true, report: foldPair({ one: first, other: second, bar }) };
962
+ }
963
+ /** What one side of a contrast pairing paints. */
964
+ async paintedSide(objectId) {
965
+ const styles = await this.stylesOf(objectId);
966
+ const { result } = await this.cdp.send("Runtime.callFunctionOn", {
967
+ objectId,
968
+ functionDeclaration: OWN_TEXT_PROBE,
969
+ returnByValue: true,
970
+ });
971
+ return {
972
+ hasText: result.value === true,
973
+ ...(styles?.color === undefined ? {} : { color: styles.color }),
974
+ ...(styles?.["background-color"] === undefined
975
+ ? {}
976
+ : { backgroundColor: styles["background-color"] }),
977
+ ...(styles?.["border-color"] === undefined
978
+ ? {}
979
+ : { borderColor: styles["border-color"] }),
980
+ sizing: {
981
+ fontSizePx: Number.parseFloat(styles?.["font-size"] ?? "16") || 16,
982
+ fontWeight: Number.parseFloat(styles?.["font-weight"] ?? "400") || 400,
983
+ },
984
+ };
985
+ }
986
+ /**
987
+ * Make an element's own text stop painting, and put it back.
988
+ *
989
+ * Only colour and shadow are touched, because neither affects
990
+ * layout: the glyphs still occupy the same pixels, they simply
991
+ * leave no mark on them.
992
+ */
993
+ async hideText(objectId, hidden) {
994
+ try {
995
+ await this.cdp.send("Runtime.callFunctionOn", {
996
+ objectId,
997
+ functionDeclaration: HIDE_TEXT,
998
+ arguments: [{ value: hidden }],
999
+ returnByValue: true,
1000
+ });
1001
+ }
1002
+ catch {
1003
+ // A page that navigated mid-measurement has already discarded
1004
+ // the element, so there is no style left to restore.
1005
+ }
1006
+ }
1007
+ async layers() {
1008
+ await this.ready();
1009
+ let latest = [];
1010
+ const onChange = (payload) => {
1011
+ if (payload.layers)
1012
+ latest = payload.layers;
1013
+ };
1014
+ this.cdp.on("LayerTree.layerTreeDidChange", onChange);
1015
+ try {
1016
+ await this.cdp.send("LayerTree.enable");
1017
+ await this.cdp.send("Page.captureScreenshot", { format: "png" });
1018
+ await this.settleLayers();
1019
+ }
1020
+ finally {
1021
+ this.cdp.off("LayerTree.layerTreeDidChange", onChange);
1022
+ // Best effort: a disable that fails leaves the domain on, which
1023
+ // costs a little work per frame but breaks nothing, and is not
1024
+ // worth failing the read over.
1025
+ await this.cdp.send("LayerTree.disable").catch(() => undefined);
1026
+ }
1027
+ // Snapshotted before anything is asked about it. Every await
1028
+ // below lets another change land, and a loop reading the live
1029
+ // array attributes one layer's reasons to another's id.
1030
+ const layers = [...latest];
1031
+ const reasons = {};
1032
+ const elements = {};
1033
+ for (const layer of layers) {
1034
+ reasons[layer.layerId] = await this.reasonsFor(layer.layerId);
1035
+ const named = await this.nameOfNode(layer.backendNodeId);
1036
+ if (named !== undefined)
1037
+ elements[layer.layerId] = named;
1038
+ }
1039
+ return foldLayers({ layers, reasons, elements });
1040
+ }
1041
+ /**
1042
+ * What the page does on hover, and whether focus does it too.
1043
+ *
1044
+ * Two halves, because either alone lies. The stylesheets say
1045
+ * which elements might hover, cheaply and in one round trip, but
1046
+ * a declared rule the cascade beat changes nothing a person can
1047
+ * see. So the scan only proposes candidates, and each one is then
1048
+ * held in hover and in focus and read, which is the only account
1049
+ * of what actually happens.
1050
+ *
1051
+ * Bounded because there is a round trip per candidate, measured
1052
+ * at roughly 9ms for the pair of states.
1053
+ */
1054
+ async hovers(limit = MAX_HOVER_CANDIDATES) {
1055
+ await this.ready();
1056
+ const scan = await this.scanForHover();
1057
+ const measurements = [];
1058
+ const seen = new Set();
1059
+ for (const selector of scan.selectors) {
1060
+ if (measurements.length >= limit)
1061
+ break;
1062
+ for (const found of await this.nodesMatching(selector)) {
1063
+ if (measurements.length >= limit)
1064
+ break;
1065
+ // One element can match two hover selectors. Measuring it
1066
+ // twice would report one treatment as two.
1067
+ if (seen.has(found))
1068
+ continue;
1069
+ seen.add(found);
1070
+ const measured = await this.measureHover(found);
1071
+ if (measured)
1072
+ measurements.push(measured);
1073
+ }
1074
+ }
1075
+ return foldHover(measurements, scan.unreadableSheets);
1076
+ }
1077
+ /** Which selectors carry a hover, as the page's own sheets say. */
1078
+ async scanForHover() {
1079
+ try {
1080
+ const { result } = await this.cdp.send("Runtime.evaluate", {
1081
+ expression: HOVER_SCAN,
1082
+ returnByValue: true,
1083
+ });
1084
+ const parsed = JSON.parse(String(result.value));
1085
+ if (typeof parsed === "object" &&
1086
+ parsed !== null &&
1087
+ "selectors" in parsed &&
1088
+ Array.isArray(parsed.selectors)) {
1089
+ const sheets = "unreadableSheets" in parsed &&
1090
+ typeof parsed.unreadableSheets === "number"
1091
+ ? parsed.unreadableSheets
1092
+ : 0;
1093
+ return {
1094
+ selectors: parsed.selectors.filter((one) => typeof one === "string"),
1095
+ unreadableSheets: sheets,
1096
+ };
1097
+ }
1098
+ return { selectors: [], unreadableSheets: 0 };
1099
+ }
1100
+ catch {
1101
+ // A page that navigated mid-scan has no stylesheets to report.
1102
+ return { selectors: [], unreadableSheets: 0 };
1103
+ }
1104
+ }
1105
+ /** Backend ids matching a selector, or none if it will not parse. */
1106
+ async nodesMatching(selector) {
1107
+ try {
1108
+ const { root } = await this.cdp.send("DOM.getDocument", { depth: 0 });
1109
+ const { nodeIds } = await this.cdp.send("DOM.querySelectorAll", {
1110
+ nodeId: root.nodeId,
1111
+ selector,
1112
+ });
1113
+ const backend = [];
1114
+ for (const nodeId of nodeIds) {
1115
+ const { node } = await this.cdp.send("DOM.describeNode", { nodeId });
1116
+ if (node.backendNodeId !== undefined)
1117
+ backend.push(node.backendNodeId);
1118
+ }
1119
+ return backend;
1120
+ }
1121
+ catch {
1122
+ // A selector Chrome will not parse, which a stylesheet can
1123
+ // legally contain once its hover part is stripped off.
1124
+ return [];
1125
+ }
1126
+ }
1127
+ /** Hold hover, then focus, and read what each changed. */
1128
+ async measureHover(backendNodeId) {
1129
+ const nodeId = await this.frontendNodeFor(backendNodeId);
1130
+ if (nodeId === undefined)
1131
+ return undefined;
1132
+ const objectId = await this.objectFor(backendNodeId);
1133
+ if (objectId === undefined)
1134
+ return undefined;
1135
+ const named = await this.nameOfNode(backendNodeId);
1136
+ const atRest = await this.stylesOf(objectId);
1137
+ if (!atRest)
1138
+ return undefined;
1139
+ const resting = curateStyles(atRest, {});
1140
+ const read = async (state) => {
1141
+ await this.cdp.send("CSS.forcePseudoState", {
1142
+ nodeId,
1143
+ forcedPseudoClasses: [state],
1144
+ });
1145
+ await this.settleForcedState(objectId);
1146
+ const held = await this.stylesOf(objectId);
1147
+ return held ? diffStyles(resting, curateStyles(held, {})) : [];
1148
+ };
1149
+ try {
1150
+ const hover = await read("hover");
1151
+ const focus = await read("focus");
1152
+ return { element: named ?? `node ${backendNodeId}`, hover, focus };
1153
+ }
1154
+ finally {
1155
+ // Always released. A page left stuck in a forced hover would
1156
+ // corrupt every later reading of it, and a restoration written
1157
+ // after the reads would not run when one of them throws.
1158
+ await this.cdp
1159
+ .send("CSS.forcePseudoState", { nodeId, forcedPseudoClasses: [] })
1160
+ .catch(() => undefined);
1161
+ }
1162
+ }
1163
+ /** Why Chrome composited one layer, or nothing if it will not say. */
1164
+ async reasonsFor(layerId) {
1165
+ try {
1166
+ const answer = await this.cdp.send("LayerTree.compositingReasons", {
1167
+ layerId,
1168
+ });
1169
+ return answer.compositingReasons ?? [];
1170
+ }
1171
+ catch {
1172
+ // The layer went away between the snapshot and the question,
1173
+ // which is silence about it rather than a reason to fail.
1174
+ return [];
1175
+ }
1176
+ }
1177
+ /** A short name for the element behind a layer. */
1178
+ async nameOfNode(backendNodeId) {
1179
+ if (backendNodeId === undefined)
1180
+ return undefined;
1181
+ try {
1182
+ const { node } = await this.cdp.send("DOM.describeNode", {
1183
+ backendNodeId,
1184
+ });
1185
+ return nameNode(node.nodeName, node.attributes);
1186
+ }
1187
+ catch {
1188
+ // A node that has left the document cannot be named, and a
1189
+ // layer without a name is still worth reporting by id.
1190
+ return undefined;
1191
+ }
1192
+ }
1193
+ /** Give the compositor a moment to finish reporting. */
1194
+ settleLayers() {
1195
+ return new Promise((resolve) => setTimeout(resolve, LAYER_SETTLE_MS));
1196
+ }
1197
+ /** Socket events dropped to stay within the buffer's budget. */
1198
+ get socketFramesDropped() {
1199
+ return this.telemetry.socketFramesDropped;
1200
+ }
1201
+ /** Where the page has been. */
1202
+ get history() {
1203
+ return this.telemetry.history;
1204
+ }
1205
+ /**
1206
+ * Put the emulation back exactly as it was.
1207
+ *
1208
+ * emulate merges, which is right for a caller adding one
1209
+ * condition and wrong for anything that has to undo itself: a
1210
+ * merge cannot clear a field, so a sweep that widened the
1211
+ * viewport could never restore a session that had none. This
1212
+ * replaces the state wholesale.
1213
+ */
1214
+ async restoreEmulation(state) {
1215
+ await this.emulation.restore(state);
1216
+ }
1217
+ /**
1218
+ * Pretend to be a different visitor, then report what the page
1219
+ * actually experiences.
1220
+ *
1221
+ * The intent is kept whole and re-applied in full, because
1222
+ * Chrome's media emulation forgets anything a call omits, so
1223
+ * asking for reduced motion after asking for dark mode would
1224
+ * otherwise turn the dark mode back off.
1225
+ */
1226
+ async emulate(change = {},
1227
+ /** Overrides to take off, which an absent key cannot express. */
1228
+ clear = []) {
1229
+ return this.emulation.change(change, clear);
1230
+ }
1231
+ /** What this session has asked the browser to pretend. */
1232
+ get emulated() {
1233
+ return this.emulation.asked;
1234
+ }
1235
+ /**
1236
+ * Read what the page has kept.
1237
+ *
1238
+ * A store that cannot be read says why rather than coming
1239
+ * back empty, since an empty store and an unreadable one lead
1240
+ * to opposite conclusions.
1241
+ */
1242
+ async storage(wanted) {
1243
+ await this.ready();
1244
+ const snapshot = { unavailable: {} };
1245
+ if (wanted.local) {
1246
+ snapshot.local = await this.domStorage(true, snapshot.unavailable);
1247
+ }
1248
+ if (wanted.session) {
1249
+ snapshot.session = await this.domStorage(false, snapshot.unavailable);
1250
+ }
1251
+ if (wanted.cookies) {
1252
+ try {
1253
+ const { cookies } = await this.cdp.send("Network.getCookies");
1254
+ snapshot.cookies = cookies;
1255
+ }
1256
+ catch (err) {
1257
+ snapshot.unavailable.cookies = String(err);
1258
+ }
1259
+ }
1260
+ if (wanted.clipboard) {
1261
+ const read = await this.clipboard();
1262
+ if (read.ok)
1263
+ snapshot.clipboard = read.text;
1264
+ else
1265
+ snapshot.unavailable.clipboard = read.why;
1266
+ }
1267
+ const { unavailable, ...rest } = snapshot;
1268
+ return Object.keys(unavailable).length > 0
1269
+ ? { ...rest, unavailable }
1270
+ : rest;
1271
+ }
1272
+ /** One of the two DOM stores, keyed by this frame's origin. */
1273
+ async domStorage(isLocalStorage, unavailable) {
1274
+ const label = isLocalStorage ? "local storage" : "session storage";
1275
+ try {
1276
+ await this.cdp.send("DOMStorage.enable");
1277
+ const { entries } = await this.cdp.send("DOMStorage.getDOMStorageItems", {
1278
+ storageId: { securityOrigin: await this.origin(), isLocalStorage },
1279
+ });
1280
+ // The protocol types an entry as a loose string array; it
1281
+ // is always a key and a value, which is what the pair type
1282
+ // says and what the renderer reads.
1283
+ return entries.map((entry) => [entry[0] ?? "", entry[1] ?? ""]);
1284
+ }
1285
+ catch (err) {
1286
+ // A page on about:blank or a file url has no security
1287
+ // origin to key a store by, which is a fact about where we
1288
+ // are rather than a failure to report.
1289
+ unavailable[label] = String(err);
1290
+ return undefined;
1291
+ }
1292
+ }
1293
+ /** Put a value into one of the DOM stores. */
1294
+ async setStored(isLocalStorage, key, value) {
1295
+ await this.ready();
1296
+ await this.cdp.send("DOMStorage.enable");
1297
+ await this.cdp.send("DOMStorage.setDOMStorageItem", {
1298
+ storageId: { securityOrigin: await this.origin(), isLocalStorage },
1299
+ key,
1300
+ value,
1301
+ });
1302
+ }
1303
+ /**
1304
+ * Measure the space between two elements.
1305
+ *
1306
+ * The border box is the one measured, because that is the edge
1307
+ * a designer sees and points at. Margins are the space itself,
1308
+ * so measuring between margin boxes would report the gap that
1309
+ * remains after the gap, which is nearly always zero.
1310
+ */
1311
+ async measure(a, b) {
1312
+ // Which target failed matters: the caller named two, and a
1313
+ // refusal that does not say which one leaves them guessing.
1314
+ const first = await this.inspect(a);
1315
+ if (!first.ok)
1316
+ return { ok: false, target: a, refusal: first.refusal };
1317
+ const second = await this.inspect(b);
1318
+ if (!second.ok)
1319
+ return { ok: false, target: b, refusal: second.refusal };
1320
+ const boxA = first.inspection.box;
1321
+ const boxB = second.inspection.box;
1322
+ if (boxA === undefined || boxB === undefined) {
1323
+ // An element with no box is not laid out, so there is no
1324
+ // space between it and anything. Saying which one it was
1325
+ // saves the caller inspecting both to find out.
1326
+ const missing = boxA === undefined ? a : b;
1327
+ return {
1328
+ ok: false,
1329
+ problem: `${missing.role} ${missing.name} has no box, so it is not ` +
1330
+ "laid out and there is no space between it and anything.",
1331
+ };
1332
+ }
1333
+ return { ok: true, measurement: measureBetween(boxA.border, boxB.border) };
1334
+ }
1335
+ /**
1336
+ * Every tab this session's context is holding open.
1337
+ *
1338
+ * A page that opens another one is doing something ordinary: a
1339
+ * target of _blank, a sign-in or a payment handed to a second
1340
+ * window. Until this existed the new tab was real, live and
1341
+ * entirely invisible, and the click that made it looked like a
1342
+ * click that did nothing.
1343
+ */
1344
+ async tabs() {
1345
+ await this.ready();
1346
+ const pages = await this.context.pages();
1347
+ return await Promise.all(pages.map(async (page, at) => ({
1348
+ index: at + 1,
1349
+ url: page.url(),
1350
+ // A tab still loading has no title yet, and asking a closing
1351
+ // one throws. Either way the url still identifies it, which
1352
+ // is what the caller picks by.
1353
+ title: await page.title().catch(() => ""),
1354
+ current: page === this.page,
1355
+ })));
1356
+ }
1357
+ /**
1358
+ * Drive a different tab from now on.
1359
+ *
1360
+ * The tab left behind is not closed. A sign-in that opened a
1361
+ * second window usually wants the first one back afterwards, and
1362
+ * closing it would make switching a one-way trip.
1363
+ */
1364
+ async switchTab(index) {
1365
+ const open = await this.tabs();
1366
+ const chosen = chooseTab(open, index);
1367
+ if ("refusal" in chosen)
1368
+ return chosen;
1369
+ const pages = await this.context.pages();
1370
+ const page = pages[chosen.index - 1];
1371
+ if (page === undefined) {
1372
+ return {
1373
+ refusal: `Tab ${index} closed between being listed and being chosen.`,
1374
+ };
1375
+ }
1376
+ if (page === this.page)
1377
+ return { ...chosen, current: true };
1378
+ await page.bringToFront();
1379
+ await this.bindTo(page);
1380
+ return { ...chosen, current: true };
1381
+ }
1382
+ /**
1383
+ * Keep everything that makes this session signed in.
1384
+ *
1385
+ * Cookies and the two DOM stores together, because being logged
1386
+ * in is rarely all in one of them: a session cookie and a token
1387
+ * in local storage is the ordinary arrangement, and saving half
1388
+ * of it produces a state that restores without signing anyone
1389
+ * in.
1390
+ */
1391
+ async saveState() {
1392
+ await this.ready();
1393
+ const snapshot = await this.storage({
1394
+ local: true,
1395
+ session: true,
1396
+ cookies: true,
1397
+ });
1398
+ return captureState(await this.origin(), snapshot);
1399
+ }
1400
+ /**
1401
+ * Wear a state saved earlier, and report exactly what landed.
1402
+ *
1403
+ * Cookies go to the browser, so they apply whatever page is
1404
+ * open. The DOM stores are scoped to an origin, so they can only
1405
+ * be written while the session is on the origin they came from,
1406
+ * and the answer says when they were not rather than counting
1407
+ * them as restored. A caller who is told they are signed in and
1408
+ * is not will blame the site.
1409
+ */
1410
+ async loadState(state) {
1411
+ await this.ready();
1412
+ if (state.cookies.length > 0) {
1413
+ await this.cdp.send("Network.setCookies", {
1414
+ cookies: state.cookies.map(({ sameSite, ...rest }) => ({
1415
+ ...rest,
1416
+ // A saved file can hold any string here, and the protocol
1417
+ // takes three. An unrecognised one is dropped rather than
1418
+ // passed on: without the attribute the browser applies its
1419
+ // own default, which is what a cookie with a nonsense
1420
+ // value would have got anyway, and sending it refuses the
1421
+ // whole batch and signs nobody in.
1422
+ ...(sameSite === "Strict" || sameSite === "Lax" || sameSite === "None"
1423
+ ? { sameSite }
1424
+ : {}),
1425
+ })),
1426
+ });
1427
+ }
1428
+ const here = await this.origin();
1429
+ if (here !== state.origin) {
1430
+ return {
1431
+ cookies: state.cookies.length,
1432
+ local: 0,
1433
+ session: 0,
1434
+ wrongOrigin: here,
1435
+ };
1436
+ }
1437
+ for (const [key, value] of state.local) {
1438
+ await this.setStored(true, key, value);
1439
+ }
1440
+ for (const [key, value] of state.session) {
1441
+ await this.setStored(false, key, value);
1442
+ }
1443
+ return {
1444
+ cookies: state.cookies.length,
1445
+ local: state.local.length,
1446
+ session: state.session.length,
1447
+ };
1448
+ }
1449
+ /** Empty the stores named, and only those. */
1450
+ async clearStorage(wanted) {
1451
+ await this.ready();
1452
+ if (wanted.local || wanted.session) {
1453
+ await this.cdp.send("DOMStorage.enable");
1454
+ const securityOrigin = await this.origin();
1455
+ if (wanted.local) {
1456
+ await this.cdp.send("DOMStorage.clear", {
1457
+ storageId: { securityOrigin, isLocalStorage: true },
1458
+ });
1459
+ }
1460
+ if (wanted.session) {
1461
+ await this.cdp.send("DOMStorage.clear", {
1462
+ storageId: { securityOrigin, isLocalStorage: false },
1463
+ });
1464
+ }
1465
+ }
1466
+ if (wanted.cookies)
1467
+ await this.cdp.send("Network.clearBrowserCookies");
1468
+ }
1469
+ /**
1470
+ * Read the clipboard.
1471
+ *
1472
+ * Chrome refuses this unless the permission is granted against
1473
+ * the browser context, not merely the origin, and unless the
1474
+ * read is attributed to a user gesture. Both were measured;
1475
+ * either alone still gets a refusal.
1476
+ */
1477
+ async clipboard() {
1478
+ try {
1479
+ await this.grantClipboard();
1480
+ const response = await this.cdp.send("Runtime.evaluate", {
1481
+ expression: "navigator.clipboard.readText()",
1482
+ awaitPromise: true,
1483
+ userGesture: true,
1484
+ returnByValue: true,
1485
+ });
1486
+ // Runtime.evaluate resolves with exceptionDetails rather
1487
+ // than rejecting, so a refused permission arrived here as
1488
+ // an undefined value and was reported as an empty
1489
+ // clipboard. "Nothing is on the clipboard" and "the page
1490
+ // would not let us look" are different answers, and only
1491
+ // one of them is about the page.
1492
+ if (response.exceptionDetails) {
1493
+ return {
1494
+ ok: false,
1495
+ why: describeThrow(response.exceptionDetails).message,
1496
+ };
1497
+ }
1498
+ return { ok: true, text: String(response.result.value ?? "") };
1499
+ }
1500
+ catch (err) {
1501
+ return { ok: false, why: String(err) };
1502
+ }
1503
+ }
1504
+ /** Put text on the clipboard. */
1505
+ async writeClipboard(text) {
1506
+ await this.ready();
1507
+ await this.grantClipboard();
1508
+ const response = await this.cdp.send("Runtime.evaluate", {
1509
+ expression: `navigator.clipboard.writeText(${JSON.stringify(text)})`,
1510
+ awaitPromise: true,
1511
+ userGesture: true,
1512
+ });
1513
+ // This inspected nothing at all, so a refused write was
1514
+ // reported to the caller as a success.
1515
+ if (response.exceptionDetails) {
1516
+ const threw = describeThrow(response.exceptionDetails);
1517
+ throw new Error(`Could not write the clipboard: ${threw.message}`);
1518
+ }
1519
+ }
1520
+ /** Ask for clipboard access against this context. */
1521
+ async grantClipboard() {
1522
+ await this.cdp.send("Browser.grantPermissions", {
1523
+ origin: await this.origin(),
1524
+ browserContextId: this.context.id,
1525
+ permissions: ["clipboardReadWrite", "clipboardSanitizedWrite"],
1526
+ });
1527
+ }
1528
+ /** The origin the current page's stores are keyed by. */
1529
+ async origin() {
1530
+ return new URL(this.page.url()).origin;
1531
+ }
1532
+ /**
1533
+ * Bend the network: mock a reply, block a request, slow it all
1534
+ * down, or stop pretending.
1535
+ *
1536
+ * Interception is only attached while there is a rule to
1537
+ * apply. Every paused request has to be answered or the page
1538
+ * waits on it forever, so an interceptor with nothing to say
1539
+ * is a liability rather than a neutral bystander.
1540
+ */
1541
+ async shape(change) {
1542
+ return this.shaper.shape(change);
1543
+ }
1544
+ /**
1545
+ * Press a chord, or a sequence of them.
1546
+ *
1547
+ * Dispatch goes through the driver's keyboard rather than the
1548
+ * protocol directly, because the driver owns the layout table
1549
+ * that turns a key name into a code, a virtual key number and
1550
+ * the text it produces. Keeping a second opinion about that
1551
+ * would be a second thing to be wrong about.
1552
+ */
1553
+ async press(keys) {
1554
+ await this.ready();
1555
+ const parsed = parseChords(keys, KNOWN_KEYS);
1556
+ if ("refusal" in parsed)
1557
+ return parsed;
1558
+ const pressed = [];
1559
+ for (const chord of parsed.chords) {
1560
+ for (const modifier of chord.modifiers) {
1561
+ await this.page.keyboard.down(modifier);
1562
+ }
1563
+ try {
1564
+ await this.page.keyboard.press(chord.key);
1565
+ }
1566
+ finally {
1567
+ // Whatever the key press did, the modifiers come back up.
1568
+ // A chord that threw mid-press used to leave Control or
1569
+ // Meta logically held for the life of a session that is
1570
+ // long lived by design, so every later click became a
1571
+ // modified click and the symptom appeared calls away
1572
+ // from the cause.
1573
+ for (const modifier of [...chord.modifiers].reverse()) {
1574
+ await this.page.keyboard.up(modifier).catch(() => {
1575
+ // The page may be gone; there is no key state left
1576
+ // to corrupt if it is.
1577
+ });
1578
+ }
1579
+ }
1580
+ pressed.push([...chord.modifiers, chord.key].join("+"));
1581
+ }
1582
+ return { pressed };
1583
+ }
1584
+ /** Send raw text as keystrokes, wherever focus happens to be. */
1585
+ async typeRaw(text) {
1586
+ await this.ready();
1587
+ await this.page.keyboard.type(text);
1588
+ }
1589
+ /** Dispatch a composed mouse gesture. */
1590
+ async pointerGesture(steps) {
1591
+ await this.ready();
1592
+ for (const step of steps) {
1593
+ await this.cdp.send("Input.dispatchMouseEvent", {
1594
+ type: step.type,
1595
+ x: step.x,
1596
+ y: step.y,
1597
+ button: step.button,
1598
+ clickCount: step.clickCount,
1599
+ });
1600
+ }
1601
+ }
1602
+ /** Scroll by wheel, which is not the same as scrolling by script. */
1603
+ async wheel(at, byX, byY) {
1604
+ await this.ready();
1605
+ await this.cdp.send("Input.dispatchMouseEvent", {
1606
+ type: "mouseWheel",
1607
+ x: at.x,
1608
+ y: at.y,
1609
+ deltaX: byX,
1610
+ deltaY: byY,
1611
+ button: "none",
1612
+ clickCount: 0,
1613
+ });
1614
+ }
1615
+ /**
1616
+ * Dispatch a composed touch gesture.
1617
+ *
1618
+ * Touch events reach the page whether or not touch emulation
1619
+ * is on, so this never refuses. What emulation changes is what
1620
+ * the page believes about itself: maxTouchPoints and the
1621
+ * coarse-pointer media query. A page that decides at load time
1622
+ * whether to attach touch handlers will not have attached any,
1623
+ * and the gesture lands on nothing, so the caller is told
1624
+ * rather than stopped.
1625
+ */
1626
+ async touchGesture(steps) {
1627
+ await this.ready();
1628
+ for (const step of steps) {
1629
+ await this.cdp.send("Input.dispatchTouchEvent", {
1630
+ type: step.type,
1631
+ touchPoints: step.points.map((point) => ({
1632
+ x: point.x,
1633
+ y: point.y,
1634
+ id: point.id,
1635
+ })),
1636
+ });
1637
+ if (step.pauseMs) {
1638
+ await new Promise((resolve) => setTimeout(resolve, step.pauseMs));
1639
+ }
1640
+ }
1641
+ }
1642
+ /**
1643
+ * Walk the page with the Tab key and report what happened.
1644
+ *
1645
+ * This is one of the two reads that deliberately change the
1646
+ * page, since there is no way to learn where focus goes
1647
+ * without moving it. Focus is put back afterwards and the
1648
+ * result says so.
1649
+ *
1650
+ * The walk runs to twice the number of focusable things plus a
1651
+ * little, which is enough for a healthy page to come round
1652
+ * twice and for a trap to show its cycle. Stopping sooner
1653
+ * would report a trap as a short page.
1654
+ */
1655
+ async keyboardWalk(maxStops) {
1656
+ await this.ready();
1657
+ // Remember where the caller's focus and viewport were before
1658
+ // disturbing either, so the page can be handed back as found.
1659
+ await this.evaluateJson(WALK_REMEMBER);
1660
+ // Start from the top of the document so the first Tab lands
1661
+ // where a reader arriving at the page would land, not
1662
+ // wherever something happened to leave focus.
1663
+ //
1664
+ // This has to happen before the resting styles are collected,
1665
+ // not after. A dialog opened with showModal focuses its first
1666
+ // control, so collecting first recorded that control's focused
1667
+ // appearance as its resting one, and comparing the two later
1668
+ // found no difference and reported a perfectly visible focus
1669
+ // ring as missing.
1670
+ await this.page.evaluate(() => {
1671
+ if (document.activeElement instanceof HTMLElement) {
1672
+ document.activeElement.blur();
1673
+ }
1674
+ });
1675
+ const collected = await this.evaluateJson(WALK_COLLECT);
1676
+ // The ceiling belongs to the default, not to the caller. It
1677
+ // used to apply to both, so a page with more controls than
1678
+ // the ceiling could not be walked to the end by any argument,
1679
+ // while the report that ran out of budget told the caller to
1680
+ // raise maxStops and walk the rest. Every larger number gave
1681
+ // the same four hundred stops and the same advice again.
1682
+ const cap = maxStops ??
1683
+ Math.min(collected.candidates.length * 2 + WALK_SLACK, MAX_WALK_STOPS);
1684
+ const stops = [];
1685
+ for (let step = 0; step < cap; step += 1) {
1686
+ await this.page.keyboard.press("Tab");
1687
+ const stop = await this.evaluateJson(WALK_READ);
1688
+ if (stop.focused === null) {
1689
+ stops.push({ ...stop, focused: BLANK_STYLE });
1690
+ continue;
1691
+ }
1692
+ stops.push(stop);
1693
+ }
1694
+ // If the walk ended stuck somewhere smaller than the page,
1695
+ // find out whether a keyboard user has any way back. That is
1696
+ // the difference between a modal and a dead end.
1697
+ //
1698
+ // The test used to be "any index repeats in the last four
1699
+ // stops", which every page whose tab cycle is three stops or
1700
+ // shorter satisfies on a healthy run, so Escape was pressed on
1701
+ // ordinary login forms. A page is only worth probing when the
1702
+ // loop it settled into is smaller than the set of things it
1703
+ // could have reached.
1704
+ let escapeFreed;
1705
+ const tail = stops.slice(-WALK_STUCK_SAMPLE).map((stop) => stop.index);
1706
+ const reached = new Set(stops.map((stop) => stop.index).filter((index) => index >= 0));
1707
+ const loopedSmall = tail.length > 0 &&
1708
+ new Set(tail).size < tail.length &&
1709
+ reached.size < collected.candidates.length;
1710
+ if (loopedSmall) {
1711
+ const before = tail.at(-1);
1712
+ await this.page.keyboard.press("Escape");
1713
+ await this.page.keyboard.press("Tab");
1714
+ const after = await this.evaluateJson(WALK_READ);
1715
+ escapeFreed = after.index !== before && !tail.includes(after.index);
1716
+ }
1717
+ await this.evaluateJson(WALK_RESTORE);
1718
+ // A walk that used its whole budget has not necessarily seen
1719
+ // the page. Saying so is what stops everything it did not
1720
+ // reach being reported as unreachable.
1721
+ const exhausted = stops.length >= cap && reached.size < collected.candidates.length;
1722
+ return {
1723
+ candidates: collected.candidates,
1724
+ stops,
1725
+ unreachable: collected.unreachable,
1726
+ ...(escapeFreed === undefined ? {} : { escapeFreed }),
1727
+ ...(exhausted ? { cappedAt: cap } : {}),
1728
+ // Which way the page reads has to come from the page. The
1729
+ // side-by-side order that is wrong in English is correct in
1730
+ // Arabic, and guessing left-to-right would report every
1731
+ // right-to-left toolbar as backwards.
1732
+ direction: await this.readingDirection(),
1733
+ };
1734
+ }
1735
+ /** Which way the document says it reads. */
1736
+ async readingDirection() {
1737
+ try {
1738
+ const reads = await this.page.evaluate(() => getComputedStyle(document.documentElement).direction);
1739
+ return reads === "rtl" ? "rtl" : "ltr";
1740
+ }
1741
+ catch {
1742
+ // Left-to-right is the safe default: it is what most pages
1743
+ // are, and being wrong here only costs a finding nobody
1744
+ // asked for rather than a missed barrier.
1745
+ return "ltr";
1746
+ }
1747
+ }
1748
+ /**
1749
+ * The whole page flattened, frames and shadow content
1750
+ * included.
1751
+ *
1752
+ * The style properties have to be named up front because the
1753
+ * protocol returns their values as a bare array positioned
1754
+ * against the request. Asking for a handful keeps the snapshot
1755
+ * small; asking for none still gives geometry and structure.
1756
+ */
1757
+ async snapshot(styleProperties = SNAPSHOT_STYLES) {
1758
+ await this.ready();
1759
+ const raw = await this.cdp.send("DOMSnapshot.captureSnapshot", {
1760
+ computedStyles: [...styleProperties],
1761
+ includeDOMRects: true,
1762
+ });
1763
+ return flattenSnapshot(raw, styleProperties);
1764
+ }
1765
+ /**
1766
+ * Run an expression in the page and describe what came back.
1767
+ *
1768
+ * The value is serialized inside the page rather than by the
1769
+ * protocol, because the protocol does not decline politely: a
1770
+ * circular object rejects the whole call with "object
1771
+ * reference chain is too long", losing the evaluation and any
1772
+ * account of it together.
1773
+ *
1774
+ * The protocol can still fail for its own reasons, so that is
1775
+ * caught too and returned as a refusal rather than thrown. An
1776
+ * expression that kills the session is not a useful tool.
1777
+ */
1778
+ async evaluate(expression) {
1779
+ await this.ready();
1780
+ try {
1781
+ const response = await this.cdp.send("Runtime.evaluate", {
1782
+ expression: evaluationSource(expression),
1783
+ returnByValue: true,
1784
+ awaitPromise: true,
1785
+ userGesture: true,
1786
+ });
1787
+ if (response.exceptionDetails) {
1788
+ const threw = describeThrow(response.exceptionDetails);
1789
+ return {
1790
+ ok: false,
1791
+ threw: {
1792
+ ...threw,
1793
+ frames: await this.resolveFrames(threw.frames),
1794
+ },
1795
+ };
1796
+ }
1797
+ const value = response.result.value;
1798
+ if (!value) {
1799
+ return {
1800
+ ok: false,
1801
+ refused: "The page returned nothing the serializer could read.",
1802
+ };
1803
+ }
1804
+ return { ok: true, result: value };
1805
+ }
1806
+ catch (error) {
1807
+ return {
1808
+ ok: false,
1809
+ refused: `The browser refused to run that: ` +
1810
+ `${error instanceof Error ? error.message : String(error)}`,
1811
+ };
1812
+ }
1813
+ }
1814
+ /**
1815
+ * Run axe-core against the page and read back its findings.
1816
+ *
1817
+ * axe is injected fresh on every call rather than kept across
1818
+ * navigations. It is half a megabyte, but a navigation clears
1819
+ * the world, and an audit that silently reported nothing
1820
+ * because the library went away with the last page would be
1821
+ * worse than a slow one.
1822
+ *
1823
+ * Only violations and incomplete results are asked for. Passes
1824
+ * and inapplicable rules together outweigh them by an order of
1825
+ * magnitude and answer a question nobody asks.
1826
+ *
1827
+ * The experimental rules are switched on. axe leaves them off,
1828
+ * and five of them carry real WCAG criteria, so leaving them
1829
+ * off means 2.5.3 (label in name) is never checked and 1.3.1
1830
+ * is checked less thoroughly than it looks. An accessibility
1831
+ * reviewer reported label in name as missing, and it was, in
1832
+ * the sense that matters: the rule sat in the bundle unused.
1833
+ * Their findings are reported as needing a person rather than
1834
+ * as failures, since axe's own doubt travels with them.
1835
+ */
1836
+ async audit(bar = "AAA") {
1837
+ await this.ready();
1838
+ const source = await readFile(axeSource(), "utf8");
1839
+ await this.cdp.send("Runtime.evaluate", { expression: source });
1840
+ const response = await this.cdp.send("Runtime.evaluate", {
1841
+ expression: "axe.run(document, { " +
1842
+ 'resultTypes: ["violations", "incomplete"], ' +
1843
+ `rules: ${JSON.stringify(enabledRules(bar))} })`,
1844
+ awaitPromise: true,
1845
+ returnByValue: true,
1846
+ });
1847
+ if (response.exceptionDetails) {
1848
+ const threw = describeThrow(response.exceptionDetails);
1849
+ throw new Error(`axe could not run: ${threw.message}`);
1850
+ }
1851
+ return readAxeRun((response.result.value ?? {}));
1852
+ }
1853
+ /**
1854
+ * The page as the structural rules need to see it.
1855
+ *
1856
+ * Two accounts from the browser, joined on the backend node id
1857
+ * they share: the snapshot for attributes and layout, the
1858
+ * accessibility tree for roles and names. Neither carries
1859
+ * both, and every structural rule needs both.
1860
+ */
1861
+ /**
1862
+ * What holds focus at this moment, moving nothing.
1863
+ *
1864
+ * The keyboard walk answers what a whole page does; this answers
1865
+ * where focus is right now, which is the question between
1866
+ * actions. It reads rather than presses, so it can be asked after
1867
+ * a click or a navigation without disturbing what it reports.
1868
+ */
1869
+ async focusHolder() {
1870
+ await this.ready();
1871
+ const { result } = await this.cdp.send("Runtime.evaluate", {
1872
+ expression: FOCUS_PROBE,
1873
+ returnByValue: true,
1874
+ });
1875
+ return result.value;
1876
+ }
1877
+ async structure() {
1878
+ // Every sibling read waits out a crash recovery first, and
1879
+ // this one did not. Promise.all evaluates this.cdp.send when
1880
+ // the array is built, so a call landing during recovery sent
1881
+ // getFullAXTree down the channel recover() was replacing.
1882
+ await this.ready();
1883
+ const [nodes, tree] = await Promise.all([
1884
+ this.snapshot(),
1885
+ this.cdp.send("Accessibility.getFullAXTree"),
1886
+ ]);
1887
+ const facts = [];
1888
+ for (const axNode of tree.nodes) {
1889
+ if (axNode.backendDOMNodeId === undefined)
1890
+ continue;
1891
+ const focusable = axNode.properties?.find((property) => property.name === "focusable");
1892
+ facts.push({
1893
+ backendNodeId: axNode.backendDOMNodeId,
1894
+ ...(axNode.role?.value === undefined
1895
+ ? {}
1896
+ : { role: axNode.role.value }),
1897
+ ...(axNode.name?.value === undefined
1898
+ ? {}
1899
+ : { name: axNode.name.value }),
1900
+ // Left absent when the tree did not say, rather than
1901
+ // flattened to false. An ignored node carries no
1902
+ // properties at all, and reporting that as "cannot take
1903
+ // focus" is how the hidden-but-focusable rule ended up
1904
+ // unable to see its own subject.
1905
+ ...(focusable === undefined
1906
+ ? {}
1907
+ : { focusable: focusable.value?.value === true }),
1908
+ });
1909
+ }
1910
+ return buildStructure(nodes, facts);
1911
+ }
1912
+ /**
1913
+ * What the layout actually did, as the browser measured it.
1914
+ *
1915
+ * Read in one page-side pass rather than a protocol call per
1916
+ * element: a page of any size would otherwise cost thousands
1917
+ * of round trips to answer one question.
1918
+ */
1919
+ /**
1920
+ * Every pointer target on the page, with the facts WCAG 2.5.8
1921
+ * needs to judge its size.
1922
+ *
1923
+ * Separate from layout() because the two ask different
1924
+ * questions: layout wants everything drawn, this wants only
1925
+ * what a finger has to hit, plus the two exceptions the
1926
+ * criterion turns on.
1927
+ */
1928
+ async targets() {
1929
+ await this.ready();
1930
+ const response = await this.cdp.send("Runtime.evaluate", {
1931
+ expression: TARGET_CAPTURE,
1932
+ returnByValue: true,
1933
+ });
1934
+ if (response.exceptionDetails) {
1935
+ const threw = describeThrow(response.exceptionDetails);
1936
+ throw new Error(`Could not measure the targets: ${threw.message}`);
1937
+ }
1938
+ return response.result.value;
1939
+ }
1940
+ async layout() {
1941
+ await this.ready();
1942
+ const response = await this.cdp.send("Runtime.evaluate", {
1943
+ expression: visualCaptureSource(),
1944
+ returnByValue: true,
1945
+ });
1946
+ if (response.exceptionDetails) {
1947
+ const threw = describeThrow(response.exceptionDetails);
1948
+ throw new Error(`Could not read the layout: ${threw.message}`);
1949
+ }
1950
+ return response.result.value;
1951
+ }
1952
+ /**
1953
+ * What the page is built from, sampled element by element.
1954
+ *
1955
+ * Read in one page-side pass, and filtered there rather than
1956
+ * here: an inherited value is not a decision, and carrying
1957
+ * every element's inherited colour back only to discard it
1958
+ * would be most of the payload.
1959
+ */
1960
+ async styleSamples() {
1961
+ await this.ready();
1962
+ const response = await this.cdp.send("Runtime.evaluate", {
1963
+ expression: inventorySource(),
1964
+ returnByValue: true,
1965
+ });
1966
+ if (response.exceptionDetails) {
1967
+ const threw = describeThrow(response.exceptionDetails);
1968
+ throw new Error(`Could not sample the styles: ${threw.message}`);
1969
+ }
1970
+ return (response.result.value ?? []);
1971
+ }
1972
+ /**
1973
+ * Install the performance observers ahead of every page.
1974
+ *
1975
+ * Largest contentful paint and layout shift are events rather
1976
+ * than state, so an observer registered after a page loads has
1977
+ * missed them. The buffered flag recovers the timings but not
1978
+ * the element that painted or the nodes that moved, which is
1979
+ * the half worth having, so this goes in through the hook that
1980
+ * runs before the document does.
1981
+ */
1982
+ async watchVitals() {
1983
+ await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", {
1984
+ source: observerBootstrap(),
1985
+ });
1986
+ }
1987
+ /** What the current page cost to show. */
1988
+ async vitals() {
1989
+ await this.ready();
1990
+ const response = await this.cdp.send("Runtime.evaluate", {
1991
+ expression: readVitalsSource(),
1992
+ returnByValue: true,
1993
+ });
1994
+ if (response.exceptionDetails) {
1995
+ const threw = describeThrow(response.exceptionDetails);
1996
+ return { shifts: [], longTasks: [], paints: {}, error: threw.message };
1997
+ }
1998
+ return response.result.value;
1999
+ }
2000
+ /** Run a page-side source string and read back its value. */
2001
+ async evaluateJson(source) {
2002
+ const { result } = await this.cdp.send("Runtime.evaluate", {
2003
+ expression: source,
2004
+ returnByValue: true,
2005
+ });
2006
+ return result.value;
2007
+ }
2008
+ /** Whether the page believes it is being touched. */
2009
+ get touchEmulated() {
2010
+ return this.emulation.touch;
2011
+ }
2012
+ /**
2013
+ * Wait for the page to reach a state, and say what happened.
2014
+ *
2015
+ * The timeout is a real answer rather than an error. A page
2016
+ * that never reaches the state is the finding, so the outcome
2017
+ * carries what was true instead.
2018
+ */
2019
+ async waitFor(condition, timeoutMs = DEFAULT_WAIT_MS) {
2020
+ await this.ready();
2021
+ const startedAt = Date.now();
2022
+ const waited = () => Date.now() - startedAt;
2023
+ if (condition.kind === "duration") {
2024
+ await new Promise((resolve) => setTimeout(resolve, condition.ms));
2025
+ return { met: true, waitedMs: waited(), condition };
2026
+ }
2027
+ // Where the request log stood when the wait began. Waiting
2028
+ // for a request used to search the whole session's log, which
2029
+ // is never cleared, so the second lap of an observe-act-wait
2030
+ // loop was satisfied instantly by the previous lap's response
2031
+ // and reported its old status. That is a green wait in the
2032
+ // log hiding a race, which is the most expensive shape of
2033
+ // test defect to chase.
2034
+ const since = this.telemetry.protocolNow();
2035
+ while (waited() < timeoutMs) {
2036
+ const reached = await this.check(condition, since);
2037
+ if (reached.met) {
2038
+ return {
2039
+ met: true,
2040
+ waitedMs: waited(),
2041
+ condition,
2042
+ ...(reached.detail === undefined ? {} : { detail: reached.detail }),
2043
+ };
2044
+ }
2045
+ await new Promise((resolve) => setTimeout(resolve, WAIT_POLL_MS));
2046
+ }
2047
+ const missed = await this.check(condition);
2048
+ return {
2049
+ met: false,
2050
+ waitedMs: waited(),
2051
+ condition,
2052
+ ...(missed.saw === undefined ? {} : { saw: missed.saw }),
2053
+ };
2054
+ }
2055
+ /** Look once: is the condition true right now? */
2056
+ async check(condition,
2057
+ /**
2058
+ * Protocol time the wait began. A request that finished
2059
+ * before this belongs to an earlier lap and cannot satisfy
2060
+ * the condition.
2061
+ */
2062
+ since) {
2063
+ switch (condition.kind) {
2064
+ case "selector":
2065
+ case "gone": {
2066
+ // A probe that could not run is not an absent element. A
2067
+ // detached frame or a dead renderer used to come back as
2068
+ // null, which satisfied kind "gone" outright, so the wait
2069
+ // succeeded precisely when the page was broken and the
2070
+ // real failure surfaced several calls later.
2071
+ let found;
2072
+ try {
2073
+ found = await this.page.$(condition.selector);
2074
+ }
2075
+ catch (error) {
2076
+ return {
2077
+ met: false,
2078
+ saw: `The page could not be asked: ${String(error)}`,
2079
+ };
2080
+ }
2081
+ const present = found !== null;
2082
+ if (found)
2083
+ await found.dispose();
2084
+ const met = condition.kind === "selector" ? present : !present;
2085
+ return {
2086
+ met,
2087
+ ...(met
2088
+ ? {}
2089
+ : {
2090
+ saw: present
2091
+ ? "It is still there."
2092
+ : "Nothing matches that selector.",
2093
+ }),
2094
+ };
2095
+ }
2096
+ case "text": {
2097
+ const has = await this.page
2098
+ .evaluate((needle) => (document.body?.innerText ?? "").includes(needle), condition.text)
2099
+ .catch(() => false);
2100
+ return {
2101
+ met: has,
2102
+ ...(has ? {} : { saw: "The page does not contain it." }),
2103
+ };
2104
+ }
2105
+ case "idle": {
2106
+ const requests = this.requests();
2107
+ const met = isIdle(requests, condition.quietMs, this.telemetry.protocolNow());
2108
+ const busy = inFlight(requests);
2109
+ return {
2110
+ met,
2111
+ ...(met
2112
+ ? {}
2113
+ : {
2114
+ saw: busy.length > 0
2115
+ ? `${busy.length} still in flight, including ` +
2116
+ `${busy[0]?.url}.`
2117
+ : "The page keeps starting new requests.",
2118
+ }),
2119
+ };
2120
+ }
2121
+ case "request": {
2122
+ const fresh = this.requests().filter((request) => since === undefined || request.startedAt >= since);
2123
+ const matched = fresh.filter((request) => matchesPattern({
2124
+ pattern: condition.pattern,
2125
+ url: request.url,
2126
+ }) && request.state !== "pending");
2127
+ const last = matched.at(-1);
2128
+ if (!last) {
2129
+ const pending = fresh.filter((request) => matchesPattern({
2130
+ pattern: condition.pattern,
2131
+ url: request.url,
2132
+ }));
2133
+ return {
2134
+ met: false,
2135
+ saw: pending.length > 0
2136
+ ? `${pending.length} matching, none finished yet.`
2137
+ : "Nothing has requested it.",
2138
+ };
2139
+ }
2140
+ if (condition.status !== undefined &&
2141
+ last.status !== condition.status) {
2142
+ // Keep waiting rather than declaring the condition met.
2143
+ // A retry may still answer with the status asked for, and
2144
+ // if none does, the timeout reports the status that kept
2145
+ // arriving, which is the finding.
2146
+ return {
2147
+ met: false,
2148
+ saw: `${last.method} ${last.url} answered ${requestStatus(last)}.`,
2149
+ };
2150
+ }
2151
+ return {
2152
+ met: true,
2153
+ // A cancelled request usually carries no failure text,
2154
+ // and printing the field raw said "cancelled: undefined"
2155
+ // to someone waiting on a request that never arrived.
2156
+ detail: last.state === "complete"
2157
+ ? `${last.method} ${last.url} answered ${requestStatus(last)}.`
2158
+ : `${last.method} ${last.url} ${requestStatus(last)}.`,
2159
+ };
2160
+ }
2161
+ case "attribute": {
2162
+ // Read through one evaluate rather than a handle, so an
2163
+ // element replaced between finding it and reading it is
2164
+ // simply absent on the next poll instead of throwing on a
2165
+ // stale handle.
2166
+ let held;
2167
+ try {
2168
+ held = await this.page.evaluate((selector, attribute) => {
2169
+ const found = document.querySelector(selector);
2170
+ if (found === null)
2171
+ return undefined;
2172
+ return found.getAttribute(attribute);
2173
+ }, condition.selector, condition.attribute);
2174
+ }
2175
+ catch (error) {
2176
+ return {
2177
+ met: false,
2178
+ saw: `The page could not be asked: ${String(error)}`,
2179
+ };
2180
+ }
2181
+ if (held === undefined) {
2182
+ // Absent element and absent attribute are different
2183
+ // answers. Treating the first as "attribute is gone"
2184
+ // would satisfy a wait for a control that never rendered.
2185
+ return { met: false, saw: "Nothing matches that selector." };
2186
+ }
2187
+ const met = condition.value === undefined
2188
+ ? held === null
2189
+ : held === condition.value;
2190
+ return {
2191
+ met,
2192
+ ...(met
2193
+ ? {}
2194
+ : {
2195
+ saw: held === null
2196
+ ? `It has no ${condition.attribute}.`
2197
+ : `Its ${condition.attribute} is "${held}".`,
2198
+ }),
2199
+ };
2200
+ }
2201
+ case "count": {
2202
+ let seen;
2203
+ try {
2204
+ seen = await this.page.evaluate((selector) => document.querySelectorAll(selector).length, condition.selector);
2205
+ }
2206
+ catch (error) {
2207
+ return {
2208
+ met: false,
2209
+ saw: `The page could not be asked: ${String(error)}`,
2210
+ };
2211
+ }
2212
+ const met = seen === condition.count;
2213
+ return {
2214
+ met,
2215
+ ...(met ? {} : { saw: `There are ${seen}.` }),
2216
+ };
2217
+ }
2218
+ case "animations": {
2219
+ // Catching to zero declared the animations settled
2220
+ // whenever the probe itself failed, which is the same
2221
+ // false pass as the selector arm above.
2222
+ let running;
2223
+ try {
2224
+ running = await this.page.evaluate(() => document
2225
+ .getAnimations()
2226
+ .filter((animation) => animation.playState === "running")
2227
+ .length);
2228
+ }
2229
+ catch (error) {
2230
+ return {
2231
+ met: false,
2232
+ saw: `The page could not be asked: ${String(error)}`,
2233
+ };
2234
+ }
2235
+ return {
2236
+ met: running === 0,
2237
+ ...(running === 0
2238
+ ? {}
2239
+ : { saw: `${running} animations are still running.` }),
2240
+ };
2241
+ }
2242
+ case "duration":
2243
+ return { met: true };
2244
+ }
2245
+ }
2246
+ /**
2247
+ * Everything this session has accumulated, in one reading.
2248
+ *
2249
+ * Emulation, interception and dialog policy all change what
2250
+ * every other reading means, and none of them is visible in
2251
+ * those readings, so they are gathered here.
2252
+ */
2253
+ async status() {
2254
+ await this.ready();
2255
+ const logs = this.logs();
2256
+ const heard = await this.heard(0);
2257
+ const requests = this.requests();
2258
+ const gaps = await this.emulation.currentGaps();
2259
+ return {
2260
+ name: this.name,
2261
+ url: this.page.url(),
2262
+ title: await this.page.title().catch(() => ""),
2263
+ emulation: this.emulation.asked,
2264
+ ...(gaps.length === 0 ? {} : { gaps }),
2265
+ rules: this.shaper.current.rules,
2266
+ ...(this.shaper.current.throttle === undefined
2267
+ ? {}
2268
+ : { throttle: this.shaper.current.throttle }),
2269
+ dialogPolicy: this.telemetry.dialogs.policy,
2270
+ dialogsSeen: this.telemetry.dialogs.seen.length,
2271
+ logs: { count: logs.entries.length, cursor: logs.cursor },
2272
+ announcements: { count: heard.entries.length, cursor: heard.cursor },
2273
+ requests: {
2274
+ count: requests.length,
2275
+ failed: requests.filter((request) => request.state === "failed").length,
2276
+ },
2277
+ history: this.history,
2278
+ artifacts: this.artifacts.written,
2279
+ recording: describeRecording(recordingInProgress(), this.name),
2280
+ };
2281
+ }
2282
+ /** How the network is currently being bent. */
2283
+ get shaping() {
2284
+ return this.shaper.current;
2285
+ }
2286
+ /**
2287
+ * What the page has announced since a cursor, with the cursor
2288
+ * to read from next time and how many were dropped.
2289
+ */
2290
+ async heard(since = 0) {
2291
+ return this.telemetry.heard(since);
2292
+ }
2293
+ /** Close the tab and the context holding its state. */
2294
+ async close() {
2295
+ try {
2296
+ await this.cdp.detach();
2297
+ }
2298
+ catch {
2299
+ // The session may already be gone; closing the context is enough.
2300
+ }
2301
+ // Closing the context disposes its pages along with the
2302
+ // cookies, storage and cache they accumulated.
2303
+ await this.context.close();
2304
+ }
2305
+ /**
2306
+ * Everything worth knowing about one element.
2307
+ *
2308
+ * Each fact is asked of the browser rather than worked out
2309
+ * here: the box from the layout engine, the occluder from a
2310
+ * hit test, the styles from the cascade, the announcement from
2311
+ * the accessibility tree.
2312
+ */
2313
+ async inspect(target, options = {}) {
2314
+ const tree = await this.axTree();
2315
+ const resolution = resolveTarget(tree, target);
2316
+ if (resolution.kind === "notFound") {
2317
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
2318
+ }
2319
+ if (resolution.kind === "ambiguous") {
2320
+ return { ok: false, refusal: ambiguityRefusal(tree, target) };
2321
+ }
2322
+ const node = subtreeAt(tree, resolution.backendDomId) ?? tree;
2323
+ const objectId = await this.objectFor(resolution.backendDomId);
2324
+ try {
2325
+ return {
2326
+ ok: true,
2327
+ inspection: await this.inspectNode(resolution.backendDomId, objectId, node, options),
2328
+ };
2329
+ }
2330
+ finally {
2331
+ await this.release(objectId);
2332
+ }
2333
+ }
2334
+ /**
2335
+ * Which fonts the browser actually painted with.
2336
+ *
2337
+ * A computed style reports the stack that was asked for, not
2338
+ * what was used. "SF Pro, Helvetica, sans-serif" reads the same
2339
+ * whether the first one loaded or the page quietly fell back to
2340
+ * the third, which is most of the answer to why a screenshot
2341
+ * from one machine does not match another.
2342
+ */
2343
+ async fontsOf(backendNodeId) {
2344
+ const nodeId = await this.frontendNodeFor(backendNodeId);
2345
+ if (nodeId === undefined)
2346
+ return undefined;
2347
+ try {
2348
+ const { fonts } = await this.cdp.send("CSS.getPlatformFontsForNode", {
2349
+ nodeId,
2350
+ });
2351
+ if (fonts.length === 0)
2352
+ return undefined;
2353
+ return fonts.map((font) => ({
2354
+ family: font.familyName,
2355
+ glyphs: font.glyphCount,
2356
+ }));
2357
+ }
2358
+ catch {
2359
+ // An element with no text has no fonts, and a detached one
2360
+ // cannot be asked. Neither is worth failing an inspection.
2361
+ return undefined;
2362
+ }
2363
+ }
2364
+ /**
2365
+ * The element's own words, and the attributes it carries.
2366
+ *
2367
+ * The accessible name answers what a control is called, which
2368
+ * is a different question from what it says. A counter reading
2369
+ * "42", a status reading "3 items updated" and an input holding
2370
+ * a typed email all have names that say none of that, so
2371
+ * asserting on them meant dropping to a raw evaluate. Data
2372
+ * attributes are here for the same reason: teams hang test
2373
+ * state on them, and the accessibility tree cannot see them.
2374
+ */
2375
+ async contentOf(objectId) {
2376
+ try {
2377
+ const { result } = await this.cdp.send("Runtime.callFunctionOn", {
2378
+ objectId,
2379
+ returnByValue: true,
2380
+ functionDeclaration: CONTENT_PROBE,
2381
+ });
2382
+ const read = result.value;
2383
+ if (read === undefined)
2384
+ return undefined;
2385
+ return {
2386
+ ...(read.text === "" ? {} : { text: read.text }),
2387
+ ...(read.value === undefined ? {} : { value: read.value }),
2388
+ ...(read.attributes.length === 0
2389
+ ? {}
2390
+ : { attributes: Object.fromEntries(read.attributes) }),
2391
+ };
2392
+ }
2393
+ catch {
2394
+ // A detached node cannot be read, and an inspection that
2395
+ // reports everything else is worth more than a refusal.
2396
+ return undefined;
2397
+ }
2398
+ }
2399
+ /**
2400
+ * A handle on the element inside this session.
2401
+ *
2402
+ * Object identifiers belong to the protocol session that
2403
+ * minted them, so puppeteer's own handles are unusable here.
2404
+ * A backend node id is the identity both sides agree on.
2405
+ */
2406
+ async objectFor(backendNodeId) {
2407
+ try {
2408
+ const { object } = await this.cdp.send("DOM.resolveNode", {
2409
+ backendNodeId,
2410
+ });
2411
+ return object.objectId;
2412
+ }
2413
+ catch {
2414
+ // The node can go out of the document between being named
2415
+ // and being looked at; the inspection reports what it can.
2416
+ return undefined;
2417
+ }
2418
+ }
2419
+ /**
2420
+ * Swap a text node for the element that paints it.
2421
+ *
2422
+ * A `StaticText` target resolves to a text node, which is right
2423
+ * for reading its box and wrong for everything that touches
2424
+ * style. Returns the original handle when it is already an
2425
+ * element, so callers can promote unconditionally.
2426
+ */
2427
+ async paintingElement(objectId) {
2428
+ try {
2429
+ const { result } = await this.cdp.send("Runtime.callFunctionOn", {
2430
+ objectId,
2431
+ functionDeclaration: PAINTING_ELEMENT_PROBE,
2432
+ });
2433
+ return result.objectId ?? objectId;
2434
+ }
2435
+ catch {
2436
+ // A detached node has no painting element either, and the
2437
+ // caller's own reads will report what it could not find.
2438
+ return objectId;
2439
+ }
2440
+ }
2441
+ /** Let go of a handle, whether or not the page still has it. */
2442
+ async release(objectId) {
2443
+ if (!objectId)
2444
+ return;
2445
+ await this.cdp
2446
+ .send("Runtime.releaseObject", { objectId })
2447
+ // Releasing a handle the page already dropped is not a
2448
+ // failure worth surfacing.
2449
+ .catch(() => { });
2450
+ }
2451
+ /** Gather every fact about an element the browser will give. */
2452
+ async inspectNode(backendNodeId, objectId, node, options) {
2453
+ const box = await this.boxOf(backendNodeId);
2454
+ const styles = objectId ? await this.stylesOf(objectId) : undefined;
2455
+ const viewport = await this.viewport();
2456
+ const coveredBy = box && objectId ? await this.occluderOf(objectId) : undefined;
2457
+ const visibility = judgeVisibility({
2458
+ rendered: box !== undefined,
2459
+ ...(box === undefined ? {} : { border: box.border }),
2460
+ ...(viewport === undefined ? {} : { viewport }),
2461
+ ...(coveredBy === undefined ? {} : { coveredBy }),
2462
+ ...(styles?.opacity === undefined
2463
+ ? {}
2464
+ : { opacity: Number(styles.opacity) }),
2465
+ ...(styles?.visibility === undefined
2466
+ ? {}
2467
+ : { visibility: styles.visibility }),
2468
+ });
2469
+ const initials = await this.initials();
2470
+ const curated = styles === undefined
2471
+ ? undefined
2472
+ : curateStyles(styles, {
2473
+ ...(initials === undefined ? {} : { initials }),
2474
+ ...(options.styles === undefined ? {} : { only: options.styles }),
2475
+ });
2476
+ const wantsBehaviour = options.behaviour === true && objectId !== undefined;
2477
+ const variants = options.states && objectId && curated
2478
+ ? await this.variantsOf(backendNodeId, objectId, curated, options.states, initials, options)
2479
+ : undefined;
2480
+ const content = objectId ? await this.contentOf(objectId) : undefined;
2481
+ const fonts = await this.fontsOf(backendNodeId);
2482
+ return {
2483
+ node,
2484
+ visibility,
2485
+ ...(content ?? {}),
2486
+ ...(fonts === undefined ? {} : { fonts }),
2487
+ ...(box === undefined ? {} : { box }),
2488
+ ...(curated === undefined ? {} : { styles: curated }),
2489
+ ...(wantsBehaviour && objectId
2490
+ ? {
2491
+ listeners: await this.listenersOf(objectId),
2492
+ delegated: await this.delegatedTo(objectId),
2493
+ animations: await this.animationsOf(objectId),
2494
+ }
2495
+ : {}),
2496
+ ...(variants === undefined ? {} : { variants }),
2497
+ ...(options.why === undefined
2498
+ ? {}
2499
+ : {
2500
+ trace: await this.traceOf(backendNodeId, options.why, styles),
2501
+ }),
2502
+ };
2503
+ }
2504
+ /**
2505
+ * Photograph the page, or one element of it.
2506
+ *
2507
+ * Images never come back inline: a screenshot is far larger
2508
+ * than any response budget and would crowd out the reading it
2509
+ * was meant to illustrate. They go to the session's bundle
2510
+ * directory and the answer carries the paths.
2511
+ *
2512
+ * A full-page capture is tiled, because a long page makes an
2513
+ * image taller than a model will accept. The tiling and its
2514
+ * ceiling are the ones web_read already uses, so a page reads
2515
+ * the same either way.
2516
+ */
2517
+ async shoot(options = {}) {
2518
+ const target = options.target;
2519
+ let held;
2520
+ try {
2521
+ if (target) {
2522
+ const tree = await this.axTree();
2523
+ const resolution = resolveTarget(tree, target);
2524
+ if (resolution.kind === "notFound") {
2525
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
2526
+ }
2527
+ if (resolution.kind === "ambiguous") {
2528
+ return { ok: false, refusal: ambiguityRefusal(tree, target) };
2529
+ }
2530
+ held = resolution.backendDomId;
2531
+ }
2532
+ if (options.state !== undefined) {
2533
+ await this.holdState(held, options.state);
2534
+ }
2535
+ return { ok: true, shot: await this.capture(held, options) };
2536
+ }
2537
+ finally {
2538
+ if (options.state !== undefined)
2539
+ await this.holdState(held, undefined);
2540
+ }
2541
+ }
2542
+ /** Force or release a state for the duration of a capture. */
2543
+ async holdState(backendNodeId, state) {
2544
+ if (backendNodeId === undefined)
2545
+ return;
2546
+ const nodeId = await this.frontendNodeFor(backendNodeId);
2547
+ if (nodeId === undefined)
2548
+ return;
2549
+ await this.cdp
2550
+ .send("CSS.forcePseudoState", {
2551
+ nodeId,
2552
+ forcedPseudoClasses: state ? [state] : [],
2553
+ })
2554
+ // A page that navigated took the forced state with it.
2555
+ .catch(() => { });
2556
+ const objectId = await this.objectFor(backendNodeId);
2557
+ try {
2558
+ // Photographing mid-transition catches a state part way
2559
+ // there, which is the one thing a picture must not do.
2560
+ if (objectId)
2561
+ await this.settleForcedState(objectId);
2562
+ }
2563
+ finally {
2564
+ await this.release(objectId);
2565
+ }
2566
+ }
2567
+ /** Take the picture and write it out. */
2568
+ async capture(backendNodeId, options) {
2569
+ const sink = this.artifacts.sink();
2570
+ const stamp = this.artifacts.nextShot();
2571
+ const keep = (path) => this.artifacts.keep(path);
2572
+ if (backendNodeId !== undefined) {
2573
+ const box = await this.boxOf(backendNodeId);
2574
+ const clip = box?.border;
2575
+ // The box model measures from the viewport and a screenshot
2576
+ // clip is measured from the document, so the two agree only
2577
+ // while the page sits at the top. Scrolled, a clip taken
2578
+ // straight from the box lands wherever the page has moved
2579
+ // from: a picture of the right size, of real content, of the
2580
+ // wrong element, with nothing about it to say so.
2581
+ const scroll = clip ? await this.scrollOffset() : undefined;
2582
+ const data = await this.page.screenshot({
2583
+ type: "png",
2584
+ encoding: "base64",
2585
+ ...(clip
2586
+ ? {
2587
+ clip: {
2588
+ x: clip.x + (scroll?.x ?? 0),
2589
+ y: clip.y + (scroll?.y ?? 0),
2590
+ width: clip.width,
2591
+ height: clip.height,
2592
+ },
2593
+ }
2594
+ : {}),
2595
+ });
2596
+ return {
2597
+ paths: [keep(sink.writeBinary(`element-${stamp}.png`, String(data)))],
2598
+ truncated: false,
2599
+ width: Math.round(clip?.width ?? 0),
2600
+ height: Math.round(clip?.height ?? 0),
2601
+ };
2602
+ }
2603
+ if (options.fullPage) {
2604
+ const captured = await captureTiles(this.page);
2605
+ return {
2606
+ paths: captured.tiles.map((tile, index) => keep(sink.writeBinary(`page-${stamp}-${String(index + 1).padStart(2, "0")}.png`, tile))),
2607
+ truncated: captured.truncated,
2608
+ width: 0,
2609
+ height: 0,
2610
+ };
2611
+ }
2612
+ const data = await this.page.screenshot({
2613
+ type: "png",
2614
+ encoding: "base64",
2615
+ });
2616
+ const seen = await this.viewport();
2617
+ return {
2618
+ paths: [keep(sink.writeBinary(`viewport-${stamp}.png`, String(data)))],
2619
+ truncated: false,
2620
+ width: seen?.width ?? 0,
2621
+ height: seen?.height ?? 0,
2622
+ };
2623
+ }
2624
+ /**
2625
+ * Compare the page now against a baseline of it, or record
2626
+ * one when there is none.
2627
+ *
2628
+ * A missing baseline is not a failure. The first run of any
2629
+ * comparison has nothing to compare against, and refusing
2630
+ * would make the tool need a setup step it can perform itself.
2631
+ *
2632
+ * The elements are read after the shot rather than before, so
2633
+ * a region is attributed against the layout that was actually
2634
+ * photographed.
2635
+ */
2636
+ async compareToBaseline(label, options = {}) {
2637
+ await this.ready();
2638
+ const safe = pathComponent(label);
2639
+ const baselinePath = path.join(this.baselineDir(), `${safe}.png`);
2640
+ const shot = Buffer.from(String(await this.page.screenshot({ type: "png", encoding: "base64" })), "base64");
2641
+ const takenUnder = this.provenance();
2642
+ if (options.update || !existsSync(baselinePath)) {
2643
+ mkdirSync(path.dirname(baselinePath), {
2644
+ recursive: true,
2645
+ mode: DIR_MODE,
2646
+ });
2647
+ writeFileSync(baselinePath, shot, { mode: FILE_MODE });
2648
+ writeFileSync(sidecarFor(baselinePath), stringify(takenUnder), {
2649
+ mode: FILE_MODE,
2650
+ });
2651
+ this.artifacts.keep(baselinePath);
2652
+ return { comparison: undefined, recorded: baselinePath, artifacts: [] };
2653
+ }
2654
+ // Refuse rather than diff two different subjects. A baseline
2655
+ // recorded before this library stored provenance has none, and
2656
+ // is compared as before rather than being thrown away.
2657
+ const sidecar = sidecarFor(baselinePath);
2658
+ const was = existsSync(sidecar)
2659
+ ? parseProvenance(readFileSync(sidecar, "utf8"))
2660
+ : undefined;
2661
+ const differs = was && describeDrift(was, takenUnder);
2662
+ if (differs) {
2663
+ return {
2664
+ comparison: { kind: "incomparable", because: differs },
2665
+ artifacts: [],
2666
+ };
2667
+ }
2668
+ // The screenshot is of the viewport; the rects are in
2669
+ // document coordinates. The two only coincide at scroll
2670
+ // origin, and driving the page before checking it is the
2671
+ // loop this tool teaches, so every region was attributed to
2672
+ // whatever sat at those coordinates at the top of the page
2673
+ // once anything had scrolled.
2674
+ const { nodes, viewport } = await this.layout();
2675
+ const offsetX = viewport.scrollX ?? 0;
2676
+ const offsetY = viewport.scrollY ?? 0;
2677
+ const placed = nodes.map((node) => ({
2678
+ selector: node.selector,
2679
+ rect: {
2680
+ ...node.rect,
2681
+ x: node.rect.x - offsetX,
2682
+ y: node.rect.y - offsetY,
2683
+ },
2684
+ }));
2685
+ const { comparison, image } = compareImages(readPng(readFileSync(baselinePath)), readPng(shot), placed, {
2686
+ ...(options.threshold === undefined
2687
+ ? {}
2688
+ : { threshold: options.threshold }),
2689
+ scale: this.emulation.asked.viewport?.deviceScaleFactor ?? 1,
2690
+ });
2691
+ if (image === undefined)
2692
+ return { comparison, artifacts: [] };
2693
+ // The three together, because a diff on its own shows where
2694
+ // something changed and never what it changed from.
2695
+ const sink = this.artifacts.sink();
2696
+ const stamp = this.artifacts.nextShot();
2697
+ const artifacts = [
2698
+ sink.writeBinary(`${safe}-${stamp}-baseline.png`, readFileSync(baselinePath).toString("base64")),
2699
+ sink.writeBinary(`${safe}-${stamp}-current.png`, shot.toString("base64")),
2700
+ sink.writeBinary(`${safe}-${stamp}-diff.png`, image.toString("base64")),
2701
+ ];
2702
+ for (const artifact of artifacts)
2703
+ this.artifacts.keep(artifact);
2704
+ return { comparison, artifacts };
2705
+ }
2706
+ /**
2707
+ * Where this session's baselines live.
2708
+ *
2709
+ * Under the data directory, not the bundle root. This used to
2710
+ * sit at BUNDLE_ROOT/baselines, which the bundle reaper treats
2711
+ * as an ownerless directory (the name is not a pid) and deletes
2712
+ * once it is six hours old. Writing a baseline touches the
2713
+ * session subdirectory rather than its parent, so the parent's
2714
+ * age kept climbing and a stable set of baselines was reaped on
2715
+ * a timer. The next comparison then found nothing, recorded the
2716
+ * current page as truth and reported a warning, which is data
2717
+ * loss wearing a first run's clothes.
2718
+ *
2719
+ * A baseline is a durable artifact the user asked for and may
2720
+ * want to look at, which is what dataDir is for.
2721
+ */
2722
+ baselineDir() {
2723
+ // The session name is chosen by whoever is driving. Joined raw,
2724
+ // a name of "../../../../tmp/evil" put the baseline write at
2725
+ // /Users/tmp/evil: the label beside it was being cleaned and
2726
+ // this was not, which is why both now go through one rule.
2727
+ return path.join(this.options.dataRoot ?? defaultBrowserDataRoot(), "baselines", pathComponent(this.name));
2728
+ }
2729
+ /**
2730
+ * What a baseline was taken under, stored beside the image.
2731
+ *
2732
+ * Without this a comparison cannot tell a regression from a
2733
+ * change of subject: record on one page, navigate to another,
2734
+ * and the diff reports confident failures attributed to the
2735
+ * second page's elements. The size check alone cannot see it,
2736
+ * because two pages at one viewport are the same size.
2737
+ */
2738
+ provenance() {
2739
+ const viewport = this.emulation.asked.viewport;
2740
+ return {
2741
+ url: this.url,
2742
+ ...(viewport?.width === undefined ? {} : { width: viewport.width }),
2743
+ ...(viewport?.height === undefined ? {} : { height: viewport.height }),
2744
+ deviceScaleFactor: viewport?.deviceScaleFactor ?? 1,
2745
+ };
2746
+ }
2747
+ /** Carry out an action against an element judged ready. */
2748
+ async perform(action, element, backendNodeId) {
2749
+ if (action.kind === "upload") {
2750
+ // Set through our own protocol channel against the node the
2751
+ // outline resolved. The driver's aria selector does not
2752
+ // offer a node it considers hidden, and a hidden input is
2753
+ // the ordinary shape of an upload, so there is no handle to
2754
+ // hold here and none is needed.
2755
+ const objectId = await this.objectFor(backendNodeId);
2756
+ if (!objectId)
2757
+ return;
2758
+ try {
2759
+ await this.cdp.send("DOM.setFileInputFiles", {
2760
+ objectId,
2761
+ files: [...action.files],
2762
+ });
2763
+ }
2764
+ finally {
2765
+ await this.release(objectId);
2766
+ }
2767
+ return;
2768
+ }
2769
+ if (!element)
2770
+ return;
2771
+ switch (action.kind) {
2772
+ case "click":
2773
+ await element.click();
2774
+ return;
2775
+ case "type":
2776
+ await element.type(action.text);
2777
+ return;
2778
+ case "hover":
2779
+ await element.hover();
2780
+ return;
2781
+ case "focus":
2782
+ await element.focus();
2783
+ return;
2784
+ case "select":
2785
+ await element.select(action.text);
2786
+ return;
2787
+ case "scrollTo":
2788
+ await element.scrollIntoView();
2789
+ return;
2790
+ case "clear": {
2791
+ // Select what is there and delete it, so the page sees
2792
+ // the same input and change events it would from a
2793
+ // person at a keyboard. The field is asked to select
2794
+ // itself rather than triple clicked, so clearing works
2795
+ // under an overlay, and rather than sending a select-all
2796
+ // shortcut, so there is no platform to guess at.
2797
+ await element.focus();
2798
+ // Resolved through our own session: an identifier minted
2799
+ // by puppeteer's connection means nothing on this one.
2800
+ const objectId = await this.objectFor(backendNodeId);
2801
+ try {
2802
+ if (objectId) {
2803
+ await this.cdp.send("Runtime.callFunctionOn", {
2804
+ objectId,
2805
+ functionDeclaration: SELECT_TEXT_PROBE,
2806
+ returnByValue: true,
2807
+ });
2808
+ }
2809
+ }
2810
+ finally {
2811
+ await this.release(objectId);
2812
+ }
2813
+ await this.page.keyboard.press("Backspace");
2814
+ return;
2815
+ }
2816
+ }
2817
+ }
2818
+ /**
2819
+ * Wait until an element can actually be acted on.
2820
+ *
2821
+ * A click on an element that is not ready does nothing and
2822
+ * says nothing, leaving a caller believing the page was acted
2823
+ * on. So readiness is established first, and when the budget
2824
+ * runs out the condition still in the way is reported rather
2825
+ * than the action being attempted regardless.
2826
+ */
2827
+ async awaitReady(target, pointer, sight = true) {
2828
+ const started = Date.now();
2829
+ let previous;
2830
+ let scrolled = false;
2831
+ let last = {
2832
+ ready: false,
2833
+ blocker: "it is not in the page",
2834
+ };
2835
+ while (Date.now() - started < READY_BUDGET_MS) {
2836
+ const look = await this.readinessOf(target, previous, pointer, sight);
2837
+ previous = look.box;
2838
+ last = judgeActionability(look.facts);
2839
+ if (last.ready && look.backendDomId !== undefined) {
2840
+ return {
2841
+ ready: true,
2842
+ waitedMs: Date.now() - started,
2843
+ backendDomId: look.backendDomId,
2844
+ };
2845
+ }
2846
+ // Waiting cannot bring an element into the viewport, so
2847
+ // polling one that is merely below the fold spends the whole
2848
+ // budget to reach the verdict it already had. Scrolling is
2849
+ // what the caller means by acting on it, and what a person
2850
+ // does. Once only: if it is still out of view after being
2851
+ // scrolled to, something else is moving it and the blocker
2852
+ // is the honest answer.
2853
+ if (!scrolled && look.facts.visibility?.state === "off screen") {
2854
+ scrolled = true;
2855
+ const found = await this.resolve(target);
2856
+ if (found.ok) {
2857
+ await found.element.scrollIntoView();
2858
+ await found.element.dispose();
2859
+ previous = undefined;
2860
+ continue;
2861
+ }
2862
+ }
2863
+ await new Promise((resolve) => setTimeout(resolve, READY_POLL_MS));
2864
+ }
2865
+ return {
2866
+ ready: false,
2867
+ waitedMs: Date.now() - started,
2868
+ blocker: last.blocker ?? "it never became ready",
2869
+ };
2870
+ }
2871
+ /** One look at whether an element is ready. */
2872
+ async readinessOf(target, previous, pointer, sight = true) {
2873
+ const tree = await this.axTree();
2874
+ const resolution = resolveTarget(tree, target);
2875
+ if (resolution.kind !== "resolved") {
2876
+ return {
2877
+ facts: { present: false, enabled: false, settled: false },
2878
+ box: undefined,
2879
+ };
2880
+ }
2881
+ const node = subtreeAt(tree, resolution.backendDomId);
2882
+ const box = await this.boxOf(resolution.backendDomId);
2883
+ // Where the click lands only matters when there is a click.
2884
+ // Focusing, typing, choosing an option and scrolling all
2885
+ // reach an element the pointer could not, so holding them to
2886
+ // a pointer's standard refuses work that would have
2887
+ // succeeded. Being rendered at a real size still matters to
2888
+ // every action, so that part of the verdict always applies.
2889
+ const viewport = pointer ? await this.viewport() : undefined;
2890
+ const coveredBy = pointer && box
2891
+ ? await this.coveredAtCentre(resolution.backendDomId)
2892
+ : undefined;
2893
+ const visibility = sight
2894
+ ? judgeVisibility({
2895
+ rendered: box !== undefined,
2896
+ ...(box === undefined ? {} : { border: box.border }),
2897
+ ...(viewport === undefined ? {} : { viewport }),
2898
+ ...(coveredBy === undefined ? {} : { coveredBy }),
2899
+ })
2900
+ : undefined;
2901
+ return {
2902
+ facts: {
2903
+ present: true,
2904
+ ...(visibility === undefined ? {} : { visibility }),
2905
+ // The tree reports what the browser exposes to assistive
2906
+ // technology, which is the same disabled a person meets.
2907
+ enabled: node?.properties.disabled !== true,
2908
+ settled: sameBox(previous, box?.border),
2909
+ },
2910
+ box: box?.border,
2911
+ backendDomId: resolution.backendDomId,
2912
+ };
2913
+ }
2914
+ /**
2915
+ * Whether something is painted over the element's centre.
2916
+ *
2917
+ * Only the centre, unlike a full inspection: this runs on
2918
+ * every poll, and the centre is where a click is aimed.
2919
+ */
2920
+ async coveredAtCentre(backendNodeId) {
2921
+ const objectId = await this.objectFor(backendNodeId);
2922
+ if (!objectId)
2923
+ return undefined;
2924
+ try {
2925
+ return await this.hitTest(objectId, false);
2926
+ }
2927
+ finally {
2928
+ await this.release(objectId);
2929
+ }
2930
+ }
2931
+ /** What is listening on the element. */
2932
+ async listenersOf(objectId) {
2933
+ try {
2934
+ const { listeners } = (await this.cdp.send("DOMDebugger.getEventListeners", { objectId }));
2935
+ return normalizeListeners(listeners);
2936
+ }
2937
+ catch {
2938
+ // Without the debugger agent this one section is missing;
2939
+ // the rest of the inspection still stands.
2940
+ return [];
2941
+ }
2942
+ }
2943
+ /**
2944
+ * What is listening further up, which events from here reach.
2945
+ *
2946
+ * The protocol reads listeners off one object at a time and can
2947
+ * walk down into children but not up, so the ancestors are
2948
+ * collected in the page and each one asked separately. A live
2949
+ * button whose click was handled on the body reported nothing
2950
+ * listening, which is the answer that most invites the wrong
2951
+ * conclusion from an inspection meant to explain a dead click.
2952
+ */
2953
+ async delegatedTo(objectId) {
2954
+ try {
2955
+ const { result } = (await this.cdp.send("Runtime.callFunctionOn", {
2956
+ objectId,
2957
+ functionDeclaration: ANCESTORS_PROBE,
2958
+ returnByValue: false,
2959
+ }));
2960
+ if (!result.objectId)
2961
+ return [];
2962
+ try {
2963
+ return await this.listenersUp(result.objectId);
2964
+ }
2965
+ finally {
2966
+ await this.release(result.objectId);
2967
+ }
2968
+ }
2969
+ catch {
2970
+ // The element's own handlers are the answer more often than
2971
+ // not, and they are already in hand. Losing the ancestors
2972
+ // costs a section, not the inspection.
2973
+ return [];
2974
+ }
2975
+ }
2976
+ /** Ask each ancestor in the array what is listening on it. */
2977
+ async listenersUp(arrayId) {
2978
+ const { result } = (await this.cdp.send("Runtime.getProperties", {
2979
+ objectId: arrayId,
2980
+ ownProperties: true,
2981
+ }));
2982
+ const found = [];
2983
+ for (const property of result) {
2984
+ // getProperties hands back 'length' and the indices together.
2985
+ if (!/^\d+$/.test(property.name))
2986
+ continue;
2987
+ const ancestor = property.value?.objectId;
2988
+ if (!ancestor)
2989
+ continue;
2990
+ try {
2991
+ const listeners = await this.listenersOf(ancestor);
2992
+ if (listeners.length > 0) {
2993
+ found.push({
2994
+ element: property.value?.description ?? "an ancestor",
2995
+ listeners,
2996
+ });
2997
+ }
2998
+ }
2999
+ finally {
3000
+ await this.release(ancestor);
3001
+ }
3002
+ }
3003
+ return found;
3004
+ }
3005
+ /** What is moving on the element. */
3006
+ async animationsOf(objectId) {
3007
+ try {
3008
+ const { result } = await this.cdp.send("Runtime.callFunctionOn", {
3009
+ objectId,
3010
+ functionDeclaration: ANIMATIONS_PROBE,
3011
+ returnByValue: true,
3012
+ });
3013
+ return normalizeAnimations((result.value ?? []));
3014
+ }
3015
+ catch {
3016
+ // A page that navigated mid-inspection has nothing left to
3017
+ // report here.
3018
+ return [];
3019
+ }
3020
+ }
3021
+ /**
3022
+ * How the element looks in each state, against how it looks at
3023
+ * rest.
3024
+ *
3025
+ * The forced state is always released afterwards, including
3026
+ * when a reading throws part way through. Leaving a page stuck
3027
+ * in a forced hover would quietly corrupt every later
3028
+ * observation of it.
3029
+ */
3030
+ async variantsOf(backendNodeId, objectId, atRest, states, initials, options) {
3031
+ const nodeId = await this.frontendNodeFor(backendNodeId);
3032
+ if (nodeId === undefined)
3033
+ return [];
3034
+ const variants = [];
3035
+ try {
3036
+ for (const state of states) {
3037
+ await this.cdp.send("CSS.forcePseudoState", {
3038
+ nodeId,
3039
+ forcedPseudoClasses: [state],
3040
+ });
3041
+ await this.settleForcedState(objectId);
3042
+ const held = await this.stylesOf(objectId);
3043
+ if (!held)
3044
+ continue;
3045
+ variants.push({
3046
+ state,
3047
+ changes: diffStyles(atRest, curateStyles(held, {
3048
+ ...(initials === undefined ? {} : { initials }),
3049
+ ...(options.styles === undefined ? {} : { only: options.styles }),
3050
+ })),
3051
+ });
3052
+ }
3053
+ }
3054
+ finally {
3055
+ await this.cdp
3056
+ .send("CSS.forcePseudoState", { nodeId, forcedPseudoClasses: [] })
3057
+ // A page that navigated took the forced state with it.
3058
+ .catch(() => { });
3059
+ // Releasing the state starts the transition back, so the
3060
+ // element is only truly as it was found once that has
3061
+ // finished. Without this the next reading catches it part
3062
+ // way home and reports a colour nobody chose.
3063
+ await this.settleForcedState(objectId);
3064
+ }
3065
+ return variants;
3066
+ }
3067
+ /**
3068
+ * Let whatever the forced state started finish before reading.
3069
+ *
3070
+ * Without this the reading catches a transition at its resting
3071
+ * values and reports that the state changes nothing, which is
3072
+ * exactly backwards.
3073
+ */
3074
+ async settleForcedState(objectId) {
3075
+ try {
3076
+ await this.cdp.send("Runtime.callFunctionOn", {
3077
+ objectId,
3078
+ functionDeclaration: SETTLE_PROBE,
3079
+ arguments: [{ value: SETTLE_CAP_MS }],
3080
+ awaitPromise: true,
3081
+ returnByValue: true,
3082
+ });
3083
+ }
3084
+ catch {
3085
+ // A reading taken early is worse than one taken late, but
3086
+ // neither is worth abandoning the inspection over.
3087
+ }
3088
+ }
3089
+ /** The front-end node id, which the CSS agent works in. */
3090
+ async frontendNodeFor(backendNodeId) {
3091
+ try {
3092
+ // A front-end id only exists once the document has been
3093
+ // walked at least once.
3094
+ await this.cdp.send("DOM.getDocument", { depth: 0 });
3095
+ const { nodeIds } = await this.cdp.send("DOM.pushNodesByBackendIdsToFrontend", { backendNodeIds: [backendNodeId] });
3096
+ return nodeIds[0];
3097
+ }
3098
+ catch {
3099
+ // Without a front-end id the CSS agent cannot be addressed.
3100
+ return undefined;
3101
+ }
3102
+ }
3103
+ /** The element's four boxes, or nothing when it has none. */
3104
+ async boxOf(backendNodeId) {
3105
+ try {
3106
+ const { model } = (await this.cdp.send("DOM.getBoxModel", {
3107
+ backendNodeId,
3108
+ }));
3109
+ return normalizeBoxModel(model);
3110
+ }
3111
+ catch {
3112
+ // An element with display none has no box at all, and the
3113
+ // protocol says so by refusing. That is an answer, not a
3114
+ // failure: the visibility verdict reports it as unrendered.
3115
+ return undefined;
3116
+ }
3117
+ }
3118
+ /** Every computed property of the element, as the browser has it. */
3119
+ async stylesOf(objectId) {
3120
+ try {
3121
+ const { result } = await this.cdp.send("Runtime.callFunctionOn", {
3122
+ objectId,
3123
+ functionDeclaration: COMPUTED_STYLE_PROBE,
3124
+ arguments: [{ value: SHORTHAND_PROPERTIES }],
3125
+ returnByValue: true,
3126
+ });
3127
+ return result.value;
3128
+ }
3129
+ catch {
3130
+ // A page that navigated mid-inspection leaves nothing to
3131
+ // read; the rest of the inspection still stands.
3132
+ return undefined;
3133
+ }
3134
+ }
3135
+ /**
3136
+ * What every property computes to untouched, read once per
3137
+ * session. These decide which values were actually chosen.
3138
+ */
3139
+ async initials() {
3140
+ if (this.initialStyles)
3141
+ return this.initialStyles;
3142
+ try {
3143
+ const { result } = await this.cdp.send("Runtime.evaluate", {
3144
+ expression: asCall(INITIALS_PROBE, SHORTHAND_PROPERTIES),
3145
+ returnByValue: true,
3146
+ });
3147
+ this.initialStyles = result.value;
3148
+ return this.initialStyles;
3149
+ }
3150
+ catch {
3151
+ // Without these nothing is suppressed as a default, which
3152
+ // is verbose but never wrong.
3153
+ return undefined;
3154
+ }
3155
+ }
3156
+ /** Why one property has the value it has. */
3157
+ async traceOf(backendNodeId, property, styles) {
3158
+ try {
3159
+ const nodeId = await this.frontendNodeFor(backendNodeId);
3160
+ if (nodeId === undefined)
3161
+ return undefined;
3162
+ const raw = (await this.cdp.send("CSS.getMatchedStylesForNode", {
3163
+ nodeId,
3164
+ }));
3165
+ const trace = traceProperty(normalizeCascade(raw), property, styles?.[property]);
3166
+ return trace === undefined ? undefined : await this.authorTrace(trace);
3167
+ }
3168
+ catch {
3169
+ // The cascade domain needs the CSS agent; without it the
3170
+ // rest of the inspection is still worth returning.
3171
+ return undefined;
3172
+ }
3173
+ }
3174
+ /**
3175
+ * Tell each declaration in a trace where it was written.
3176
+ *
3177
+ * A build step that rewrote the CSS makes the reported line
3178
+ * useless in the same way a minified stack frame is: it names
3179
+ * a file nobody edits. The winner is usually the declaration
3180
+ * somebody wants to go and change, so it is the one that most
3181
+ * needs an address that exists.
3182
+ */
3183
+ async authorTrace(trace) {
3184
+ const declarations = await Promise.all(trace.declarations.map(async (declaration) => {
3185
+ const { source } = declaration;
3186
+ if (source?.styleSheet === undefined || source.line === undefined) {
3187
+ return declaration;
3188
+ }
3189
+ const authored = await this.sourceMaps.authoredForSheet(source.styleSheet, { line: source.line, column: source.column ?? 0 });
3190
+ if (authored === undefined)
3191
+ return declaration;
3192
+ return { ...declaration, source: { ...source, authored } };
3193
+ }));
3194
+ // The winner is compared by identity, so it has to be
3195
+ // re-found among the rebuilt declarations rather than kept.
3196
+ const winnerAt = trace.winner
3197
+ ? trace.declarations.indexOf(trace.winner)
3198
+ : -1;
3199
+ return {
3200
+ ...trace,
3201
+ declarations,
3202
+ ...(winnerAt < 0 ? {} : { winner: declarations[winnerAt] }),
3203
+ };
3204
+ }
3205
+ /** The area a person can currently see. */
3206
+ async viewport() {
3207
+ try {
3208
+ const metrics = (await this.cdp.send("Page.getLayoutMetrics"));
3209
+ const seen = metrics.cssVisualViewport;
3210
+ if (!seen)
3211
+ return undefined;
3212
+ return { width: seen.clientWidth, height: seen.clientHeight };
3213
+ }
3214
+ catch {
3215
+ // Without the viewport an element is judged on everything
3216
+ // else known about it rather than not at all.
3217
+ return undefined;
3218
+ }
3219
+ }
3220
+ /**
3221
+ * How far the page has been scrolled, in CSS pixels.
3222
+ *
3223
+ * Read from the browser's own layout metrics rather than from the
3224
+ * page, so a capture does not have to run script to be correct.
3225
+ * Answers zero when the metrics are unavailable, which is what the
3226
+ * offset was assumed to be before it was asked for at all.
3227
+ */
3228
+ async scrollOffset() {
3229
+ try {
3230
+ const metrics = (await this.cdp.send("Page.getLayoutMetrics"));
3231
+ const seen = metrics.cssVisualViewport;
3232
+ return { x: seen?.pageX ?? 0, y: seen?.pageY ?? 0 };
3233
+ }
3234
+ catch {
3235
+ // A page that went away mid-capture has no offset to report,
3236
+ // and the capture around this will fail on its own terms.
3237
+ // Zero is what the clip assumed before the offset was asked
3238
+ // for at all, so it is the honest fallback rather than a new
3239
+ // guess.
3240
+ return { x: 0, y: 0 };
3241
+ }
3242
+ }
3243
+ /**
3244
+ * What is painted over the element, if anything.
3245
+ *
3246
+ * The centre and the corners, since a full inspection should
3247
+ * report an element clipped at one edge. Anything the element
3248
+ * contains counts as itself, since a click there still reaches
3249
+ * it.
3250
+ */
3251
+ async occluderOf(objectId) {
3252
+ return await this.hitTest(objectId, true);
3253
+ }
3254
+ /**
3255
+ * Who receives a click, when it is not us.
3256
+ *
3257
+ * The point is chosen inside the page rather than out here. Both
3258
+ * halves of the question used to be asked separately, the box
3259
+ * from the box model and the hit from `DOM.getNodeForLocation`,
3260
+ * and they disagree by the scroll offset, so anything the driver
3261
+ * had to scroll to was reported as covered by whatever sat at
3262
+ * the wrong point.
3263
+ */
3264
+ async hitTest(objectId, alsoCorners) {
3265
+ try {
3266
+ const { result } = await this.cdp.send("Runtime.callFunctionOn", {
3267
+ objectId,
3268
+ functionDeclaration: OCCLUDER_PROBE,
3269
+ arguments: [{ value: alsoCorners }],
3270
+ returnByValue: true,
3271
+ });
3272
+ return result.value ?? undefined;
3273
+ }
3274
+ catch {
3275
+ // An element that has gone from the page between resolving
3276
+ // and asking cannot be occluded by anything, and the next
3277
+ // poll reports it as absent.
3278
+ return undefined;
3279
+ }
3280
+ }
3281
+ async resolve(target) {
3282
+ // Confirm uniqueness against our own outline so an ambiguous
3283
+ // target becomes a prompt to narrow it rather than a wrong click.
3284
+ const tree = await this.axTree();
3285
+ const resolution = resolveTarget(tree, target);
3286
+ if (resolution.kind === "notFound") {
3287
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
3288
+ }
3289
+ if (resolution.kind === "ambiguous") {
3290
+ return { ok: false, refusal: ambiguityRefusal(tree, target) };
3291
+ }
3292
+ // Drive the real element through puppeteer's aria selector, which
3293
+ // brings the scrolling, visibility and hit testing that a raw
3294
+ // protocol click does not. But the selector is page-wide and knows
3295
+ // nothing of a container, and its ordering is its own, so the
3296
+ // candidate that matches is chosen by the identity the resolution
3297
+ // already settled rather than by position in a second list.
3298
+ // Choosing by index here is how a click lands on the wrong row.
3299
+ // A target with no name is written with none, not with the
3300
+ // word "undefined": interpolating a missing name produced
3301
+ // aria/undefined[role="button"], which matches nothing, so an
3302
+ // element the outline had just offered refused every act.
3303
+ const handles = await this.page.$$(`aria/${target.name ?? ""}[role="${target.role}"]`);
3304
+ let element;
3305
+ for (const handle of handles) {
3306
+ if (element === undefined &&
3307
+ (await this.backendIdOf(handle)) === resolution.backendDomId) {
3308
+ element = handle;
3309
+ continue;
3310
+ }
3311
+ await handle.dispose();
3312
+ }
3313
+ // The outline named a node the aria selector does not offer: the
3314
+ // page moved, or the two matchers disagree. Refusing is the only
3315
+ // honest answer, because any handle we still hold is a guess.
3316
+ if (!element)
3317
+ return { ok: false, refusal: notFoundRefusal(tree, target) };
3318
+ return { ok: true, element };
3319
+ }
3320
+ /**
3321
+ * The backend node id behind a handle, or undefined if it has gone.
3322
+ *
3323
+ * This asks puppeteer rather than sending DOM.describeNode ourselves
3324
+ * because a remote object id is scoped to the session that minted it,
3325
+ * and these handles were minted by puppeteer's session, not ours.
3326
+ * Resolving them through this.cdp fails for every handle, which reads
3327
+ * as "no candidate matched" and refuses every act.
3328
+ */
3329
+ async backendIdOf(handle) {
3330
+ try {
3331
+ return await handle.backendNodeId();
3332
+ }
3333
+ catch {
3334
+ // The node left the document between the outline and this
3335
+ // lookup. It cannot be the one we resolved, so say so.
3336
+ return undefined;
3337
+ }
3338
+ }
3339
+ async axTree() {
3340
+ // Nearly every reading starts here, which makes it the right
3341
+ // place to wait out a crash recovery rather than issuing a
3342
+ // call to a renderer that will never answer.
3343
+ await this.ready();
3344
+ const { nodes } = (await this.cdp.send("Accessibility.getFullAXTree"));
3345
+ return normalizeAxTree(spliceFrames(nodes, await this.frameTrees()));
3346
+ }
3347
+ /**
3348
+ * Each same-origin child frame's tree, and what owns it.
3349
+ *
3350
+ * Chrome answers for one frame at a time, so a page built from
3351
+ * embeds read as a row of empty boxes: the outline stopped at
3352
+ * the Iframe node and said nothing about stopping. Everything
3353
+ * downstream inherited that, so a keyboard walk skipped an
3354
+ * embedded form and an audit passed a page it had not seen.
3355
+ *
3356
+ * Breadth-first, parents before children, because the splice
3357
+ * attaches each frame to an owner that must already be in the
3358
+ * tree by the time its turn comes.
3359
+ *
3360
+ * A frame that cannot be read is left out rather than reported
3361
+ * as empty. Cross-origin frames are the common case and are a
3362
+ * real limit, not a fault; the count of what could not be read
3363
+ * belongs to the callers that already report it.
3364
+ */
3365
+ async frameTrees() {
3366
+ let tree;
3367
+ try {
3368
+ tree = (await this.cdp.send("Page.getFrameTree"));
3369
+ }
3370
+ catch {
3371
+ // No frame tree means nothing to splice, which is the answer
3372
+ // for the overwhelming majority of pages.
3373
+ return [];
3374
+ }
3375
+ const queue = [...(tree.frameTree.childFrames ?? [])];
3376
+ const collected = [];
3377
+ while (queue.length > 0) {
3378
+ const node = queue.shift();
3379
+ if (!node)
3380
+ break;
3381
+ queue.push(...(node.childFrames ?? []));
3382
+ const frameId = node.frame.id;
3383
+ try {
3384
+ const { backendNodeId } = (await this.cdp.send("DOM.getFrameOwner", {
3385
+ frameId,
3386
+ }));
3387
+ const { nodes } = (await this.cdp.send("Accessibility.getFullAXTree", {
3388
+ frameId,
3389
+ }));
3390
+ collected.push({ ownerBackendNodeId: backendNodeId, nodes });
3391
+ }
3392
+ catch {
3393
+ // A cross-origin frame refuses both calls, and a frame can
3394
+ // be torn down between listing it and asking about it.
3395
+ // Neither is a reason to fail the whole read.
3396
+ }
3397
+ }
3398
+ return collected;
3399
+ }
3400
+ }