qa360 2.2.20 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/README.md +155 -262
  2. package/{cli/dist → dist}/commands/ai.js +1 -1
  3. package/{cli/dist → dist}/commands/coverage.js +1 -1
  4. package/{cli/dist → dist}/commands/crawl.d.ts +12 -1
  5. package/{cli/dist → dist}/commands/crawl.js +70 -9
  6. package/{cli/dist → dist}/commands/doctor.js +2 -2
  7. package/{cli/dist → dist}/commands/explain.js +2 -2
  8. package/{cli/dist → dist}/commands/flakiness.js +1 -1
  9. package/{cli/dist → dist}/commands/generate.js +1 -1
  10. package/{cli/dist → dist}/commands/history.js +1 -1
  11. package/{cli/dist → dist}/commands/monitor.js +3 -3
  12. package/{cli/dist → dist}/commands/ollama.js +1 -1
  13. package/{cli/dist → dist}/commands/pack.js +2 -2
  14. package/{cli/dist → dist}/commands/regression.js +1 -1
  15. package/{cli/dist → dist}/commands/repair.js +1 -1
  16. package/{cli/dist → dist}/commands/retry.js +1 -1
  17. package/{cli/dist → dist}/commands/run.d.ts +1 -1
  18. package/{cli/dist → dist}/commands/run.js +1 -1
  19. package/{cli/dist → dist}/commands/secrets.js +1 -1
  20. package/{cli/dist → dist}/commands/serve.js +1 -1
  21. package/{cli/dist → dist}/commands/slo.js +1 -1
  22. package/{cli/dist → dist}/commands/verify.js +1 -1
  23. package/{cli/dist → dist}/core/adapters/playwright-native-api.d.ts +2 -0
  24. package/{cli/dist → dist}/core/adapters/playwright-native-api.js +20 -1
  25. package/{cli/dist → dist}/core/adapters/playwright-ui.d.ts +21 -0
  26. package/dist/core/adapters/playwright-ui.js +2050 -0
  27. package/{cli/dist → dist}/core/ai/ollama-provider.js +15 -3
  28. package/{cli/dist → dist}/core/artifacts/ui-artifacts.js +24 -4
  29. package/dist/core/auth/backup-codes-provider.d.ts +91 -0
  30. package/dist/core/auth/backup-codes-provider.js +215 -0
  31. package/{cli/dist → dist}/core/auth/basic-auth-provider.d.ts +6 -0
  32. package/{cli/dist → dist}/core/auth/basic-auth-provider.js +24 -6
  33. package/dist/core/auth/digest-auth-provider.d.ts +116 -0
  34. package/dist/core/auth/digest-auth-provider.js +244 -0
  35. package/dist/core/auth/hcaptcha-handler.d.ts +103 -0
  36. package/dist/core/auth/hcaptcha-handler.js +288 -0
  37. package/{cli/dist → dist}/core/auth/index.d.ts +81 -4
  38. package/{cli/dist → dist}/core/auth/index.js +15 -1
  39. package/dist/core/auth/oauth-handler.d.ts +408 -0
  40. package/dist/core/auth/oauth-handler.js +636 -0
  41. package/{cli/dist → dist}/core/auth/oauth2-provider.d.ts +9 -0
  42. package/dist/core/auth/oauth2-provider.js +227 -0
  43. package/dist/core/auth/otp-provider.d.ts +93 -0
  44. package/dist/core/auth/otp-provider.js +288 -0
  45. package/dist/core/auth/recaptcha-handler.d.ts +119 -0
  46. package/dist/core/auth/recaptcha-handler.js +301 -0
  47. package/dist/core/auth/remember-me-handler.d.ts +142 -0
  48. package/dist/core/auth/remember-me-handler.js +255 -0
  49. package/dist/core/auth/saml-handler.d.ts +173 -0
  50. package/dist/core/auth/saml-handler.js +364 -0
  51. package/dist/core/auth/webauthn-handler.d.ts +182 -0
  52. package/dist/core/auth/webauthn-handler.js +310 -0
  53. package/dist/core/crawler/advanced-interactions.d.ts +342 -0
  54. package/dist/core/crawler/advanced-interactions.js +1069 -0
  55. package/dist/core/crawler/blob-url-download-handler.d.ts +145 -0
  56. package/dist/core/crawler/blob-url-download-handler.js +392 -0
  57. package/dist/core/crawler/consent-handler.d.ts +49 -0
  58. package/dist/core/crawler/consent-handler.js +258 -0
  59. package/dist/core/crawler/cookie-manager.d.ts +166 -0
  60. package/dist/core/crawler/cookie-manager.js +353 -0
  61. package/dist/core/crawler/coop-coep-handler.d.ts +136 -0
  62. package/dist/core/crawler/coop-coep-handler.js +338 -0
  63. package/dist/core/crawler/csp-handler.d.ts +151 -0
  64. package/dist/core/crawler/csp-handler.js +415 -0
  65. package/dist/core/crawler/download-handler.d.ts +155 -0
  66. package/dist/core/crawler/download-handler.js +370 -0
  67. package/dist/core/crawler/email-testing-handler.d.ts +214 -0
  68. package/dist/core/crawler/email-testing-handler.js +398 -0
  69. package/dist/core/crawler/error-tracking-handler.d.ts +177 -0
  70. package/dist/core/crawler/error-tracking-handler.js +378 -0
  71. package/dist/core/crawler/form-handler.d.ts +100 -0
  72. package/dist/core/crawler/form-handler.js +465 -0
  73. package/dist/core/crawler/framework-wait-handler.d.ts +96 -0
  74. package/dist/core/crawler/framework-wait-handler.js +464 -0
  75. package/dist/core/crawler/geolocation-handler.d.ts +112 -0
  76. package/dist/core/crawler/geolocation-handler.js +276 -0
  77. package/dist/core/crawler/index.d.ts +78 -0
  78. package/{cli/dist → dist}/core/crawler/index.js +74 -1
  79. package/dist/core/crawler/intelligent-selector-generator.d.ts +164 -0
  80. package/dist/core/crawler/intelligent-selector-generator.js +612 -0
  81. package/{cli/dist → dist}/core/crawler/journey-generator.js +44 -1
  82. package/{cli/dist → dist}/core/crawler/page-analyzer.d.ts +16 -1
  83. package/{cli/dist → dist}/core/crawler/page-analyzer.js +469 -17
  84. package/dist/core/crawler/permissions-handler.d.ts +112 -0
  85. package/dist/core/crawler/permissions-handler.js +236 -0
  86. package/dist/core/crawler/permissions-policy-handler.d.ts +113 -0
  87. package/dist/core/crawler/permissions-policy-handler.js +402 -0
  88. package/dist/core/crawler/presets.d.ts +100 -0
  89. package/dist/core/crawler/presets.js +887 -0
  90. package/dist/core/crawler/repl-debug-handler.d.ts +105 -0
  91. package/dist/core/crawler/repl-debug-handler.js +552 -0
  92. package/dist/core/crawler/reporting-api-handler.d.ts +212 -0
  93. package/dist/core/crawler/reporting-api-handler.js +344 -0
  94. package/{cli/dist → dist}/core/crawler/selector-generator.d.ts +9 -0
  95. package/{cli/dist → dist}/core/crawler/selector-generator.js +99 -23
  96. package/dist/core/crawler/site-profiler.d.ts +89 -0
  97. package/dist/core/crawler/site-profiler.js +290 -0
  98. package/dist/core/crawler/sourcemaps-handler.d.ts +144 -0
  99. package/dist/core/crawler/sourcemaps-handler.js +420 -0
  100. package/dist/core/crawler/stacked-modals-handler.d.ts +118 -0
  101. package/dist/core/crawler/stacked-modals-handler.js +429 -0
  102. package/dist/core/crawler/trusted-types-handler.d.ts +149 -0
  103. package/dist/core/crawler/trusted-types-handler.js +413 -0
  104. package/{cli/dist → dist}/core/crawler/types.d.ts +68 -2
  105. package/dist/core/crawler/wait-strategies.d.ts +108 -0
  106. package/dist/core/crawler/wait-strategies.js +399 -0
  107. package/dist/core/fixtures/factories.d.ts +180 -0
  108. package/dist/core/fixtures/factories.js +279 -0
  109. package/dist/core/fixtures/index.d.ts +6 -0
  110. package/dist/core/fixtures/index.js +6 -0
  111. package/{cli/dist → dist}/core/generation/crawler-pack-generator.d.ts +13 -3
  112. package/dist/core/generation/crawler-pack-generator.js +232 -0
  113. package/{cli/dist → dist}/core/generation/index.d.ts +2 -0
  114. package/{cli/dist → dist}/core/generation/index.js +2 -0
  115. package/{cli/dist → dist}/core/index.d.ts +2 -0
  116. package/{cli/dist → dist}/core/index.js +4 -0
  117. package/dist/core/network/index.d.ts +7 -0
  118. package/dist/core/network/index.js +7 -0
  119. package/dist/core/network/network-manager.d.ts +237 -0
  120. package/dist/core/network/network-manager.js +343 -0
  121. package/dist/core/network/network-simulator.d.ts +158 -0
  122. package/dist/core/network/network-simulator.js +261 -0
  123. package/{cli/dist → dist}/core/pack/validator.js +2 -2
  124. package/{cli/dist → dist}/core/pack-v2/migrator.d.ts +5 -0
  125. package/{cli/dist → dist}/core/pack-v2/migrator.js +81 -6
  126. package/{cli/dist → dist}/core/pack-v2/validator.js +4 -3
  127. package/{cli/dist → dist}/core/pom/base-page.js +1 -1
  128. package/{cli/dist → dist}/core/pom/loader.js +1 -1
  129. package/dist/core/reporting/index.d.ts +9 -0
  130. package/dist/core/reporting/index.js +10 -0
  131. package/dist/core/reporting/junit-reporter.d.ts +114 -0
  132. package/dist/core/reporting/junit-reporter.js +306 -0
  133. package/{cli/dist → dist}/core/runner/e2e-helpers.d.ts +1 -1
  134. package/{cli/dist → dist}/core/runner/e2e-helpers.js +2 -2
  135. package/{cli/dist → dist}/core/runner/phase3-runner.d.ts +3 -0
  136. package/{cli/dist → dist}/core/runner/phase3-runner.js +45 -14
  137. package/dist/core/sharding/test-sharding.d.ts +137 -0
  138. package/dist/core/sharding/test-sharding.js +233 -0
  139. package/dist/core/storage/cookie-manager.d.ts +160 -0
  140. package/dist/core/storage/cookie-manager.js +268 -0
  141. package/dist/core/storage/index.d.ts +7 -0
  142. package/dist/core/storage/index.js +7 -0
  143. package/dist/core/storage/storage-helpers.d.ts +138 -0
  144. package/dist/core/storage/storage-helpers.js +315 -0
  145. package/dist/core/test-helpers/index.d.ts +6 -0
  146. package/dist/core/test-helpers/index.js +6 -0
  147. package/dist/core/test-helpers/state-reset.d.ts +119 -0
  148. package/dist/core/test-helpers/state-reset.js +234 -0
  149. package/{cli/dist → dist}/core/types/pack-v1.d.ts +15 -2
  150. package/{cli/dist → dist}/core/types/pack-v2.d.ts +1 -1
  151. package/dist/core/upload/chunked-uploader.d.ts +150 -0
  152. package/dist/core/upload/chunked-uploader.js +289 -0
  153. package/dist/core/upload/index.d.ts +11 -0
  154. package/dist/core/upload/index.js +8 -0
  155. package/dist/core/upload/mime-validator.d.ts +119 -0
  156. package/dist/core/upload/mime-validator.js +373 -0
  157. package/dist/core/upload/presigned-uploader.d.ts +118 -0
  158. package/dist/core/upload/presigned-uploader.js +274 -0
  159. package/dist/core/utils/device-emulation.d.ts +194 -0
  160. package/dist/core/utils/device-emulation.js +380 -0
  161. package/dist/core/utils/index.d.ts +8 -0
  162. package/dist/core/utils/index.js +8 -0
  163. package/dist/core/utils/retry.d.ts +145 -0
  164. package/dist/core/utils/retry.js +242 -0
  165. package/dist/core/utils/smart-wait.d.ts +133 -0
  166. package/dist/core/utils/smart-wait.js +417 -0
  167. package/dist/core/visual/index.d.ts +7 -0
  168. package/dist/core/visual/index.js +7 -0
  169. package/dist/core/visual/pixel-diff.d.ts +87 -0
  170. package/dist/core/visual/pixel-diff.js +213 -0
  171. package/dist/core/visual/screenshot-helper.d.ts +130 -0
  172. package/dist/core/visual/screenshot-helper.js +223 -0
  173. package/{cli/dist → dist}/utils/config.d.ts +1 -1
  174. package/examples/README.md +160 -0
  175. package/examples/accessibility.yml +48 -0
  176. package/examples/api-basic.yml +27 -0
  177. package/examples/complete.yml +146 -0
  178. package/examples/crawler.yml +38 -0
  179. package/examples/fullstack.yml +78 -0
  180. package/examples/security.yml +58 -0
  181. package/examples/ui-advanced.yml +49 -0
  182. package/examples/ui-basic.yml +24 -0
  183. package/package.json +33 -67
  184. package/CHANGELOG.md +0 -262
  185. package/CONTRIBUTING.md +0 -273
  186. package/QUICK_START.md +0 -191
  187. package/cli/CHANGELOG.md +0 -84
  188. package/cli/LICENSE +0 -24
  189. package/cli/README.md +0 -222
  190. package/cli/dist/core/adapters/playwright-ui.js +0 -864
  191. package/cli/dist/core/auth/oauth2-provider.js +0 -114
  192. package/cli/dist/core/coverage/analyzer.d.ts +0 -101
  193. package/cli/dist/core/coverage/analyzer.js +0 -415
  194. package/cli/dist/core/coverage/collector.d.ts +0 -74
  195. package/cli/dist/core/coverage/collector.js +0 -459
  196. package/cli/dist/core/coverage/config.d.ts +0 -37
  197. package/cli/dist/core/coverage/config.js +0 -156
  198. package/cli/dist/core/coverage/index.d.ts +0 -11
  199. package/cli/dist/core/coverage/index.js +0 -15
  200. package/cli/dist/core/coverage/types.d.ts +0 -267
  201. package/cli/dist/core/coverage/types.js +0 -6
  202. package/cli/dist/core/coverage/vault.d.ts +0 -95
  203. package/cli/dist/core/coverage/vault.js +0 -405
  204. package/cli/dist/core/crawler/index.d.ts +0 -57
  205. package/cli/dist/core/fixtures/index.d.ts +0 -8
  206. package/cli/dist/core/fixtures/index.js +0 -8
  207. package/cli/dist/core/generation/crawler-pack-generator.js +0 -231
  208. package/cli/dist/core/reporting/index.d.ts +0 -6
  209. package/cli/dist/core/reporting/index.js +0 -6
  210. package/cli/dist/core/visual/index.d.ts +0 -6
  211. package/cli/dist/core/visual/index.js +0 -6
  212. package/cli/package.json +0 -76
  213. package/core/LICENSE +0 -24
  214. package/core/README.md +0 -64
  215. package/core/package.json +0 -81
  216. package/core/schemas/pack.schema.json +0 -236
  217. /package/{cli/bin → bin}/qa360.js +0 -0
  218. /package/{cli/dist → dist}/cli-minimal.d.ts +0 -0
  219. /package/{cli/dist → dist}/cli-minimal.js +0 -0
  220. /package/{cli/dist → dist}/commands/ai.d.ts +0 -0
  221. /package/{cli/dist → dist}/commands/ask.d.ts +0 -0
  222. /package/{cli/dist → dist}/commands/ask.js +0 -0
  223. /package/{cli/dist → dist}/commands/coverage.d.ts +0 -0
  224. /package/{cli/dist → dist}/commands/doctor.d.ts +0 -0
  225. /package/{cli/dist → dist}/commands/examples.d.ts +0 -0
  226. /package/{cli/dist → dist}/commands/examples.js +0 -0
  227. /package/{cli/dist → dist}/commands/explain.d.ts +0 -0
  228. /package/{cli/dist → dist}/commands/flakiness.d.ts +0 -0
  229. /package/{cli/dist → dist}/commands/generate.d.ts +0 -0
  230. /package/{cli/dist → dist}/commands/history.d.ts +0 -0
  231. /package/{cli/dist → dist}/commands/init.d.ts +0 -0
  232. /package/{cli/dist → dist}/commands/init.js +0 -0
  233. /package/{cli/dist → dist}/commands/monitor.d.ts +0 -0
  234. /package/{cli/dist → dist}/commands/ollama.d.ts +0 -0
  235. /package/{cli/dist → dist}/commands/pack.d.ts +0 -0
  236. /package/{cli/dist → dist}/commands/regression.d.ts +0 -0
  237. /package/{cli/dist → dist}/commands/repair.d.ts +0 -0
  238. /package/{cli/dist → dist}/commands/report.d.ts +0 -0
  239. /package/{cli/dist → dist}/commands/report.js +0 -0
  240. /package/{cli/dist → dist}/commands/retry.d.ts +0 -0
  241. /package/{cli/dist → dist}/commands/scan.d.ts +0 -0
  242. /package/{cli/dist → dist}/commands/scan.js +0 -0
  243. /package/{cli/dist → dist}/commands/secrets.d.ts +0 -0
  244. /package/{cli/dist → dist}/commands/serve.d.ts +0 -0
  245. /package/{cli/dist → dist}/commands/slo.d.ts +0 -0
  246. /package/{cli/dist → dist}/commands/verify.d.ts +0 -0
  247. /package/{cli/dist → dist}/core/adapters/gitleaks-secrets.d.ts +0 -0
  248. /package/{cli/dist → dist}/core/adapters/gitleaks-secrets.js +0 -0
  249. /package/{cli/dist → dist}/core/adapters/jest-adapter.d.ts +0 -0
  250. /package/{cli/dist → dist}/core/adapters/jest-adapter.js +0 -0
  251. /package/{cli/dist → dist}/core/adapters/k6-perf.d.ts +0 -0
  252. /package/{cli/dist → dist}/core/adapters/k6-perf.js +0 -0
  253. /package/{cli/dist → dist}/core/adapters/osv-deps.d.ts +0 -0
  254. /package/{cli/dist → dist}/core/adapters/osv-deps.js +0 -0
  255. /package/{cli/dist → dist}/core/adapters/playwright-native-adapter.d.ts +0 -0
  256. /package/{cli/dist → dist}/core/adapters/playwright-native-adapter.js +0 -0
  257. /package/{cli/dist → dist}/core/adapters/pytest-adapter.d.ts +0 -0
  258. /package/{cli/dist → dist}/core/adapters/pytest-adapter.js +0 -0
  259. /package/{cli/dist → dist}/core/adapters/semgrep-sast.d.ts +0 -0
  260. /package/{cli/dist → dist}/core/adapters/semgrep-sast.js +0 -0
  261. /package/{cli/dist → dist}/core/adapters/unit-test-types.d.ts +0 -0
  262. /package/{cli/dist → dist}/core/adapters/unit-test-types.js +0 -0
  263. /package/{cli/dist → dist}/core/adapters/vitest-adapter.d.ts +0 -0
  264. /package/{cli/dist → dist}/core/adapters/vitest-adapter.js +0 -0
  265. /package/{cli/dist → dist}/core/adapters/zap-dast.d.ts +0 -0
  266. /package/{cli/dist → dist}/core/adapters/zap-dast.js +0 -0
  267. /package/{cli/dist → dist}/core/ai/anthropic-provider.d.ts +0 -0
  268. /package/{cli/dist → dist}/core/ai/anthropic-provider.js +0 -0
  269. /package/{cli/dist → dist}/core/ai/deepseek-provider.d.ts +0 -0
  270. /package/{cli/dist → dist}/core/ai/deepseek-provider.js +0 -0
  271. /package/{cli/dist → dist}/core/ai/index.d.ts +0 -0
  272. /package/{cli/dist → dist}/core/ai/index.js +0 -0
  273. /package/{cli/dist → dist}/core/ai/llm-client.d.ts +0 -0
  274. /package/{cli/dist → dist}/core/ai/llm-client.js +0 -0
  275. /package/{cli/dist → dist}/core/ai/mock-provider.d.ts +0 -0
  276. /package/{cli/dist → dist}/core/ai/mock-provider.js +0 -0
  277. /package/{cli/dist → dist}/core/ai/ollama-provider.d.ts +0 -0
  278. /package/{cli/dist → dist}/core/ai/openai-provider.d.ts +0 -0
  279. /package/{cli/dist → dist}/core/ai/openai-provider.js +0 -0
  280. /package/{cli/dist → dist}/core/ai/provider-factory.d.ts +0 -0
  281. /package/{cli/dist → dist}/core/ai/provider-factory.js +0 -0
  282. /package/{cli/dist → dist}/core/artifacts/index.d.ts +0 -0
  283. /package/{cli/dist → dist}/core/artifacts/index.js +0 -0
  284. /package/{cli/dist → dist}/core/artifacts/ui-artifacts.d.ts +0 -0
  285. /package/{cli/dist → dist}/core/assertions/engine.d.ts +0 -0
  286. /package/{cli/dist → dist}/core/assertions/engine.js +0 -0
  287. /package/{cli/dist → dist}/core/assertions/index.d.ts +0 -0
  288. /package/{cli/dist → dist}/core/assertions/index.js +0 -0
  289. /package/{cli/dist → dist}/core/assertions/types.d.ts +0 -0
  290. /package/{cli/dist → dist}/core/assertions/types.js +0 -0
  291. /package/{cli/dist → dist}/core/auth/api-key-provider.d.ts +0 -0
  292. /package/{cli/dist → dist}/core/auth/api-key-provider.js +0 -0
  293. /package/{cli/dist → dist}/core/auth/aws-iam-provider.d.ts +0 -0
  294. /package/{cli/dist → dist}/core/auth/aws-iam-provider.js +0 -0
  295. /package/{cli/dist → dist}/core/auth/azure-ad-provider.d.ts +0 -0
  296. /package/{cli/dist → dist}/core/auth/azure-ad-provider.js +0 -0
  297. /package/{cli/dist → dist}/core/auth/gcp-adc-provider.d.ts +0 -0
  298. /package/{cli/dist → dist}/core/auth/gcp-adc-provider.js +0 -0
  299. /package/{cli/dist → dist}/core/auth/jwt-provider.d.ts +0 -0
  300. /package/{cli/dist → dist}/core/auth/jwt-provider.js +0 -0
  301. /package/{cli/dist → dist}/core/auth/manager.d.ts +0 -0
  302. /package/{cli/dist → dist}/core/auth/manager.js +0 -0
  303. /package/{cli/dist → dist}/core/auth/totp-provider.d.ts +0 -0
  304. /package/{cli/dist → dist}/core/auth/totp-provider.js +0 -0
  305. /package/{cli/dist → dist}/core/auth/ui-login-provider.d.ts +0 -0
  306. /package/{cli/dist → dist}/core/auth/ui-login-provider.js +0 -0
  307. /package/{cli/dist → dist}/core/cache/index.d.ts +0 -0
  308. /package/{cli/dist → dist}/core/cache/index.js +0 -0
  309. /package/{cli/dist → dist}/core/cache/lru-cache.d.ts +0 -0
  310. /package/{cli/dist → dist}/core/cache/lru-cache.js +0 -0
  311. /package/{cli/dist/core → dist}/core/coverage/analyzer.d.ts +0 -0
  312. /package/{cli/dist/core → dist}/core/coverage/analyzer.js +0 -0
  313. /package/{cli/dist/core → dist}/core/coverage/collector.d.ts +0 -0
  314. /package/{cli/dist/core → dist}/core/coverage/collector.js +0 -0
  315. /package/{cli/dist/core → dist}/core/coverage/config.d.ts +0 -0
  316. /package/{cli/dist/core → dist}/core/coverage/config.js +0 -0
  317. /package/{cli/dist/core → dist}/core/coverage/index.d.ts +0 -0
  318. /package/{cli/dist/core → dist}/core/coverage/index.js +0 -0
  319. /package/{cli/dist/core → dist}/core/coverage/types.d.ts +0 -0
  320. /package/{cli/dist/core → dist}/core/coverage/types.js +0 -0
  321. /package/{cli/dist/core → dist}/core/coverage/vault.d.ts +0 -0
  322. /package/{cli/dist/core → dist}/core/coverage/vault.js +0 -0
  323. /package/{cli/dist → dist}/core/crawler/journey-generator.d.ts +0 -0
  324. /package/{cli/dist → dist}/core/crawler/types.js +0 -0
  325. /package/{cli/dist → dist}/core/dashboard/assets.d.ts +0 -0
  326. /package/{cli/dist → dist}/core/dashboard/assets.js +0 -0
  327. /package/{cli/dist → dist}/core/dashboard/index.d.ts +0 -0
  328. /package/{cli/dist → dist}/core/dashboard/index.js +0 -0
  329. /package/{cli/dist → dist}/core/dashboard/server.d.ts +0 -0
  330. /package/{cli/dist → dist}/core/dashboard/server.js +0 -0
  331. /package/{cli/dist → dist}/core/dashboard/types.d.ts +0 -0
  332. /package/{cli/dist → dist}/core/dashboard/types.js +0 -0
  333. /package/{cli/dist → dist}/core/discoverer/index.d.ts +0 -0
  334. /package/{cli/dist → dist}/core/discoverer/index.js +0 -0
  335. /package/{cli/dist → dist}/core/fixtures/loader.d.ts +0 -0
  336. /package/{cli/dist → dist}/core/fixtures/loader.js +0 -0
  337. /package/{cli/dist → dist}/core/fixtures/resolver.d.ts +0 -0
  338. /package/{cli/dist → dist}/core/fixtures/resolver.js +0 -0
  339. /package/{cli/dist → dist}/core/fixtures/types.d.ts +0 -0
  340. /package/{cli/dist → dist}/core/fixtures/types.js +0 -0
  341. /package/{cli/dist → dist}/core/flakiness/index.d.ts +0 -0
  342. /package/{cli/dist → dist}/core/flakiness/index.js +0 -0
  343. /package/{cli/dist → dist}/core/generation/code-formatter.d.ts +0 -0
  344. /package/{cli/dist → dist}/core/generation/code-formatter.js +0 -0
  345. /package/{cli/dist → dist}/core/generation/code-generator.d.ts +0 -0
  346. /package/{cli/dist → dist}/core/generation/code-generator.js +0 -0
  347. /package/{cli/dist → dist}/core/generation/generator.d.ts +0 -0
  348. /package/{cli/dist → dist}/core/generation/generator.js +0 -0
  349. /package/{cli/dist → dist}/core/generation/pack-generator.d.ts +0 -0
  350. /package/{cli/dist → dist}/core/generation/pack-generator.js +0 -0
  351. /package/{cli/dist → dist}/core/generation/prompt-builder.d.ts +0 -0
  352. /package/{cli/dist → dist}/core/generation/prompt-builder.js +0 -0
  353. /package/{cli/dist → dist}/core/generation/source-analyzer.d.ts +0 -0
  354. /package/{cli/dist → dist}/core/generation/source-analyzer.js +0 -0
  355. /package/{cli/dist → dist}/core/generation/test-optimizer.d.ts +0 -0
  356. /package/{cli/dist → dist}/core/generation/test-optimizer.js +0 -0
  357. /package/{cli/dist → dist}/core/generation/types.d.ts +0 -0
  358. /package/{cli/dist → dist}/core/generation/types.js +0 -0
  359. /package/{cli/dist → dist}/core/hooks/compose.d.ts +0 -0
  360. /package/{cli/dist → dist}/core/hooks/compose.js +0 -0
  361. /package/{cli/dist → dist}/core/hooks/runner.d.ts +0 -0
  362. /package/{cli/dist → dist}/core/hooks/runner.js +0 -0
  363. /package/{cli/dist → dist}/core/pack/migrator.d.ts +0 -0
  364. /package/{cli/dist → dist}/core/pack/migrator.js +0 -0
  365. /package/{cli/dist → dist}/core/pack/validator.d.ts +0 -0
  366. /package/{cli/dist → dist}/core/pack-v2/index.d.ts +0 -0
  367. /package/{cli/dist → dist}/core/pack-v2/index.js +0 -0
  368. /package/{cli/dist → dist}/core/pack-v2/loader.d.ts +0 -0
  369. /package/{cli/dist → dist}/core/pack-v2/loader.js +0 -0
  370. /package/{cli/dist → dist}/core/pack-v2/validator.d.ts +0 -0
  371. /package/{cli/dist → dist}/core/parallel/index.d.ts +0 -0
  372. /package/{cli/dist → dist}/core/parallel/index.js +0 -0
  373. /package/{cli/dist → dist}/core/parallel/parallel-runner.d.ts +0 -0
  374. /package/{cli/dist → dist}/core/parallel/parallel-runner.js +0 -0
  375. /package/{cli/dist → dist}/core/pom/base-page.d.ts +0 -0
  376. /package/{cli/dist → dist}/core/pom/index.d.ts +0 -0
  377. /package/{cli/dist → dist}/core/pom/index.js +0 -0
  378. /package/{cli/dist → dist}/core/pom/loader.d.ts +0 -0
  379. /package/{cli/dist → dist}/core/pom/types.d.ts +0 -0
  380. /package/{cli/dist → dist}/core/pom/types.js +0 -0
  381. /package/{cli/dist → dist}/core/proof/bundle.d.ts +0 -0
  382. /package/{cli/dist → dist}/core/proof/bundle.js +0 -0
  383. /package/{cli/dist → dist}/core/proof/canonicalize.d.ts +0 -0
  384. /package/{cli/dist → dist}/core/proof/canonicalize.js +0 -0
  385. /package/{cli/dist → dist}/core/proof/index.d.ts +0 -0
  386. /package/{cli/dist → dist}/core/proof/index.js +0 -0
  387. /package/{cli/dist → dist}/core/proof/schema.d.ts +0 -0
  388. /package/{cli/dist → dist}/core/proof/schema.js +0 -0
  389. /package/{cli/dist → dist}/core/proof/signer.d.ts +0 -0
  390. /package/{cli/dist → dist}/core/proof/signer.js +0 -0
  391. /package/{cli/dist → dist}/core/proof/verifier.d.ts +0 -0
  392. /package/{cli/dist → dist}/core/proof/verifier.js +0 -0
  393. /package/{cli/dist → dist}/core/regression/detector.d.ts +0 -0
  394. /package/{cli/dist → dist}/core/regression/detector.js +0 -0
  395. /package/{cli/dist → dist}/core/regression/index.d.ts +0 -0
  396. /package/{cli/dist → dist}/core/regression/index.js +0 -0
  397. /package/{cli/dist → dist}/core/regression/trend-analyzer.d.ts +0 -0
  398. /package/{cli/dist → dist}/core/regression/trend-analyzer.js +0 -0
  399. /package/{cli/dist → dist}/core/regression/types.d.ts +0 -0
  400. /package/{cli/dist → dist}/core/regression/types.js +0 -0
  401. /package/{cli/dist → dist}/core/regression/vault.d.ts +0 -0
  402. /package/{cli/dist → dist}/core/regression/vault.js +0 -0
  403. /package/{cli/dist → dist}/core/repair/engine/fixer.d.ts +0 -0
  404. /package/{cli/dist → dist}/core/repair/engine/fixer.js +0 -0
  405. /package/{cli/dist → dist}/core/repair/engine/suggestion-engine.d.ts +0 -0
  406. /package/{cli/dist → dist}/core/repair/engine/suggestion-engine.js +0 -0
  407. /package/{cli/dist → dist}/core/repair/index.d.ts +0 -0
  408. /package/{cli/dist → dist}/core/repair/index.js +0 -0
  409. /package/{cli/dist → dist}/core/repair/repairer.d.ts +0 -0
  410. /package/{cli/dist → dist}/core/repair/repairer.js +0 -0
  411. /package/{cli/dist → dist}/core/repair/types.d.ts +0 -0
  412. /package/{cli/dist → dist}/core/repair/types.js +0 -0
  413. /package/{cli/dist → dist}/core/repair/utils/error-analyzer.d.ts +0 -0
  414. /package/{cli/dist → dist}/core/repair/utils/error-analyzer.js +0 -0
  415. /package/{cli/dist → dist}/core/reporting/html-reporter.d.ts +0 -0
  416. /package/{cli/dist → dist}/core/reporting/html-reporter.js +0 -0
  417. /package/{cli/dist → dist}/core/retry/flakiness-integration.d.ts +0 -0
  418. /package/{cli/dist → dist}/core/retry/flakiness-integration.js +0 -0
  419. /package/{cli/dist → dist}/core/retry/index.d.ts +0 -0
  420. /package/{cli/dist → dist}/core/retry/index.js +0 -0
  421. /package/{cli/dist → dist}/core/retry/retry-engine.d.ts +0 -0
  422. /package/{cli/dist → dist}/core/retry/retry-engine.js +0 -0
  423. /package/{cli/dist → dist}/core/retry/types.d.ts +0 -0
  424. /package/{cli/dist → dist}/core/retry/types.js +0 -0
  425. /package/{cli/dist → dist}/core/retry/vault.d.ts +0 -0
  426. /package/{cli/dist → dist}/core/retry/vault.js +0 -0
  427. /package/{cli/dist → dist}/core/schemas/pack.schema.json +0 -0
  428. /package/{cli/dist → dist}/core/secrets/crypto.d.ts +0 -0
  429. /package/{cli/dist → dist}/core/secrets/crypto.js +0 -0
  430. /package/{cli/dist → dist}/core/secrets/manager.d.ts +0 -0
  431. /package/{cli/dist → dist}/core/secrets/manager.js +0 -0
  432. /package/{cli/dist → dist}/core/security/redaction-patterns-extended.d.ts +0 -0
  433. /package/{cli/dist → dist}/core/security/redaction-patterns-extended.js +0 -0
  434. /package/{cli/dist → dist}/core/security/redactor.d.ts +0 -0
  435. /package/{cli/dist → dist}/core/security/redactor.js +0 -0
  436. /package/{cli/dist → dist}/core/self-healing/assertion-healer.d.ts +0 -0
  437. /package/{cli/dist → dist}/core/self-healing/assertion-healer.js +0 -0
  438. /package/{cli/dist → dist}/core/self-healing/engine.d.ts +0 -0
  439. /package/{cli/dist → dist}/core/self-healing/engine.js +0 -0
  440. /package/{cli/dist → dist}/core/self-healing/index.d.ts +0 -0
  441. /package/{cli/dist → dist}/core/self-healing/index.js +0 -0
  442. /package/{cli/dist → dist}/core/self-healing/selector-healer.d.ts +0 -0
  443. /package/{cli/dist → dist}/core/self-healing/selector-healer.js +0 -0
  444. /package/{cli/dist → dist}/core/self-healing/types.d.ts +0 -0
  445. /package/{cli/dist → dist}/core/self-healing/types.js +0 -0
  446. /package/{cli/dist → dist}/core/serve/diagnostics-collector.d.ts +0 -0
  447. /package/{cli/dist → dist}/core/serve/diagnostics-collector.js +0 -0
  448. /package/{cli/dist → dist}/core/serve/health-checker.d.ts +0 -0
  449. /package/{cli/dist → dist}/core/serve/health-checker.js +0 -0
  450. /package/{cli/dist → dist}/core/serve/index.d.ts +0 -0
  451. /package/{cli/dist → dist}/core/serve/index.js +0 -0
  452. /package/{cli/dist → dist}/core/serve/metrics-collector.d.ts +0 -0
  453. /package/{cli/dist → dist}/core/serve/metrics-collector.js +0 -0
  454. /package/{cli/dist → dist}/core/serve/process-manager.d.ts +0 -0
  455. /package/{cli/dist → dist}/core/serve/process-manager.js +0 -0
  456. /package/{cli/dist → dist}/core/serve/server.d.ts +0 -0
  457. /package/{cli/dist → dist}/core/serve/server.js +0 -0
  458. /package/{cli/dist → dist}/core/slo/config.d.ts +0 -0
  459. /package/{cli/dist → dist}/core/slo/config.js +0 -0
  460. /package/{cli/dist → dist}/core/slo/index.d.ts +0 -0
  461. /package/{cli/dist → dist}/core/slo/index.js +0 -0
  462. /package/{cli/dist → dist}/core/slo/sli-calculator.d.ts +0 -0
  463. /package/{cli/dist → dist}/core/slo/sli-calculator.js +0 -0
  464. /package/{cli/dist → dist}/core/slo/slo-tracker.d.ts +0 -0
  465. /package/{cli/dist → dist}/core/slo/slo-tracker.js +0 -0
  466. /package/{cli/dist → dist}/core/slo/types.d.ts +0 -0
  467. /package/{cli/dist → dist}/core/slo/types.js +0 -0
  468. /package/{cli/dist → dist}/core/slo/vault.d.ts +0 -0
  469. /package/{cli/dist → dist}/core/slo/vault.js +0 -0
  470. /package/{cli/dist → dist}/core/tui/index.d.ts +0 -0
  471. /package/{cli/dist → dist}/core/tui/index.js +0 -0
  472. /package/{cli/dist → dist}/core/tui/monitor.d.ts +0 -0
  473. /package/{cli/dist → dist}/core/tui/monitor.js +0 -0
  474. /package/{cli/dist → dist}/core/tui/renderer.d.ts +0 -0
  475. /package/{cli/dist → dist}/core/tui/renderer.js +0 -0
  476. /package/{cli/dist → dist}/core/tui/types.d.ts +0 -0
  477. /package/{cli/dist → dist}/core/tui/types.js +0 -0
  478. /package/{cli/dist → dist}/core/types/pack-v1.js +0 -0
  479. /package/{cli/dist → dist}/core/types/pack-v2.js +0 -0
  480. /package/{cli/dist → dist}/core/types/trust-score.d.ts +0 -0
  481. /package/{cli/dist → dist}/core/types/trust-score.js +0 -0
  482. /package/{cli/dist → dist}/core/vault/cas.d.ts +0 -0
  483. /package/{cli/dist → dist}/core/vault/cas.js +0 -0
  484. /package/{cli/dist → dist}/core/vault/index.d.ts +0 -0
  485. /package/{cli/dist → dist}/core/vault/index.js +0 -0
  486. /package/{cli/dist → dist}/core/visual/visual-regression.d.ts +0 -0
  487. /package/{cli/dist → dist}/core/visual/visual-regression.js +0 -0
  488. /package/{cli/dist → dist}/core/watch/index.d.ts +0 -0
  489. /package/{cli/dist → dist}/core/watch/index.js +0 -0
  490. /package/{cli/dist → dist}/core/watch/watch-mode.d.ts +0 -0
  491. /package/{cli/dist → dist}/core/watch/watch-mode.js +0 -0
  492. /package/{cli/dist → dist}/generators/index.d.ts +0 -0
  493. /package/{cli/dist → dist}/generators/index.js +0 -0
  494. /package/{cli/dist → dist}/generators/json-reporter.d.ts +0 -0
  495. /package/{cli/dist → dist}/generators/json-reporter.js +0 -0
  496. /package/{cli/dist → dist}/generators/test-generator.d.ts +0 -0
  497. /package/{cli/dist → dist}/generators/test-generator.js +0 -0
  498. /package/{cli/dist → dist}/index.d.ts +0 -0
  499. /package/{cli/dist → dist}/index.js +0 -0
  500. /package/{cli/dist → dist}/scanners/dom-scanner.d.ts +0 -0
  501. /package/{cli/dist → dist}/scanners/dom-scanner.js +0 -0
  502. /package/{cli/dist → dist}/scanners/index.d.ts +0 -0
  503. /package/{cli/dist → dist}/scanners/index.js +0 -0
  504. /package/{cli/dist → dist}/schemas/pack.schema.json +0 -0
  505. /package/{cli/dist → dist}/types/scan.d.ts +0 -0
  506. /package/{cli/dist → dist}/types/scan.js +0 -0
  507. /package/{cli/dist → dist}/utils/config.js +0 -0
@@ -0,0 +1,2050 @@
1
+ /**
2
+ * QA360 Playwright UI Adapter (Extended)
3
+ * Complete UI E2E testing with all Playwright actions
4
+ *
5
+ * Playwright++ Features:
6
+ * - Video recording (always/retain-on-fail/never)
7
+ * - Automatic screenshots (before/after steps, on error)
8
+ * - Trace capture for debugging
9
+ * - Artifacts management with CAS
10
+ * - HTML report generation
11
+ */
12
+ import { chromium, firefox, webkit } from '@playwright/test';
13
+ import { SecurityRedactor } from '../security/redactor.js';
14
+ import { createAssertionsEngine } from '../assertions/index.js';
15
+ import { UIArtifactsManager } from '../artifacts/index.js';
16
+ import { HTMLReporter } from '../reporting/index.js';
17
+ import { SiteProfiler } from '../crawler/site-profiler.js';
18
+ import { IntelligentSelectorGenerator } from '../crawler/intelligent-selector-generator.js';
19
+ import { ConsentHandler } from '../crawler/consent-handler.js';
20
+ import { mkdirSync, existsSync } from 'fs';
21
+ import { join } from 'path';
22
+ export class PlaywrightUiAdapter {
23
+ browser;
24
+ context;
25
+ page;
26
+ redactor;
27
+ auth;
28
+ assertions;
29
+ // Storage for artifacts
30
+ artifactDir;
31
+ videoDir;
32
+ traceDir;
33
+ // Playwright++: Artifacts manager
34
+ artifactsManager;
35
+ failureCount = 0;
36
+ currentTestId;
37
+ allScreenshots = [];
38
+ allVideos = [];
39
+ allTraces = [];
40
+ // ═══════════════════════════════════════════════════════════════════════════════
41
+ // DIALOG & POPUP HANDLING (Part 2 & 3)
42
+ // ═══════════════════════════════════════════════════════════════════════════════
43
+ lastDialog;
44
+ dialogBehavior = 'accept';
45
+ dialogPromptValue = 'test-value';
46
+ openPages = [];
47
+ currentPageIndex = 0;
48
+ globalDialogHandler; // Reference to global handler for temporary removal
49
+ pendingDialogHandler; // Stores pending expectDialog handler for next actions
50
+ // ═══════════════════════════════════════════════════════════════════════════════
51
+ // INTELLIGENT CRAWLER (SiteProfiler, SelectorGenerator, ConsentHandler)
52
+ // ═══════════════════════════════════════════════════════════════════════════════
53
+ siteProfiler;
54
+ siteAnalysis;
55
+ siteProfile;
56
+ selectorGenerator;
57
+ consentHandler = ConsentHandler;
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+ // IFRAME SUPPORT (Part 5)
60
+ // ═══════════════════════════════════════════════════════════════════════════════
61
+ currentFrame;
62
+ constructor() {
63
+ this.redactor = SecurityRedactor.forLogs();
64
+ this.artifactDir = '.qa360/artifacts/ui';
65
+ this.videoDir = `${this.artifactDir}/videos`;
66
+ this.traceDir = `${this.artifactDir}/traces`;
67
+ // Initialize intelligent crawler components
68
+ this.siteProfiler = new SiteProfiler();
69
+ this.selectorGenerator = new IntelligentSelectorGenerator();
70
+ }
71
+ /**
72
+ * Set authentication credentials for requests
73
+ */
74
+ setAuth(credentials) {
75
+ this.auth = credentials;
76
+ }
77
+ /**
78
+ * Execute UI smoke tests with accessibility
79
+ * Playwright++: Supports artifacts, screenshots, video, trace, HTML reporting
80
+ */
81
+ async runSmokeTests(config) {
82
+ const startTime = Date.now();
83
+ const outputDir = config.artifacts?.outputDir || this.artifactDir;
84
+ // Ensure output directories exist
85
+ for (const dir of [outputDir, this.videoDir, this.traceDir]) {
86
+ if (!existsSync(dir)) {
87
+ mkdirSync(dir, { recursive: true });
88
+ }
89
+ }
90
+ // Initialize artifacts manager if Playwright++ features enabled
91
+ const artifactsEnabled = config.artifacts?.screenshots !== 'never' ||
92
+ config.artifacts?.video !== 'never' ||
93
+ config.artifacts?.trace !== 'never';
94
+ if (artifactsEnabled) {
95
+ this.artifactsManager = new UIArtifactsManager(outputDir, '.qa360/runs/cas');
96
+ }
97
+ try {
98
+ // Store auth config
99
+ this.auth = config.auth;
100
+ this.failureCount = 0;
101
+ this.allScreenshots = [];
102
+ this.allVideos = [];
103
+ this.allTraces = [];
104
+ await this.setupBrowser(config);
105
+ const results = [];
106
+ const pages = config.target.pages || [config.target.baseUrl];
107
+ console.log(`🖥️ Running UI smoke tests (${pages.length} pages)`);
108
+ // Optional login first
109
+ if (config.login) {
110
+ await this.performLogin(config.login);
111
+ }
112
+ for (const pageUrl of pages) {
113
+ // Start artifacts for this test
114
+ this.currentTestId = `smoke-${this.failureCount}`;
115
+ this.artifactsManager?.startTest(this.currentTestId);
116
+ // Check if page has actions (object format) or is a simple URL string
117
+ let testResult;
118
+ if (typeof pageUrl === 'string') {
119
+ testResult = await this.testPage(pageUrl, config);
120
+ }
121
+ else if (pageUrl && typeof pageUrl === 'object' && 'url' in pageUrl) {
122
+ const pageObj = pageUrl;
123
+ if (pageObj.actions && Array.isArray(pageObj.actions) && pageObj.actions.length > 0) {
124
+ // Page with actions (journey format)
125
+ testResult = await this.testPageWithActions(pageObj.url, pageObj.actions, config);
126
+ }
127
+ else {
128
+ // Object format but no actions
129
+ testResult = await this.testPage(pageObj.url, config);
130
+ }
131
+ }
132
+ else {
133
+ // Fallback
134
+ testResult = await this.testPage(String(pageUrl), config);
135
+ }
136
+ results.push(testResult);
137
+ const displayUrl = typeof pageUrl === 'string' ? pageUrl : pageUrl.url;
138
+ if (testResult.success) {
139
+ const a11yInfo = testResult.accessibility ?
140
+ ` | A11y: ${testResult.accessibility.score}% (${testResult.accessibility.violations.length} issues)` : '';
141
+ console.log(` ✅ ${displayUrl} -> ${testResult.loadTime}ms${a11yInfo}`);
142
+ }
143
+ else {
144
+ this.failureCount++;
145
+ console.log(` ❌ ${displayUrl} -> ${testResult.error}`);
146
+ // Check bail condition
147
+ if (config.bail && this.failureCount >= config.bail) {
148
+ console.log(` 🛑 Bailing after ${this.failureCount} failures`);
149
+ break;
150
+ }
151
+ }
152
+ this.artifactsManager?.endTest();
153
+ }
154
+ // Run E2E tests if defined
155
+ let e2eResults = [];
156
+ if (config.target.uiTests && config.target.uiTests.length > 0) {
157
+ console.log(`🧪 Running E2E tests (${config.target.uiTests.length} tests)`);
158
+ for (const test of config.target.uiTests) {
159
+ if (test.enabled !== false) {
160
+ // Start artifacts for this test
161
+ this.currentTestId = `e2e-${test.name}`;
162
+ this.artifactsManager?.startTest(this.currentTestId);
163
+ const result = await this.runE2eTest(test, config);
164
+ e2eResults.push(result);
165
+ const status = result.success ? '✅' : '❌';
166
+ console.log(` ${status} ${test.name} (${result.duration}ms)`);
167
+ if (!result.success) {
168
+ console.log(` Error: ${result.error}`);
169
+ this.failureCount++;
170
+ // Check bail condition
171
+ if (config.bail && this.failureCount >= config.bail) {
172
+ console.log(` 🛑 Bailing after ${this.failureCount} failures`);
173
+ this.artifactsManager?.endTest();
174
+ break;
175
+ }
176
+ }
177
+ this.artifactsManager?.endTest();
178
+ }
179
+ }
180
+ }
181
+ const summary = this.calculateSummary(results, e2eResults);
182
+ const junit = this.generateJUnit(results, e2eResults);
183
+ // Playwright++: Generate HTML report if requested
184
+ if (config.htmlReport) {
185
+ await this.generateHtmlReport(config, results, e2eResults, summary);
186
+ }
187
+ return {
188
+ success: summary.failed === 0,
189
+ results,
190
+ e2eResults,
191
+ summary,
192
+ junit
193
+ };
194
+ }
195
+ finally {
196
+ await this.cleanup(config);
197
+ }
198
+ }
199
+ /**
200
+ * Run a single E2E test
201
+ * Playwright++: Takes before/after screenshots, captures artifacts on failure
202
+ */
203
+ async runE2eTest(test, config) {
204
+ const startTime = Date.now();
205
+ const steps = [];
206
+ const screenshotMode = config.artifacts?.screenshots || 'never';
207
+ try {
208
+ // Determine starting URL
209
+ const startUrl = test.url || `${config.target.baseUrl.replace(/\/$/, '')}${test.path || ''}`;
210
+ // Navigate to start URL
211
+ console.log(` 📍 Navigate to: ${startUrl}`);
212
+ await this.page.goto(startUrl, { timeout: test.timeout || config.timeout || 30000 });
213
+ // Take initial screenshot
214
+ if (screenshotMode === 'always') {
215
+ await this.artifactsManager?.takeScreenshot(this.page, {}, {
216
+ testId: this.currentTestId || 'unknown',
217
+ type: 'screenshot',
218
+ tags: ['initial'],
219
+ });
220
+ }
221
+ // Initialize assertions engine
222
+ this.assertions = createAssertionsEngine(this.page);
223
+ // Execute each step
224
+ for (let i = 0; i < test.steps.length; i++) {
225
+ const step = test.steps[i];
226
+ // Take before screenshot if configured
227
+ if (screenshotMode === 'always') {
228
+ await this.artifactsManager?.takeBeforeScreenshot(this.page, step.action || 'step', i);
229
+ }
230
+ const stepResult = await this.executeStep(step, config);
231
+ steps.push(stepResult);
232
+ // Take after screenshot (always on failure, or always if configured)
233
+ if (screenshotMode === 'always' || (screenshotMode === 'only-on-failure' && !stepResult.success)) {
234
+ await this.artifactsManager?.takeAfterScreenshot(this.page, step.action || 'step', i, stepResult.success);
235
+ }
236
+ if (!stepResult.success) {
237
+ // Take error screenshot
238
+ await this.artifactsManager?.takeErrorScreenshot(this.page, new Error(stepResult.error || 'Step failed'), step.action);
239
+ return {
240
+ test,
241
+ success: false,
242
+ steps,
243
+ duration: Date.now() - startTime,
244
+ error: stepResult.error,
245
+ };
246
+ }
247
+ }
248
+ return {
249
+ test,
250
+ success: true,
251
+ steps,
252
+ duration: Date.now() - startTime,
253
+ };
254
+ }
255
+ catch (error) {
256
+ // Take error screenshot
257
+ await this.artifactsManager?.takeErrorScreenshot(this.page, error);
258
+ return {
259
+ test,
260
+ success: false,
261
+ steps,
262
+ duration: Date.now() - startTime,
263
+ error: this.redactor.redact(error instanceof Error ? error.message : 'Unknown error'),
264
+ };
265
+ }
266
+ }
267
+ /**
268
+ * Execute a single UI test step
269
+ * Playwright++: Enhanced error handling with artifacts
270
+ */
271
+ async executeStep(step, config) {
272
+ const startTime = Date.now();
273
+ let screenshot;
274
+ try {
275
+ const { action, selector, value, options = {}, expected } = step;
276
+ const opts = { timeout: 10000, ...options };
277
+ switch (action) {
278
+ case 'navigate':
279
+ await this.page.goto(value, opts);
280
+ break;
281
+ case 'click':
282
+ // If there's a pending dialog handler, the click should trigger it
283
+ if (this.pendingDialogHandler && this.pendingDialogHandler.promise) {
284
+ // Do the click and wait for the dialog to be handled
285
+ await Promise.all([
286
+ this.pendingDialogHandler.promise,
287
+ this.page.click(selector, opts)
288
+ ]);
289
+ // Clear the pending handler after dialog is handled
290
+ this.pendingDialogHandler = undefined;
291
+ }
292
+ else {
293
+ await this.page.click(selector, opts);
294
+ }
295
+ break;
296
+ case 'dblClick':
297
+ await this.page.dblclick(selector, opts);
298
+ break;
299
+ case 'rightClick':
300
+ await this.page.click(selector, { ...opts, button: 'right' });
301
+ break;
302
+ case 'hover':
303
+ await this.page.hover(selector, opts);
304
+ break;
305
+ case 'focus':
306
+ await this.page.focus(selector, opts);
307
+ break;
308
+ case 'fill':
309
+ await this.page.fill(selector, value, opts);
310
+ break;
311
+ case 'type':
312
+ await this.page.type(selector, value, opts);
313
+ break;
314
+ case 'clear':
315
+ await this.page.fill(selector, '', opts);
316
+ break;
317
+ case 'select':
318
+ if (value !== undefined) {
319
+ await this.page.selectOption(selector, value, opts);
320
+ }
321
+ break;
322
+ case 'check':
323
+ await this.page.check(selector, opts);
324
+ break;
325
+ case 'uncheck':
326
+ await this.page.uncheck(selector, opts);
327
+ break;
328
+ case 'upload':
329
+ if (value !== undefined) {
330
+ await this.page.setInputFiles(selector, value, opts);
331
+ }
332
+ break;
333
+ case 'press': {
334
+ const delay = options?.delay;
335
+ const pressOpts = delay !== undefined ? { delay } : {};
336
+ await this.page.keyboard.press(value, pressOpts);
337
+ break;
338
+ }
339
+ case 'waitFor':
340
+ case 'waitForSelector':
341
+ await this.page.waitForSelector(selector, opts);
342
+ break;
343
+ case 'waitForNavigation':
344
+ await this.page.waitForNavigation(opts);
345
+ break;
346
+ case 'waitForTimeout':
347
+ await this.page.waitForTimeout(parseInt(value, 10));
348
+ break;
349
+ case 'scroll':
350
+ if (selector) {
351
+ await this.page.evaluate((sel) => {
352
+ const el = document.querySelector(sel);
353
+ if (el)
354
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
355
+ }, selector);
356
+ }
357
+ break;
358
+ case 'dragAndDrop':
359
+ await this.page.dragAndDrop(selector, value, opts);
360
+ break;
361
+ case 'tap':
362
+ await this.page.tap(selector, opts);
363
+ break;
364
+ // ═══════════════════════════════════════════════════════════════════════════════
365
+ // PART 2: DIALOG ACTIONS
366
+ // ═══════════════════════════════════════════════════════════════════════════════
367
+ case 'expectDialog':
368
+ // Set up expectation for a dialog that will be triggered by a subsequent action
369
+ // This action completes immediately; the next action (click, etc.) will wait for the dialog
370
+ const expectedDialogType = value || 'alert';
371
+ const dialogTimeout = options?.timeout || 10000;
372
+ const shouldDismiss = options?.dismiss === true;
373
+ console.log(` 🔔 Expecting ${expectedDialogType} dialog on next action...`);
374
+ // Temporarily remove global dialog handler to prevent interference
375
+ if (this.globalDialogHandler) {
376
+ this.page.off('dialog', this.globalDialogHandler);
377
+ }
378
+ // Variables to store handler and timeout for cleanup
379
+ let dialogHandler;
380
+ let timeoutId;
381
+ // Create a promise that will resolve when the dialog appears
382
+ const dialogPromise = new Promise((resolve, reject) => {
383
+ timeoutId = setTimeout(() => {
384
+ // Clean up on timeout
385
+ if (dialogHandler)
386
+ this.page.off('dialog', dialogHandler);
387
+ this.pendingDialogHandler = undefined;
388
+ // Re-attach global handler
389
+ if (this.globalDialogHandler) {
390
+ this.page.on('dialog', this.globalDialogHandler);
391
+ }
392
+ reject(new Error(`No ${expectedDialogType} dialog within ${dialogTimeout}ms`));
393
+ }, dialogTimeout);
394
+ dialogHandler = async (dialog) => {
395
+ if (dialog.type() === expectedDialogType) {
396
+ clearTimeout(timeoutId);
397
+ // Remove this handler after it's been triggered
398
+ this.page.off('dialog', dialogHandler);
399
+ if (shouldDismiss) {
400
+ await dialog.dismiss();
401
+ console.log(` 🔔 Dialog [${expectedDialogType}] dismissed`);
402
+ }
403
+ else if (expectedDialogType === 'prompt' && options?.promptValue) {
404
+ await dialog.accept(options.promptValue);
405
+ console.log(` 🔔 Dialog [${expectedDialogType}] accepted with value`);
406
+ }
407
+ else {
408
+ await dialog.accept();
409
+ console.log(` 🔔 Dialog [${expectedDialogType}] accepted`);
410
+ }
411
+ // Re-attach global dialog handler after dialog is handled
412
+ if (this.globalDialogHandler) {
413
+ this.page.on('dialog', this.globalDialogHandler);
414
+ }
415
+ resolve();
416
+ }
417
+ };
418
+ // Attach the dialog handler
419
+ this.page.on('dialog', dialogHandler);
420
+ });
421
+ // Store the pending handler and promise so subsequent actions can wait for it
422
+ this.pendingDialogHandler = {
423
+ expectedType: expectedDialogType,
424
+ promise: dialogPromise,
425
+ handler: dialogHandler,
426
+ timeoutId
427
+ };
428
+ break;
429
+ // ═══════════════════════════════════════════════════════════════════════════════
430
+ // PART 3: POPUP/TAB ACTIONS
431
+ // ═══════════════════════════════════════════════════════════════════════════════
432
+ case 'clickAndWaitForPopup': {
433
+ const [popup] = await Promise.all([
434
+ this.context.waitForEvent('page', { timeout: options?.timeout || 30000 }),
435
+ this.page.click(selector, opts),
436
+ ]);
437
+ await popup.waitForLoadState('domcontentloaded');
438
+ this.openPages.push(popup);
439
+ this.currentPageIndex = this.openPages.length - 1;
440
+ this.page = popup;
441
+ console.log(` 📑 Switched to new tab: ${popup.url().slice(0, 50)}...`);
442
+ break;
443
+ }
444
+ case 'switchToTab': {
445
+ const tabIndex = parseInt(value, 10);
446
+ if (tabIndex >= 0 && tabIndex < this.openPages.length) {
447
+ this.page = this.openPages[tabIndex];
448
+ this.currentPageIndex = tabIndex;
449
+ await this.page.bringToFront();
450
+ console.log(` 📑 Switched to tab ${tabIndex}: ${this.page.url().slice(0, 50)}...`);
451
+ }
452
+ else {
453
+ throw new Error(`Tab index ${tabIndex} out of range (0-${this.openPages.length - 1})`);
454
+ }
455
+ break;
456
+ }
457
+ case 'closeTab': {
458
+ const closeIndex = value !== undefined ? parseInt(value, 10) : this.openPages.length - 1;
459
+ if (closeIndex > 0) { // Never close first tab
460
+ await this.openPages[closeIndex].close();
461
+ this.openPages.splice(closeIndex, 1);
462
+ this.currentPageIndex = this.openPages.length - 1;
463
+ this.page = this.openPages[this.currentPageIndex];
464
+ await this.page.bringToFront();
465
+ console.log(` 📑 Closed tab ${closeIndex}, switched to tab ${this.currentPageIndex}`);
466
+ }
467
+ else if (closeIndex === 0 && this.openPages.length > 1) {
468
+ throw new Error('Cannot close the first tab when multiple tabs are open');
469
+ }
470
+ break;
471
+ }
472
+ case 'waitForNewTab': {
473
+ const newTab = await this.context.waitForEvent('page', { timeout: options?.timeout || 30000 });
474
+ await newTab.waitForLoadState('domcontentloaded');
475
+ this.openPages.push(newTab);
476
+ if (options?.switchTo !== false) {
477
+ this.currentPageIndex = this.openPages.length - 1;
478
+ this.page = newTab;
479
+ console.log(` 📑 Switched to new tab: ${newTab.url().slice(0, 50)}...`);
480
+ }
481
+ else {
482
+ console.log(` 📑 New tab detected (not switched): ${newTab.url().slice(0, 50)}...`);
483
+ }
484
+ break;
485
+ }
486
+ // ═══════════════════════════════════════════════════════════════════════════════
487
+ // PART 5: IFRAME ACTIONS
488
+ // ═══════════════════════════════════════════════════════════════════════════════
489
+ case 'switchToFrame': {
490
+ if (!selector) {
491
+ throw new Error('switchToFrame requires a selector');
492
+ }
493
+ // Wait for iframe element and get its content frame
494
+ const frameElement = await this.page.waitForSelector(selector, opts);
495
+ if (!frameElement) {
496
+ throw new Error(`Frame element not found: ${selector}`);
497
+ }
498
+ const contentFrame = await frameElement.contentFrame();
499
+ if (!contentFrame) {
500
+ throw new Error(`Frame has no content frame: ${selector}`);
501
+ }
502
+ // Store the current frame for subsequent operations
503
+ this.currentFrame = contentFrame;
504
+ console.log(` 🔲 Switched to frame: ${selector}`);
505
+ break;
506
+ }
507
+ case 'switchToMainFrame':
508
+ // Return to main page context
509
+ this.currentFrame = undefined;
510
+ console.log(` 🔲 Switched to main frame`);
511
+ break;
512
+ case 'waitForFrame': {
513
+ if (!selector) {
514
+ throw new Error('waitForFrame requires a selector');
515
+ }
516
+ const frameElement = await this.page.waitForSelector(selector, opts);
517
+ if (!frameElement) {
518
+ throw new Error(`Frame element not found within timeout: ${selector}`);
519
+ }
520
+ const contentFrame = await frameElement.contentFrame();
521
+ if (!contentFrame) {
522
+ throw new Error(`Frame has no content: ${selector}`);
523
+ }
524
+ console.log(` 🔲 Frame detected: ${selector}`);
525
+ break;
526
+ }
527
+ case 'clickInFrame': {
528
+ if (!selector) {
529
+ throw new Error('clickInFrame requires a selector');
530
+ }
531
+ const frameSelector = options?.frame;
532
+ if (!frameSelector) {
533
+ throw new Error('clickInFrame requires frame option');
534
+ }
535
+ const frameElement = await this.page.waitForSelector(frameSelector, opts);
536
+ if (!frameElement) {
537
+ throw new Error(`Frame element not found: ${frameSelector}`);
538
+ }
539
+ const contentFrame = await frameElement.contentFrame();
540
+ if (!contentFrame) {
541
+ throw new Error(`Frame has no content: ${frameSelector}`);
542
+ }
543
+ await contentFrame.click(selector, opts);
544
+ console.log(` 🔲 Clicked in frame: ${frameSelector} > ${selector}`);
545
+ break;
546
+ }
547
+ case 'fillInFrame': {
548
+ if (!selector) {
549
+ throw new Error('fillInFrame requires a selector');
550
+ }
551
+ const frameSelector = options?.frame;
552
+ if (!frameSelector) {
553
+ throw new Error('fillInFrame requires frame option');
554
+ }
555
+ const frameElement = await this.page.waitForSelector(frameSelector, opts);
556
+ if (!frameElement) {
557
+ throw new Error(`Frame element not found: ${frameSelector}`);
558
+ }
559
+ const contentFrame = await frameElement.contentFrame();
560
+ if (!contentFrame) {
561
+ throw new Error(`Frame has no content: ${frameSelector}`);
562
+ }
563
+ await contentFrame.fill(selector, value, opts);
564
+ console.log(` 🔲 Filled in frame: ${frameSelector} > ${selector} = "${value}"`);
565
+ break;
566
+ }
567
+ // ═══════════════════════════════════════════════════════════════════════════════
568
+ // PART 6: SHADOW DOM ACTIONS
569
+ // ═══════════════════════════════════════════════════════════════════════════════
570
+ case 'clickShadow': {
571
+ if (!selector) {
572
+ throw new Error('clickShadow requires a selector');
573
+ }
574
+ // Use pierce selector to penetrate shadow DOM
575
+ const pierceSelector = `pierce/${selector}`;
576
+ await this.page.click(pierceSelector, opts);
577
+ console.log(` 🌑 Clicked shadow element: ${selector}`);
578
+ break;
579
+ }
580
+ case 'fillShadow': {
581
+ if (!selector) {
582
+ throw new Error('fillShadow requires a selector');
583
+ }
584
+ const pierceSelector = `pierce/${selector}`;
585
+ await this.page.fill(pierceSelector, value, opts);
586
+ console.log(` 🌑 Filled shadow element: ${selector}`);
587
+ break;
588
+ }
589
+ case 'waitForShadow': {
590
+ if (!selector) {
591
+ throw new Error('waitForShadow requires a selector');
592
+ }
593
+ const pierceSelector = `pierce/${selector}`;
594
+ await this.page.waitForSelector(pierceSelector, opts);
595
+ console.log(` 🌑 Shadow element detected: ${selector}`);
596
+ break;
597
+ }
598
+ // ═══════════════════════════════════════════════════════════════════════════════
599
+ // PART 7: DOWNLOAD ACTIONS
600
+ // ═══════════════════════════════════════════════════════════════════════════════
601
+ case 'clickAndDownload': {
602
+ if (!selector) {
603
+ throw new Error('clickAndDownload requires a selector');
604
+ }
605
+ // Set up download handler before clicking
606
+ const downloadPromise = this.page.waitForEvent('download', {
607
+ timeout: options?.timeout || 30000,
608
+ });
609
+ await this.page.click(selector, opts);
610
+ const download = await downloadPromise;
611
+ const downloadPath = options?.savePath
612
+ ? await download.saveAs(options.savePath)
613
+ : await download.path();
614
+ console.log(` 📥 Download started: ${download.suggestedFilename()}`);
615
+ console.log(` 📥 Saved to: ${downloadPath || 'browser default location'}`);
616
+ break;
617
+ }
618
+ case 'waitForDownload': {
619
+ const download = await this.page.waitForEvent('download', {
620
+ timeout: options?.timeout || 30000,
621
+ });
622
+ const downloadPath = options?.savePath
623
+ ? await download.saveAs(options.savePath)
624
+ : await download.path();
625
+ console.log(` 📥 Download detected: ${download.suggestedFilename()}`);
626
+ console.log(` 📥 Saved to: ${downloadPath || 'browser default location'}`);
627
+ break;
628
+ }
629
+ // ═══════════════════════════════════════════════════════════════════════════════
630
+ // PART 8: NETWORK MOCKING/ACTIONS
631
+ // ═══════════════════════════════════════════════════════════════════════════════
632
+ case 'waitForResponse': {
633
+ const urlPattern = value || selector;
634
+ if (!urlPattern) {
635
+ throw new Error('waitForResponse requires url pattern');
636
+ }
637
+ const response = await this.page.waitForResponse(urlPattern, opts);
638
+ console.log(` 🌐 Response received: ${response.url()} (${response.status()})`);
639
+ break;
640
+ }
641
+ case 'mockRoute': {
642
+ const urlPattern = value;
643
+ if (!urlPattern) {
644
+ throw new Error('mockRoute requires url pattern');
645
+ }
646
+ const mockData = options?.mockData;
647
+ const mockStatus = options?.mockStatus || 200;
648
+ await this.page.route(urlPattern, (route) => {
649
+ if (mockData) {
650
+ route.fulfill({
651
+ status: mockStatus,
652
+ contentType: options?.contentType || 'application/json',
653
+ body: typeof mockData === 'string' ? mockData : JSON.stringify(mockData),
654
+ });
655
+ }
656
+ else {
657
+ route.abort();
658
+ }
659
+ });
660
+ console.log(` 🌐 Route mocked: ${urlPattern}`);
661
+ break;
662
+ }
663
+ case 'unmockRoute': {
664
+ const urlPattern = value || '**/*';
665
+ await this.page.unroute(urlPattern);
666
+ console.log(` 🌐 Route unmocked: ${urlPattern}`);
667
+ break;
668
+ }
669
+ // ═══════════════════════════════════════════════════════════════════════════════
670
+ // PART 9: STORAGE ACTIONS
671
+ // ═══════════════════════════════════════════════════════════════════════════════
672
+ case 'setLocalStorage': {
673
+ if (!selector || !value) {
674
+ throw new Error('setLocalStorage requires key (selector) and value');
675
+ }
676
+ await this.page.evaluate((args) => localStorage.setItem(args.key, args.value), { key: selector, value });
677
+ console.log(` 💾 LocalStorage set: ${selector}`);
678
+ break;
679
+ }
680
+ case 'getLocalStorage': {
681
+ if (!selector) {
682
+ throw new Error('getLocalStorage requires key (selector)');
683
+ }
684
+ const result = await this.page.evaluate((key) => localStorage.getItem(key), selector);
685
+ console.log(` 💾 LocalStorage get: ${selector} = ${result}`);
686
+ break;
687
+ }
688
+ case 'clearLocalStorage':
689
+ await this.page.evaluate(() => localStorage.clear());
690
+ console.log(` 💾 LocalStorage cleared`);
691
+ break;
692
+ case 'setSessionStorage': {
693
+ if (!selector || !value) {
694
+ throw new Error('setSessionStorage requires key (selector) and value');
695
+ }
696
+ await this.page.evaluate((args) => sessionStorage.setItem(args.key, args.value), { key: selector, value });
697
+ console.log(` 💾 SessionStorage set: ${selector}`);
698
+ break;
699
+ }
700
+ case 'getSessionStorage': {
701
+ if (!selector) {
702
+ throw new Error('getSessionStorage requires key (selector)');
703
+ }
704
+ const result = await this.page.evaluate((key) => sessionStorage.getItem(key), selector);
705
+ console.log(` 💾 SessionStorage get: ${selector} = ${result}`);
706
+ break;
707
+ }
708
+ case 'clearSessionStorage':
709
+ await this.page.evaluate(() => sessionStorage.clear());
710
+ console.log(` 💾 SessionStorage cleared`);
711
+ break;
712
+ case 'setCookie': {
713
+ const cookieName = selector;
714
+ const cookieValue = value;
715
+ if (!cookieName || !cookieValue) {
716
+ throw new Error('setCookie requires name (selector) and value');
717
+ }
718
+ await this.context.addCookies([
719
+ {
720
+ name: cookieName,
721
+ value: cookieValue,
722
+ domain: new URL(this.page.url()).hostname,
723
+ path: '/',
724
+ },
725
+ ]);
726
+ console.log(` 🍪 Cookie set: ${cookieName}`);
727
+ break;
728
+ }
729
+ case 'getCookie': {
730
+ if (!selector) {
731
+ throw new Error('getCookie requires cookie name (selector)');
732
+ }
733
+ const cookies = await this.context.cookies();
734
+ const cookie = cookies.find(c => c.name === selector);
735
+ console.log(` 🍪 Cookie get: ${selector} = ${cookie?.value || 'undefined'}`);
736
+ break;
737
+ }
738
+ case 'clearCookies':
739
+ await this.context.clearCookies();
740
+ console.log(` 🍪 All cookies cleared`);
741
+ break;
742
+ // ═══════════════════════════════════════════════════════════════════════════════
743
+ // PART 10: PERMISSIONS/GEOLOCATION ACTIONS
744
+ // ═══════════════════════════════════════════════════════════════════════════════
745
+ case 'grantPermissions': {
746
+ const permissions = value ? value.split(',').map((p) => p.trim()) : ['geolocation'];
747
+ await this.context.grantPermissions(permissions);
748
+ console.log(` 🔐 Permissions granted: ${permissions.join(', ')}`);
749
+ break;
750
+ }
751
+ case 'setGeolocation': {
752
+ const coords = value ? JSON.parse(value) : { latitude: 48.8566, longitude: 2.3522 }; // Paris default
753
+ await this.context.setGeolocation(coords);
754
+ console.log(` 📍 Geolocation set: ${coords.latitude}, ${coords.longitude}`);
755
+ break;
756
+ }
757
+ case 'clearGeolocation':
758
+ await this.context.setGeolocation({ latitude: 0, longitude: 0 });
759
+ console.log(` 📍 Geolocation cleared`);
760
+ break;
761
+ // ═══════════════════════════════════════════════════════════════════════════════
762
+ // PART 11: MOBILE/VIEWPORT ACTIONS
763
+ // ═══════════════════════════════════════════════════════════════════════════════
764
+ case 'setViewport': {
765
+ const viewportSize = value ? JSON.parse(value) : { width: 375, height: 667 };
766
+ await this.page.setViewportSize(viewportSize);
767
+ console.log(` 📱 Viewport set: ${viewportSize.width}x${viewportSize.height}`);
768
+ break;
769
+ }
770
+ case 'emulateDevice': {
771
+ const device = value || 'iPhone';
772
+ // Playwright has built-in device descriptors
773
+ const devices = {
774
+ 'iPhone': { viewport: { width: 375, height: 667 }, userAgent: 'iPhone' },
775
+ 'iPad': { viewport: { width: 768, height: 1024 }, userAgent: 'iPad' },
776
+ 'Pixel 5': { viewport: { width: 393, height: 851 }, userAgent: 'Pixel 5' },
777
+ };
778
+ const deviceConfig = devices[device];
779
+ if (deviceConfig) {
780
+ await this.page.setViewportSize(deviceConfig.viewport);
781
+ if (deviceConfig.userAgent) {
782
+ await this.page.setExtraHTTPHeaders({ 'User-Agent': deviceConfig.userAgent });
783
+ }
784
+ console.log(` 📱 Emulating device: ${device}`);
785
+ }
786
+ else {
787
+ console.log(` ⚠️ Unknown device: ${device}`);
788
+ }
789
+ break;
790
+ }
791
+ case 'touchAction':
792
+ // Touch actions are handled by tap/click in Playwright
793
+ await this.page.tap(selector, opts);
794
+ console.log(` 👆 Touched: ${selector}`);
795
+ break;
796
+ // ═══════════════════════════════════════════════════════════════════════════════
797
+ // PART 12: ADDITIONAL ACTIONS
798
+ // ═══════════════════════════════════════════════════════════════════════════════
799
+ case 'retry': {
800
+ const maxRetries = options?.retries || 3;
801
+ let lastError = null;
802
+ for (let i = 0; i < maxRetries; i++) {
803
+ try {
804
+ if (selector) {
805
+ await this.page.click(selector, opts);
806
+ }
807
+ console.log(` 🔄 Retry succeeded on attempt ${i + 1}`);
808
+ break;
809
+ }
810
+ catch (error) {
811
+ lastError = error;
812
+ if (i < maxRetries - 1) {
813
+ await this.page.waitForTimeout(options?.delay || 1000);
814
+ }
815
+ }
816
+ }
817
+ if (lastError && options?.retries !== 0) {
818
+ throw lastError;
819
+ }
820
+ break;
821
+ }
822
+ case 'waitForFunction': {
823
+ const script = value || selector;
824
+ if (!script) {
825
+ throw new Error('waitForFunction requires script (value or selector)');
826
+ }
827
+ await this.page.waitForFunction(script, undefined, opts);
828
+ console.log(` ⏳ Function completed: ${script.slice(0, 50)}...`);
829
+ break;
830
+ }
831
+ case 'evaluate': {
832
+ const script = value || selector;
833
+ if (!script) {
834
+ throw new Error('evaluate requires script (value or selector)');
835
+ }
836
+ // Use current frame if active, otherwise use main page
837
+ const context = this.currentFrame || this.page;
838
+ const result = await context.evaluate(script);
839
+ const scriptPreview = script.slice(0, 50);
840
+ const resultPreview = result !== undefined ? JSON.stringify(result).slice(0, 50) : 'undefined';
841
+ console.log(` 📝 Evaluated: ${scriptPreview}... => ${resultPreview}...`);
842
+ break;
843
+ }
844
+ case 'waitForURL': {
845
+ const urlPattern = value || selector;
846
+ if (!urlPattern) {
847
+ throw new Error('waitForURL requires url pattern (value or selector)');
848
+ }
849
+ await this.page.waitForURL(urlPattern, opts);
850
+ console.log(` 🔗 URL matched: ${urlPattern}`);
851
+ break;
852
+ }
853
+ case 'reload':
854
+ await this.page.reload(opts);
855
+ console.log(` 🔄 Page reloaded`);
856
+ break;
857
+ case 'goBack':
858
+ await this.page.goBack(opts);
859
+ console.log(` ⬅️ Navigated back`);
860
+ break;
861
+ case 'goForward':
862
+ await this.page.goForward(opts);
863
+ console.log(` ➡️ Navigated forward`);
864
+ break;
865
+ // ═══════════════════════════════════════════════════════════════════════════════
866
+ // PART 10: INTELLIGENT CRAWLER ACTIONS
867
+ // ═══════════════════════════════════════════════════════════════════════════════
868
+ case 'analyzeSite': {
869
+ console.log(` 🔍 Analyzing site profile...`);
870
+ // Run site analysis
871
+ this.siteAnalysis = await this.siteProfiler.analyzeSite(this.page);
872
+ this.siteProfile = this.siteProfiler.determineProfile(this.siteAnalysis);
873
+ // Initialize selector generator with analysis
874
+ this.selectorGenerator.initialize(this.siteAnalysis);
875
+ // Store results in localStorage for verification
876
+ const profileData = {
877
+ profile: this.siteProfile,
878
+ framework: this.siteAnalysis.framework,
879
+ cssFramework: this.siteAnalysis.cssFramework,
880
+ testabilityScore: this.siteAnalysis.testabilityScore,
881
+ accessibilityScore: this.siteAnalysis.accessibilityScore,
882
+ hasTestIds: this.siteAnalysis.hasTestIds,
883
+ hasAriaLabels: this.siteAnalysis.hasAriaLabels,
884
+ };
885
+ await this.page.evaluate((data) => {
886
+ localStorage.setItem('qa360_site_profile', JSON.stringify(data));
887
+ }, profileData);
888
+ console.log(` 🎯 Site Profile: ${this.siteProfile}`);
889
+ console.log(` 📊 Framework: ${this.siteAnalysis.framework || 'none'}, CSS: ${this.siteAnalysis.cssFramework || 'none'}`);
890
+ console.log(` ✅ Testability: ${this.siteAnalysis.testabilityScore}%, A11y: ${this.siteAnalysis.accessibilityScore}%`);
891
+ break;
892
+ }
893
+ case 'handleConsent': {
894
+ const consentAction = value || 'accept';
895
+ const consentTimeout = options?.timeout || 3000;
896
+ const customSelector = options?.customSelector;
897
+ console.log(` 🍪 Handling consent banners (${consentAction})...`);
898
+ const handled = await this.consentHandler.handleConsent(this.page, {
899
+ action: consentAction,
900
+ timeout: consentTimeout,
901
+ customSelector
902
+ });
903
+ if (handled) {
904
+ console.log(` ✅ Consent banner handled`);
905
+ }
906
+ else {
907
+ console.log(` ℹ️ No consent banner detected`);
908
+ }
909
+ break;
910
+ }
911
+ case 'generateSelector': {
912
+ // Generate optimal selector for element
913
+ if (!selector) {
914
+ throw new Error('generateSelector requires a selector to analyze');
915
+ }
916
+ const element = await this.page.$(selector);
917
+ if (!element) {
918
+ throw new Error(`Element not found: ${selector}`);
919
+ }
920
+ // Extract element info
921
+ const elementInfo = await element.evaluate((el) => ({
922
+ tagName: el.tagName?.toLowerCase(),
923
+ id: el.id,
924
+ className: el.className,
925
+ textContent: el.textContent?.trim().substring(0, 50),
926
+ role: el.getAttribute('role'),
927
+ name: el.getAttribute('name'),
928
+ value: el.getAttribute('value'),
929
+ type: el.getAttribute('type'),
930
+ attributes: {
931
+ 'data-testid': el.getAttribute('data-testid'),
932
+ 'aria-label': el.getAttribute('aria-label'),
933
+ 'placeholder': el.getAttribute('placeholder'),
934
+ }
935
+ }));
936
+ const result = this.selectorGenerator.generateSelector(elementInfo);
937
+ console.log(` 🎯 Generated selector (tier ${result.tier}): ${result.selector}`);
938
+ console.log(` Type: ${result.type}, Weight: ${result.weight}`);
939
+ // Store result in localStorage for verification
940
+ await this.page.evaluate((data) => {
941
+ localStorage.setItem('qa360_generated_selector', JSON.stringify(data));
942
+ }, result);
943
+ break;
944
+ }
945
+ // ═══════════════════════════════════════════════════════════════════════════════
946
+ // PART 14: ASSERTION ACTIONS
947
+ // ═══════════════════════════════════════════════════════════════════════════════
948
+ case 'assertVisible': {
949
+ if (!selector)
950
+ throw new Error('assertVisible requires selector');
951
+ const element = await this.page.$(selector);
952
+ const isVisible = element ? await element.isVisible().catch(() => false) : false;
953
+ if (!isVisible) {
954
+ throw new Error(`Element not visible: ${selector}`);
955
+ }
956
+ console.log(` ✅ Element visible: ${selector}`);
957
+ break;
958
+ }
959
+ case 'assertHidden': {
960
+ if (!selector)
961
+ throw new Error('assertHidden requires selector');
962
+ const element = await this.page.$(selector);
963
+ const isVisible = element ? await element.isVisible().catch(() => false) : false;
964
+ if (isVisible) {
965
+ throw new Error(`Element is visible (should be hidden): ${selector}`);
966
+ }
967
+ console.log(` ✅ Element hidden: ${selector}`);
968
+ break;
969
+ }
970
+ case 'assertAttached': {
971
+ if (!selector)
972
+ throw new Error('assertAttached requires selector');
973
+ const element = await this.page.$(selector);
974
+ if (!element) {
975
+ throw new Error(`Element not attached to DOM: ${selector}`);
976
+ }
977
+ console.log(` ✅ Element attached: ${selector}`);
978
+ break;
979
+ }
980
+ case 'assertText': {
981
+ if (!selector)
982
+ throw new Error('assertText requires selector and expected value');
983
+ const text = await this.page.textContent(selector) || '';
984
+ const expectedText = value || '';
985
+ if (text.trim() !== expectedText.trim()) {
986
+ throw new Error(`Text mismatch for ${selector}: expected "${expectedText}", got "${text.trim()}"`);
987
+ }
988
+ console.log(` ✅ Text matches: ${selector}`);
989
+ break;
990
+ }
991
+ case 'assertContains': {
992
+ if (!selector)
993
+ throw new Error('assertContains requires selector and expected value');
994
+ const text = await this.page.textContent(selector) || '';
995
+ const expectedText = value || '';
996
+ if (!text.includes(expectedText)) {
997
+ throw new Error(`Text does not contain "${expectedText}" for ${selector}`);
998
+ }
999
+ console.log(` ✅ Text contains: ${selector}`);
1000
+ break;
1001
+ }
1002
+ case 'assertValue': {
1003
+ if (!selector)
1004
+ throw new Error('assertValue requires selector');
1005
+ const inputValue = await this.page.inputValue(selector);
1006
+ const expectedValue = value || '';
1007
+ if (inputValue !== expectedValue) {
1008
+ throw new Error(`Value mismatch for ${selector}: expected "${expectedValue}", got "${inputValue}"`);
1009
+ }
1010
+ console.log(` ✅ Value matches: ${selector}`);
1011
+ break;
1012
+ }
1013
+ case 'assertAttribute': {
1014
+ if (!selector)
1015
+ throw new Error('assertAttribute requires selector and attribute name');
1016
+ const attrName = value || '';
1017
+ const attrValue = options?.expected;
1018
+ const actualValue = await this.page.getAttribute(selector, attrName);
1019
+ if (attrValue !== undefined && actualValue !== attrValue) {
1020
+ throw new Error(`Attribute ${attrName} mismatch for ${selector}: expected "${attrValue}", got "${actualValue}"`);
1021
+ }
1022
+ console.log(` ✅ Attribute ${attrName} matches: ${selector}`);
1023
+ break;
1024
+ }
1025
+ case 'assertClass': {
1026
+ if (!selector)
1027
+ throw new Error('assertClass requires selector and class name');
1028
+ const element = await this.page.$(selector);
1029
+ if (!element)
1030
+ throw new Error(`Element not found: ${selector}`);
1031
+ const hasClass = await element.evaluate((el, cls) => el.classList.contains(cls), value);
1032
+ if (!hasClass) {
1033
+ throw new Error(`Element does not have class "${value}": ${selector}`);
1034
+ }
1035
+ console.log(` ✅ Element has class: ${selector} → ${value}`);
1036
+ break;
1037
+ }
1038
+ case 'assertCount': {
1039
+ if (!selector)
1040
+ throw new Error('assertCount requires selector');
1041
+ const count = await this.page.locator(selector).count();
1042
+ const expectedCount = parseInt(value || '0', 10);
1043
+ if (count !== expectedCount) {
1044
+ throw new Error(`Count mismatch for ${selector}: expected ${expectedCount}, got ${count}`);
1045
+ }
1046
+ console.log(` ✅ Count matches: ${selector} → ${count} elements`);
1047
+ break;
1048
+ }
1049
+ case 'assertURL': {
1050
+ const url = this.page.url();
1051
+ const expectedURL = value || '';
1052
+ if (url !== expectedURL && !url.includes(expectedURL)) {
1053
+ throw new Error(`URL mismatch: expected "${expectedURL}", got "${url}"`);
1054
+ }
1055
+ console.log(` ✅ URL matches: ${url}`);
1056
+ break;
1057
+ }
1058
+ case 'assertTitle': {
1059
+ const title = await this.page.title();
1060
+ const expectedTitle = value || '';
1061
+ if (title !== expectedTitle && !title.includes(expectedTitle)) {
1062
+ throw new Error(`Title mismatch: expected "${expectedTitle}", got "${title}"`);
1063
+ }
1064
+ console.log(` ✅ Title matches: ${title}`);
1065
+ break;
1066
+ }
1067
+ case 'assertEnabled': {
1068
+ if (!selector)
1069
+ throw new Error('assertEnabled requires selector');
1070
+ const element = await this.page.$(selector);
1071
+ if (!element)
1072
+ throw new Error(`Element not found: ${selector}`);
1073
+ const isEnabled = await element.isEnabled().catch(() => false);
1074
+ if (!isEnabled) {
1075
+ throw new Error(`Element is disabled: ${selector}`);
1076
+ }
1077
+ console.log(` ✅ Element enabled: ${selector}`);
1078
+ break;
1079
+ }
1080
+ case 'assertDisabled': {
1081
+ if (!selector)
1082
+ throw new Error('assertDisabled requires selector');
1083
+ const element = await this.page.$(selector);
1084
+ if (!element)
1085
+ throw new Error(`Element not found: ${selector}`);
1086
+ const isEnabled = await element.isEnabled().catch(() => true);
1087
+ if (isEnabled) {
1088
+ throw new Error(`Element is enabled (should be disabled): ${selector}`);
1089
+ }
1090
+ console.log(` ✅ Element disabled: ${selector}`);
1091
+ break;
1092
+ }
1093
+ case 'assertChecked': {
1094
+ if (!selector)
1095
+ throw new Error('assertChecked requires selector');
1096
+ const isChecked = await this.page.isChecked(selector);
1097
+ if (!isChecked) {
1098
+ throw new Error(`Element is not checked: ${selector}`);
1099
+ }
1100
+ console.log(` ✅ Element checked: ${selector}`);
1101
+ break;
1102
+ }
1103
+ case 'assertUnchecked': {
1104
+ if (!selector)
1105
+ throw new Error('assertUnchecked requires selector');
1106
+ const isChecked = await this.page.isChecked(selector);
1107
+ if (isChecked) {
1108
+ throw new Error(`Element is checked (should be unchecked): ${selector}`);
1109
+ }
1110
+ console.log(` ✅ Element unchecked: ${selector}`);
1111
+ break;
1112
+ }
1113
+ case 'assertFocused': {
1114
+ if (!selector)
1115
+ throw new Error('assertFocused requires selector');
1116
+ const isFocused = await this.page.evaluate((sel) => {
1117
+ const el = document.querySelector(sel);
1118
+ return el === document.activeElement;
1119
+ }, selector);
1120
+ if (!isFocused) {
1121
+ throw new Error(`Element is not focused: ${selector}`);
1122
+ }
1123
+ console.log(` ✅ Element focused: ${selector}`);
1124
+ break;
1125
+ }
1126
+ case 'assertEmpty': {
1127
+ if (!selector)
1128
+ throw new Error('assertEmpty requires selector');
1129
+ const text = await this.page.textContent(selector) || '';
1130
+ if (text.trim() !== '') {
1131
+ throw new Error(`Element is not empty: ${selector}`);
1132
+ }
1133
+ console.log(` ✅ Element empty: ${selector}`);
1134
+ break;
1135
+ }
1136
+ case 'assertNotEmpty': {
1137
+ if (!selector)
1138
+ throw new Error('assertNotEmpty requires selector');
1139
+ const text = await this.page.textContent(selector) || '';
1140
+ if (text.trim() === '') {
1141
+ throw new Error(`Element is empty: ${selector}`);
1142
+ }
1143
+ console.log(` ✅ Element not empty: ${selector}`);
1144
+ break;
1145
+ }
1146
+ case 'assertReadOnly': {
1147
+ if (!selector)
1148
+ throw new Error('assertReadOnly requires selector');
1149
+ const isReadOnly = await this.page.getAttribute(selector, 'readonly');
1150
+ if (!isReadOnly) {
1151
+ throw new Error(`Element is not read-only: ${selector}`);
1152
+ }
1153
+ console.log(` ✅ Element read-only: ${selector}`);
1154
+ break;
1155
+ }
1156
+ case 'assertEditable': {
1157
+ if (!selector)
1158
+ throw new Error('assertEditable requires selector');
1159
+ const isReadOnly = await this.page.getAttribute(selector, 'readonly');
1160
+ if (isReadOnly) {
1161
+ throw new Error(`Element is read-only: ${selector}`);
1162
+ }
1163
+ console.log(` ✅ Element editable: ${selector}`);
1164
+ break;
1165
+ }
1166
+ case 'assertInViewport': {
1167
+ if (!selector)
1168
+ throw new Error('assertInViewport requires selector');
1169
+ const isInViewport = await this.page.locator(selector).isVisible().catch(() => false);
1170
+ if (!isInViewport) {
1171
+ throw new Error(`Element not in viewport: ${selector}`);
1172
+ }
1173
+ console.log(` ✅ Element in viewport: ${selector}`);
1174
+ break;
1175
+ }
1176
+ case 'assertHasAttribute': {
1177
+ if (!selector)
1178
+ throw new Error('assertHasAttribute requires selector and attribute name');
1179
+ const attrName = value || '';
1180
+ const attrValue = await this.page.getAttribute(selector, attrName);
1181
+ if (attrValue === null) {
1182
+ throw new Error(`Element does not have attribute "${attrName}": ${selector}`);
1183
+ }
1184
+ console.log(` ✅ Element has attribute: ${selector} → ${attrName}="${attrValue}"`);
1185
+ break;
1186
+ }
1187
+ // ═══════════════════════════════════════════════════════════════════════════════
1188
+ // PART 15: UTILITY ACTIONS
1189
+ // ═══════════════════════════════════════════════════════════════════════════════
1190
+ case 'screenshot': {
1191
+ const timestamp = Date.now();
1192
+ const filename = `screenshot-${timestamp}.png`;
1193
+ await this.page.screenshot({ path: filename });
1194
+ console.log(` 📸 Screenshot saved: ${filename}`);
1195
+ break;
1196
+ }
1197
+ case 'fullPageScreenshot': {
1198
+ const timestamp = Date.now();
1199
+ const filename = `screenshot-full-${timestamp}.png`;
1200
+ await this.page.screenshot({ path: filename, fullPage: true });
1201
+ console.log(` 📸 Full page screenshot saved: ${filename}`);
1202
+ break;
1203
+ }
1204
+ case 'waitForLoadState': {
1205
+ const state = value || 'load';
1206
+ await this.page.waitForLoadState(state);
1207
+ console.log(` ⏳ Load state reached: ${state}`);
1208
+ break;
1209
+ }
1210
+ case 'waitForSelectorState': {
1211
+ if (!selector)
1212
+ throw new Error('waitForSelectorState requires selector');
1213
+ const state = value || 'attached';
1214
+ await this.page.waitForSelector(selector, { state: state });
1215
+ console.log(` ⏳ Selector state reached: ${selector} → ${state}`);
1216
+ break;
1217
+ }
1218
+ case 'waitForText': {
1219
+ if (!selector)
1220
+ throw new Error('waitForText requires selector');
1221
+ const expectedText = value || '';
1222
+ await this.page.waitForSelector(selector);
1223
+ await this.page.waitForFunction((args) => {
1224
+ const el = document.querySelector(args.sel);
1225
+ return el?.textContent?.includes(args.text);
1226
+ }, { sel: selector, text: expectedText });
1227
+ console.log(` ⏳ Text found: ${selector} contains "${expectedText}"`);
1228
+ break;
1229
+ }
1230
+ case 'waitForValue': {
1231
+ if (!selector)
1232
+ throw new Error('waitForValue requires selector');
1233
+ const expectedValue = value || '';
1234
+ await this.page.waitForSelector(selector);
1235
+ await this.page.waitForFunction((args) => {
1236
+ const el = document.querySelector(args.sel);
1237
+ return el?.value === args.val;
1238
+ }, { sel: selector, val: expectedValue });
1239
+ console.log(` ⏳ Value found: ${selector} has value "${expectedValue}"`);
1240
+ break;
1241
+ }
1242
+ case 'getText': {
1243
+ if (!selector)
1244
+ throw new Error('getText requires selector');
1245
+ const text = await this.page.textContent(selector) || '';
1246
+ console.log(` 📄 Text content: ${selector} → "${text.trim()}"`);
1247
+ break;
1248
+ }
1249
+ case 'getAttribute': {
1250
+ if (!selector)
1251
+ throw new Error('getAttribute requires selector and attribute name');
1252
+ const attrName = value || '';
1253
+ const attrValue = await this.page.getAttribute(selector, attrName);
1254
+ console.log(` 📄 Attribute ${attrName}: ${selector} → "${attrValue}"`);
1255
+ break;
1256
+ }
1257
+ case 'getElementCount': {
1258
+ if (!selector)
1259
+ throw new Error('getElementCount requires selector');
1260
+ const count = await this.page.locator(selector).count();
1261
+ console.log(` 🔢 Element count: ${selector} → ${count} elements`);
1262
+ break;
1263
+ }
1264
+ case 'getHTML': {
1265
+ if (!selector)
1266
+ throw new Error('getHTML requires selector');
1267
+ const html = await this.page.innerHTML(selector);
1268
+ console.log(` 📄 HTML: ${selector} → ${html.substring(0, 100)}...`);
1269
+ break;
1270
+ }
1271
+ case 'getBoundingBox': {
1272
+ if (!selector)
1273
+ throw new Error('getBoundingBox requires selector');
1274
+ const box = await this.page.locator(selector).boundingBox();
1275
+ console.log(` 📐 Bounding box: ${selector} → ${JSON.stringify(box)}`);
1276
+ break;
1277
+ }
1278
+ case 'highlight': {
1279
+ if (!selector)
1280
+ throw new Error('highlight requires selector');
1281
+ await this.page.evaluate((sel) => {
1282
+ const el = document.querySelector(sel);
1283
+ if (el) {
1284
+ el.style.outline = '3px solid red';
1285
+ el.style.outlineOffset = '2px';
1286
+ }
1287
+ }, selector);
1288
+ console.log(` 🎨 Highlighted: ${selector}`);
1289
+ break;
1290
+ }
1291
+ default:
1292
+ throw new Error(`Unknown action: ${action}`);
1293
+ }
1294
+ // Add wait after action if specified
1295
+ if (step.wait) {
1296
+ await this.page.waitForTimeout(step.wait);
1297
+ }
1298
+ // Take screenshot on failure if configured (will be checked in catch block)
1299
+ // or take screenshot if requested
1300
+ // Verify expected outcomes
1301
+ if (expected) {
1302
+ await this.verifyExpected(expected);
1303
+ }
1304
+ return {
1305
+ step,
1306
+ success: true,
1307
+ duration: Date.now() - startTime,
1308
+ screenshot,
1309
+ };
1310
+ }
1311
+ catch (error) {
1312
+ // Take screenshot on failure
1313
+ try {
1314
+ const buffer = await this.page.screenshot({ type: 'png' });
1315
+ screenshot = `data:image/png;base64,${buffer.toString('base64')}`;
1316
+ }
1317
+ catch { }
1318
+ return {
1319
+ step,
1320
+ success: false,
1321
+ duration: Date.now() - startTime,
1322
+ error: this.redactor.redact(error instanceof Error ? error.message : 'Unknown error'),
1323
+ screenshot,
1324
+ };
1325
+ }
1326
+ }
1327
+ /**
1328
+ * Verify expected outcomes after a step
1329
+ */
1330
+ async verifyExpected(expected) {
1331
+ if (!expected)
1332
+ return;
1333
+ if (expected.url !== undefined) {
1334
+ const currentUrl = this.page.url();
1335
+ if (currentUrl !== expected.url) {
1336
+ throw new Error(`URL mismatch: expected "${expected.url}", got "${currentUrl}"`);
1337
+ }
1338
+ }
1339
+ if (expected.urlContains !== undefined) {
1340
+ const currentUrl = this.page.url();
1341
+ if (!currentUrl.includes(expected.urlContains)) {
1342
+ throw new Error(`URL does not contain "${expected.urlContains}": "${currentUrl}"`);
1343
+ }
1344
+ }
1345
+ if (expected.visible !== undefined) {
1346
+ const element = this.page.locator(expected.visible);
1347
+ if (!(await element.isVisible())) {
1348
+ throw new Error(`Expected element to be visible: ${expected.visible}`);
1349
+ }
1350
+ }
1351
+ if (expected.hidden !== undefined) {
1352
+ const element = this.page.locator(expected.hidden);
1353
+ if (!(await element.isHidden())) {
1354
+ throw new Error(`Expected element to be hidden: ${expected.hidden}`);
1355
+ }
1356
+ }
1357
+ if (expected.elementText) {
1358
+ const element = this.page.locator(expected.elementText.selector);
1359
+ const text = await element.textContent();
1360
+ if (text !== expected.elementText.text) {
1361
+ throw new Error(`Text mismatch for ${expected.elementText.selector}: expected "${expected.elementText.text}", got "${text}"`);
1362
+ }
1363
+ }
1364
+ }
1365
+ /**
1366
+ * Test single page with accessibility
1367
+ */
1368
+ async testPage(pageUrl, config) {
1369
+ try {
1370
+ const startTime = Date.now();
1371
+ // Navigate to page
1372
+ const response = await this.page.goto(pageUrl, {
1373
+ timeout: config.timeout || 30000,
1374
+ waitUntil: 'networkidle'
1375
+ });
1376
+ const loadTime = Date.now() - startTime;
1377
+ if (!response || !response.ok()) {
1378
+ return {
1379
+ page: pageUrl,
1380
+ success: false,
1381
+ loadTime,
1382
+ error: `HTTP ${response?.status() || 'unknown'}: Failed to load page`
1383
+ };
1384
+ }
1385
+ // Take screenshot based on config
1386
+ const screenshot = await this.takeScreenshot(pageUrl, config.target.screenshot);
1387
+ // Get DOM snapshot
1388
+ const domSnapshot = await this.getDomSnapshot();
1389
+ // Run accessibility tests
1390
+ const accessibility = await this.runAccessibilityTests(config.budgets);
1391
+ // Check if accessibility meets budget
1392
+ const a11yPassed = !config.budgets?.a11y_min ||
1393
+ (accessibility?.score !== undefined && accessibility.score >= config.budgets.a11y_min);
1394
+ return {
1395
+ page: pageUrl,
1396
+ success: a11yPassed,
1397
+ loadTime,
1398
+ screenshot,
1399
+ accessibility,
1400
+ domSnapshot,
1401
+ error: a11yPassed ? undefined :
1402
+ `Accessibility score ${accessibility?.score || 0}% below budget ${config.budgets?.a11y_min}%`
1403
+ };
1404
+ }
1405
+ catch (error) {
1406
+ return {
1407
+ page: pageUrl,
1408
+ success: false,
1409
+ loadTime: 0,
1410
+ error: this.redactor.redact(error instanceof Error ? error.message : 'Unknown error')
1411
+ };
1412
+ }
1413
+ }
1414
+ /**
1415
+ * Test page with actions (for user journeys)
1416
+ */
1417
+ async testPageWithActions(pageUrl, actions, config) {
1418
+ try {
1419
+ const startTime = Date.now();
1420
+ // Navigate to page
1421
+ const response = await this.page.goto(pageUrl, {
1422
+ timeout: config.timeout || 30000,
1423
+ waitUntil: 'networkidle'
1424
+ });
1425
+ const loadTime = Date.now() - startTime;
1426
+ if (!response || !response.ok()) {
1427
+ return {
1428
+ page: pageUrl,
1429
+ success: false,
1430
+ loadTime,
1431
+ error: `HTTP ${response?.status() || 'unknown'}: Failed to load page`
1432
+ };
1433
+ }
1434
+ // Execute actions
1435
+ for (const action of actions) {
1436
+ try {
1437
+ await this.executeAction(action);
1438
+ }
1439
+ catch (actionError) {
1440
+ return {
1441
+ page: pageUrl,
1442
+ success: false,
1443
+ loadTime,
1444
+ error: `Action failed (${action.type}): ${this.redactor.redact(actionError instanceof Error ? actionError.message : 'Unknown error')}`
1445
+ };
1446
+ }
1447
+ }
1448
+ // Take screenshot based on config
1449
+ const screenshot = await this.takeScreenshot(pageUrl, config.target.screenshot);
1450
+ // Get DOM snapshot
1451
+ const domSnapshot = await this.getDomSnapshot();
1452
+ // Run accessibility tests
1453
+ const accessibility = await this.runAccessibilityTests(config.budgets);
1454
+ // Check if accessibility meets budget
1455
+ const a11yPassed = !config.budgets?.a11y_min ||
1456
+ (accessibility?.score !== undefined && accessibility.score >= config.budgets.a11y_min);
1457
+ return {
1458
+ page: pageUrl,
1459
+ success: a11yPassed,
1460
+ loadTime,
1461
+ screenshot,
1462
+ accessibility,
1463
+ domSnapshot,
1464
+ error: a11yPassed ? undefined :
1465
+ `Accessibility score ${accessibility?.score || 0}% below budget ${config.budgets?.a11y_min}%`
1466
+ };
1467
+ }
1468
+ catch (error) {
1469
+ return {
1470
+ page: pageUrl,
1471
+ success: false,
1472
+ loadTime: 0,
1473
+ error: this.redactor.redact(error instanceof Error ? error.message : 'Unknown error')
1474
+ };
1475
+ }
1476
+ }
1477
+ /**
1478
+ * Execute a single page action
1479
+ */
1480
+ async executeAction(action) {
1481
+ const timeout = action.options?.timeout ? Number(action.options.timeout) : 10000;
1482
+ switch (action.type) {
1483
+ case 'goto':
1484
+ case 'navigate':
1485
+ // Navigation is handled separately (page.goto is called before actions)
1486
+ break;
1487
+ case 'click':
1488
+ if (!action.selector) {
1489
+ throw new Error('Click action requires a selector');
1490
+ }
1491
+ await this.page.click(action.selector, { timeout });
1492
+ break;
1493
+ case 'fill':
1494
+ if (!action.selector) {
1495
+ throw new Error('Fill action requires a selector');
1496
+ }
1497
+ await this.page.fill(action.selector, action.value || '', { timeout });
1498
+ break;
1499
+ case 'select':
1500
+ if (!action.selector) {
1501
+ throw new Error('Select action requires a selector');
1502
+ }
1503
+ await this.page.selectOption(action.selector, action.value || '', { timeout });
1504
+ break;
1505
+ case 'check':
1506
+ if (!action.selector) {
1507
+ throw new Error('Check action requires a selector');
1508
+ }
1509
+ await this.page.check(action.selector, { timeout });
1510
+ break;
1511
+ case 'uncheck':
1512
+ if (!action.selector) {
1513
+ throw new Error('Uncheck action requires a selector');
1514
+ }
1515
+ await this.page.uncheck(action.selector, { timeout });
1516
+ break;
1517
+ case 'wait':
1518
+ case 'waitFor':
1519
+ if (action.selector) {
1520
+ await this.page.waitForSelector(action.selector, { timeout });
1521
+ }
1522
+ else {
1523
+ await this.page.waitForTimeout(timeout);
1524
+ }
1525
+ break;
1526
+ case 'screenshot':
1527
+ await this.page.screenshot({
1528
+ path: action.options?.path ? String(action.options.path) : undefined
1529
+ });
1530
+ break;
1531
+ case 'assert':
1532
+ if (action.selector) {
1533
+ const element = await this.page.$(action.selector);
1534
+ if (!element) {
1535
+ throw new Error(`Element not found: ${action.selector}`);
1536
+ }
1537
+ }
1538
+ break;
1539
+ default:
1540
+ console.warn(`Unknown action type: ${action.type}`);
1541
+ }
1542
+ }
1543
+ /**
1544
+ * Setup browser with all options
1545
+ * Playwright++: Enhanced video/trace recording support
1546
+ */
1547
+ async setupBrowser(config) {
1548
+ // Determine browser type
1549
+ const browserType = config.target.browser || 'chromium';
1550
+ const browserTypeObj = browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
1551
+ // Determine headed mode (CLI override > target config > default headless)
1552
+ const headless = config.cliHeaded ?? config.target.headless ?? true;
1553
+ // Launch browser
1554
+ this.browser = await browserTypeObj.launch({
1555
+ headless,
1556
+ args: ['--no-sandbox', '--disable-dev-shm-usage'],
1557
+ slowMo: config.target.slowMo || 0,
1558
+ });
1559
+ // Build extra HTTP headers with auth
1560
+ const extraHTTPHeaders = {
1561
+ 'User-Agent': 'QA360-UI-Test/1.0'
1562
+ };
1563
+ if (this.auth?.headers) {
1564
+ Object.assign(extraHTTPHeaders, this.auth.headers);
1565
+ }
1566
+ // Setup viewport based on device or explicit config
1567
+ let viewport = { width: 1280, height: 720 };
1568
+ if (config.target.device === 'mobile') {
1569
+ viewport = { width: 375, height: 667 };
1570
+ }
1571
+ else if (config.target.device === 'tablet') {
1572
+ viewport = { width: 768, height: 1024 };
1573
+ }
1574
+ else if (config.target.viewport) {
1575
+ viewport = config.target.viewport;
1576
+ }
1577
+ // Playwright++: Determine video recording mode
1578
+ const videoMode = config.artifacts?.video ?? config.target.video;
1579
+ const shouldRecordVideo = videoMode === 'always' || videoMode === 'retain-on-failure';
1580
+ // Create context with video recording if enabled
1581
+ const recordVideo = shouldRecordVideo
1582
+ ? { dir: this.videoDir, size: viewport }
1583
+ : undefined;
1584
+ // P0: HTTP credentials for native browser Basic Auth popup (WWW-Authenticate)
1585
+ const httpCredentials = this.auth?.httpCredentials
1586
+ ? { username: this.auth.httpCredentials.username, password: this.auth.httpCredentials.password }
1587
+ : undefined;
1588
+ this.context = await this.browser.newContext({
1589
+ viewport,
1590
+ userAgent: 'QA360-UI-Test/1.0',
1591
+ extraHTTPHeaders,
1592
+ recordVideo,
1593
+ httpCredentials, // P0: Native Basic Auth popup support
1594
+ });
1595
+ // Playwright++: Start tracing if enabled
1596
+ const traceMode = config.artifacts?.trace ?? config.target.trace;
1597
+ if (traceMode === 'always' || traceMode === 'on-first-failure' || traceMode === 'retain-on-failure') {
1598
+ // Start tracing - will be saved in cleanup
1599
+ // Note: Playwright's trace API is context.startTracing()
1600
+ // Implementation depends on Playwright version
1601
+ }
1602
+ // Add cookies from auth credentials after context creation
1603
+ if (this.auth?.cookies && this.auth.cookies.length > 0) {
1604
+ await this.context.addCookies(this.auth.cookies.map(c => ({
1605
+ name: c.name,
1606
+ value: c.value,
1607
+ domain: c.domain || '',
1608
+ path: c.path || '/',
1609
+ httpOnly: c.httpOnly || false,
1610
+ secure: c.secure || false
1611
+ })));
1612
+ }
1613
+ this.page = await this.context.newPage();
1614
+ // ═══════════════════════════════════════════════════════════════════════════════
1615
+ // PART 2: AUTO-HANDLE JAVASCRIPT DIALOGS
1616
+ // ═══════════════════════════════════════════════════════════════════════════════
1617
+ // Note: Only attach handlers if page.on exists (not present in all mocks)
1618
+ if (typeof this.page.on === 'function') {
1619
+ this.globalDialogHandler = async (dialog) => {
1620
+ const type = dialog.type(); // 'alert' | 'confirm' | 'prompt' | 'beforeunload'
1621
+ const message = dialog.message();
1622
+ console.log(` 🔔 Dialog [${type}]: "${message.slice(0, 50)}${message.length > 50 ? '...' : ''}"`);
1623
+ // Store for assertions
1624
+ this.lastDialog = { type, message, timestamp: Date.now() };
1625
+ // Default behavior based on config
1626
+ const dialogConfig = config.target.dialogs || this.dialogBehavior;
1627
+ if (dialogConfig === 'dismiss') {
1628
+ await dialog.dismiss();
1629
+ console.log(` ↪️ Dialog dismissed`);
1630
+ }
1631
+ else if (type === 'prompt') {
1632
+ const promptValue = config.target.dialogPromptValue || this.dialogPromptValue;
1633
+ await dialog.accept(promptValue);
1634
+ console.log(` ↪️ Dialog accepted with value: "${promptValue}"`);
1635
+ }
1636
+ else {
1637
+ await dialog.accept();
1638
+ console.log(` ↪️ Dialog accepted`);
1639
+ }
1640
+ };
1641
+ this.page.on('dialog', this.globalDialogHandler);
1642
+ }
1643
+ // ═══════════════════════════════════════════════════════════════════════════════
1644
+ // PART 3: TRACK NEW TABS/PAGES
1645
+ // ═══════════════════════════════════════════════════════════════════════════════
1646
+ this.openPages = [this.page];
1647
+ this.currentPageIndex = 0;
1648
+ if (typeof this.context.on === 'function') {
1649
+ this.context.on('page', async (newPage) => {
1650
+ console.log(` 📑 New tab opened: ${newPage.url()}`);
1651
+ this.openPages.push(newPage);
1652
+ // Auto-attach dialog handler to new pages
1653
+ if (typeof newPage.on === 'function') {
1654
+ newPage.on('dialog', async (dialog) => {
1655
+ const type = dialog.type();
1656
+ const message = dialog.message();
1657
+ console.log(` 🔔 Dialog [${type}] in new tab: "${message.slice(0, 50)}..."`);
1658
+ this.lastDialog = { type, message, timestamp: Date.now() };
1659
+ const dialogConfig = config.target.dialogs || this.dialogBehavior;
1660
+ if (dialogConfig === 'dismiss') {
1661
+ await dialog.dismiss();
1662
+ }
1663
+ else if (type === 'prompt') {
1664
+ await dialog.accept(config.target.dialogPromptValue || this.dialogPromptValue);
1665
+ }
1666
+ else {
1667
+ await dialog.accept();
1668
+ }
1669
+ });
1670
+ }
1671
+ });
1672
+ }
1673
+ }
1674
+ /**
1675
+ * Determine if video should be recorded
1676
+ */
1677
+ shouldRecordVideo(mode) {
1678
+ return mode === 'always' || mode === 'retain-on-fail';
1679
+ }
1680
+ /**
1681
+ * Playwright++: Generate HTML report
1682
+ */
1683
+ async generateHtmlReport(config, results, e2eResults, summary) {
1684
+ const reportPath = config.htmlReport || join(config.artifacts?.outputDir || this.artifactDir, 'report.html');
1685
+ const timestamp = new Date().toISOString();
1686
+ const reportData = {
1687
+ title: `QA360 UI Test Report - ${timestamp}`,
1688
+ summary: {
1689
+ total: summary.total,
1690
+ passed: summary.passed,
1691
+ failed: summary.failed,
1692
+ skipped: 0,
1693
+ duration: summary.avgLoadTime * summary.total,
1694
+ timestamp,
1695
+ },
1696
+ tests: [
1697
+ ...results.map((r, i) => ({
1698
+ id: `smoke-${i}`,
1699
+ name: `UI Smoke: ${r.page}`,
1700
+ status: (r.success ? 'passed' : 'failed'),
1701
+ duration: r.loadTime,
1702
+ error: r.error,
1703
+ steps: [],
1704
+ artifacts: r.artifacts,
1705
+ })),
1706
+ ...e2eResults.map((r, i) => ({
1707
+ id: `e2e-${i}`,
1708
+ name: r.test.name || 'E2E Test',
1709
+ status: (r.success ? 'passed' : 'failed'),
1710
+ duration: r.duration,
1711
+ error: r.error,
1712
+ steps: r.steps.map((s, j) => ({
1713
+ name: s.step.action || `Step ${j + 1}`,
1714
+ action: s.step.action || 'step',
1715
+ selector: s.step.selector,
1716
+ value: s.step.value,
1717
+ status: (s.success ? 'passed' : 'failed'),
1718
+ duration: s.duration,
1719
+ error: s.error,
1720
+ })),
1721
+ artifacts: undefined,
1722
+ })),
1723
+ ],
1724
+ artifacts: {
1725
+ screenshots: this.allScreenshots.map(s => ({
1726
+ path: s.localPath,
1727
+ timestamp: s.metadata.timestamp,
1728
+ type: 'after',
1729
+ })),
1730
+ videos: this.allVideos.map(v => ({ path: v, duration: 0 })),
1731
+ traces: this.allTraces.map(t => ({ path: t, format: 'zip' })),
1732
+ },
1733
+ environment: {
1734
+ browser: config.target.browser || 'chromium',
1735
+ platform: process.platform,
1736
+ nodeVersion: process.version,
1737
+ },
1738
+ };
1739
+ HTMLReporter.generate(reportData, reportPath);
1740
+ console.log(`\n📊 HTML report generated: ${reportPath}`);
1741
+ }
1742
+ /**
1743
+ * Perform login if configured
1744
+ */
1745
+ async performLogin(login) {
1746
+ if (!login || !login.username || !login.password) {
1747
+ return;
1748
+ }
1749
+ console.log(` 🔐 Performing login...`);
1750
+ const loginUrl = login.url || this.page.url();
1751
+ await this.page.goto(loginUrl);
1752
+ // Fill login form
1753
+ const usernameSelector = login.usernameSelector ||
1754
+ 'input[name="username"], input[name="email"], input[type="email"], #username, #email';
1755
+ const passwordSelector = login.passwordSelector ||
1756
+ 'input[name="password"], input[type="password"], #password';
1757
+ const submitSelector = login.submitSelector ||
1758
+ 'button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")';
1759
+ await this.page.fill(usernameSelector, login.username);
1760
+ await this.page.fill(passwordSelector, login.password);
1761
+ await this.page.click(submitSelector);
1762
+ // Wait for navigation or login completion
1763
+ try {
1764
+ await this.page.waitForLoadState('networkidle', { timeout: 10000 });
1765
+ }
1766
+ catch {
1767
+ // Continue even if navigation doesn't complete
1768
+ }
1769
+ }
1770
+ /**
1771
+ * Run accessibility tests using axe-core
1772
+ */
1773
+ async runAccessibilityTests(budgets) {
1774
+ try {
1775
+ // Inject axe-core
1776
+ await this.page.addScriptTag({
1777
+ url: 'https://unpkg.com/axe-core@4.8.2/axe.min.js'
1778
+ });
1779
+ // Run axe analysis
1780
+ const axeResults = await this.page.evaluate(() => {
1781
+ return new Promise((resolve) => {
1782
+ // @ts-ignore - axe is injected
1783
+ if (typeof axe !== 'undefined') {
1784
+ // @ts-ignore
1785
+ axe.run((err, results) => {
1786
+ if (err) {
1787
+ resolve({ violations: [], passes: [], incomplete: [] });
1788
+ }
1789
+ else {
1790
+ resolve(results);
1791
+ }
1792
+ });
1793
+ }
1794
+ else {
1795
+ resolve({ violations: [], passes: [], incomplete: [] });
1796
+ }
1797
+ });
1798
+ });
1799
+ // Process violations
1800
+ const violations = axeResults.violations?.map((violation) => ({
1801
+ id: violation.id,
1802
+ impact: violation.impact || 'minor',
1803
+ description: violation.description || violation.help || 'Accessibility issue',
1804
+ nodes: violation.nodes?.length || 0
1805
+ })) || [];
1806
+ // Calculate score (simple scoring: 100 - weighted violations)
1807
+ const criticalCount = violations.filter((v) => v.impact === 'critical').length;
1808
+ const seriousCount = violations.filter((v) => v.impact === 'serious').length;
1809
+ const moderateCount = violations.filter((v) => v.impact === 'moderate').length;
1810
+ const minorCount = violations.filter((v) => v.impact === 'minor').length;
1811
+ const score = Math.max(0, 100 - (criticalCount * 25 +
1812
+ seriousCount * 10 +
1813
+ moderateCount * 5 +
1814
+ minorCount * 1));
1815
+ return {
1816
+ score: Math.round(score),
1817
+ violations
1818
+ };
1819
+ }
1820
+ catch (error) {
1821
+ console.log(` ⚠️ Accessibility test failed: ${error}`);
1822
+ return {
1823
+ score: 0,
1824
+ violations: [{
1825
+ id: 'axe-error',
1826
+ impact: 'critical',
1827
+ description: 'Failed to run accessibility analysis',
1828
+ nodes: 0
1829
+ }]
1830
+ };
1831
+ }
1832
+ }
1833
+ /**
1834
+ * Get DOM snapshot for debugging
1835
+ */
1836
+ async getDomSnapshot() {
1837
+ try {
1838
+ const snapshot = await this.page.evaluate(() => {
1839
+ return {
1840
+ title: document.title,
1841
+ url: window.location.href,
1842
+ elements: {
1843
+ buttons: document.querySelectorAll('button, input[type="button"], input[type="submit"]').length,
1844
+ links: document.querySelectorAll('a[href]').length,
1845
+ forms: document.querySelectorAll('form').length,
1846
+ inputs: document.querySelectorAll('input, textarea, select').length
1847
+ }
1848
+ };
1849
+ });
1850
+ return snapshot;
1851
+ }
1852
+ catch {
1853
+ return {
1854
+ title: 'Unknown',
1855
+ url: this.page.url(),
1856
+ elements: { buttons: 0, links: 0, forms: 0, inputs: 0 }
1857
+ };
1858
+ }
1859
+ }
1860
+ /**
1861
+ * Take screenshot for debugging
1862
+ */
1863
+ async takeScreenshot(pageUrl, mode) {
1864
+ const shouldTake = mode === 'always' || mode === 'only-on-fail';
1865
+ if (!shouldTake)
1866
+ return '';
1867
+ try {
1868
+ const screenshot = await this.page.screenshot({
1869
+ type: 'png',
1870
+ fullPage: false // Just viewport for performance
1871
+ });
1872
+ // Return base64 data URL for embedding
1873
+ return `data:image/png;base64,${screenshot.toString('base64')}`;
1874
+ }
1875
+ catch {
1876
+ return '';
1877
+ }
1878
+ }
1879
+ /**
1880
+ * Calculate test summary
1881
+ */
1882
+ calculateSummary(results, e2eResults = []) {
1883
+ const smokeTests = results.length;
1884
+ const e2eTests = e2eResults.length;
1885
+ const total = smokeTests + e2eTests;
1886
+ const smokePassed = results.filter(r => r.success).length;
1887
+ const e2ePassed = e2eResults.filter(r => r.success).length;
1888
+ const passed = smokePassed + e2ePassed;
1889
+ const failed = total - passed;
1890
+ const avgLoadTime = smokeTests > 0 ?
1891
+ Math.round(results.reduce((sum, r) => sum + r.loadTime, 0) / smokeTests) : 0;
1892
+ const a11yScores = results
1893
+ .map(r => r.accessibility?.score)
1894
+ .filter((score) => typeof score === 'number');
1895
+ const avgA11yScore = a11yScores.length > 0 ?
1896
+ Math.round(a11yScores.reduce((sum, score) => sum + score, 0) / a11yScores.length) : 0;
1897
+ return {
1898
+ total,
1899
+ passed,
1900
+ failed,
1901
+ avgLoadTime,
1902
+ avgA11yScore
1903
+ };
1904
+ }
1905
+ /**
1906
+ * Generate JUnit XML fragment
1907
+ */
1908
+ generateJUnit(results, e2eResults = []) {
1909
+ const summary = this.calculateSummary(results, e2eResults);
1910
+ const timestamp = new Date().toISOString();
1911
+ let junit = `<?xml version="1.0" encoding="UTF-8"?>
1912
+ <testsuites>
1913
+ <testsuite name="UI Smoke Tests" tests="${results.length}" failures="${results.filter(r => !r.success).length}" time="${summary.avgLoadTime / 1000}" timestamp="${timestamp}">
1914
+ `;
1915
+ for (const result of results) {
1916
+ const testName = `UI Test: ${result.page}`;
1917
+ const time = result.loadTime / 1000;
1918
+ junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
1919
+ `;
1920
+ if (!result.success) {
1921
+ junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(JSON.stringify(result, null, 2))}</failure>
1922
+ `;
1923
+ }
1924
+ junit += ` </testcase>
1925
+ `;
1926
+ }
1927
+ junit += ` </testsuite>
1928
+ `;
1929
+ // Add E2E test suite
1930
+ if (e2eResults.length > 0) {
1931
+ const e2eFailed = e2eResults.filter(r => !r.success).length;
1932
+ const e2eDuration = e2eResults.reduce((sum, r) => sum + r.duration, 0) / 1000;
1933
+ junit += ` <testsuite name="E2E Tests" tests="${e2eResults.length}" failures="${e2eFailed}" time="${e2eDuration}" timestamp="${timestamp}">
1934
+ `;
1935
+ for (const result of e2eResults) {
1936
+ const testName = result.test.name;
1937
+ const time = result.duration / 1000;
1938
+ junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
1939
+ `;
1940
+ if (!result.success) {
1941
+ const failedSteps = result.steps.filter(s => !s.success);
1942
+ const failureDetails = failedSteps.map(s => `${s.step.action}: ${s.error}`).join('; ');
1943
+ junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(failureDetails)}</failure>
1944
+ `;
1945
+ }
1946
+ junit += ` </testcase>
1947
+ `;
1948
+ }
1949
+ junit += ` </testsuite>
1950
+ `;
1951
+ }
1952
+ junit += `</testsuites>`;
1953
+ return junit;
1954
+ }
1955
+ /**
1956
+ * Escape XML special characters
1957
+ */
1958
+ escapeXml(str) {
1959
+ return str
1960
+ .replace(/&/g, '&amp;')
1961
+ .replace(/</g, '&lt;')
1962
+ .replace(/>/g, '&gt;')
1963
+ .replace(/"/g, '&quot;')
1964
+ .replace(/'/g, '&apos;');
1965
+ }
1966
+ /**
1967
+ * Cleanup browser resources and manage artifacts
1968
+ */
1969
+ async cleanup(config) {
1970
+ const videoMode = config.artifacts?.video ?? config.target.video;
1971
+ const traceMode = config.artifacts?.trace ?? config.target.trace;
1972
+ const hasFailures = this.failureCount > 0;
1973
+ // Save video artifacts if configured and failures occurred
1974
+ if (this.context && videoMode === 'retain-on-failure') {
1975
+ if (hasFailures) {
1976
+ // Retain videos for failed tests - video is saved automatically by Playwright
1977
+ console.log(' 📹 Video artifacts retained on failure');
1978
+ }
1979
+ else {
1980
+ // No failures, video will be automatically deleted by Playwright
1981
+ console.log(' ✓ No failures - video artifacts cleaned up');
1982
+ }
1983
+ }
1984
+ // Save trace artifacts if configured and failures occurred
1985
+ if (this.context && (traceMode === 'retain-on-failure' || traceMode === 'on-first-failure')) {
1986
+ if (hasFailures && traceMode === 'retain-on-failure') {
1987
+ // Stop tracing and save for failed tests
1988
+ const tracePath = join(this.artifactDir, 'traces', `trace-${Date.now()}.zip`);
1989
+ await this.context.tracing.stop({ path: tracePath });
1990
+ console.log(` 📊 Trace retained: ${tracePath}`);
1991
+ }
1992
+ else if (!hasFailures) {
1993
+ // Stop tracing without saving
1994
+ await this.context.tracing.stop();
1995
+ console.log(' ✓ No failures - trace artifacts cleaned up');
1996
+ }
1997
+ else if (traceMode === 'on-first-failure') {
1998
+ // Already saved on first failure, just stop
1999
+ await this.context.tracing.stop();
2000
+ }
2001
+ }
2002
+ // Close browser resources
2003
+ if (this.page) {
2004
+ await this.page.close();
2005
+ }
2006
+ if (this.context) {
2007
+ await this.context.close();
2008
+ }
2009
+ if (this.browser) {
2010
+ await this.browser.close();
2011
+ }
2012
+ }
2013
+ /**
2014
+ * Validate UI target configuration
2015
+ */
2016
+ static validateConfig(target) {
2017
+ const errors = [];
2018
+ if (!target.baseUrl) {
2019
+ errors.push('UI target requires baseUrl');
2020
+ }
2021
+ else {
2022
+ try {
2023
+ new URL(target.baseUrl);
2024
+ }
2025
+ catch {
2026
+ errors.push('UI target baseUrl must be a valid URL');
2027
+ }
2028
+ }
2029
+ if (target.pages) {
2030
+ for (const page of target.pages) {
2031
+ try {
2032
+ // Handle both string URLs and object format { url, actions }
2033
+ const pageUrl = typeof page === 'string' ? page : (page && typeof page === 'object' && 'url' in page ? page.url : null);
2034
+ if (!pageUrl) {
2035
+ errors.push(`Invalid page: missing URL`);
2036
+ continue;
2037
+ }
2038
+ new URL(pageUrl, target.baseUrl);
2039
+ }
2040
+ catch {
2041
+ errors.push(`Invalid page URL: ${page}`);
2042
+ }
2043
+ }
2044
+ }
2045
+ return {
2046
+ valid: errors.length === 0,
2047
+ errors
2048
+ };
2049
+ }
2050
+ }