tabwright 1.0.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 (599) hide show
  1. package/LICENSE +21 -0
  2. package/bin.js +4 -0
  3. package/dist/a11y-client.js +204 -0
  4. package/dist/agent-skills/tabwright/SKILL.md +119 -0
  5. package/dist/aria-snapshot.d.ts +262 -0
  6. package/dist/aria-snapshot.d.ts.map +1 -0
  7. package/dist/aria-snapshot.js +1256 -0
  8. package/dist/aria-snapshot.js.map +1 -0
  9. package/dist/aria-snapshot.test.d.ts +2 -0
  10. package/dist/aria-snapshot.test.d.ts.map +1 -0
  11. package/dist/aria-snapshot.test.js +270 -0
  12. package/dist/aria-snapshot.test.js.map +1 -0
  13. package/dist/aria-snapshot.unit.test.d.ts +2 -0
  14. package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
  15. package/dist/aria-snapshot.unit.test.js +542 -0
  16. package/dist/aria-snapshot.unit.test.js.map +1 -0
  17. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  18. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  19. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  20. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  21. package/dist/bippy.js +894 -0
  22. package/dist/browser-config.d.ts +13 -0
  23. package/dist/browser-config.d.ts.map +1 -0
  24. package/dist/browser-config.js +126 -0
  25. package/dist/browser-config.js.map +1 -0
  26. package/dist/browser-install.d.ts +16 -0
  27. package/dist/browser-install.d.ts.map +1 -0
  28. package/dist/browser-install.js +238 -0
  29. package/dist/browser-install.js.map +1 -0
  30. package/dist/browser-launch.d.ts +17 -0
  31. package/dist/browser-launch.d.ts.map +1 -0
  32. package/dist/browser-launch.js +44 -0
  33. package/dist/browser-launch.js.map +1 -0
  34. package/dist/builtin-capabilities.d.ts +28 -0
  35. package/dist/builtin-capabilities.d.ts.map +1 -0
  36. package/dist/builtin-capabilities.js +2673 -0
  37. package/dist/builtin-capabilities.js.map +1 -0
  38. package/dist/capability-agent-skill.d.ts +60 -0
  39. package/dist/capability-agent-skill.d.ts.map +1 -0
  40. package/dist/capability-agent-skill.js +225 -0
  41. package/dist/capability-agent-skill.js.map +1 -0
  42. package/dist/capability-auth-state.d.ts +34 -0
  43. package/dist/capability-auth-state.d.ts.map +1 -0
  44. package/dist/capability-auth-state.js +157 -0
  45. package/dist/capability-auth-state.js.map +1 -0
  46. package/dist/capability-auth.d.ts +36 -0
  47. package/dist/capability-auth.d.ts.map +1 -0
  48. package/dist/capability-auth.js +198 -0
  49. package/dist/capability-auth.js.map +1 -0
  50. package/dist/capability-cli-confirmation.test.d.ts +2 -0
  51. package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
  52. package/dist/capability-cli-confirmation.test.js +79 -0
  53. package/dist/capability-cli-confirmation.test.js.map +1 -0
  54. package/dist/capability-options.d.ts +20 -0
  55. package/dist/capability-options.d.ts.map +1 -0
  56. package/dist/capability-options.js +31 -0
  57. package/dist/capability-options.js.map +1 -0
  58. package/dist/capability-options.test.d.ts +2 -0
  59. package/dist/capability-options.test.d.ts.map +1 -0
  60. package/dist/capability-options.test.js +81 -0
  61. package/dist/capability-options.test.js.map +1 -0
  62. package/dist/capability-package.d.ts +27 -0
  63. package/dist/capability-package.d.ts.map +1 -0
  64. package/dist/capability-package.js +444 -0
  65. package/dist/capability-package.js.map +1 -0
  66. package/dist/capability-package.test.d.ts +2 -0
  67. package/dist/capability-package.test.d.ts.map +1 -0
  68. package/dist/capability-package.test.js +179 -0
  69. package/dist/capability-package.test.js.map +1 -0
  70. package/dist/capability-registry.d.ts +262 -0
  71. package/dist/capability-registry.d.ts.map +1 -0
  72. package/dist/capability-registry.js +931 -0
  73. package/dist/capability-registry.js.map +1 -0
  74. package/dist/capability-registry.test.d.ts +2 -0
  75. package/dist/capability-registry.test.d.ts.map +1 -0
  76. package/dist/capability-registry.test.js +1683 -0
  77. package/dist/capability-registry.test.js.map +1 -0
  78. package/dist/capability-runner.d.ts +99 -0
  79. package/dist/capability-runner.d.ts.map +1 -0
  80. package/dist/capability-runner.js +608 -0
  81. package/dist/capability-runner.js.map +1 -0
  82. package/dist/capability-search-relevance.test.d.ts +2 -0
  83. package/dist/capability-search-relevance.test.d.ts.map +1 -0
  84. package/dist/capability-search-relevance.test.js +67 -0
  85. package/dist/capability-search-relevance.test.js.map +1 -0
  86. package/dist/capability-studio.d.ts +11 -0
  87. package/dist/capability-studio.d.ts.map +1 -0
  88. package/dist/capability-studio.js +593 -0
  89. package/dist/capability-studio.js.map +1 -0
  90. package/dist/capability-studio.test.d.ts +2 -0
  91. package/dist/capability-studio.test.d.ts.map +1 -0
  92. package/dist/capability-studio.test.js +41 -0
  93. package/dist/capability-studio.test.js.map +1 -0
  94. package/dist/cdp-log.d.ts +19 -0
  95. package/dist/cdp-log.d.ts.map +1 -0
  96. package/dist/cdp-log.js +83 -0
  97. package/dist/cdp-log.js.map +1 -0
  98. package/dist/cdp-log.test.d.ts +2 -0
  99. package/dist/cdp-log.test.d.ts.map +1 -0
  100. package/dist/cdp-log.test.js +109 -0
  101. package/dist/cdp-log.test.js.map +1 -0
  102. package/dist/cdp-relay.d.ts +19 -0
  103. package/dist/cdp-relay.d.ts.map +1 -0
  104. package/dist/cdp-relay.js +2375 -0
  105. package/dist/cdp-relay.js.map +1 -0
  106. package/dist/cdp-session.d.ts +37 -0
  107. package/dist/cdp-session.d.ts.map +1 -0
  108. package/dist/cdp-session.js +36 -0
  109. package/dist/cdp-session.js.map +1 -0
  110. package/dist/cdp-types.d.ts +65 -0
  111. package/dist/cdp-types.d.ts.map +1 -0
  112. package/dist/cdp-types.js +91 -0
  113. package/dist/cdp-types.js.map +1 -0
  114. package/dist/channel-owner-inspect.test.d.ts +2 -0
  115. package/dist/channel-owner-inspect.test.d.ts.map +1 -0
  116. package/dist/channel-owner-inspect.test.js +75 -0
  117. package/dist/channel-owner-inspect.test.js.map +1 -0
  118. package/dist/chrome-discovery.d.ts +65 -0
  119. package/dist/chrome-discovery.d.ts.map +1 -0
  120. package/dist/chrome-discovery.js +173 -0
  121. package/dist/chrome-discovery.js.map +1 -0
  122. package/dist/chrome-discovery.test.d.ts +2 -0
  123. package/dist/chrome-discovery.test.d.ts.map +1 -0
  124. package/dist/chrome-discovery.test.js +41 -0
  125. package/dist/chrome-discovery.test.js.map +1 -0
  126. package/dist/clean-html.d.ts +11 -0
  127. package/dist/clean-html.d.ts.map +1 -0
  128. package/dist/clean-html.js +106 -0
  129. package/dist/clean-html.js.map +1 -0
  130. package/dist/cli-help.test.d.ts +2 -0
  131. package/dist/cli-help.test.d.ts.map +1 -0
  132. package/dist/cli-help.test.js +98 -0
  133. package/dist/cli-help.test.js.map +1 -0
  134. package/dist/cli.d.ts +3 -0
  135. package/dist/cli.d.ts.map +1 -0
  136. package/dist/cli.js +2709 -0
  137. package/dist/cli.js.map +1 -0
  138. package/dist/cloud-client.d.ts +56 -0
  139. package/dist/cloud-client.d.ts.map +1 -0
  140. package/dist/cloud-client.js +127 -0
  141. package/dist/cloud-client.js.map +1 -0
  142. package/dist/create-logger.d.ts +9 -0
  143. package/dist/create-logger.d.ts.map +1 -0
  144. package/dist/create-logger.js +27 -0
  145. package/dist/create-logger.js.map +1 -0
  146. package/dist/debugger-api.md +458 -0
  147. package/dist/debugger-examples-types.d.ts +24 -0
  148. package/dist/debugger-examples-types.d.ts.map +1 -0
  149. package/dist/debugger-examples-types.js +2 -0
  150. package/dist/debugger-examples-types.js.map +1 -0
  151. package/dist/debugger-examples.d.ts +6 -0
  152. package/dist/debugger-examples.d.ts.map +1 -0
  153. package/dist/debugger-examples.js +53 -0
  154. package/dist/debugger-examples.js.map +1 -0
  155. package/dist/debugger.d.ts +381 -0
  156. package/dist/debugger.d.ts.map +1 -0
  157. package/dist/debugger.js +630 -0
  158. package/dist/debugger.js.map +1 -0
  159. package/dist/diff-utils.d.ts +20 -0
  160. package/dist/diff-utils.d.ts.map +1 -0
  161. package/dist/diff-utils.js +50 -0
  162. package/dist/diff-utils.js.map +1 -0
  163. package/dist/diff-utils.test.d.ts +2 -0
  164. package/dist/diff-utils.test.d.ts.map +1 -0
  165. package/dist/diff-utils.test.js +137 -0
  166. package/dist/diff-utils.test.js.map +1 -0
  167. package/dist/doctor.d.ts +44 -0
  168. package/dist/doctor.d.ts.map +1 -0
  169. package/dist/doctor.js +246 -0
  170. package/dist/doctor.js.map +1 -0
  171. package/dist/doctor.test.d.ts +2 -0
  172. package/dist/doctor.test.d.ts.map +1 -0
  173. package/dist/doctor.test.js +208 -0
  174. package/dist/doctor.test.js.map +1 -0
  175. package/dist/editor-api.md +374 -0
  176. package/dist/editor-examples.d.ts +11 -0
  177. package/dist/editor-examples.d.ts.map +1 -0
  178. package/dist/editor-examples.js +124 -0
  179. package/dist/editor-examples.js.map +1 -0
  180. package/dist/editor.d.ts +203 -0
  181. package/dist/editor.d.ts.map +1 -0
  182. package/dist/editor.js +363 -0
  183. package/dist/editor.js.map +1 -0
  184. package/dist/env-compat.d.ts +2 -0
  185. package/dist/env-compat.d.ts.map +1 -0
  186. package/dist/env-compat.js +27 -0
  187. package/dist/env-compat.js.map +1 -0
  188. package/dist/executor.d.ts +229 -0
  189. package/dist/executor.d.ts.map +1 -0
  190. package/dist/executor.js +1465 -0
  191. package/dist/executor.js.map +1 -0
  192. package/dist/executor.unit.test.d.ts +2 -0
  193. package/dist/executor.unit.test.d.ts.map +1 -0
  194. package/dist/executor.unit.test.js +143 -0
  195. package/dist/executor.unit.test.js.map +1 -0
  196. package/dist/extension/_locales/en/messages.json +278 -0
  197. package/dist/extension/_locales/zh_CN/messages.json +256 -0
  198. package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
  199. package/dist/extension/background.js +3994 -0
  200. package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
  201. package/dist/extension/icons/icon-black-128.png +0 -0
  202. package/dist/extension/icons/icon-black-16.png +0 -0
  203. package/dist/extension/icons/icon-black-32.png +0 -0
  204. package/dist/extension/icons/icon-black-48.png +0 -0
  205. package/dist/extension/icons/icon-gray-128.png +0 -0
  206. package/dist/extension/icons/icon-gray-16.png +0 -0
  207. package/dist/extension/icons/icon-gray-32.png +0 -0
  208. package/dist/extension/icons/icon-gray-48.png +0 -0
  209. package/dist/extension/icons/icon-green-128.png +0 -0
  210. package/dist/extension/icons/icon-green-16.png +0 -0
  211. package/dist/extension/icons/icon-green-32.png +0 -0
  212. package/dist/extension/icons/icon-green-48.png +0 -0
  213. package/dist/extension/icons/tabwright-icon-black.png +0 -0
  214. package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
  215. package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
  216. package/dist/extension/manifest.json +54 -0
  217. package/dist/extension/options.js +16145 -0
  218. package/dist/extension/rrweb-recorder.js +12952 -0
  219. package/dist/extension/src/options.html +165 -0
  220. package/dist/extension/src/prism-bash.min.js +1 -0
  221. package/dist/extension/src/prism.min.js +1 -0
  222. package/dist/extension/src/welcome.html +396 -0
  223. package/dist/extension-connection.test.d.ts +2 -0
  224. package/dist/extension-connection.test.d.ts.map +1 -0
  225. package/dist/extension-connection.test.js +708 -0
  226. package/dist/extension-connection.test.js.map +1 -0
  227. package/dist/extension-icon-warning.test.d.ts +2 -0
  228. package/dist/extension-icon-warning.test.d.ts.map +1 -0
  229. package/dist/extension-icon-warning.test.js +33 -0
  230. package/dist/extension-icon-warning.test.js.map +1 -0
  231. package/dist/ghost-browser.d.ts +62 -0
  232. package/dist/ghost-browser.d.ts.map +1 -0
  233. package/dist/ghost-browser.js +107 -0
  234. package/dist/ghost-browser.js.map +1 -0
  235. package/dist/ghost-cursor-client.js +374 -0
  236. package/dist/ghost-cursor-controller.d.ts +46 -0
  237. package/dist/ghost-cursor-controller.d.ts.map +1 -0
  238. package/dist/ghost-cursor-controller.js +98 -0
  239. package/dist/ghost-cursor-controller.js.map +1 -0
  240. package/dist/ghost-cursor.d.ts +27 -0
  241. package/dist/ghost-cursor.d.ts.map +1 -0
  242. package/dist/ghost-cursor.js +79 -0
  243. package/dist/ghost-cursor.js.map +1 -0
  244. package/dist/htmlrewrite.d.ts +8 -0
  245. package/dist/htmlrewrite.d.ts.map +1 -0
  246. package/dist/htmlrewrite.js +339 -0
  247. package/dist/htmlrewrite.js.map +1 -0
  248. package/dist/htmlrewrite.test.d.ts +2 -0
  249. package/dist/htmlrewrite.test.d.ts.map +1 -0
  250. package/dist/htmlrewrite.test.js +7975 -0
  251. package/dist/htmlrewrite.test.js.map +1 -0
  252. package/dist/index.d.ts +20 -0
  253. package/dist/index.d.ts.map +1 -0
  254. package/dist/index.js +11 -0
  255. package/dist/index.js.map +1 -0
  256. package/dist/kill-port.d.ts +29 -0
  257. package/dist/kill-port.d.ts.map +1 -0
  258. package/dist/kill-port.js +190 -0
  259. package/dist/kill-port.js.map +1 -0
  260. package/dist/kitty-graphics.d.ts +22 -0
  261. package/dist/kitty-graphics.d.ts.map +1 -0
  262. package/dist/kitty-graphics.js +71 -0
  263. package/dist/kitty-graphics.js.map +1 -0
  264. package/dist/kitty-graphics.test.d.ts +2 -0
  265. package/dist/kitty-graphics.test.d.ts.map +1 -0
  266. package/dist/kitty-graphics.test.js +125 -0
  267. package/dist/kitty-graphics.test.js.map +1 -0
  268. package/dist/locator-selector.test.d.ts +2 -0
  269. package/dist/locator-selector.test.d.ts.map +1 -0
  270. package/dist/locator-selector.test.js +98 -0
  271. package/dist/locator-selector.test.js.map +1 -0
  272. package/dist/mcp-client.d.ts +20 -0
  273. package/dist/mcp-client.d.ts.map +1 -0
  274. package/dist/mcp-client.js +56 -0
  275. package/dist/mcp-client.js.map +1 -0
  276. package/dist/mcp.d.ts +6 -0
  277. package/dist/mcp.d.ts.map +1 -0
  278. package/dist/mcp.js +511 -0
  279. package/dist/mcp.js.map +1 -0
  280. package/dist/on-mouse-action.test.d.ts +2 -0
  281. package/dist/on-mouse-action.test.d.ts.map +1 -0
  282. package/dist/on-mouse-action.test.js +180 -0
  283. package/dist/on-mouse-action.test.js.map +1 -0
  284. package/dist/options-page.test.d.ts +2 -0
  285. package/dist/options-page.test.d.ts.map +1 -0
  286. package/dist/options-page.test.js +773 -0
  287. package/dist/options-page.test.js.map +1 -0
  288. package/dist/package-paths.d.ts +4 -0
  289. package/dist/package-paths.d.ts.map +1 -0
  290. package/dist/package-paths.js +30 -0
  291. package/dist/package-paths.js.map +1 -0
  292. package/dist/page-markdown.d.ts +40 -0
  293. package/dist/page-markdown.d.ts.map +1 -0
  294. package/dist/page-markdown.js +181 -0
  295. package/dist/page-markdown.js.map +1 -0
  296. package/dist/performance-examples.d.ts +5 -0
  297. package/dist/performance-examples.d.ts.map +1 -0
  298. package/dist/performance-examples.js +112 -0
  299. package/dist/performance-examples.js.map +1 -0
  300. package/dist/performance-profiling.md +417 -0
  301. package/dist/playwright-import.d.ts +19 -0
  302. package/dist/playwright-import.d.ts.map +1 -0
  303. package/dist/playwright-import.js +40 -0
  304. package/dist/playwright-import.js.map +1 -0
  305. package/dist/popup-relocation.test.d.ts +7 -0
  306. package/dist/popup-relocation.test.d.ts.map +1 -0
  307. package/dist/popup-relocation.test.js +139 -0
  308. package/dist/popup-relocation.test.js.map +1 -0
  309. package/dist/product-paths.d.ts +9 -0
  310. package/dist/product-paths.d.ts.map +1 -0
  311. package/dist/product-paths.js +24 -0
  312. package/dist/product-paths.js.map +1 -0
  313. package/dist/product-paths.test.d.ts +2 -0
  314. package/dist/product-paths.test.d.ts.map +1 -0
  315. package/dist/product-paths.test.js +39 -0
  316. package/dist/product-paths.test.js.map +1 -0
  317. package/dist/prompt.md +1004 -0
  318. package/dist/protocol.d.ts +248 -0
  319. package/dist/protocol.d.ts.map +1 -0
  320. package/dist/protocol.js +59 -0
  321. package/dist/protocol.js.map +1 -0
  322. package/dist/protocol.test.d.ts +2 -0
  323. package/dist/protocol.test.d.ts.map +1 -0
  324. package/dist/protocol.test.js +31 -0
  325. package/dist/protocol.test.js.map +1 -0
  326. package/dist/react-source.d.ts +57 -0
  327. package/dist/react-source.d.ts.map +1 -0
  328. package/dist/react-source.js +254 -0
  329. package/dist/react-source.js.map +1 -0
  330. package/dist/readability.js +1674 -0
  331. package/dist/relay-client.d.ts +104 -0
  332. package/dist/relay-client.d.ts.map +1 -0
  333. package/dist/relay-client.js +426 -0
  334. package/dist/relay-client.js.map +1 -0
  335. package/dist/relay-client.test.d.ts +2 -0
  336. package/dist/relay-client.test.d.ts.map +1 -0
  337. package/dist/relay-client.test.js +305 -0
  338. package/dist/relay-client.test.js.map +1 -0
  339. package/dist/relay-core.test.d.ts +2 -0
  340. package/dist/relay-core.test.d.ts.map +1 -0
  341. package/dist/relay-core.test.js +1427 -0
  342. package/dist/relay-core.test.js.map +1 -0
  343. package/dist/relay-navigation.test.d.ts +2 -0
  344. package/dist/relay-navigation.test.d.ts.map +1 -0
  345. package/dist/relay-navigation.test.js +661 -0
  346. package/dist/relay-navigation.test.js.map +1 -0
  347. package/dist/relay-session.test.d.ts +2 -0
  348. package/dist/relay-session.test.d.ts.map +1 -0
  349. package/dist/relay-session.test.js +1121 -0
  350. package/dist/relay-session.test.js.map +1 -0
  351. package/dist/relay-state.d.ts +178 -0
  352. package/dist/relay-state.d.ts.map +1 -0
  353. package/dist/relay-state.js +346 -0
  354. package/dist/relay-state.js.map +1 -0
  355. package/dist/relay-state.test.d.ts +2 -0
  356. package/dist/relay-state.test.d.ts.map +1 -0
  357. package/dist/relay-state.test.js +594 -0
  358. package/dist/relay-state.test.js.map +1 -0
  359. package/dist/replay-ai-index.d.ts +99 -0
  360. package/dist/replay-ai-index.d.ts.map +1 -0
  361. package/dist/replay-ai-index.js +677 -0
  362. package/dist/replay-ai-index.js.map +1 -0
  363. package/dist/replay-ai-index.test.d.ts +2 -0
  364. package/dist/replay-ai-index.test.d.ts.map +1 -0
  365. package/dist/replay-ai-index.test.js +255 -0
  366. package/dist/replay-ai-index.test.js.map +1 -0
  367. package/dist/replay-cli.test.d.ts +2 -0
  368. package/dist/replay-cli.test.d.ts.map +1 -0
  369. package/dist/replay-cli.test.js +275 -0
  370. package/dist/replay-cli.test.js.map +1 -0
  371. package/dist/replay-eval.d.ts +44 -0
  372. package/dist/replay-eval.d.ts.map +1 -0
  373. package/dist/replay-eval.js +902 -0
  374. package/dist/replay-eval.js.map +1 -0
  375. package/dist/replay-handoff.d.ts +29 -0
  376. package/dist/replay-handoff.d.ts.map +1 -0
  377. package/dist/replay-handoff.js +83 -0
  378. package/dist/replay-handoff.js.map +1 -0
  379. package/dist/replay-handoff.test.d.ts +2 -0
  380. package/dist/replay-handoff.test.d.ts.map +1 -0
  381. package/dist/replay-handoff.test.js +123 -0
  382. package/dist/replay-handoff.test.js.map +1 -0
  383. package/dist/replay-workflow-compiler.d.ts +44 -0
  384. package/dist/replay-workflow-compiler.d.ts.map +1 -0
  385. package/dist/replay-workflow-compiler.js +601 -0
  386. package/dist/replay-workflow-compiler.js.map +1 -0
  387. package/dist/replay-workflow-compiler.test.d.ts +2 -0
  388. package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
  389. package/dist/replay-workflow-compiler.test.js +223 -0
  390. package/dist/replay-workflow-compiler.test.js.map +1 -0
  391. package/dist/rrweb-recording-relay.d.ts +56 -0
  392. package/dist/rrweb-recording-relay.d.ts.map +1 -0
  393. package/dist/rrweb-recording-relay.js +299 -0
  394. package/dist/rrweb-recording-relay.js.map +1 -0
  395. package/dist/rrweb-recording-relay.test.d.ts +2 -0
  396. package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
  397. package/dist/rrweb-recording-relay.test.js +85 -0
  398. package/dist/rrweb-recording-relay.test.js.map +1 -0
  399. package/dist/rrweb-recording.d.ts +104 -0
  400. package/dist/rrweb-recording.d.ts.map +1 -0
  401. package/dist/rrweb-recording.js +180 -0
  402. package/dist/rrweb-recording.js.map +1 -0
  403. package/dist/scoped-fs.d.ts +95 -0
  404. package/dist/scoped-fs.d.ts.map +1 -0
  405. package/dist/scoped-fs.js +358 -0
  406. package/dist/scoped-fs.js.map +1 -0
  407. package/dist/scoped-fs.test.d.ts +5 -0
  408. package/dist/scoped-fs.test.d.ts.map +1 -0
  409. package/dist/scoped-fs.test.js +73 -0
  410. package/dist/scoped-fs.test.js.map +1 -0
  411. package/dist/selector-generator.js +7378 -0
  412. package/dist/skill-stub.test.d.ts +2 -0
  413. package/dist/skill-stub.test.d.ts.map +1 -0
  414. package/dist/skill-stub.test.js +22 -0
  415. package/dist/skill-stub.test.js.map +1 -0
  416. package/dist/snapshot-tools.test.d.ts +2 -0
  417. package/dist/snapshot-tools.test.d.ts.map +1 -0
  418. package/dist/snapshot-tools.test.js +856 -0
  419. package/dist/snapshot-tools.test.js.map +1 -0
  420. package/dist/start-relay-server.d.ts +6 -0
  421. package/dist/start-relay-server.d.ts.map +1 -0
  422. package/dist/start-relay-server.js +57 -0
  423. package/dist/start-relay-server.js.map +1 -0
  424. package/dist/styles-api.md +124 -0
  425. package/dist/styles-examples.d.ts +8 -0
  426. package/dist/styles-examples.d.ts.map +1 -0
  427. package/dist/styles-examples.js +64 -0
  428. package/dist/styles-examples.js.map +1 -0
  429. package/dist/styles.d.ts +27 -0
  430. package/dist/styles.d.ts.map +1 -0
  431. package/dist/styles.js +231 -0
  432. package/dist/styles.js.map +1 -0
  433. package/dist/tabwright-agent-skill.d.ts +30 -0
  434. package/dist/tabwright-agent-skill.d.ts.map +1 -0
  435. package/dist/tabwright-agent-skill.js +74 -0
  436. package/dist/tabwright-agent-skill.js.map +1 -0
  437. package/dist/tabwright-agent-skill.test.d.ts +2 -0
  438. package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
  439. package/dist/tabwright-agent-skill.test.js +51 -0
  440. package/dist/tabwright-agent-skill.test.js.map +1 -0
  441. package/dist/test-declarations.d.ts +13 -0
  442. package/dist/test-declarations.d.ts.map +1 -0
  443. package/dist/test-declarations.js +2 -0
  444. package/dist/test-declarations.js.map +1 -0
  445. package/dist/test-utils.d.ts +69 -0
  446. package/dist/test-utils.d.ts.map +1 -0
  447. package/dist/test-utils.js +448 -0
  448. package/dist/test-utils.js.map +1 -0
  449. package/dist/test-utils.test.d.ts +2 -0
  450. package/dist/test-utils.test.d.ts.map +1 -0
  451. package/dist/test-utils.test.js +55 -0
  452. package/dist/test-utils.test.js.map +1 -0
  453. package/dist/utils.d.ts +26 -0
  454. package/dist/utils.d.ts.map +1 -0
  455. package/dist/utils.js +60 -0
  456. package/dist/utils.js.map +1 -0
  457. package/dist/wait-for-page-load.d.ts +16 -0
  458. package/dist/wait-for-page-load.d.ts.map +1 -0
  459. package/dist/wait-for-page-load.js +146 -0
  460. package/dist/wait-for-page-load.js.map +1 -0
  461. package/dist/workflow-capability.d.ts +72 -0
  462. package/dist/workflow-capability.d.ts.map +1 -0
  463. package/dist/workflow-capability.js +316 -0
  464. package/dist/workflow-capability.js.map +1 -0
  465. package/dist/workflow-script-flow.test.d.ts +2 -0
  466. package/dist/workflow-script-flow.test.d.ts.map +1 -0
  467. package/dist/workflow-script-flow.test.js +285 -0
  468. package/dist/workflow-script-flow.test.js.map +1 -0
  469. package/package.json +80 -0
  470. package/src/__snapshots__/x.com.processed.html +1003 -0
  471. package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
  472. package/src/a11y-client.ts +241 -0
  473. package/src/agent-skills/conan-config/SKILL.md +141 -0
  474. package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
  475. package/src/aria-snapshot.test.ts +311 -0
  476. package/src/aria-snapshot.ts +1681 -0
  477. package/src/aria-snapshot.unit.test.ts +615 -0
  478. package/src/aria-snapshots/github-interactive.txt +193 -0
  479. package/src/aria-snapshots/github-raw.txt +309 -0
  480. package/src/aria-snapshots/hackernews-interactive.txt +477 -0
  481. package/src/aria-snapshots/hackernews-raw.txt +428 -0
  482. package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
  483. package/src/aria-snapshots/prosemirror-raw.txt +145 -0
  484. package/src/assets/aria-labels-hacker-news.png +0 -0
  485. package/src/assets/aria-labels-old-reddit.png +0 -0
  486. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  487. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  488. package/src/assets/x.com.html +32946 -0
  489. package/src/browser-config.ts +179 -0
  490. package/src/browser-install.ts +284 -0
  491. package/src/browser-launch.ts +75 -0
  492. package/src/builtin-capabilities.ts +2765 -0
  493. package/src/capability-agent-skill.ts +305 -0
  494. package/src/capability-auth-state.ts +197 -0
  495. package/src/capability-auth.ts +249 -0
  496. package/src/capability-cli-confirmation.test.ts +90 -0
  497. package/src/capability-options.test.ts +89 -0
  498. package/src/capability-options.ts +57 -0
  499. package/src/capability-package.test.ts +204 -0
  500. package/src/capability-package.ts +555 -0
  501. package/src/capability-registry.test.ts +1905 -0
  502. package/src/capability-registry.ts +1234 -0
  503. package/src/capability-runner.ts +801 -0
  504. package/src/capability-search-relevance.test.ts +73 -0
  505. package/src/capability-studio.test.ts +45 -0
  506. package/src/capability-studio.ts +641 -0
  507. package/src/cdp-log.test.ts +131 -0
  508. package/src/cdp-log.ts +112 -0
  509. package/src/cdp-relay.ts +2944 -0
  510. package/src/cdp-session.ts +77 -0
  511. package/src/cdp-types.ts +158 -0
  512. package/src/channel-owner-inspect.test.ts +94 -0
  513. package/src/chrome-discovery.test.ts +50 -0
  514. package/src/chrome-discovery.ts +220 -0
  515. package/src/clean-html.ts +141 -0
  516. package/src/cli-help.test.ts +123 -0
  517. package/src/cli.ts +3181 -0
  518. package/src/cloud-client.ts +179 -0
  519. package/src/create-logger.ts +38 -0
  520. package/src/debugger-examples-types.ts +16 -0
  521. package/src/debugger-examples.ts +66 -0
  522. package/src/debugger.ts +708 -0
  523. package/src/diff-utils.test.ts +152 -0
  524. package/src/diff-utils.ts +70 -0
  525. package/src/doctor.test.ts +244 -0
  526. package/src/doctor.ts +321 -0
  527. package/src/editor-examples.ts +158 -0
  528. package/src/editor.ts +426 -0
  529. package/src/env-compat.ts +28 -0
  530. package/src/executor.ts +1824 -0
  531. package/src/executor.unit.test.ts +168 -0
  532. package/src/extension-connection.test.ts +855 -0
  533. package/src/extension-icon-warning.test.ts +45 -0
  534. package/src/ghost-browser.ts +149 -0
  535. package/src/ghost-cursor-client.ts +494 -0
  536. package/src/ghost-cursor-controller.ts +129 -0
  537. package/src/ghost-cursor.ts +123 -0
  538. package/src/htmlrewrite.test.ts +8008 -0
  539. package/src/htmlrewrite.ts +390 -0
  540. package/src/index.ts +53 -0
  541. package/src/kill-port.ts +219 -0
  542. package/src/kitty-graphics.test.ts +139 -0
  543. package/src/kitty-graphics.ts +79 -0
  544. package/src/locator-selector.test.ts +119 -0
  545. package/src/mcp-client.ts +78 -0
  546. package/src/mcp.ts +629 -0
  547. package/src/on-mouse-action.test.ts +226 -0
  548. package/src/options-page.test.ts +839 -0
  549. package/src/package-paths.ts +38 -0
  550. package/src/page-markdown.ts +240 -0
  551. package/src/performance-examples.ts +186 -0
  552. package/src/playwright-import.ts +59 -0
  553. package/src/popup-relocation.test.ts +163 -0
  554. package/src/product-paths.test.ts +47 -0
  555. package/src/product-paths.ts +28 -0
  556. package/src/protocol.test.ts +52 -0
  557. package/src/protocol.ts +351 -0
  558. package/src/react-source.ts +379 -0
  559. package/src/relay-client.test.ts +375 -0
  560. package/src/relay-client.ts +572 -0
  561. package/src/relay-core.test.ts +1603 -0
  562. package/src/relay-navigation.test.ts +790 -0
  563. package/src/relay-session.test.ts +1375 -0
  564. package/src/relay-state.test.ts +729 -0
  565. package/src/relay-state.ts +561 -0
  566. package/src/replay-ai-index.test.ts +278 -0
  567. package/src/replay-ai-index.ts +872 -0
  568. package/src/replay-cli.test.ts +325 -0
  569. package/src/replay-eval.ts +1039 -0
  570. package/src/replay-handoff.test.ts +163 -0
  571. package/src/replay-handoff.ts +109 -0
  572. package/src/replay-workflow-compiler.test.ts +250 -0
  573. package/src/replay-workflow-compiler.ts +700 -0
  574. package/src/resource.md +409 -0
  575. package/src/rrweb-recording-relay.test.ts +99 -0
  576. package/src/rrweb-recording-relay.ts +400 -0
  577. package/src/rrweb-recording.ts +322 -0
  578. package/src/scoped-fs.test.ts +80 -0
  579. package/src/scoped-fs.ts +420 -0
  580. package/src/skill-stub.test.ts +24 -0
  581. package/src/skill.md +1413 -0
  582. package/src/snapshot-tools.test.ts +1017 -0
  583. package/src/snapshots/hacker-news-accessibility-full.md +81 -0
  584. package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
  585. package/src/snapshots/page-markdown-output.txt +16 -0
  586. package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
  587. package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
  588. package/src/start-relay-server.ts +69 -0
  589. package/src/styles-examples.ts +84 -0
  590. package/src/styles.ts +343 -0
  591. package/src/tabwright-agent-skill.test.ts +61 -0
  592. package/src/tabwright-agent-skill.ts +105 -0
  593. package/src/test-declarations.ts +13 -0
  594. package/src/test-utils.test.ts +70 -0
  595. package/src/test-utils.ts +566 -0
  596. package/src/utils.ts +78 -0
  597. package/src/wait-for-page-load.ts +198 -0
  598. package/src/workflow-capability.ts +417 -0
  599. package/src/workflow-script-flow.test.ts +388 -0
@@ -0,0 +1,1427 @@
1
+ import { createMCPClient } from './mcp-client.js';
2
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import { chromium } from '@xmorse/playwright-core';
4
+ import { getCDPSessionForPage } from './cdp-session.js';
5
+ import { getCdpUrl, LOG_CDP_FILE_PATH } from './utils.js';
6
+ import fs from 'node:fs';
7
+ import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, withTimeout, js, tryJsonParse, createSimpleServer, safeCloseCDPBrowser, } from './test-utils.js';
8
+ import './test-declarations.js';
9
+ const TEST_PORT = 19987;
10
+ describe('Relay Core Tests', () => {
11
+ let client;
12
+ let cleanup = null;
13
+ let testCtx = null;
14
+ beforeAll(async () => {
15
+ testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-test-', toggleExtension: true });
16
+ const result = await createMCPClient({ port: TEST_PORT });
17
+ client = result.client;
18
+ cleanup = result.cleanup;
19
+ }, 600000);
20
+ afterAll(async () => {
21
+ await cleanupTestContext(testCtx, cleanup);
22
+ cleanup = null;
23
+ testCtx = null;
24
+ });
25
+ const getBrowserContext = () => {
26
+ if (!testCtx?.browserContext)
27
+ throw new Error('Browser not initialized');
28
+ return testCtx.browserContext;
29
+ };
30
+ const ensureConnectedTabForExecute = async () => {
31
+ const browserContext = getBrowserContext();
32
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
33
+ const connectedTabCount = await serviceWorker.evaluate(async () => {
34
+ const state = globalThis.getExtensionState();
35
+ return state.tabs.size;
36
+ });
37
+ if (connectedTabCount > 0) {
38
+ return;
39
+ }
40
+ const page = await browserContext.newPage();
41
+ await page.goto('about:blank');
42
+ await page.bringToFront();
43
+ await serviceWorker.evaluate(async () => {
44
+ await globalThis.toggleExtensionForActiveTab();
45
+ });
46
+ await new Promise((r) => {
47
+ setTimeout(r, 100);
48
+ });
49
+ };
50
+ it('should inject script via addScriptTag through CDP relay', async () => {
51
+ const browserContext = getBrowserContext();
52
+ const serviceWorker = await withTimeout({
53
+ promise: getExtensionServiceWorker(browserContext),
54
+ timeoutMs: 5000,
55
+ errorMessage: 'Timed out waiting for extension service worker for iframe test',
56
+ });
57
+ const page = await browserContext.newPage();
58
+ const html = '<html><body><button id="btn">Click</button></body></html>';
59
+ const dataUrl = `data:text/html,${encodeURIComponent(html)}`;
60
+ await page.goto(dataUrl);
61
+ await page.bringToFront();
62
+ await withTimeout({
63
+ promise: serviceWorker.evaluate(async () => {
64
+ await globalThis.toggleExtensionForActiveTab();
65
+ }),
66
+ timeoutMs: 10000,
67
+ errorMessage: 'Timed out toggling extension for active tab',
68
+ });
69
+ await new Promise((r) => {
70
+ setTimeout(r, 100);
71
+ });
72
+ const cdpSession = await withTimeout({
73
+ promise: getCDPSessionForPage({ page }),
74
+ timeoutMs: 10000,
75
+ errorMessage: 'Timed out creating CDP session for page',
76
+ });
77
+ const hasGlobalBefore = await page.evaluate(() => {
78
+ return Boolean(globalThis.__testGlobal);
79
+ });
80
+ expect(hasGlobalBefore).toBe(false);
81
+ await withTimeout({
82
+ promise: (async () => {
83
+ await cdpSession.send('Page.enable');
84
+ await cdpSession.send('Page.addScriptToEvaluateOnNewDocument', {
85
+ source: 'globalThis.__testGlobal = { foo: "bar" }',
86
+ });
87
+ await page.reload({ waitUntil: 'domcontentloaded' });
88
+ })(),
89
+ timeoutMs: 10000,
90
+ errorMessage: 'Timed out injecting script via CDP session',
91
+ });
92
+ const hasGlobalAfter = await page.evaluate(() => {
93
+ return globalThis.__testGlobal;
94
+ });
95
+ expect(hasGlobalAfter).toEqual({ foo: 'bar' });
96
+ await cdpSession.detach();
97
+ await page.close();
98
+ }, 60000);
99
+ it('should emit download events for both Browser and Page domains in extension mode', async () => {
100
+ const browserContext = getBrowserContext();
101
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
102
+ const logFilePath = LOG_CDP_FILE_PATH;
103
+ const logLineCountBefore = fs.existsSync(logFilePath)
104
+ ? fs
105
+ .readFileSync(logFilePath, 'utf-8')
106
+ .split('\n')
107
+ .filter((line) => {
108
+ return line.trim().length > 0;
109
+ }).length
110
+ : 0;
111
+ const server = await createSimpleServer({
112
+ routes: {
113
+ '/': `<!doctype html>
114
+ <html>
115
+ <body>
116
+ <button id="download-button">Download</button>
117
+ <script>
118
+ const button = document.getElementById('download-button');
119
+ button.addEventListener('click', () => {
120
+ const blob = new Blob(['tabwright-download-test'], { type: 'text/plain' });
121
+ const url = URL.createObjectURL(blob);
122
+ const anchor = document.createElement('a');
123
+ anchor.href = url;
124
+ anchor.download = 'tabwright-download-test.txt';
125
+ document.body.appendChild(anchor);
126
+ anchor.click();
127
+ anchor.remove();
128
+ setTimeout(() => {
129
+ URL.revokeObjectURL(url);
130
+ }, 1000);
131
+ });
132
+ </script>
133
+ </body>
134
+ </html>`,
135
+ },
136
+ });
137
+ const page = await browserContext.newPage();
138
+ await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
139
+ await page.bringToFront();
140
+ await serviceWorker.evaluate(async () => {
141
+ await globalThis.toggleExtensionForActiveTab();
142
+ });
143
+ const directBrowser = await withTimeout({
144
+ promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
145
+ timeoutMs: 10000,
146
+ errorMessage: 'Timed out connecting over CDP for download reproduction test',
147
+ });
148
+ const connectedPage = directBrowser
149
+ .contexts()[0]
150
+ .pages()
151
+ .find((candidatePage) => {
152
+ return candidatePage.url() === server.baseUrl + '/';
153
+ });
154
+ if (!connectedPage) {
155
+ throw new Error('Connected page not found for download reproduction test');
156
+ }
157
+ const downloadResult = await Promise.all([
158
+ connectedPage.waitForEvent('download', { timeout: 3000 }).then((download) => {
159
+ return { timedOut: false, suggestedFilename: download.suggestedFilename() };
160
+ }, (error) => {
161
+ return { timedOut: true, errorMessage: error.message };
162
+ }),
163
+ connectedPage.click('#download-button'),
164
+ ]);
165
+ expect(downloadResult[0]).toMatchInlineSnapshot(`
166
+ {
167
+ "suggestedFilename": "tabwright-download-test.txt",
168
+ "timedOut": false,
169
+ }
170
+ `);
171
+ await directBrowser.close();
172
+ await page.close();
173
+ await server.close();
174
+ const logLinesAfter = fs
175
+ .readFileSync(logFilePath, 'utf-8')
176
+ .split('\n')
177
+ .filter((line) => {
178
+ return line.trim().length > 0;
179
+ })
180
+ .slice(logLineCountBefore);
181
+ const newEntries = logLinesAfter
182
+ .map((line) => {
183
+ return tryJsonParse(line);
184
+ })
185
+ .filter((entry) => {
186
+ return Boolean(entry && typeof entry === 'object' && 'direction' in entry && 'message' in entry);
187
+ });
188
+ const methods = newEntries
189
+ .map((entry) => {
190
+ return {
191
+ direction: entry.direction,
192
+ method: typeof entry.message?.method === 'string' ? entry.message.method : 'response',
193
+ };
194
+ })
195
+ .filter((entry) => {
196
+ return (entry.method.includes('download') ||
197
+ entry.method === 'Browser.setDownloadBehavior' ||
198
+ entry.method === 'Page.setDownloadBehavior');
199
+ });
200
+ const summary = {
201
+ hasBrowserSetDownloadBehavior: methods.some((entry) => {
202
+ return entry.direction === 'from-playwright' && entry.method === 'Browser.setDownloadBehavior';
203
+ }),
204
+ hasPageSetDownloadBehavior: methods.some((entry) => {
205
+ return entry.direction === 'to-extension' && entry.method === 'Page.setDownloadBehavior';
206
+ }),
207
+ hasPageDownloadWillBegin: methods.some((entry) => {
208
+ return entry.method === 'Page.downloadWillBegin';
209
+ }),
210
+ hasPageDownloadProgress: methods.some((entry) => {
211
+ return entry.method === 'Page.downloadProgress';
212
+ }),
213
+ hasBrowserDownloadWillBegin: methods.some((entry) => {
214
+ return entry.method === 'Browser.downloadWillBegin';
215
+ }),
216
+ hasBrowserDownloadProgress: methods.some((entry) => {
217
+ return entry.method === 'Browser.downloadProgress';
218
+ }),
219
+ };
220
+ expect(summary).toMatchInlineSnapshot(`
221
+ {
222
+ "hasBrowserDownloadProgress": true,
223
+ "hasBrowserDownloadWillBegin": true,
224
+ "hasBrowserSetDownloadBehavior": true,
225
+ "hasPageDownloadProgress": true,
226
+ "hasPageDownloadWillBegin": true,
227
+ "hasPageSetDownloadBehavior": true,
228
+ }
229
+ `);
230
+ }, 120000);
231
+ it('should ignore duplicate dialog dismissals from multiple CDP clients', async () => {
232
+ const browserContext = getBrowserContext();
233
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
234
+ const logFilePath = `${process.cwd()}/relay-server.log`;
235
+ const logLineCountBefore = fs.existsSync(logFilePath)
236
+ ? fs
237
+ .readFileSync(logFilePath, 'utf-8')
238
+ .split('\n')
239
+ .filter((line) => {
240
+ return line.trim().length > 0;
241
+ }).length
242
+ : 0;
243
+ const server = await createSimpleServer({
244
+ routes: {
245
+ '/': `<!doctype html>
246
+ <html>
247
+ <body>
248
+ <button id="open-dialog">Open dialog</button>
249
+ <script>
250
+ document.getElementById('open-dialog').addEventListener('click', () => {
251
+ alert('shared dialog')
252
+ })
253
+ </script>
254
+ </body>
255
+ </html>`,
256
+ },
257
+ });
258
+ const page = await browserContext.newPage();
259
+ let browserA = null;
260
+ let browserB = null;
261
+ try {
262
+ await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
263
+ await page.bringToFront();
264
+ await serviceWorker.evaluate(async () => {
265
+ await globalThis.toggleExtensionForActiveTab();
266
+ });
267
+ await new Promise((r) => {
268
+ setTimeout(r, 100);
269
+ });
270
+ browserA = await withTimeout({
271
+ promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
272
+ timeoutMs: 10000,
273
+ errorMessage: 'Timed out connecting first CDP client for dialog test',
274
+ });
275
+ browserB = await withTimeout({
276
+ promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
277
+ timeoutMs: 10000,
278
+ errorMessage: 'Timed out connecting second CDP client for dialog test',
279
+ });
280
+ const getConnectedPage = (browser) => {
281
+ return browser
282
+ .contexts()[0]
283
+ ?.pages()
284
+ .find((candidatePage) => {
285
+ return candidatePage.url() === server.baseUrl + '/';
286
+ });
287
+ };
288
+ const connectedPageA = getConnectedPage(browserA);
289
+ const connectedPageB = getConnectedPage(browserB);
290
+ expect(connectedPageA).toBeDefined();
291
+ expect(connectedPageB).toBeDefined();
292
+ await withTimeout({
293
+ promise: connectedPageA.click('#open-dialog'),
294
+ timeoutMs: 5000,
295
+ errorMessage: 'Timed out opening dialog with first CDP client',
296
+ });
297
+ const secondClientResult = await withTimeout({
298
+ promise: connectedPageB.evaluate(() => {
299
+ return 2 + 2;
300
+ }),
301
+ timeoutMs: 5000,
302
+ errorMessage: 'Second CDP client stopped responding after shared dialog dismissal',
303
+ });
304
+ expect(secondClientResult).toBe(4);
305
+ await new Promise((r) => {
306
+ setTimeout(r, 200);
307
+ });
308
+ }
309
+ finally {
310
+ if (browserA) {
311
+ await safeCloseCDPBrowser(browserA);
312
+ }
313
+ if (browserB) {
314
+ await safeCloseCDPBrowser(browserB);
315
+ }
316
+ await page.close();
317
+ await server.close();
318
+ }
319
+ const logLinesAfter = fs
320
+ .readFileSync(logFilePath, 'utf-8')
321
+ .split('\n')
322
+ .filter((line) => {
323
+ return line.trim().length > 0;
324
+ })
325
+ .slice(logLineCountBefore);
326
+ const unexpectedRelayCrashes = logLinesAfter.filter((line) => {
327
+ return line.includes('Unhandled Rejection:');
328
+ });
329
+ expect(unexpectedRelayCrashes).toEqual([]);
330
+ }, 60000);
331
+ it('should execute code and capture console output', async () => {
332
+ await client.callTool({
333
+ name: 'execute',
334
+ arguments: {
335
+ code: js `
336
+ const newPage = await context.newPage();
337
+ state.page = newPage;
338
+ if (!state.pages) state.pages = [];
339
+ state.pages.push(newPage);
340
+ `,
341
+ },
342
+ });
343
+ const result = await client.callTool({
344
+ name: 'execute',
345
+ arguments: {
346
+ code: js `
347
+ await state.page.goto('https://example.com');
348
+ const title = await state.page.title();
349
+ console.log('Page title:', title);
350
+ return { url: state.page.url(), title };
351
+ `,
352
+ },
353
+ });
354
+ expect(result.content).toMatchInlineSnapshot(`
355
+ [
356
+ {
357
+ "text": "Console output:
358
+ [log] Page title: Example Domain
359
+
360
+ [return value] { url: 'https://example.com/', title: 'Example Domain' }",
361
+ "type": "text",
362
+ },
363
+ ]
364
+ `);
365
+ expect(result.content).toBeDefined();
366
+ }, 30000);
367
+ // Repro test for https://github.com/remorses/playwriter/issues/66.
368
+ // Current limitation: extension-mode routing does not support root-session
369
+ // Storage.getCookies in playwriter. MUST use Network.getCookies via page CDP
370
+ // session instead (see test below), so this repro stays skipped.
371
+ it.skip('should reproduce page.route failure in MCP execute path (issue #66)', async () => {
372
+ const server = await createSimpleServer({
373
+ routes: {
374
+ '/': '<!doctype html><html><body>route issue repro</body></html>',
375
+ '/api/data': '{"ok":true}',
376
+ },
377
+ });
378
+ try {
379
+ const result = await client.callTool({
380
+ name: 'execute',
381
+ arguments: {
382
+ code: js `
383
+ const newPage = await context.newPage();
384
+ state.issue66Page = newPage;
385
+ await newPage.goto('${server.baseUrl}', { waitUntil: 'domcontentloaded' });
386
+
387
+ let routeFetchError = null;
388
+ await newPage.route('**/api/**', async (route) => {
389
+ try {
390
+ const response = await route.fetch();
391
+ await route.fulfill({ response });
392
+ } catch (error) {
393
+ routeFetchError = error instanceof Error ? error.message : String(error);
394
+ await route.abort();
395
+ }
396
+ });
397
+
398
+ await newPage.evaluate(async () => {
399
+ await fetch('/api/data').catch(() => null);
400
+ });
401
+
402
+ return { routeFetchError };
403
+ `,
404
+ },
405
+ });
406
+ const resultWithContent = result;
407
+ const content = Array.isArray(resultWithContent.content) ? resultWithContent.content : [];
408
+ const firstContent = content[0];
409
+ const output = typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent
410
+ ? String(firstContent.text ?? '')
411
+ : '';
412
+ expect(output).toContain('routeFetchError');
413
+ expect(output).toContain('Storage.getCookies');
414
+ expect(output).toContain('No tab found for method Storage.getCookies');
415
+ }
416
+ finally {
417
+ try {
418
+ await client.callTool({
419
+ name: 'execute',
420
+ arguments: {
421
+ code: js `
422
+ if (state.issue66Page && !state.issue66Page.isClosed()) {
423
+ await state.issue66Page.close();
424
+ }
425
+ delete state.issue66Page;
426
+ `,
427
+ },
428
+ });
429
+ }
430
+ catch {
431
+ // Ignore cleanup failure if MCP disconnected due to the repro.
432
+ }
433
+ await server.close();
434
+ }
435
+ }, 30000);
436
+ it('should read cookies via Network.getCookies through page CDP session', async () => {
437
+ const browserContext = getBrowserContext();
438
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
439
+ const server = await createSimpleServer({
440
+ routes: {
441
+ '/': '<!doctype html><html><body>cookies test</body></html>',
442
+ },
443
+ });
444
+ const page = await browserContext.newPage();
445
+ try {
446
+ await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
447
+ await page.bringToFront();
448
+ await serviceWorker.evaluate(async () => {
449
+ await globalThis.toggleExtensionForActiveTab();
450
+ });
451
+ await new Promise((r) => {
452
+ setTimeout(r, 200);
453
+ });
454
+ await page.evaluate(() => {
455
+ document.cookie = 'issue66=ok; path=/';
456
+ });
457
+ const cdpSession = await getCDPSessionForPage({ page });
458
+ const cookiesResult = await cdpSession.send('Network.getCookies', { urls: [page.url()] });
459
+ const cookie = cookiesResult.cookies.find((value) => {
460
+ return value.name === 'issue66';
461
+ });
462
+ expect(cookie?.value).toBe('ok');
463
+ }
464
+ finally {
465
+ await page.close();
466
+ await server.close();
467
+ }
468
+ }, 30000);
469
+ it('should show extension as connected for pages created via newPage()', async () => {
470
+ const browserContext = getBrowserContext();
471
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
472
+ // Create a page via MCP (which uses context.newPage())
473
+ await client.callTool({
474
+ name: 'execute',
475
+ arguments: {
476
+ code: js `
477
+ const newPage = await context.newPage();
478
+ state.testPage = newPage;
479
+ await newPage.goto('https://example.com/mcp-test');
480
+ return newPage.url();
481
+ `,
482
+ },
483
+ });
484
+ // Get extension state to verify the page is marked as connected
485
+ const extensionState = await serviceWorker.evaluate(async () => {
486
+ const state = globalThis.getExtensionState();
487
+ const tabs = await chrome.tabs.query({});
488
+ const testTab = tabs.find((t) => t.url?.includes('mcp-test'));
489
+ return {
490
+ connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
491
+ tabId: testTab?.id,
492
+ tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
493
+ connectionState: state.connectionState,
494
+ };
495
+ });
496
+ expect(extensionState.connected).toBe(true);
497
+ expect(extensionState.tabInfo?.state).toBe('connected');
498
+ expect(extensionState.connectionState).toBe('connected');
499
+ // Clean up
500
+ await client.callTool({
501
+ name: 'execute',
502
+ arguments: {
503
+ code: js `
504
+ if (state.testPage) {
505
+ await state.testPage.close();
506
+ delete state.testPage;
507
+ }
508
+ `,
509
+ },
510
+ });
511
+ }, 30000);
512
+ const snapshotTestCases = [
513
+ {
514
+ name: 'hacker-news',
515
+ url: 'https://news.ycombinator.com/item?id=1',
516
+ expectedContent: ['role=link', 'Hacker News'],
517
+ waitForCode: js `
518
+ await state.page.locator('a[href="news"]').first().waitFor({ timeout: 10000 });
519
+ await state.page.locator('a.hnuser').first().waitFor({ timeout: 10000 });
520
+ `,
521
+ },
522
+ {
523
+ name: 'shadcn-ui',
524
+ url: 'https://ui.shadcn.com/',
525
+ expectedContent: ['shadcn'],
526
+ waitForCode: js `
527
+ await state.page.getByRole('heading', { name: 'The Foundation for your Design System' }).waitFor({ timeout: 10000 });
528
+ `,
529
+ },
530
+ ];
531
+ for (const testCase of snapshotTestCases) {
532
+ it(`should get accessibility snapshot of ${testCase.name}`, async () => {
533
+ await client.callTool({
534
+ name: 'execute',
535
+ arguments: {
536
+ code: js `
537
+ const newPage = await context.newPage();
538
+ state.page = newPage;
539
+ if (!state.pages) state.pages = [];
540
+ state.pages.push(newPage);
541
+ `,
542
+ },
543
+ });
544
+ // Capture interactiveOnly=true snapshot (default)
545
+ const interactiveResult = await client.callTool({
546
+ name: 'execute',
547
+ arguments: {
548
+ code: js `
549
+ // External pages can expose a partial AX tree right after domcontentloaded,
550
+ // so wait for stable page-specific content before snapshotting.
551
+ await state.page.goto('${testCase.url}', { waitUntil: 'domcontentloaded' });
552
+ ${testCase.waitForCode}
553
+ const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: true });
554
+ return snap;
555
+ `,
556
+ timeout: 30000,
557
+ },
558
+ });
559
+ const interactiveData = typeof interactiveResult === 'object' && interactiveResult.content?.[0]?.text
560
+ ? tryJsonParse(interactiveResult.content[0].text)
561
+ : interactiveResult;
562
+ await expect(interactiveData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-interactive.md`);
563
+ expect(interactiveResult.content).toBeDefined();
564
+ for (const expected of testCase.expectedContent) {
565
+ expect(interactiveData).toContain(expected);
566
+ }
567
+ // Capture interactiveOnly=false snapshot (full tree)
568
+ const fullResult = await client.callTool({
569
+ name: 'execute',
570
+ arguments: {
571
+ code: js `
572
+ const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
573
+ return snap;
574
+ `,
575
+ },
576
+ });
577
+ const fullData = typeof fullResult === 'object' && fullResult.content?.[0]?.text
578
+ ? tryJsonParse(fullResult.content[0].text)
579
+ : fullResult;
580
+ await expect(fullData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-full.md`);
581
+ expect(fullResult.content).toBeDefined();
582
+ for (const expected of testCase.expectedContent) {
583
+ expect(fullData).toContain(expected);
584
+ }
585
+ }, 60000);
586
+ }
587
+ it('should close all created pages', async () => {
588
+ const result = await client.callTool({
589
+ name: 'execute',
590
+ arguments: {
591
+ code: js `
592
+ if (state.pages && state.pages.length > 0) {
593
+ for (const page of state.pages) {
594
+ await page.close();
595
+ }
596
+ const closedCount = state.pages.length;
597
+ state.pages = [];
598
+ return { closedCount };
599
+ }
600
+ return { closedCount: 0 };
601
+ `,
602
+ },
603
+ });
604
+ });
605
+ it('should capture browser console logs with getLatestLogs', async () => {
606
+ // Ensure clean state and clear any existing logs
607
+ const resetResult = await client.callTool({
608
+ name: 'execute',
609
+ arguments: {
610
+ code: js `
611
+ // Clear any existing logs from previous tests
612
+ clearAllLogs();
613
+ console.log('Cleared all existing logs');
614
+
615
+ // Verify connection is working
616
+ const pages = context.pages();
617
+ console.log('Current pages count:', pages.length);
618
+
619
+ return { success: true, pagesCount: pages.length };
620
+ `,
621
+ },
622
+ });
623
+ console.log('Cleanup result:', resetResult);
624
+ // Create a new page for this test
625
+ await client.callTool({
626
+ name: 'execute',
627
+ arguments: {
628
+ code: js `
629
+ const newPage = await context.newPage();
630
+ state.testLogPage = newPage;
631
+ await newPage.goto('about:blank');
632
+ `,
633
+ },
634
+ });
635
+ // Generate some console logs in the browser
636
+ await client.callTool({
637
+ name: 'execute',
638
+ arguments: {
639
+ code: js `
640
+ await state.testLogPage.evaluate(() => {
641
+ console.log('Test log 12345');
642
+ console.error('Test error 67890');
643
+ console.warn('Test warning 11111');
644
+ console.log('Test log 2 with', { data: 'object' });
645
+ setTimeout(() => { throw new Error('Test pageerror 22222'); }, 0);
646
+ });
647
+ // Wait for logs to be captured
648
+ await new Promise(resolve => setTimeout(resolve, 100));
649
+ `,
650
+ },
651
+ });
652
+ // Test getting all logs
653
+ const allLogsResult = await client.callTool({
654
+ name: 'execute',
655
+ arguments: {
656
+ code: js `
657
+ const logs = await getLatestLogs();
658
+ logs.forEach(log => console.log(log));
659
+ `,
660
+ },
661
+ });
662
+ const output = allLogsResult.content[0].text;
663
+ expect(output).toContain('[log] Test log 12345');
664
+ expect(output).toContain('[error] Test error 67890');
665
+ expect(output).toContain('[warning] Test warning 11111');
666
+ expect(output).toContain('[pageerror] Test pageerror 22222');
667
+ // Test filtering by search string
668
+ const errorLogsResult = await client.callTool({
669
+ name: 'execute',
670
+ arguments: {
671
+ code: js `
672
+ const logs = await getLatestLogs({ search: 'error' });
673
+ logs.forEach(log => console.log(log));
674
+ `,
675
+ },
676
+ });
677
+ const errorOutput = errorLogsResult.content[0].text;
678
+ expect(errorOutput).toContain('[error] Test error 67890');
679
+ // With context lines (5 above/below), nearby logs are also included
680
+ expect(errorOutput).toContain('[log] Test log 12345');
681
+ // Test that logs persist across page reload
682
+ await client.callTool({
683
+ name: 'execute',
684
+ arguments: {
685
+ code: js `
686
+ // First add a log before reload
687
+ await state.testLogPage.evaluate(() => {
688
+ console.log('Before reload 99999');
689
+ });
690
+ await new Promise(resolve => setTimeout(resolve, 100));
691
+ `,
692
+ },
693
+ });
694
+ // Verify the log exists
695
+ const beforeReloadResult = await client.callTool({
696
+ name: 'execute',
697
+ arguments: {
698
+ code: js `
699
+ const logs = await getLatestLogs({ page: state.testLogPage });
700
+ console.log('Logs before reload:', logs.length);
701
+ logs.forEach(log => console.log(log));
702
+ `,
703
+ },
704
+ });
705
+ const beforeReloadOutput = beforeReloadResult.content[0].text;
706
+ expect(beforeReloadOutput).toContain('[log] Before reload 99999');
707
+ // Reload the page
708
+ await client.callTool({
709
+ name: 'execute',
710
+ arguments: {
711
+ code: js `
712
+ await state.testLogPage.reload();
713
+ await state.testLogPage.evaluate(() => {
714
+ console.log('After reload 88888');
715
+ });
716
+ await new Promise(resolve => setTimeout(resolve, 100));
717
+ `,
718
+ },
719
+ });
720
+ // Check logs after reload - old logs persist (no longer cleared on navigation)
721
+ // so both pre- and post-reload logs are present. Use sinceLastCall to get
722
+ // only new logs if needed.
723
+ const afterReloadResult = await client.callTool({
724
+ name: 'execute',
725
+ arguments: {
726
+ code: js `
727
+ const logs = await getLatestLogs({ page: state.testLogPage });
728
+ console.log('Logs after reload:', logs.length);
729
+ logs.forEach(log => console.log(log));
730
+ `,
731
+ },
732
+ });
733
+ const afterReloadOutput = afterReloadResult.content[0].text;
734
+ expect(afterReloadOutput).toContain('[log] After reload 88888');
735
+ expect(afterReloadOutput).toContain('[log] Before reload 99999');
736
+ // Clean up
737
+ await client.callTool({
738
+ name: 'execute',
739
+ arguments: {
740
+ code: js `
741
+ await state.testLogPage.close();
742
+ delete state.testLogPage;
743
+ `,
744
+ },
745
+ });
746
+ }, 30000);
747
+ it('should keep logs separate between different pages', async () => {
748
+ // Clear any existing logs from previous tests
749
+ await client.callTool({
750
+ name: 'execute',
751
+ arguments: {
752
+ code: js `
753
+ clearAllLogs();
754
+ console.log('Cleared all existing logs for second log test');
755
+ `,
756
+ },
757
+ });
758
+ // Create two pages
759
+ await client.callTool({
760
+ name: 'execute',
761
+ arguments: {
762
+ code: js `
763
+ state.pageA = await context.newPage();
764
+ state.pageB = await context.newPage();
765
+ await state.pageA.goto('about:blank');
766
+ await state.pageB.goto('about:blank');
767
+ `,
768
+ },
769
+ });
770
+ // Generate logs in page A
771
+ await client.callTool({
772
+ name: 'execute',
773
+ arguments: {
774
+ code: js `
775
+ await state.pageA.evaluate(() => {
776
+ console.log('PageA log 11111');
777
+ console.error('PageA error 22222');
778
+ });
779
+ await new Promise(resolve => setTimeout(resolve, 100));
780
+ `,
781
+ },
782
+ });
783
+ // Generate logs in page B
784
+ await client.callTool({
785
+ name: 'execute',
786
+ arguments: {
787
+ code: js `
788
+ await state.pageB.evaluate(() => {
789
+ console.log('PageB log 33333');
790
+ console.error('PageB error 44444');
791
+ });
792
+ await new Promise(resolve => setTimeout(resolve, 100));
793
+ `,
794
+ },
795
+ });
796
+ // Check logs for page A - should only have page A logs
797
+ const pageALogsResult = await client.callTool({
798
+ name: 'execute',
799
+ arguments: {
800
+ code: js `
801
+ const logs = await getLatestLogs({ page: state.pageA });
802
+ console.log('Page A logs:', logs.length);
803
+ logs.forEach(log => console.log(log));
804
+ `,
805
+ },
806
+ });
807
+ const pageAOutput = pageALogsResult.content[0].text;
808
+ expect(pageAOutput).toContain('[log] PageA log 11111');
809
+ expect(pageAOutput).toContain('[error] PageA error 22222');
810
+ expect(pageAOutput).not.toContain('PageB');
811
+ // Check logs for page B - should only have page B logs
812
+ const pageBLogsResult = await client.callTool({
813
+ name: 'execute',
814
+ arguments: {
815
+ code: js `
816
+ const logs = await getLatestLogs({ page: state.pageB });
817
+ console.log('Page B logs:', logs.length);
818
+ logs.forEach(log => console.log(log));
819
+ `,
820
+ },
821
+ });
822
+ const pageBOutput = pageBLogsResult.content[0].text;
823
+ expect(pageBOutput).toContain('[log] PageB log 33333');
824
+ expect(pageBOutput).toContain('[error] PageB error 44444');
825
+ expect(pageBOutput).not.toContain('PageA');
826
+ // Check all logs - should have logs from both pages
827
+ const allLogsResult = await client.callTool({
828
+ name: 'execute',
829
+ arguments: {
830
+ code: js `
831
+ const logs = await getLatestLogs();
832
+ console.log('All logs:', logs.length);
833
+ logs.forEach(log => console.log(log));
834
+ `,
835
+ },
836
+ });
837
+ const allOutput = allLogsResult.content[0].text;
838
+ expect(allOutput).toContain('[log] PageA log 11111');
839
+ expect(allOutput).toContain('[log] PageB log 33333');
840
+ // Test that reloading page A preserves logs (no longer cleared on navigation)
841
+ await client.callTool({
842
+ name: 'execute',
843
+ arguments: {
844
+ code: js `
845
+ await state.pageA.reload();
846
+ await state.pageA.evaluate(() => {
847
+ console.log('PageA after reload 55555');
848
+ });
849
+ await new Promise(resolve => setTimeout(resolve, 100));
850
+ `,
851
+ },
852
+ });
853
+ // Check page A logs - logs persist across navigation, so both old and new are present
854
+ const pageAAfterReloadResult = await client.callTool({
855
+ name: 'execute',
856
+ arguments: {
857
+ code: js `
858
+ const logs = await getLatestLogs({ page: state.pageA });
859
+ console.log('Page A logs after reload:', logs.length);
860
+ logs.forEach(log => console.log(log));
861
+ `,
862
+ },
863
+ });
864
+ const pageAAfterReloadOutput = pageAAfterReloadResult.content[0].text;
865
+ expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555');
866
+ expect(pageAAfterReloadOutput).toContain('[log] PageA log 11111');
867
+ // Check page B logs - should still have original logs
868
+ const pageBAfterAReloadResult = await client.callTool({
869
+ name: 'execute',
870
+ arguments: {
871
+ code: js `
872
+ const logs = await getLatestLogs({ page: state.pageB });
873
+ console.log('Page B logs after A reload:', logs.length);
874
+ logs.forEach(log => console.log(log));
875
+ `,
876
+ },
877
+ });
878
+ const pageBAfterAReloadOutput = pageBAfterAReloadResult.content[0].text;
879
+ expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333');
880
+ expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444');
881
+ // Test that logs are deleted when page is closed
882
+ await client.callTool({
883
+ name: 'execute',
884
+ arguments: {
885
+ code: js `
886
+ // Close page A
887
+ await state.pageA.close();
888
+ await new Promise(resolve => setTimeout(resolve, 100));
889
+ `,
890
+ },
891
+ });
892
+ // Check all logs - page A logs should be gone
893
+ const logsAfterCloseResult = await client.callTool({
894
+ name: 'execute',
895
+ arguments: {
896
+ code: js `
897
+ const logs = await getLatestLogs();
898
+ console.log('All logs after closing page A:', logs.length);
899
+ logs.forEach(log => console.log(log));
900
+ `,
901
+ },
902
+ });
903
+ const logsAfterCloseOutput = logsAfterCloseResult.content[0].text;
904
+ expect(logsAfterCloseOutput).not.toContain('PageA');
905
+ expect(logsAfterCloseOutput).toContain('[log] PageB log 33333');
906
+ // Clean up remaining page
907
+ await client.callTool({
908
+ name: 'execute',
909
+ arguments: {
910
+ code: js `
911
+ await state.pageB.close();
912
+ delete state.pageA;
913
+ delete state.pageB;
914
+ `,
915
+ },
916
+ });
917
+ }, 30000);
918
+ it('should capture console logs from cross-origin iframes', async () => {
919
+ // Two servers on different ports = different origins
920
+ const iframeServer = await createSimpleServer({
921
+ routes: {
922
+ '/iframe.html': `<!doctype html><html><body>
923
+ <script>
924
+ console.log('iframe-log-ALPHA');
925
+ console.error('iframe-error-BETA');
926
+ console.warn('iframe-warn-GAMMA');
927
+ </script>
928
+ <p>cross-origin iframe</p>
929
+ </body></html>`,
930
+ },
931
+ });
932
+ const parentServer = await createSimpleServer({
933
+ routes: {
934
+ '/': `<!doctype html><html><body>
935
+ <script>console.log('parent-log-DELTA');</script>
936
+ <iframe src="${iframeServer.baseUrl}/iframe.html"></iframe>
937
+ </body></html>`,
938
+ },
939
+ });
940
+ try {
941
+ // Clear logs and navigate to the parent page with cross-origin iframe
942
+ await client.callTool({
943
+ name: 'execute',
944
+ arguments: {
945
+ code: js `
946
+ clearAllLogs();
947
+ state.iframePage = await context.newPage();
948
+ await state.iframePage.goto('${parentServer.baseUrl}', { waitUntil: 'networkidle' });
949
+ // Wait for iframe to load and logs to be captured
950
+ await state.iframePage.frameLocator('iframe').locator('p').waitFor({ timeout: 5000 });
951
+ await new Promise(resolve => setTimeout(resolve, 500));
952
+ `,
953
+ },
954
+ });
955
+ // Retrieve logs and verify both parent and iframe logs are captured
956
+ const logsResult = await client.callTool({
957
+ name: 'execute',
958
+ arguments: {
959
+ code: js `
960
+ const logs = await getLatestLogs({ page: state.iframePage });
961
+ console.log('Cross-origin iframe logs count:', logs.length);
962
+ logs.forEach(log => console.log(log));
963
+ `,
964
+ },
965
+ });
966
+ const output = logsResult.content[0].text;
967
+ // Parent page log
968
+ expect(output).toContain('parent-log-DELTA');
969
+ // Cross-origin iframe logs
970
+ expect(output).toContain('iframe-log-ALPHA');
971
+ expect(output).toContain('iframe-error-BETA');
972
+ expect(output).toContain('iframe-warn-GAMMA');
973
+ // Clean up
974
+ await client.callTool({
975
+ name: 'execute',
976
+ arguments: {
977
+ code: js `
978
+ await state.iframePage.close();
979
+ delete state.iframePage;
980
+ `,
981
+ },
982
+ });
983
+ }
984
+ finally {
985
+ await Promise.all([parentServer.close(), iframeServer.close()]);
986
+ }
987
+ }, 60000);
988
+ it('should preserve system color scheme instead of forcing light mode', async () => {
989
+ const browserContext = getBrowserContext();
990
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
991
+ const page = await browserContext.newPage();
992
+ await page.goto('https://example.com');
993
+ await page.bringToFront();
994
+ // test-utils launches with colorScheme: 'dark', so before MCP connection
995
+ // the browser should report dark mode
996
+ const colorSchemeBefore = await page.evaluate(() => {
997
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
998
+ });
999
+ expect(colorSchemeBefore).toBe('dark');
1000
+ await serviceWorker.evaluate(async () => {
1001
+ await globalThis.toggleExtensionForActiveTab();
1002
+ });
1003
+ await new Promise((r) => setTimeout(r, 500));
1004
+ const result = await client.callTool({
1005
+ name: 'execute',
1006
+ arguments: {
1007
+ code: js `
1008
+ const pages = context.pages();
1009
+ const urls = pages.map(p => p.url());
1010
+ const targetPage = pages.find(p => p.url().includes('example.com'));
1011
+ if (!targetPage) {
1012
+ return { error: 'Page not found', urls };
1013
+ }
1014
+ const isDark = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
1015
+ const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
1016
+ return { matchesDark: isDark, matchesLight: isLight };
1017
+ `,
1018
+ },
1019
+ });
1020
+ console.log('Color scheme after MCP connection:', result.content);
1021
+ // After MCP connection, color scheme should NOT be forced to light.
1022
+ // The page.ts default is now 'no-override', so the browser's actual
1023
+ // color scheme (dark, from test-utils launch config) should be preserved.
1024
+ expect(result.content).toMatchInlineSnapshot(`
1025
+ [
1026
+ {
1027
+ "text": "[return value] { matchesDark: true, matchesLight: false }",
1028
+ "type": "text",
1029
+ },
1030
+ ]
1031
+ `);
1032
+ await page.close();
1033
+ }, 60000);
1034
+ it('should get clean HTML with getCleanHTML', async () => {
1035
+ const browserContext = getBrowserContext();
1036
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1037
+ const page = await browserContext.newPage();
1038
+ await page.setContent(`
1039
+ <html>
1040
+ <head>
1041
+ <style>.hidden { display: none; }</style>
1042
+ <script>console.log('test')</script>
1043
+ </head>
1044
+ <body>
1045
+ <div class="container" data-testid="main">
1046
+ <h1>Hello World</h1>
1047
+ <button id="btn" aria-label="Click me">Submit</button>
1048
+ <a href="/about" title="About page">About</a>
1049
+ <input type="text" placeholder="Enter name" />
1050
+ </div>
1051
+ </body>
1052
+ </html>
1053
+ `);
1054
+ await page.bringToFront();
1055
+ await serviceWorker.evaluate(async () => {
1056
+ await globalThis.toggleExtensionForActiveTab();
1057
+ });
1058
+ await new Promise((r) => setTimeout(r, 400));
1059
+ // Test basic getCleanHTML
1060
+ const result = await client.callTool({
1061
+ name: 'execute',
1062
+ arguments: {
1063
+ code: js `
1064
+ let testPage;
1065
+ for (const p of context.pages()) {
1066
+ const html = await p.content();
1067
+ if (html.includes('Hello World')) { testPage = p; break; }
1068
+ }
1069
+ if (!testPage) throw new Error('Test page not found');
1070
+ const html = await getCleanHTML({ locator: testPage.locator('body') });
1071
+ return html;
1072
+ `,
1073
+ timeout: 15000,
1074
+ },
1075
+ });
1076
+ expect(result.isError).toBeFalsy();
1077
+ const text = result.content[0]?.text || '';
1078
+ // Inline snapshot of cleaned HTML
1079
+ expect(text).toMatchInlineSnapshot(`
1080
+ "[return value] <div data-testid="main">
1081
+ <h1>Hello World</h1>
1082
+ <button aria-label="Click me">Submit</button>
1083
+ <a href="/about" title="About page">About</a>
1084
+ <input type="text" placeholder="Enter name">
1085
+ </div>"
1086
+ `);
1087
+ // Should NOT contain script/style tags (they're removed)
1088
+ expect(text).not.toContain('<script');
1089
+ expect(text).not.toContain('<style');
1090
+ expect(text).not.toContain('console.log');
1091
+ // Test search functionality
1092
+ const searchResult = await client.callTool({
1093
+ name: 'execute',
1094
+ arguments: {
1095
+ code: js `
1096
+ let testPage;
1097
+ for (const p of context.pages()) {
1098
+ const html = await p.content();
1099
+ if (html.includes('Hello World')) { testPage = p; break; }
1100
+ }
1101
+ if (!testPage) throw new Error('Test page not found');
1102
+ const html = await getCleanHTML({ locator: testPage, search: /button/i });
1103
+ return html;
1104
+ `,
1105
+ timeout: 15000,
1106
+ },
1107
+ });
1108
+ expect(searchResult.isError).toBeFalsy();
1109
+ const searchText = searchResult.content[0]?.text || '';
1110
+ expect(searchText).toContain('button');
1111
+ await page.close();
1112
+ }, 60000);
1113
+ it('should extract page content as markdown with getPageMarkdown', async () => {
1114
+ const browserContext = getBrowserContext();
1115
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1116
+ const page = await browserContext.newPage();
1117
+ // Create a realistic article-like page structure
1118
+ await page.setContent(`
1119
+ <html>
1120
+ <head>
1121
+ <title>Test Article Title</title>
1122
+ <meta name="author" content="John Doe">
1123
+ <script>console.log('analytics')</script>
1124
+ <style>.nav { background: blue; }</style>
1125
+ </head>
1126
+ <body>
1127
+ <nav class="nav">
1128
+ <a href="/">Home</a>
1129
+ <a href="/about">About</a>
1130
+ </nav>
1131
+ <article>
1132
+ <h1>Test Article Title</h1>
1133
+ <p>This is the first paragraph of the article content.</p>
1134
+ <p>This is the second paragraph with more details about the topic.</p>
1135
+ <p>The article continues with important information here.</p>
1136
+ </article>
1137
+ <aside>
1138
+ <h3>Related Posts</h3>
1139
+ <ul><li>Post 1</li><li>Post 2</li></ul>
1140
+ </aside>
1141
+ <footer>Copyright 2024</footer>
1142
+ </body>
1143
+ </html>
1144
+ `);
1145
+ await page.bringToFront();
1146
+ await serviceWorker.evaluate(async () => {
1147
+ await globalThis.toggleExtensionForActiveTab();
1148
+ });
1149
+ await new Promise((r) => setTimeout(r, 400));
1150
+ // Test basic getPageMarkdown
1151
+ const result = await client.callTool({
1152
+ name: 'execute',
1153
+ arguments: {
1154
+ code: js `
1155
+ let testPage;
1156
+ for (const p of context.pages()) {
1157
+ const html = await p.content();
1158
+ if (html.includes('Test Article Title')) { testPage = p; break; }
1159
+ }
1160
+ if (!testPage) throw new Error('Test page not found');
1161
+ const content = await getPageMarkdown({ page: testPage });
1162
+ console.log(content);
1163
+ `,
1164
+ timeout: 15000,
1165
+ },
1166
+ });
1167
+ expect(result.isError).toBeFalsy();
1168
+ const text = result.content[0]?.text || '';
1169
+ // Snapshot the full output
1170
+ await expect(text).toMatchFileSnapshot('./snapshots/page-markdown-output.txt');
1171
+ // Should contain article content
1172
+ expect(text).toContain('Test Article Title');
1173
+ expect(text).toContain('first paragraph');
1174
+ expect(text).toContain('second paragraph');
1175
+ // Should NOT contain script/style content
1176
+ expect(text).not.toContain('analytics');
1177
+ expect(text).not.toContain('background: blue');
1178
+ // Test search functionality
1179
+ const searchResult = await client.callTool({
1180
+ name: 'execute',
1181
+ arguments: {
1182
+ code: js `
1183
+ let testPage;
1184
+ for (const p of context.pages()) {
1185
+ const html = await p.content();
1186
+ if (html.includes('Test Article Title')) { testPage = p; break; }
1187
+ }
1188
+ if (!testPage) throw new Error('Test page not found');
1189
+ const content = await getPageMarkdown({ page: testPage, search: /important/i, showDiffSinceLastCall: false });
1190
+ return content;
1191
+ `,
1192
+ timeout: 15000,
1193
+ },
1194
+ });
1195
+ expect(searchResult.isError).toBeFalsy();
1196
+ const searchText = searchResult.content[0]?.text || '';
1197
+ expect(searchText).toContain('important');
1198
+ await page.close();
1199
+ }, 60000);
1200
+ it('should handle default page being closed and switch to another available page', async () => {
1201
+ // This test verifies that when the default `page` in MCP scope is closed,
1202
+ // the MCP automatically switches to another available page instead of failing
1203
+ // with cryptic "page closed" errors.
1204
+ const browserContext = getBrowserContext();
1205
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1206
+ // 1. Disconnect everything to start fresh
1207
+ await serviceWorker.evaluate(async () => {
1208
+ await globalThis.disconnectEverything();
1209
+ });
1210
+ await new Promise((r) => setTimeout(r, 100));
1211
+ // 2. Create first page and enable extension
1212
+ const page1 = await browserContext.newPage();
1213
+ await page1.goto('https://example.com/first-page');
1214
+ await page1.bringToFront();
1215
+ await serviceWorker.evaluate(async () => {
1216
+ await globalThis.toggleExtensionForActiveTab();
1217
+ });
1218
+ await new Promise((r) => setTimeout(r, 100));
1219
+ // 3. Reset MCP to ensure page1 becomes the default page (only page available)
1220
+ const resetResult = await client.callTool({
1221
+ name: 'reset',
1222
+ arguments: {},
1223
+ });
1224
+ expect(resetResult.content[0].text).toContain('Connection reset successfully');
1225
+ // 4. Verify initial page is accessible via default `page`
1226
+ const initialResult = await client.callTool({
1227
+ name: 'execute',
1228
+ arguments: {
1229
+ code: js `
1230
+ const url = page.url();
1231
+ console.log('Initial page URL:', url);
1232
+ return { url };
1233
+ `,
1234
+ },
1235
+ });
1236
+ expect(initialResult.content[0].text).toContain('first-page');
1237
+ // 5. Create second page and enable extension
1238
+ const page2 = await browserContext.newPage();
1239
+ await page2.goto('https://example.com/second-page');
1240
+ await page2.bringToFront();
1241
+ await serviceWorker.evaluate(async () => {
1242
+ await globalThis.toggleExtensionForActiveTab();
1243
+ });
1244
+ await new Promise((r) => setTimeout(r, 100));
1245
+ // 6. Close the first page (which is the default `page` in MCP scope)
1246
+ await page1.close();
1247
+ await new Promise((r) => setTimeout(r, 100));
1248
+ // 7. Execute code via MCP - should NOT fail with "page closed" error
1249
+ // Instead, it should automatically switch to the second page
1250
+ const afterCloseResult = await client.callTool({
1251
+ name: 'execute',
1252
+ arguments: {
1253
+ code: js `
1254
+ const url = page.url();
1255
+ console.log('Page URL after close:', url);
1256
+ const title = await page.title();
1257
+ return { url, title };
1258
+ `,
1259
+ },
1260
+ });
1261
+ // Should succeed and return the second page's info
1262
+ expect(afterCloseResult.isError).toBeFalsy();
1263
+ const output = afterCloseResult.content[0].text;
1264
+ expect(output).toContain('second-page');
1265
+ expect(output).not.toContain('page closed');
1266
+ expect(output).not.toContain('Target closed');
1267
+ // Cleanup
1268
+ await page2.close();
1269
+ }, 60000);
1270
+ it('should show descriptive error when clicking a hidden element', async () => {
1271
+ await ensureConnectedTabForExecute();
1272
+ // Create a fresh page and set content with a collapsed details element
1273
+ await client.callTool({
1274
+ name: 'execute',
1275
+ arguments: {
1276
+ code: js `
1277
+ state.errorTestPage = await context.newPage();
1278
+ await state.errorTestPage.setContent(\`
1279
+ <details>
1280
+ <summary>Toggle</summary>
1281
+ <button id="hidden-btn">Hidden Button</button>
1282
+ </details>
1283
+ \`);
1284
+ `,
1285
+ },
1286
+ });
1287
+ const result = await client.callTool({
1288
+ name: 'execute',
1289
+ arguments: {
1290
+ code: js `
1291
+ await state.errorTestPage.click('#hidden-btn', { timeout: 100 });
1292
+ `,
1293
+ },
1294
+ });
1295
+ expect(result).toMatchInlineSnapshot(`
1296
+ {
1297
+ "content": [
1298
+ {
1299
+ "text": "
1300
+ Error executing code: page.click: Timeout 100ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks
1301
+ Call log:
1302
+  - waiting for locator('#hidden-btn')
1303
+  - locator resolved to <button id="hidden-btn">Hidden Button</button>
1304
+  - attempting click action
1305
+  2 × waiting for element to be visible, enabled and stable
1306
+  - element is not visible
1307
+  - retrying click action
1308
+  - waiting 20ms
1309
+  - waiting for element to be visible, enabled and stable
1310
+  - element is not visible
1311
+  - retrying click action
1312
+  - waiting 100ms
1313
+ ",
1314
+ "type": "text",
1315
+ },
1316
+ ],
1317
+ "isError": true,
1318
+ }
1319
+ `);
1320
+ // Cleanup
1321
+ await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
1322
+ }, 30000);
1323
+ it('should show descriptive error when clicking an element covered by another', async () => {
1324
+ await ensureConnectedTabForExecute();
1325
+ await client.callTool({
1326
+ name: 'execute',
1327
+ arguments: {
1328
+ code: js `
1329
+ state.errorTestPage = await context.newPage();
1330
+ await state.errorTestPage.setContent(\`
1331
+ <div style="position:relative">
1332
+ <button id="covered-btn" style="position:absolute;top:0;left:0">Covered</button>
1333
+ <div id="overlay" style="position:absolute;top:0;left:0;width:200px;height:200px;background:red;z-index:10">Overlay</div>
1334
+ </div>
1335
+ \`);
1336
+ `,
1337
+ },
1338
+ });
1339
+ const result = await client.callTool({
1340
+ name: 'execute',
1341
+ arguments: {
1342
+ code: js `
1343
+ await state.errorTestPage.click('#covered-btn', { timeout: 100 });
1344
+ `,
1345
+ },
1346
+ });
1347
+ expect(result).toMatchInlineSnapshot(`
1348
+ {
1349
+ "content": [
1350
+ {
1351
+ "text": "
1352
+ Error executing code: page.click: Timeout 100ms exceeded. <div id="overlay">Overlay</div> intercepts pointer events
1353
+ Call log:
1354
+  - waiting for locator('#covered-btn')
1355
+  - locator resolved to <button id="covered-btn">Covered</button>
1356
+  - attempting click action
1357
+  2 × waiting for element to be visible, enabled and stable
1358
+  - element is visible, enabled and stable
1359
+  - scrolling into view if needed
1360
+  - done scrolling
1361
+  - <div id="overlay">Overlay</div> intercepts pointer events
1362
+  - retrying click action
1363
+  - waiting 20ms
1364
+  - waiting for element to be visible, enabled and stable
1365
+  - element is visible, enabled and stable
1366
+  - scrolling into view if needed
1367
+  - done scrolling
1368
+  - <div id="overlay">Overlay</div> intercepts pointer events
1369
+  - retrying click action
1370
+  - waiting 100ms
1371
+ ",
1372
+ "type": "text",
1373
+ },
1374
+ ],
1375
+ "isError": true,
1376
+ }
1377
+ `);
1378
+ await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
1379
+ }, 30000);
1380
+ it('should show descriptive error when clicking a display:none element', async () => {
1381
+ await ensureConnectedTabForExecute();
1382
+ await client.callTool({
1383
+ name: 'execute',
1384
+ arguments: {
1385
+ code: js `
1386
+ state.errorTestPage = await context.newPage();
1387
+ await state.errorTestPage.setContent('<button id="invisible" style="display:none">Invisible</button>');
1388
+ `,
1389
+ },
1390
+ });
1391
+ const result = await client.callTool({
1392
+ name: 'execute',
1393
+ arguments: {
1394
+ code: js `
1395
+ await state.errorTestPage.click('#invisible', { timeout: 100 });
1396
+ `,
1397
+ },
1398
+ });
1399
+ expect(result).toMatchInlineSnapshot(`
1400
+ {
1401
+ "content": [
1402
+ {
1403
+ "text": "
1404
+ Error executing code: page.click: Timeout 100ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks
1405
+ Call log:
1406
+  - waiting for locator('#invisible')
1407
+  - locator resolved to <button id="invisible">Invisible</button>
1408
+  - attempting click action
1409
+  2 × waiting for element to be visible, enabled and stable
1410
+  - element is not visible
1411
+  - retrying click action
1412
+  - waiting 20ms
1413
+  - waiting for element to be visible, enabled and stable
1414
+  - element is not visible
1415
+  - retrying click action
1416
+  - waiting 100ms
1417
+ ",
1418
+ "type": "text",
1419
+ },
1420
+ ],
1421
+ "isError": true,
1422
+ }
1423
+ `);
1424
+ await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
1425
+ }, 30000);
1426
+ });
1427
+ //# sourceMappingURL=relay-core.test.js.map