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.
- package/README.md +39 -27
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1187 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -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 +230 -0
- package/docs/continuous-integration.md +497 -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 +136 -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/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -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/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +26 -23
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -0
- package/lib/command/run.js +1 -1
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -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 +4 -3
- 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 +228 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- 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 +126 -3
- package/lib/mocha/cli.js +14 -2
- package/lib/mocha/factory.js +7 -2
- 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 +453 -0
- package/lib/plugin/analyze.js +1 -1
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +53 -49
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- 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.js +77 -3
- package/lib/workers.js +52 -22
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- 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/dontSeeCurrentPathEquals.mustache +0 -10
- 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/seeCurrentPathEquals.mustache +0 -10
- 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/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/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
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
|
|
@@ -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)
|
package/lib/command/run.js
CHANGED
|
@@ -21,8 +21,8 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData
|
|
|
21
21
|
|
|
22
22
|
// Global error handlers to catch critical errors but not test failures
|
|
23
23
|
process.on('uncaughtException', (err) => {
|
|
24
|
-
if (
|
|
25
|
-
const fileMapping =
|
|
24
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
25
|
+
const fileMapping = container.tsFileMapping()
|
|
26
26
|
if (fileMapping) {
|
|
27
27
|
fixErrorStack(err, fileMapping)
|
|
28
28
|
}
|
|
@@ -40,8 +40,8 @@ process.on('uncaughtException', (err) => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
process.on('unhandledRejection', (reason, promise) => {
|
|
43
|
-
if (reason && typeof reason === 'object' && reason.stack &&
|
|
44
|
-
const fileMapping =
|
|
43
|
+
if (reason && typeof reason === 'object' && reason.stack && container?.tsFileMapping && fixErrorStack) {
|
|
44
|
+
const fileMapping = container.tsFileMapping()
|
|
45
45
|
if (fileMapping) {
|
|
46
46
|
fixErrorStack(reason, fileMapping)
|
|
47
47
|
}
|
|
@@ -163,8 +163,8 @@ initPromise = (async function () {
|
|
|
163
163
|
// IMPORTANT: await is required here since getConfig is async
|
|
164
164
|
baseConfig = await getConfig(options.config || testRoot)
|
|
165
165
|
} catch (configErr) {
|
|
166
|
-
if (
|
|
167
|
-
const fileMapping =
|
|
166
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
167
|
+
const fileMapping = container.tsFileMapping()
|
|
168
168
|
if (fileMapping) {
|
|
169
169
|
fixErrorStack(configErr, fileMapping)
|
|
170
170
|
}
|
|
@@ -185,8 +185,8 @@ initPromise = (async function () {
|
|
|
185
185
|
try {
|
|
186
186
|
await codecept.init(testRoot)
|
|
187
187
|
} catch (initErr) {
|
|
188
|
-
if (
|
|
189
|
-
const fileMapping =
|
|
188
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
189
|
+
const fileMapping = container.tsFileMapping()
|
|
190
190
|
if (fileMapping) {
|
|
191
191
|
fixErrorStack(initErr, fileMapping)
|
|
192
192
|
}
|
|
@@ -218,8 +218,8 @@ initPromise = (async function () {
|
|
|
218
218
|
parentPort?.close()
|
|
219
219
|
}
|
|
220
220
|
} catch (err) {
|
|
221
|
-
if (
|
|
222
|
-
const fileMapping =
|
|
221
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
222
|
+
const fileMapping = container.tsFileMapping()
|
|
223
223
|
if (fileMapping) {
|
|
224
224
|
fixErrorStack(err, fileMapping)
|
|
225
225
|
}
|
package/lib/config.js
CHANGED
|
@@ -15,8 +15,9 @@ const defaultConfig = {
|
|
|
15
15
|
hooks: [],
|
|
16
16
|
gherkin: {},
|
|
17
17
|
plugins: {
|
|
18
|
-
|
|
19
|
-
enabled: true,
|
|
18
|
+
screenshot: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
on: 'fail',
|
|
20
21
|
},
|
|
21
22
|
},
|
|
22
23
|
stepTimeout: 0,
|
|
@@ -32,9 +33,27 @@ const defaultConfig = {
|
|
|
32
33
|
],
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Array<{ fn: (cfg) => void, ran: boolean, error?: Error }>
|
|
35
37
|
let hooks = []
|
|
36
38
|
let config = {}
|
|
37
39
|
|
|
40
|
+
// Apply a single hook against `cfg`, swallowing errors so one broken hook
|
|
41
|
+
// can't take down the whole run. The failure is logged through the
|
|
42
|
+
// framework's own output module (when available) so it shows up in test
|
|
43
|
+
// reports; the hook is still marked ran so it doesn't get retried.
|
|
44
|
+
function applyHook(hook, cfg) {
|
|
45
|
+
try {
|
|
46
|
+
hook.fn(cfg)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
hook.error = err
|
|
49
|
+
const out = globalThis.codeceptjs?.output
|
|
50
|
+
if (out && typeof out.error === 'function') out.error(`config hook failed: ${err.message}`)
|
|
51
|
+
else console.error('config hook failed:', err)
|
|
52
|
+
} finally {
|
|
53
|
+
hook.ran = true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.cjs', 'codecept.conf.cjs', 'codecept.config.ts', 'codecept.conf.ts']
|
|
39
58
|
|
|
40
59
|
/**
|
|
@@ -49,7 +68,11 @@ class Config {
|
|
|
49
68
|
*/
|
|
50
69
|
static create(newConfig) {
|
|
51
70
|
config = deepMerge(deepClone(defaultConfig), newConfig)
|
|
52
|
-
hooks
|
|
71
|
+
// Re-apply every hook against the freshly built config; hooks added later
|
|
72
|
+
// (e.g. from plugin boot) stay pending until runPendingHooks. Array
|
|
73
|
+
// iterators re-check length on each step, so hooks pushed during a hook
|
|
74
|
+
// execution are visited in this same pass.
|
|
75
|
+
for (const hook of hooks) applyHook(hook, config)
|
|
53
76
|
return config
|
|
54
77
|
}
|
|
55
78
|
|
|
@@ -121,7 +144,48 @@ class Config {
|
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
static addHook(fn) {
|
|
124
|
-
hooks.push(fn)
|
|
147
|
+
hooks.push({ fn, ran: false })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Run every hook that hasn't been applied to the current config yet.
|
|
152
|
+
* Hooks added after `Config.create()` (e.g. from plugin boot code) stay
|
|
153
|
+
* pending until this is called; once it runs, they're marked applied so
|
|
154
|
+
* subsequent calls are no-ops. Hooks added while pending hooks are running
|
|
155
|
+
* are picked up in the same pass (the array iterator re-checks length).
|
|
156
|
+
*
|
|
157
|
+
* Failures are logged through `output.error` and don't abort the loop —
|
|
158
|
+
* a broken hook can't poison the run, but its error is visible.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object<string, *>} [cfg] target config (defaults to the live singleton)
|
|
161
|
+
* @return {boolean} true if any hook ran
|
|
162
|
+
*/
|
|
163
|
+
static runPendingHooks(cfg = config) {
|
|
164
|
+
let ran = false
|
|
165
|
+
for (const hook of hooks) {
|
|
166
|
+
if (hook.ran) continue
|
|
167
|
+
applyHook(hook, cfg)
|
|
168
|
+
ran = true
|
|
169
|
+
}
|
|
170
|
+
return ran
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Number of registered config hooks. Useful for snapshotting before a phase
|
|
175
|
+
* (e.g. plugin loading) and re-running only the hooks added during it.
|
|
176
|
+
* @return {number}
|
|
177
|
+
*/
|
|
178
|
+
static hooksCount() {
|
|
179
|
+
return hooks.length
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run hooks in `[fromIndex, end)` against the given config object, mutating it.
|
|
184
|
+
* @param {number} fromIndex
|
|
185
|
+
* @param {Object<string, *>} cfg
|
|
186
|
+
*/
|
|
187
|
+
static runHooksFrom(fromIndex, cfg) {
|
|
188
|
+
for (let i = fromIndex; i < hooks.length; i++) hooks[i](cfg)
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
/**
|
|
@@ -150,6 +214,15 @@ async function loadConfigFile(configFile) {
|
|
|
150
214
|
const require = createRequire(import.meta.url)
|
|
151
215
|
const extensionName = path.extname(configFile)
|
|
152
216
|
|
|
217
|
+
// Populate the in-process registry that packages like @codeceptjs/configure
|
|
218
|
+
// look up at config-import time (their proxies throw if `globalThis.codeceptjs`
|
|
219
|
+
// is missing). initCodeceptGlobals sets this too, but only later during
|
|
220
|
+
// bootstrap — config files are imported here first.
|
|
221
|
+
if (!globalThis.codeceptjs) {
|
|
222
|
+
const indexModule = await import('./index.js')
|
|
223
|
+
globalThis.codeceptjs = indexModule.default || indexModule
|
|
224
|
+
}
|
|
225
|
+
|
|
153
226
|
// .conf.js config file
|
|
154
227
|
if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
|
|
155
228
|
let configModule
|