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/container.js
CHANGED
|
@@ -15,9 +15,15 @@ import store from './store.js'
|
|
|
15
15
|
import Result from './result.js'
|
|
16
16
|
import ai from './ai.js'
|
|
17
17
|
import actorFactory from './actor.js'
|
|
18
|
+
import Config from './config.js'
|
|
18
19
|
|
|
19
20
|
let asyncHelperPromise
|
|
20
21
|
|
|
22
|
+
let beforeCalledSet = new Set()
|
|
23
|
+
|
|
24
|
+
export function getBeforeCalledSet() { return beforeCalledSet }
|
|
25
|
+
export function resetBeforeCalledSet() { beforeCalledSet = new Set() }
|
|
26
|
+
|
|
21
27
|
let container = {
|
|
22
28
|
helpers: {},
|
|
23
29
|
support: {},
|
|
@@ -116,6 +122,18 @@ class Container {
|
|
|
116
122
|
// Wait for all async helpers to finish loading and populate the actor
|
|
117
123
|
await asyncHelperPromise
|
|
118
124
|
|
|
125
|
+
// Plugins may have registered Config hooks during their boot. Run anything
|
|
126
|
+
// that hasn't been applied yet and re-feed the mutated helper config to the
|
|
127
|
+
// already-instantiated helpers.
|
|
128
|
+
if (Config.runPendingHooks(config)) {
|
|
129
|
+
for (const name of Object.keys(container.helpers)) {
|
|
130
|
+
const helper = container.helpers[name]
|
|
131
|
+
if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {
|
|
132
|
+
helper._setConfig(config.helpers[name])
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
|
|
120
138
|
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
|
|
121
139
|
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
|
|
@@ -150,10 +168,23 @@ class Container {
|
|
|
150
168
|
if (!name) {
|
|
151
169
|
return container.proxySupport
|
|
152
170
|
}
|
|
153
|
-
|
|
171
|
+
if (typeof container.support[name] === 'function') {
|
|
172
|
+
return container.support[name]
|
|
173
|
+
}
|
|
154
174
|
return container.proxySupport[name]
|
|
155
175
|
}
|
|
156
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Get raw (non-proxied) support objects for direct access.
|
|
179
|
+
* Used by listeners to call lifecycle hooks without MetaStep wrapping.
|
|
180
|
+
*
|
|
181
|
+
* @api
|
|
182
|
+
* @returns {object}
|
|
183
|
+
*/
|
|
184
|
+
static supportObjects() {
|
|
185
|
+
return container.support
|
|
186
|
+
}
|
|
187
|
+
|
|
157
188
|
/**
|
|
158
189
|
* Get all helpers or get a helper by name
|
|
159
190
|
*
|
|
@@ -183,7 +214,7 @@ class Container {
|
|
|
183
214
|
* @api
|
|
184
215
|
*/
|
|
185
216
|
static tsFileMapping() {
|
|
186
|
-
return
|
|
217
|
+
return store.tsFileMapping
|
|
187
218
|
}
|
|
188
219
|
|
|
189
220
|
/**
|
|
@@ -426,11 +457,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
426
457
|
tempJsFile = allTempFiles
|
|
427
458
|
fileMapping = mapping
|
|
428
459
|
// Store file mapping in container for runtime error fixing (merge with existing)
|
|
429
|
-
if (!
|
|
430
|
-
|
|
460
|
+
if (!store.tsFileMapping) {
|
|
461
|
+
store.tsFileMapping = new Map()
|
|
431
462
|
}
|
|
432
463
|
for (const [key, value] of mapping.entries()) {
|
|
433
|
-
|
|
464
|
+
store.tsFileMapping.set(key, value)
|
|
434
465
|
}
|
|
435
466
|
} catch (tsError) {
|
|
436
467
|
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
@@ -542,6 +573,19 @@ function createSupportObjects(config) {
|
|
|
542
573
|
let currentValue = currentObject[prop]
|
|
543
574
|
|
|
544
575
|
if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
|
|
576
|
+
if (prop.toString().charAt(0) !== '_' && currentObject._before && !beforeCalledSet.has(name)) {
|
|
577
|
+
beforeCalledSet.add(name)
|
|
578
|
+
const originalValue = currentValue
|
|
579
|
+
const wrappedValue = async function (...args) {
|
|
580
|
+
await currentObject._before()
|
|
581
|
+
return originalValue.apply(currentObject, args)
|
|
582
|
+
}
|
|
583
|
+
const ms = new MetaStep(name, prop)
|
|
584
|
+
ms.setContext(currentObject)
|
|
585
|
+
debug(`metastep is created for ${name}.${prop.toString()}() (with _before)`)
|
|
586
|
+
return ms.run.bind(ms, asyncWrapper(wrappedValue))
|
|
587
|
+
}
|
|
588
|
+
|
|
545
589
|
const ms = new MetaStep(name, prop)
|
|
546
590
|
ms.setContext(currentObject)
|
|
547
591
|
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
|
|
@@ -600,6 +644,8 @@ function createSupportObjects(config) {
|
|
|
600
644
|
let value
|
|
601
645
|
if (container.sharedKeys.has(prop) && prop in container.support) {
|
|
602
646
|
value = container.support[prop]
|
|
647
|
+
} else if (prop in container.support && typeof container.support[prop] === 'function') {
|
|
648
|
+
value = container.support[prop]
|
|
603
649
|
} else {
|
|
604
650
|
value = lazyLoad(prop)
|
|
605
651
|
}
|
|
@@ -614,6 +660,9 @@ function createSupportObjects(config) {
|
|
|
614
660
|
if (container.sharedKeys.has(key) && key in container.support) {
|
|
615
661
|
return container.support[key]
|
|
616
662
|
}
|
|
663
|
+
if (key in container.support && typeof container.support[key] === 'function') {
|
|
664
|
+
return container.support[key]
|
|
665
|
+
}
|
|
617
666
|
return lazyLoad(key)
|
|
618
667
|
},
|
|
619
668
|
},
|
|
@@ -654,26 +703,55 @@ async function loadPluginFallback(modulePath, config) {
|
|
|
654
703
|
async function createPlugins(config, options = {}) {
|
|
655
704
|
const plugins = {}
|
|
656
705
|
|
|
657
|
-
const
|
|
706
|
+
const pluginOptionMap = new Map()
|
|
707
|
+
for (const token of (options.plugins || '').split(',').filter(Boolean)) {
|
|
708
|
+
const parts = token.split(':')
|
|
709
|
+
pluginOptionMap.set(parts[0], parts.slice(1))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
for (const [name] of pluginOptionMap) {
|
|
713
|
+
if (!config[name]) config[name] = {}
|
|
714
|
+
}
|
|
715
|
+
|
|
658
716
|
for (const pluginName in config) {
|
|
659
717
|
if (!config[pluginName]) config[pluginName] = {}
|
|
660
|
-
|
|
718
|
+
const pluginConfig = config[pluginName]
|
|
719
|
+
const enabledByCli = pluginOptionMap.has(pluginName)
|
|
720
|
+
if (!pluginConfig.enabled && !enabledByCli) {
|
|
661
721
|
continue // plugin is disabled
|
|
662
722
|
}
|
|
723
|
+
|
|
724
|
+
if (enabledByCli && pluginOptionMap.get(pluginName).length > 0) {
|
|
725
|
+
pluginConfig._args = pluginOptionMap.get(pluginName)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Generic workers gate:
|
|
729
|
+
// - runInWorker / runInWorkers controls plugin execution inside worker threads.
|
|
730
|
+
// - runInParent / runInMain can disable plugin in workers parent process.
|
|
731
|
+
const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true)
|
|
732
|
+
const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true
|
|
733
|
+
|
|
734
|
+
if (options.child && !runInWorker) {
|
|
735
|
+
continue
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!options.child && store.workerMode && !runInParent) {
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
663
741
|
let module
|
|
664
742
|
try {
|
|
665
|
-
if (
|
|
666
|
-
module =
|
|
743
|
+
if (pluginConfig.require) {
|
|
744
|
+
module = pluginConfig.require
|
|
667
745
|
if (module.startsWith('.')) {
|
|
668
746
|
// local
|
|
669
|
-
module = path.resolve(
|
|
747
|
+
module = path.resolve(store.codeceptDir, module) // custom plugin
|
|
670
748
|
}
|
|
671
749
|
} else {
|
|
672
750
|
module = `./plugin/${pluginName}.js`
|
|
673
751
|
}
|
|
674
752
|
|
|
675
753
|
// Use async loading for all plugins (ESM and CJS)
|
|
676
|
-
plugins[pluginName] = await loadPluginAsync(module,
|
|
754
|
+
plugins[pluginName] = await loadPluginAsync(module, pluginConfig)
|
|
677
755
|
debug(`plugin ${pluginName} loaded via async import`)
|
|
678
756
|
} catch (err) {
|
|
679
757
|
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
|
|
@@ -683,12 +761,24 @@ async function createPlugins(config, options = {}) {
|
|
|
683
761
|
}
|
|
684
762
|
|
|
685
763
|
async function loadGherkinStepsAsync(paths) {
|
|
764
|
+
// Import BDD module to access step file tracking functions and step DSL
|
|
765
|
+
const bddModule = await import('./mocha/bdd.js')
|
|
766
|
+
|
|
686
767
|
global.Before = fn => event.dispatcher.on(event.test.started, fn)
|
|
687
768
|
global.After = fn => event.dispatcher.on(event.test.finished, fn)
|
|
688
769
|
global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
|
|
689
770
|
|
|
690
|
-
//
|
|
691
|
-
|
|
771
|
+
// Scope-inject Given/When/Then/And while loading step files so they work
|
|
772
|
+
// with noGlobals: true. When noGlobals: false, globals.js has already set
|
|
773
|
+
// them as permanent globals — skip to avoid deleting them at the end.
|
|
774
|
+
const injectStepDsl = !!store.noGlobals
|
|
775
|
+
if (injectStepDsl) {
|
|
776
|
+
global.Given = bddModule.Given
|
|
777
|
+
global.When = bddModule.When
|
|
778
|
+
global.Then = bddModule.Then
|
|
779
|
+
global.And = bddModule.And
|
|
780
|
+
global.DefineParameterType = bddModule.defineParameterType
|
|
781
|
+
}
|
|
692
782
|
|
|
693
783
|
// If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
|
|
694
784
|
// If gherkin.steps is Array, it will go the old way
|
|
@@ -701,7 +791,7 @@ async function loadGherkinStepsAsync(paths) {
|
|
|
701
791
|
bddModule.clearCurrentStepFile()
|
|
702
792
|
}
|
|
703
793
|
} else {
|
|
704
|
-
const folderPath = paths.startsWith('.') ? normalizeAndJoin(
|
|
794
|
+
const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : ''
|
|
705
795
|
if (folderPath !== '') {
|
|
706
796
|
const files = globSync(folderPath)
|
|
707
797
|
for (const file of files) {
|
|
@@ -716,6 +806,13 @@ async function loadGherkinStepsAsync(paths) {
|
|
|
716
806
|
delete global.Before
|
|
717
807
|
delete global.After
|
|
718
808
|
delete global.Fail
|
|
809
|
+
if (injectStepDsl) {
|
|
810
|
+
delete global.Given
|
|
811
|
+
delete global.When
|
|
812
|
+
delete global.Then
|
|
813
|
+
delete global.And
|
|
814
|
+
delete global.DefineParameterType
|
|
815
|
+
}
|
|
719
816
|
}
|
|
720
817
|
|
|
721
818
|
function loadGherkinSteps(paths) {
|
|
@@ -749,7 +846,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
749
846
|
}
|
|
750
847
|
}
|
|
751
848
|
if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
|
|
752
|
-
modulePath = path.join(
|
|
849
|
+
modulePath = path.join(store.codeceptDir, modulePath)
|
|
753
850
|
}
|
|
754
851
|
try {
|
|
755
852
|
// Use dynamic import for both ESM and CJS modules
|
|
@@ -873,7 +970,7 @@ async function loadTranslation(locale, vocabularies) {
|
|
|
873
970
|
const langs = await Translation.getLangs()
|
|
874
971
|
if (langs[locale]) {
|
|
875
972
|
translation = new Translation(langs[locale])
|
|
876
|
-
} else if (fileExists(path.join(
|
|
973
|
+
} else if (fileExists(path.join(store.codeceptDir, locale))) {
|
|
877
974
|
// get from a provided file instead
|
|
878
975
|
translation = Translation.createDefault()
|
|
879
976
|
translation.loadVocabulary(locale)
|
|
@@ -890,7 +987,7 @@ function getHelperModuleName(helperName, config) {
|
|
|
890
987
|
// classical require
|
|
891
988
|
if (config[helperName].require) {
|
|
892
989
|
if (config[helperName].require.startsWith('.')) {
|
|
893
|
-
let helperPath = path.resolve(
|
|
990
|
+
let helperPath = path.resolve(store.codeceptDir, config[helperName].require)
|
|
894
991
|
// Add .js extension if not present for ESM compatibility
|
|
895
992
|
if (!path.extname(helperPath)) {
|
|
896
993
|
helperPath += '.js'
|
package/lib/effects.js
CHANGED
|
@@ -4,6 +4,7 @@ import store from './store.js'
|
|
|
4
4
|
import event from './event.js'
|
|
5
5
|
import container from './container.js'
|
|
6
6
|
import MetaStep from './step/meta.js'
|
|
7
|
+
import { empty } from './assert/empty.js'
|
|
7
8
|
import { isAsyncFunction } from './utils.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -111,6 +112,11 @@ class WithinStep extends MetaStep {
|
|
|
111
112
|
*
|
|
112
113
|
* @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
|
|
113
114
|
*/
|
|
115
|
+
let hopeThatFailures = []
|
|
116
|
+
event.dispatcher.on(event.test.before, () => {
|
|
117
|
+
hopeThatFailures = []
|
|
118
|
+
})
|
|
119
|
+
|
|
114
120
|
async function hopeThat(callback) {
|
|
115
121
|
if (store.dryRun) return
|
|
116
122
|
const sessionName = 'hopeThat'
|
|
@@ -131,6 +137,7 @@ async function hopeThat(callback) {
|
|
|
131
137
|
result = false
|
|
132
138
|
const msg = err.inspect ? err.inspect() : err.toString()
|
|
133
139
|
output.debug(`Unsuccessful assertion > ${msg}`)
|
|
140
|
+
hopeThatFailures.push(msg)
|
|
134
141
|
event.dispatcher.once(event.test.finished, test => {
|
|
135
142
|
if (!test.notes) test.notes = []
|
|
136
143
|
test.notes.push({ type: 'conditionalError', text: msg })
|
|
@@ -153,6 +160,16 @@ async function hopeThat(callback) {
|
|
|
153
160
|
)
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Asserts that no `hopeThat` soft assertion has failed in the current test.
|
|
165
|
+
* Call once at the end of a scenario to fail it when any soft assertion failed.
|
|
166
|
+
*/
|
|
167
|
+
hopeThat.noErrors = function () {
|
|
168
|
+
const failures = hopeThatFailures
|
|
169
|
+
hopeThatFailures = []
|
|
170
|
+
empty('soft assertions').assert(failures)
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
/**
|
|
157
174
|
* A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval.
|
|
158
175
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
|
+
import { simplifyHtmlElement } from '../html.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Unified WebElement class that wraps native element instances from different helpers
|
|
@@ -81,6 +82,10 @@ class WebElement {
|
|
|
81
82
|
async getProperty(name) {
|
|
82
83
|
switch (this.helperType) {
|
|
83
84
|
case 'playwright':
|
|
85
|
+
// For Locator objects, use inputValue() for the 'value' property
|
|
86
|
+
if (name === 'value' && this.element.inputValue) {
|
|
87
|
+
return this.element.inputValue()
|
|
88
|
+
}
|
|
84
89
|
return this.element.evaluate((el, propName) => el[propName], name)
|
|
85
90
|
case 'webdriver':
|
|
86
91
|
return this.element.getProperty(name)
|
|
@@ -236,16 +241,149 @@ class WebElement {
|
|
|
236
241
|
async type(text, options = {}) {
|
|
237
242
|
switch (this.helperType) {
|
|
238
243
|
case 'playwright':
|
|
244
|
+
// Playwright Locator objects use fill() instead of type()
|
|
245
|
+
if (this.element.fill) {
|
|
246
|
+
return this.element.fill(text, options)
|
|
247
|
+
}
|
|
239
248
|
return this.element.type(text, options)
|
|
240
249
|
case 'webdriver':
|
|
241
250
|
return this.element.setValue(text)
|
|
242
251
|
case 'puppeteer':
|
|
252
|
+
await this.element.evaluate(el => { el.value = '' })
|
|
243
253
|
return this.element.type(text, options)
|
|
244
254
|
default:
|
|
245
255
|
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Run a function in the browser with this element as the first argument.
|
|
261
|
+
* @param {Function} fn Browser-side function. Receives the element, then extra args.
|
|
262
|
+
* @param {...any} args Additional arguments passed to the function
|
|
263
|
+
* @returns {Promise<any>} Value returned by fn
|
|
264
|
+
*/
|
|
265
|
+
async evaluate(fn, ...args) {
|
|
266
|
+
switch (this.helperType) {
|
|
267
|
+
case 'playwright':
|
|
268
|
+
case 'puppeteer':
|
|
269
|
+
return this.element.evaluate(fn, ...args)
|
|
270
|
+
case 'webdriver':
|
|
271
|
+
return this.helper.executeScript(fn, this.element, ...args)
|
|
272
|
+
default:
|
|
273
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Focus the element.
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
async focus() {
|
|
282
|
+
switch (this.helperType) {
|
|
283
|
+
case 'playwright':
|
|
284
|
+
return this.element.focus()
|
|
285
|
+
case 'puppeteer':
|
|
286
|
+
if (this.element.focus) return this.element.focus()
|
|
287
|
+
return this.element.evaluate(el => el.focus())
|
|
288
|
+
case 'webdriver':
|
|
289
|
+
return this.helper.executeScript(el => el.focus(), this.element)
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Type characters via the page/browser keyboard into the focused element.
|
|
297
|
+
* Unlike `type()`, this does not call `.fill()`/`.setValue()`, so it works
|
|
298
|
+
* with contenteditable nodes, iframe bodies, and editor-owned hidden textareas.
|
|
299
|
+
* @param {string} text Text to send
|
|
300
|
+
* @param {Object} [options] Options (e.g. `{ delay }`)
|
|
301
|
+
* @returns {Promise<void>}
|
|
302
|
+
*/
|
|
303
|
+
async typeText(text, options = {}) {
|
|
304
|
+
const s = String(text)
|
|
305
|
+
switch (this.helperType) {
|
|
306
|
+
case 'playwright':
|
|
307
|
+
case 'puppeteer':
|
|
308
|
+
return this.helper.page.keyboard.type(s, options)
|
|
309
|
+
case 'webdriver': {
|
|
310
|
+
const ENTER = '\uE007'
|
|
311
|
+
const parts = s.split('\n')
|
|
312
|
+
for (let i = 0; i < parts.length; i++) {
|
|
313
|
+
if (parts[i]) await this.helper.browser.keys(parts[i])
|
|
314
|
+
if (i < parts.length - 1) await this.helper.browser.keys(ENTER)
|
|
315
|
+
}
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Select all content in the focused field and delete it via keyboard input.
|
|
325
|
+
* Sends Ctrl+A and Meta+A (so it works across platforms) followed by Backspace.
|
|
326
|
+
* @returns {Promise<void>}
|
|
327
|
+
*/
|
|
328
|
+
async selectAllAndDelete() {
|
|
329
|
+
switch (this.helperType) {
|
|
330
|
+
case 'playwright':
|
|
331
|
+
await this.helper.page.keyboard.press('Control+a').catch(() => {})
|
|
332
|
+
await this.helper.page.keyboard.press('Meta+a').catch(() => {})
|
|
333
|
+
await this.helper.page.keyboard.press('Backspace')
|
|
334
|
+
return
|
|
335
|
+
case 'puppeteer':
|
|
336
|
+
for (const mod of ['Control', 'Meta']) {
|
|
337
|
+
try {
|
|
338
|
+
await this.helper.page.keyboard.down(mod)
|
|
339
|
+
await this.helper.page.keyboard.press('KeyA')
|
|
340
|
+
await this.helper.page.keyboard.up(mod)
|
|
341
|
+
} catch (e) {}
|
|
342
|
+
}
|
|
343
|
+
await this.helper.page.keyboard.press('Backspace')
|
|
344
|
+
return
|
|
345
|
+
case 'webdriver': {
|
|
346
|
+
const b = this.helper.browser
|
|
347
|
+
await b.keys(['Control', 'a']).catch(() => {})
|
|
348
|
+
await b.keys(['Meta', 'a']).catch(() => {})
|
|
349
|
+
await b.keys(['Backspace'])
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
default:
|
|
353
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Treat this element as an iframe; invoke `fn` with a WebElement wrapping
|
|
359
|
+
* the iframe body. For WebDriver this switches the browser into the frame
|
|
360
|
+
* for the duration of the callback and switches back on exit.
|
|
361
|
+
* @param {(body: WebElement) => Promise<any>} fn
|
|
362
|
+
* @returns {Promise<any>} Return value of fn
|
|
363
|
+
*/
|
|
364
|
+
async inIframe(fn) {
|
|
365
|
+
switch (this.helperType) {
|
|
366
|
+
case 'playwright':
|
|
367
|
+
case 'puppeteer': {
|
|
368
|
+
const frame = await this.element.contentFrame()
|
|
369
|
+
const body = await frame.$('body')
|
|
370
|
+
return fn(new WebElement(body, this.helper))
|
|
371
|
+
}
|
|
372
|
+
case 'webdriver': {
|
|
373
|
+
const browser = this.helper.browser
|
|
374
|
+
await browser.switchFrame(this.element)
|
|
375
|
+
try {
|
|
376
|
+
const body = await browser.$('body')
|
|
377
|
+
return await fn(new WebElement(body, this.helper))
|
|
378
|
+
} finally {
|
|
379
|
+
await browser.switchFrame(null)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
249
387
|
/**
|
|
250
388
|
* Find first child element matching the locator
|
|
251
389
|
* @param {string|Object} locator Element locator
|
|
@@ -256,7 +394,18 @@ class WebElement {
|
|
|
256
394
|
|
|
257
395
|
switch (this.helperType) {
|
|
258
396
|
case 'playwright':
|
|
259
|
-
|
|
397
|
+
// Playwright Locator objects use locator() method
|
|
398
|
+
if (this.element.locator) {
|
|
399
|
+
const childLocator = this.element.locator(this._normalizeLocator(locator))
|
|
400
|
+
// Get the element handle from the locator
|
|
401
|
+
try {
|
|
402
|
+
childElement = await childLocator.elementHandle()
|
|
403
|
+
} catch (e) {
|
|
404
|
+
return null
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
408
|
+
}
|
|
260
409
|
break
|
|
261
410
|
case 'webdriver':
|
|
262
411
|
try {
|
|
@@ -285,7 +434,14 @@ class WebElement {
|
|
|
285
434
|
|
|
286
435
|
switch (this.helperType) {
|
|
287
436
|
case 'playwright':
|
|
288
|
-
|
|
437
|
+
// Playwright Locator objects use locator() method
|
|
438
|
+
if (this.element.locator) {
|
|
439
|
+
const childLocator = this.element.locator(this._normalizeLocator(locator))
|
|
440
|
+
// Get all element handles from the locator
|
|
441
|
+
childElements = await childLocator.elementHandles()
|
|
442
|
+
} else {
|
|
443
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
444
|
+
}
|
|
289
445
|
break
|
|
290
446
|
case 'webdriver':
|
|
291
447
|
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
@@ -306,6 +462,94 @@ class WebElement {
|
|
|
306
462
|
* @returns {string} Normalized CSS selector
|
|
307
463
|
* @private
|
|
308
464
|
*/
|
|
465
|
+
async toAbsoluteXPath() {
|
|
466
|
+
const xpathFn = (el) => {
|
|
467
|
+
const parts = []
|
|
468
|
+
let current = el
|
|
469
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
470
|
+
let index = 0
|
|
471
|
+
let sibling = current.previousSibling
|
|
472
|
+
while (sibling) {
|
|
473
|
+
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
|
|
474
|
+
index++
|
|
475
|
+
}
|
|
476
|
+
sibling = sibling.previousSibling
|
|
477
|
+
}
|
|
478
|
+
const tagName = current.tagName.toLowerCase()
|
|
479
|
+
const pathIndex = index > 0 ? `[${index + 1}]` : ''
|
|
480
|
+
parts.unshift(`${tagName}${pathIndex}`)
|
|
481
|
+
current = current.parentElement
|
|
482
|
+
}
|
|
483
|
+
return '//' + parts.join('/')
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
switch (this.helperType) {
|
|
487
|
+
case 'playwright':
|
|
488
|
+
return this.element.evaluate(xpathFn)
|
|
489
|
+
case 'puppeteer':
|
|
490
|
+
return this.element.evaluate(xpathFn)
|
|
491
|
+
case 'webdriver':
|
|
492
|
+
return this.helper.browser.execute(xpathFn, this.element)
|
|
493
|
+
default:
|
|
494
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async toOuterHTML() {
|
|
499
|
+
switch (this.helperType) {
|
|
500
|
+
case 'playwright':
|
|
501
|
+
return this.element.evaluate(el => el.outerHTML)
|
|
502
|
+
case 'puppeteer':
|
|
503
|
+
return this.element.evaluate(el => el.outerHTML)
|
|
504
|
+
case 'webdriver':
|
|
505
|
+
return this.helper.browser.execute(el => el.outerHTML, this.element)
|
|
506
|
+
default:
|
|
507
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async toSimplifiedHTML(maxLength = 300) {
|
|
512
|
+
const outerHTML = await this.toOuterHTML()
|
|
513
|
+
return simplifyHtmlElement(outerHTML, maxLength)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Plain-object snapshot of the element — text, simplified HTML, visibility,
|
|
518
|
+
* enabled state, and a curated set of attributes. Each underlying call is
|
|
519
|
+
* isolated so a single failure (e.g. detached element) doesn't poison the
|
|
520
|
+
* rest. Suitable for JSON.stringify, log output, MCP tool responses.
|
|
521
|
+
*
|
|
522
|
+
* @param {object} [opts]
|
|
523
|
+
* @param {number} [opts.maxHtmlLength=300] passed through to toSimplifiedHTML
|
|
524
|
+
* @param {string[]} [opts.attrs] attribute names to surface
|
|
525
|
+
* @returns {Promise<{text?: string, html?: string, visible?: boolean, enabled?: boolean, attrs?: object}>}
|
|
526
|
+
*/
|
|
527
|
+
async describe({ maxHtmlLength = 300, attrs = ['id', 'class', 'name', 'role', 'type', 'href', 'value', 'aria-label', 'placeholder', 'data-testid'] } = {}) {
|
|
528
|
+
const out = {}
|
|
529
|
+
await Promise.all([
|
|
530
|
+
this.toSimplifiedHTML(maxHtmlLength).then(v => { if (v) out.html = v }, () => {}),
|
|
531
|
+
this.getText().then(v => { const t = v?.trim(); if (t) out.text = t }, () => {}),
|
|
532
|
+
this.isVisible().then(v => { out.visible = v }, () => {}),
|
|
533
|
+
this.isEnabled().then(v => { out.enabled = v }, () => {}),
|
|
534
|
+
])
|
|
535
|
+
const collected = {}
|
|
536
|
+
await Promise.all(attrs.map(async name => {
|
|
537
|
+
try {
|
|
538
|
+
const v = await this.getAttribute(name)
|
|
539
|
+
if (v != null && v !== '') collected[name] = v
|
|
540
|
+
} catch {}
|
|
541
|
+
}))
|
|
542
|
+
if (Object.keys(collected).length) out.attrs = collected
|
|
543
|
+
return out
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Make accidental JSON.stringify (e.g. returning a WebElement from MCP run_code)
|
|
547
|
+
// produce a usable hint instead of `{}` — the underlying handle isn't
|
|
548
|
+
// serializable. Use .describe() for the real plain-object snapshot.
|
|
549
|
+
toJSON() {
|
|
550
|
+
return `[WebElement ${this.helperType} — call .describe() for a plain-object snapshot or .toSimplifiedHTML() for HTML]`
|
|
551
|
+
}
|
|
552
|
+
|
|
309
553
|
_normalizeLocator(locator) {
|
|
310
554
|
if (typeof locator === 'string') {
|
|
311
555
|
return locator
|
package/lib/els.js
CHANGED
|
@@ -6,10 +6,11 @@ import recordStep from './step/record.js'
|
|
|
6
6
|
import FuncStep from './step/func.js'
|
|
7
7
|
import { truth } from './assert/truth.js'
|
|
8
8
|
import { isAsyncFunction, humanizeFunction } from './utils.js'
|
|
9
|
+
import WebElement from './element/WebElement.js'
|
|
9
10
|
|
|
10
11
|
function element(purpose, locator, fn) {
|
|
11
12
|
let stepConfig
|
|
12
|
-
if (arguments[arguments.length - 1]
|
|
13
|
+
if (StepConfig.isStepConfig(arguments[arguments.length - 1])) {
|
|
13
14
|
stepConfig = arguments[arguments.length - 1]
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -28,7 +29,8 @@ function element(purpose, locator, fn) {
|
|
|
28
29
|
const els = await step.helper._locate(locator)
|
|
29
30
|
output.debug(`Found ${els.length} elements, using first element`)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
const wrapped = new WebElement(els[0], step.helper)
|
|
33
|
+
return fn(wrapped)
|
|
32
34
|
},
|
|
33
35
|
stepConfig,
|
|
34
36
|
)
|
|
@@ -52,7 +54,8 @@ function eachElement(purpose, locator, fn) {
|
|
|
52
54
|
let i = 0
|
|
53
55
|
for (const el of els) {
|
|
54
56
|
try {
|
|
55
|
-
|
|
57
|
+
const wrapped = new WebElement(el, step.helper)
|
|
58
|
+
await fn(wrapped, i)
|
|
56
59
|
} catch (err) {
|
|
57
60
|
output.error(`eachElement: failed operation on element #${i} ${el}`)
|
|
58
61
|
errs.push(err)
|
|
@@ -74,7 +77,8 @@ function expectElement(locator, fn) {
|
|
|
74
77
|
const els = await step.helper._locate(locator)
|
|
75
78
|
output.debug(`Found ${els.length} elements, first will be used for assertion`)
|
|
76
79
|
|
|
77
|
-
const
|
|
80
|
+
const wrapped = new WebElement(els[0], step.helper)
|
|
81
|
+
const result = await fn(wrapped)
|
|
78
82
|
const assertion = truth(`element (${locator})`, fn.toString())
|
|
79
83
|
assertion.assert(result)
|
|
80
84
|
})
|
|
@@ -92,7 +96,8 @@ function expectAnyElement(locator, fn) {
|
|
|
92
96
|
|
|
93
97
|
let found = false
|
|
94
98
|
for (const el of els) {
|
|
95
|
-
const
|
|
99
|
+
const wrapped = new WebElement(el, step.helper)
|
|
100
|
+
const result = await fn(wrapped)
|
|
96
101
|
if (result) {
|
|
97
102
|
found = true
|
|
98
103
|
break
|
|
@@ -113,7 +118,8 @@ function expectAllElements(locator, fn) {
|
|
|
113
118
|
let i = 1
|
|
114
119
|
for (const el of els) {
|
|
115
120
|
output.debug(`checking element #${i}: ${el}`)
|
|
116
|
-
const
|
|
121
|
+
const wrapped = new WebElement(el, step.helper)
|
|
122
|
+
const result = await fn(wrapped)
|
|
117
123
|
const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
|
|
118
124
|
assertion.assert(result)
|
|
119
125
|
i++
|