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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import minimatch from 'minimatch'
|
|
4
|
+
import store from '../../store.js'
|
|
5
|
+
import assert from 'assert'
|
|
6
|
+
|
|
7
|
+
function getDownloadDir() {
|
|
8
|
+
return path.join(store.outputDir, 'downloads')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getNewFiles(downloadDir, sinceTimestamp) {
|
|
12
|
+
if (!fs.existsSync(downloadDir)) return []
|
|
13
|
+
return fs.readdirSync(downloadDir).filter(name => {
|
|
14
|
+
const stat = fs.statSync(path.join(downloadDir, name))
|
|
15
|
+
return stat.isFile() && stat.mtimeMs >= sinceTimestamp
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function seeFileDownloaded(arg) {
|
|
20
|
+
const downloadDir = getDownloadDir()
|
|
21
|
+
const files = getNewFiles(downloadDir, this._downloadStartTimestamp)
|
|
22
|
+
|
|
23
|
+
if (arg === undefined || arg === null) {
|
|
24
|
+
assert.ok(files.length > 0, `No files downloaded to ${downloadDir}`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
if (typeof arg === 'number') {
|
|
28
|
+
assert.strictEqual(files.length, arg, `Expected ${arg} downloaded file(s), found ${files.length}: [${files.join(', ')}]`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const regexMatch = arg.match(/^\/(.+)\/$/)
|
|
32
|
+
if (regexMatch) {
|
|
33
|
+
const re = new RegExp(regexMatch[1])
|
|
34
|
+
assert.ok(files.some(f => re.test(f)), `No file matches ${arg}. Downloaded: [${files.join(', ')}]`)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if (/[*?[\]]/.test(arg)) {
|
|
38
|
+
const matched = minimatch.match(files, arg)
|
|
39
|
+
assert.ok(matched.length > 0, `No file matches glob "${arg}". Downloaded: [${files.join(', ')}]`)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
assert.ok(files.includes(arg), `File "${arg}" not downloaded. Downloaded: [${files.join(', ')}]`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { seeFileDownloaded, getDownloadDir }
|
|
@@ -1,52 +1,61 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
|
|
4
|
+
let resqScript
|
|
5
|
+
|
|
1
6
|
async function findReact(matcher, locator) {
|
|
2
|
-
// Handle both Locator objects and raw locator objects
|
|
3
7
|
const reactLocator = locator.locator || locator
|
|
4
|
-
|
|
5
|
-
let props = '';
|
|
8
|
+
const page = typeof matcher.page === 'function' ? matcher.page() : matcher
|
|
6
9
|
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
_locator += props;
|
|
10
|
+
if (!resqScript) {
|
|
11
|
+
resqScript = fs.readFileSync(fileURLToPath(import.meta.resolve('resq'))).toString()
|
|
10
12
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
await page.evaluate(resqScript)
|
|
14
|
+
await page.evaluate(() => window.resq.waitToLoadReact())
|
|
15
|
+
|
|
16
|
+
const arrayHandle = await page.evaluateHandle(
|
|
17
|
+
({ selector, props, state }) => {
|
|
18
|
+
let elements = window.resq.resq$$(selector)
|
|
19
|
+
if (Object.keys(props).length) elements = elements.byProps(props)
|
|
20
|
+
if (Object.keys(state).length) elements = elements.byState(state)
|
|
21
|
+
if (!elements.length) return []
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
let nodes = []
|
|
24
|
+
elements.forEach(element => {
|
|
25
|
+
let { node, isFragment } = element
|
|
26
|
+
if (!node) {
|
|
27
|
+
isFragment = true
|
|
28
|
+
node = element.children
|
|
29
|
+
}
|
|
30
|
+
if (isFragment) nodes = nodes.concat(node)
|
|
31
|
+
else nodes.push(node)
|
|
32
|
+
})
|
|
33
|
+
return [...nodes]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
selector: reactLocator.react,
|
|
37
|
+
props: reactLocator.props || {},
|
|
38
|
+
state: reactLocator.state || {},
|
|
39
|
+
},
|
|
40
|
+
)
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
const properties = await arrayHandle.getProperties()
|
|
43
|
+
await arrayHandle.dispose()
|
|
44
|
+
const result = []
|
|
45
|
+
for (const property of properties.values()) {
|
|
46
|
+
const elementHandle = property.asElement()
|
|
47
|
+
if (elementHandle) result.push(elementHandle)
|
|
23
48
|
}
|
|
24
|
-
return
|
|
49
|
+
return result
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
async function findByPlaywrightLocator(matcher, locator) {
|
|
28
|
-
// Handle both Locator objects and raw locator objects
|
|
29
53
|
const pwLocator = locator.locator || locator
|
|
30
54
|
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
|
|
31
|
-
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
55
|
+
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
32
56
|
}
|
|
33
57
|
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
|
|
34
|
-
return matcher.locator(pwValue).all()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function propBuilder(props) {
|
|
38
|
-
let _props = '';
|
|
39
|
-
|
|
40
|
-
for (const [key, value] of Object.entries(props)) {
|
|
41
|
-
if (typeof value === 'object') {
|
|
42
|
-
for (const [k, v] of Object.entries(value)) {
|
|
43
|
-
_props += `[${key}.${k} = "${v}"]`;
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
_props += `[${key} = "${value}"]`;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return _props;
|
|
58
|
+
return matcher.locator(pwValue).all()
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
export { findReact,
|
|
61
|
+
export { findReact, findByPlaywrightLocator }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import store from '../../store.js'
|
|
2
|
+
import output from '../../output.js'
|
|
3
|
+
import WebElement from '../../element/WebElement.js'
|
|
4
|
+
import MultipleElementsFound from '../errors/MultipleElementsFound.js'
|
|
5
|
+
|
|
6
|
+
function resolveElementIndex(value) {
|
|
7
|
+
if (value === 'first') return 1
|
|
8
|
+
if (value === 'last') return -1
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isStrictStep(opts, helper) {
|
|
13
|
+
if (opts?.exact === true || opts?.strictMode === true) return true
|
|
14
|
+
if (opts?.exact === false || opts?.strictMode === false) return false
|
|
15
|
+
return helper.options.strict
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function selectElement(els, locator, helper) {
|
|
19
|
+
const opts = store.currentStep?.opts
|
|
20
|
+
const rawIndex = opts?.elementIndex
|
|
21
|
+
const elementIndex = resolveElementIndex(rawIndex)
|
|
22
|
+
|
|
23
|
+
if (elementIndex != null) {
|
|
24
|
+
if (els.length === 1) return els[0]
|
|
25
|
+
|
|
26
|
+
if (!Number.isInteger(elementIndex) || elementIndex === 0) {
|
|
27
|
+
throw new Error(`elementIndex must be a non-zero integer or 'first'/'last', got: ${rawIndex}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let idx
|
|
31
|
+
if (elementIndex > 0) {
|
|
32
|
+
idx = elementIndex - 1
|
|
33
|
+
if (idx >= els.length) {
|
|
34
|
+
throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
idx = els.length + elementIndex
|
|
38
|
+
if (idx < 0) {
|
|
39
|
+
throw new Error(`elementIndex ${elementIndex} exceeds the number of elements found (${els.length}) for "${locator}"`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
output.debug(`[Elements] Using element #${elementIndex} out of ${els.length}`)
|
|
44
|
+
return els[idx]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isStrictStep(opts, helper)) {
|
|
48
|
+
if (els.length > 1) {
|
|
49
|
+
const webElements = Array.from(els).map(el => new WebElement(el, helper))
|
|
50
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (els.length > 1) output.debug(`[Elements] Using first element out of ${els.length}`)
|
|
55
|
+
return els[0]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { selectElement }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import store from '../../store.js'
|
|
2
|
+
import NonFocusedType from '../errors/NonFocusedType.js'
|
|
3
|
+
|
|
4
|
+
const MODIFIER_PATTERN = /^(control|ctrl|meta|cmd|command|commandorcontrol|ctrlorcommand)/i
|
|
5
|
+
const EDITING_KEYS = new Set(['a', 'c', 'x', 'v', 'z', 'y'])
|
|
6
|
+
|
|
7
|
+
async function isNoElementFocused(helper) {
|
|
8
|
+
return helper.executeScript(() => {
|
|
9
|
+
const ae = document.activeElement
|
|
10
|
+
return !ae || ae === document.documentElement || (ae === document.body && !ae.isContentEditable)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function checkFocusBeforeType(helper) {
|
|
15
|
+
if (!helper.options.strict && !store.debugMode) return
|
|
16
|
+
if (!await isNoElementFocused(helper)) return
|
|
17
|
+
|
|
18
|
+
const message = 'No element is in focus. Use I.click() or I.focus() to activate an element before typing.'
|
|
19
|
+
if (helper.options.strict) throw new NonFocusedType(message)
|
|
20
|
+
helper.debugSection('Warning', message)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function checkFocusBeforePressKey(helper, originalKey) {
|
|
24
|
+
if (!helper.options.strict && !store.debugMode) return
|
|
25
|
+
if (!Array.isArray(originalKey)) return
|
|
26
|
+
|
|
27
|
+
let hasCtrlOrMeta = false
|
|
28
|
+
let actionKey = null
|
|
29
|
+
for (const k of originalKey) {
|
|
30
|
+
if (MODIFIER_PATTERN.test(k)) {
|
|
31
|
+
hasCtrlOrMeta = true
|
|
32
|
+
} else {
|
|
33
|
+
actionKey = k
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!hasCtrlOrMeta || !actionKey || !EDITING_KEYS.has(actionKey.toLowerCase())) return
|
|
37
|
+
|
|
38
|
+
if (!await isNoElementFocused(helper)) return
|
|
39
|
+
|
|
40
|
+
const message = `No element is in focus. Key combination with "${originalKey.join('+')}" may not work as expected. Use I.click() or I.focus() first.`
|
|
41
|
+
if (helper.options.strict) throw new NonFocusedType(message)
|
|
42
|
+
helper.debugSection('Warning', message)
|
|
43
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import WebElement from '../../element/WebElement.js'
|
|
2
|
+
|
|
3
|
+
const MARKER = 'data-codeceptjs-rte-target'
|
|
4
|
+
|
|
5
|
+
const EDITOR = {
|
|
6
|
+
STANDARD: 'standard',
|
|
7
|
+
IFRAME: 'iframe',
|
|
8
|
+
CONTENTEDITABLE: 'contenteditable',
|
|
9
|
+
HIDDEN_TEXTAREA: 'hidden-textarea',
|
|
10
|
+
UNREACHABLE: 'unreachable',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function detectAndMark(el, opts) {
|
|
14
|
+
const marker = opts.marker
|
|
15
|
+
const kinds = opts.kinds
|
|
16
|
+
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
17
|
+
|
|
18
|
+
function mark(kind, target) {
|
|
19
|
+
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
20
|
+
if (target && target.nodeType === 1) target.setAttribute(marker, '1')
|
|
21
|
+
return kind
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!el || el.nodeType !== 1) return mark(kinds.STANDARD, el)
|
|
25
|
+
|
|
26
|
+
const tag = el.tagName
|
|
27
|
+
if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
|
|
28
|
+
if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
|
|
29
|
+
|
|
30
|
+
const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
|
|
31
|
+
if ((tag === 'INPUT' || tag === 'TEXTAREA') && !isFormHidden) {
|
|
32
|
+
const style = window.getComputedStyle(el)
|
|
33
|
+
if (style.display === 'none') return mark(kinds.UNREACHABLE, el)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
|
|
37
|
+
if (canSearchDescendants) {
|
|
38
|
+
const iframe = el.querySelector('iframe')
|
|
39
|
+
if (iframe) return mark(kinds.IFRAME, iframe)
|
|
40
|
+
const ce = el.querySelector(CE)
|
|
41
|
+
if (ce) return mark(kinds.CONTENTEDITABLE, ce)
|
|
42
|
+
const textareas = [...el.querySelectorAll('textarea')]
|
|
43
|
+
const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
|
|
44
|
+
const textarea = focusable || textareas[0]
|
|
45
|
+
if (textarea) return mark(kinds.HIDDEN_TEXTAREA, textarea)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return mark(kinds.STANDARD, el)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectInsideFrame() {
|
|
52
|
+
const MARKER = 'data-codeceptjs-rte-target'
|
|
53
|
+
const CE = '[contenteditable="true"], [contenteditable=""]'
|
|
54
|
+
const CONTENTEDITABLE = 'contenteditable'
|
|
55
|
+
const HIDDEN_TEXTAREA = 'hidden-textarea'
|
|
56
|
+
const body = document.body
|
|
57
|
+
document.querySelectorAll('[' + MARKER + ']').forEach(n => n.removeAttribute(MARKER))
|
|
58
|
+
|
|
59
|
+
if (body.isContentEditable) return CONTENTEDITABLE
|
|
60
|
+
|
|
61
|
+
const ce = body.querySelector(CE)
|
|
62
|
+
if (ce) {
|
|
63
|
+
ce.setAttribute(MARKER, '1')
|
|
64
|
+
return CONTENTEDITABLE
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const textareas = [...body.querySelectorAll('textarea')]
|
|
68
|
+
const focusable = textareas.find(t => window.getComputedStyle(t).display !== 'none')
|
|
69
|
+
const textarea = focusable || textareas[0]
|
|
70
|
+
if (textarea) {
|
|
71
|
+
textarea.setAttribute(MARKER, '1')
|
|
72
|
+
return HIDDEN_TEXTAREA
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return CONTENTEDITABLE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function evaluateInFrame(helper, body, fn) {
|
|
79
|
+
if (body.helperType === 'webdriver') {
|
|
80
|
+
return helper.executeScript(fn)
|
|
81
|
+
}
|
|
82
|
+
return body.element.evaluate(fn)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function focusMarkedInFrameScript() {
|
|
86
|
+
const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
|
|
87
|
+
el.focus()
|
|
88
|
+
return document.activeElement === el
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function selectAllInFrameScript() {
|
|
92
|
+
const el = document.querySelector('[data-codeceptjs-rte-target]') || document.body
|
|
93
|
+
el.focus()
|
|
94
|
+
const range = document.createRange()
|
|
95
|
+
range.selectNodeContents(el)
|
|
96
|
+
const sel = window.getSelection()
|
|
97
|
+
sel.removeAllRanges()
|
|
98
|
+
sel.addRange(range)
|
|
99
|
+
return document.activeElement === el
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function selectAllInEditable(el) {
|
|
103
|
+
const doc = el.ownerDocument
|
|
104
|
+
const win = doc.defaultView
|
|
105
|
+
el.focus()
|
|
106
|
+
const range = doc.createRange()
|
|
107
|
+
range.selectNodeContents(el)
|
|
108
|
+
const sel = win.getSelection()
|
|
109
|
+
sel.removeAllRanges()
|
|
110
|
+
sel.addRange(range)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function unmarkAll(marker) {
|
|
114
|
+
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isActive(el) {
|
|
118
|
+
return el.ownerDocument.activeElement === el
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function assertFocused(target) {
|
|
122
|
+
const focused = await target.evaluate(isActive)
|
|
123
|
+
if (!focused) {
|
|
124
|
+
throw new Error('fillField: rich editor target did not accept focus. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable) — not a hidden backing element.')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function findMarked(helper) {
|
|
129
|
+
const root = helper.page || helper.browser
|
|
130
|
+
const raw = await root.$('[' + MARKER + ']')
|
|
131
|
+
return new WebElement(raw, helper)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function clearMarker(helper) {
|
|
135
|
+
if (helper.page) return helper.page.evaluate(unmarkAll, MARKER)
|
|
136
|
+
return helper.executeScript(unmarkAll, MARKER)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function fillRichEditor(helper, el, value) {
|
|
140
|
+
const source = el instanceof WebElement ? el : new WebElement(el, helper)
|
|
141
|
+
const kind = await source.evaluate(detectAndMark, { marker: MARKER, kinds: EDITOR })
|
|
142
|
+
if (kind === EDITOR.STANDARD) return false
|
|
143
|
+
if (kind === EDITOR.UNREACHABLE) {
|
|
144
|
+
throw new Error('fillField: cannot fill a display:none form control. Locator must point at the visible editor surface (a wrapper, iframe, or contenteditable).')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const target = await findMarked(helper)
|
|
148
|
+
const delay = helper.options.pressKeyDelay
|
|
149
|
+
|
|
150
|
+
if (kind === EDITOR.IFRAME) {
|
|
151
|
+
await target.inIframe(async body => {
|
|
152
|
+
const innerKind = await evaluateInFrame(helper, body, detectInsideFrame)
|
|
153
|
+
if (innerKind === EDITOR.HIDDEN_TEXTAREA) {
|
|
154
|
+
const focused = await evaluateInFrame(helper, body, focusMarkedInFrameScript)
|
|
155
|
+
if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
|
|
156
|
+
await body.selectAllAndDelete()
|
|
157
|
+
await body.typeText(value, { delay })
|
|
158
|
+
} else {
|
|
159
|
+
const focused = await evaluateInFrame(helper, body, selectAllInFrameScript)
|
|
160
|
+
if (!focused) throw new Error('fillField: rich editor target inside iframe did not accept focus.')
|
|
161
|
+
await body.typeText(value, { delay })
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
} else if (kind === EDITOR.HIDDEN_TEXTAREA) {
|
|
165
|
+
await target.focus()
|
|
166
|
+
await assertFocused(target)
|
|
167
|
+
await target.selectAllAndDelete()
|
|
168
|
+
await target.typeText(value, { delay })
|
|
169
|
+
} else if (kind === EDITOR.CONTENTEDITABLE) {
|
|
170
|
+
await target.click()
|
|
171
|
+
await target.evaluate(selectAllInEditable)
|
|
172
|
+
await assertFocused(target)
|
|
173
|
+
await target.typeText(value, { delay })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await clearMarker(helper)
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const dropFile = (el, { base64Content, fileName, mimeType }) => {
|
|
2
|
+
const binaryStr = atob(base64Content)
|
|
3
|
+
const bytes = new Uint8Array(binaryStr.length)
|
|
4
|
+
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i)
|
|
5
|
+
const fileObj = new File([bytes], fileName, { type: mimeType })
|
|
6
|
+
const dataTransfer = new DataTransfer()
|
|
7
|
+
dataTransfer.items.add(fileObj)
|
|
8
|
+
el.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }))
|
|
9
|
+
el.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }))
|
|
10
|
+
el.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }))
|
|
11
|
+
}
|
package/lib/history.js
CHANGED
|
@@ -2,6 +2,7 @@ import colors from 'chalk'
|
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import output from './output.js'
|
|
5
|
+
import store from './store.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* REPL history records REPL commands and stores them in
|
|
@@ -9,8 +10,8 @@ import output from './output.js'
|
|
|
9
10
|
*/
|
|
10
11
|
class ReplHistory {
|
|
11
12
|
constructor() {
|
|
12
|
-
if (
|
|
13
|
-
this.historyFile = path.join(
|
|
13
|
+
if (store.outputDir) {
|
|
14
|
+
this.historyFile = path.join(store.outputDir, 'cli-history')
|
|
14
15
|
}
|
|
15
16
|
this.commands = []
|
|
16
17
|
}
|
package/lib/html.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parse, serialize } from 'parse5'
|
|
2
2
|
import { minify } from 'html-minifier-terser'
|
|
3
|
+
import beautify from 'js-beautify'
|
|
4
|
+
|
|
5
|
+
const { html: html_beautify } = beautify
|
|
3
6
|
|
|
4
7
|
async function minifyHtml(html) {
|
|
5
8
|
return minify(html, {
|
|
@@ -14,6 +17,62 @@ async function minifyHtml(html) {
|
|
|
14
17
|
})
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
21
|
+
|
|
22
|
+
function isTrashClass(className) {
|
|
23
|
+
if (!className) return true
|
|
24
|
+
if (/\d/.test(className)) return true
|
|
25
|
+
if (TRASH_HTML_CLASSES.test(className)) return true
|
|
26
|
+
if (/(:|__)/.test(className)) return true
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function filterClassValue(value) {
|
|
31
|
+
return (value || '')
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.filter(c => c && !isTrashClass(c))
|
|
34
|
+
.join(' ')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DROP_TAGS = new Set(['style', 'noscript'])
|
|
38
|
+
const DROP_ATTRS = new Set(['style'])
|
|
39
|
+
|
|
40
|
+
function cleanHtml(html) {
|
|
41
|
+
const document = parse(html)
|
|
42
|
+
|
|
43
|
+
function walk(node) {
|
|
44
|
+
if (!node) return false
|
|
45
|
+
|
|
46
|
+
if (DROP_TAGS.has(node.nodeName) || (node.nodeName === 'script' && !(node.attrs || []).some(a => a.name === 'src'))) {
|
|
47
|
+
const parent = node.parentNode
|
|
48
|
+
const idx = parent.childNodes.indexOf(node)
|
|
49
|
+
if (idx >= 0) parent.childNodes.splice(idx, 1)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (node.attrs) {
|
|
54
|
+
node.attrs = node.attrs.filter(attr => {
|
|
55
|
+
if (DROP_ATTRS.has(attr.name)) return false
|
|
56
|
+
if (attr.name === 'class') {
|
|
57
|
+
attr.value = filterClassValue(attr.value)
|
|
58
|
+
if (!attr.value) return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.childNodes) {
|
|
65
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
66
|
+
walk(node.childNodes[i])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
walk(document)
|
|
73
|
+
return serialize(document)
|
|
74
|
+
}
|
|
75
|
+
|
|
17
76
|
const defaultHtmlOpts = {
|
|
18
77
|
interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
|
|
19
78
|
textElements: ['label', 'h1', 'h2'],
|
|
@@ -28,7 +87,6 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
28
87
|
// Parse the HTML into a document tree
|
|
29
88
|
const document = parse(html)
|
|
30
89
|
|
|
31
|
-
const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
32
90
|
// Array to store interactive elements
|
|
33
91
|
const removeElements = ['path', 'script']
|
|
34
92
|
|
|
@@ -103,21 +161,10 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
103
161
|
if (node.attrs) {
|
|
104
162
|
// Filter and keep allowed attributes, accessibility attributes
|
|
105
163
|
node.attrs = node.attrs.filter(attr => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Remove classes containing digits
|
|
109
|
-
attr.value = value
|
|
110
|
-
.split(' ')
|
|
111
|
-
// remove classes containing digits/
|
|
112
|
-
.filter(className => !/\d/.test(className))
|
|
113
|
-
// remove popular trash classes
|
|
114
|
-
.filter(className => !className.match(trashHtmlClasses))
|
|
115
|
-
// remove classes with : and __ in them
|
|
116
|
-
.filter(className => !className.match(/(:|__)/))
|
|
117
|
-
.join(' ')
|
|
164
|
+
if (attr.name === 'class') {
|
|
165
|
+
attr.value = filterClassValue(attr.value)
|
|
118
166
|
}
|
|
119
|
-
|
|
120
|
-
return allowedAttrs.includes(name)
|
|
167
|
+
return allowedAttrs.includes(attr.name)
|
|
121
168
|
})
|
|
122
169
|
}
|
|
123
170
|
|
|
@@ -245,4 +292,44 @@ function splitByChunks(text, chunkSize) {
|
|
|
245
292
|
return chunks.map(chunk => chunk.trim())
|
|
246
293
|
}
|
|
247
294
|
|
|
248
|
-
|
|
295
|
+
function simplifyHtmlElement(html, maxLength = 300) {
|
|
296
|
+
try {
|
|
297
|
+
html = removeNonInteractiveElements(html)
|
|
298
|
+
html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
|
|
299
|
+
} catch (e) {
|
|
300
|
+
// keep raw html if minification fails
|
|
301
|
+
}
|
|
302
|
+
if (html.length > maxLength) {
|
|
303
|
+
html = html.slice(0, maxLength) + '...'
|
|
304
|
+
}
|
|
305
|
+
return html
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function formatHtml(html) {
|
|
309
|
+
let processed = html
|
|
310
|
+
try {
|
|
311
|
+
processed = await minifyHtml(processed)
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// keep raw html if minification fails
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
processed = cleanHtml(processed)
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// keep minified html if cleaning fails
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return html_beautify(processed, {
|
|
322
|
+
indent_size: 2,
|
|
323
|
+
wrap_line_length: 0,
|
|
324
|
+
preserve_newlines: false,
|
|
325
|
+
end_with_newline: false,
|
|
326
|
+
// Force every element onto its own line so line numbers in trace HTML
|
|
327
|
+
// map 1:1 to elements (consumed by codeceptq for AI/agent debugging).
|
|
328
|
+
inline: [],
|
|
329
|
+
})
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return processed
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement, formatHtml, cleanHtml, isTrashClass }
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,10 @@ import heal from './heal.js'
|
|
|
23
23
|
import ai from './ai.js'
|
|
24
24
|
import Workers from './workers.js'
|
|
25
25
|
import Secret, { secret } from './secret.js'
|
|
26
|
+
import session from './session.js'
|
|
27
|
+
|
|
28
|
+
const inject = (name) => container.support(name)
|
|
29
|
+
const locate = (query) => locator.build(query)
|
|
26
30
|
|
|
27
31
|
export default {
|
|
28
32
|
/** @type {typeof CodeceptJS.Codecept} */
|
|
@@ -67,7 +71,11 @@ export default {
|
|
|
67
71
|
Secret,
|
|
68
72
|
/** @type {typeof CodeceptJS.secret} */
|
|
69
73
|
secret,
|
|
74
|
+
|
|
75
|
+
session,
|
|
76
|
+
inject,
|
|
77
|
+
locate,
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// Named exports for ESM compatibility
|
|
73
|
-
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret }
|
|
81
|
+
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, session, inject, locate }
|
package/lib/listener/config.js
CHANGED
|
@@ -2,16 +2,18 @@ import event from '../event.js'
|
|
|
2
2
|
import recorder from '../recorder.js'
|
|
3
3
|
import { deepMerge, deepClone, ucfirst } from '../utils.js'
|
|
4
4
|
import output from '../output.js'
|
|
5
|
+
import container from '../container.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Enable Helpers to listen to test events
|
|
8
9
|
*/
|
|
10
|
+
let initialized = false
|
|
11
|
+
|
|
9
12
|
export default function () {
|
|
10
|
-
|
|
11
|
-
if (global.__codeceptConfigListenerInitialized) {
|
|
13
|
+
if (initialized) {
|
|
12
14
|
return
|
|
13
15
|
}
|
|
14
|
-
|
|
16
|
+
initialized = true
|
|
15
17
|
|
|
16
18
|
enableDynamicConfigFor('suite')
|
|
17
19
|
enableDynamicConfigFor('test')
|
|
@@ -20,7 +22,7 @@ export default function () {
|
|
|
20
22
|
event.dispatcher.on(event[type].before, (context = {}) => {
|
|
21
23
|
// Get helpers dynamically at runtime, not at initialization time
|
|
22
24
|
// This ensures we get the actual helper instances, not placeholders
|
|
23
|
-
const helpers =
|
|
25
|
+
const helpers = container.helpers()
|
|
24
26
|
|
|
25
27
|
function updateHelperConfig(helper, config) {
|
|
26
28
|
// Guard against undefined or invalid helpers
|
package/lib/listener/emptyRun.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import figures from 'figures'
|
|
2
2
|
import event from '../event.js'
|
|
3
3
|
import output from '../output.js'
|
|
4
|
+
import container from '../container.js'
|
|
4
5
|
import { searchWithFusejs } from '../utils.js'
|
|
5
6
|
|
|
6
7
|
export default function () {
|
|
@@ -12,7 +13,7 @@ export default function () {
|
|
|
12
13
|
|
|
13
14
|
event.dispatcher.on(event.all.result, () => {
|
|
14
15
|
if (isEmptyRun) {
|
|
15
|
-
const mocha =
|
|
16
|
+
const mocha = container.mocha()
|
|
16
17
|
|
|
17
18
|
if (mocha.options.grep) {
|
|
18
19
|
output.print()
|