codeceptjs 4.0.2-beta.9 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +39 -28
  2. package/bin/codecept.js +15 -2
  3. package/bin/codeceptq.js +49 -0
  4. package/bin/mcp-server.js +1189 -0
  5. package/docs/advanced.md +201 -0
  6. package/docs/agents.md +181 -0
  7. package/docs/ai.md +489 -0
  8. package/docs/aitrace.md +266 -0
  9. package/docs/api.md +332 -0
  10. package/docs/architecture.md +235 -0
  11. package/docs/assertions.md +415 -0
  12. package/docs/auth.md +318 -0
  13. package/docs/basics.md +424 -0
  14. package/docs/bdd.md +539 -0
  15. package/docs/best.md +240 -0
  16. package/docs/bootstrap.md +132 -0
  17. package/docs/commands.md +352 -0
  18. package/docs/community-helpers.md +63 -0
  19. package/docs/configuration.md +185 -0
  20. package/docs/continuous-integration.md +431 -0
  21. package/docs/custom-helpers.md +297 -0
  22. package/docs/data.md +448 -0
  23. package/docs/debugging.md +332 -0
  24. package/docs/detox.md +235 -0
  25. package/docs/docker.md +107 -0
  26. package/docs/effects.md +179 -0
  27. package/docs/element-based-testing.md +295 -0
  28. package/docs/element-selection.md +125 -0
  29. package/docs/els.md +328 -0
  30. package/docs/environment-variables.md +131 -0
  31. package/docs/examples.md +160 -0
  32. package/docs/heal.md +213 -0
  33. package/docs/helpers/ApiDataFactory.md +267 -0
  34. package/docs/helpers/Appium.md +1419 -0
  35. package/docs/helpers/Detox.md +665 -0
  36. package/docs/helpers/ExpectHelper.md +275 -0
  37. package/docs/helpers/FileSystem.md +152 -0
  38. package/docs/helpers/GraphQL.md +152 -0
  39. package/docs/helpers/GraphQLDataFactory.md +226 -0
  40. package/docs/helpers/JSONResponse.md +255 -0
  41. package/docs/helpers/MockRequest.md +377 -0
  42. package/docs/helpers/Playwright.md +2970 -0
  43. package/docs/helpers/Puppeteer-firefox.md +86 -0
  44. package/docs/helpers/Puppeteer.md +2583 -0
  45. package/docs/helpers/REST.md +289 -0
  46. package/docs/helpers/WebDriver.md +2639 -0
  47. package/docs/hooks.md +148 -0
  48. package/docs/index.md +111 -0
  49. package/docs/installation.md +121 -0
  50. package/docs/internal-test-server.md +89 -0
  51. package/docs/locators.md +355 -0
  52. package/docs/mcp.md +485 -0
  53. package/docs/migrate-from-cypress.md +98 -0
  54. package/docs/migrate-from-java.md +108 -0
  55. package/docs/migrate-from-protractor.md +101 -0
  56. package/docs/migrate-from-testcafe.md +99 -0
  57. package/docs/migration-4.md +745 -0
  58. package/docs/mobile.md +338 -0
  59. package/docs/pageobjects.md +399 -0
  60. package/docs/parallel.md +187 -0
  61. package/docs/playwright.md +714 -0
  62. package/docs/plugins/aiTrace.md +49 -0
  63. package/docs/plugins/analyze.md +66 -0
  64. package/docs/plugins/auth.md +241 -0
  65. package/docs/plugins/autoDelay.md +48 -0
  66. package/docs/plugins/browser.md +41 -0
  67. package/docs/plugins/coverage.md +39 -0
  68. package/docs/plugins/customLocator.md +119 -0
  69. package/docs/plugins/customReporter.md +16 -0
  70. package/docs/plugins/expose.md +75 -0
  71. package/docs/plugins/heal.md +44 -0
  72. package/docs/plugins/junitReporter.md +51 -0
  73. package/docs/plugins/pageInfo.md +34 -0
  74. package/docs/plugins/pause.md +43 -0
  75. package/docs/plugins/pauseOnFail.md +18 -0
  76. package/docs/plugins/retryFailedStep.md +75 -0
  77. package/docs/plugins/screencast.md +55 -0
  78. package/docs/plugins/screenshot.md +58 -0
  79. package/docs/plugins/screenshotOnFail.md +18 -0
  80. package/docs/plugins/stepTimeout.md +65 -0
  81. package/docs/plugins.md +87 -0
  82. package/docs/puppeteer.md +314 -0
  83. package/docs/quickstart.md +120 -0
  84. package/docs/reports.md +195 -0
  85. package/docs/retry.md +311 -0
  86. package/docs/secrets.md +150 -0
  87. package/docs/sessions.md +80 -0
  88. package/docs/shadow.md +68 -0
  89. package/docs/store.md +94 -0
  90. package/docs/test-structure.md +275 -0
  91. package/docs/timeouts.md +183 -0
  92. package/docs/translation.md +247 -0
  93. package/docs/tutorial.md +323 -0
  94. package/docs/typescript.md +159 -0
  95. package/docs/web-element.md +251 -0
  96. package/docs/webdriver.md +641 -0
  97. package/docs/within.md +55 -0
  98. package/lib/actor.js +1 -36
  99. package/lib/ai.js +3 -2
  100. package/lib/aria.js +260 -0
  101. package/lib/assertions.js +18 -0
  102. package/lib/codecept.js +34 -25
  103. package/lib/command/check.js +2 -1
  104. package/lib/command/definitions.js +6 -7
  105. package/lib/command/dryRun.js +24 -5
  106. package/lib/command/generate.js +3 -1
  107. package/lib/command/gherkin/snippets.js +5 -4
  108. package/lib/command/init.js +249 -270
  109. package/lib/command/list.js +150 -10
  110. package/lib/command/query.js +218 -0
  111. package/lib/command/run-multiple.js +3 -1
  112. package/lib/command/run-workers.js +2 -14
  113. package/lib/command/run.js +3 -17
  114. package/lib/command/utils.js +14 -0
  115. package/lib/command/workers/runTests.js +84 -41
  116. package/lib/config.js +96 -18
  117. package/lib/container.js +115 -17
  118. package/lib/effects.js +17 -0
  119. package/lib/element/WebElement.js +246 -2
  120. package/lib/els.js +12 -6
  121. package/lib/globals.js +32 -19
  122. package/lib/heal.js +7 -4
  123. package/lib/helper/ApiDataFactory.js +2 -1
  124. package/lib/helper/Appium.js +8 -8
  125. package/lib/helper/FileSystem.js +3 -2
  126. package/lib/helper/GraphQLDataFactory.js +2 -1
  127. package/lib/helper/Playwright.js +358 -467
  128. package/lib/helper/Puppeteer.js +335 -192
  129. package/lib/helper/WebDriver.js +324 -111
  130. package/lib/helper/errors/ElementNotFound.js +5 -2
  131. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  132. package/lib/helper/errors/NonFocusedType.js +8 -0
  133. package/lib/helper/extras/Download.js +45 -0
  134. package/lib/helper/extras/PlaywrightLocator.js +7 -107
  135. package/lib/helper/extras/elementSelection.js +58 -0
  136. package/lib/helper/extras/focusCheck.js +43 -0
  137. package/lib/helper/extras/richTextEditor.js +178 -0
  138. package/lib/helper/scripts/dropFile.js +11 -0
  139. package/lib/history.js +3 -2
  140. package/lib/html.js +103 -16
  141. package/lib/index.js +9 -1
  142. package/lib/listener/config.js +6 -4
  143. package/lib/listener/emptyRun.js +2 -1
  144. package/lib/listener/globalRetry.js +32 -6
  145. package/lib/listener/helpers.js +4 -1
  146. package/lib/listener/mocha.js +2 -1
  147. package/lib/listener/pageobjects.js +43 -0
  148. package/lib/listener/result.js +3 -2
  149. package/lib/locator.js +158 -16
  150. package/lib/mocha/cli.js +19 -1
  151. package/lib/mocha/factory.js +11 -1
  152. package/lib/mocha/inject.js +1 -1
  153. package/lib/mocha/scenarioConfig.js +2 -1
  154. package/lib/mocha/ui.js +5 -6
  155. package/lib/parser.js +2 -2
  156. package/lib/pause.js +38 -4
  157. package/lib/plugin/aiTrace.js +457 -0
  158. package/lib/plugin/analyze.js +9 -9
  159. package/lib/plugin/auth.js +5 -4
  160. package/lib/plugin/browser.js +77 -0
  161. package/lib/plugin/expose.js +159 -0
  162. package/lib/plugin/heal.js +47 -3
  163. package/lib/plugin/junitReporter.js +303 -0
  164. package/lib/plugin/pageInfo.js +54 -52
  165. package/lib/plugin/pause.js +131 -0
  166. package/lib/plugin/pauseOnFail.js +11 -33
  167. package/lib/plugin/retryFailedStep.js +43 -32
  168. package/lib/plugin/screencast.js +289 -0
  169. package/lib/plugin/screenshot.js +558 -0
  170. package/lib/plugin/screenshotOnFail.js +9 -170
  171. package/lib/plugin/stepTimeout.js +3 -2
  172. package/lib/recorder.js +1 -1
  173. package/lib/rerun.js +2 -1
  174. package/lib/result.js +2 -1
  175. package/lib/step/base.js +10 -9
  176. package/lib/step/comment.js +2 -2
  177. package/lib/step/config.js +15 -2
  178. package/lib/step/helper.js +4 -4
  179. package/lib/step/meta.js +3 -3
  180. package/lib/step/record.js +5 -5
  181. package/lib/store.js +72 -3
  182. package/lib/translation.js +2 -1
  183. package/lib/utils/loaderCheck.js +28 -0
  184. package/lib/utils/mask_data.js +2 -1
  185. package/lib/utils/pluginParser.js +151 -0
  186. package/lib/utils/trace.js +297 -0
  187. package/lib/utils/typescript.js +188 -23
  188. package/lib/utils.js +77 -3
  189. package/lib/workers.js +65 -40
  190. package/package.json +35 -30
  191. package/typings/index.d.ts +119 -8
  192. package/typings/promiseBasedTypes.d.ts +3158 -6065
  193. package/typings/types.d.ts +3453 -6494
  194. package/docs/webapi/amOnPage.mustache +0 -11
  195. package/docs/webapi/appendField.mustache +0 -11
  196. package/docs/webapi/attachFile.mustache +0 -12
  197. package/docs/webapi/blur.mustache +0 -18
  198. package/docs/webapi/checkOption.mustache +0 -13
  199. package/docs/webapi/clearCookie.mustache +0 -9
  200. package/docs/webapi/clearField.mustache +0 -9
  201. package/docs/webapi/click.mustache +0 -29
  202. package/docs/webapi/clickLink.mustache +0 -8
  203. package/docs/webapi/closeCurrentTab.mustache +0 -7
  204. package/docs/webapi/closeOtherTabs.mustache +0 -8
  205. package/docs/webapi/dontSee.mustache +0 -11
  206. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
  207. package/docs/webapi/dontSeeCookie.mustache +0 -8
  208. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
  209. package/docs/webapi/dontSeeElement.mustache +0 -8
  210. package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
  211. package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
  212. package/docs/webapi/dontSeeInField.mustache +0 -11
  213. package/docs/webapi/dontSeeInSource.mustache +0 -8
  214. package/docs/webapi/dontSeeInTitle.mustache +0 -8
  215. package/docs/webapi/dontSeeTraffic.mustache +0 -13
  216. package/docs/webapi/doubleClick.mustache +0 -13
  217. package/docs/webapi/downloadFile.mustache +0 -12
  218. package/docs/webapi/dragAndDrop.mustache +0 -9
  219. package/docs/webapi/dragSlider.mustache +0 -11
  220. package/docs/webapi/executeAsyncScript.mustache +0 -24
  221. package/docs/webapi/executeScript.mustache +0 -26
  222. package/docs/webapi/fillField.mustache +0 -16
  223. package/docs/webapi/flushNetworkTraffics.mustache +0 -5
  224. package/docs/webapi/focus.mustache +0 -13
  225. package/docs/webapi/forceClick.mustache +0 -28
  226. package/docs/webapi/forceRightClick.mustache +0 -18
  227. package/docs/webapi/grabAllWindowHandles.mustache +0 -7
  228. package/docs/webapi/grabAttributeFrom.mustache +0 -10
  229. package/docs/webapi/grabAttributeFromAll.mustache +0 -9
  230. package/docs/webapi/grabBrowserLogs.mustache +0 -9
  231. package/docs/webapi/grabCookie.mustache +0 -11
  232. package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
  233. package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
  234. package/docs/webapi/grabCurrentUrl.mustache +0 -9
  235. package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
  236. package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
  237. package/docs/webapi/grabElementBoundingRect.mustache +0 -20
  238. package/docs/webapi/grabGeoLocation.mustache +0 -8
  239. package/docs/webapi/grabHTMLFrom.mustache +0 -10
  240. package/docs/webapi/grabHTMLFromAll.mustache +0 -9
  241. package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
  242. package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
  243. package/docs/webapi/grabPageScrollPosition.mustache +0 -8
  244. package/docs/webapi/grabPopupText.mustache +0 -5
  245. package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
  246. package/docs/webapi/grabSource.mustache +0 -8
  247. package/docs/webapi/grabTextFrom.mustache +0 -10
  248. package/docs/webapi/grabTextFromAll.mustache +0 -9
  249. package/docs/webapi/grabTitle.mustache +0 -8
  250. package/docs/webapi/grabValueFrom.mustache +0 -9
  251. package/docs/webapi/grabValueFromAll.mustache +0 -8
  252. package/docs/webapi/grabWebElement.mustache +0 -9
  253. package/docs/webapi/grabWebElements.mustache +0 -9
  254. package/docs/webapi/moveCursorTo.mustache +0 -12
  255. package/docs/webapi/openNewTab.mustache +0 -7
  256. package/docs/webapi/pressKey.mustache +0 -12
  257. package/docs/webapi/pressKeyDown.mustache +0 -12
  258. package/docs/webapi/pressKeyUp.mustache +0 -12
  259. package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
  260. package/docs/webapi/refreshPage.mustache +0 -6
  261. package/docs/webapi/resizeWindow.mustache +0 -6
  262. package/docs/webapi/rightClick.mustache +0 -14
  263. package/docs/webapi/saveElementScreenshot.mustache +0 -10
  264. package/docs/webapi/saveScreenshot.mustache +0 -12
  265. package/docs/webapi/say.mustache +0 -10
  266. package/docs/webapi/scrollIntoView.mustache +0 -11
  267. package/docs/webapi/scrollPageToBottom.mustache +0 -6
  268. package/docs/webapi/scrollPageToTop.mustache +0 -6
  269. package/docs/webapi/scrollTo.mustache +0 -12
  270. package/docs/webapi/see.mustache +0 -11
  271. package/docs/webapi/seeAttributesOnElements.mustache +0 -9
  272. package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
  273. package/docs/webapi/seeCookie.mustache +0 -8
  274. package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
  275. package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
  276. package/docs/webapi/seeElement.mustache +0 -8
  277. package/docs/webapi/seeElementInDOM.mustache +0 -8
  278. package/docs/webapi/seeInCurrentUrl.mustache +0 -8
  279. package/docs/webapi/seeInField.mustache +0 -12
  280. package/docs/webapi/seeInPopup.mustache +0 -8
  281. package/docs/webapi/seeInSource.mustache +0 -7
  282. package/docs/webapi/seeInTitle.mustache +0 -8
  283. package/docs/webapi/seeNumberOfElements.mustache +0 -11
  284. package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
  285. package/docs/webapi/seeTextEquals.mustache +0 -9
  286. package/docs/webapi/seeTitleEquals.mustache +0 -8
  287. package/docs/webapi/seeTraffic.mustache +0 -36
  288. package/docs/webapi/selectOption.mustache +0 -21
  289. package/docs/webapi/setCookie.mustache +0 -16
  290. package/docs/webapi/setGeoLocation.mustache +0 -12
  291. package/docs/webapi/startRecordingTraffic.mustache +0 -8
  292. package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
  293. package/docs/webapi/stopRecordingTraffic.mustache +0 -5
  294. package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
  295. package/docs/webapi/switchTo.mustache +0 -9
  296. package/docs/webapi/switchToNextTab.mustache +0 -10
  297. package/docs/webapi/switchToPreviousTab.mustache +0 -10
  298. package/docs/webapi/type.mustache +0 -21
  299. package/docs/webapi/uncheckOption.mustache +0 -13
  300. package/docs/webapi/wait.mustache +0 -8
  301. package/docs/webapi/waitForClickable.mustache +0 -11
  302. package/docs/webapi/waitForCookie.mustache +0 -9
  303. package/docs/webapi/waitForDetached.mustache +0 -10
  304. package/docs/webapi/waitForDisabled.mustache +0 -6
  305. package/docs/webapi/waitForElement.mustache +0 -11
  306. package/docs/webapi/waitForEnabled.mustache +0 -6
  307. package/docs/webapi/waitForFunction.mustache +0 -17
  308. package/docs/webapi/waitForInvisible.mustache +0 -10
  309. package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
  310. package/docs/webapi/waitForText.mustache +0 -13
  311. package/docs/webapi/waitForValue.mustache +0 -10
  312. package/docs/webapi/waitForVisible.mustache +0 -10
  313. package/docs/webapi/waitInUrl.mustache +0 -9
  314. package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
  315. package/docs/webapi/waitToHide.mustache +0 -10
  316. package/docs/webapi/waitUrlEquals.mustache +0 -10
  317. package/lib/helper/AI.js +0 -214
  318. package/lib/helper/Mochawesome.js +0 -96
  319. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
  320. package/lib/helper/extras/React.js +0 -65
  321. package/lib/listener/enhancedGlobalRetry.js +0 -110
  322. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  323. package/lib/plugin/htmlReporter.js +0 -3648
  324. package/lib/plugin/stepByStepReport.js +0 -427
  325. package/lib/plugin/subtitles.js +0 -89
  326. package/lib/retryCoordinator.js +0 -207
@@ -1,3 +1,7 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import * as acorn from 'acorn'
1
5
  import { getConfig, getTestRoot } from './utils.js'
2
6
  import Codecept from '../codecept.js'
3
7
  import container from '../container.js'
@@ -5,33 +9,169 @@ import { getParamsToString } from '../parser.js'
5
9
  import { methodsOfObject } from '../utils.js'
6
10
  import output from '../output.js'
7
11
 
8
- export default async function (path) {
9
- const testsPath = getTestRoot(path)
10
- const config = await getConfig(testsPath)
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+ const helperDir = path.resolve(__dirname, '..', 'helper')
14
+ const webapiDir = path.resolve(__dirname, '..', '..', 'docs', 'webapi')
15
+
16
+ let partialsCache = null
17
+
18
+ function loadWebApiPartials() {
19
+ if (partialsCache) return partialsCache
20
+ const map = new Map()
21
+ if (fs.existsSync(webapiDir)) {
22
+ for (const file of fs.readdirSync(webapiDir)) {
23
+ if (path.extname(file) !== '.mustache') continue
24
+ const name = path.basename(file, '.mustache')
25
+ map.set(name, fs.readFileSync(path.join(webapiDir, file), 'utf8'))
26
+ }
27
+ }
28
+ partialsCache = map
29
+ return map
30
+ }
31
+
32
+ function resolveHelperSource(helper, helperName, config, testsPath) {
33
+ const builtin = path.join(helperDir, `${helper.constructor.name}.js`)
34
+ if (fs.existsSync(builtin)) return builtin
35
+ const requirePath = config?.helpers?.[helperName]?.require
36
+ if (requirePath) {
37
+ const resolved = path.isAbsolute(requirePath) ? requirePath : path.resolve(testsPath, requirePath)
38
+ if (fs.existsSync(resolved)) return resolved
39
+ }
40
+ return null
41
+ }
42
+
43
+ function findClassNode(ast) {
44
+ for (const node of ast.body) {
45
+ if (node.type === 'ClassDeclaration') return node
46
+ if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'ClassDeclaration') return node.declaration
47
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration?.type === 'ClassDeclaration') return node.declaration
48
+ }
49
+ return null
50
+ }
51
+
52
+ function stripJsDoc(value) {
53
+ return value
54
+ .split('\n')
55
+ .map(line => line.replace(/^\s*\* ?/, ''))
56
+ .join('\n')
57
+ .trim()
58
+ }
59
+
60
+ function resolvePartials(text, partials) {
61
+ return text.replace(/\{\{>\s*([\w-]+)\s*\}\}/g, (match, name) => {
62
+ return partials.has(name) ? partials.get(name) : match
63
+ })
64
+ }
65
+
66
+ function extractMethodDocs(helper, helperName, config, testsPath, partials) {
67
+ const result = new Map()
68
+ const sourceFile = resolveHelperSource(helper, helperName, config, testsPath)
69
+ if (!sourceFile) return result
70
+
71
+ let source
72
+ try {
73
+ source = fs.readFileSync(sourceFile, 'utf8')
74
+ } catch {
75
+ return result
76
+ }
77
+
78
+ const comments = []
79
+ let ast
80
+ try {
81
+ ast = acorn.parse(source, {
82
+ ecmaVersion: 'latest',
83
+ sourceType: 'module',
84
+ locations: true,
85
+ onComment: comments,
86
+ })
87
+ } catch {
88
+ return result
89
+ }
90
+
91
+ const classNode = findClassNode(ast)
92
+ if (!classNode) return result
93
+
94
+ const blockComments = comments
95
+ .filter(c => c.type === 'Block' && c.value.startsWith('*'))
96
+ .sort((a, b) => a.start - b.start)
97
+
98
+ let cursor = 0
99
+ for (const member of classNode.body.body) {
100
+ if (member.type !== 'MethodDefinition') continue
101
+ if (member.kind === 'constructor' || member.static) continue
102
+ const name = member.key?.name
103
+ if (!name || name.startsWith('_')) continue
104
+
105
+ let attached = null
106
+ let attachedIdx = -1
107
+ for (let i = cursor; i < blockComments.length; i++) {
108
+ const c = blockComments[i]
109
+ if (c.end > member.start) break
110
+ attached = c
111
+ attachedIdx = i
112
+ }
113
+ if (attached) {
114
+ cursor = attachedIdx + 1
115
+ const stripped = stripJsDoc(attached.value)
116
+ const resolved = resolvePartials(stripped, partials)
117
+ result.set(name, resolved)
118
+ }
119
+ }
120
+
121
+ return result
122
+ }
123
+
124
+ function printDocBlock(doc) {
125
+ if (!doc) return
126
+ for (const line of doc.split('\n')) {
127
+ output.print(` ${line}`)
128
+ }
129
+ output.print('')
130
+ }
131
+
132
+ export default async function (path, options = {}) {
133
+ const configFile = options.config
134
+ const testsPath = getTestRoot(configFile || path)
135
+ const config = await getConfig(configFile || testsPath)
11
136
  const codecept = new Codecept(config, {})
12
137
  await codecept.init(testsPath)
13
138
  await container.started()
14
139
 
15
- output.print('List of test actions: -- ')
140
+ const filter = options.action ? options.action.replace(/^I\./, '') : null
141
+ const showDocs = !!(options.docs || filter)
142
+ const partials = showDocs ? loadWebApiPartials() : null
143
+
144
+ if (!filter) output.print('List of test actions: -- ')
16
145
  const helpers = container.helpers()
17
146
  const supportI = container.support('I')
18
147
  const actions = []
148
+ let matched = false
19
149
  for (const name in helpers) {
20
150
  const helper = helpers[name]
151
+ const docs = showDocs ? extractMethodDocs(helper, name, config, testsPath, partials) : null
21
152
  methodsOfObject(helper).forEach(action => {
22
- const params = getParamsToString(helper[action])
23
153
  actions[action] = 1
154
+ if (filter && action !== filter) return
155
+ const params = getParamsToString(helper[action])
24
156
  output.print(` ${output.colors.grey(name)} I.${output.colors.bold(action)}(${params})`)
157
+ if (docs && docs.has(action)) printDocBlock(docs.get(action))
158
+ matched = true
25
159
  })
26
160
  }
27
161
  for (const name in supportI) {
28
- if (actions[name]) {
29
- continue
30
- }
162
+ if (actions[name]) continue
163
+ if (filter && name !== filter) continue
31
164
  const actor = supportI[name]
32
165
  const params = getParamsToString(actor)
33
166
  output.print(` I.${output.colors.bold(name)}(${params})`)
167
+ matched = true
168
+ }
169
+ if (filter && !matched) {
170
+ output.print(`No action named ${output.colors.bold(filter)} found in enabled helpers or support objects.`)
171
+ return
172
+ }
173
+ if (!filter) {
174
+ output.print('PS: Actions are retrieved from enabled helpers. ')
175
+ output.print('Implement custom actions in your helper classes.')
34
176
  }
35
- output.print('PS: Actions are retrieved from enabled helpers. ')
36
- output.print('Implement custom actions in your helper classes.')
37
177
  }
@@ -0,0 +1,218 @@
1
+ import fs from 'fs'
2
+ import * as parse5 from 'parse5'
3
+ import { DOMImplementation, XMLSerializer } from '@xmldom/xmldom'
4
+ import xpath from 'xpath'
5
+ import Locator from '../locator.js'
6
+ import { xpathLocator } from '../utils.js'
7
+
8
+ export default async function query(locator, context, options = {}) {
9
+ const html = options.file ? fs.readFileSync(options.file, 'utf8') : await readStdin()
10
+
11
+ if (!html || !html.trim()) {
12
+ console.error('codeceptq: no HTML input. Pipe HTML via stdin or use --file <path>.')
13
+ process.exitCode = 2
14
+ return
15
+ }
16
+
17
+ let xpathExpr
18
+ let contextExpr = null
19
+ try {
20
+ xpathExpr = buildXPath(locator, options)
21
+ if (context) contextExpr = buildXPath(context, {})
22
+ } catch (err) {
23
+ console.error(`codeceptq: cannot build XPath: ${err.message}`)
24
+ process.exitCode = 2
25
+ return
26
+ }
27
+
28
+ const { doc, source } = htmlToDoc(html)
29
+
30
+ let nodes
31
+ try {
32
+ if (contextExpr) {
33
+ const ctxNodes = toArray(xpath.select(contextExpr, doc))
34
+ const seen = new Set()
35
+ nodes = []
36
+ for (const ctx of ctxNodes) {
37
+ for (const m of toArray(xpath.select(xpathExpr, ctx))) {
38
+ if (!seen.has(m)) {
39
+ seen.add(m)
40
+ nodes.push(m)
41
+ }
42
+ }
43
+ }
44
+ } else {
45
+ nodes = toArray(xpath.select(xpathExpr, doc))
46
+ }
47
+ } catch (err) {
48
+ console.error(`codeceptq: XPath evaluation failed for "${xpathExpr}": ${err.message}`)
49
+ process.exitCode = 2
50
+ return
51
+ }
52
+
53
+ const limit = parseInt(options.limit, 10) || 20
54
+ const snippetLen = parseInt(options.snippet, 10) || 500
55
+ const truncated = nodes.slice(0, limit)
56
+ const where = options.file || 'stdin'
57
+
58
+ if (options.json) {
59
+ process.stdout.write(
60
+ JSON.stringify(
61
+ {
62
+ locator,
63
+ context: context || null,
64
+ xpath: xpathExpr,
65
+ contextXPath: contextExpr,
66
+ source: where,
67
+ total: nodes.length,
68
+ shown: truncated.length,
69
+ matches: truncated.map(n => ({
70
+ line: n.__line ?? null,
71
+ snippet: renderSnippet(n, source, snippetLen, options.full),
72
+ })),
73
+ },
74
+ null,
75
+ 2,
76
+ ) + '\n',
77
+ )
78
+ } else {
79
+ if (nodes.length === 0) {
80
+ console.log(`No matches for ${quote(locator)}${context ? ` within ${quote(context)}` : ''} in ${where}`)
81
+ console.log(`(xpath: ${xpathExpr})`)
82
+ } else {
83
+ const noun = nodes.length === 1 ? 'match' : 'matches'
84
+ const more = nodes.length > truncated.length ? ` (showing first ${truncated.length})` : ''
85
+ console.log(`${nodes.length} ${noun} for ${quote(locator)}${context ? ` within ${quote(context)}` : ''} in ${where}${more}`)
86
+ console.log()
87
+ truncated.forEach((node, i) => {
88
+ const line = node.__line ?? '?'
89
+ console.log(`${i + 1}. Line ${line}`)
90
+ const snippet = renderSnippet(node, source, snippetLen, options.full)
91
+ snippet.split('\n').forEach(l => console.log(' ' + l))
92
+ console.log()
93
+ })
94
+ }
95
+ }
96
+
97
+ if (nodes.length === 0) process.exitCode = 1
98
+ }
99
+
100
+ function buildXPath(input, options) {
101
+ const literal = xpathLocator.literal(input)
102
+ if (options.field) return Locator.field.byText(literal)
103
+ if (options.click || options.clickable) return Locator.clickable.wide(literal)
104
+ if (options.checkable) return Locator.checkable.byText(literal)
105
+ if (options.select) {
106
+ return Locator.select.byVisibleText(literal).replace(/\.\/(option|optgroup)/g, './/$1')
107
+ }
108
+
109
+ if (options.xpath) return new Locator({ xpath: input }).toXPath()
110
+ if (options.css) return new Locator({ css: input }).toXPath()
111
+
112
+ const loc = new Locator(input)
113
+ if (loc.type === 'fuzzy') {
114
+ return xpathLocator.combine([Locator.clickable.wide(literal), Locator.field.byText(literal)])
115
+ }
116
+ return loc.toXPath()
117
+ }
118
+
119
+ function htmlToDoc(html) {
120
+ const p5doc = parse5.parse(html, { sourceCodeLocationInfo: true })
121
+ const impl = new DOMImplementation()
122
+ const doc = impl.createDocument(null, null, null)
123
+ walkParse5(p5doc, doc, doc)
124
+ return { doc, source: html }
125
+ }
126
+
127
+ function walkParse5(p5node, xmlParent, xmlDoc) {
128
+ for (const child of p5node.childNodes || []) {
129
+ const name = child.nodeName
130
+ if (name === '#text') {
131
+ if (child.value != null) {
132
+ const t = xmlDoc.createTextNode(child.value)
133
+ if (child.sourceCodeLocation) t.__line = child.sourceCodeLocation.startLine
134
+ xmlParent.appendChild(t)
135
+ }
136
+ } else if (name === '#comment') {
137
+ try {
138
+ xmlParent.appendChild(xmlDoc.createComment(child.data || ''))
139
+ } catch {
140
+ // ignore comments xmldom rejects
141
+ }
142
+ } else if (name === '#documentType') {
143
+ // skip doctype
144
+ } else {
145
+ const tagName = child.tagName || name
146
+ let el
147
+ try {
148
+ el = xmlDoc.createElement(tagName)
149
+ } catch {
150
+ continue
151
+ }
152
+ for (const attr of child.attrs || []) {
153
+ try {
154
+ el.setAttribute(attr.name, attr.value)
155
+ } catch {
156
+ // ignore attrs xmldom rejects (namespaces, invalid names)
157
+ }
158
+ }
159
+ const loc = child.sourceCodeLocation
160
+ if (loc) {
161
+ el.__line = loc.startLine
162
+ el.__startOffset = loc.startOffset
163
+ el.__endOffset = loc.endOffset
164
+ el.__startTagEndOffset = loc.startTag ? loc.startTag.endOffset : loc.endOffset
165
+ }
166
+ xmlParent.appendChild(el)
167
+ walkParse5(child, el, xmlDoc)
168
+ }
169
+ }
170
+ }
171
+
172
+ function renderSnippet(node, source, snippetLen, full) {
173
+ if (typeof node.__startOffset !== 'number') {
174
+ try {
175
+ return new XMLSerializer().serializeToString(node)
176
+ } catch {
177
+ return `<${node.nodeName || '?'}>`
178
+ }
179
+ }
180
+ const start = node.__startOffset
181
+ const end = node.__endOffset ?? start
182
+ if (full) return source.slice(start, end)
183
+
184
+ const tagEnd = node.__startTagEndOffset ?? end
185
+ const openingTag = source.slice(start, tagEnd)
186
+ if (end <= tagEnd) return openingTag
187
+
188
+ const totalLen = end - start
189
+ if (totalLen <= snippetLen) return source.slice(start, end)
190
+
191
+ const remaining = Math.max(0, snippetLen - openingTag.length)
192
+ if (remaining < 20) return openingTag + ' …'
193
+ return openingTag + source.slice(tagEnd, tagEnd + remaining) + ' …'
194
+ }
195
+
196
+ function readStdin() {
197
+ return new Promise((resolve, reject) => {
198
+ if (process.stdin.isTTY) {
199
+ resolve('')
200
+ return
201
+ }
202
+ let data = ''
203
+ process.stdin.setEncoding('utf8')
204
+ process.stdin.on('data', chunk => (data += chunk))
205
+ process.stdin.on('end', () => resolve(data))
206
+ process.stdin.on('error', reject)
207
+ })
208
+ }
209
+
210
+ function toArray(v) {
211
+ if (Array.isArray(v)) return v
212
+ if (v == null || v === '' || typeof v === 'boolean' || typeof v === 'number') return []
213
+ return [v]
214
+ }
215
+
216
+ function quote(s) {
217
+ return `'${String(s).replace(/'/g, "\\'")}'`
218
+ }
@@ -8,6 +8,7 @@ import event from '../event.js'
8
8
  import { createRuns } from './run-multiple/collection.js'
9
9
  import { clearString, replaceValueDeep } from '../utils.js'
10
10
  import { getConfig, getTestRoot, fail } from './utils.js'
11
+ import store from '../store.js'
11
12
 
12
13
  const __filename = fileURLToPath(import.meta.url)
13
14
  const __dirname = path.dirname(__filename)
@@ -35,6 +36,7 @@ export default async function (selectedRuns, options) {
35
36
  const configFile = options.config
36
37
 
37
38
  const testRoot = getTestRoot(configFile)
39
+ store.codeceptDir = testRoot
38
40
  global.codecept_dir = testRoot
39
41
 
40
42
  // copy opts to run
@@ -136,7 +138,7 @@ function executeRun(runName, runConfig) {
136
138
 
137
139
  outputDir = clearString(outputDir)
138
140
 
139
- // tweaking default output directories and for mochawesome
141
+ // tweaking default output directories
140
142
  overriddenConfig = replaceValueDeep(overriddenConfig, 'output', path.join(config.output, outputDir))
141
143
  overriddenConfig = replaceValueDeep(overriddenConfig, 'reportDir', path.join(config.output, outputDir))
142
144
  overriddenConfig = replaceValueDeep(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, `${browserName}_report.xml`))
@@ -41,6 +41,8 @@ export default async function (workerCount, selectedRuns, options) {
41
41
  output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
42
42
  output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
43
43
  store.hasWorkers = true
44
+ store.workerMode = true
45
+ process.env.RUNS_WITH_WORKERS = 'true'
44
46
 
45
47
  const workers = new Workers(numberOfWorkers, config)
46
48
  workers.overrideConfig(overrideConfigs)
@@ -85,19 +87,5 @@ export default async function (workerCount, selectedRuns, options) {
85
87
  process.exitCode = 1
86
88
  } finally {
87
89
  await workers.teardownAll()
88
-
89
- // Force exit if event loop doesn't clear naturally
90
- // This is needed because worker threads may leave handles open
91
- // even after proper cleanup, preventing natural process termination
92
- if (!options.noExit) {
93
- // Use beforeExit to ensure we run after all other exit handlers
94
- // have set the correct exit code
95
- process.once('beforeExit', (code) => {
96
- // Give cleanup a moment to complete, then force exit with the correct code
97
- setTimeout(() => {
98
- process.exit(code || process.exitCode || 0)
99
- }, 100)
100
- })
101
- }
102
90
  }
103
91
  }
@@ -1,8 +1,7 @@
1
- import { getConfig, printError, getTestRoot, createOutputDir } from './utils.js'
1
+ import { getConfig, printError, getTestRoot, createOutputDir, autoExit } from './utils.js'
2
2
  import Config from '../config.js'
3
3
  import store from '../store.js'
4
4
  import Codecept from '../codecept.js'
5
- import container from '../container.js'
6
5
 
7
6
  export default async function (test, options) {
8
7
  // registering options globally to use in config
@@ -32,7 +31,7 @@ export default async function (test, options) {
32
31
  codecept.loadTests(test)
33
32
 
34
33
  if (options.verbose) {
35
- global.debugMode = true
34
+ store.debugMode = true
36
35
  const { getMachineInfo } = await import('./info.js')
37
36
  await getMachineInfo()
38
37
  }
@@ -43,19 +42,6 @@ export default async function (test, options) {
43
42
  process.exitCode = 1
44
43
  } finally {
45
44
  await codecept.teardown()
46
-
47
- // Schedule a delayed exit to prevent process hanging due to browser helper event loops
48
- // Only needed for Playwright/Puppeteer which keep the event loop alive
49
- // Wait 1 second to allow final cleanup and output to complete
50
- if (!process.env.CODECEPT_DISABLE_AUTO_EXIT) {
51
- const helpers = container.helpers()
52
- const hasBrowserHelper = helpers && (helpers.Playwright || helpers.Puppeteer || helpers.WebDriver)
53
-
54
- if (hasBrowserHelper) {
55
- setTimeout(() => {
56
- process.exit(process.exitCode || 0)
57
- }, 1000).unref()
58
- }
59
- }
45
+ await autoExit()
60
46
  }
61
47
  }
@@ -107,6 +107,20 @@ export const createOutputDir = (config, testRoot) => {
107
107
  }
108
108
  }
109
109
 
110
+ export async function autoExit() {
111
+ const timeout = parseInt(process.env.CODECEPT_AUTO_EXIT_TIMEOUT, 10)
112
+ if (timeout === 0) return
113
+ const exitTimeout = timeout || 2000
114
+
115
+ const { default: container } = await import('../container.js')
116
+ const helpers = container.helpers()
117
+ if (!helpers || !Object.values(helpers).some(h => typeof h._cleanup === 'function')) return
118
+
119
+ const { default: recorder } = await import('../recorder.js')
120
+ await Promise.race([recorder.promise(), new Promise(resolve => setTimeout(resolve, exitTimeout))])
121
+ process.exit(process.exitCode || 0)
122
+ }
123
+
110
124
  export const findConfigFile = testsPath => {
111
125
  const extensions = ['js', 'ts']
112
126
  for (const ext of extensions) {