codeceptjs 4.0.2-beta.9 → 4.0.2

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 (326) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +745 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +195 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +6 -7
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -1
  112. package/lib/command/run-workers.js +2 -14
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +84 -41
  116. package/lib/config.js +96 -18
  117. package/lib/container.js +115 -17
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +246 -2
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +8 -8
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +358 -467
  128. package/lib/helper/Puppeteer.js +335 -192
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +4 -1
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/cli.js +19 -1
  151. package/lib/mocha/factory.js +11 -1
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/ui.js +5 -6
  155. package/lib/parser.js +2 -2
  156. package/lib/pause.js +38 -4
  157. package/lib/plugin/aiTrace.js +457 -0
  158. package/lib/plugin/analyze.js +9 -9
  159. package/lib/plugin/auth.js +5 -4
  160. package/lib/plugin/browser.js +77 -0
  161. package/lib/plugin/expose.js +159 -0
  162. package/lib/plugin/heal.js +47 -3
  163. package/lib/plugin/junitReporter.js +303 -0
  164. package/lib/plugin/pageInfo.js +54 -52
  165. package/lib/plugin/pause.js +131 -0
  166. package/lib/plugin/pauseOnFail.js +11 -33
  167. package/lib/plugin/retryFailedStep.js +43 -32
  168. package/lib/plugin/screencast.js +289 -0
  169. package/lib/plugin/screenshot.js +558 -0
  170. package/lib/plugin/screenshotOnFail.js +9 -170
  171. package/lib/plugin/stepTimeout.js +3 -2
  172. package/lib/recorder.js +1 -1
  173. package/lib/rerun.js +2 -1
  174. package/lib/result.js +2 -1
  175. package/lib/step/base.js +10 -9
  176. package/lib/step/comment.js +2 -2
  177. package/lib/step/config.js +15 -2
  178. package/lib/step/helper.js +4 -4
  179. package/lib/step/meta.js +3 -3
  180. package/lib/step/record.js +5 -5
  181. package/lib/store.js +72 -3
  182. package/lib/translation.js +2 -1
  183. package/lib/utils/loaderCheck.js +28 -0
  184. package/lib/utils/mask_data.js +2 -1
  185. package/lib/utils/pluginParser.js +151 -0
  186. package/lib/utils/trace.js +297 -0
  187. package/lib/utils/typescript.js +188 -23
  188. package/lib/utils.js +77 -3
  189. package/lib/workers.js +65 -40
  190. package/package.json +35 -30
  191. package/typings/index.d.ts +119 -8
  192. package/typings/promiseBasedTypes.d.ts +3158 -6065
  193. package/typings/types.d.ts +3453 -6494
  194. package/docs/webapi/amOnPage.mustache +0 -11
  195. package/docs/webapi/appendField.mustache +0 -11
  196. package/docs/webapi/attachFile.mustache +0 -12
  197. package/docs/webapi/blur.mustache +0 -18
  198. package/docs/webapi/checkOption.mustache +0 -13
  199. package/docs/webapi/clearCookie.mustache +0 -9
  200. package/docs/webapi/clearField.mustache +0 -9
  201. package/docs/webapi/click.mustache +0 -29
  202. package/docs/webapi/clickLink.mustache +0 -8
  203. package/docs/webapi/closeCurrentTab.mustache +0 -7
  204. package/docs/webapi/closeOtherTabs.mustache +0 -8
  205. package/docs/webapi/dontSee.mustache +0 -11
  206. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  207. package/docs/webapi/dontSeeCookie.mustache +0 -8
  208. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  209. package/docs/webapi/dontSeeElement.mustache +0 -8
  210. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  211. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  212. package/docs/webapi/dontSeeInField.mustache +0 -11
  213. package/docs/webapi/dontSeeInSource.mustache +0 -8
  214. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  215. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  216. package/docs/webapi/doubleClick.mustache +0 -13
  217. package/docs/webapi/downloadFile.mustache +0 -12
  218. package/docs/webapi/dragAndDrop.mustache +0 -9
  219. package/docs/webapi/dragSlider.mustache +0 -11
  220. package/docs/webapi/executeAsyncScript.mustache +0 -24
  221. package/docs/webapi/executeScript.mustache +0 -26
  222. package/docs/webapi/fillField.mustache +0 -16
  223. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  224. package/docs/webapi/focus.mustache +0 -13
  225. package/docs/webapi/forceClick.mustache +0 -28
  226. package/docs/webapi/forceRightClick.mustache +0 -18
  227. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  228. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  229. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  230. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  231. package/docs/webapi/grabCookie.mustache +0 -11
  232. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  233. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  234. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  235. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  236. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  237. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  238. package/docs/webapi/grabGeoLocation.mustache +0 -8
  239. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  240. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  241. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  242. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  243. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  244. package/docs/webapi/grabPopupText.mustache +0 -5
  245. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  246. package/docs/webapi/grabSource.mustache +0 -8
  247. package/docs/webapi/grabTextFrom.mustache +0 -10
  248. package/docs/webapi/grabTextFromAll.mustache +0 -9
  249. package/docs/webapi/grabTitle.mustache +0 -8
  250. package/docs/webapi/grabValueFrom.mustache +0 -9
  251. package/docs/webapi/grabValueFromAll.mustache +0 -8
  252. package/docs/webapi/grabWebElement.mustache +0 -9
  253. package/docs/webapi/grabWebElements.mustache +0 -9
  254. package/docs/webapi/moveCursorTo.mustache +0 -12
  255. package/docs/webapi/openNewTab.mustache +0 -7
  256. package/docs/webapi/pressKey.mustache +0 -12
  257. package/docs/webapi/pressKeyDown.mustache +0 -12
  258. package/docs/webapi/pressKeyUp.mustache +0 -12
  259. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  260. package/docs/webapi/refreshPage.mustache +0 -6
  261. package/docs/webapi/resizeWindow.mustache +0 -6
  262. package/docs/webapi/rightClick.mustache +0 -14
  263. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  264. package/docs/webapi/saveScreenshot.mustache +0 -12
  265. package/docs/webapi/say.mustache +0 -10
  266. package/docs/webapi/scrollIntoView.mustache +0 -11
  267. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  268. package/docs/webapi/scrollPageToTop.mustache +0 -6
  269. package/docs/webapi/scrollTo.mustache +0 -12
  270. package/docs/webapi/see.mustache +0 -11
  271. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  272. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  273. package/docs/webapi/seeCookie.mustache +0 -8
  274. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  275. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  276. package/docs/webapi/seeElement.mustache +0 -8
  277. package/docs/webapi/seeElementInDOM.mustache +0 -8
  278. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  279. package/docs/webapi/seeInField.mustache +0 -12
  280. package/docs/webapi/seeInPopup.mustache +0 -8
  281. package/docs/webapi/seeInSource.mustache +0 -7
  282. package/docs/webapi/seeInTitle.mustache +0 -8
  283. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  284. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  285. package/docs/webapi/seeTextEquals.mustache +0 -9
  286. package/docs/webapi/seeTitleEquals.mustache +0 -8
  287. package/docs/webapi/seeTraffic.mustache +0 -36
  288. package/docs/webapi/selectOption.mustache +0 -21
  289. package/docs/webapi/setCookie.mustache +0 -16
  290. package/docs/webapi/setGeoLocation.mustache +0 -12
  291. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  292. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  293. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  294. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  295. package/docs/webapi/switchTo.mustache +0 -9
  296. package/docs/webapi/switchToNextTab.mustache +0 -10
  297. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  298. package/docs/webapi/type.mustache +0 -21
  299. package/docs/webapi/uncheckOption.mustache +0 -13
  300. package/docs/webapi/wait.mustache +0 -8
  301. package/docs/webapi/waitForClickable.mustache +0 -11
  302. package/docs/webapi/waitForCookie.mustache +0 -9
  303. package/docs/webapi/waitForDetached.mustache +0 -10
  304. package/docs/webapi/waitForDisabled.mustache +0 -6
  305. package/docs/webapi/waitForElement.mustache +0 -11
  306. package/docs/webapi/waitForEnabled.mustache +0 -6
  307. package/docs/webapi/waitForFunction.mustache +0 -17
  308. package/docs/webapi/waitForInvisible.mustache +0 -10
  309. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  310. package/docs/webapi/waitForText.mustache +0 -13
  311. package/docs/webapi/waitForValue.mustache +0 -10
  312. package/docs/webapi/waitForVisible.mustache +0 -10
  313. package/docs/webapi/waitInUrl.mustache +0 -9
  314. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  315. package/docs/webapi/waitToHide.mustache +0 -10
  316. package/docs/webapi/waitUrlEquals.mustache +0 -10
  317. package/lib/helper/AI.js +0 -214
  318. package/lib/helper/Mochawesome.js +0 -96
  319. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  320. package/lib/helper/extras/React.js +0 -65
  321. package/lib/listener/enhancedGlobalRetry.js +0 -110
  322. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  323. package/lib/plugin/htmlReporter.js +0 -3648
  324. package/lib/plugin/stepByStepReport.js +0 -427
  325. package/lib/plugin/subtitles.js +0 -89
  326. package/lib/retryCoordinator.js +0 -207
@@ -0,0 +1,151 @@
1
+ import Container from '../container.js'
2
+ import output from '../output.js'
3
+
4
+ const supportedHelpers = Container.STANDARD_ACTING_HELPERS
5
+
6
+ const RESERVED_KEYS = new Set(['on', 'path', 'line', 'pattern'])
7
+ const ALL_MODES = ['fail', 'test', 'step', 'file', 'url']
8
+
9
+ /**
10
+ * Parse a plugin's _args (from CLI `-p plugin:key=value:key=value`) into a flat dict.
11
+ * Each entry is split on `;` then on the first `=`. Bare segments become `{ key: true }`.
12
+ *
13
+ * Examples:
14
+ * parsePluginArgs(['on=fail'])
15
+ * → { on: 'fail' }
16
+ * parsePluginArgs(['on=file', 'path=tests/foo.js;line=43'])
17
+ * → { on: 'file', path: 'tests/foo.js', line: '43' }
18
+ * parsePluginArgs(['on=file', 'path=tests/foo.js', 'line=43'])
19
+ * → { on: 'file', path: 'tests/foo.js', line: '43' }
20
+ * parsePluginArgs(['show'])
21
+ * → { show: true }
22
+ */
23
+ export function parsePluginArgs(args = []) {
24
+ const opts = {}
25
+ for (const arg of args) {
26
+ if (!arg) continue
27
+ for (const segment of arg.split(';')) {
28
+ if (!segment) continue
29
+ if (segment.includes('=')) {
30
+ const eq = segment.indexOf('=')
31
+ const key = segment.slice(0, eq)
32
+ const value = segment.slice(eq + 1)
33
+ opts[key] = coerce(value)
34
+ } else {
35
+ opts[segment] = true
36
+ }
37
+ }
38
+ }
39
+ return opts
40
+ }
41
+
42
+ function coerce(v) {
43
+ if (v === 'true') return true
44
+ if (v === 'false') return false
45
+ return v
46
+ }
47
+
48
+ /**
49
+ * Compose CLI args > config > defaults into a normalized trigger spec, then
50
+ * validate it. Returns `{ on, path, line, pattern, ...rest }` with `line`
51
+ * coerced to a number, or `null` if validation failed (an error is printed).
52
+ *
53
+ * @param {object} cliArgs — output of parsePluginArgs(config._args)
54
+ * @param {object} config — full plugin config object
55
+ * @param {object} defaults — fallback values, e.g. `{ on: 'fail' }`
56
+ * @param {object} options
57
+ * @param {string} options.name — plugin name, used in error messages
58
+ * @param {string[]} [options.validModes] — accepted values for `on`
59
+ * (default: fail, test, step, file, url)
60
+ */
61
+ export function resolveTrigger(cliArgs = {}, config = {}, defaults = {}, options = {}) {
62
+ const { name = 'plugin', validModes = ALL_MODES } = options
63
+ const merged = { ...defaults, ...pickKnown(config), ...cliArgs }
64
+ if (merged.line != null) merged.line = parseInt(merged.line, 10)
65
+
66
+ const valid = new Set(validModes)
67
+ if (!valid.has(merged.on)) {
68
+ output.error(`${name}: unknown on="${merged.on}". Valid: ${validModes.join(', ')}`)
69
+ return null
70
+ }
71
+ if (merged.on === 'file' && !merged.path) {
72
+ output.error(`${name}:on=file requires path=. Example: -p ${name}:on=file:path=tests/foo.js`)
73
+ return null
74
+ }
75
+ if (merged.on === 'url' && !merged.pattern) {
76
+ output.error(`${name}:on=url requires pattern=. Example: -p ${name}:on=url:pattern=/users/*`)
77
+ return null
78
+ }
79
+
80
+ return merged
81
+ }
82
+
83
+ function pickKnown(config) {
84
+ const out = {}
85
+ for (const key of Object.keys(config || {})) {
86
+ if (RESERVED_KEYS.has(key)) out[key] = config[key]
87
+ }
88
+ return out
89
+ }
90
+
91
+ /**
92
+ * Match a step's source location against a `path` (substring/suffix) and optional `line`.
93
+ * Reads the step's stack via `step.line()` to get `file:row:col`.
94
+ */
95
+ export function matchStepFile(step, targetPath, targetLine) {
96
+ if (!targetPath) return false
97
+ const stepLine = step.line && step.line()
98
+ if (!stepLine) return false
99
+
100
+ const parsed = parseStepLine(stepLine)
101
+ if (!parsed) return false
102
+
103
+ const fileMatches = parsed.file.includes(targetPath) || parsed.file.endsWith(targetPath)
104
+ if (!fileMatches) return false
105
+
106
+ if (targetLine != null && !Number.isNaN(targetLine) && parsed.line !== targetLine) return false
107
+ return true
108
+ }
109
+
110
+ function parseStepLine(stepLine) {
111
+ let line = stepLine.trim()
112
+ if (line.startsWith('at ')) line = line.substring(3).trim()
113
+
114
+ const lastColon = line.lastIndexOf(':')
115
+ if (lastColon < 0) return null
116
+ const secondLastColon = line.lastIndexOf(':', lastColon - 1)
117
+ if (secondLastColon < 0) return null
118
+
119
+ const file = line.substring(0, secondLastColon)
120
+ const lineNum = parseInt(line.substring(secondLastColon + 1, lastColon), 10)
121
+
122
+ if (Number.isNaN(lineNum)) return null
123
+ return { file, line: lineNum }
124
+ }
125
+
126
+ /**
127
+ * Match a URL string against a glob-style pattern (supports `*` wildcards).
128
+ */
129
+ export function matchUrl(currentUrl, pattern) {
130
+ if (!pattern || !currentUrl) return false
131
+ return patternToRegex(pattern).test(currentUrl)
132
+ }
133
+
134
+ function patternToRegex(pattern) {
135
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
136
+ const regexStr = escaped.replace(/\*/g, '.*')
137
+ return new RegExp(regexStr)
138
+ }
139
+
140
+ /**
141
+ * Return the first available standard browser helper, or null.
142
+ */
143
+ export function getBrowserHelper() {
144
+ const helpers = Container.helpers()
145
+ for (const name of supportedHelpers) {
146
+ if (Object.keys(helpers).indexOf(name) > -1) {
147
+ return helpers[name]
148
+ }
149
+ }
150
+ return null
151
+ }
@@ -0,0 +1,297 @@
1
+ import crypto from 'crypto'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { pathToFileURL } from 'url'
5
+ import Container from '../container.js'
6
+ import { clearString } from '../utils.js'
7
+ import { formatHtml } from '../html.js'
8
+ import { diffAriaSnapshots } from '../aria.js'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helper / directory naming
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function pickActingHelper(helpers) {
15
+ for (const name of Container.STANDARD_ACTING_HELPERS) {
16
+ if (helpers[name]) return helpers[name]
17
+ }
18
+ return null
19
+ }
20
+
21
+ export function traceDirFor(testFile, testTitle, baseDir) {
22
+ const hash = crypto.createHash('sha256').update((testFile || '') + (testTitle || '')).digest('hex').slice(0, 8)
23
+ const cleanTitle = clearString(testTitle || '').slice(0, 200)
24
+ return path.resolve(baseDir, `trace_${cleanTitle}_${hash}`)
25
+ }
26
+
27
+ export function snapshotDirFor(baseDir) {
28
+ const hash = crypto.randomBytes(4).toString('hex')
29
+ return path.resolve(baseDir, `snapshot_${Date.now()}_${hash}`)
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Artifact link rendering (trace.md)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const ARTIFACT_LABELS = {
37
+ html: 'HTML',
38
+ aria: 'ARIA',
39
+ screenshot: 'Screenshot',
40
+ console: 'Browser Logs',
41
+ storage: 'Storage',
42
+ }
43
+
44
+ export function artifactLinks(artifacts, { indent = ' ', consoleCount } = {}) {
45
+ const lines = []
46
+ const order = ['html', 'aria', 'screenshot', 'console', 'storage']
47
+
48
+ for (const key of order) {
49
+ const file = artifacts[key]
50
+ if (!file) continue
51
+ const label = ARTIFACT_LABELS[key]
52
+ let line = `${indent}> [${label}](./${file})`
53
+ if (key === 'console') {
54
+ const count = consoleCount ?? artifacts.consoleCount ?? 0
55
+ line += ` (${count} entries)`
56
+ } else if (key === 'storage') {
57
+ const cookies = artifacts.cookieCount ?? 0
58
+ const ls = artifacts.localStorageCount ?? 0
59
+ line += ` (${cookies} cookies, ${ls} localStorage)`
60
+ }
61
+ lines.push(line)
62
+ }
63
+
64
+ return lines.join('\n')
65
+ }
66
+
67
+ export function fileToUrl(dir, basename) {
68
+ return pathToFileURL(path.join(dir, basename)).href
69
+ }
70
+
71
+ export function writeTraceMarkdown({ dir, title, file, durationMs, commands, captured, error }) {
72
+ let md = `file: ${file || 'mcp'}\n`
73
+ md += `name: ${title}\n`
74
+ md += `time: ${(durationMs / 1000).toFixed(2)}s\n`
75
+ md += `---\n\n`
76
+
77
+ if (error) md += `Error: ${error}\n\n---\n\n`
78
+
79
+ if (commands && commands.length) {
80
+ md += `### Commands\n`
81
+ for (const c of commands) md += `- ${c}\n`
82
+ md += `\n`
83
+ }
84
+
85
+ md += `### Final State\n`
86
+ if (captured.url) md += ` > URL: ${captured.url}\n`
87
+ const links = artifactLinks(captured)
88
+ if (links) md += links + '\n'
89
+
90
+ const traceFile = path.join(dir, 'trace.md')
91
+ fs.writeFileSync(traceFile, md)
92
+ return traceFile
93
+ }
94
+
95
+ export function artifactsToFileUrls(captured, dir) {
96
+ const out = {}
97
+ if (captured.url) out.url = captured.url
98
+ if (captured.screenshot) out.screenshot = fileToUrl(dir, captured.screenshot)
99
+ if (captured.html) out.html = fileToUrl(dir, captured.html)
100
+ if (captured.aria) out.aria = fileToUrl(dir, captured.aria)
101
+ if (captured.console) out.console = fileToUrl(dir, captured.console)
102
+ if (captured.storage) out.storage = fileToUrl(dir, captured.storage)
103
+ if (typeof captured.consoleCount === 'number') out.consoleCount = captured.consoleCount
104
+ if (typeof captured.cookieCount === 'number') out.cookieCount = captured.cookieCount
105
+ if (typeof captured.localStorageCount === 'number') out.localStorageCount = captured.localStorageCount
106
+ return out
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Snapshot capture (HTML / ARIA / screenshot / console / storage)
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function normalizeBrowserLogs(logs) {
114
+ return (logs || []).map(l => {
115
+ if (typeof l === 'string') return l
116
+ if (l && typeof l.type === 'function' && typeof l.text === 'function') {
117
+ return { type: l.type(), text: l.text() }
118
+ }
119
+ return l
120
+ })
121
+ }
122
+
123
+ async function captureStorageState(helper) {
124
+ if (typeof helper.grabStorageState === 'function') {
125
+ try {
126
+ const state = await helper.grabStorageState()
127
+ if (state) return state
128
+ } catch {}
129
+ }
130
+
131
+ const state = { cookies: [], origins: [] }
132
+
133
+ if (typeof helper.grabCookie === 'function') {
134
+ try {
135
+ const cookies = await helper.grabCookie()
136
+ if (Array.isArray(cookies)) state.cookies = cookies
137
+ } catch {}
138
+ }
139
+
140
+ if (typeof helper.executeScript === 'function') {
141
+ try {
142
+ const result = await helper.executeScript(() => {
143
+ const out = { origin: location.origin, items: [] }
144
+ for (let i = 0; i < localStorage.length; i++) {
145
+ const name = localStorage.key(i)
146
+ out.items.push({ name, value: localStorage.getItem(name) })
147
+ }
148
+ return out
149
+ })
150
+ if (result?.items?.length) {
151
+ state.origins.push({ origin: result.origin, localStorage: result.items })
152
+ }
153
+ } catch {}
154
+ }
155
+
156
+ return state
157
+ }
158
+
159
+ export async function captureSnapshot(helper, {
160
+ dir,
161
+ prefix = 'snapshot',
162
+ fullPage = false,
163
+ captureURL = true,
164
+ captureScreenshot = true,
165
+ captureHTML = true,
166
+ captureARIA = true,
167
+ captureBrowserLogs = true,
168
+ captureStorage = true,
169
+ } = {}) {
170
+ if (!helper) return {}
171
+ const out = {}
172
+
173
+ if (captureURL) {
174
+ try {
175
+ if (helper.grabCurrentUrl) out.url = await helper.grabCurrentUrl()
176
+ } catch {}
177
+ }
178
+
179
+ if (captureScreenshot && helper.saveScreenshot) {
180
+ try {
181
+ const file = `${prefix}_screenshot.png`
182
+ await helper.saveScreenshot(path.join(dir, file), fullPage)
183
+ out.screenshot = file
184
+ } catch {}
185
+ }
186
+
187
+ if (captureHTML && helper.grabSource) {
188
+ try {
189
+ const html = await helper.grabSource()
190
+ // Universal funnel: every captured HTML snapshot flows through formatHtml
191
+ // (minify -> cleanHtml -> beautify). Don't add direct grabSource->writeFile
192
+ // paths elsewhere; route through this util so trash-class cleanup stays
193
+ // consistent across aiTrace, pageInfo, and MCP tools.
194
+ const formatted = await formatHtml(html)
195
+ const file = `${prefix}_page.html`
196
+ fs.writeFileSync(path.join(dir, file), formatted)
197
+ out.html = file
198
+ // Expose pre-cleanup HTML for consumers that need to inspect classes
199
+ // stripped by cleanHtml (e.g. pageInfo's error-class scan).
200
+ out.htmlRaw = html
201
+ } catch {}
202
+ }
203
+
204
+ if (captureARIA && helper.grabAriaSnapshot) {
205
+ try {
206
+ const aria = await helper.grabAriaSnapshot()
207
+ const file = `${prefix}_aria.txt`
208
+ fs.writeFileSync(path.join(dir, file), aria)
209
+ out.aria = file
210
+ } catch {}
211
+ }
212
+
213
+ if (captureBrowserLogs && helper.grabBrowserLogs) {
214
+ try {
215
+ const logs = await helper.grabBrowserLogs()
216
+ const normalized = normalizeBrowserLogs(logs)
217
+ const file = `${prefix}_console.json`
218
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(normalized, null, 2))
219
+ out.console = file
220
+ out.consoleCount = normalized.length
221
+ } catch {}
222
+ }
223
+
224
+ if (captureStorage) {
225
+ try {
226
+ const state = await captureStorageState(helper)
227
+ const cookieCount = state.cookies?.length || 0
228
+ const localStorageCount = (state.origins || [])
229
+ .reduce((sum, o) => sum + (o.localStorage?.length || 0), 0)
230
+ if (cookieCount || localStorageCount) {
231
+ const file = `${prefix}_storage.json`
232
+ fs.writeFileSync(path.join(dir, file), JSON.stringify(state, null, 2))
233
+ out.storage = file
234
+ out.cookieCount = cookieCount
235
+ out.localStorageCount = localStorageCount
236
+ }
237
+ } catch {}
238
+ }
239
+
240
+ return out
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // TraceReader — read artifacts already on disk (written by aiTrace, MCP, etc.)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ const KIND_SUFFIX = {
248
+ aria: '_aria.txt',
249
+ html: '_page.html',
250
+ screenshot: '_screenshot.png',
251
+ console: '_console.json',
252
+ storage: '_storage.json',
253
+ }
254
+
255
+ export class TraceReader {
256
+ constructor(dir) {
257
+ this.dir = dir
258
+ }
259
+
260
+ // Filenames of a given kind, sorted in capture order. aiTrace prefixes with
261
+ // a zero-padded step index (`0000_`, `0001_`...), so a lexical sort is
262
+ // chronological.
263
+ list(kind) {
264
+ const suffix = KIND_SUFFIX[kind]
265
+ if (!suffix || !this.dir || !fs.existsSync(this.dir)) return []
266
+ let entries
267
+ try { entries = fs.readdirSync(this.dir) } catch { return [] }
268
+ return entries.filter(f => f.endsWith(suffix)).sort()
269
+ }
270
+
271
+ // Path of the n-th file of `kind`, or null. Python-style indexing:
272
+ // 0..N-1 from the start, -1..-N from the end.
273
+ pathAt(n, kind) {
274
+ const files = this.list(kind)
275
+ if (!files.length) return null
276
+ const i = n < 0 ? files.length + n : n
277
+ if (i < 0 || i >= files.length) return null
278
+ return path.join(this.dir, files[i])
279
+ }
280
+
281
+ // Read content of the n-th file of `kind`. Binary kinds (screenshot) are
282
+ // returned as Buffer; text kinds as utf8 string.
283
+ nth(n, kind) {
284
+ const p = this.pathAt(n, kind)
285
+ if (!p) return null
286
+ try {
287
+ if (kind === 'screenshot') return fs.readFileSync(p)
288
+ return fs.readFileSync(p, 'utf8')
289
+ } catch { return null }
290
+ }
291
+
292
+ first(kind) { return this.nth(0, kind) }
293
+ last(kind) { return this.nth(-1, kind) }
294
+ count(kind) { return this.list(kind).length }
295
+ }
296
+
297
+ export const ariaDiff = diffAriaSnapshots