codeceptjs 4.0.0-rc.9 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/README.md +9 -10
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +751 -172
  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 +743 -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 +198 -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 +7 -7
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/dryRun.js +24 -5
  105. package/lib/command/generate.js +2 -0
  106. package/lib/command/gherkin/snippets.js +5 -4
  107. package/lib/command/init.js +248 -266
  108. package/lib/command/list.js +150 -10
  109. package/lib/command/query.js +218 -0
  110. package/lib/command/run-multiple.js +3 -2
  111. package/lib/command/run-workers.js +1 -14
  112. package/lib/command/run.js +3 -17
  113. package/lib/command/utils.js +14 -0
  114. package/lib/command/workers/runTests.js +11 -15
  115. package/lib/config.js +77 -4
  116. package/lib/container.js +97 -15
  117. package/lib/effects.js +17 -0
  118. package/lib/element/WebElement.js +194 -2
  119. package/lib/els.js +12 -6
  120. package/lib/globals.js +32 -19
  121. package/lib/heal.js +7 -4
  122. package/lib/helper/ApiDataFactory.js +2 -1
  123. package/lib/helper/FileSystem.js +3 -2
  124. package/lib/helper/GraphQLDataFactory.js +2 -1
  125. package/lib/helper/Playwright.js +63 -70
  126. package/lib/helper/Puppeteer.js +20 -109
  127. package/lib/helper/WebDriver.js +13 -30
  128. package/lib/helper/errors/NonFocusedType.js +8 -0
  129. package/lib/helper/extras/Download.js +45 -0
  130. package/lib/helper/extras/PlaywrightLocator.js +10 -0
  131. package/lib/helper/extras/elementSelection.js +10 -3
  132. package/lib/helper/extras/focusCheck.js +43 -0
  133. package/lib/helper/extras/richTextEditor.js +178 -0
  134. package/lib/history.js +3 -2
  135. package/lib/html.js +90 -16
  136. package/lib/index.js +9 -1
  137. package/lib/listener/config.js +6 -4
  138. package/lib/listener/emptyRun.js +2 -1
  139. package/lib/listener/helpers.js +4 -1
  140. package/lib/listener/mocha.js +2 -1
  141. package/lib/listener/pageobjects.js +43 -0
  142. package/lib/listener/result.js +3 -2
  143. package/lib/locator.js +126 -16
  144. package/lib/mocha/cli.js +4 -2
  145. package/lib/mocha/factory.js +7 -2
  146. package/lib/mocha/inject.js +1 -1
  147. package/lib/mocha/scenarioConfig.js +2 -1
  148. package/lib/mocha/ui.js +5 -6
  149. package/lib/parser.js +2 -2
  150. package/lib/pause.js +38 -4
  151. package/lib/plugin/aiTrace.js +96 -103
  152. package/lib/plugin/analyze.js +9 -9
  153. package/lib/plugin/auth.js +3 -3
  154. package/lib/plugin/browser.js +77 -0
  155. package/lib/plugin/expose.js +159 -0
  156. package/lib/plugin/heal.js +47 -3
  157. package/lib/plugin/junitReporter.js +303 -0
  158. package/lib/plugin/pageInfo.js +54 -52
  159. package/lib/plugin/pause.js +131 -0
  160. package/lib/plugin/pauseOnFail.js +11 -33
  161. package/lib/plugin/retryFailedStep.js +15 -13
  162. package/lib/plugin/screencast.js +289 -0
  163. package/lib/plugin/screenshot.js +558 -0
  164. package/lib/plugin/screenshotOnFail.js +9 -170
  165. package/lib/plugin/stepTimeout.js +3 -2
  166. package/lib/recorder.js +1 -1
  167. package/lib/rerun.js +2 -1
  168. package/lib/result.js +2 -1
  169. package/lib/step/base.js +10 -9
  170. package/lib/step/comment.js +2 -2
  171. package/lib/step/config.js +7 -0
  172. package/lib/step/helper.js +4 -4
  173. package/lib/step/meta.js +3 -3
  174. package/lib/step/record.js +5 -5
  175. package/lib/store.js +72 -3
  176. package/lib/translation.js +2 -1
  177. package/lib/utils/mask_data.js +2 -1
  178. package/lib/utils/pluginParser.js +151 -0
  179. package/lib/utils/trace.js +297 -0
  180. package/lib/utils.js +29 -3
  181. package/lib/workers.js +14 -22
  182. package/package.json +17 -14
  183. package/typings/index.d.ts +0 -5
  184. package/docs/webapi/amOnPage.mustache +0 -11
  185. package/docs/webapi/appendField.mustache +0 -16
  186. package/docs/webapi/attachFile.mustache +0 -24
  187. package/docs/webapi/blur.mustache +0 -18
  188. package/docs/webapi/checkOption.mustache +0 -13
  189. package/docs/webapi/clearCookie.mustache +0 -9
  190. package/docs/webapi/clearField.mustache +0 -14
  191. package/docs/webapi/click.mustache +0 -29
  192. package/docs/webapi/clickLink.mustache +0 -8
  193. package/docs/webapi/closeCurrentTab.mustache +0 -7
  194. package/docs/webapi/closeOtherTabs.mustache +0 -8
  195. package/docs/webapi/dontSee.mustache +0 -11
  196. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  197. package/docs/webapi/dontSeeCookie.mustache +0 -8
  198. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  199. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  200. package/docs/webapi/dontSeeElement.mustache +0 -12
  201. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  202. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  203. package/docs/webapi/dontSeeInField.mustache +0 -16
  204. package/docs/webapi/dontSeeInSource.mustache +0 -8
  205. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  206. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  207. package/docs/webapi/doubleClick.mustache +0 -13
  208. package/docs/webapi/downloadFile.mustache +0 -12
  209. package/docs/webapi/dragAndDrop.mustache +0 -9
  210. package/docs/webapi/dragSlider.mustache +0 -11
  211. package/docs/webapi/executeAsyncScript.mustache +0 -24
  212. package/docs/webapi/executeScript.mustache +0 -26
  213. package/docs/webapi/fillField.mustache +0 -21
  214. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  215. package/docs/webapi/focus.mustache +0 -13
  216. package/docs/webapi/forceClick.mustache +0 -28
  217. package/docs/webapi/forceRightClick.mustache +0 -18
  218. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  219. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  220. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  221. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  222. package/docs/webapi/grabCookie.mustache +0 -11
  223. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  224. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  225. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  226. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  227. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  228. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  229. package/docs/webapi/grabGeoLocation.mustache +0 -8
  230. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  231. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  232. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  233. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  234. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  235. package/docs/webapi/grabPopupText.mustache +0 -5
  236. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  237. package/docs/webapi/grabSource.mustache +0 -8
  238. package/docs/webapi/grabTextFrom.mustache +0 -10
  239. package/docs/webapi/grabTextFromAll.mustache +0 -9
  240. package/docs/webapi/grabTitle.mustache +0 -8
  241. package/docs/webapi/grabValueFrom.mustache +0 -9
  242. package/docs/webapi/grabValueFromAll.mustache +0 -8
  243. package/docs/webapi/grabWebElement.mustache +0 -9
  244. package/docs/webapi/grabWebElements.mustache +0 -9
  245. package/docs/webapi/moveCursorTo.mustache +0 -16
  246. package/docs/webapi/openNewTab.mustache +0 -7
  247. package/docs/webapi/pressKey.mustache +0 -12
  248. package/docs/webapi/pressKeyDown.mustache +0 -12
  249. package/docs/webapi/pressKeyUp.mustache +0 -12
  250. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  251. package/docs/webapi/refreshPage.mustache +0 -6
  252. package/docs/webapi/resizeWindow.mustache +0 -6
  253. package/docs/webapi/rightClick.mustache +0 -14
  254. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  255. package/docs/webapi/saveScreenshot.mustache +0 -12
  256. package/docs/webapi/say.mustache +0 -10
  257. package/docs/webapi/scrollIntoView.mustache +0 -11
  258. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  259. package/docs/webapi/scrollPageToTop.mustache +0 -6
  260. package/docs/webapi/scrollTo.mustache +0 -12
  261. package/docs/webapi/see.mustache +0 -11
  262. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  263. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  264. package/docs/webapi/seeCookie.mustache +0 -8
  265. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  266. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  267. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  268. package/docs/webapi/seeElement.mustache +0 -12
  269. package/docs/webapi/seeElementInDOM.mustache +0 -8
  270. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  271. package/docs/webapi/seeInField.mustache +0 -17
  272. package/docs/webapi/seeInPopup.mustache +0 -8
  273. package/docs/webapi/seeInSource.mustache +0 -7
  274. package/docs/webapi/seeInTitle.mustache +0 -8
  275. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  276. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  277. package/docs/webapi/seeTextEquals.mustache +0 -9
  278. package/docs/webapi/seeTitleEquals.mustache +0 -8
  279. package/docs/webapi/seeTraffic.mustache +0 -36
  280. package/docs/webapi/selectOption.mustache +0 -26
  281. package/docs/webapi/setCookie.mustache +0 -16
  282. package/docs/webapi/setGeoLocation.mustache +0 -12
  283. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  284. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  285. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  286. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  287. package/docs/webapi/switchTo.mustache +0 -9
  288. package/docs/webapi/switchToNextTab.mustache +0 -10
  289. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  290. package/docs/webapi/type.mustache +0 -21
  291. package/docs/webapi/uncheckOption.mustache +0 -13
  292. package/docs/webapi/wait.mustache +0 -8
  293. package/docs/webapi/waitForClickable.mustache +0 -11
  294. package/docs/webapi/waitForCookie.mustache +0 -9
  295. package/docs/webapi/waitForDetached.mustache +0 -10
  296. package/docs/webapi/waitForDisabled.mustache +0 -6
  297. package/docs/webapi/waitForElement.mustache +0 -11
  298. package/docs/webapi/waitForEnabled.mustache +0 -6
  299. package/docs/webapi/waitForFunction.mustache +0 -17
  300. package/docs/webapi/waitForInvisible.mustache +0 -10
  301. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  302. package/docs/webapi/waitForText.mustache +0 -13
  303. package/docs/webapi/waitForValue.mustache +0 -10
  304. package/docs/webapi/waitForVisible.mustache +0 -10
  305. package/docs/webapi/waitInUrl.mustache +0 -9
  306. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  307. package/docs/webapi/waitToHide.mustache +0 -10
  308. package/docs/webapi/waitUrlEquals.mustache +0 -10
  309. package/lib/helper/AI.js +0 -214
  310. package/lib/helper/Mochawesome.js +0 -96
  311. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  312. package/lib/helper/extras/React.js +0 -65
  313. package/lib/plugin/stepByStepReport.js +0 -431
  314. package/lib/plugin/subtitles.js +0 -89
@@ -0,0 +1,43 @@
1
+ import store from '../../store.js'
2
+ import NonFocusedType from '../errors/NonFocusedType.js'
3
+
4
+ const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i
5
+ const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
6
+
7
+ async function isNoElementFocused(helper) {
8
+ return helper.executeScript(() => {
9
+ const ae = document.activeElement
10
+ return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
11
+ })
12
+ }
13
+
14
+ export async function checkFocusBeforeType(helper) {
15
+ if (!helper.options.strict && !store.debugMode) return
16
+ if (!await isNoElementFocused(helper)) return
17
+
18
+ const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
19
+ if (helper.options.strict) throw new NonFocusedType(message)
20
+ helper.debugSection('Warning', message)
21
+ }
22
+
23
+ export async function checkFocusBeforePressKey(helper, originalKey) {
24
+ if (!helper.options.strict && !store.debugMode) return
25
+ if (!Array.isArray(originalKey)) return
26
+
27
+ let hasCtrlOrMeta = false
28
+ let actionKey = null
29
+ for (const k of originalKey) {
30
+ if (MODIFIER_PATTERN.test(k)) {
31
+ hasCtrlOrMeta = true
32
+ } else {
33
+ actionKey = k
34
+ }
35
+ }
36
+ if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return
37
+
38
+ if (!await isNoElementFocused(helper)) return
39
+
40
+ const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.`
41
+ if (helper.options.strict) throw new NonFocusedType(message)
42
+ helper.debugSection('Warning', message)
43
+ }
@@ -0,0 +1,178 @@
1
+ import WebElement from '../../element/WebElement.js'
2
+
3
+ const MARKER = 'data-codeceptjs-rte-target'
4
+
5
+ const EDITOR = {
6
+ STANDARD: 'standard',
7
+ IFRAME: 'iframe',
8
+ CONTENTEDITABLE: 'contenteditable',
9
+ HIDDEN_TEXTAREA: 'hidden-textarea',
10
+ UNREACHABLE: 'unreachable',
11
+ }
12
+
13
+ function detectAndMark(el, opts) {
14
+ const marker = opts.marker
15
+ const kinds = opts.kinds
16
+ const CE = '[contenteditable="true"], [contenteditable=""]'
17
+
18
+ function mark(kind, target) {
19
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
20
+ if (target && target.nodeType === 1) target.setAttribute(marker, '1')
21
+ return kind
22
+ }
23
+
24
+ if (!el || el.nodeType !== 1) return mark(kinds.STANDARD, el)
25
+
26
+ const tag = el.tagName
27
+ if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
28
+ if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
29
+
30
+ const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
31
+ if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
32
+ const style = window.getComputedStyle(el)
33
+ if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
34
+ }
35
+
36
+ const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
37
+ if (canSearchDescendants) {
38
+ const iframe = el.querySelector('iframe')
39
+ if (iframe) return mark(kinds.IFRAME, iframe)
40
+ const ce = el.querySelector(CE)
41
+ if (ce) return mark(kinds.CONTENTEDITABLE, ce)
42
+ const textareas = [...el.querySelectorAll('textarea')]
43
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
44
+ const textarea = focusable || textareas[0]
45
+ if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
46
+ }
47
+
48
+ return mark(kinds.STANDARD, el)
49
+ }
50
+
51
+ function detectInsideFrame() {
52
+ const MARKER = 'data-codeceptjs-rte-target'
53
+ const CE = '[contenteditable="true"], [contenteditable=""]'
54
+ const CONTENTEDITABLE = 'contenteditable'
55
+ const HIDDEN_TEXTAREA = 'hidden-textarea'
56
+ const body = document.body
57
+ document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
58
+
59
+ if (body.isContentEditable) return CONTENTEDITABLE
60
+
61
+ const ce = body.querySelector(CE)
62
+ if (ce) {
63
+ ce.setAttribute(MARKER, '1')
64
+ return CONTENTEDITABLE
65
+ }
66
+
67
+ const textareas = [...body.querySelectorAll('textarea')]
68
+ const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
69
+ const textarea = focusable || textareas[0]
70
+ if (textarea) {
71
+ textarea.setAttribute(MARKER, '1')
72
+ return HIDDEN_TEXTAREA
73
+ }
74
+
75
+ return CONTENTEDITABLE
76
+ }
77
+
78
+ async function evaluateInFrame(helper, body, fn) {
79
+ if (body.helperType === 'webdriver') {
80
+ return helper.executeScript(fn)
81
+ }
82
+ return body.element.evaluate(fn)
83
+ }
84
+
85
+ function focusMarkedInFrameScript() {
86
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
87
+ el.focus()
88
+ return document.activeElement === el
89
+ }
90
+
91
+ function selectAllInFrameScript() {
92
+ const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
93
+ el.focus()
94
+ const range = document.createRange()
95
+ range.selectNodeContents(el)
96
+ const sel = window.getSelection()
97
+ sel.removeAllRanges()
98
+ sel.addRange(range)
99
+ return document.activeElement === el
100
+ }
101
+
102
+ function selectAllInEditable(el) {
103
+ const doc = el.ownerDocument
104
+ const win = doc.defaultView
105
+ el.focus()
106
+ const range = doc.createRange()
107
+ range.selectNodeContents(el)
108
+ const sel = win.getSelection()
109
+ sel.removeAllRanges()
110
+ sel.addRange(range)
111
+ }
112
+
113
+ function unmarkAll(marker) {
114
+ document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
115
+ }
116
+
117
+ function isActive(el) {
118
+ return el.ownerDocument.activeElement === el
119
+ }
120
+
121
+ async function assertFocused(target) {
122
+ const focused = await target.evaluate(isActive)
123
+ if (!focused) {
124
+ throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
125
+ }
126
+ }
127
+
128
+ async function findMarked(helper) {
129
+ const root = helper.page || helper.browser
130
+ const raw = await root.$('[' + MARKER + ']')
131
+ return new WebElement(raw, helper)
132
+ }
133
+
134
+ async function clearMarker(helper) {
135
+ if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
136
+ return helper.executeScript(unmarkAll, MARKER)
137
+ }
138
+
139
+ export async function fillRichEditor(helper, el, value) {
140
+ const source = el instanceof WebElement ? el : new WebElement(el, helper)
141
+ const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
142
+ if (kind === EDITOR.STANDARD) return false
143
+ if (kind === EDITOR.UNREACHABLE) {
144
+ throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
145
+ }
146
+
147
+ const target = await findMarked(helper)
148
+ const delay = helper.options.pressKeyDelay
149
+
150
+ if (kind === EDITOR.IFRAME) {
151
+ await target.inIframe(async body => {
152
+ const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
153
+ if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
154
+ const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
155
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
156
+ await body.selectAllAndDelete()
157
+ await body.typeText(value, { delay })
158
+ } else {
159
+ const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
160
+ if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
161
+ await body.typeText(value, { delay })
162
+ }
163
+ })
164
+ } else if (kind === EDITOR.HIDDEN_TEXTAREA) {
165
+ await target.focus()
166
+ await assertFocused(target)
167
+ await target.selectAllAndDelete()
168
+ await target.typeText(value, { delay })
169
+ } else if (kind === EDITOR.CONTENTEDITABLE) {
170
+ await target.click()
171
+ await target.evaluate(selectAllInEditable)
172
+ await assertFocused(target)
173
+ await target.typeText(value, { delay })
174
+ }
175
+
176
+ await clearMarker(helper)
177
+ return true
178
+ }
package/lib/history.js CHANGED
@@ -2,6 +2,7 @@ import colors from 'chalk'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
4
  import output from './output.js'
5
+ import store from './store.js'
5
6
 
6
7
  /**
7
8
  * REPL history records REPL commands and stores them in
@@ -9,8 +10,8 @@ import output from './output.js'
9
10
  */
10
11
  class ReplHistory {
11
12
  constructor() {
12
- if (global.output_dir) {
13
- this.historyFile = path.join(global.output_dir, 'cli-history')
13
+ if (store.outputDir) {
14
+ this.historyFile = path.join(store.outputDir, 'cli-history')
14
15
  }
15
16
  this.commands = []
16
17
  }
package/lib/html.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { parse, serialize } from 'parse5'
2
2
  import { minify } from 'html-minifier-terser'
3
+ import beautify from 'js-beautify'
4
+
5
+ const { html: html_beautify } = beautify
3
6
 
4
7
  async function minifyHtml(html) {
5
8
  return minify(html, {
@@ -14,6 +17,62 @@ async function minifyHtml(html) {
14
17
  })
15
18
  }
16
19
 
20
+ const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
21
+
22
+ function isTrashClass(className) {
23
+ if (!className) return true
24
+ if (/\d/.test(className)) return true
25
+ if (TRASH_HTML_CLASSES.test(className)) return true
26
+ if (/(:|__)/.test(className)) return true
27
+ return false
28
+ }
29
+
30
+ function filterClassValue(value) {
31
+ return (value || '')
32
+ .split(/\s+/)
33
+ .filter(c => c && !isTrashClass(c))
34
+ .join(' ')
35
+ }
36
+
37
+ const DROP_TAGS = new Set(['style', 'noscript'])
38
+ const DROP_ATTRS = new Set(['style'])
39
+
40
+ function cleanHtml(html) {
41
+ const document = parse(html)
42
+
43
+ function walk(node) {
44
+ if (!node) return false
45
+
46
+ if (DROP_TAGS.has(node.nodeName) || (node.nodeName === 'script' && !(node.attrs || []).some(a => a.name === 'src'))) {
47
+ const parent = node.parentNode
48
+ const idx = parent.childNodes.indexOf(node)
49
+ if (idx >= 0) parent.childNodes.splice(idx, 1)
50
+ return true
51
+ }
52
+
53
+ if (node.attrs) {
54
+ node.attrs = node.attrs.filter(attr => {
55
+ if (DROP_ATTRS.has(attr.name)) return false
56
+ if (attr.name === 'class') {
57
+ attr.value = filterClassValue(attr.value)
58
+ if (!attr.value) return false
59
+ }
60
+ return true
61
+ })
62
+ }
63
+
64
+ if (node.childNodes) {
65
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
66
+ walk(node.childNodes[i])
67
+ }
68
+ }
69
+ return false
70
+ }
71
+
72
+ walk(document)
73
+ return serialize(document)
74
+ }
75
+
17
76
  const defaultHtmlOpts = {
18
77
  interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
19
78
  textElements: ['label', 'h1', 'h2'],
@@ -28,7 +87,6 @@ function removeNonInteractiveElements(html, opts = {}) {
28
87
  // Parse the HTML into a document tree
29
88
  const document = parse(html)
30
89
 
31
- const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
32
90
  // Array to store interactive elements
33
91
  const removeElements = ['path', 'script']
34
92
 
@@ -103,21 +161,10 @@ function removeNonInteractiveElements(html, opts = {}) {
103
161
  if (node.attrs) {
104
162
  // Filter and keep allowed attributes, accessibility attributes
105
163
  node.attrs = node.attrs.filter(attr => {
106
- const { name, value } = attr
107
- if (name === 'class') {
108
- // Remove classes containing digits
109
- attr.value = value
110
- .split(' ')
111
- // remove classes containing digits/
112
- .filter(className => !/\d/.test(className))
113
- // remove popular trash classes
114
- .filter(className => !className.match(trashHtmlClasses))
115
- // remove classes with : and __ in them
116
- .filter(className => !className.match(/(:|__)/))
117
- .join(' ')
164
+ if (attr.name === 'class') {
165
+ attr.value = filterClassValue(attr.value)
118
166
  }
119
-
120
- return allowedAttrs.includes(name)
167
+ return allowedAttrs.includes(attr.name)
121
168
  })
122
169
  }
123
170
 
@@ -258,4 +305,31 @@ function simplifyHtmlElement(html, maxLength = 300) {
258
305
  return html
259
306
  }
260
307
 
261
- export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement }
308
+ async function formatHtml(html) {
309
+ let processed = html
310
+ try {
311
+ processed = await minifyHtml(processed)
312
+ } catch (e) {
313
+ // keep raw html if minification fails
314
+ }
315
+ try {
316
+ processed = cleanHtml(processed)
317
+ } catch (e) {
318
+ // keep minified html if cleaning fails
319
+ }
320
+ try {
321
+ return html_beautify(processed, {
322
+ indent_size: 2,
323
+ wrap_line_length: 0,
324
+ preserve_newlines: false,
325
+ end_with_newline: false,
326
+ // Force every element onto its own line so line numbers in trace HTML
327
+ // map 1:1 to elements (consumed by codeceptq for AI/agent debugging).
328
+ inline: [],
329
+ })
330
+ } catch (e) {
331
+ return processed
332
+ }
333
+ }
334
+
335
+ export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement, formatHtml, cleanHtml, isTrashClass }
package/lib/index.js CHANGED
@@ -23,6 +23,10 @@ import heal from './heal.js'
23
23
  import ai from './ai.js'
24
24
  import Workers from './workers.js'
25
25
  import Secret, { secret } from './secret.js'
26
+ import session from './session.js'
27
+
28
+ const inject = (name) => container.support(name)
29
+ const locate = (query) => locator.build(query)
26
30
 
27
31
  export default {
28
32
  /** @type {typeof CodeceptJS.Codecept} */
@@ -67,7 +71,11 @@ export default {
67
71
  Secret,
68
72
  /** @type {typeof CodeceptJS.secret} */
69
73
  secret,
74
+
75
+ session,
76
+ inject,
77
+ locate,
70
78
  }
71
79
 
72
80
  // Named exports for ESM compatibility
73
- export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret }
81
+ export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, session, inject, locate }
@@ -2,16 +2,18 @@ import event from '../event.js'
2
2
  import recorder from '../recorder.js'
3
3
  import { deepMerge, deepClone, ucfirst } from '../utils.js'
4
4
  import output from '../output.js'
5
+ import container from '../container.js'
5
6
 
6
7
  /**
7
8
  * Enable Helpers to listen to test events
8
9
  */
10
+ let initialized = false
11
+
9
12
  export default function () {
10
- // Use global flag to prevent duplicate initialization across module re-imports
11
- if (global.__codeceptConfigListenerInitialized) {
13
+ if (initialized) {
12
14
  return
13
15
  }
14
- global.__codeceptConfigListenerInitialized = true
16
+ initialized = true
15
17
 
16
18
  enableDynamicConfigFor('suite')
17
19
  enableDynamicConfigFor('test')
@@ -20,7 +22,7 @@ export default function () {
20
22
  event.dispatcher.on(event[type].before, (context = {}) => {
21
23
  // Get helpers dynamically at runtime, not at initialization time
22
24
  // This ensures we get the actual helper instances, not placeholders
23
- const helpers = global.container.helpers()
25
+ const helpers = container.helpers()
24
26
 
25
27
  function updateHelperConfig(helper, config) {
26
28
  // Guard against undefined or invalid helpers
@@ -1,6 +1,7 @@
1
1
  import figures from 'figures'
2
2
  import event from '../event.js'
3
3
  import output from '../output.js'
4
+ import container from '../container.js'
4
5
  import { searchWithFusejs } from '../utils.js'
5
6
 
6
7
  export default function () {
@@ -12,7 +13,7 @@ export default function () {
12
13
 
13
14
  event.dispatcher.on(event.all.result, () => {
14
15
  if (isEmptyRun) {
15
- const mocha = global.container.mocha()
16
+ const mocha = container.mocha()
16
17
 
17
18
  if (mocha.options.grep) {
18
19
  output.print()
@@ -3,11 +3,12 @@ import event from '../event.js'
3
3
  import recorder from '../recorder.js'
4
4
  import store from '../store.js'
5
5
  import output from '../output.js'
6
+ import container from '../container.js'
6
7
  /**
7
8
  * Enable Helpers to listen to test events
8
9
  */
9
10
  export default function () {
10
- const helpers = global.container.helpers()
11
+ const helpers = container.helpers()
11
12
 
12
13
  const runHelpersHook = (hook, param) => {
13
14
  if (store.dryRun) return
@@ -29,11 +30,13 @@ export default function () {
29
30
  event.dispatcher.on(event.suite.before, suite => {
30
31
  // if (suite.parent) return; // only for root suite
31
32
  runAsyncHelpersHook('_beforeSuite', suite, true)
33
+ recorder.catch()
32
34
  })
33
35
 
34
36
  event.dispatcher.on(event.suite.after, suite => {
35
37
  // if (suite.parent) return; // only for root suite
36
38
  runAsyncHelpersHook('_afterSuite', suite, true)
39
+ recorder.catch()
37
40
  })
38
41
 
39
42
  event.dispatcher.on(event.test.started, test => {
@@ -1,10 +1,11 @@
1
1
  import event from '../event.js'
2
+ import container from '../container.js'
2
3
 
3
4
  export default function () {
4
5
  let mocha
5
6
 
6
7
  event.dispatcher.on(event.all.before, () => {
7
- mocha = global.container.mocha()
8
+ mocha = container.mocha()
8
9
  })
9
10
 
10
11
  event.dispatcher.on(event.test.passed, test => {
@@ -0,0 +1,43 @@
1
+ import event from '../event.js'
2
+ import recorder from '../recorder.js'
3
+ import store from '../store.js'
4
+ import container from '../container.js'
5
+ import { resetBeforeCalledSet, getBeforeCalledSet } from '../container.js'
6
+
7
+ export default function () {
8
+ const runAsyncSupportHook = (hook, param, force) => {
9
+ if (store.dryRun) return
10
+ const support = container.supportObjects()
11
+ Object.keys(support).forEach(key => {
12
+ if (key === 'I') return
13
+ const obj = support[key]
14
+ if (!obj || typeof obj !== 'object' || !obj[hook]) return
15
+ recorder.add(`pageobject ${key}.${hook}()`, () => obj[hook](param), force, false)
16
+ })
17
+ }
18
+
19
+ event.dispatcher.on(event.test.started, () => {
20
+ resetBeforeCalledSet()
21
+ })
22
+
23
+ event.dispatcher.on(event.test.after, () => {
24
+ if (store.dryRun) return
25
+ const support = container.supportObjects()
26
+ const called = getBeforeCalledSet()
27
+ called.forEach(name => {
28
+ const obj = support[name]
29
+ if (obj && obj._after) {
30
+ recorder.add(`pageobject ${name}._after()`, () => obj._after(), true, false)
31
+ }
32
+ })
33
+ recorder.catchWithoutStop(() => {})
34
+ })
35
+
36
+ event.dispatcher.on(event.suite.after, suite => {
37
+ runAsyncSupportHook('_afterSuite', suite, true)
38
+ })
39
+
40
+ event.dispatcher.on(event.suite.before, suite => {
41
+ runAsyncSupportHook('_beforeSuite', suite, true)
42
+ })
43
+ }
@@ -1,11 +1,12 @@
1
1
  import event from '../event.js'
2
+ import container from '../container.js'
2
3
 
3
4
  export default function () {
4
5
  event.dispatcher.on(event.hook.failed, err => {
5
- global.container.result().addStats({ failedHooks: 1 })
6
+ container.result().addStats({ failedHooks: 1 })
6
7
  })
7
8
 
8
9
  event.dispatcher.on(event.test.before, test => {
9
- global.container.result().addTest(test)
10
+ container.result().addTest(test)
10
11
  })
11
12
  }