@superblocksteam/vite-plugin-file-sync 2.0.72-next.11 → 2.0.72-next.14

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 (284) hide show
  1. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts +2 -1
  2. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  3. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +18 -2
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  5. package/dist/ai-service/agent/subagents/testing/index.d.ts +3 -0
  6. package/dist/ai-service/agent/subagents/testing/index.d.ts.map +1 -0
  7. package/dist/ai-service/agent/subagents/testing/index.js +2 -0
  8. package/dist/ai-service/agent/subagents/testing/index.js.map +1 -0
  9. package/dist/ai-service/agent/subagents/testing/prompt-builder.d.ts +10 -0
  10. package/dist/ai-service/agent/subagents/testing/prompt-builder.d.ts.map +1 -0
  11. package/dist/ai-service/agent/subagents/testing/prompt-builder.js +162 -0
  12. package/dist/ai-service/agent/subagents/testing/prompt-builder.js.map +1 -0
  13. package/dist/ai-service/agent/subagents/testing/types.d.ts +67 -0
  14. package/dist/ai-service/agent/subagents/testing/types.d.ts.map +1 -0
  15. package/dist/ai-service/agent/subagents/testing/types.js +2 -0
  16. package/dist/ai-service/agent/subagents/testing/types.js.map +1 -0
  17. package/dist/ai-service/agent/subagents/types.d.ts +9 -8
  18. package/dist/ai-service/agent/subagents/types.d.ts.map +1 -1
  19. package/dist/ai-service/agent/subagents/types.js +9 -9
  20. package/dist/ai-service/agent/subagents/types.js.map +1 -1
  21. package/dist/ai-service/agent/tool-message-utils.d.ts +7 -2
  22. package/dist/ai-service/agent/tool-message-utils.d.ts.map +1 -1
  23. package/dist/ai-service/agent/tool-message-utils.js +21 -2
  24. package/dist/ai-service/agent/tool-message-utils.js.map +1 -1
  25. package/dist/ai-service/agent/tools/apis/api-source.d.ts +1 -1
  26. package/dist/ai-service/agent/tools/apis/api-source.d.ts.map +1 -1
  27. package/dist/ai-service/agent/tools/apis/api-source.js +37 -9
  28. package/dist/ai-service/agent/tools/apis/api-source.js.map +1 -1
  29. package/dist/ai-service/agent/tools/apis/test-api.js +1 -1
  30. package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
  31. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts +1 -0
  32. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
  33. package/dist/ai-service/agent/tools/build-capture-screenshot.js +4 -2
  34. package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
  35. package/dist/ai-service/agent/tools/build-delete-file.d.ts +1 -1
  36. package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +1 -1
  37. package/dist/ai-service/agent/tools/{build-read-files.d.ts → build-read-file.d.ts} +10 -6
  38. package/dist/ai-service/agent/tools/build-read-file.d.ts.map +1 -0
  39. package/dist/ai-service/agent/tools/build-read-file.js +139 -0
  40. package/dist/ai-service/agent/tools/build-read-file.js.map +1 -0
  41. package/dist/ai-service/agent/tools/build-reload-file.d.ts +9 -3
  42. package/dist/ai-service/agent/tools/build-reload-file.d.ts.map +1 -1
  43. package/dist/ai-service/agent/tools/build-reload-file.js +20 -9
  44. package/dist/ai-service/agent/tools/build-reload-file.js.map +1 -1
  45. package/dist/ai-service/agent/tools/get-console-logs.js +1 -1
  46. package/dist/ai-service/agent/tools/get-console-logs.js.map +1 -1
  47. package/dist/ai-service/agent/tools/get-runtime-errors.js +1 -1
  48. package/dist/ai-service/agent/tools/get-runtime-errors.js.map +1 -1
  49. package/dist/ai-service/agent/tools/index.d.ts +5 -1
  50. package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
  51. package/dist/ai-service/agent/tools/index.js +5 -1
  52. package/dist/ai-service/agent/tools/index.js.map +1 -1
  53. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +1 -1
  54. package/dist/ai-service/agent/tools/integrations/internal.d.ts.map +1 -1
  55. package/dist/ai-service/agent/tools/integrations/internal.js +8 -1
  56. package/dist/ai-service/agent/tools/integrations/internal.js.map +1 -1
  57. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  58. package/dist/ai-service/agent/tools.js +124 -31
  59. package/dist/ai-service/agent/tools.js.map +1 -1
  60. package/dist/ai-service/agent/tools2/access-control.d.ts +23 -1
  61. package/dist/ai-service/agent/tools2/access-control.d.ts.map +1 -1
  62. package/dist/ai-service/agent/tools2/access-control.js +67 -1
  63. package/dist/ai-service/agent/tools2/access-control.js.map +1 -1
  64. package/dist/ai-service/agent/tools2/entity-permissions.d.ts +26 -0
  65. package/dist/ai-service/agent/tools2/entity-permissions.d.ts.map +1 -1
  66. package/dist/ai-service/agent/tools2/entity-permissions.js +15 -0
  67. package/dist/ai-service/agent/tools2/entity-permissions.js.map +1 -1
  68. package/dist/ai-service/agent/tools2/example.d.ts.map +1 -1
  69. package/dist/ai-service/agent/tools2/example.js +2 -4
  70. package/dist/ai-service/agent/tools2/example.js.map +1 -1
  71. package/dist/ai-service/agent/tools2/index.d.ts +1 -1
  72. package/dist/ai-service/agent/tools2/index.d.ts.map +1 -1
  73. package/dist/ai-service/agent/tools2/index.js +1 -1
  74. package/dist/ai-service/agent/tools2/index.js.map +1 -1
  75. package/dist/ai-service/agent/tools2/registry.d.ts.map +1 -1
  76. package/dist/ai-service/agent/tools2/registry.js +37 -23
  77. package/dist/ai-service/agent/tools2/registry.js.map +1 -1
  78. package/dist/ai-service/agent/tools2/tools/ask-multi-choice.d.ts +0 -1
  79. package/dist/ai-service/agent/tools2/tools/ask-multi-choice.d.ts.map +1 -1
  80. package/dist/ai-service/agent/tools2/tools/ask-multi-choice.js +1 -6
  81. package/dist/ai-service/agent/tools2/tools/ask-multi-choice.js.map +1 -1
  82. package/dist/ai-service/agent/tools2/tools/end-test-run.d.ts +31 -0
  83. package/dist/ai-service/agent/tools2/tools/end-test-run.d.ts.map +1 -0
  84. package/dist/ai-service/agent/tools2/tools/end-test-run.js +107 -0
  85. package/dist/ai-service/agent/tools2/tools/end-test-run.js.map +1 -0
  86. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts +2 -1
  87. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
  88. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +63 -76
  89. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
  90. package/dist/ai-service/agent/tools2/tools/list-attachments.d.ts +15 -0
  91. package/dist/ai-service/agent/tools2/tools/list-attachments.d.ts.map +1 -0
  92. package/dist/ai-service/agent/tools2/tools/list-attachments.js +47 -0
  93. package/dist/ai-service/agent/tools2/tools/list-attachments.js.map +1 -0
  94. package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts +24 -0
  95. package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts.map +1 -0
  96. package/dist/ai-service/agent/tools2/tools/start-test-run.js +340 -0
  97. package/dist/ai-service/agent/tools2/tools/start-test-run.js.map +1 -0
  98. package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts +29 -0
  99. package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts.map +1 -0
  100. package/dist/ai-service/agent/tools2/tools/update-test-case-status.js +106 -0
  101. package/dist/ai-service/agent/tools2/tools/update-test-case-status.js.map +1 -0
  102. package/dist/ai-service/agent/tools2/types.d.ts +6 -24
  103. package/dist/ai-service/agent/tools2/types.d.ts.map +1 -1
  104. package/dist/ai-service/agent/tools2/types.js +4 -15
  105. package/dist/ai-service/agent/tools2/types.js.map +1 -1
  106. package/dist/ai-service/agent/utils.d.ts +10 -0
  107. package/dist/ai-service/agent/utils.d.ts.map +1 -1
  108. package/dist/ai-service/agent/utils.js +160 -2
  109. package/dist/ai-service/agent/utils.js.map +1 -1
  110. package/dist/ai-service/attachments/index.d.ts +2 -0
  111. package/dist/ai-service/attachments/index.d.ts.map +1 -0
  112. package/dist/ai-service/attachments/index.js +2 -0
  113. package/dist/ai-service/attachments/index.js.map +1 -0
  114. package/dist/ai-service/attachments/store.d.ts +65 -0
  115. package/dist/ai-service/attachments/store.d.ts.map +1 -0
  116. package/dist/ai-service/attachments/store.js +158 -0
  117. package/dist/ai-service/attachments/store.js.map +1 -0
  118. package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
  119. package/dist/ai-service/chat/chat-session-store.js +133 -1
  120. package/dist/ai-service/chat/chat-session-store.js.map +1 -1
  121. package/dist/ai-service/context-download.d.ts +24 -0
  122. package/dist/ai-service/context-download.d.ts.map +1 -0
  123. package/dist/ai-service/context-download.js +127 -0
  124. package/dist/ai-service/context-download.js.map +1 -0
  125. package/dist/ai-service/context-upload.d.ts +17 -0
  126. package/dist/ai-service/context-upload.d.ts.map +1 -0
  127. package/dist/ai-service/context-upload.js +100 -0
  128. package/dist/ai-service/context-upload.js.map +1 -0
  129. package/dist/ai-service/features.d.ts +4 -0
  130. package/dist/ai-service/features.d.ts.map +1 -1
  131. package/dist/ai-service/features.js +4 -0
  132. package/dist/ai-service/features.js.map +1 -1
  133. package/dist/ai-service/index.d.ts +27 -3
  134. package/dist/ai-service/index.d.ts.map +1 -1
  135. package/dist/ai-service/index.js +259 -20
  136. package/dist/ai-service/index.js.map +1 -1
  137. package/dist/ai-service/judge/integration/mcp-client.d.ts +3 -6
  138. package/dist/ai-service/judge/integration/mcp-client.d.ts.map +1 -1
  139. package/dist/ai-service/judge/integration/mcp-client.js.map +1 -1
  140. package/dist/ai-service/judge/tools/playwright-action.d.ts +1 -1
  141. package/dist/ai-service/judge/tools/submit-feedback.d.ts +1 -1
  142. package/dist/ai-service/llm/client.d.ts +6 -0
  143. package/dist/ai-service/llm/client.d.ts.map +1 -1
  144. package/dist/ai-service/llm/client.js +9 -0
  145. package/dist/ai-service/llm/client.js.map +1 -1
  146. package/dist/ai-service/llm/context/constants.d.ts +8 -0
  147. package/dist/ai-service/llm/context/constants.d.ts.map +1 -1
  148. package/dist/ai-service/llm/context/constants.js +8 -0
  149. package/dist/ai-service/llm/context/constants.js.map +1 -1
  150. package/dist/ai-service/llm/context/context.d.ts +4 -0
  151. package/dist/ai-service/llm/context/context.d.ts.map +1 -1
  152. package/dist/ai-service/llm/context/context.js +22 -9
  153. package/dist/ai-service/llm/context/context.js.map +1 -1
  154. package/dist/ai-service/llm/context/manager.d.ts +6 -1
  155. package/dist/ai-service/llm/context/manager.d.ts.map +1 -1
  156. package/dist/ai-service/llm/context/manager.js +9 -1
  157. package/dist/ai-service/llm/context/manager.js.map +1 -1
  158. package/dist/ai-service/llm/context/serialization.d.ts +3 -0
  159. package/dist/ai-service/llm/context/serialization.d.ts.map +1 -1
  160. package/dist/ai-service/llm/context/utils/message-utils.d.ts +10 -0
  161. package/dist/ai-service/llm/context/utils/message-utils.d.ts.map +1 -1
  162. package/dist/ai-service/llm/context/utils/message-utils.js +92 -0
  163. package/dist/ai-service/llm/context/utils/message-utils.js.map +1 -1
  164. package/dist/ai-service/llm/interaction/provider.d.ts +1 -0
  165. package/dist/ai-service/llm/interaction/provider.d.ts.map +1 -1
  166. package/dist/ai-service/llm/stream/config.d.ts +2 -0
  167. package/dist/ai-service/llm/stream/config.d.ts.map +1 -1
  168. package/dist/ai-service/llm/stream/config.js +1 -0
  169. package/dist/ai-service/llm/stream/config.js.map +1 -1
  170. package/dist/ai-service/llm/stream/event-bus.d.ts +5 -0
  171. package/dist/ai-service/llm/stream/event-bus.d.ts.map +1 -1
  172. package/dist/ai-service/llm/stream/event-bus.js.map +1 -1
  173. package/dist/ai-service/llm/stream/observers/llmobs.d.ts +4 -1
  174. package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
  175. package/dist/ai-service/llm/stream/observers/llmobs.js +194 -10
  176. package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
  177. package/dist/ai-service/llm/stream/observers/logging.d.ts +1 -0
  178. package/dist/ai-service/llm/stream/observers/logging.d.ts.map +1 -1
  179. package/dist/ai-service/llm/stream/observers/logging.js +92 -20
  180. package/dist/ai-service/llm/stream/observers/logging.js.map +1 -1
  181. package/dist/ai-service/llm/stream/orchestrator.d.ts +7 -1
  182. package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
  183. package/dist/ai-service/llm/stream/orchestrator.js +23 -3
  184. package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
  185. package/dist/ai-service/llm/stream/retry-engine.d.ts +1 -0
  186. package/dist/ai-service/llm/stream/retry-engine.d.ts.map +1 -1
  187. package/dist/ai-service/llm/stream/retry-engine.js +20 -1
  188. package/dist/ai-service/llm/stream/retry-engine.js.map +1 -1
  189. package/dist/ai-service/llm/stream/session.d.ts +14 -2
  190. package/dist/ai-service/llm/stream/session.d.ts.map +1 -1
  191. package/dist/ai-service/llm/stream/session.js +15 -28
  192. package/dist/ai-service/llm/stream/session.js.map +1 -1
  193. package/dist/ai-service/llmobs/otel-exporter.d.ts +58 -0
  194. package/dist/ai-service/llmobs/otel-exporter.d.ts.map +1 -0
  195. package/dist/ai-service/llmobs/otel-exporter.js +182 -0
  196. package/dist/ai-service/llmobs/otel-exporter.js.map +1 -0
  197. package/dist/ai-service/llmobs/tracer.d.ts +9 -5
  198. package/dist/ai-service/llmobs/tracer.d.ts.map +1 -1
  199. package/dist/ai-service/llmobs/tracer.js +46 -41
  200. package/dist/ai-service/llmobs/tracer.js.map +1 -1
  201. package/dist/ai-service/llmobs/types.d.ts +1 -0
  202. package/dist/ai-service/llmobs/types.d.ts.map +1 -1
  203. package/dist/ai-service/mcp/adapter/mcp-tool-adapter.d.ts +1 -1
  204. package/dist/ai-service/mcp/adapter/mcp-tool-adapter.d.ts.map +1 -1
  205. package/dist/ai-service/mcp/adapter/mcp-tool-adapter.js +5 -2
  206. package/dist/ai-service/mcp/adapter/mcp-tool-adapter.js.map +1 -1
  207. package/dist/ai-service/mcp/embedded-playwright-mcp-server.d.ts +33 -1
  208. package/dist/ai-service/mcp/embedded-playwright-mcp-server.d.ts.map +1 -1
  209. package/dist/ai-service/mcp/embedded-playwright-mcp-server.js +949 -91
  210. package/dist/ai-service/mcp/embedded-playwright-mcp-server.js.map +1 -1
  211. package/dist/ai-service/mcp/playwright-server.d.ts +39 -0
  212. package/dist/ai-service/mcp/playwright-server.d.ts.map +1 -1
  213. package/dist/ai-service/mcp/playwright-server.js +56 -0
  214. package/dist/ai-service/mcp/playwright-server.js.map +1 -1
  215. package/dist/ai-service/mcp/types.d.ts +4 -0
  216. package/dist/ai-service/mcp/types.d.ts.map +1 -1
  217. package/dist/ai-service/prompts/explain-code.d.ts +2 -2
  218. package/dist/ai-service/prompts/explain-code.d.ts.map +1 -1
  219. package/dist/ai-service/prompts/explain-code.js +2 -2
  220. package/dist/ai-service/prompts/explain-code.js.map +1 -1
  221. package/dist/ai-service/state-machine/clark-fsm.d.ts +20 -1
  222. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  223. package/dist/ai-service/state-machine/clark-fsm.js +16 -0
  224. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  225. package/dist/ai-service/state-machine/fsm.d.ts.map +1 -1
  226. package/dist/ai-service/state-machine/fsm.js +1 -0
  227. package/dist/ai-service/state-machine/fsm.js.map +1 -1
  228. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  229. package/dist/ai-service/state-machine/handlers/agent-planning.js +71 -7
  230. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  231. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  232. package/dist/ai-service/state-machine/handlers/llm-generating.js +112 -33
  233. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  234. package/dist/ai-service/state-machine/helpers/peer.d.ts +1 -1
  235. package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
  236. package/dist/ai-service/state-machine/helpers/peer.js +21 -4
  237. package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
  238. package/dist/ai-service/state-machine/mocks.d.ts.map +1 -1
  239. package/dist/ai-service/state-machine/mocks.js +2 -0
  240. package/dist/ai-service/state-machine/mocks.js.map +1 -1
  241. package/dist/ai-service/state-machine/traced-fsm.d.ts +2 -0
  242. package/dist/ai-service/state-machine/traced-fsm.d.ts.map +1 -1
  243. package/dist/ai-service/state-machine/traced-fsm.js +18 -0
  244. package/dist/ai-service/state-machine/traced-fsm.js.map +1 -1
  245. package/dist/ai-service/util/safe-parse.d.ts +2 -0
  246. package/dist/ai-service/util/safe-parse.d.ts.map +1 -0
  247. package/dist/ai-service/util/safe-parse.js +9 -0
  248. package/dist/ai-service/util/safe-parse.js.map +1 -0
  249. package/dist/ai-service/util/safe-stringify.d.ts.map +1 -1
  250. package/dist/ai-service/util/safe-stringify.js +7 -0
  251. package/dist/ai-service/util/safe-stringify.js.map +1 -1
  252. package/dist/ai-service/util/scoped-token-utils.d.ts +15 -0
  253. package/dist/ai-service/util/scoped-token-utils.d.ts.map +1 -0
  254. package/dist/ai-service/util/scoped-token-utils.js +131 -0
  255. package/dist/ai-service/util/scoped-token-utils.js.map +1 -0
  256. package/dist/ai-service/util/stop-condition.d.ts +1 -0
  257. package/dist/ai-service/util/stop-condition.d.ts.map +1 -1
  258. package/dist/ai-service/util/stop-condition.js +5 -0
  259. package/dist/ai-service/util/stop-condition.js.map +1 -1
  260. package/dist/ai-service/util/strip-content.d.ts +2 -0
  261. package/dist/ai-service/util/strip-content.d.ts.map +1 -0
  262. package/dist/ai-service/util/strip-content.js +31 -0
  263. package/dist/ai-service/util/strip-content.js.map +1 -0
  264. package/dist/ai-service/util/tool-signature.d.ts +13 -0
  265. package/dist/ai-service/util/tool-signature.d.ts.map +1 -0
  266. package/dist/ai-service/util/tool-signature.js +38 -0
  267. package/dist/ai-service/util/tool-signature.js.map +1 -0
  268. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  269. package/dist/file-sync-vite-plugin.js +4 -1
  270. package/dist/file-sync-vite-plugin.js.map +1 -1
  271. package/dist/injected-index.js +2 -2
  272. package/dist/injected-index.js.map +1 -1
  273. package/dist/parsing/jsx.d.ts.map +1 -1
  274. package/dist/parsing/jsx.js +0 -2
  275. package/dist/parsing/jsx.js.map +1 -1
  276. package/dist/server-rpc/index.js +1 -1
  277. package/dist/server-rpc/index.js.map +1 -1
  278. package/dist/socket-manager.d.ts.map +1 -1
  279. package/dist/socket-manager.js +61 -4
  280. package/dist/socket-manager.js.map +1 -1
  281. package/package.json +9 -9
  282. package/dist/ai-service/agent/tools/build-read-files.d.ts.map +0 -1
  283. package/dist/ai-service/agent/tools/build-read-files.js +0 -67
  284. package/dist/ai-service/agent/tools/build-read-files.js.map +0 -1
@@ -2,6 +2,164 @@ import { once } from "node:events";
2
2
  import fs from "node:fs";
3
3
  import http from "node:http";
4
4
  import { chromium, firefox, webkit, } from "playwright";
5
+ /**
6
+ * Playwright action logger that captures browser actions and saves them as log artifacts.
7
+ */
8
+ class PlaywrightActionLogger {
9
+ logContent = "";
10
+ sessionId;
11
+ startTime;
12
+ actionCount = 0;
13
+ saveArtifact;
14
+ runTimestamp;
15
+ logger;
16
+ constructor(options) {
17
+ this.sessionId = options.sessionId;
18
+ this.startTime = Date.now();
19
+ this.saveArtifact = options.saveArtifact;
20
+ this.runTimestamp = options.runTimestamp || new Date().toISOString();
21
+ this.logger = options.logger;
22
+ this.logContent += this.formatSection("PLAYWRIGHT SESSION START") + "\n";
23
+ this.logContent += `Session ID: ${this.sessionId}\n`;
24
+ this.logContent += `Timestamp: ${new Date().toISOString()}\n\n`;
25
+ }
26
+ formatSection(title) {
27
+ const prefix = `------ ${title} `;
28
+ const remaining = Math.max(0, 72 - prefix.length);
29
+ return prefix + "-".repeat(remaining);
30
+ }
31
+ truncateString(s, maxBytes) {
32
+ if (s.length <= maxBytes)
33
+ return s;
34
+ return (s.slice(0, maxBytes) + `\n... [truncated ${s.length - maxBytes} bytes]`);
35
+ }
36
+ /**
37
+ * Log the start of a Playwright action.
38
+ */
39
+ logActionStart(action, params) {
40
+ this.actionCount++;
41
+ const timestamp = new Date().toISOString();
42
+ this.logContent += `ACTION #${this.actionCount}: ${action}\n`;
43
+ this.logContent += ` Timestamp: ${timestamp}\n`;
44
+ // Log relevant parameters (exclude screenshot data)
45
+ const logParams = {};
46
+ for (const [key, value] of Object.entries(params)) {
47
+ if (key === "screenshot" || key === "data")
48
+ continue;
49
+ logParams[key] = value;
50
+ }
51
+ if (Object.keys(logParams).length > 0) {
52
+ this.logContent += ` Params:\n`;
53
+ for (const [key, value] of Object.entries(logParams)) {
54
+ const valueStr = typeof value === "string"
55
+ ? this.truncateString(value, 200)
56
+ : JSON.stringify(value);
57
+ this.logContent += ` ${key}: ${valueStr}\n`;
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Log the result of a Playwright action.
63
+ */
64
+ logActionResult(result) {
65
+ const success = result.success !== false;
66
+ this.logContent += ` Result: ${success ? "SUCCESS" : "FAILED"}\n`;
67
+ // Log error if present
68
+ if (result.error) {
69
+ this.logContent += ` Error: ${result.error}\n`;
70
+ }
71
+ // Log runtime errors if present
72
+ if (result.runtimeErrors?.length > 0) {
73
+ this.logContent += ` Runtime Errors:\n`;
74
+ for (const err of result.runtimeErrors) {
75
+ this.logContent += ` - ${err.header}\n`;
76
+ if (err.stack) {
77
+ const truncatedStack = this.truncateString(err.stack, 500);
78
+ this.logContent += ` Stack: ${truncatedStack}\n`;
79
+ }
80
+ }
81
+ }
82
+ // Log specific result fields (excluding large data like screenshots)
83
+ const fieldsToLog = ["url", "text", "hasErrors", "errorCount", "count"];
84
+ for (const field of fieldsToLog) {
85
+ if (field in result && result[field] !== undefined) {
86
+ this.logContent += ` ${field}: ${JSON.stringify(result[field])}\n`;
87
+ }
88
+ }
89
+ this.logContent += "\n";
90
+ }
91
+ /**
92
+ * Log console messages from the page.
93
+ */
94
+ logConsoleLogs(logs) {
95
+ if (logs.length === 0)
96
+ return;
97
+ this.logContent += ` Console Logs (${logs.length}):\n`;
98
+ const maxLogs = 20;
99
+ const logsToShow = logs.slice(-maxLogs);
100
+ for (const log of logsToShow) {
101
+ const text = this.truncateString(log.text, 200);
102
+ this.logContent += ` [${log.type}] ${text}\n`;
103
+ }
104
+ if (logs.length > maxLogs) {
105
+ this.logContent += ` ... and ${logs.length - maxLogs} more logs\n`;
106
+ }
107
+ }
108
+ /**
109
+ * Save the log to an artifact file.
110
+ */
111
+ async save() {
112
+ if (!this.saveArtifact) {
113
+ this.logger.debug("[PlaywrightLogger] No saveArtifact function provided, skipping log save");
114
+ return;
115
+ }
116
+ const duration = Date.now() - this.startTime;
117
+ this.logContent += this.formatSection("PLAYWRIGHT SESSION END") + "\n";
118
+ this.logContent += `End Timestamp: ${new Date().toISOString()}\n`;
119
+ this.logContent += `Duration: ${duration}ms\n`;
120
+ this.logContent += `Total Actions: ${this.actionCount}\n`;
121
+ try {
122
+ const artifact = {
123
+ type: "file",
124
+ filePath: `playwright-session-${this.sessionId}.log`,
125
+ content: this.logContent,
126
+ };
127
+ const stepId = `playwright-session-${this.sessionId}`;
128
+ await this.saveArtifact(artifact, stepId, this.runTimestamp);
129
+ this.logger.debug(`[PlaywrightLogger] Saved log artifact: ${artifact.filePath}`);
130
+ }
131
+ catch (error) {
132
+ this.logger.error(`[PlaywrightLogger] Failed to save log: ${String(error)}`);
133
+ }
134
+ }
135
+ /**
136
+ * Save the current log and reset for a new session.
137
+ * Used to save logs after each test run without closing the server.
138
+ */
139
+ async saveAndReset() {
140
+ // Only save if there are actions logged
141
+ if (this.actionCount === 0) {
142
+ this.logger.debug("[PlaywrightLogger] No actions logged, skipping save and reset");
143
+ return;
144
+ }
145
+ await this.save();
146
+ // Reset for next session
147
+ const newSessionId = String(Date.now());
148
+ this.sessionId = newSessionId;
149
+ this.startTime = Date.now();
150
+ this.actionCount = 0;
151
+ this.logContent = "";
152
+ this.logContent += this.formatSection("PLAYWRIGHT SESSION START") + "\n";
153
+ this.logContent += `Session ID: ${this.sessionId}\n`;
154
+ this.logContent += `Timestamp: ${new Date().toISOString()}\n\n`;
155
+ this.logger.debug(`[PlaywrightLogger] Reset for new session: ${newSessionId}`);
156
+ }
157
+ getLogContent() {
158
+ return this.logContent;
159
+ }
160
+ }
161
+ /** Default viewport dimensions for consistent screenshots */
162
+ const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
5
163
  const PLAYWRIGHT_ACTIONS = [
6
164
  "navigate",
7
165
  "click",
@@ -12,36 +170,46 @@ const PLAYWRIGHT_ACTIONS = [
12
170
  "evaluate",
13
171
  "getUrl",
14
172
  "reload",
173
+ "getConsoleLogs",
174
+ "scroll",
175
+ "scrollIntoView",
176
+ "checkRuntimeErrors",
15
177
  ];
178
+ let capturedConsoleLogs = [];
16
179
  export async function startEmbeddedPlaywrightMcpServer(options) {
180
+ const logger = options.logger;
181
+ // Create action logger for capturing Playwright actions
182
+ const sessionId = String(Date.now());
183
+ const actionLogger = new PlaywrightActionLogger({
184
+ sessionId,
185
+ saveArtifact: options.saveArtifact,
186
+ runTimestamp: options.runTimestamp,
187
+ logger,
188
+ });
17
189
  let browser;
18
190
  let context = null;
19
191
  let shouldCloseBrowser = true;
20
192
  if (options?.connectWsEndpoint) {
21
- console.log(`🎬 MCP Server: Connecting to existing browser at ${options.connectWsEndpoint}`);
22
- browser = await chromium.connect({ wsEndpoint: options.connectWsEndpoint });
193
+ browser = await chromium.connect({
194
+ wsEndpoint: options.connectWsEndpoint,
195
+ });
23
196
  shouldCloseBrowser = false;
24
197
  }
25
198
  else {
26
- console.log("🎬 MCP Server: Launching new browser with storage state");
27
199
  browser = await launchBrowser();
28
200
  }
29
- console.log(`🎬 MCP Server: Browser contexts available before selection: ${browser.contexts().length}`);
30
201
  // Reuse an existing context when connecting to an existing browser
31
202
  if (options?.connectWsEndpoint) {
32
203
  const existing = browser.contexts();
33
204
  if (existing.length > 0) {
34
205
  context = existing[0];
35
- console.log("🎬 MCP Server: Reusing existing browser context");
36
- }
37
- else {
38
- console.warn("🎬 MCP Server: No existing context found on shared browser; falling back to new context with storage/JWT");
39
206
  }
40
207
  }
41
208
  if (!context) {
42
- const contextOptions = {};
209
+ const contextOptions = {
210
+ viewport: DEFAULT_VIEWPORT,
211
+ };
43
212
  if (options?.storageStateData) {
44
- console.log("🎬 MCP Server: Using provided storageStateData");
45
213
  contextOptions.storageState = options.storageStateData;
46
214
  // Duplicate auth cookies onto app host if needed (e.g., when original domain is auth0)
47
215
  if (options.appUrl && options.storageStateData.cookies?.length) {
@@ -62,12 +230,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
62
230
  }
63
231
  else if (options?.storageStatePath) {
64
232
  if (fs.existsSync(options.storageStatePath)) {
65
- console.log(`🎬 MCP Server: Loading storage state from ${options.storageStatePath}`);
66
233
  contextOptions.storageState = options.storageStatePath;
67
234
  }
68
- else {
69
- console.warn(`🎬 MCP Server: Storage state not found at ${options.storageStatePath}, starting without it`);
70
- }
71
235
  }
72
236
  context = await browser.newContext(contextOptions);
73
237
  }
@@ -75,6 +239,154 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
75
239
  if (!context) {
76
240
  throw new Error("Failed to create or reuse a Playwright context");
77
241
  }
242
+ // Inject sb-init and sb-bootstrap-response data for direct iframe access
243
+ // This simulates the parent window sending these messages to the iframe
244
+ if (options?.initData || options?.bootstrapData) {
245
+ const initData = options.initData;
246
+ const bootstrapData = options.bootstrapData;
247
+ /**
248
+ * CRITICAL: Mock Parent Window Setup for Playwright Browser Automation
249
+ *
250
+ * WHY THIS IS NECESSARY:
251
+ * The Superblocks library code (in @superblocksteam/library) expects to run inside an iframe
252
+ * embedded by the Superblocks editor. It uses `window.parent.postMessage()` to communicate
253
+ * with the editor and relies on `window.parent !== window` to detect the embedded state.
254
+ *
255
+ * When running in Playwright for E2E testing, there is no parent iframe - the page runs
256
+ * standalone. Without this mock, the library would:
257
+ * 1. Fail the `isEmbeddedBySuperblocksFirstParty()` check
258
+ * 2. Never receive the `sb-init` message needed to establish socket connections
259
+ * 3. Never receive the `sb-bootstrap-response` with auth tokens and app data
260
+ *
261
+ * HOW IT WORKS:
262
+ * 1. We override `window.parent` with a mock object BEFORE library code loads (via addInitScript)
263
+ * 2. The mock intercepts `postMessage` calls from the library
264
+ * 3. When the library sends "sb-ready", we respond with "sb-init" containing peerId/auth data
265
+ * 4. When the library sends "sb-editor-request-bootstrap", we respond with bootstrap data
266
+ * 5. We also send initial messages proactively in case the library sets up listeners late
267
+ *
268
+ * WHAT COULD BREAK THIS:
269
+ * - If the library changes how it detects parent window embedding (e.g., different checks)
270
+ * - If the message types or payload shapes change in the library
271
+ * - If the library adds additional security checks (e.g., origin verification on parent)
272
+ * - If timing changes require different setTimeout delays
273
+ *
274
+ * TESTING NOTE:
275
+ * Integration tests for this setup should verify:
276
+ * 1. The app successfully initializes and shows content
277
+ * 2. API calls are authenticated (tokens were passed correctly)
278
+ * 3. Real-time features work (socket connection established via peerId)
279
+ */
280
+ await context.addInitScript((payload) => {
281
+ const { initData: init, bootstrapData: bootstrap } = payload;
282
+ /**
283
+ * Mock parent window object that intercepts postMessage calls from the library.
284
+ * When the app sends messages expecting a parent response, we handle them here.
285
+ */
286
+ const mockParent = {
287
+ postMessage: (message, _targetOrigin) => {
288
+ // When the app sends sb-ready, we respond with sb-init
289
+ // This triggers the library to establish its socket connection
290
+ if (message?.type === "sb-ready" && init) {
291
+ setTimeout(() => {
292
+ window.postMessage({
293
+ type: "sb-init",
294
+ payload: {
295
+ peerId: init.peerId,
296
+ userId: init.userId,
297
+ devServerAuthorization: init.devServerAuthorization,
298
+ appId: init.appId,
299
+ windowOriginUrl: init.windowOriginUrl,
300
+ },
301
+ startTime: Date.now(),
302
+ }, "*");
303
+ }, 10);
304
+ }
305
+ // When the app sends sb-editor-request-bootstrap, we respond with bootstrap data
306
+ // This provides auth tokens and configuration needed by the API manager
307
+ if (message?.type === "sb-editor-request-bootstrap" && bootstrap) {
308
+ setTimeout(() => {
309
+ window.postMessage({
310
+ type: "sb-bootstrap-response",
311
+ payload: bootstrap,
312
+ startTime: Date.now(),
313
+ }, "*");
314
+ }, 10);
315
+ }
316
+ // When the app sends authenticate-api-request, we respond with resolve-promise
317
+ // This allows API calls to proceed in test mode without actual auth
318
+ if (message?.type === "authenticate-api-request") {
319
+ const { callbackId } = message.payload || {};
320
+ if (callbackId) {
321
+ setTimeout(() => {
322
+ window.postMessage({
323
+ type: "resolve-promise",
324
+ callbackId,
325
+ payload: {}, // Empty success result - no auth errors
326
+ }, "*");
327
+ }, 10);
328
+ }
329
+ }
330
+ },
331
+ // These properties make the mock look like a real window object
332
+ // which helps pass any instanceof or property existence checks
333
+ window: window,
334
+ document: document,
335
+ location: window.location,
336
+ };
337
+ // Override window.parent to point to our mock
338
+ // This must happen before the library code runs (hence addInitScript)
339
+ Object.defineProperty(window, "parent", {
340
+ value: mockParent,
341
+ writable: false,
342
+ configurable: true,
343
+ });
344
+ // Send initial messages proactively after a delay
345
+ // This handles the case where the library sets up listeners after checking window.parent
346
+ if (init) {
347
+ setTimeout(() => {
348
+ window.postMessage({
349
+ type: "sb-init",
350
+ payload: {
351
+ peerId: init.peerId,
352
+ userId: init.userId,
353
+ devServerAuthorization: init.devServerAuthorization,
354
+ appId: init.appId,
355
+ windowOriginUrl: init.windowOriginUrl,
356
+ },
357
+ startTime: Date.now(),
358
+ }, "*");
359
+ }, 100);
360
+ }
361
+ if (bootstrap) {
362
+ setTimeout(() => {
363
+ window.postMessage({
364
+ type: "sb-bootstrap-response",
365
+ payload: bootstrap,
366
+ startTime: Date.now(),
367
+ }, "*");
368
+ }, 200);
369
+ // Send sb-global-sync with profiles data for API execution
370
+ // This sets superblocksContext.profiles which is needed for profileId
371
+ if (bootstrap.profiles) {
372
+ setTimeout(() => {
373
+ window.postMessage({
374
+ type: "sb-global-sync",
375
+ payload: {
376
+ global: {
377
+ profiles: bootstrap.profiles,
378
+ },
379
+ },
380
+ startTime: Date.now(),
381
+ }, "*");
382
+ }, 300);
383
+ }
384
+ }
385
+ }, {
386
+ initData,
387
+ bootstrapData,
388
+ });
389
+ }
78
390
  // Seed a cookie/localStorage with JWT for app domain if provided
79
391
  if (options?.jwt && options?.appUrl) {
80
392
  try {
@@ -145,7 +457,13 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
145
457
  await context.addCookies(cookies);
146
458
  }
147
459
  catch (error) {
148
- console.warn(`🎬 MCP Server: Failed to seed JWT cookie/localStorage: ${String(error)}`);
460
+ logger.error(`[MCP-Server] Failed to seed JWT cookie/localStorage: ${String(error)}`, {
461
+ error: {
462
+ kind: "McpServerSeedJwtError",
463
+ message: String(error),
464
+ stack: error instanceof Error ? error.stack : undefined,
465
+ },
466
+ });
149
467
  }
150
468
  }
151
469
  // Seed session storage for app origin if provided
@@ -165,8 +483,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
165
483
  }
166
484
  }, { targetOrigin: origin, entries: items });
167
485
  }
168
- catch (error) {
169
- console.warn(`🎬 MCP Server: Failed to seed sessionStorage: ${String(error)}`);
486
+ catch {
487
+ // ignore session storage seeding errors
170
488
  }
171
489
  }
172
490
  // Seed additional origins storage (local/session) if provided
@@ -202,8 +520,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
202
520
  ssEntries: seedSession,
203
521
  });
204
522
  }
205
- catch (error) {
206
- console.warn(`🎬 MCP Server: Failed to seed extra origin storage for ${origin}: ${String(error)}`);
523
+ catch {
524
+ // ignore extra origin storage seeding errors
207
525
  }
208
526
  }
209
527
  }
@@ -212,6 +530,35 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
212
530
  // Prefer an existing page in the reused context (e.g., the CLI-authenticated one)
213
531
  const existingPages = context.pages();
214
532
  const activePage = existingPages.length > 0 ? existingPages[0] : page;
533
+ // Set up console log capture for the page
534
+ capturedConsoleLogs = []; // Reset logs for new session
535
+ activePage.on("console", (msg) => {
536
+ capturedConsoleLogs.push({
537
+ type: msg.type(),
538
+ text: msg.text(),
539
+ timestamp: Date.now(),
540
+ });
541
+ });
542
+ // Auto-navigate to the app URL if provided, so the agent doesn't need to navigate manually
543
+ if (options?.appUrl) {
544
+ try {
545
+ await activePage.goto(options.appUrl, {
546
+ waitUntil: "load",
547
+ timeout: 60000,
548
+ });
549
+ }
550
+ catch (error) {
551
+ // Don't throw - let the agent handle navigation if auto-nav fails
552
+ // Log the error for debugging purposes
553
+ logger.warn(`[MCP-Server] Auto-navigation to ${options.appUrl} failed: ${String(error)}`, {
554
+ error: {
555
+ kind: "McpServerAutoNavError",
556
+ message: String(error),
557
+ stack: error instanceof Error ? error.stack : undefined,
558
+ },
559
+ });
560
+ }
561
+ }
215
562
  const server = http.createServer((req, res) => {
216
563
  void (async () => {
217
564
  try {
@@ -244,7 +591,7 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
244
591
  }));
245
592
  return;
246
593
  }
247
- const response = await handleJsonRpcRequest(request, activePage);
594
+ const response = await handleJsonRpcRequest(request, activePage, actionLogger);
248
595
  res.writeHead(200, { "Content-Type": "application/json" });
249
596
  res.end(JSON.stringify(response));
250
597
  }
@@ -268,6 +615,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
268
615
  return {
269
616
  url,
270
617
  close: async () => {
618
+ // Save the action log before closing
619
+ await actionLogger.save();
271
620
  await Promise.allSettled([
272
621
  new Promise((resolve, reject) => {
273
622
  server.close((error) => {
@@ -286,6 +635,62 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
286
635
  : Promise.resolve(),
287
636
  ]);
288
637
  },
638
+ flushLog: async () => {
639
+ await actionLogger.saveAndReset();
640
+ },
641
+ captureInteractiveElements: async () => {
642
+ try {
643
+ // Wait for page to be ready
644
+ await page.waitForLoadState("domcontentloaded");
645
+ const elements = await page.evaluate(() => {
646
+ const selector = 'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"], [onclick], [data-testid]';
647
+ const nodeList = document.querySelectorAll(selector);
648
+ return Array.from(nodeList)
649
+ .map((el) => {
650
+ const text = el.textContent?.trim().slice(0, 100);
651
+ const id = el.id || undefined;
652
+ const className = el.className || undefined;
653
+ const testId = el.getAttribute("data-testid") || undefined;
654
+ const placeholder = el.getAttribute("placeholder") || undefined;
655
+ const href = el.getAttribute("href") || undefined;
656
+ const type = el.getAttribute("type") || undefined;
657
+ const role = el.getAttribute("role") || undefined;
658
+ const ariaLabel = el.getAttribute("aria-label") || undefined;
659
+ const name = el.getAttribute("name") || undefined;
660
+ const disabled = el.hasAttribute("disabled") ||
661
+ el.getAttribute("aria-disabled") === "true";
662
+ return {
663
+ tag: el.tagName.toLowerCase(),
664
+ text: text || undefined,
665
+ id,
666
+ className: typeof className === "string" ? className : undefined,
667
+ testId,
668
+ placeholder,
669
+ href,
670
+ type,
671
+ role,
672
+ ariaLabel,
673
+ name,
674
+ disabled: disabled || undefined,
675
+ };
676
+ })
677
+ .filter((el) => {
678
+ // Filter out elements with no useful selector info
679
+ return (el.text ||
680
+ el.id ||
681
+ el.testId ||
682
+ el.placeholder ||
683
+ el.ariaLabel ||
684
+ el.name);
685
+ });
686
+ });
687
+ return elements;
688
+ }
689
+ catch (error) {
690
+ logger.warn(`Failed to capture interactive elements: ${String(error)}`);
691
+ return [];
692
+ }
693
+ },
289
694
  };
290
695
  }
291
696
  async function launchBrowser() {
@@ -310,16 +715,10 @@ async function readRequestBody(req) {
310
715
  }
311
716
  return Buffer.concat(chunks).toString("utf-8");
312
717
  }
313
- async function handleJsonRpcRequest(request, page) {
718
+ async function handleJsonRpcRequest(request, page, actionLogger) {
314
719
  const { method, id } = request;
315
- console.log("🎬 MCP Server: Received request", {
316
- method,
317
- id,
318
- hasParams: !!request.params,
319
- });
320
720
  try {
321
721
  if (method === "tools/list") {
322
- console.log("🎬 MCP Server: Listing tools");
323
722
  return {
324
723
  jsonrpc: "2.0",
325
724
  id,
@@ -327,16 +726,113 @@ async function handleJsonRpcRequest(request, page) {
327
726
  tools: [
328
727
  {
329
728
  name: "playwright_action",
330
- description: "Execute Playwright browser automation actions (navigate, click, fill, screenshot, etc.)",
729
+ description: `Execute Playwright browser automation actions. The browser is already navigated to the app.
730
+
731
+ ACTIONS:
732
+ - screenshot: Take a screenshot. Params: { fullPage?: boolean }
733
+ - click: Click an element. Params: { selector: string }
734
+ - fill: Type into an input. Params: { selector: string, value: string }
735
+ - getText: Get text content. Params: { selector: string }
736
+ - waitForSelector: Wait for element. Params: { selector: string }
737
+ - evaluate: Run JavaScript. Params: { script: string }
738
+ - getUrl: Get current URL. No params.
739
+ - reload: Reload the page. No params.
740
+ - navigate: Go to URL. Params: { url: string }
741
+ - getConsoleLogs: Get browser console logs. Params: { clear?: boolean }
742
+ - scroll: Scroll the page. Params: { x?: number, y?: number, deltaX?: number, deltaY?: number }
743
+ - scrollIntoView: Scroll element into view. Params: { selector: string }
744
+ - checkRuntimeErrors: Explicitly check for component crashes. No params.
745
+
746
+ RUNTIME ERROR DETECTION:
747
+ Actions (navigate, click, fill, evaluate) automatically detect component crashes.
748
+ When a component crashes, the action returns:
749
+ - success: false
750
+ - runtimeErrors: Array of { header: string, stack?: string }
751
+ - error: Summary message
752
+
753
+ If you see runtimeErrors in a response, the test should FAIL. Include the error details in your summary.
754
+
755
+ SELECTOR EXAMPLES:
756
+ - By testid: [data-testid="login-form"]
757
+ - By name: input[name="username"], textarea[name="bio"]
758
+ - By placeholder: input[placeholder="Enter name"]
759
+ - By type: input[type="email"], button[type="submit"]
760
+ - By text content: button:has-text("Submit"), a:has-text("Click here")
761
+ - By ID: #submitBtn, #loginForm
762
+ - By class: .btn-primary, .form-input
763
+ - Combining: form#login input[name="password"]
764
+
765
+ Any selector that playwright supports can be used.
766
+
767
+ CSS SELECTOR SYNTAX (IMPORTANT):
768
+ ✓ CORRECT: input[name="email"], button[type="submit"], #myId, .myClass
769
+ ✗ WRONG: input [name="email"] (no space before bracket!)
770
+ ✗ WRONG: input[name=email] (quotes required around value)
771
+
772
+ TIPS:
773
+ 1. Use specific selectors: prefer [name="x"] or [placeholder="x"] over generic .class
774
+ 2. If click/fill fails, the element may not be visible - try waitForSelector first
775
+ 3. For dynamic content, use waitForSelector before interacting
776
+ 4. Navigate, click, and fill will automatically take a screenshot of the page after the action is performed
777
+ 5. If success is false due to runtimeErrors, FAIL the test and report the error details`,
331
778
  inputSchema: {
332
779
  type: "object",
333
780
  properties: {
334
781
  action: {
335
782
  type: "string",
336
783
  enum: PLAYWRIGHT_ACTIONS,
784
+ description: "The action to perform. Most actions require a 'selector' parameter.",
785
+ },
786
+ description: {
787
+ type: "string",
788
+ description: "Brief description of what this test step is verifying (e.g., 'Verify login form appears', 'Test submit button functionality'). This will be displayed in the test report.",
789
+ },
790
+ selector: {
791
+ type: "string",
792
+ description: 'CSS selector for the target element. NO SPACE before brackets! Example: input[name="email"] NOT input [name="email"]',
793
+ },
794
+ value: {
795
+ type: "string",
796
+ description: "Value to fill (for 'fill' action)",
797
+ },
798
+ url: {
799
+ type: "string",
800
+ description: "URL to navigate to (for 'navigate' action)",
801
+ },
802
+ script: {
803
+ type: "string",
804
+ description: "JavaScript to execute in browser (for 'evaluate' action). No imports allowed.",
805
+ },
806
+ fullPage: {
807
+ type: "boolean",
808
+ description: "Capture full scrollable page (for 'screenshot' action)",
809
+ },
810
+ clear: {
811
+ type: "boolean",
812
+ description: "Clear logs after retrieving (for 'getConsoleLogs' action)",
813
+ },
814
+ x: {
815
+ type: "number",
816
+ description: "Absolute horizontal scroll position in pixels (for 'scroll' action)",
817
+ },
818
+ y: {
819
+ type: "number",
820
+ description: "Absolute vertical scroll position in pixels (for 'scroll' action)",
821
+ },
822
+ deltaX: {
823
+ type: "number",
824
+ description: "Relative horizontal scroll amount in pixels (for 'scroll' action)",
825
+ },
826
+ deltaY: {
827
+ type: "number",
828
+ description: "Relative vertical scroll amount in pixels (for 'scroll' action)",
829
+ },
830
+ testCaseId: {
831
+ type: "string",
832
+ description: "ID of the test case this action belongs to. Required when test cases are predefined. Links this action to a specific test case for status tracking.",
337
833
  },
338
834
  },
339
- required: ["action"],
835
+ required: ["action", "description"],
340
836
  },
341
837
  },
342
838
  ],
@@ -344,13 +840,7 @@ async function handleJsonRpcRequest(request, page) {
344
840
  };
345
841
  }
346
842
  if (method === "tools/call") {
347
- console.log("🎬 MCP Server: tools/call method");
348
843
  const params = request.params;
349
- console.log("🎬 MCP Server: Tool call params", {
350
- toolName: params?.name,
351
- hasArguments: !!params?.arguments,
352
- action: params?.arguments?.action,
353
- });
354
844
  if (!params || params.name !== "playwright_action") {
355
845
  throw new Error(`Unknown tool: ${params?.name ?? "undefined"}`);
356
846
  }
@@ -358,12 +848,16 @@ async function handleJsonRpcRequest(request, page) {
358
848
  if (!action || !PLAYWRIGHT_ACTIONS.includes(action)) {
359
849
  throw new Error(`Unsupported Playwright action: ${String(action)}`);
360
850
  }
361
- console.log(`🎬 MCP Server: Executing action: ${action}`);
851
+ // Log the action start
852
+ actionLogger.logActionStart(action, params.arguments ?? {});
362
853
  const result = await executePlaywrightAction(action, page, params.arguments ?? {});
363
- console.log(`🎬 MCP Server: Action ${action} completed`, {
364
- success: result?.success,
365
- hasResult: !!result,
366
- });
854
+ // Log the action result
855
+ actionLogger.logActionResult(result);
856
+ // Include testCaseId in the result if provided
857
+ const testCaseId = params.arguments?.testCaseId;
858
+ if (testCaseId) {
859
+ result.testCaseId = testCaseId;
860
+ }
367
861
  return {
368
862
  jsonrpc: "2.0",
369
863
  id,
@@ -392,96 +886,298 @@ async function handleJsonRpcRequest(request, page) {
392
886
  };
393
887
  }
394
888
  }
395
- async function executePlaywrightAction(action, page, params) {
396
- console.log("🎬 MCP Server: executePlaywrightAction called", {
397
- action,
398
- paramKeys: Object.keys(params),
399
- url: page.url(),
889
+ /**
890
+ * Checks for runtime errors displayed by ErrorBoundary components.
891
+ * These are detected by looking for elements with data-testid="runtime-error-details".
892
+ *
893
+ * @param page - The Playwright page to check
894
+ * @returns Object with hasErrors flag and array of error details
895
+ */
896
+ async function checkForRuntimeErrors(page) {
897
+ try {
898
+ const errorElements = await page
899
+ .locator('[data-testid="runtime-error-details"]')
900
+ .all();
901
+ if (errorElements.length === 0) {
902
+ return { hasErrors: false, errors: [] };
903
+ }
904
+ const errors = [];
905
+ for (const element of errorElements) {
906
+ try {
907
+ // Get the error header (h3 contains "Something went wrong." or custom header)
908
+ const header = (await element.locator("h3").textContent()) || "Component error";
909
+ // Try to get the stack trace from the pre > code element inside details
910
+ let stack;
911
+ try {
912
+ const codeElement = element.locator("pre code");
913
+ const codeText = await codeElement.textContent({ timeout: 1000 });
914
+ stack = codeText || undefined;
915
+ }
916
+ catch {
917
+ // Stack not available or details not expanded
918
+ }
919
+ errors.push({ header, stack });
920
+ }
921
+ catch {
922
+ errors.push({ header: "Unknown component error" });
923
+ }
924
+ }
925
+ return { hasErrors: true, errors };
926
+ }
927
+ catch {
928
+ return { hasErrors: false, errors: [] };
929
+ }
930
+ }
931
+ /**
932
+ * Captures a screenshot in PNG format.
933
+ *
934
+ * TODO: Re-enable WebP conversion using sharp once the build issue is resolved.
935
+ * WebP provides ~25-34% smaller file sizes compared to JPEG at equivalent
936
+ * visual quality, with better compression than PNG.
937
+ *
938
+ * @param page - The Playwright page to capture
939
+ * @returns Screenshot data as base64 string with format identifier
940
+ */
941
+ async function captureScreenshot(page) {
942
+ try {
943
+ await page.evaluate(() => document.fonts.ready);
944
+ await page.waitForTimeout(100);
945
+ }
946
+ catch {
947
+ // Continue anyway if fonts.ready fails
948
+ }
949
+ const pngBuffer = await page.screenshot({
950
+ fullPage: false,
951
+ scale: "css",
952
+ type: "png",
400
953
  });
954
+ // TODO: Re-enable WebP conversion once sharp build issue is resolved
955
+ // const webpBuffer = await sharp(pngBuffer).webp({ quality: 80 }).toBuffer();
956
+ // return { data: webpBuffer.toString("base64"), format: "webp" };
957
+ return {
958
+ data: pngBuffer.toString("base64"),
959
+ format: "png",
960
+ };
961
+ }
962
+ async function executePlaywrightAction(action, page, params) {
401
963
  switch (action) {
402
964
  case "navigate": {
403
965
  const url = params.url;
966
+ const description = params.description || "Navigate to page";
404
967
  if (!url)
405
968
  throw new Error("Missing url parameter for navigate action");
406
- console.log(`🎬 MCP Server: Navigating to ${url}`);
407
969
  try {
408
970
  await page.goto(url, {
409
971
  waitUntil: "load",
410
972
  timeout: 60000, // 60 second timeout
411
973
  });
412
- console.log(`🎬 MCP Server: Navigation complete`);
413
- return { success: true, url };
974
+ // Auto-capture screenshot after navigation
975
+ const screenshot = await captureScreenshot(page);
976
+ // Check for runtime errors (component crashes)
977
+ const runtimeErrors = await checkForRuntimeErrors(page);
978
+ return {
979
+ success: !runtimeErrors.hasErrors,
980
+ action: "navigate",
981
+ description,
982
+ screenshot: screenshot.data,
983
+ format: screenshot.format,
984
+ context: {
985
+ url,
986
+ },
987
+ ...(runtimeErrors.hasErrors && {
988
+ runtimeErrors: runtimeErrors.errors,
989
+ error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
990
+ }),
991
+ };
414
992
  }
415
993
  catch (error) {
416
994
  console.error(`🎬 MCP Server: Navigation failed: ${String(error)}`);
417
- // Return error but don't throw - let the judge handle it
995
+ // Capture screenshot even on error
996
+ let errorScreenshot = null;
997
+ try {
998
+ errorScreenshot = await captureScreenshot(page);
999
+ }
1000
+ catch {
1001
+ // Ignore screenshot errors
1002
+ }
418
1003
  return {
419
1004
  success: false,
1005
+ action: "navigate",
1006
+ description,
1007
+ screenshot: errorScreenshot?.data,
1008
+ format: errorScreenshot?.format ?? "png",
1009
+ context: {
1010
+ url,
1011
+ },
420
1012
  error: `Navigation to ${url} failed: ${String(error)}`,
421
- url,
422
1013
  };
423
1014
  }
424
1015
  }
425
1016
  case "click": {
426
1017
  const selector = params.selector;
1018
+ const description = params.description || "Click element";
427
1019
  if (!selector)
428
1020
  throw new Error("Missing selector for click action");
429
- // Always target elements inside the iframe
430
- const frame = page.frameLocator('[data-test="sb-iframe"]');
431
- await frame.locator(selector).click({ timeout: params.timeout });
432
- return { success: true };
1021
+ try {
1022
+ await page
1023
+ .locator(selector)
1024
+ .click({ timeout: params.timeout ?? 10000 });
1025
+ // Auto-capture screenshot after action
1026
+ const screenshot = await captureScreenshot(page);
1027
+ // Check for runtime errors (component crashes)
1028
+ const runtimeErrors = await checkForRuntimeErrors(page);
1029
+ return {
1030
+ success: !runtimeErrors.hasErrors,
1031
+ action: "click",
1032
+ description,
1033
+ screenshot: screenshot.data,
1034
+ format: screenshot.format,
1035
+ context: {
1036
+ selector,
1037
+ },
1038
+ ...(runtimeErrors.hasErrors && {
1039
+ runtimeErrors: runtimeErrors.errors,
1040
+ error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
1041
+ }),
1042
+ };
1043
+ }
1044
+ catch (error) {
1045
+ // Capture screenshot even on error
1046
+ let errorScreenshot = null;
1047
+ try {
1048
+ errorScreenshot = await captureScreenshot(page);
1049
+ }
1050
+ catch {
1051
+ // Ignore screenshot errors
1052
+ }
1053
+ return {
1054
+ success: false,
1055
+ action: "click",
1056
+ description,
1057
+ screenshot: errorScreenshot?.data,
1058
+ format: errorScreenshot?.format ?? "png",
1059
+ context: {
1060
+ selector,
1061
+ },
1062
+ error: `Click failed on "${selector}": ${String(error)}`,
1063
+ };
1064
+ }
433
1065
  }
434
1066
  case "fill": {
435
1067
  const selector = params.selector;
436
1068
  const value = params.value ?? "";
1069
+ const description = params.description || "Fill input";
437
1070
  if (!selector)
438
1071
  throw new Error("Missing selector for fill action");
439
- // Always target elements inside the iframe
440
- const frame = page.frameLocator('[data-test="sb-iframe"]');
441
- await frame.locator(selector).fill(value, { timeout: params.timeout });
442
- return { success: true };
1072
+ try {
1073
+ await page
1074
+ .locator(selector)
1075
+ .fill(value, { timeout: params.timeout ?? 10000 });
1076
+ // Auto-capture screenshot after fill
1077
+ const screenshot = await captureScreenshot(page);
1078
+ // Check for runtime errors (component crashes)
1079
+ const runtimeErrors = await checkForRuntimeErrors(page);
1080
+ return {
1081
+ success: !runtimeErrors.hasErrors,
1082
+ action: "fill",
1083
+ description,
1084
+ screenshot: screenshot.data,
1085
+ format: screenshot.format,
1086
+ context: {
1087
+ selector,
1088
+ value,
1089
+ },
1090
+ ...(runtimeErrors.hasErrors && {
1091
+ runtimeErrors: runtimeErrors.errors,
1092
+ error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
1093
+ }),
1094
+ };
1095
+ }
1096
+ catch (error) {
1097
+ // Capture screenshot even on error
1098
+ let errorScreenshot = null;
1099
+ try {
1100
+ errorScreenshot = await captureScreenshot(page);
1101
+ }
1102
+ catch {
1103
+ // Ignore screenshot errors
1104
+ }
1105
+ return {
1106
+ success: false,
1107
+ action: "fill",
1108
+ description,
1109
+ screenshot: errorScreenshot?.data,
1110
+ format: errorScreenshot?.format ?? "png",
1111
+ context: {
1112
+ selector,
1113
+ value,
1114
+ },
1115
+ error: `Fill failed on "${selector}": ${String(error)}`,
1116
+ };
1117
+ }
443
1118
  }
444
1119
  case "screenshot": {
445
- const buffer = await page.screenshot({
446
- fullPage: Boolean(params.fullPage),
447
- });
1120
+ const description = params.description || "Capture current state";
1121
+ // Capture screenshot in WebP format for optimal compression
1122
+ const screenshot = await captureScreenshot(page);
448
1123
  return {
449
1124
  success: true,
450
- screenshot: buffer.toString("base64"),
1125
+ action: "screenshot",
1126
+ description,
1127
+ screenshot: screenshot.data,
1128
+ format: screenshot.format,
1129
+ context: {
1130
+ fullPage: Boolean(params.fullPage),
1131
+ },
451
1132
  };
452
1133
  }
453
1134
  case "getText": {
454
1135
  const selector = params.selector;
455
1136
  if (!selector)
456
1137
  throw new Error("Missing selector for getText action");
457
- // Always target elements inside the iframe
458
- const frame = page.frameLocator('[data-test="sb-iframe"]');
459
- const text = await frame.locator(selector).textContent({
460
- timeout: params.timeout,
461
- });
462
- return { success: true, text };
1138
+ try {
1139
+ const text = await page.locator(selector).textContent({
1140
+ timeout: params.timeout ?? 10000,
1141
+ });
1142
+ return { success: true, text };
1143
+ }
1144
+ catch (error) {
1145
+ return {
1146
+ success: false,
1147
+ error: `getText failed on "${selector}": ${String(error)}`,
1148
+ };
1149
+ }
463
1150
  }
464
1151
  case "waitForSelector": {
465
1152
  const selector = params.selector;
466
1153
  if (!selector) {
467
1154
  throw new Error("Missing selector for waitForSelector action");
468
1155
  }
469
- // Always target elements inside the iframe
470
- const frame = page.frameLocator('[data-test="sb-iframe"]');
471
- await frame.locator(selector).waitFor({
472
- timeout: params.timeout ?? 30_000,
473
- });
474
- return { success: true };
1156
+ try {
1157
+ await page.locator(selector).waitFor({
1158
+ timeout: params.timeout ?? 15000,
1159
+ });
1160
+ return { success: true };
1161
+ }
1162
+ catch (error) {
1163
+ return {
1164
+ success: false,
1165
+ error: `waitForSelector failed on "${selector}": ${String(error)}`,
1166
+ };
1167
+ }
475
1168
  }
476
1169
  case "evaluate": {
477
1170
  const script = params.script;
1171
+ const description = params.description || "Execute JavaScript";
478
1172
  if (typeof script !== "string") {
479
1173
  throw new Error("Missing script for evaluate action");
480
1174
  }
481
1175
  // Check for import/export/require statements that can't be used in browser context
482
- if (script.includes("import ") ||
483
- script.includes("export ") ||
484
- script.includes("require(")) {
1176
+ // Use regex with word boundaries to avoid false positives on strings like "important"
1177
+ const hasImport = /\bimport\s+/.test(script);
1178
+ const hasExport = /\bexport\s+/.test(script);
1179
+ const hasRequire = /\brequire\s*\(/.test(script);
1180
+ if (hasImport || hasExport || hasRequire) {
485
1181
  throw new Error("Cannot use import/export/require statements in browser evaluate context. " +
486
1182
  "Please use plain JavaScript that can run in a browser console. " +
487
1183
  "For example, use 'document.querySelector' instead of importing libraries.");
@@ -496,32 +1192,70 @@ async function executePlaywrightAction(action, page, params) {
496
1192
  return { error: e.toString(), stack: e.stack };
497
1193
  }
498
1194
  })()`;
499
- // Always execute JavaScript inside the iframe
500
- const frameElement = await page
501
- .locator('[data-test="sb-iframe"]')
502
- .elementHandle();
503
- const frame = await frameElement?.contentFrame();
504
- if (!frame) {
505
- throw new Error("Could not find iframe [data-test='sb-iframe'] or iframe not loaded");
506
- }
507
- const result = await frame.evaluate(wrappedScript);
1195
+ const result = await page.evaluate(wrappedScript);
508
1196
  // Check if the evaluation returned an error
509
1197
  if (result && typeof result === "object" && "error" in result) {
510
1198
  throw new Error(`Browser evaluation failed: ${result.error}`);
511
1199
  }
512
- return { success: true, result };
1200
+ // Auto-capture screenshot after evaluation
1201
+ const screenshot = await captureScreenshot(page);
1202
+ // Check for runtime errors (component crashes)
1203
+ const runtimeErrors = await checkForRuntimeErrors(page);
1204
+ return {
1205
+ success: !runtimeErrors.hasErrors,
1206
+ action: "evaluate",
1207
+ description,
1208
+ result,
1209
+ screenshot: screenshot.data,
1210
+ format: screenshot.format,
1211
+ context: {
1212
+ script: script.substring(0, 100) + (script.length > 100 ? "..." : ""),
1213
+ },
1214
+ ...(runtimeErrors.hasErrors && {
1215
+ runtimeErrors: runtimeErrors.errors,
1216
+ error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
1217
+ }),
1218
+ };
513
1219
  }
514
1220
  catch (error) {
1221
+ // Capture screenshot even on error
1222
+ let errorScreenshot = null;
1223
+ try {
1224
+ errorScreenshot = await captureScreenshot(page);
1225
+ }
1226
+ catch {
1227
+ // Ignore screenshot errors
1228
+ }
515
1229
  // If the error looks like a syntax error, provide helpful guidance
516
1230
  const errorMsg = String(error);
517
1231
  if (errorMsg.includes("SyntaxError") ||
518
1232
  errorMsg.includes("import") ||
519
1233
  errorMsg.includes("module")) {
520
- throw new Error("JavaScript evaluation failed. Make sure your code is browser-compatible " +
521
- "(no import/export statements). Error: " +
522
- errorMsg);
1234
+ return {
1235
+ success: false,
1236
+ action: "evaluate",
1237
+ description,
1238
+ screenshot: errorScreenshot?.data,
1239
+ format: errorScreenshot?.format ?? "png",
1240
+ context: {
1241
+ script: script.substring(0, 100) + (script.length > 100 ? "..." : ""),
1242
+ },
1243
+ error: "JavaScript evaluation failed. Make sure your code is browser-compatible " +
1244
+ "(no import/export statements). Error: " +
1245
+ errorMsg,
1246
+ };
523
1247
  }
524
- throw error;
1248
+ return {
1249
+ success: false,
1250
+ action: "evaluate",
1251
+ description,
1252
+ screenshot: errorScreenshot?.data,
1253
+ format: errorScreenshot?.format ?? "png",
1254
+ context: {
1255
+ script: script.substring(0, 100) + (script.length > 100 ? "..." : ""),
1256
+ },
1257
+ error: String(error),
1258
+ };
525
1259
  }
526
1260
  }
527
1261
  case "getUrl": {
@@ -534,6 +1268,130 @@ async function executePlaywrightAction(action, page, params) {
534
1268
  });
535
1269
  return { success: true };
536
1270
  }
1271
+ case "getConsoleLogs": {
1272
+ // Return captured console logs and optionally clear them
1273
+ const logs = [...capturedConsoleLogs];
1274
+ if (params.clear) {
1275
+ capturedConsoleLogs = [];
1276
+ }
1277
+ return {
1278
+ success: true,
1279
+ logs,
1280
+ count: logs.length,
1281
+ };
1282
+ }
1283
+ case "scroll": {
1284
+ const { x, y, deltaX, deltaY } = params;
1285
+ const description = params.description || "Scroll page";
1286
+ try {
1287
+ await page.evaluate(({ x, y, deltaX, deltaY }) => {
1288
+ if (deltaX !== undefined || deltaY !== undefined) {
1289
+ window.scrollBy(deltaX ?? 0, deltaY ?? 0);
1290
+ }
1291
+ else if (x !== undefined || y !== undefined) {
1292
+ window.scrollTo(x ?? window.scrollX, y ?? window.scrollY);
1293
+ }
1294
+ }, { x, y, deltaX, deltaY });
1295
+ const screenshot = await captureScreenshot(page);
1296
+ return {
1297
+ success: true,
1298
+ action: "scroll",
1299
+ description,
1300
+ screenshot: screenshot.data,
1301
+ format: screenshot.format,
1302
+ context: {
1303
+ x,
1304
+ y,
1305
+ deltaX,
1306
+ deltaY,
1307
+ },
1308
+ };
1309
+ }
1310
+ catch (error) {
1311
+ let errorScreenshot = null;
1312
+ try {
1313
+ errorScreenshot = await captureScreenshot(page);
1314
+ }
1315
+ catch {
1316
+ // Ignore screenshot errors
1317
+ }
1318
+ return {
1319
+ success: false,
1320
+ action: "scroll",
1321
+ description,
1322
+ screenshot: errorScreenshot?.data,
1323
+ format: errorScreenshot?.format ?? "png",
1324
+ context: {
1325
+ x,
1326
+ y,
1327
+ deltaX,
1328
+ deltaY,
1329
+ },
1330
+ error: `Scroll failed: ${String(error)}`,
1331
+ };
1332
+ }
1333
+ }
1334
+ case "scrollIntoView": {
1335
+ const selector = params.selector;
1336
+ const description = params.description || "Scroll element into view";
1337
+ if (!selector) {
1338
+ throw new Error("Missing selector for scrollIntoView action");
1339
+ }
1340
+ try {
1341
+ await page.locator(selector).scrollIntoViewIfNeeded({
1342
+ timeout: params.timeout ?? 10000,
1343
+ });
1344
+ const screenshot = await captureScreenshot(page);
1345
+ return {
1346
+ success: true,
1347
+ action: "scrollIntoView",
1348
+ description,
1349
+ screenshot: screenshot.data,
1350
+ format: screenshot.format,
1351
+ context: {
1352
+ selector,
1353
+ },
1354
+ };
1355
+ }
1356
+ catch (error) {
1357
+ let errorScreenshot = null;
1358
+ try {
1359
+ errorScreenshot = await captureScreenshot(page);
1360
+ }
1361
+ catch {
1362
+ // Ignore screenshot errors
1363
+ }
1364
+ return {
1365
+ success: false,
1366
+ action: "scrollIntoView",
1367
+ description,
1368
+ screenshot: errorScreenshot?.data,
1369
+ format: errorScreenshot?.format ?? "png",
1370
+ context: {
1371
+ selector,
1372
+ },
1373
+ error: `scrollIntoView failed on "${selector}": ${String(error)}`,
1374
+ };
1375
+ }
1376
+ }
1377
+ case "checkRuntimeErrors": {
1378
+ const description = params.description || "Check for component runtime errors";
1379
+ const screenshot = await captureScreenshot(page);
1380
+ const runtimeErrors = await checkForRuntimeErrors(page);
1381
+ return {
1382
+ success: !runtimeErrors.hasErrors,
1383
+ action: "checkRuntimeErrors",
1384
+ description,
1385
+ screenshot: screenshot.data,
1386
+ format: screenshot.format,
1387
+ hasErrors: runtimeErrors.hasErrors,
1388
+ errorCount: runtimeErrors.errors.length,
1389
+ ...(runtimeErrors.hasErrors && {
1390
+ runtimeErrors: runtimeErrors.errors,
1391
+ error: `Found ${runtimeErrors.errors.length} component crash(es): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
1392
+ }),
1393
+ };
1394
+ }
537
1395
  default:
538
1396
  throw new Error(`Unhandled Playwright action: ${action}`);
539
1397
  }