codeceptjs 4.0.2-beta.9 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -28
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +745 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +195 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +34 -25
- package/lib/command/check.js +2 -1
- package/lib/command/definitions.js +6 -7
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +3 -1
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +249 -270
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -1
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +84 -41
- package/lib/config.js +96 -18
- package/lib/container.js +115 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +358 -467
- package/lib/helper/Puppeteer.js +335 -192
- package/lib/helper/WebDriver.js +324 -111
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +7 -107
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +158 -16
- package/lib/mocha/cli.js +19 -1
- package/lib/mocha/factory.js +11 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +457 -0
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +5 -4
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +43 -32
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +15 -2
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/loaderCheck.js +28 -0
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils/typescript.js +188 -23
- package/lib/utils.js +77 -3
- package/lib/workers.js +65 -40
- package/package.json +35 -30
- package/typings/index.d.ts +119 -8
- package/typings/promiseBasedTypes.d.ts +3158 -6065
- package/typings/types.d.ts +3453 -6494
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
|
@@ -5,10 +5,13 @@ import Locator from '../../locator.js'
|
|
|
5
5
|
*/
|
|
6
6
|
class ElementNotFound {
|
|
7
7
|
constructor(locator, prefixMessage = 'Element', postfixMessage = 'was not found by text|CSS|XPath') {
|
|
8
|
+
let locatorStr
|
|
8
9
|
if (typeof locator === 'object') {
|
|
9
|
-
|
|
10
|
+
locatorStr = JSON.stringify(locator)
|
|
11
|
+
} else {
|
|
12
|
+
locatorStr = new Locator(locator).toString()
|
|
10
13
|
}
|
|
11
|
-
throw new Error(`${prefixMessage} "${
|
|
14
|
+
throw new Error(`${prefixMessage} "${locatorStr}" ${postfixMessage}`)
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Locator from '../../locator.js'
|
|
2
|
+
|
|
3
|
+
class MultipleElementsFound extends Error {
|
|
4
|
+
constructor(locator, webElements) {
|
|
5
|
+
const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
|
|
6
|
+
? new Locator(locator).toString()
|
|
7
|
+
: String(locator)
|
|
8
|
+
super(`Multiple elements (${webElements.length}) found for "${locatorStr}" in strict mode. Call fetchDetails() for full information.`)
|
|
9
|
+
this.name = 'MultipleElementsFound'
|
|
10
|
+
this.locator = locator
|
|
11
|
+
this.webElements = webElements
|
|
12
|
+
this.count = webElements.length
|
|
13
|
+
this._detailsFetched = false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async fetchDetails() {
|
|
17
|
+
if (this._detailsFetched) return
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const items = []
|
|
21
|
+
const maxToShow = Math.min(this.count, 10)
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < maxToShow; i++) {
|
|
24
|
+
const webEl = this.webElements[i]
|
|
25
|
+
try {
|
|
26
|
+
const xpath = await webEl.toAbsoluteXPath()
|
|
27
|
+
const html = await webEl.toSimplifiedHTML()
|
|
28
|
+
items.push(` ${i + 1}. > ${xpath}\n ${html}`)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.count > 10) {
|
|
35
|
+
items.push(` ... and ${this.count - 10} more`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const locatorStr = (typeof this.locator === 'object' && !(this.locator instanceof Locator))
|
|
39
|
+
? new Locator(this.locator).toString()
|
|
40
|
+
: String(this.locator)
|
|
41
|
+
this.message = `Multiple elements (${this.count}) found for "${locatorStr}" in strict mode.\n` +
|
|
42
|
+
items.join('\n') +
|
|
43
|
+
`\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
|
|
44
|
+
} catch (err) {
|
|
45
|
+
this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this._detailsFetched = true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default MultipleElementsFound
|
|
@@ -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,110 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return `${locator.type}=${locator.value}`
|
|
6
|
-
}
|
|
7
|
-
if (locator.isXPath()) {
|
|
8
|
-
return `xpath=${locator.value}`
|
|
9
|
-
}
|
|
10
|
-
return locator.simplify()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function findElements(matcher, locator) {
|
|
14
|
-
const matchedLocator = new Locator(locator, 'css')
|
|
15
|
-
|
|
16
|
-
if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
|
|
17
|
-
if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
|
|
18
|
-
if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator)
|
|
19
|
-
if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator)
|
|
20
|
-
|
|
21
|
-
return matcher.locator(buildLocatorString(matchedLocator)).all()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function findElement(matcher, locator) {
|
|
25
|
-
const matchedLocator = new Locator(locator, 'css')
|
|
26
|
-
|
|
27
|
-
if (matchedLocator.type === 'react') return findReact(matcher, matchedLocator)
|
|
28
|
-
if (matchedLocator.type === 'vue') return findVue(matcher, matchedLocator)
|
|
29
|
-
if (matchedLocator.type === 'pw') return findByPlaywrightLocator(matcher, matchedLocator, { first: true })
|
|
30
|
-
if (matchedLocator.isRole()) return findByRole(matcher, matchedLocator, { first: true })
|
|
31
|
-
|
|
32
|
-
return matcher.locator(buildLocatorString(matchedLocator)).first()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function getVisibleElements(elements) {
|
|
36
|
-
const visibleElements = []
|
|
37
|
-
for (const element of elements) {
|
|
38
|
-
if (await element.isVisible()) {
|
|
39
|
-
visibleElements.push(element)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (visibleElements.length === 0) {
|
|
43
|
-
return elements
|
|
44
|
-
}
|
|
45
|
-
return visibleElements
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function findReact(matcher, locator) {
|
|
49
|
-
const details = locator.locator ?? { react: locator.value }
|
|
50
|
-
let locatorString = `_react=${details.react}`
|
|
51
|
-
|
|
52
|
-
if (details.props) {
|
|
53
|
-
locatorString += propBuilder(details.props)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return matcher.locator(locatorString).all()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function findVue(matcher, locator) {
|
|
60
|
-
const details = locator.locator ?? { vue: locator.value }
|
|
61
|
-
let locatorString = `_vue=${details.vue}`
|
|
62
|
-
|
|
63
|
-
if (details.props) {
|
|
64
|
-
locatorString += propBuilder(details.props)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return matcher.locator(locatorString).all()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function findByPlaywrightLocator(matcher, locator, { first = false } = {}) {
|
|
71
|
-
const details = locator.locator ?? { pw: locator.value }
|
|
72
|
-
const locatorValue = details.pw
|
|
73
|
-
|
|
74
|
-
const handle = matcher.locator(locatorValue)
|
|
75
|
-
return first ? handle.first() : handle.all()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function findByRole(matcher, locator, { first = false } = {}) {
|
|
79
|
-
const details = locator.locator ?? { role: locator.value }
|
|
80
|
-
const { role, text, name, exact, includeHidden, ...rest } = details
|
|
81
|
-
const options = { ...rest }
|
|
82
|
-
|
|
83
|
-
if (includeHidden !== undefined) options.includeHidden = includeHidden
|
|
84
|
-
|
|
85
|
-
const accessibleName = name ?? text
|
|
86
|
-
if (accessibleName !== undefined) {
|
|
87
|
-
options.name = accessibleName
|
|
88
|
-
if (exact === true) options.exact = true
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const roleLocator = matcher.getByRole(role, options)
|
|
92
|
-
return first ? roleLocator.first() : roleLocator.all()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function propBuilder(props) {
|
|
96
|
-
let _props = ''
|
|
97
|
-
|
|
98
|
-
for (const [key, value] of Object.entries(props)) {
|
|
99
|
-
if (typeof value === 'object') {
|
|
100
|
-
for (const [k, v] of Object.entries(value)) {
|
|
101
|
-
_props += `[${key}.${k} = "${v}"]`
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
_props += `[${key} = "${value}"]`
|
|
105
|
-
}
|
|
1
|
+
async function findByPlaywrightLocator(matcher, locator) {
|
|
2
|
+
const pwLocator = locator.locator || locator
|
|
3
|
+
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
|
|
4
|
+
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
106
5
|
}
|
|
107
|
-
|
|
6
|
+
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
|
|
7
|
+
return matcher.locator(pwValue).all()
|
|
108
8
|
}
|
|
109
9
|
|
|
110
|
-
export {
|
|
10
|
+
export { 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
|
}
|