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
@@ -5,16 +5,27 @@ import { isNotSet } from '../utils.js'
5
5
 
6
6
  const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
7
7
 
8
+ const RETRY_PRIORITIES = {
9
+ MANUAL_STEP: 100,
10
+ STEP_PLUGIN: 50,
11
+ SCENARIO_CONFIG: 30,
12
+ FEATURE_CONFIG: 20,
13
+ HOOK_CONFIG: 10,
14
+ }
15
+
8
16
  export default function () {
9
17
  event.dispatcher.on(event.suite.before, suite => {
10
18
  let retryConfig = Config.get('retry')
11
19
  if (!retryConfig) return
12
20
 
13
21
  if (Number.isInteger(+retryConfig)) {
14
- // is number
15
22
  const retryNum = +retryConfig
16
23
  output.log(`Retries: ${retryNum}`)
17
- suite.retries(retryNum)
24
+
25
+ if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
26
+ suite.retries(retryNum)
27
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
28
+ }
18
29
  return
19
30
  }
20
31
 
@@ -30,11 +41,18 @@ export default function () {
30
41
  hooks
31
42
  .filter(hook => !!config[hook])
32
43
  .forEach(hook => {
33
- if (isNotSet(suite.opts[`retry${hook}`])) suite.opts[`retry${hook}`] = config[hook]
44
+ const retryKey = `retry${hook}`
45
+ if (isNotSet(suite.opts[retryKey])) {
46
+ suite.opts[retryKey] = config[hook]
47
+ suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
48
+ }
34
49
  })
35
50
 
36
51
  if (config.Feature) {
37
- if (isNotSet(suite.retries())) suite.retries(config.Feature)
52
+ if (suite.retries() === -1 || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
53
+ suite.retries(config.Feature)
54
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
55
+ }
38
56
  }
39
57
 
40
58
  output.log(`Retries: ${JSON.stringify(config)}`)
@@ -46,7 +64,10 @@ export default function () {
46
64
  if (!retryConfig) return
47
65
 
48
66
  if (Number.isInteger(+retryConfig)) {
49
- if (test.retries() === -1) test.retries(retryConfig)
67
+ if (test.retries() === -1) {
68
+ test.retries(retryConfig)
69
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
70
+ }
50
71
  return
51
72
  }
52
73
 
@@ -62,9 +83,14 @@ export default function () {
62
83
  }
63
84
 
64
85
  if (config.Scenario) {
65
- if (test.retries() === -1) test.retries(config.Scenario)
86
+ if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
87
+ test.retries(config.Scenario)
88
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
89
+ }
66
90
  output.log(`Retries: ${config.Scenario}`)
67
91
  }
68
92
  }
69
93
  })
70
94
  }
95
+
96
+ export { RETRY_PRIORITIES }
@@ -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
  }
package/lib/locator.js CHANGED
@@ -381,9 +381,121 @@ class Locator {
381
381
  return new Locator({ xpath })
382
382
  }
383
383
 
384
+ /**
385
+ * Find an element with all of the provided CSS classes (word-exact match).
386
+ * Accepts variadic class names; all must be present.
387
+ *
388
+ * Example:
389
+ * locate('button').withClass('btn-primary', 'btn-lg')
390
+ *
391
+ * @param {...string} classes
392
+ * @returns {Locator}
393
+ */
394
+ withClass(...classes) {
395
+ if (!classes.length) return this
396
+ const predicates = classes.map(c => `contains(concat(' ', normalize-space(@class), ' '), ' ${c} ')`)
397
+ const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
398
+ return new Locator({ xpath })
399
+ }
400
+
401
+ /**
402
+ * Find an element with none of the provided CSS classes.
403
+ *
404
+ * Example:
405
+ * locate('tr').withoutClass('deleted')
406
+ *
407
+ * @param {...string} classes
408
+ * @returns {Locator}
409
+ */
410
+ withoutClass(...classes) {
411
+ if (!classes.length) return this
412
+ const predicates = classes.map(c => `not(contains(concat(' ', normalize-space(@class), ' '), ' ${c} '))`)
413
+ const xpath = sprintf('%s[%s]', this.toXPath(), predicates.join(' and '))
414
+ return new Locator({ xpath })
415
+ }
416
+
417
+ /**
418
+ * Find an element that does NOT contain the provided text.
419
+ * @param {string} text
420
+ * @returns {Locator}
421
+ */
422
+ withoutText(text) {
423
+ text = xpathLocator.literal(text)
424
+ const xpath = sprintf('%s[%s]', this.toXPath(), `not(contains(., ${text}))`)
425
+ return new Locator({ xpath })
426
+ }
427
+
428
+ /**
429
+ * Find an element that does NOT have any of the provided attribute/value pairs.
430
+ * @param {Object.<string, string>} attributes
431
+ * @returns {Locator}
432
+ */
433
+ withoutAttr(attributes) {
434
+ const operands = []
435
+ for (const attr of Object.keys(attributes)) {
436
+ operands.push(`not(@${attr} = ${xpathLocator.literal(attributes[attr])})`)
437
+ }
438
+ const xpath = sprintf('%s[%s]', this.toXPath(), operands.join(' and '))
439
+ return new Locator({ xpath })
440
+ }
441
+
442
+ /**
443
+ * Find an element that has no direct child matching the provided locator.
444
+ * @param {CodeceptJS.LocatorOrString} locator
445
+ * @returns {Locator}
446
+ */
447
+ withoutChild(locator) {
448
+ const xpath = sprintf('%s[not(./child::%s)]', this.toXPath(), convertToSubSelector(locator))
449
+ return new Locator({ xpath })
450
+ }
451
+
452
+ /**
453
+ * Find an element that has no descendant matching the provided locator.
454
+ *
455
+ * Example:
456
+ * locate('button').withoutDescendant('svg')
457
+ *
458
+ * @param {CodeceptJS.LocatorOrString} locator
459
+ * @returns {Locator}
460
+ */
461
+ withoutDescendant(locator) {
462
+ const xpath = sprintf('%s[not(./descendant::%s)]', this.toXPath(), convertToSubSelector(locator))
463
+ return new Locator({ xpath })
464
+ }
465
+
466
+ /**
467
+ * Append a raw XPath predicate. Escape hatch for expressions not covered by the DSL.
468
+ * Argument is inserted as-is inside `[ ]`; quoting/escaping is the caller's responsibility.
469
+ *
470
+ * Example:
471
+ * locate('input').and('@type="text" or @type="email"')
472
+ *
473
+ * @param {string} xpathExpression
474
+ * @returns {Locator}
475
+ */
476
+ and(xpathExpression) {
477
+ const xpath = sprintf('%s[%s]', this.toXPath(), xpathExpression)
478
+ return new Locator({ xpath })
479
+ }
480
+
481
+ /**
482
+ * Append a negated raw XPath predicate: `[not(expr)]`.
483
+ *
484
+ * Example:
485
+ * locate('button').andNot('.//svg') // button without a descendant svg
486
+ *
487
+ * @param {string} xpathExpression
488
+ * @returns {Locator}
489
+ */
490
+ andNot(xpathExpression) {
491
+ const xpath = sprintf('%s[not(%s)]', this.toXPath(), xpathExpression)
492
+ return new Locator({ xpath })
493
+ }
494
+
384
495
  /**
385
496
  * @param {String} text
386
497
  * @returns {Locator}
498
+ * @deprecated Use {@link Locator#withClass} for word-exact class matching, or {@link Locator#withAttrContains} for substring matching.
387
499
  */
388
500
  withClassAttr(text) {
389
501
  const xpath = sprintf('%s[%s]', this.toXPath(), `contains(@class, '${text}')`)
@@ -477,15 +589,26 @@ Locator.clickable = {
477
589
  `.//button[./@name = ${literal}]`,
478
590
  `.//*[@aria-label = ${literal}]`,
479
591
  `.//*[@title = ${literal}]`,
480
- `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
592
+ `.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
481
593
  `.//*[@role='button'][normalize-space(.)=${literal}]`,
594
+ `.//*[@role='tab' or @role='link' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='treeitem'][contains(normalize-space(string(.)), ${literal})]`,
482
595
  ]),
483
596
 
484
597
  /**
485
598
  * @param {string} literal
486
599
  * @returns {string}
487
600
  */
488
- self: literal => `./self::*[contains(normalize-space(string(.)), ${literal}) or contains(normalize-space(@value), ${literal})]`,
601
+ self: literal => {
602
+ // Narrowest-match: prefer the deepest descendant whose string-value contains the literal.
603
+ // Falling back to `self` without the `not(descendant...)` guard would match a container
604
+ // whose concatenated text happens to include the literal (e.g. a <ul role="tablist"> whose
605
+ // tab labels all sit in its string-value) and click the container itself.
606
+ const narrowest = `contains(normalize-space(string(.)), ${literal}) and not(.//*[contains(normalize-space(string(.)), ${literal})])`
607
+ return xpathLocator.combine([
608
+ `.//*[${narrowest}]`,
609
+ `./self::*[${narrowest} or contains(normalize-space(@value), ${literal})]`,
610
+ ])
611
+ },
489
612
  }
490
613
 
491
614
  Locator.field = {
@@ -509,7 +632,7 @@ Locator.field = {
509
632
  `.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
510
633
  `.//*[@aria-label = ${literal}]`,
511
634
  `.//*[@title = ${literal}]`,
512
- `.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id ]`,
635
+ `.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
513
636
  ]),
514
637
 
515
638
  /**
package/lib/mocha/cli.js CHANGED
@@ -8,6 +8,7 @@ import { dirname, join } from 'path'
8
8
  import event from '../event.js'
9
9
  import AssertionFailedError from '../assert/error.js'
10
10
  import output from '../output.js'
11
+ import store from '../store.js'
11
12
  import test, { cloneTest } from './test.js'
12
13
  import { fixErrorStack } from '../utils/typescript.js'
13
14
 
@@ -41,7 +42,7 @@ class Cli extends Base {
41
42
  if (opts.verbose) level = 3
42
43
  output.level(level)
43
44
  output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`)
44
- output.print(`Using test root "${global.codecept_dir}"`)
45
+ output.print(`Using test root "${store.codeceptDir}"`)
45
46
 
46
47
  const showSteps = level >= 1
47
48
 
@@ -202,7 +203,18 @@ class Cli extends Base {
202
203
 
203
204
  // failures
204
205
  if (stats.failures) {
206
+ for (const test of this.failures) {
207
+ if (test.err && typeof test.err.fetchDetails === 'function') {
208
+ try {
209
+ await test.err.fetchDetails()
210
+ } catch (e) {
211
+ // ignore fetch errors
212
+ }
213
+ }
214
+ }
215
+
205
216
  // append step traces
217
+ const Container = await getContainer()
206
218
  this.failures = this.failures.map(test => {
207
219
  // we will change the stack trace, so we need to clone the test
208
220
  const err = test.err
@@ -265,7 +277,7 @@ class Cli extends Base {
265
277
  }
266
278
 
267
279
  try {
268
- const fileMapping = global.container?.tsFileMapping?.()
280
+ const fileMapping = Container?.tsFileMapping?.()
269
281
  if (fileMapping) {
270
282
  fixErrorStack(err, fileMapping)
271
283
  }
@@ -8,6 +8,7 @@ import output from '../output.js'
8
8
  import scenarioUiFunction from './ui.js'
9
9
  import { initMochaGlobals } from '../globals.js'
10
10
  import { fixErrorStack } from '../utils/typescript.js'
11
+ import container from '../container.js'
11
12
 
12
13
  const __filename = fileURLToPath(import.meta.url)
13
14
  const __dirname = fsPath.dirname(__filename)
@@ -16,7 +17,11 @@ let mocha
16
17
 
17
18
  class MochaFactory {
18
19
  static create(config, opts) {
19
- mocha = new Mocha(Object.assign(config, opts))
20
+ const merged = Object.assign({}, config, opts)
21
+ mocha = new Mocha(merged)
22
+ if (merged.cleanReferencesAfterRun !== true) {
23
+ mocha.cleanReferencesAfterRun(false)
24
+ }
20
25
  output.process(opts.child)
21
26
  mocha.ui(scenarioUiFunction)
22
27
 
@@ -35,7 +40,7 @@ class MochaFactory {
35
40
  // Handle ECONNREFUSED without dynamic import for now
36
41
  err = new Error('Connection refused: ' + err.toString())
37
42
  }
38
- const fileMapping = global.container?.tsFileMapping?.()
43
+ const fileMapping = container?.tsFileMapping?.()
39
44
  if (fileMapping) {
40
45
  fixErrorStack(err, fileMapping)
41
46
  }
@@ -5,7 +5,7 @@ const getInjectedArguments = async (fn, test, suite) => {
5
5
  const container = containerModule.default || containerModule
6
6
 
7
7
  const testArgs = {}
8
- const params = getParams(fn) || []
8
+ const params = getParams(fn, { warnOnLegacyFormat: true }) || []
9
9
  const objects = container.support()
10
10
 
11
11
  for (const key of params) {
@@ -1,4 +1,5 @@
1
1
  import { isAsyncFunction } from '../utils.js'
2
+ import store from '../store.js'
2
3
 
3
4
  /** @class */
4
5
  class ScenarioConfig {
@@ -40,7 +41,7 @@ class ScenarioConfig {
40
41
  * @returns {this}
41
42
  */
42
43
  retry(retries) {
43
- if (process.env.SCENARIO_ONLY) retries = -retries
44
+ if (store.scenarioOnly) retries = -retries
44
45
  this.test.retries(retries)
45
46
  return this
46
47
  }
package/lib/mocha/ui.js CHANGED
@@ -9,13 +9,12 @@ import { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } fr
9
9
  import { initMochaGlobals } from '../globals.js'
10
10
  import common from 'mocha/lib/interfaces/common.js'
11
11
  import container from '../container.js'
12
+ import store from '../store.js'
12
13
 
13
14
  const setContextTranslation = context => {
14
- // Try global container first, then local container instance
15
- const containerToUse = global.container || container
16
- if (!containerToUse) return
15
+ if (!container) return
17
16
 
18
- const translation = containerToUse.translation?.() || containerToUse.translation
17
+ const translation = container.translation?.() || container.translation
19
18
  const contexts = translation?.value?.('contexts')
20
19
 
21
20
  if (contexts) {
@@ -119,7 +118,7 @@ export default function (suite) {
119
118
  context.Feature.only = function (title, opts) {
120
119
  const reString = `^${escapeRe(`${title}:`)}`
121
120
  mocha.grep(new RegExp(reString))
122
- process.env.FEATURE_ONLY = true
121
+ store.featureOnly = true
123
122
  return context.Feature(title, opts)
124
123
  }
125
124
 
@@ -171,7 +170,7 @@ export default function (suite) {
171
170
  context.Scenario.only = function (title, opts, fn) {
172
171
  const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`
173
172
  mocha.grep(new RegExp(reString))
174
- process.env.SCENARIO_ONLY = true
173
+ store.scenarioOnly = true
175
174
  return addScenario(title, opts, fn)
176
175
  }
177
176
 
package/lib/parser.js CHANGED
@@ -14,11 +14,11 @@ export const getParamsToString = function (fn) {
14
14
  return getParams(newFn).join(', ')
15
15
  }
16
16
 
17
- function getParams(fn) {
17
+ function getParams(fn, { warnOnLegacyFormat = false } = {}) {
18
18
  if (fn.isSinonProxy) return []
19
19
  try {
20
20
  const reflected = parser.parse(fn)
21
- if (reflected.args.length > 1 || reflected.args[0] === 'I') {
21
+ if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) {
22
22
  output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
23
23
  }
24
24
  if (reflected.destructuredArgs.length > 0) reflected.args = [...reflected.destructuredArgs]
package/lib/pause.js CHANGED
@@ -18,6 +18,8 @@ let nextStep
18
18
  let finish
19
19
  let next
20
20
  let registeredVariables = {}
21
+ let externalHandler = null
22
+
21
23
  /**
22
24
  * Pauses test execution and starts interactive shell
23
25
  * @param {Object<string, *>} [passedObject]
@@ -37,10 +39,10 @@ const pause = function (passedObject = {}) {
37
39
  })
38
40
 
39
41
  event.dispatcher.on(event.test.finished, () => {
40
- finish()
42
+ if (typeof finish === 'function') finish()
41
43
  recorder.session.restore('pause')
42
- rl.close()
43
- history.save()
44
+ if (rl) rl.close()
45
+ if (!externalHandler) history.save()
44
46
  })
45
47
 
46
48
  recorder.add('Start new session', () => pauseSession(passedObject))
@@ -49,6 +51,15 @@ const pause = function (passedObject = {}) {
49
51
  function pauseSession(passedObject = {}) {
50
52
  registeredVariables = passedObject
51
53
  recorder.session.start('pause')
54
+
55
+ if (externalHandler) {
56
+ store.onPause = true
57
+ return externalHandler({ registeredVariables }).then(() => {
58
+ store.onPause = false
59
+ recorder.session.restore('pause')
60
+ })
61
+ }
62
+
52
63
  if (!next) {
53
64
  let vars = Object.keys(registeredVariables).join(', ')
54
65
  if (vars) vars = `(vars: ${vars})`
@@ -234,5 +245,28 @@ function registerVariable(name, value) {
234
245
  registeredVariables[name] = value
235
246
  }
236
247
 
248
+ /**
249
+ * Hook for external pause drivers (e.g. the MCP server). When set, pauseSession
250
+ * delegates to the handler instead of opening a readline REPL. The handler
251
+ * receives `{ registeredVariables }` and returns a Promise that resolves when
252
+ * the driver decides to continue (resume) or step.
253
+ *
254
+ * The driver controls step-vs-resume by mutating `next` via setNextStep before
255
+ * resolving its Promise.
256
+ */
257
+ function setPauseHandler(handler) {
258
+ externalHandler = handler
259
+ }
260
+
261
+ /**
262
+ * Trigger a one-shot pause from outside the test (e.g. the MCP server,
263
+ * pausing the test at a specific step index without modifying the test).
264
+ * Schedules pauseSession through the recorder so it slots between steps.
265
+ */
266
+ function pauseNow(passedObject = {}) {
267
+ if (store.dryRun) return
268
+ recorder.add('Triggered pause', () => pauseSession(passedObject))
269
+ }
270
+
237
271
  export default pause
238
- export { registerVariable }
272
+ export { registerVariable, setPauseHandler, pauseNow }