codeceptjs 4.0.2-beta.8 → 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.
- package/README.md +39 -28
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +745 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +195 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +34 -25
- package/lib/command/check.js +2 -1
- package/lib/command/definitions.js +6 -7
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +3 -1
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +249 -270
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -1
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +91 -37
- package/lib/config.js +96 -18
- package/lib/container.js +115 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +358 -467
- package/lib/helper/Puppeteer.js +335 -192
- package/lib/helper/WebDriver.js +324 -111
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +7 -107
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +158 -16
- package/lib/mocha/cli.js +19 -1
- package/lib/mocha/factory.js +11 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +457 -0
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +5 -4
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +43 -32
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +15 -2
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/loaderCheck.js +28 -0
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils/typescript.js +188 -23
- package/lib/utils.js +77 -3
- package/lib/workers.js +65 -40
- package/package.json +35 -30
- package/typings/index.d.ts +119 -8
- package/typings/promiseBasedTypes.d.ts +3158 -6065
- package/typings/types.d.ts +3453 -6494
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
package/lib/command/list.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/lib/command/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/lib/command/utils.js
CHANGED
|
@@ -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) {
|