tabwright 1.0.0 → 1.0.1

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 (562) hide show
  1. package/dist/aria-snapshot.d.ts +0 -1
  2. package/dist/aria-snapshot.js +9 -1
  3. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +0 -1
  4. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +0 -1
  5. package/dist/bippy.js +1 -1
  6. package/dist/browser-config.d.ts +0 -1
  7. package/dist/browser-config.js +0 -1
  8. package/dist/browser-install.d.ts +0 -1
  9. package/dist/browser-install.js +0 -1
  10. package/dist/browser-launch.d.ts +0 -1
  11. package/dist/browser-launch.js +0 -1
  12. package/dist/builtin-capabilities.d.ts +0 -1
  13. package/dist/builtin-capabilities.js +0 -1
  14. package/dist/capability-agent-skill.d.ts +0 -1
  15. package/dist/capability-agent-skill.js +0 -1
  16. package/dist/capability-auth-state.d.ts +0 -1
  17. package/dist/capability-auth-state.js +0 -1
  18. package/dist/capability-auth.d.ts +0 -1
  19. package/dist/capability-auth.js +0 -1
  20. package/dist/capability-options.d.ts +0 -1
  21. package/dist/capability-options.js +0 -1
  22. package/dist/capability-package.d.ts +0 -1
  23. package/dist/capability-package.js +0 -1
  24. package/dist/capability-registry.d.ts +0 -1
  25. package/dist/capability-registry.js +0 -1
  26. package/dist/capability-runner.d.ts +0 -1
  27. package/dist/capability-runner.js +0 -1
  28. package/dist/capability-studio.d.ts +0 -1
  29. package/dist/capability-studio.js +0 -1
  30. package/dist/cdp-log.d.ts +0 -1
  31. package/dist/cdp-log.js +0 -1
  32. package/dist/cdp-relay.d.ts +0 -1
  33. package/dist/cdp-relay.js +0 -1
  34. package/dist/cdp-session.d.ts +0 -1
  35. package/dist/cdp-session.js +0 -1
  36. package/dist/cdp-types.d.ts +0 -1
  37. package/dist/cdp-types.js +0 -1
  38. package/dist/chrome-discovery.d.ts +0 -1
  39. package/dist/chrome-discovery.js +0 -1
  40. package/dist/clean-html.d.ts +0 -1
  41. package/dist/clean-html.js +0 -1
  42. package/dist/cli.d.ts +0 -1
  43. package/dist/cli.js +9 -2
  44. package/dist/cloud-client.d.ts +0 -1
  45. package/dist/cloud-client.js +0 -1
  46. package/dist/create-logger.d.ts +0 -1
  47. package/dist/create-logger.js +0 -1
  48. package/dist/debugger-examples-types.d.ts +0 -1
  49. package/dist/debugger-examples-types.js +0 -1
  50. package/dist/debugger-examples.d.ts +0 -1
  51. package/dist/debugger-examples.js +0 -1
  52. package/dist/debugger.d.ts +0 -1
  53. package/dist/debugger.js +0 -1
  54. package/dist/diff-utils.d.ts +0 -1
  55. package/dist/diff-utils.js +0 -1
  56. package/dist/doctor.d.ts +0 -1
  57. package/dist/doctor.js +5 -7
  58. package/dist/editor-examples.d.ts +0 -1
  59. package/dist/editor-examples.js +0 -1
  60. package/dist/editor.d.ts +0 -1
  61. package/dist/editor.js +0 -1
  62. package/dist/env-compat.d.ts +0 -1
  63. package/dist/env-compat.js +0 -1
  64. package/dist/executor.d.ts +0 -1
  65. package/dist/executor.js +1 -2
  66. package/dist/extension/background.js +7 -3
  67. package/dist/extension/manifest.json +1 -1
  68. package/dist/ghost-browser.d.ts +0 -1
  69. package/dist/ghost-browser.js +0 -1
  70. package/dist/ghost-cursor-controller.d.ts +0 -1
  71. package/dist/ghost-cursor-controller.js +0 -1
  72. package/dist/ghost-cursor.d.ts +0 -1
  73. package/dist/ghost-cursor.js +0 -1
  74. package/dist/htmlrewrite.d.ts +0 -1
  75. package/dist/htmlrewrite.js +0 -1
  76. package/dist/index.d.ts +0 -1
  77. package/dist/index.js +0 -1
  78. package/dist/kill-port.d.ts +0 -1
  79. package/dist/kill-port.js +0 -1
  80. package/dist/kitty-graphics.d.ts +0 -1
  81. package/dist/kitty-graphics.js +0 -1
  82. package/dist/mcp.d.ts +0 -1
  83. package/dist/mcp.js +0 -1
  84. package/dist/package-paths.d.ts +0 -1
  85. package/dist/package-paths.js +0 -1
  86. package/dist/page-markdown.d.ts +0 -1
  87. package/dist/page-markdown.js +0 -1
  88. package/dist/performance-examples.d.ts +0 -1
  89. package/dist/performance-examples.js +0 -1
  90. package/dist/playwright-import.d.ts +0 -1
  91. package/dist/playwright-import.js +0 -1
  92. package/dist/product-paths.d.ts +0 -1
  93. package/dist/product-paths.js +0 -1
  94. package/dist/protocol.d.ts +0 -1
  95. package/dist/protocol.js +0 -1
  96. package/dist/react-source.d.ts +0 -1
  97. package/dist/react-source.js +0 -1
  98. package/dist/readability.js +1 -1
  99. package/dist/relay-client.d.ts +0 -1
  100. package/dist/relay-client.js +0 -1
  101. package/dist/relay-state.d.ts +0 -1
  102. package/dist/relay-state.js +0 -1
  103. package/dist/replay-ai-index.d.ts +0 -1
  104. package/dist/replay-ai-index.js +0 -1
  105. package/dist/replay-eval.d.ts +0 -1
  106. package/dist/replay-eval.js +0 -1
  107. package/dist/replay-handoff.d.ts +0 -1
  108. package/dist/replay-handoff.js +0 -1
  109. package/dist/replay-workflow-compiler.d.ts +0 -1
  110. package/dist/replay-workflow-compiler.js +0 -1
  111. package/dist/rrweb-recording-relay.d.ts +0 -1
  112. package/dist/rrweb-recording-relay.js +0 -1
  113. package/dist/rrweb-recording.d.ts +0 -1
  114. package/dist/rrweb-recording.js +0 -1
  115. package/dist/scoped-fs.d.ts +0 -1
  116. package/dist/scoped-fs.js +0 -1
  117. package/dist/selector-generator.js +1 -1
  118. package/dist/start-relay-server.d.ts +0 -1
  119. package/dist/start-relay-server.js +0 -1
  120. package/dist/styles-examples.d.ts +0 -1
  121. package/dist/styles-examples.js +0 -1
  122. package/dist/styles.d.ts +0 -1
  123. package/dist/styles.js +0 -1
  124. package/dist/tabwright-agent-skill.d.ts +0 -1
  125. package/dist/tabwright-agent-skill.js +0 -1
  126. package/dist/test-declarations.d.ts +0 -1
  127. package/dist/test-declarations.js +0 -1
  128. package/dist/utils.d.ts +0 -1
  129. package/dist/utils.js +2 -2
  130. package/dist/wait-for-page-load.d.ts +0 -1
  131. package/dist/wait-for-page-load.js +0 -1
  132. package/dist/workflow-capability.d.ts +0 -1
  133. package/dist/workflow-capability.js +0 -1
  134. package/package.json +9 -4
  135. package/readme.md +35 -0
  136. package/dist/aria-snapshot.d.ts.map +0 -1
  137. package/dist/aria-snapshot.js.map +0 -1
  138. package/dist/aria-snapshot.test.d.ts +0 -2
  139. package/dist/aria-snapshot.test.d.ts.map +0 -1
  140. package/dist/aria-snapshot.test.js +0 -270
  141. package/dist/aria-snapshot.test.js.map +0 -1
  142. package/dist/aria-snapshot.unit.test.d.ts +0 -2
  143. package/dist/aria-snapshot.unit.test.d.ts.map +0 -1
  144. package/dist/aria-snapshot.unit.test.js +0 -542
  145. package/dist/aria-snapshot.unit.test.js.map +0 -1
  146. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +0 -1
  147. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +0 -1
  148. package/dist/browser-config.d.ts.map +0 -1
  149. package/dist/browser-config.js.map +0 -1
  150. package/dist/browser-install.d.ts.map +0 -1
  151. package/dist/browser-install.js.map +0 -1
  152. package/dist/browser-launch.d.ts.map +0 -1
  153. package/dist/browser-launch.js.map +0 -1
  154. package/dist/builtin-capabilities.d.ts.map +0 -1
  155. package/dist/builtin-capabilities.js.map +0 -1
  156. package/dist/capability-agent-skill.d.ts.map +0 -1
  157. package/dist/capability-agent-skill.js.map +0 -1
  158. package/dist/capability-auth-state.d.ts.map +0 -1
  159. package/dist/capability-auth-state.js.map +0 -1
  160. package/dist/capability-auth.d.ts.map +0 -1
  161. package/dist/capability-auth.js.map +0 -1
  162. package/dist/capability-cli-confirmation.test.d.ts +0 -2
  163. package/dist/capability-cli-confirmation.test.d.ts.map +0 -1
  164. package/dist/capability-cli-confirmation.test.js +0 -79
  165. package/dist/capability-cli-confirmation.test.js.map +0 -1
  166. package/dist/capability-options.d.ts.map +0 -1
  167. package/dist/capability-options.js.map +0 -1
  168. package/dist/capability-options.test.d.ts +0 -2
  169. package/dist/capability-options.test.d.ts.map +0 -1
  170. package/dist/capability-options.test.js +0 -81
  171. package/dist/capability-options.test.js.map +0 -1
  172. package/dist/capability-package.d.ts.map +0 -1
  173. package/dist/capability-package.js.map +0 -1
  174. package/dist/capability-package.test.d.ts +0 -2
  175. package/dist/capability-package.test.d.ts.map +0 -1
  176. package/dist/capability-package.test.js +0 -179
  177. package/dist/capability-package.test.js.map +0 -1
  178. package/dist/capability-registry.d.ts.map +0 -1
  179. package/dist/capability-registry.js.map +0 -1
  180. package/dist/capability-registry.test.d.ts +0 -2
  181. package/dist/capability-registry.test.d.ts.map +0 -1
  182. package/dist/capability-registry.test.js +0 -1683
  183. package/dist/capability-registry.test.js.map +0 -1
  184. package/dist/capability-runner.d.ts.map +0 -1
  185. package/dist/capability-runner.js.map +0 -1
  186. package/dist/capability-search-relevance.test.d.ts +0 -2
  187. package/dist/capability-search-relevance.test.d.ts.map +0 -1
  188. package/dist/capability-search-relevance.test.js +0 -67
  189. package/dist/capability-search-relevance.test.js.map +0 -1
  190. package/dist/capability-studio.d.ts.map +0 -1
  191. package/dist/capability-studio.js.map +0 -1
  192. package/dist/capability-studio.test.d.ts +0 -2
  193. package/dist/capability-studio.test.d.ts.map +0 -1
  194. package/dist/capability-studio.test.js +0 -41
  195. package/dist/capability-studio.test.js.map +0 -1
  196. package/dist/cdp-log.d.ts.map +0 -1
  197. package/dist/cdp-log.js.map +0 -1
  198. package/dist/cdp-log.test.d.ts +0 -2
  199. package/dist/cdp-log.test.d.ts.map +0 -1
  200. package/dist/cdp-log.test.js +0 -109
  201. package/dist/cdp-log.test.js.map +0 -1
  202. package/dist/cdp-relay.d.ts.map +0 -1
  203. package/dist/cdp-relay.js.map +0 -1
  204. package/dist/cdp-session.d.ts.map +0 -1
  205. package/dist/cdp-session.js.map +0 -1
  206. package/dist/cdp-types.d.ts.map +0 -1
  207. package/dist/cdp-types.js.map +0 -1
  208. package/dist/channel-owner-inspect.test.d.ts +0 -2
  209. package/dist/channel-owner-inspect.test.d.ts.map +0 -1
  210. package/dist/channel-owner-inspect.test.js +0 -75
  211. package/dist/channel-owner-inspect.test.js.map +0 -1
  212. package/dist/chrome-discovery.d.ts.map +0 -1
  213. package/dist/chrome-discovery.js.map +0 -1
  214. package/dist/chrome-discovery.test.d.ts +0 -2
  215. package/dist/chrome-discovery.test.d.ts.map +0 -1
  216. package/dist/chrome-discovery.test.js +0 -41
  217. package/dist/chrome-discovery.test.js.map +0 -1
  218. package/dist/clean-html.d.ts.map +0 -1
  219. package/dist/clean-html.js.map +0 -1
  220. package/dist/cli-help.test.d.ts +0 -2
  221. package/dist/cli-help.test.d.ts.map +0 -1
  222. package/dist/cli-help.test.js +0 -98
  223. package/dist/cli-help.test.js.map +0 -1
  224. package/dist/cli.d.ts.map +0 -1
  225. package/dist/cli.js.map +0 -1
  226. package/dist/cloud-client.d.ts.map +0 -1
  227. package/dist/cloud-client.js.map +0 -1
  228. package/dist/create-logger.d.ts.map +0 -1
  229. package/dist/create-logger.js.map +0 -1
  230. package/dist/debugger-examples-types.d.ts.map +0 -1
  231. package/dist/debugger-examples-types.js.map +0 -1
  232. package/dist/debugger-examples.d.ts.map +0 -1
  233. package/dist/debugger-examples.js.map +0 -1
  234. package/dist/debugger.d.ts.map +0 -1
  235. package/dist/debugger.js.map +0 -1
  236. package/dist/diff-utils.d.ts.map +0 -1
  237. package/dist/diff-utils.js.map +0 -1
  238. package/dist/diff-utils.test.d.ts +0 -2
  239. package/dist/diff-utils.test.d.ts.map +0 -1
  240. package/dist/diff-utils.test.js +0 -137
  241. package/dist/diff-utils.test.js.map +0 -1
  242. package/dist/doctor.d.ts.map +0 -1
  243. package/dist/doctor.js.map +0 -1
  244. package/dist/doctor.test.d.ts +0 -2
  245. package/dist/doctor.test.d.ts.map +0 -1
  246. package/dist/doctor.test.js +0 -208
  247. package/dist/doctor.test.js.map +0 -1
  248. package/dist/editor-examples.d.ts.map +0 -1
  249. package/dist/editor-examples.js.map +0 -1
  250. package/dist/editor.d.ts.map +0 -1
  251. package/dist/editor.js.map +0 -1
  252. package/dist/env-compat.d.ts.map +0 -1
  253. package/dist/env-compat.js.map +0 -1
  254. package/dist/executor.d.ts.map +0 -1
  255. package/dist/executor.js.map +0 -1
  256. package/dist/executor.unit.test.d.ts +0 -2
  257. package/dist/executor.unit.test.d.ts.map +0 -1
  258. package/dist/executor.unit.test.js +0 -143
  259. package/dist/executor.unit.test.js.map +0 -1
  260. package/dist/extension-connection.test.d.ts +0 -2
  261. package/dist/extension-connection.test.d.ts.map +0 -1
  262. package/dist/extension-connection.test.js +0 -708
  263. package/dist/extension-connection.test.js.map +0 -1
  264. package/dist/extension-icon-warning.test.d.ts +0 -2
  265. package/dist/extension-icon-warning.test.d.ts.map +0 -1
  266. package/dist/extension-icon-warning.test.js +0 -33
  267. package/dist/extension-icon-warning.test.js.map +0 -1
  268. package/dist/ghost-browser.d.ts.map +0 -1
  269. package/dist/ghost-browser.js.map +0 -1
  270. package/dist/ghost-cursor-controller.d.ts.map +0 -1
  271. package/dist/ghost-cursor-controller.js.map +0 -1
  272. package/dist/ghost-cursor.d.ts.map +0 -1
  273. package/dist/ghost-cursor.js.map +0 -1
  274. package/dist/htmlrewrite.d.ts.map +0 -1
  275. package/dist/htmlrewrite.js.map +0 -1
  276. package/dist/htmlrewrite.test.d.ts +0 -2
  277. package/dist/htmlrewrite.test.d.ts.map +0 -1
  278. package/dist/htmlrewrite.test.js +0 -7975
  279. package/dist/htmlrewrite.test.js.map +0 -1
  280. package/dist/index.d.ts.map +0 -1
  281. package/dist/index.js.map +0 -1
  282. package/dist/kill-port.d.ts.map +0 -1
  283. package/dist/kill-port.js.map +0 -1
  284. package/dist/kitty-graphics.d.ts.map +0 -1
  285. package/dist/kitty-graphics.js.map +0 -1
  286. package/dist/kitty-graphics.test.d.ts +0 -2
  287. package/dist/kitty-graphics.test.d.ts.map +0 -1
  288. package/dist/kitty-graphics.test.js +0 -125
  289. package/dist/kitty-graphics.test.js.map +0 -1
  290. package/dist/locator-selector.test.d.ts +0 -2
  291. package/dist/locator-selector.test.d.ts.map +0 -1
  292. package/dist/locator-selector.test.js +0 -98
  293. package/dist/locator-selector.test.js.map +0 -1
  294. package/dist/mcp-client.d.ts +0 -20
  295. package/dist/mcp-client.d.ts.map +0 -1
  296. package/dist/mcp-client.js +0 -56
  297. package/dist/mcp-client.js.map +0 -1
  298. package/dist/mcp.d.ts.map +0 -1
  299. package/dist/mcp.js.map +0 -1
  300. package/dist/on-mouse-action.test.d.ts +0 -2
  301. package/dist/on-mouse-action.test.d.ts.map +0 -1
  302. package/dist/on-mouse-action.test.js +0 -180
  303. package/dist/on-mouse-action.test.js.map +0 -1
  304. package/dist/options-page.test.d.ts +0 -2
  305. package/dist/options-page.test.d.ts.map +0 -1
  306. package/dist/options-page.test.js +0 -773
  307. package/dist/options-page.test.js.map +0 -1
  308. package/dist/package-paths.d.ts.map +0 -1
  309. package/dist/package-paths.js.map +0 -1
  310. package/dist/page-markdown.d.ts.map +0 -1
  311. package/dist/page-markdown.js.map +0 -1
  312. package/dist/performance-examples.d.ts.map +0 -1
  313. package/dist/performance-examples.js.map +0 -1
  314. package/dist/playwright-import.d.ts.map +0 -1
  315. package/dist/playwright-import.js.map +0 -1
  316. package/dist/popup-relocation.test.d.ts +0 -7
  317. package/dist/popup-relocation.test.d.ts.map +0 -1
  318. package/dist/popup-relocation.test.js +0 -139
  319. package/dist/popup-relocation.test.js.map +0 -1
  320. package/dist/product-paths.d.ts.map +0 -1
  321. package/dist/product-paths.js.map +0 -1
  322. package/dist/product-paths.test.d.ts +0 -2
  323. package/dist/product-paths.test.d.ts.map +0 -1
  324. package/dist/product-paths.test.js +0 -39
  325. package/dist/product-paths.test.js.map +0 -1
  326. package/dist/protocol.d.ts.map +0 -1
  327. package/dist/protocol.js.map +0 -1
  328. package/dist/protocol.test.d.ts +0 -2
  329. package/dist/protocol.test.d.ts.map +0 -1
  330. package/dist/protocol.test.js +0 -31
  331. package/dist/protocol.test.js.map +0 -1
  332. package/dist/react-source.d.ts.map +0 -1
  333. package/dist/react-source.js.map +0 -1
  334. package/dist/relay-client.d.ts.map +0 -1
  335. package/dist/relay-client.js.map +0 -1
  336. package/dist/relay-client.test.d.ts +0 -2
  337. package/dist/relay-client.test.d.ts.map +0 -1
  338. package/dist/relay-client.test.js +0 -305
  339. package/dist/relay-client.test.js.map +0 -1
  340. package/dist/relay-core.test.d.ts +0 -2
  341. package/dist/relay-core.test.d.ts.map +0 -1
  342. package/dist/relay-core.test.js +0 -1427
  343. package/dist/relay-core.test.js.map +0 -1
  344. package/dist/relay-navigation.test.d.ts +0 -2
  345. package/dist/relay-navigation.test.d.ts.map +0 -1
  346. package/dist/relay-navigation.test.js +0 -661
  347. package/dist/relay-navigation.test.js.map +0 -1
  348. package/dist/relay-session.test.d.ts +0 -2
  349. package/dist/relay-session.test.d.ts.map +0 -1
  350. package/dist/relay-session.test.js +0 -1121
  351. package/dist/relay-session.test.js.map +0 -1
  352. package/dist/relay-state.d.ts.map +0 -1
  353. package/dist/relay-state.js.map +0 -1
  354. package/dist/relay-state.test.d.ts +0 -2
  355. package/dist/relay-state.test.d.ts.map +0 -1
  356. package/dist/relay-state.test.js +0 -594
  357. package/dist/relay-state.test.js.map +0 -1
  358. package/dist/replay-ai-index.d.ts.map +0 -1
  359. package/dist/replay-ai-index.js.map +0 -1
  360. package/dist/replay-ai-index.test.d.ts +0 -2
  361. package/dist/replay-ai-index.test.d.ts.map +0 -1
  362. package/dist/replay-ai-index.test.js +0 -255
  363. package/dist/replay-ai-index.test.js.map +0 -1
  364. package/dist/replay-cli.test.d.ts +0 -2
  365. package/dist/replay-cli.test.d.ts.map +0 -1
  366. package/dist/replay-cli.test.js +0 -275
  367. package/dist/replay-cli.test.js.map +0 -1
  368. package/dist/replay-eval.d.ts.map +0 -1
  369. package/dist/replay-eval.js.map +0 -1
  370. package/dist/replay-handoff.d.ts.map +0 -1
  371. package/dist/replay-handoff.js.map +0 -1
  372. package/dist/replay-handoff.test.d.ts +0 -2
  373. package/dist/replay-handoff.test.d.ts.map +0 -1
  374. package/dist/replay-handoff.test.js +0 -123
  375. package/dist/replay-handoff.test.js.map +0 -1
  376. package/dist/replay-workflow-compiler.d.ts.map +0 -1
  377. package/dist/replay-workflow-compiler.js.map +0 -1
  378. package/dist/replay-workflow-compiler.test.d.ts +0 -2
  379. package/dist/replay-workflow-compiler.test.d.ts.map +0 -1
  380. package/dist/replay-workflow-compiler.test.js +0 -223
  381. package/dist/replay-workflow-compiler.test.js.map +0 -1
  382. package/dist/rrweb-recording-relay.d.ts.map +0 -1
  383. package/dist/rrweb-recording-relay.js.map +0 -1
  384. package/dist/rrweb-recording-relay.test.d.ts +0 -2
  385. package/dist/rrweb-recording-relay.test.d.ts.map +0 -1
  386. package/dist/rrweb-recording-relay.test.js +0 -85
  387. package/dist/rrweb-recording-relay.test.js.map +0 -1
  388. package/dist/rrweb-recording.d.ts.map +0 -1
  389. package/dist/rrweb-recording.js.map +0 -1
  390. package/dist/scoped-fs.d.ts.map +0 -1
  391. package/dist/scoped-fs.js.map +0 -1
  392. package/dist/scoped-fs.test.d.ts +0 -5
  393. package/dist/scoped-fs.test.d.ts.map +0 -1
  394. package/dist/scoped-fs.test.js +0 -73
  395. package/dist/scoped-fs.test.js.map +0 -1
  396. package/dist/skill-stub.test.d.ts +0 -2
  397. package/dist/skill-stub.test.d.ts.map +0 -1
  398. package/dist/skill-stub.test.js +0 -22
  399. package/dist/skill-stub.test.js.map +0 -1
  400. package/dist/snapshot-tools.test.d.ts +0 -2
  401. package/dist/snapshot-tools.test.d.ts.map +0 -1
  402. package/dist/snapshot-tools.test.js +0 -856
  403. package/dist/snapshot-tools.test.js.map +0 -1
  404. package/dist/start-relay-server.d.ts.map +0 -1
  405. package/dist/start-relay-server.js.map +0 -1
  406. package/dist/styles-examples.d.ts.map +0 -1
  407. package/dist/styles-examples.js.map +0 -1
  408. package/dist/styles.d.ts.map +0 -1
  409. package/dist/styles.js.map +0 -1
  410. package/dist/tabwright-agent-skill.d.ts.map +0 -1
  411. package/dist/tabwright-agent-skill.js.map +0 -1
  412. package/dist/tabwright-agent-skill.test.d.ts +0 -2
  413. package/dist/tabwright-agent-skill.test.d.ts.map +0 -1
  414. package/dist/tabwright-agent-skill.test.js +0 -51
  415. package/dist/tabwright-agent-skill.test.js.map +0 -1
  416. package/dist/test-declarations.d.ts.map +0 -1
  417. package/dist/test-declarations.js.map +0 -1
  418. package/dist/test-utils.d.ts +0 -69
  419. package/dist/test-utils.d.ts.map +0 -1
  420. package/dist/test-utils.js +0 -448
  421. package/dist/test-utils.js.map +0 -1
  422. package/dist/test-utils.test.d.ts +0 -2
  423. package/dist/test-utils.test.d.ts.map +0 -1
  424. package/dist/test-utils.test.js +0 -55
  425. package/dist/test-utils.test.js.map +0 -1
  426. package/dist/utils.d.ts.map +0 -1
  427. package/dist/utils.js.map +0 -1
  428. package/dist/wait-for-page-load.d.ts.map +0 -1
  429. package/dist/wait-for-page-load.js.map +0 -1
  430. package/dist/workflow-capability.d.ts.map +0 -1
  431. package/dist/workflow-capability.js.map +0 -1
  432. package/dist/workflow-script-flow.test.d.ts +0 -2
  433. package/dist/workflow-script-flow.test.d.ts.map +0 -1
  434. package/dist/workflow-script-flow.test.js +0 -285
  435. package/dist/workflow-script-flow.test.js.map +0 -1
  436. package/src/__snapshots__/x.com.processed.html +0 -1003
  437. package/src/__snapshots__/x.com.processed.withStyles.html +0 -2720
  438. package/src/a11y-client.ts +0 -241
  439. package/src/aria-snapshot.test.ts +0 -311
  440. package/src/aria-snapshot.ts +0 -1681
  441. package/src/aria-snapshot.unit.test.ts +0 -615
  442. package/src/aria-snapshots/github-interactive.txt +0 -193
  443. package/src/aria-snapshots/github-raw.txt +0 -309
  444. package/src/aria-snapshots/hackernews-interactive.txt +0 -477
  445. package/src/aria-snapshots/hackernews-raw.txt +0 -428
  446. package/src/aria-snapshots/prosemirror-interactive.txt +0 -66
  447. package/src/aria-snapshots/prosemirror-raw.txt +0 -145
  448. package/src/assets/aria-labels-hacker-news.png +0 -0
  449. package/src/assets/aria-labels-old-reddit.png +0 -0
  450. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +0 -5
  451. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +0 -18
  452. package/src/assets/x.com.html +0 -32946
  453. package/src/browser-config.ts +0 -179
  454. package/src/browser-install.ts +0 -284
  455. package/src/browser-launch.ts +0 -75
  456. package/src/builtin-capabilities.ts +0 -2765
  457. package/src/capability-agent-skill.ts +0 -305
  458. package/src/capability-auth-state.ts +0 -197
  459. package/src/capability-auth.ts +0 -249
  460. package/src/capability-cli-confirmation.test.ts +0 -90
  461. package/src/capability-options.test.ts +0 -89
  462. package/src/capability-options.ts +0 -57
  463. package/src/capability-package.test.ts +0 -204
  464. package/src/capability-package.ts +0 -555
  465. package/src/capability-registry.test.ts +0 -1905
  466. package/src/capability-registry.ts +0 -1234
  467. package/src/capability-runner.ts +0 -801
  468. package/src/capability-search-relevance.test.ts +0 -73
  469. package/src/capability-studio.test.ts +0 -45
  470. package/src/capability-studio.ts +0 -641
  471. package/src/cdp-log.test.ts +0 -131
  472. package/src/cdp-log.ts +0 -112
  473. package/src/cdp-relay.ts +0 -2944
  474. package/src/cdp-session.ts +0 -77
  475. package/src/cdp-types.ts +0 -158
  476. package/src/channel-owner-inspect.test.ts +0 -94
  477. package/src/chrome-discovery.test.ts +0 -50
  478. package/src/chrome-discovery.ts +0 -220
  479. package/src/clean-html.ts +0 -141
  480. package/src/cli-help.test.ts +0 -123
  481. package/src/cli.ts +0 -3181
  482. package/src/cloud-client.ts +0 -179
  483. package/src/create-logger.ts +0 -38
  484. package/src/debugger-examples-types.ts +0 -16
  485. package/src/debugger-examples.ts +0 -66
  486. package/src/debugger.ts +0 -708
  487. package/src/diff-utils.test.ts +0 -152
  488. package/src/diff-utils.ts +0 -70
  489. package/src/doctor.test.ts +0 -244
  490. package/src/doctor.ts +0 -321
  491. package/src/editor-examples.ts +0 -158
  492. package/src/editor.ts +0 -426
  493. package/src/env-compat.ts +0 -28
  494. package/src/executor.ts +0 -1824
  495. package/src/executor.unit.test.ts +0 -168
  496. package/src/extension-connection.test.ts +0 -855
  497. package/src/extension-icon-warning.test.ts +0 -45
  498. package/src/ghost-browser.ts +0 -149
  499. package/src/ghost-cursor-client.ts +0 -494
  500. package/src/ghost-cursor-controller.ts +0 -129
  501. package/src/ghost-cursor.ts +0 -123
  502. package/src/htmlrewrite.test.ts +0 -8008
  503. package/src/htmlrewrite.ts +0 -390
  504. package/src/index.ts +0 -53
  505. package/src/kill-port.ts +0 -219
  506. package/src/kitty-graphics.test.ts +0 -139
  507. package/src/kitty-graphics.ts +0 -79
  508. package/src/locator-selector.test.ts +0 -119
  509. package/src/mcp-client.ts +0 -78
  510. package/src/mcp.ts +0 -629
  511. package/src/on-mouse-action.test.ts +0 -226
  512. package/src/options-page.test.ts +0 -839
  513. package/src/package-paths.ts +0 -38
  514. package/src/page-markdown.ts +0 -240
  515. package/src/performance-examples.ts +0 -186
  516. package/src/playwright-import.ts +0 -59
  517. package/src/popup-relocation.test.ts +0 -163
  518. package/src/product-paths.test.ts +0 -47
  519. package/src/product-paths.ts +0 -28
  520. package/src/protocol.test.ts +0 -52
  521. package/src/protocol.ts +0 -351
  522. package/src/react-source.ts +0 -379
  523. package/src/relay-client.test.ts +0 -375
  524. package/src/relay-client.ts +0 -572
  525. package/src/relay-core.test.ts +0 -1603
  526. package/src/relay-navigation.test.ts +0 -790
  527. package/src/relay-session.test.ts +0 -1375
  528. package/src/relay-state.test.ts +0 -729
  529. package/src/relay-state.ts +0 -561
  530. package/src/replay-ai-index.test.ts +0 -278
  531. package/src/replay-ai-index.ts +0 -872
  532. package/src/replay-cli.test.ts +0 -325
  533. package/src/replay-eval.ts +0 -1039
  534. package/src/replay-handoff.test.ts +0 -163
  535. package/src/replay-handoff.ts +0 -109
  536. package/src/replay-workflow-compiler.test.ts +0 -250
  537. package/src/replay-workflow-compiler.ts +0 -700
  538. package/src/resource.md +0 -409
  539. package/src/rrweb-recording-relay.test.ts +0 -99
  540. package/src/rrweb-recording-relay.ts +0 -400
  541. package/src/rrweb-recording.ts +0 -322
  542. package/src/scoped-fs.test.ts +0 -80
  543. package/src/scoped-fs.ts +0 -420
  544. package/src/skill-stub.test.ts +0 -24
  545. package/src/snapshot-tools.test.ts +0 -1017
  546. package/src/snapshots/hacker-news-accessibility-full.md +0 -81
  547. package/src/snapshots/hacker-news-accessibility-interactive.md +0 -43
  548. package/src/snapshots/page-markdown-output.txt +0 -16
  549. package/src/snapshots/shadcn-ui-accessibility-full.md +0 -201
  550. package/src/snapshots/shadcn-ui-accessibility-interactive.md +0 -109
  551. package/src/start-relay-server.ts +0 -69
  552. package/src/styles-examples.ts +0 -84
  553. package/src/styles.ts +0 -343
  554. package/src/tabwright-agent-skill.test.ts +0 -61
  555. package/src/tabwright-agent-skill.ts +0 -105
  556. package/src/test-declarations.ts +0 -13
  557. package/src/test-utils.test.ts +0 -70
  558. package/src/test-utils.ts +0 -566
  559. package/src/utils.ts +0 -78
  560. package/src/wait-for-page-load.ts +0 -198
  561. package/src/workflow-capability.ts +0 -417
  562. package/src/workflow-script-flow.test.ts +0 -388
@@ -1,1681 +0,0 @@
1
- // Accessibility snapshot pipeline: build raw AX tree, filter to a
2
- // tree (interactive-only, labels/contexts, wrapper hoisting, ignored
3
- // indent preservation), then render lines and locators.
4
- import type { Page, Locator, ElementHandle, Frame, FrameLocator } from '@xmorse/playwright-core'
5
- import crypto from 'node:crypto'
6
- import fs from 'node:fs'
7
- import path from 'node:path'
8
- import { fileURLToPath } from 'node:url'
9
- import type { Protocol } from 'devtools-protocol'
10
- import { Sema } from 'async-sema'
11
- import type { ICDPSession } from './cdp-session.js'
12
- import { getCDPSessionForPage } from './cdp-session.js'
13
-
14
- // Import sharp at module level - resolves to null if not available
15
- const sharpPromise = import('sharp')
16
- .then((m) => {
17
- return m.default
18
- })
19
- .catch(() => {
20
- return null
21
- })
22
-
23
- // ============================================================================
24
- // Snapshot Format Types
25
- // ============================================================================
26
-
27
- export type SnapshotFormat = 'raw'
28
-
29
- export const DEFAULT_SNAPSHOT_FORMAT: SnapshotFormat = 'raw'
30
-
31
- // ============================================================================
32
- // A11y Client Code Loading
33
- // ============================================================================
34
-
35
- let a11yClientCode: string | null = null
36
-
37
- function getA11yClientCode(): string {
38
- if (a11yClientCode) {
39
- return a11yClientCode
40
- }
41
- const currentDir = path.dirname(fileURLToPath(import.meta.url))
42
- const a11yClientPath = path.join(currentDir, '..', 'dist', 'a11y-client.js')
43
- a11yClientCode = fs.readFileSync(a11yClientPath, 'utf-8')
44
- return a11yClientCode
45
- }
46
-
47
- async function ensureA11yClient(page: Page): Promise<void> {
48
- const hasA11y = await page.evaluate(() => !!(globalThis as any).__a11y)
49
- if (!hasA11y) {
50
- const code = getA11yClientCode()
51
- await page.evaluate(code)
52
- }
53
- }
54
-
55
- // ============================================================================
56
- // Types
57
- // ============================================================================
58
-
59
- export interface AriaRef {
60
- role: string
61
- name: string
62
- ref: string
63
- shortRef: string
64
- backendNodeId?: Protocol.DOM.BackendNodeId
65
- }
66
-
67
- type AriaRefDraft = Omit<AriaRef, 'shortRef'> & { selector?: string }
68
-
69
- export type AriaSnapshotNode = {
70
- role: string
71
- name: string
72
- locator?: string
73
- ref?: string
74
- shortRef?: string
75
- backendNodeId?: Protocol.DOM.BackendNodeId
76
- children: AriaSnapshotNode[]
77
- }
78
-
79
- export interface ScreenshotResult {
80
- path: string
81
- base64: string
82
- mimeType: 'image/png'
83
- snapshot: string
84
- labelCount: number
85
- }
86
-
87
- // ============================================================================
88
- // Image Resize Utility
89
- // ============================================================================
90
-
91
- /**
92
- * LLM-optimal max dimension. Claude auto-resizes images larger than 1568px
93
- * on any edge, adding latency. Token cost: (width * height) / 750.
94
- */
95
- export const LLM_MAX_DIMENSION = 1568
96
-
97
- export interface ResizeImageOptions {
98
- /** Input: file path or Buffer */
99
- input: string | Buffer
100
- /** Max pixels on longest edge. Default 1568 (Claude-optimal).
101
- * Ignored if explicit width/height provided. */
102
- maxDimension?: number
103
- /** Explicit target width in px (aspect ratio preserved unless both width+height set) */
104
- width?: number
105
- /** Explicit target height in px */
106
- height?: number
107
- /** How to fit when both width+height specified. Default 'inside' (preserve aspect ratio, no crop) */
108
- fit?: 'inside' | 'cover' | 'contain' | 'fill'
109
- /** JPEG quality 1-100. Default 80 */
110
- quality?: number
111
- /** Output format. Default 'jpeg'. Use 'png' for lossless output (e.g. Kitty Graphics). */
112
- format?: 'jpeg' | 'png'
113
- /** Output file path. Defaults to overwriting the input file (when input is a path) */
114
- output?: string
115
- }
116
-
117
- export interface ResizeImageResult {
118
- buffer: Buffer
119
- mimeType: 'image/png' | 'image/jpeg'
120
- /** Only set if output path was provided */
121
- path?: string
122
- }
123
-
124
- /**
125
- * Resize an image using sharp. Useful for shrinking screenshots before reading
126
- * them back into context so they consume fewer tokens.
127
- *
128
- * Default behavior (no width/height): fits within 1568×1568px, preserving
129
- * aspect ratio, never upscales. This is optimal for Claude vision.
130
- *
131
- * Explicit width/height: resizes to those dimensions using the fit strategy.
132
- */
133
- export async function resizeImageForAgent(options: ResizeImageOptions): Promise<ResizeImageResult> {
134
- const sharp = await sharpPromise
135
- if (!sharp) {
136
- throw new Error('sharp is not installed — install it with: pnpm add sharp')
137
- }
138
-
139
- const inputBuffer = (() => {
140
- if (Buffer.isBuffer(options.input)) {
141
- return options.input
142
- }
143
- return fs.readFileSync(options.input)
144
- })()
145
-
146
- const quality = options.quality ?? 80
147
- const hasExplicitDimensions = options.width !== undefined || options.height !== undefined
148
-
149
- const fit = options.fit ?? 'inside'
150
- const resizeOpts = (() => {
151
- if (hasExplicitDimensions) {
152
- return {
153
- width: options.width,
154
- height: options.height,
155
- fit,
156
- withoutEnlargement: false,
157
- }
158
- }
159
- const max = options.maxDimension ?? LLM_MAX_DIMENSION
160
- return {
161
- width: max,
162
- height: max,
163
- fit,
164
- withoutEnlargement: true,
165
- }
166
- })()
167
-
168
- const fmt = options.format ?? 'png'
169
- const pipeline = sharp(inputBuffer).resize(resizeOpts)
170
- const buffer = await (fmt === 'png' ? pipeline.png() : pipeline.jpeg({ quality })).toBuffer()
171
-
172
- // Default: overwrite input file. When input is a Buffer, no file is written
173
- // unless output is explicitly set.
174
- const outputPath = (() => {
175
- if (options.output) {
176
- return options.output
177
- }
178
- if (typeof options.input === 'string') {
179
- return options.input
180
- }
181
- return undefined
182
- })()
183
-
184
- if (outputPath) {
185
- fs.writeFileSync(outputPath, buffer)
186
- }
187
-
188
- const mimeType: 'image/png' | 'image/jpeg' = fmt === 'png' ? 'image/png' : 'image/jpeg'
189
- return {
190
- buffer,
191
- mimeType,
192
- ...(outputPath ? { path: outputPath } : {}),
193
- }
194
- }
195
-
196
- export interface AriaSnapshotResult {
197
- snapshot: string
198
- tree: AriaSnapshotNode[]
199
- refs: AriaRef[]
200
- refToElement: Map<string, { role: string; name: string; shortRef: string }>
201
- refToSelector: Map<string, string>
202
- /**
203
- * Get a CSS selector for a ref. Use with page.locator().
204
- * For stable test IDs, returns [data-testid="..."] or [id="..."]
205
- * For fallback refs, returns a role-based selector.
206
- */
207
- getSelectorForRef: (ref: string) => string | null
208
- getRefsForLocators: (locators: Array<Locator | ElementHandle>) => Promise<Array<AriaRef | null>>
209
- getRefForLocator: (locator: Locator | ElementHandle) => Promise<AriaRef | null>
210
- getRefStringForLocator: (locator: Locator | ElementHandle) => Promise<string | null>
211
- }
212
-
213
- type LabelBox = {
214
- x: number
215
- y: number
216
- width: number
217
- height: number
218
- }
219
-
220
- type AriaLabel = {
221
- ref: string
222
- role: string
223
- box: LabelBox
224
- }
225
-
226
- export function buildShortRefMap({ refs }: { refs: Array<{ ref: string }> }): Map<string, string> {
227
- const map = new Map<string, string>()
228
- refs.forEach((entry, index) => {
229
- map.set(entry.ref, `e${index + 1}`)
230
- })
231
- return map
232
- }
233
-
234
- // Roles that represent interactive elements
235
- const INTERACTIVE_ROLES = new Set([
236
- 'button',
237
- 'link',
238
- 'textbox',
239
- 'combobox',
240
- 'searchbox',
241
- 'checkbox',
242
- 'radio',
243
- 'slider',
244
- 'spinbutton',
245
- 'switch',
246
- 'menuitem',
247
- 'menuitemcheckbox',
248
- 'menuitemradio',
249
- 'option',
250
- 'tab',
251
- 'treeitem',
252
- 'img',
253
- 'video',
254
- 'audio',
255
- ])
256
-
257
- const LABEL_ROLES = new Set(['labeltext'])
258
-
259
- const MAX_LABEL_POSITION_CONCURRENCY = 24
260
- const BOX_MODEL_TIMEOUT_MS = 5000
261
-
262
- const CONTEXT_ROLES = new Set([
263
- 'navigation',
264
- 'main',
265
- 'contentinfo',
266
- 'banner',
267
- 'form',
268
- 'section',
269
- 'region',
270
- 'list',
271
- 'listitem',
272
- 'table',
273
- 'rowgroup',
274
- 'row',
275
- 'cell',
276
- ])
277
-
278
- const SKIP_WRAPPER_ROLES = new Set(['generic', 'group', 'none', 'presentation'])
279
-
280
- const TEST_ID_ATTRS = [
281
- 'data-testid',
282
- 'data-test-id',
283
- 'data-test',
284
- 'data-cy',
285
- 'data-pw',
286
- 'data-qa',
287
- 'data-e2e',
288
- 'data-automation-id',
289
- ]
290
-
291
- type DomNodeInfo = {
292
- nodeId: Protocol.DOM.NodeId
293
- parentId?: Protocol.DOM.NodeId
294
- backendNodeId: Protocol.DOM.BackendNodeId
295
- nodeName: string
296
- attributes: Map<string, string>
297
- }
298
-
299
- function toAttributeMap(attributes?: string[]): Map<string, string> {
300
- const result = new Map<string, string>()
301
- if (!attributes) {
302
- return result
303
- }
304
- for (let i = 0; i < attributes.length; i += 2) {
305
- const name = attributes[i]
306
- const value = attributes[i + 1]
307
- if (name) {
308
- result.set(name, value ?? '')
309
- }
310
- }
311
- return result
312
- }
313
-
314
- function getStableRefFromAttributes(attributes: Map<string, string>): { value: string; attr: string } | null {
315
- // Test IDs first: they are explicitly placed by developers for automation
316
- // and more stable than id which is often auto-generated by frameworks
317
- for (const attr of TEST_ID_ATTRS) {
318
- const value = attributes.get(attr)
319
- if (value) {
320
- return { value, attr }
321
- }
322
- }
323
- const id = attributes.get('id')
324
- if (id) {
325
- return { value: id, attr: 'id' }
326
- }
327
- return null
328
- }
329
-
330
- function escapeLocatorValue(value: string): string {
331
- return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
332
- }
333
-
334
- function buildLocatorFromStable(stable: { value: string; attr: string }): string {
335
- const escaped = escapeLocatorValue(stable.value)
336
- return `[${stable.attr}="${escaped}"]`
337
- }
338
-
339
- function buildBaseLocator({
340
- role,
341
- name,
342
- stable,
343
- isPromotedContentEditable,
344
- }: {
345
- role: string
346
- name: string
347
- stable: { value: string; attr: string } | null
348
- isPromotedContentEditable?: boolean
349
- }): string {
350
- if (stable) {
351
- return buildLocatorFromStable(stable)
352
- }
353
- // For promoted contenteditable elements (bare <div contenteditable="true"> without
354
- // role="textbox"), use CSS attribute selector. Playwright's role=textbox selector
355
- // won't match these because Chrome doesn't assign an implicit textbox role to
356
- // contenteditable divs. Elements that already had role="textbox" use the normal
357
- // role-based locator since Playwright matches those correctly.
358
- if (isPromotedContentEditable) {
359
- return `[contenteditable="true"]`
360
- }
361
- const trimmedName = name.trim()
362
- if (trimmedName.length > 0) {
363
- const escapedName = escapeLocatorValue(trimmedName)
364
- return `role=${role}[name="${escapedName}"]`
365
- }
366
- return `role=${role}`
367
- }
368
-
369
- function getAxValueString(value?: Protocol.Accessibility.AXValue): string {
370
- if (!value) {
371
- return ''
372
- }
373
- const raw = value.value
374
- if (typeof raw === 'string') {
375
- return raw
376
- }
377
- if (raw === undefined || raw === null) {
378
- return ''
379
- }
380
- return String(raw)
381
- }
382
-
383
- function getAxRole(node: Protocol.Accessibility.AXNode): string {
384
- const role = getAxValueString(node.role)
385
- return role.toLowerCase()
386
- }
387
-
388
- export type SnapshotLine = {
389
- text: string
390
- baseLocator?: string
391
- hasChildren?: boolean
392
- role?: string
393
- name?: string
394
- indent?: number
395
- }
396
-
397
- export type SnapshotNode = {
398
- role: string
399
- name: string
400
- baseLocator?: string
401
- ref?: string
402
- backendNodeId?: Protocol.DOM.BackendNodeId
403
- indentOffset?: number
404
- ignored?: boolean
405
- children: SnapshotNode[]
406
- }
407
-
408
- function buildSnapshotLine({
409
- role,
410
- name,
411
- baseLocator,
412
- indent,
413
- hasChildren,
414
- }: {
415
- role: string
416
- name: string
417
- baseLocator?: string
418
- indent: number
419
- hasChildren: boolean
420
- }): SnapshotLine {
421
- const prefix = ' '.repeat(indent)
422
- let text = `${prefix}- ${role}`
423
- if (name) {
424
- const escapedName = name.replace(/"/g, '\\"')
425
- text += ` "${escapedName}"`
426
- }
427
- return { text, baseLocator, hasChildren, role, name, indent }
428
- }
429
-
430
- function buildTextLine(text: string, indent: number): SnapshotLine {
431
- const prefix = ' '.repeat(indent)
432
- const escaped = text.replace(/"/g, '\\"')
433
- return { text: `${prefix}- text: "${escaped}"` }
434
- }
435
-
436
- export function buildSnapshotLines(nodes: SnapshotNode[], indent = 0): SnapshotLine[] {
437
- return nodes.flatMap((node) => {
438
- const nodeIndent = indent + (node.indentOffset ?? 0)
439
- const line =
440
- node.role === 'text'
441
- ? buildTextLine(node.name, nodeIndent)
442
- : buildSnapshotLine({
443
- role: node.role,
444
- name: node.name,
445
- baseLocator: node.baseLocator,
446
- indent: nodeIndent,
447
- hasChildren: node.children.length > 0,
448
- })
449
- return [line, ...buildSnapshotLines(node.children, nodeIndent + 1)]
450
- })
451
- }
452
-
453
- function shiftIndent(nodes: SnapshotNode[], offset: number): SnapshotNode[] {
454
- return nodes.map((node) => {
455
- return { ...node, indentOffset: (node.indentOffset ?? 0) + offset }
456
- })
457
- }
458
-
459
- export function buildRawSnapshotTree(options: {
460
- nodeId: Protocol.Accessibility.AXNodeId
461
- axById: Map<Protocol.Accessibility.AXNodeId, Protocol.Accessibility.AXNode>
462
- isNodeInScope: (node: Protocol.Accessibility.AXNode) => boolean
463
- }): SnapshotNode | null {
464
- const node = options.axById.get(options.nodeId)
465
- if (!node) {
466
- return null
467
- }
468
-
469
- const role = getAxRole(node)
470
- const name = getAxValueString(node.name).trim()
471
- const children = (node.childIds ?? [])
472
- .map((childId) => {
473
- return buildRawSnapshotTree({
474
- nodeId: childId,
475
- axById: options.axById,
476
- isNodeInScope: options.isNodeInScope,
477
- })
478
- })
479
- .filter(isTruthy)
480
-
481
- const inScope = options.isNodeInScope(node) || children.length > 0
482
- if (!inScope) {
483
- return null
484
- }
485
-
486
- return {
487
- role,
488
- name,
489
- backendNodeId: node.backendDOMNodeId,
490
- ignored: node.ignored,
491
- children,
492
- }
493
- }
494
-
495
- export function filterInteractiveSnapshotTree(options: {
496
- node: SnapshotNode
497
- ancestorNames: string[]
498
- labelContext: boolean
499
- refFilter?: (entry: { role: string; name: string }) => boolean
500
- domByBackendId: Map<Protocol.DOM.BackendNodeId, DomNodeInfo>
501
- promotedContentEditableIds?: Set<Protocol.DOM.BackendNodeId>
502
- createRefForNode: (options: {
503
- backendNodeId?: Protocol.DOM.BackendNodeId
504
- role: string
505
- name: string
506
- }) => string | null
507
- }): { nodes: SnapshotNode[]; names: Set<string> } {
508
- const role = options.node.role
509
- const name = options.node.name
510
- const hasName = name.length > 0
511
- const nextAncestors = hasName ? [...options.ancestorNames, name] : options.ancestorNames
512
-
513
- const isLabel = LABEL_ROLES.has(role)
514
- const nextLabelContext = options.labelContext || isLabel
515
-
516
- const childResults = options.node.children.map((child) => {
517
- return filterInteractiveSnapshotTree({
518
- node: child,
519
- ancestorNames: nextAncestors,
520
- labelContext: nextLabelContext,
521
- refFilter: options.refFilter,
522
- domByBackendId: options.domByBackendId,
523
- promotedContentEditableIds: options.promotedContentEditableIds,
524
- createRefForNode: options.createRefForNode,
525
- })
526
- })
527
- const childNodes = childResults.flatMap((result) => {
528
- return result.nodes
529
- })
530
- const childNames = childResults.reduce((acc, result) => {
531
- result.names.forEach((childName) => {
532
- acc.add(childName)
533
- })
534
- return acc
535
- }, new Set<string>())
536
-
537
- if (options.node.ignored) {
538
- return { nodes: shiftIndent(childNodes, 1), names: childNames }
539
- }
540
-
541
- if (isTextRole(role)) {
542
- if (!hasName) {
543
- return { nodes: childNodes, names: childNames }
544
- }
545
- if (!options.labelContext) {
546
- return { nodes: childNodes, names: childNames }
547
- }
548
- const isRedundantText = options.ancestorNames.some((ancestor) => {
549
- return ancestor.includes(name) || name.includes(ancestor)
550
- })
551
- if (isRedundantText) {
552
- return { nodes: childNodes, names: childNames }
553
- }
554
- const names = new Set(childNames)
555
- names.add(name)
556
- const textNode: SnapshotNode = { role: 'text', name, children: [] }
557
- return { nodes: [textNode], names }
558
- }
559
-
560
- const hasChildren = childNodes.length > 0
561
- const nameToUse = hasName && (childNames.has(name) || isSubstringOfAny(name, childNames)) ? '' : name
562
- const hasNameToUse = nameToUse.length > 0
563
- const isWrapper = SKIP_WRAPPER_ROLES.has(role)
564
- const isInteractive = INTERACTIVE_ROLES.has(role)
565
- const isContext = CONTEXT_ROLES.has(role)
566
- const passesRefFilter = !options.refFilter || options.refFilter({ role, name })
567
- const includeInteractive = isInteractive && passesRefFilter
568
- const shouldInclude = includeInteractive || isLabel || isContext || hasChildren
569
- if (!shouldInclude) {
570
- return { nodes: childNodes, names: childNames }
571
- }
572
-
573
- if (!includeInteractive && !isLabel && !isContext) {
574
- if (!hasChildren) {
575
- return { nodes: [], names: childNames }
576
- }
577
- return { nodes: childNodes, names: childNames }
578
- }
579
-
580
- if (isWrapper && !hasNameToUse) {
581
- if (!hasChildren) {
582
- return { nodes: [], names: childNames }
583
- }
584
- return { nodes: childNodes, names: childNames }
585
- }
586
-
587
- let baseLocator: string | undefined
588
- let ref: string | null = null
589
- if (includeInteractive) {
590
- const domInfo = options.node.backendNodeId ? options.domByBackendId.get(options.node.backendNodeId) : undefined
591
- const stable = domInfo ? getStableRefFromAttributes(domInfo.attributes) : null
592
- const isPromoted = options.node.backendNodeId != null && (options.promotedContentEditableIds?.has(options.node.backendNodeId) ?? false)
593
- baseLocator = buildBaseLocator({ role, name, stable, isPromotedContentEditable: isPromoted })
594
- ref = options.createRefForNode({ backendNodeId: options.node.backendNodeId, role, name })
595
- }
596
-
597
- const nodeEntry: SnapshotNode = {
598
- role,
599
- name: nameToUse,
600
- baseLocator,
601
- ref: ref ?? undefined,
602
- backendNodeId: options.node.backendNodeId,
603
- children: childNodes,
604
- }
605
- const names = new Set(childNames)
606
- if (hasNameToUse) {
607
- names.add(nameToUse)
608
- }
609
- return { nodes: [nodeEntry], names }
610
- }
611
-
612
- export function filterFullSnapshotTree(options: {
613
- node: SnapshotNode
614
- ancestorNames: string[]
615
- refFilter?: (entry: { role: string; name: string }) => boolean
616
- domByBackendId: Map<Protocol.DOM.BackendNodeId, DomNodeInfo>
617
- promotedContentEditableIds?: Set<Protocol.DOM.BackendNodeId>
618
- createRefForNode: (options: {
619
- backendNodeId?: Protocol.DOM.BackendNodeId
620
- role: string
621
- name: string
622
- }) => string | null
623
- }): { nodes: SnapshotNode[]; names: Set<string> } {
624
- const role = options.node.role
625
- const name = options.node.name
626
- const hasName = name.length > 0
627
- const nextAncestors = hasName ? [...options.ancestorNames, name] : options.ancestorNames
628
-
629
- const childResults = options.node.children.map((child) => {
630
- return filterFullSnapshotTree({
631
- node: child,
632
- ancestorNames: nextAncestors,
633
- refFilter: options.refFilter,
634
- domByBackendId: options.domByBackendId,
635
- promotedContentEditableIds: options.promotedContentEditableIds,
636
- createRefForNode: options.createRefForNode,
637
- })
638
- })
639
- const childNodes = childResults.flatMap((result) => {
640
- return result.nodes
641
- })
642
- const childNames = childResults.reduce((acc, result) => {
643
- result.names.forEach((childName) => {
644
- acc.add(childName)
645
- })
646
- return acc
647
- }, new Set<string>())
648
-
649
- if (options.node.ignored) {
650
- return { nodes: shiftIndent(childNodes, 1), names: childNames }
651
- }
652
-
653
- if (isTextRole(role)) {
654
- if (!hasName) {
655
- return { nodes: childNodes, names: childNames }
656
- }
657
- const isRedundantText = options.ancestorNames.some((ancestor) => {
658
- return ancestor.includes(name) || name.includes(ancestor)
659
- })
660
- if (isRedundantText) {
661
- return { nodes: childNodes, names: childNames }
662
- }
663
- const names = new Set(childNames)
664
- names.add(name)
665
- const textNode: SnapshotNode = { role: 'text', name, children: [] }
666
- return { nodes: [textNode], names }
667
- }
668
-
669
- const hasChildren = childNodes.length > 0
670
- const nameToUse = hasName && (childNames.has(name) || isSubstringOfAny(name, childNames)) ? '' : name
671
- const hasNameToUse = nameToUse.length > 0
672
- const isWrapper = SKIP_WRAPPER_ROLES.has(role)
673
- const isInteractive = INTERACTIVE_ROLES.has(role)
674
- const passesRefFilter = !options.refFilter || options.refFilter({ role, name })
675
- const includeInteractive = isInteractive && passesRefFilter
676
- const shouldInclude = includeInteractive || hasNameToUse || hasChildren
677
- if (!shouldInclude) {
678
- return { nodes: childNodes, names: childNames }
679
- }
680
-
681
- if (isWrapper && !hasNameToUse) {
682
- if (!hasChildren) {
683
- return { nodes: [], names: childNames }
684
- }
685
- return { nodes: childNodes, names: childNames }
686
- }
687
-
688
- let baseLocator: string | undefined
689
- let ref: string | null = null
690
- if (includeInteractive) {
691
- const domInfo = options.node.backendNodeId ? options.domByBackendId.get(options.node.backendNodeId) : undefined
692
- const stable = domInfo ? getStableRefFromAttributes(domInfo.attributes) : null
693
- const isPromoted = options.node.backendNodeId != null && (options.promotedContentEditableIds?.has(options.node.backendNodeId) ?? false)
694
- baseLocator = buildBaseLocator({ role, name, stable, isPromotedContentEditable: isPromoted })
695
- ref = options.createRefForNode({ backendNodeId: options.node.backendNodeId, role, name })
696
- }
697
-
698
- const nodeEntry: SnapshotNode = {
699
- role,
700
- name: nameToUse,
701
- baseLocator,
702
- ref: ref ?? undefined,
703
- backendNodeId: options.node.backendNodeId,
704
- children: childNodes,
705
- }
706
- const names = new Set(childNames)
707
- if (hasNameToUse) {
708
- names.add(nameToUse)
709
- }
710
- return { nodes: [nodeEntry], names }
711
- }
712
-
713
- function buildLocatorLineText({ line, locator }: { line: SnapshotLine; locator: string }): string {
714
- const prefix = ' '.repeat(line.indent ?? 0)
715
- const role = line.role ?? ''
716
- const name = line.name ?? ''
717
- const escapedName = name.replace(/"/g, '\\"')
718
-
719
- const hasRoleInLocator = role ? locator.includes(role) : false
720
- const hasNameInLocator = name ? locator.includes(escapedName) : false
721
-
722
- const parts: string[] = []
723
- if (role && !hasRoleInLocator) {
724
- parts.push(role)
725
- }
726
- if (name && !hasNameInLocator) {
727
- parts.push(`"${escapedName}"`)
728
- }
729
-
730
- const base = parts.length > 0 ? `${prefix}- ${parts.join(' ')}` : `${prefix}-`
731
- return `${base} ${locator}`
732
- }
733
-
734
- export function finalizeSnapshotOutput(
735
- lines: SnapshotLine[],
736
- nodes: SnapshotNode[],
737
- shortRefMap: Map<string, string>,
738
- ): { snapshot: string; tree: AriaSnapshotNode[] } {
739
- const locatorCounts = lines.reduce<Map<string, number>>((acc, line) => {
740
- if (!line.baseLocator) {
741
- return acc
742
- }
743
- acc.set(line.baseLocator, (acc.get(line.baseLocator) ?? 0) + 1)
744
- return acc
745
- }, new Map<string, number>())
746
-
747
- const locatorIndices = new Map<string, number>()
748
- const locatorSequence = lines.reduce<string[]>((acc, line) => {
749
- if (!line.baseLocator) {
750
- return acc
751
- }
752
- const count = locatorCounts.get(line.baseLocator) ?? 0
753
- const index = locatorIndices.get(line.baseLocator) ?? 0
754
- locatorIndices.set(line.baseLocator, index + 1)
755
- const locator = count > 1 ? `${line.baseLocator} >> nth=${index}` : line.baseLocator
756
- acc.push(locator)
757
- return acc
758
- }, [])
759
-
760
- let lineLocatorIndex = 0
761
- const snapshot = lines
762
- .map((line) => {
763
- let text = line.text
764
- if (line.baseLocator) {
765
- const locator = locatorSequence[lineLocatorIndex]
766
- lineLocatorIndex += 1
767
- text = buildLocatorLineText({ line, locator })
768
- }
769
- if (line.hasChildren) {
770
- text += ':'
771
- }
772
- return text
773
- })
774
- .join('\n')
775
-
776
- let nodeLocatorIndex = 0
777
- const applyLocators = (items: SnapshotNode[]): AriaSnapshotNode[] => {
778
- return items.map((item) => {
779
- const locator = item.baseLocator ? locatorSequence[nodeLocatorIndex++] : undefined
780
- const children = applyLocators(item.children)
781
- return {
782
- role: item.role,
783
- name: item.name,
784
- locator,
785
- ref: item.ref,
786
- shortRef: item.ref ? (shortRefMap.get(item.ref) ?? item.ref) : undefined,
787
- backendNodeId: item.backendNodeId,
788
- children,
789
- }
790
- })
791
- }
792
-
793
- return { snapshot, tree: applyLocators(nodes) }
794
- }
795
-
796
- function buildDomIndex(nodes: Protocol.DOM.Node[]): {
797
- domById: Map<Protocol.DOM.NodeId, DomNodeInfo>
798
- domByBackendId: Map<Protocol.DOM.BackendNodeId, DomNodeInfo>
799
- childrenByParent: Map<Protocol.DOM.NodeId, Protocol.DOM.NodeId[]>
800
- } {
801
- const domById = new Map<Protocol.DOM.NodeId, DomNodeInfo>()
802
- const domByBackendId = new Map<Protocol.DOM.BackendNodeId, DomNodeInfo>()
803
- const childrenByParent = new Map<Protocol.DOM.NodeId, Protocol.DOM.NodeId[]>()
804
-
805
- for (const node of nodes) {
806
- const info: DomNodeInfo = {
807
- nodeId: node.nodeId,
808
- parentId: node.parentId,
809
- backendNodeId: node.backendNodeId,
810
- nodeName: node.nodeName,
811
- attributes: toAttributeMap(node.attributes),
812
- }
813
- domById.set(node.nodeId, info)
814
- domByBackendId.set(node.backendNodeId, info)
815
- if (node.parentId) {
816
- if (!childrenByParent.has(node.parentId)) {
817
- childrenByParent.set(node.parentId, [])
818
- }
819
- childrenByParent.get(node.parentId)!.push(node.nodeId)
820
- }
821
- }
822
-
823
- return { domById, domByBackendId, childrenByParent }
824
- }
825
-
826
- function findScopeRootNodeId(
827
- nodes: Protocol.DOM.Node[],
828
- attrName: string,
829
- attrValue: string,
830
- ): Protocol.DOM.NodeId | null {
831
- for (const node of nodes) {
832
- if (!node.attributes) {
833
- continue
834
- }
835
- for (let i = 0; i < node.attributes.length; i += 2) {
836
- const name = node.attributes[i]
837
- const value = node.attributes[i + 1]
838
- if (name === attrName && value === attrValue) {
839
- return node.nodeId
840
- }
841
- }
842
- }
843
- return null
844
- }
845
-
846
- function buildBackendIdSet(
847
- rootNodeId: Protocol.DOM.NodeId,
848
- childrenByParent: Map<Protocol.DOM.NodeId, Protocol.DOM.NodeId[]>,
849
- domById: Map<Protocol.DOM.NodeId, DomNodeInfo>,
850
- ): Set<Protocol.DOM.BackendNodeId> {
851
- const result = new Set<Protocol.DOM.BackendNodeId>()
852
- const stack: Protocol.DOM.NodeId[] = [rootNodeId]
853
- while (stack.length > 0) {
854
- const current = stack.pop()
855
- if (current === undefined) {
856
- continue
857
- }
858
- const node = domById.get(current)
859
- if (node) {
860
- result.add(node.backendNodeId)
861
- }
862
- const children = childrenByParent.get(current)
863
- if (children && children.length > 0) {
864
- stack.push(...children)
865
- }
866
- }
867
- return result
868
- }
869
-
870
- function isTextRole(role: string): boolean {
871
- return role === 'statictext' || role === 'inlinetextbox'
872
- }
873
-
874
- // Per HTML spec, contenteditable is editable when the attribute is present
875
- // with value "true", "" (empty string), or "plaintext-only". Bare attribute
876
- // (no value) is also treated as empty string by the browser DOM parser.
877
- function isContentEditable(value: string | undefined | null): boolean {
878
- if (value == null) {
879
- return false
880
- }
881
- const v = value.trim().toLowerCase()
882
- return v === '' || v === 'true' || v === 'plaintext-only'
883
- }
884
-
885
- function isSubstringOfAny(needle: string, haystack: Set<string>): boolean {
886
- for (const str of haystack) {
887
- if (str.includes(needle)) {
888
- return true
889
- }
890
- }
891
- return false
892
- }
893
-
894
- // ============================================================================
895
- // Frame Resolution
896
- // ============================================================================
897
-
898
- /**
899
- * Resolve a FrameLocator to an actual Frame object. FrameLocator (returned by
900
- * locator.contentFrame()) is a scoping helper that doesn't have CDP methods like
901
- * frameId(). We need the real Frame from page.frames() for OOPIF session attachment.
902
- *
903
- * If a Frame is passed directly, it's returned as-is.
904
- * If a FrameLocator is passed, we find the matching Frame by locating the iframe
905
- * element and matching its src URL against page.frames().
906
- */
907
- async function resolveFrame({ frame, page }: { frame?: Frame | FrameLocator; page: Page }): Promise<Frame | undefined> {
908
- if (!frame) {
909
- return undefined
910
- }
911
- // Frame has frameId(), FrameLocator does not
912
- if (typeof (frame as Frame).frameId === 'function') {
913
- return frame as Frame
914
- }
915
- // It's a FrameLocator — resolve to a Frame via the owner iframe element.
916
- // Use elementHandle().contentFrame() which returns the actual Frame object
917
- // directly from the <iframe> element, avoiding ambiguity when multiple
918
- // frames share the same origin (e.g. framer plugin iframes).
919
- const frameLocator = frame as FrameLocator
920
- const owner = frameLocator.owner()
921
- const handle = await owner.elementHandle()
922
- if (!handle) {
923
- throw new Error('Could not resolve FrameLocator to a Frame: iframe element not found')
924
- }
925
- const resolved = await handle.contentFrame()
926
- if (!resolved) {
927
- throw new Error('Could not resolve FrameLocator to a Frame: contentFrame() returned null')
928
- }
929
- return resolved
930
- }
931
-
932
- // ============================================================================
933
- // Main Functions
934
- // ============================================================================
935
-
936
- /**
937
- * Get an accessibility snapshot with utilities to look up refs for elements.
938
- * Uses the browser accessibility tree (CDP) and maps nodes to DOM attributes.
939
- *
940
- * Refs are generated from stable test IDs when available (data-testid, data-test-id, etc.)
941
- * or fall back to e1, e2, e3...
942
- *
943
- * @param page - Playwright page
944
- * @param locator - Optional locator to scope the snapshot to a subtree
945
- * @param refFilter - Optional filter for which elements get refs
946
- *
947
- * @example
948
- * ```ts
949
- * const { snapshot, getSelectorForRef } = await getAriaSnapshot({ page })
950
- * // Snapshot shows locators like [id="submit-btn"] or role=button[name="Submit"]
951
- * const selector = getSelectorForRef('submit-btn')
952
- * await page.locator(selector).click()
953
- * ```
954
- */
955
- export async function getAriaSnapshot({
956
- page,
957
- frame,
958
- locator,
959
- refFilter,
960
- interactiveOnly = false,
961
- cdp,
962
- }: {
963
- page: Page
964
- frame?: Frame | FrameLocator
965
- locator?: Locator
966
- refFilter?: (info: { role: string; name: string }) => boolean
967
- interactiveOnly?: boolean
968
- cdp?: ICDPSession
969
- }): Promise<AriaSnapshotResult> {
970
- const session = cdp || (await getCDPSessionForPage({ page }))
971
-
972
- // Resolve FrameLocator to an actual Frame. FrameLocator (from locator.contentFrame())
973
- // is a scoping helper without CDP access. We need the real Frame from page.frames()
974
- // which has frameId() for OOPIF session attachment.
975
- const resolvedFrame = await resolveFrame({ frame, page })
976
-
977
- // For cross-origin iframes (OOPIFs), we need to attach to the iframe's target
978
- // to get a separate CDP session. Same-origin iframes can use frameId directly.
979
- let oopifSessionId: string | null = null
980
- const frameId = resolvedFrame?.frameId() ?? null
981
-
982
- if (frameId) {
983
- const { targetInfos } = (await session.send('Target.getTargets')) as Protocol.Target.GetTargetsResponse
984
- const frameUrl = resolvedFrame!.url()
985
- const iframeTarget = targetInfos.find((t) => {
986
- return t.type === 'iframe' && t.url === frameUrl
987
- })
988
- if (iframeTarget) {
989
- const { sessionId } = (await session.send('Target.attachToTarget', {
990
- targetId: iframeTarget.targetId,
991
- flatten: true,
992
- })) as Protocol.Target.AttachToTargetResponse
993
- oopifSessionId = sessionId
994
- await session.send('Runtime.runIfWaitingForDebugger', undefined, oopifSessionId)
995
- }
996
- }
997
-
998
- await session.send('DOM.enable', undefined, oopifSessionId)
999
- await session.send('Accessibility.enable', undefined, oopifSessionId)
1000
- const scopeAttr = 'data-pw-scope'
1001
- const scopeValue = crypto.randomUUID()
1002
- let scopeApplied = false
1003
- const scopeLocator = locator
1004
-
1005
- try {
1006
- if (scopeLocator) {
1007
- await scopeLocator.evaluate(
1008
- (element, data) => {
1009
- element.setAttribute(data.attr, data.value)
1010
- },
1011
- { attr: scopeAttr, value: scopeValue },
1012
- )
1013
- scopeApplied = true
1014
- }
1015
-
1016
- const { nodes: domNodes } = (await session.send(
1017
- 'DOM.getFlattenedDocument',
1018
- { depth: -1, pierce: true },
1019
- oopifSessionId,
1020
- )) as Protocol.DOM.GetFlattenedDocumentResponse
1021
- const { domById, domByBackendId, childrenByParent } = buildDomIndex(domNodes)
1022
-
1023
- let scopeRootNodeId: Protocol.DOM.NodeId | null = null
1024
- let scopeRootBackendId: Protocol.DOM.BackendNodeId | null = null
1025
- if (scopeLocator) {
1026
- scopeRootNodeId = findScopeRootNodeId(domNodes, scopeAttr, scopeValue)
1027
- if (scopeRootNodeId) {
1028
- const scopeNode = domById.get(scopeRootNodeId)
1029
- if (scopeNode) {
1030
- scopeRootBackendId = scopeNode.backendNodeId
1031
- }
1032
- }
1033
- }
1034
-
1035
- const allowedBackendIds = scopeRootNodeId ? buildBackendIdSet(scopeRootNodeId, childrenByParent, domById) : null
1036
-
1037
- const axParams = !oopifSessionId && frameId ? { frameId } : undefined
1038
- const { nodes: axNodes } = (await session.send(
1039
- 'Accessibility.getFullAXTree',
1040
- axParams,
1041
- oopifSessionId,
1042
- )) as Protocol.Accessibility.GetFullAXTreeResponse
1043
-
1044
- const axById = new Map<Protocol.Accessibility.AXNodeId, Protocol.Accessibility.AXNode>()
1045
- for (const node of axNodes) {
1046
- axById.set(node.nodeId, node)
1047
- }
1048
-
1049
- // Index AX nodes by backendDOMNodeId for O(1) lookups during promotion
1050
- // and root finding (instead of repeated O(n) axNodes.find() calls)
1051
- const axByBackendId = new Map<Protocol.DOM.BackendNodeId, Protocol.Accessibility.AXNode>()
1052
- for (const node of axNodes) {
1053
- if (node.backendDOMNodeId) {
1054
- axByBackendId.set(node.backendDOMNodeId, node)
1055
- }
1056
- }
1057
-
1058
- // Promote contenteditable elements that Chrome's AX tree doesn't classify as
1059
- // interactive. Rich text editors (ProseMirror, Tiptap, Slate, Lexical, etc.) use
1060
- // bare <div contenteditable="true"> without role="textbox", so Chrome reports
1061
- // them as "generic" and they become invisible in the snapshot. We detect these
1062
- // via the DOM tree and override the AX role to "textbox" so they appear as
1063
- // interactive elements the AI can target.
1064
- const promotedContentEditableIds = new Set<Protocol.DOM.BackendNodeId>()
1065
- for (const [, domInfo] of domByBackendId) {
1066
- if (!isContentEditable(domInfo.attributes.get('contenteditable'))) {
1067
- continue
1068
- }
1069
- const axNode = axByBackendId.get(domInfo.backendNodeId)
1070
- if (!axNode) {
1071
- continue
1072
- }
1073
- const currentRole = getAxRole(axNode)
1074
- if (INTERACTIVE_ROLES.has(currentRole)) {
1075
- continue
1076
- }
1077
- axNode.role = { type: 'role', value: 'textbox' }
1078
- promotedContentEditableIds.add(domInfo.backendNodeId)
1079
- }
1080
-
1081
- const findRootAxNodeId = (): Protocol.Accessibility.AXNodeId | null => {
1082
- if (scopeRootBackendId) {
1083
- const scoped = axByBackendId.get(scopeRootBackendId)
1084
- if (scoped) {
1085
- return scoped.nodeId
1086
- }
1087
- }
1088
- const rootWebArea = axNodes.find((node) => {
1089
- return getAxRole(node) === 'rootwebarea'
1090
- })
1091
- if (rootWebArea) {
1092
- return rootWebArea.nodeId
1093
- }
1094
- const webArea = axNodes.find((node) => {
1095
- return getAxRole(node) === 'webarea'
1096
- })
1097
- if (webArea) {
1098
- return webArea.nodeId
1099
- }
1100
- const topLevel = axNodes.find((node) => {
1101
- return !node.parentId
1102
- })
1103
- return topLevel ? topLevel.nodeId : null
1104
- }
1105
-
1106
- const rootAxNodeId = findRootAxNodeId()
1107
-
1108
- const refCounts = new Map<string, number>()
1109
- let fallbackCounter = 0
1110
- const refs: AriaRefDraft[] = []
1111
-
1112
- const createRefForNode = (options: {
1113
- backendNodeId?: Protocol.DOM.BackendNodeId
1114
- role: string
1115
- name: string
1116
- }): string | null => {
1117
- if (!INTERACTIVE_ROLES.has(options.role)) {
1118
- return null
1119
- }
1120
-
1121
- const domInfo = options.backendNodeId ? domByBackendId.get(options.backendNodeId) : undefined
1122
- const stable = domInfo ? getStableRefFromAttributes(domInfo.attributes) : null
1123
- let baseRef = stable?.value
1124
- if (!baseRef) {
1125
- fallbackCounter += 1
1126
- baseRef = `e${fallbackCounter}`
1127
- }
1128
-
1129
- const count = refCounts.get(baseRef) ?? 0
1130
- refCounts.set(baseRef, count + 1)
1131
- const ref = count === 0 ? baseRef : `${baseRef}-${count + 1}`
1132
-
1133
- let selector: string | undefined
1134
- if (stable && count === 0) {
1135
- selector = buildLocatorFromStable(stable)
1136
- }
1137
- // For promoted contenteditable elements without a stable selector, store
1138
- // [contenteditable="true"] so getSelectorForRef() doesn't fall back to
1139
- // role=textbox which Playwright can't match on bare contenteditable divs.
1140
- if (!selector && options.backendNodeId != null && promotedContentEditableIds.has(options.backendNodeId)) {
1141
- selector = '[contenteditable="true"]'
1142
- }
1143
-
1144
- refs.push({ ref, role: options.role, name: options.name, selector, backendNodeId: options.backendNodeId })
1145
- return ref
1146
- }
1147
-
1148
- const isNodeInScope = (node: Protocol.Accessibility.AXNode): boolean => {
1149
- if (!allowedBackendIds) {
1150
- return true
1151
- }
1152
- if (!node.backendDOMNodeId) {
1153
- return false
1154
- }
1155
- return allowedBackendIds.has(node.backendDOMNodeId)
1156
- }
1157
-
1158
- let snapshotNodes: SnapshotNode[] = []
1159
- if (rootAxNodeId) {
1160
- const rootNode = axById.get(rootAxNodeId)
1161
- const rootRole = rootNode ? getAxRole(rootNode) : ''
1162
- const rawRoots =
1163
- rootNode && (rootRole === 'rootwebarea' || rootRole === 'webarea') && rootNode.childIds
1164
- ? rootNode.childIds
1165
- .map((childId) => {
1166
- return buildRawSnapshotTree({ nodeId: childId, axById, isNodeInScope })
1167
- })
1168
- .filter(isTruthy)
1169
- : [buildRawSnapshotTree({ nodeId: rootAxNodeId, axById, isNodeInScope })].filter(isTruthy)
1170
-
1171
- const filtered = rawRoots.flatMap((rawNode) => {
1172
- if (interactiveOnly) {
1173
- return filterInteractiveSnapshotTree({
1174
- node: rawNode,
1175
- ancestorNames: [],
1176
- labelContext: false,
1177
- refFilter,
1178
- domByBackendId,
1179
- promotedContentEditableIds,
1180
- createRefForNode,
1181
- }).nodes
1182
- }
1183
- return filterFullSnapshotTree({
1184
- node: rawNode,
1185
- ancestorNames: [],
1186
- refFilter,
1187
- domByBackendId,
1188
- promotedContentEditableIds,
1189
- createRefForNode,
1190
- }).nodes
1191
- })
1192
- snapshotNodes = filtered
1193
- }
1194
-
1195
- const snapshotLines = buildSnapshotLines(snapshotNodes)
1196
-
1197
- const shortRefMap = buildShortRefMap({ refs })
1198
- const finalized = finalizeSnapshotOutput(snapshotLines, snapshotNodes, shortRefMap)
1199
- const refsWithShortRef: Array<AriaRef & { selector?: string }> = refs.map((entry) => {
1200
- return {
1201
- ...entry,
1202
- shortRef: shortRefMap.get(entry.ref) ?? entry.ref,
1203
- }
1204
- })
1205
- const result = { snapshot: finalized.snapshot, tree: finalized.tree, refs: refsWithShortRef }
1206
-
1207
- // Build refToElement map
1208
- const refToElement = new Map<string, { role: string; name: string; shortRef: string }>()
1209
- const refToSelector = new Map<string, string>()
1210
- for (const { ref, role, name, shortRef } of result.refs) {
1211
- if (!refFilter || refFilter({ role, name })) {
1212
- refToElement.set(ref, { role, name, shortRef })
1213
- }
1214
- }
1215
-
1216
- for (const { ref, selector } of result.refs) {
1217
- if (!selector) {
1218
- continue
1219
- }
1220
- refToSelector.set(ref, selector)
1221
- }
1222
-
1223
- const snapshot = result.snapshot
1224
-
1225
- const getSelectorForRef = (ref: string): string | null => {
1226
- const mapped = refToSelector.get(ref)
1227
- if (mapped) {
1228
- return mapped
1229
- }
1230
- const info = refToElement.get(ref)
1231
- if (!info) {
1232
- return null
1233
- }
1234
- const escapedName = info.name.replace(/"/g, '\\"')
1235
- return `role=${info.role}[name="${escapedName}"]`
1236
- }
1237
-
1238
- const getRefsForLocators = async (locators: Array<Locator | ElementHandle>): Promise<Array<AriaRef | null>> => {
1239
- if (locators.length === 0) {
1240
- return []
1241
- }
1242
-
1243
- const targetHandles = await Promise.all(
1244
- locators.map(async (loc) => {
1245
- try {
1246
- return 'elementHandle' in loc
1247
- ? await (loc as Locator).elementHandle({ timeout: 1000 })
1248
- : (loc as ElementHandle)
1249
- } catch {
1250
- return null
1251
- }
1252
- }),
1253
- )
1254
-
1255
- const matchingRefs = await page.evaluate(
1256
- ({ targets, refData }) => {
1257
- return targets.map((target) => {
1258
- if (!target) {
1259
- return null
1260
- }
1261
-
1262
- const testIdAttrs = [
1263
- 'data-testid',
1264
- 'data-test-id',
1265
- 'data-test',
1266
- 'data-cy',
1267
- 'data-pw',
1268
- 'data-qa',
1269
- 'data-e2e',
1270
- 'data-automation-id',
1271
- ]
1272
- for (const attr of testIdAttrs) {
1273
- const value = target.getAttribute(attr)
1274
- if (value) {
1275
- const match = refData.find((ref) => {
1276
- return ref.ref === value || ref.ref.startsWith(value)
1277
- })
1278
- if (match) {
1279
- return match.ref
1280
- }
1281
- }
1282
- }
1283
-
1284
- const id = target.getAttribute('id')
1285
- if (id) {
1286
- const match = refData.find((ref) => {
1287
- return ref.ref === id || ref.ref.startsWith(id)
1288
- })
1289
- if (match) {
1290
- return match.ref
1291
- }
1292
- }
1293
-
1294
- return null
1295
- })
1296
- },
1297
- {
1298
- targets: targetHandles,
1299
- refData: result.refs,
1300
- },
1301
- )
1302
-
1303
- return matchingRefs.map((ref) => {
1304
- if (!ref) {
1305
- return null
1306
- }
1307
- const info = refToElement.get(ref)
1308
- return info ? { ...info, ref } : null
1309
- })
1310
- }
1311
-
1312
- return {
1313
- snapshot,
1314
- tree: result.tree,
1315
- refs: result.refs,
1316
- refToElement,
1317
- refToSelector,
1318
- getSelectorForRef,
1319
- getRefsForLocators,
1320
- getRefForLocator: async (loc) => (await getRefsForLocators([loc]))[0],
1321
- getRefStringForLocator: async (loc) => (await getRefsForLocators([loc]))[0]?.ref ?? null,
1322
- }
1323
- } finally {
1324
- if (scopeApplied && scopeLocator) {
1325
- await scopeLocator.evaluate((element, attr) => {
1326
- element.removeAttribute(attr)
1327
- }, scopeAttr)
1328
- }
1329
- if (oopifSessionId) {
1330
- await session.send('Target.detachFromTarget', { sessionId: oopifSessionId }).catch((e) => {
1331
- console.error('[aria-snapshot] Failed to detach OOPIF session:', oopifSessionId, e)
1332
- })
1333
- }
1334
- if (!cdp) {
1335
- await session.detach()
1336
- }
1337
- }
1338
- }
1339
-
1340
- function buildBoxFromQuad(quad?: number[]): LabelBox | null {
1341
- if (!quad || quad.length < 8) {
1342
- return null
1343
- }
1344
- const xs = [quad[0], quad[2], quad[4], quad[6]]
1345
- const ys = [quad[1], quad[3], quad[5], quad[7]]
1346
- const left = Math.min(...xs)
1347
- const right = Math.max(...xs)
1348
- const top = Math.min(...ys)
1349
- const bottom = Math.max(...ys)
1350
- return {
1351
- x: left,
1352
- y: top,
1353
- width: Math.max(0, right - left),
1354
- height: Math.max(0, bottom - top),
1355
- }
1356
- }
1357
-
1358
- function isTruthy<T>(value: T): value is NonNullable<T> {
1359
- return Boolean(value)
1360
- }
1361
-
1362
- async function getLabelBoxesForRefs({
1363
- page,
1364
- refs,
1365
- maxConcurrency = MAX_LABEL_POSITION_CONCURRENCY,
1366
- logger,
1367
- cdp,
1368
- }: {
1369
- page: Page
1370
- refs: AriaRef[]
1371
- maxConcurrency?: number
1372
- logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }
1373
- cdp?: ICDPSession
1374
- }): Promise<AriaLabel[]> {
1375
- const log = logger?.info ?? logger?.error ?? console.error
1376
- const session = cdp || (await getCDPSessionForPage({ page }))
1377
- const sema = new Sema(maxConcurrency)
1378
- const labelRefs = refs.filter((ref) => {
1379
- return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role)
1380
- })
1381
-
1382
- log(`[getLabelBoxesForRefs] processing ${labelRefs.length} interactive refs (concurrency: ${maxConcurrency})`)
1383
- const startTime = Date.now()
1384
- let completed = 0
1385
- let timedOut = 0
1386
- let failed = 0
1387
-
1388
- try {
1389
- const labels = await Promise.all(
1390
- labelRefs.map(async (ref) => {
1391
- if (!ref.backendNodeId) {
1392
- return null
1393
- }
1394
- await sema.acquire()
1395
- try {
1396
- const response = await Promise.race([
1397
- session.send('DOM.getBoxModel', {
1398
- backendNodeId: ref.backendNodeId,
1399
- }) as Promise<Protocol.DOM.GetBoxModelResponse>,
1400
- new Promise<null>((resolve) => {
1401
- setTimeout(() => {
1402
- resolve(null)
1403
- }, BOX_MODEL_TIMEOUT_MS)
1404
- }),
1405
- ])
1406
- completed++
1407
- if (completed % 50 === 0 || completed === labelRefs.length) {
1408
- log(
1409
- `[getLabelBoxesForRefs] progress: ${completed}/${labelRefs.length} (${timedOut} timeouts, ${failed} errors) - ${Date.now() - startTime}ms`,
1410
- )
1411
- }
1412
- if (!response) {
1413
- timedOut++
1414
- return null
1415
- }
1416
- const box = buildBoxFromQuad(response.model.border)
1417
- if (!box) {
1418
- return null
1419
- }
1420
- return { ref: ref.ref, role: ref.role, box }
1421
- } catch (error) {
1422
- completed++
1423
- failed++
1424
- return null
1425
- } finally {
1426
- sema.release()
1427
- }
1428
- }),
1429
- )
1430
- log(
1431
- `[getLabelBoxesForRefs] done: ${completed} completed, ${timedOut} timeouts, ${failed} errors - ${Date.now() - startTime}ms`,
1432
- )
1433
- return labels.filter(isTruthy)
1434
- } finally {
1435
- if (!cdp) {
1436
- await session.detach()
1437
- }
1438
- }
1439
- }
1440
-
1441
- /**
1442
- * Show Vimium-style labels on interactive elements.
1443
- * Labels are colored badges positioned above each element showing the ref.
1444
- * Use with screenshots so agents can see which elements are interactive.
1445
- *
1446
- * Labels auto-hide after 30 seconds to prevent stale labels.
1447
- * Call this function again if the page HTML changes to get fresh labels.
1448
- *
1449
- * @param page - Playwright page
1450
- * @param locator - Optional locator to scope labels to a subtree
1451
- * @param interactiveOnly - Only show labels for interactive elements (default: true)
1452
- *
1453
- * @example
1454
- * ```ts
1455
- * const { snapshot, labelCount } = await showAriaRefLabels({ page })
1456
- * await page.screenshot({ path: '/tmp/screenshot.png' })
1457
- * // Agent sees [submit-btn] label on "Submit" button
1458
- * await page.locator('[data-testid="submit-btn"]').click()
1459
- * ```
1460
- */
1461
- export async function showAriaRefLabels({
1462
- page,
1463
- locator,
1464
- interactiveOnly = true,
1465
- logger,
1466
- }: {
1467
- page: Page
1468
- locator?: Locator
1469
- interactiveOnly?: boolean
1470
- logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }
1471
- }): Promise<{
1472
- snapshot: string
1473
- labelCount: number
1474
- }> {
1475
- const startTime = Date.now()
1476
- const log = logger?.info ?? logger?.error ?? console.error
1477
-
1478
- log(`[showAriaRefLabels] starting...`)
1479
- await ensureA11yClient(page)
1480
- log(`[showAriaRefLabels] ensureA11yClient: ${Date.now() - startTime}ms`)
1481
-
1482
- const cdpStart = Date.now()
1483
- const cdp = await getCDPSessionForPage({ page })
1484
- log(`[showAriaRefLabels] getCDPSessionForPage: ${Date.now() - cdpStart}ms`)
1485
-
1486
- try {
1487
- const snapshotStart = Date.now()
1488
- const { snapshot, refs } = await getAriaSnapshot({ page, locator, interactiveOnly, cdp })
1489
- const shortRefMap = new Map(
1490
- refs.map((entry) => {
1491
- return [entry.ref, entry.shortRef]
1492
- }),
1493
- )
1494
- const interactiveRefs = refs.filter((ref) => Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role))
1495
- log(
1496
- `[showAriaRefLabels] getAriaSnapshot: ${Date.now() - snapshotStart}ms (${refs.length} refs, ${interactiveRefs.length} interactive)`,
1497
- )
1498
-
1499
- const rootHandle = locator ? await locator.elementHandle() : null
1500
-
1501
- const labelsStart = Date.now()
1502
- const labels = await getLabelBoxesForRefs({ page, refs, logger, cdp })
1503
- const shortLabels = labels.map((label) => {
1504
- return {
1505
- ...label,
1506
- ref: shortRefMap.get(label.ref) ?? label.ref,
1507
- }
1508
- })
1509
- log(`[showAriaRefLabels] getLabelBoxesForRefs: ${Date.now() - labelsStart}ms (${labels.length} boxes)`)
1510
-
1511
- const renderStart = Date.now()
1512
- const labelCount = await page.evaluate(
1513
- ({ entries, root, interactiveOnly: intOnly }) => {
1514
- const a11y = (
1515
- globalThis as {
1516
- __a11y?: {
1517
- renderA11yLabels?: (labels: typeof entries) => number
1518
- computeA11ySnapshot?: (options: { root: unknown; interactiveOnly: boolean; renderLabels: boolean }) => {
1519
- labelCount: number
1520
- }
1521
- }
1522
- }
1523
- ).__a11y
1524
- if (a11y?.renderA11yLabels) {
1525
- return a11y.renderA11yLabels(entries)
1526
- }
1527
- if (a11y?.computeA11ySnapshot) {
1528
- const rootElement = root || document.body
1529
- return a11y.computeA11ySnapshot({ root: rootElement, interactiveOnly: intOnly, renderLabels: true })
1530
- .labelCount
1531
- }
1532
- throw new Error('a11y client not loaded')
1533
- },
1534
- { entries: shortLabels, root: rootHandle, interactiveOnly },
1535
- )
1536
-
1537
- log(`[showAriaRefLabels] renderA11yLabels: ${Date.now() - renderStart}ms (${labelCount} labels)`)
1538
- log(`[showAriaRefLabels] total: ${Date.now() - startTime}ms`)
1539
-
1540
- return { snapshot, labelCount }
1541
- } finally {
1542
- await cdp.detach()
1543
- }
1544
- }
1545
-
1546
- /**
1547
- * Remove all aria ref labels from the page.
1548
- */
1549
- export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void> {
1550
- await page.evaluate(() => {
1551
- const a11y = (globalThis as any).__a11y
1552
- if (a11y) {
1553
- a11y.hideA11yLabels()
1554
- } else {
1555
- // Fallback if client not loaded
1556
- const doc = document
1557
- const win = window as any
1558
- const timerKey = '__tabwright_labels_timer__'
1559
- if (win[timerKey]) {
1560
- win.clearTimeout(win[timerKey])
1561
- win[timerKey] = null
1562
- }
1563
- doc.getElementById('__tabwright_labels__')?.remove()
1564
- }
1565
- })
1566
- }
1567
-
1568
- /**
1569
- * Take a screenshot with accessibility labels overlaid on interactive elements.
1570
- * Shows Vimium-style labels, captures the screenshot, then removes the labels.
1571
- * The screenshot is automatically included in the MCP response.
1572
- *
1573
- * @param page - Playwright page
1574
- * @param locator - Optional locator to scope labels to a subtree
1575
- * @param collector - Array to collect screenshots (passed by MCP execute tool)
1576
- *
1577
- * @example
1578
- * ```ts
1579
- * await screenshotWithAccessibilityLabels({ page })
1580
- * // Screenshot is automatically included in the MCP response
1581
- * // Use ref from the snapshot to interact with elements
1582
- * await page.locator('[data-testid="submit-btn"]').click()
1583
- * ```
1584
- */
1585
- export async function screenshotWithAccessibilityLabels({
1586
- page,
1587
- locator,
1588
- interactiveOnly = true,
1589
- collector,
1590
- logger,
1591
- }: {
1592
- page: Page
1593
- locator?: Locator
1594
- interactiveOnly?: boolean
1595
- collector: ScreenshotResult[]
1596
- logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }
1597
- }): Promise<void> {
1598
- const log = logger?.info ?? logger?.error
1599
- const showLabelsStart = Date.now()
1600
- const { snapshot, labelCount } = await showAriaRefLabels({ page, locator, interactiveOnly, logger })
1601
- if (log) {
1602
- log(`showAriaRefLabels: ${Date.now() - showLabelsStart}ms`)
1603
- }
1604
-
1605
- // Generate unique filename with timestamp
1606
- const timestamp = Date.now()
1607
- const random = Math.random().toString(36).slice(2, 6)
1608
- const filename = `tabwright-screenshot-${timestamp}-${random}.png`
1609
-
1610
- // Use ./tmp folder (gitignored) instead of system temp
1611
- const tmpDir = path.join(process.cwd(), 'tmp')
1612
- if (!fs.existsSync(tmpDir)) {
1613
- fs.mkdirSync(tmpDir, { recursive: true })
1614
- }
1615
- const screenshotPath = path.join(tmpDir, filename)
1616
-
1617
- // Get viewport size to clip screenshot to visible area
1618
- const viewport = (await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })')) as {
1619
- width: number
1620
- height: number
1621
- }
1622
-
1623
- // Check if sharp is available for resizing
1624
- const sharp = await sharpPromise
1625
-
1626
- // Clip dimensions: if sharp unavailable, limit capture area to LLM_MAX_DIMENSION
1627
- const clipWidth = sharp ? viewport.width : Math.min(viewport.width, LLM_MAX_DIMENSION)
1628
- const clipHeight = sharp ? viewport.height : Math.min(viewport.height, LLM_MAX_DIMENSION)
1629
-
1630
- // Take viewport screenshot as PNG for Kitty Graphics Protocol compatibility.
1631
- // PNG is lossless and the only format extracted by kitty-graphics-agent (f=100).
1632
- const screenshotStart = Date.now()
1633
- const rawBuffer = await page.screenshot({
1634
- type: 'png',
1635
- scale: 'css',
1636
- clip: { x: 0, y: 0, width: clipWidth, height: clipHeight },
1637
- })
1638
- if (log) {
1639
- log(`page.screenshot: ${Date.now() - screenshotStart}ms`)
1640
- }
1641
-
1642
- // Resize with resizeImage if sharp available, otherwise use clipped raw buffer
1643
- const resizeStart = Date.now()
1644
- const buffer = await (async () => {
1645
- if (!sharp) {
1646
- logger?.error?.('[tabwright] sharp not available, using clipped screenshot (max', LLM_MAX_DIMENSION, 'px)')
1647
- return rawBuffer
1648
- }
1649
- try {
1650
- const result = await resizeImageForAgent({ input: rawBuffer, format: 'png' })
1651
- return result.buffer
1652
- } catch (err) {
1653
- logger?.error?.('[tabwright] sharp resize failed, using raw buffer:', err)
1654
- return rawBuffer
1655
- }
1656
- })()
1657
- if (log) {
1658
- log(`screenshot resize: ${Date.now() - resizeStart}ms`)
1659
- }
1660
-
1661
- // Save to file
1662
- fs.writeFileSync(screenshotPath, buffer)
1663
-
1664
- // Convert to base64
1665
- const base64 = buffer.toString('base64')
1666
-
1667
- // Hide labels
1668
- await hideAriaRefLabels({ page })
1669
-
1670
- // Add to collector array
1671
- collector.push({
1672
- path: screenshotPath,
1673
- base64,
1674
- mimeType: 'image/png',
1675
- snapshot,
1676
- labelCount,
1677
- })
1678
- }
1679
-
1680
- // Re-export for backward compatibility
1681
- export { getAriaSnapshot as getAriaSnapshotWithRefs }