codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20

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 (294) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1187 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +159 -0
  7. package/docs/ai.md +537 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/assertions.md +415 -0
  11. package/docs/auth.md +318 -0
  12. package/docs/basics.md +424 -0
  13. package/docs/bdd.md +539 -0
  14. package/docs/best.md +240 -0
  15. package/docs/bootstrap.md +132 -0
  16. package/docs/commands.md +352 -0
  17. package/docs/community-helpers.md +63 -0
  18. package/docs/configuration.md +230 -0
  19. package/docs/continuous-integration.md +497 -0
  20. package/docs/custom-helpers.md +297 -0
  21. package/docs/data.md +448 -0
  22. package/docs/debugging.md +332 -0
  23. package/docs/detox.md +235 -0
  24. package/docs/docker.md +136 -0
  25. package/docs/effects.md +179 -0
  26. package/docs/element-based-testing.md +295 -0
  27. package/docs/element-selection.md +125 -0
  28. package/docs/els.md +328 -0
  29. package/docs/examples.md +161 -0
  30. package/docs/heal.md +213 -0
  31. package/docs/helpers/ApiDataFactory.md +267 -0
  32. package/docs/helpers/Appium.md +1405 -0
  33. package/docs/helpers/Detox.md +665 -0
  34. package/docs/helpers/ExpectHelper.md +275 -0
  35. package/docs/helpers/FileSystem.md +152 -0
  36. package/docs/helpers/GraphQL.md +152 -0
  37. package/docs/helpers/GraphQLDataFactory.md +226 -0
  38. package/docs/helpers/JSONResponse.md +255 -0
  39. package/docs/helpers/Mochawesome.md +8 -0
  40. package/docs/helpers/MockRequest.md +377 -0
  41. package/docs/helpers/MockServer.md +212 -0
  42. package/docs/helpers/Playwright.md +2969 -0
  43. package/docs/helpers/Polly.md +44 -0
  44. package/docs/helpers/Protractor.md +1769 -0
  45. package/docs/helpers/Puppeteer-firefox.md +86 -0
  46. package/docs/helpers/Puppeteer.md +2690 -0
  47. package/docs/helpers/REST.md +289 -0
  48. package/docs/helpers/SoftExpectHelper.md +352 -0
  49. package/docs/helpers/WebDriver.md +2682 -0
  50. package/docs/hooks.md +339 -0
  51. package/docs/index.md +111 -0
  52. package/docs/installation.md +83 -0
  53. package/docs/internal-api.md +265 -0
  54. package/docs/internal-test-server.md +89 -0
  55. package/docs/locators.md +355 -0
  56. package/docs/mcp.md +485 -0
  57. package/docs/migration-4.md +556 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +585 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins.md +866 -0
  63. package/docs/puppeteer.md +314 -0
  64. package/docs/quickstart.md +120 -0
  65. package/docs/react.md +70 -0
  66. package/docs/reports.md +483 -0
  67. package/docs/retry.md +274 -0
  68. package/docs/secrets.md +150 -0
  69. package/docs/sessions.md +80 -0
  70. package/docs/shadow.md +68 -0
  71. package/docs/test-structure.md +275 -0
  72. package/docs/timeouts.md +183 -0
  73. package/docs/translation.md +247 -0
  74. package/docs/tutorial.md +271 -0
  75. package/docs/typescript.md +374 -0
  76. package/docs/web-element.md +251 -0
  77. package/docs/webdriver.md +708 -0
  78. package/docs/within.md +55 -0
  79. package/lib/ai.js +3 -2
  80. package/lib/aria.js +260 -0
  81. package/lib/assertions.js +18 -0
  82. package/lib/codecept.js +26 -23
  83. package/lib/command/check.js +2 -1
  84. package/lib/command/dryRun.js +24 -5
  85. package/lib/command/generate.js +2 -0
  86. package/lib/command/gherkin/snippets.js +5 -4
  87. package/lib/command/init.js +248 -269
  88. package/lib/command/list.js +150 -10
  89. package/lib/command/query.js +218 -0
  90. package/lib/command/run-multiple.js +2 -0
  91. package/lib/command/run-workers.js +2 -0
  92. package/lib/command/run.js +1 -1
  93. package/lib/command/workers/runTests.js +10 -10
  94. package/lib/config.js +77 -4
  95. package/lib/container.js +114 -17
  96. package/lib/effects.js +17 -0
  97. package/lib/element/WebElement.js +246 -2
  98. package/lib/els.js +12 -6
  99. package/lib/globals.js +32 -19
  100. package/lib/heal.js +4 -3
  101. package/lib/helper/ApiDataFactory.js +2 -1
  102. package/lib/helper/Appium.js +8 -8
  103. package/lib/helper/FileSystem.js +3 -2
  104. package/lib/helper/GraphQLDataFactory.js +2 -1
  105. package/lib/helper/Playwright.js +228 -162
  106. package/lib/helper/Puppeteer.js +208 -76
  107. package/lib/helper/WebDriver.js +173 -68
  108. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  109. package/lib/helper/errors/NonFocusedType.js +8 -0
  110. package/lib/helper/extras/Download.js +45 -0
  111. package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
  112. package/lib/helper/extras/elementSelection.js +58 -0
  113. package/lib/helper/extras/focusCheck.js +43 -0
  114. package/lib/helper/extras/richTextEditor.js +178 -0
  115. package/lib/helper/scripts/dropFile.js +11 -0
  116. package/lib/history.js +3 -2
  117. package/lib/html.js +103 -16
  118. package/lib/index.js +9 -1
  119. package/lib/listener/config.js +6 -4
  120. package/lib/listener/emptyRun.js +2 -1
  121. package/lib/listener/globalRetry.js +32 -6
  122. package/lib/listener/helpers.js +4 -1
  123. package/lib/listener/mocha.js +2 -1
  124. package/lib/listener/pageobjects.js +43 -0
  125. package/lib/listener/result.js +3 -2
  126. package/lib/locator.js +126 -3
  127. package/lib/mocha/cli.js +14 -2
  128. package/lib/mocha/factory.js +7 -2
  129. package/lib/mocha/inject.js +1 -1
  130. package/lib/mocha/scenarioConfig.js +2 -1
  131. package/lib/mocha/ui.js +5 -6
  132. package/lib/parser.js +2 -2
  133. package/lib/pause.js +38 -4
  134. package/lib/plugin/aiTrace.js +453 -0
  135. package/lib/plugin/analyze.js +1 -1
  136. package/lib/plugin/auth.js +3 -3
  137. package/lib/plugin/browser.js +77 -0
  138. package/lib/plugin/expose.js +159 -0
  139. package/lib/plugin/heal.js +44 -1
  140. package/lib/plugin/pageInfo.js +53 -49
  141. package/lib/plugin/pause.js +131 -0
  142. package/lib/plugin/pauseOnFail.js +10 -34
  143. package/lib/plugin/retryFailedStep.js +28 -19
  144. package/lib/plugin/screencast.js +287 -0
  145. package/lib/plugin/screenshot.js +563 -0
  146. package/lib/plugin/screenshotOnFail.js +8 -171
  147. package/lib/rerun.js +2 -1
  148. package/lib/result.js +2 -1
  149. package/lib/step/base.js +3 -2
  150. package/lib/step/config.js +15 -2
  151. package/lib/step/record.js +2 -2
  152. package/lib/store.js +72 -3
  153. package/lib/translation.js +2 -1
  154. package/lib/utils/mask_data.js +2 -1
  155. package/lib/utils/pluginParser.js +151 -0
  156. package/lib/utils/trace.js +297 -0
  157. package/lib/utils.js +77 -3
  158. package/lib/workers.js +52 -22
  159. package/package.json +19 -13
  160. package/typings/index.d.ts +19 -5
  161. package/docs/webapi/amOnPage.mustache +0 -11
  162. package/docs/webapi/appendField.mustache +0 -11
  163. package/docs/webapi/attachFile.mustache +0 -12
  164. package/docs/webapi/blur.mustache +0 -18
  165. package/docs/webapi/checkOption.mustache +0 -13
  166. package/docs/webapi/clearCookie.mustache +0 -9
  167. package/docs/webapi/clearField.mustache +0 -9
  168. package/docs/webapi/click.mustache +0 -29
  169. package/docs/webapi/clickLink.mustache +0 -8
  170. package/docs/webapi/closeCurrentTab.mustache +0 -7
  171. package/docs/webapi/closeOtherTabs.mustache +0 -8
  172. package/docs/webapi/dontSee.mustache +0 -11
  173. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  174. package/docs/webapi/dontSeeCookie.mustache +0 -8
  175. package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
  176. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  177. package/docs/webapi/dontSeeElement.mustache +0 -8
  178. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  179. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  180. package/docs/webapi/dontSeeInField.mustache +0 -11
  181. package/docs/webapi/dontSeeInSource.mustache +0 -8
  182. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  183. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  184. package/docs/webapi/doubleClick.mustache +0 -13
  185. package/docs/webapi/downloadFile.mustache +0 -12
  186. package/docs/webapi/dragAndDrop.mustache +0 -9
  187. package/docs/webapi/dragSlider.mustache +0 -11
  188. package/docs/webapi/executeAsyncScript.mustache +0 -24
  189. package/docs/webapi/executeScript.mustache +0 -26
  190. package/docs/webapi/fillField.mustache +0 -16
  191. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  192. package/docs/webapi/focus.mustache +0 -13
  193. package/docs/webapi/forceClick.mustache +0 -28
  194. package/docs/webapi/forceRightClick.mustache +0 -18
  195. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  196. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  197. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  198. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  199. package/docs/webapi/grabCookie.mustache +0 -11
  200. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  201. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  202. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  203. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  204. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  205. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  206. package/docs/webapi/grabGeoLocation.mustache +0 -8
  207. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  208. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  209. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  210. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  211. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  212. package/docs/webapi/grabPopupText.mustache +0 -5
  213. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  214. package/docs/webapi/grabSource.mustache +0 -8
  215. package/docs/webapi/grabTextFrom.mustache +0 -10
  216. package/docs/webapi/grabTextFromAll.mustache +0 -9
  217. package/docs/webapi/grabTitle.mustache +0 -8
  218. package/docs/webapi/grabValueFrom.mustache +0 -9
  219. package/docs/webapi/grabValueFromAll.mustache +0 -8
  220. package/docs/webapi/grabWebElement.mustache +0 -9
  221. package/docs/webapi/grabWebElements.mustache +0 -9
  222. package/docs/webapi/moveCursorTo.mustache +0 -12
  223. package/docs/webapi/openNewTab.mustache +0 -7
  224. package/docs/webapi/pressKey.mustache +0 -12
  225. package/docs/webapi/pressKeyDown.mustache +0 -12
  226. package/docs/webapi/pressKeyUp.mustache +0 -12
  227. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  228. package/docs/webapi/refreshPage.mustache +0 -6
  229. package/docs/webapi/resizeWindow.mustache +0 -6
  230. package/docs/webapi/rightClick.mustache +0 -14
  231. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  232. package/docs/webapi/saveScreenshot.mustache +0 -12
  233. package/docs/webapi/say.mustache +0 -10
  234. package/docs/webapi/scrollIntoView.mustache +0 -11
  235. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  236. package/docs/webapi/scrollPageToTop.mustache +0 -6
  237. package/docs/webapi/scrollTo.mustache +0 -12
  238. package/docs/webapi/see.mustache +0 -11
  239. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  240. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  241. package/docs/webapi/seeCookie.mustache +0 -8
  242. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  243. package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
  244. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  245. package/docs/webapi/seeElement.mustache +0 -8
  246. package/docs/webapi/seeElementInDOM.mustache +0 -8
  247. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  248. package/docs/webapi/seeInField.mustache +0 -12
  249. package/docs/webapi/seeInPopup.mustache +0 -8
  250. package/docs/webapi/seeInSource.mustache +0 -7
  251. package/docs/webapi/seeInTitle.mustache +0 -8
  252. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  253. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  254. package/docs/webapi/seeTextEquals.mustache +0 -9
  255. package/docs/webapi/seeTitleEquals.mustache +0 -8
  256. package/docs/webapi/seeTraffic.mustache +0 -36
  257. package/docs/webapi/selectOption.mustache +0 -21
  258. package/docs/webapi/setCookie.mustache +0 -16
  259. package/docs/webapi/setGeoLocation.mustache +0 -12
  260. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  261. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  262. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  263. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  264. package/docs/webapi/switchTo.mustache +0 -9
  265. package/docs/webapi/switchToNextTab.mustache +0 -10
  266. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  267. package/docs/webapi/type.mustache +0 -21
  268. package/docs/webapi/uncheckOption.mustache +0 -13
  269. package/docs/webapi/wait.mustache +0 -8
  270. package/docs/webapi/waitForClickable.mustache +0 -11
  271. package/docs/webapi/waitForCookie.mustache +0 -9
  272. package/docs/webapi/waitForDetached.mustache +0 -10
  273. package/docs/webapi/waitForDisabled.mustache +0 -6
  274. package/docs/webapi/waitForElement.mustache +0 -11
  275. package/docs/webapi/waitForEnabled.mustache +0 -6
  276. package/docs/webapi/waitForFunction.mustache +0 -17
  277. package/docs/webapi/waitForInvisible.mustache +0 -10
  278. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  279. package/docs/webapi/waitForText.mustache +0 -13
  280. package/docs/webapi/waitForValue.mustache +0 -10
  281. package/docs/webapi/waitForVisible.mustache +0 -10
  282. package/docs/webapi/waitInUrl.mustache +0 -9
  283. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  284. package/docs/webapi/waitToHide.mustache +0 -10
  285. package/docs/webapi/waitUrlEquals.mustache +0 -10
  286. package/lib/helper/AI.js +0 -214
  287. package/lib/listener/enhancedGlobalRetry.js +0 -110
  288. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  289. package/lib/plugin/htmlReporter.js +0 -3648
  290. package/lib/plugin/stepByStepReport.js +0 -427
  291. package/lib/plugin/subtitles.js +0 -89
  292. package/lib/retryCoordinator.js +0 -207
  293. package/typings/promiseBasedTypes.d.ts +0 -9469
  294. package/typings/types.d.ts +0 -11402
@@ -0,0 +1,45 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import minimatch from 'minimatch'
4
+ import store from '../../store.js'
5
+ import assert from 'assert'
6
+
7
+ function getDownloadDir() {
8
+ return path.join(store.outputDir, 'downloads')
9
+ }
10
+
11
+ function getNewFiles(downloadDir, sinceTimestamp) {
12
+ if (!fs.existsSync(downloadDir)) return []
13
+ return fs.readdirSync(downloadDir).filter(name => {
14
+ const stat = fs.statSync(path.join(downloadDir, name))
15
+ return stat.isFile() && stat.mtimeMs >= sinceTimestamp
16
+ })
17
+ }
18
+
19
+ function seeFileDownloaded(arg) {
20
+ const downloadDir = getDownloadDir()
21
+ const files = getNewFiles(downloadDir, this._downloadStartTimestamp)
22
+
23
+ if (arg === undefined || arg === null) {
24
+ assert.ok(files.length > 0, `No files downloaded to ${downloadDir}`)
25
+ return
26
+ }
27
+ if (typeof arg === 'number') {
28
+ assert.strictEqual(files.length, arg, `Expected ${arg} downloaded file(s), found ${files.length}: [${files.join(', ')}]`)
29
+ return
30
+ }
31
+ const regexMatch = arg.match(/^\/(.+)\/$/)
32
+ if (regexMatch) {
33
+ const re = new RegExp(regexMatch[1])
34
+ assert.ok(files.some(f => re.test(f)), `No file matches ${arg}. Downloaded: [${files.join(', ')}]`)
35
+ return
36
+ }
37
+ if (/[*?[\]]/.test(arg)) {
38
+ const matched = minimatch.match(files, arg)
39
+ assert.ok(matched.length > 0, `No file matches glob "${arg}". Downloaded: [${files.join(', ')}]`)
40
+ return
41
+ }
42
+ assert.ok(files.includes(arg), `File "${arg}" not downloaded. Downloaded: [${files.join(', ')}]`)
43
+ }
44
+
45
+ export { seeFileDownloaded, getDownloadDir }
@@ -1,52 +1,61 @@
1
+ import fs from 'fs'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ let resqScript
5
+
1
6
  async function findReact(matcher, locator) {
2
- // Handle both Locator objects and raw locator objects
3
7
  const reactLocator = locator.locator || locator
4
- let _locator = `_react=${reactLocator.react}`;
5
- let props = '';
8
+ const page = typeof matcher.page === 'function' ? matcher.page() : matcher
6
9
 
7
- if (reactLocator.props) {
8
- props += propBuilder(reactLocator.props);
9
- _locator += props;
10
+ if (!resqScript) {
11
+ resqScript = fs.readFileSync(fileURLToPath(import.meta.resolve('resq'))).toString()
10
12
  }
11
- return matcher.locator(_locator).all();
12
- }
13
+ await page.evaluate(resqScript)
14
+ await page.evaluate(() => window.resq.waitToLoadReact())
15
+
16
+ const arrayHandle = await page.evaluateHandle(
17
+ ({ selector, props, state }) => {
18
+ let elements = window.resq.resq$$(selector)
19
+ if (Object.keys(props).length) elements = elements.byProps(props)
20
+ if (Object.keys(state).length) elements = elements.byState(state)
21
+ if (!elements.length) return []
13
22
 
14
- async function findVue(matcher, locator) {
15
- // Handle both Locator objects and raw locator objects
16
- const vueLocator = locator.locator || locator
17
- let _locator = `_vue=${vueLocator.vue}`;
18
- let props = '';
23
+ let nodes = []
24
+ elements.forEach(element => {
25
+ let { node, isFragment } = element
26
+ if (!node) {
27
+ isFragment = true
28
+ node = element.children
29
+ }
30
+ if (isFragment) nodes = nodes.concat(node)
31
+ else nodes.push(node)
32
+ })
33
+ return [...nodes]
34
+ },
35
+ {
36
+ selector: reactLocator.react,
37
+ props: reactLocator.props || {},
38
+ state: reactLocator.state || {},
39
+ },
40
+ )
19
41
 
20
- if (vueLocator.props) {
21
- props += propBuilder(vueLocator.props);
22
- _locator += props;
42
+ const properties = await arrayHandle.getProperties()
43
+ await arrayHandle.dispose()
44
+ const result = []
45
+ for (const property of properties.values()) {
46
+ const elementHandle = property.asElement()
47
+ if (elementHandle) result.push(elementHandle)
23
48
  }
24
- return matcher.locator(_locator).all();
49
+ return result
25
50
  }
26
51
 
27
52
  async function findByPlaywrightLocator(matcher, locator) {
28
- // Handle both Locator objects and raw locator objects
29
53
  const pwLocator = locator.locator || locator
30
54
  if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
31
- return matcher.getByTestId(pwLocator.pw.value.split('=')[1]);
55
+ return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
32
56
  }
33
57
  const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
34
- return matcher.locator(pwValue).all();
35
- }
36
-
37
- function propBuilder(props) {
38
- let _props = '';
39
-
40
- for (const [key, value] of Object.entries(props)) {
41
- if (typeof value === 'object') {
42
- for (const [k, v] of Object.entries(value)) {
43
- _props += `[${key}.${k} = "${v}"]`;
44
- }
45
- } else {
46
- _props += `[${key} = "${value}"]`;
47
- }
48
- }
49
- return _props;
58
+ return matcher.locator(pwValue).all()
50
59
  }
51
60
 
52
- export { findReact, findVue, findByPlaywrightLocator };
61
+ export { findReact, findByPlaywrightLocator }
@@ -0,0 +1,58 @@
1
+ import store from '../../store.js'
2
+ import output from '../../output.js'
3
+ import WebElement from '../../element/WebElement.js'
4
+ import MultipleElementsFound from '../errors/MultipleElementsFound.js'
5
+
6
+ function resolveElementIndex(value) {
7
+ if (value === 'first') return 1
8
+ if (value === 'last') return -1
9
+ return value
10
+ }
11
+
12
+ function isStrictStep(opts, helper) {
13
+ if (opts?.exact === true || opts?.strictMode === true) return true
14
+ if (opts?.exact === false || opts?.strictMode === false) return false
15
+ return helper.options.strict
16
+ }
17
+
18
+ function selectElement(els, locator, helper) {
19
+ const opts = store.currentStep?.opts
20
+ const rawIndex = opts?.elementIndex
21
+ const elementIndex = resolveElementIndex(rawIndex)
22
+
23
+ if (elementIndex != null) {
24
+ if (els.length === 1) return els[0]
25
+
26
+ if (!Number.isInteger(elementIndex) || elementIndex === 0) {
27
+ throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
28
+ }
29
+
30
+ let idx
31
+ if (elementIndex > 0) {
32
+ idx = elementIndex - 1
33
+ if (idx >= els.length) {
34
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
35
+ }
36
+ } else {
37
+ idx = els.length + elementIndex
38
+ if (idx < 0) {
39
+ throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
40
+ }
41
+ }
42
+
43
+ output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
44
+ return els[idx]
45
+ }
46
+
47
+ if (isStrictStep(opts, helper)) {
48
+ if (els.length > 1) {
49
+ const webElements = Array.from(els).map(el => new WebElement(el, helper))
50
+ throw new MultipleElementsFound(locator, webElements)
51
+ }
52
+ }
53
+
54
+ if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
55
+ return els[0]
56
+ }
57
+
58
+ export { selectElement }
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ export const dropFile = (el, { base64Content, fileName, mimeType }) => {
2
+ const binaryStr = atob(base64Content)
3
+ const bytes = new Uint8Array(binaryStr.length)
4
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
5
+ const fileObj = new File([bytes], fileName, { type: mimeType })
6
+ const dataTransfer = new DataTransfer()
7
+ dataTransfer.items.add(fileObj)
8
+ el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
9
+ el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
10
+ el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
11
+ }
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
 
@@ -245,4 +292,44 @@ function splitByChunks(text, chunkSize) {
245
292
  return chunks.map(chunk => chunk.trim())
246
293
  }
247
294
 
248
- export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml }
295
+ function simplifyHtmlElement(html, maxLength = 300) {
296
+ try {
297
+ html = removeNonInteractiveElements(html)
298
+ html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
299
+ } catch (e) {
300
+ // keep raw html if minification fails
301
+ }
302
+ if (html.length > maxLength) {
303
+ html = html.slice(0, maxLength) + '...'
304
+ }
305
+ return html
306
+ }
307
+
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()