codeceptjs 4.0.0-rc.8 → 4.0.0
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 +9 -10
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +751 -172
- 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 +743 -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 +198 -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 +7 -7
- 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 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -2
- package/lib/command/run-workers.js +1 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +11 -15
- package/lib/config.js +77 -4
- package/lib/container.js +97 -15
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +195 -3
- 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/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +96 -115
- package/lib/helper/Puppeteer.js +43 -131
- package/lib/helper/WebDriver.js +42 -52
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +10 -0
- 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/history.js +3 -2
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- 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 -16
- package/lib/mocha/cli.js +4 -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 +96 -103
- package/lib/plugin/analyze.js +9 -9
- 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 +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 +15 -13
- 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/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +29 -3
- package/lib/workers.js +14 -22
- package/package.json +17 -14
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- 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 -14
- 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 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- 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 -21
- 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 -16
- 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 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- 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 -26
- 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/plugin/stepByStepReport.js +0 -431
- package/lib/plugin/subtitles.js +0 -89
package/lib/helper/WebDriver.js
CHANGED
|
@@ -10,6 +10,7 @@ import promiseRetry from 'promise-retry'
|
|
|
10
10
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
11
11
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
12
12
|
import store from '../store.js'
|
|
13
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
13
14
|
import output from '../output.js'
|
|
14
15
|
const { debug } = output
|
|
15
16
|
import { empty } from '../assert/empty.js'
|
|
@@ -40,6 +41,8 @@ import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElem
|
|
|
40
41
|
import { dropFile } from './scripts/dropFile.js'
|
|
41
42
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
42
43
|
import WebElement from '../element/WebElement.js'
|
|
44
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
45
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
43
46
|
|
|
44
47
|
const SHADOW = 'shadow'
|
|
45
48
|
const webRoot = 'body'
|
|
@@ -94,11 +97,7 @@ const config = {}
|
|
|
94
97
|
* WebDriver helper which wraps [webdriverio](http://webdriver.io/) library to
|
|
95
98
|
* manipulate browser using Selenium WebDriver or PhantomJS.
|
|
96
99
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* With the release of WebdriverIO version v8.14.0, and onwards, all driver management hassles are now a thing of the past 🙌. Read more [here](https://webdriver.io/blog/2023/07/31/driver-management/).
|
|
100
|
-
* One of the significant advantages of this update is that you can now get rid of any driver services you previously had to manage, such as
|
|
101
|
-
* `wdio-chromedriver-service`, `wdio-geckodriver-service`, `wdio-edgedriver-service`, `wdio-safaridriver-service`, and even `@wdio/selenium-standalone-service`.
|
|
100
|
+
* No Selenium Server, ChromeDriver, or GeckoDriver to install or start. Since WebdriverIO 9, driver management is fully automatic — WebdriverIO downloads and starts the matching driver for you. Read more [here](https://webdriver.io/blog/2023/07/31/driver-management/). Please check [Testing with WebDriver](https://codecept.io/webdriver/#testing-with-webdriver) for more details.
|
|
102
101
|
*
|
|
103
102
|
* For those who require custom driver options, fear not; WebDriver Helper allows you to pass in driver options through custom WebDriver configuration.
|
|
104
103
|
* If you have a custom grid, use a cloud service, or prefer to run your own driver, there's no need to worry since WebDriver Helper will only start a driver when there are no other connection information settings like hostname or port specified.
|
|
@@ -905,13 +904,6 @@ class WebDriver extends Helper {
|
|
|
905
904
|
return els
|
|
906
905
|
}
|
|
907
906
|
|
|
908
|
-
// special locator type for React
|
|
909
|
-
if (locator.react) {
|
|
910
|
-
const els = await this.browser.react$$(locator.react, locator.props || undefined, locator.state || undefined)
|
|
911
|
-
this.debugSection('Elements', `Found ${els.length} react components`)
|
|
912
|
-
return els
|
|
913
|
-
}
|
|
914
|
-
|
|
915
907
|
// special locator type for ARIA roles
|
|
916
908
|
if (locator.role) {
|
|
917
909
|
return this._locateByRole(locator)
|
|
@@ -1081,7 +1073,6 @@ class WebDriver extends Helper {
|
|
|
1081
1073
|
/**
|
|
1082
1074
|
* {{> click }}
|
|
1083
1075
|
*
|
|
1084
|
-
* {{ react }}
|
|
1085
1076
|
*/
|
|
1086
1077
|
async click(locator, context = null) {
|
|
1087
1078
|
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
@@ -1093,8 +1084,7 @@ class WebDriver extends Helper {
|
|
|
1093
1084
|
} else {
|
|
1094
1085
|
assertElementExists(res, locator, 'Clickable element')
|
|
1095
1086
|
}
|
|
1096
|
-
|
|
1097
|
-
const elem = usingFirstElement(res)
|
|
1087
|
+
const elem = selectElement(res, locator, this)
|
|
1098
1088
|
highlightActiveElement.call(this, elem)
|
|
1099
1089
|
return this.browser[clickMethod](getElementId(elem))
|
|
1100
1090
|
}
|
|
@@ -1102,7 +1092,6 @@ class WebDriver extends Helper {
|
|
|
1102
1092
|
/**
|
|
1103
1093
|
* {{> forceClick }}
|
|
1104
1094
|
*
|
|
1105
|
-
* {{ react }}
|
|
1106
1095
|
*/
|
|
1107
1096
|
async forceClick(locator, context = null) {
|
|
1108
1097
|
const locateFn = prepareLocateFn.call(this, context)
|
|
@@ -1113,8 +1102,7 @@ class WebDriver extends Helper {
|
|
|
1113
1102
|
} else {
|
|
1114
1103
|
assertElementExists(res, locator, 'Clickable element')
|
|
1115
1104
|
}
|
|
1116
|
-
|
|
1117
|
-
const elem = usingFirstElement(res)
|
|
1105
|
+
const elem = selectElement(res, locator, this)
|
|
1118
1106
|
highlightActiveElement.call(this, elem)
|
|
1119
1107
|
|
|
1120
1108
|
return this.executeScript(el => {
|
|
@@ -1130,7 +1118,6 @@ class WebDriver extends Helper {
|
|
|
1130
1118
|
/**
|
|
1131
1119
|
* {{> doubleClick }}
|
|
1132
1120
|
*
|
|
1133
|
-
* {{ react }}
|
|
1134
1121
|
*/
|
|
1135
1122
|
async doubleClick(locator, context = null) {
|
|
1136
1123
|
const locateFn = prepareLocateFn.call(this, context)
|
|
@@ -1141,9 +1128,8 @@ class WebDriver extends Helper {
|
|
|
1141
1128
|
} else {
|
|
1142
1129
|
assertElementExists(res, locator, 'Clickable element')
|
|
1143
1130
|
}
|
|
1144
|
-
if (this.options.strict) assertOnlyOneElement(res, locator, this)
|
|
1145
1131
|
|
|
1146
|
-
const elem =
|
|
1132
|
+
const elem = selectElement(res, locator, this)
|
|
1147
1133
|
highlightActiveElement.call(this, elem)
|
|
1148
1134
|
return elem.doubleClick()
|
|
1149
1135
|
}
|
|
@@ -1151,7 +1137,6 @@ class WebDriver extends Helper {
|
|
|
1151
1137
|
/**
|
|
1152
1138
|
* {{> rightClick }}
|
|
1153
1139
|
*
|
|
1154
|
-
* {{ react }}
|
|
1155
1140
|
*/
|
|
1156
1141
|
async rightClick(locator, context) {
|
|
1157
1142
|
const locateFn = prepareLocateFn.call(this, context)
|
|
@@ -1162,9 +1147,8 @@ class WebDriver extends Helper {
|
|
|
1162
1147
|
} else {
|
|
1163
1148
|
assertElementExists(res, locator, 'Clickable element')
|
|
1164
1149
|
}
|
|
1165
|
-
if (this.options.strict) assertOnlyOneElement(res, locator, this)
|
|
1166
1150
|
|
|
1167
|
-
const el =
|
|
1151
|
+
const el = selectElement(res, locator, this)
|
|
1168
1152
|
|
|
1169
1153
|
await el.moveTo()
|
|
1170
1154
|
|
|
@@ -1247,7 +1231,6 @@ class WebDriver extends Helper {
|
|
|
1247
1231
|
/**
|
|
1248
1232
|
* {{> forceRightClick }}
|
|
1249
1233
|
*
|
|
1250
|
-
* {{ react }}
|
|
1251
1234
|
*/
|
|
1252
1235
|
async forceRightClick(locator, context = null) {
|
|
1253
1236
|
const locateFn = prepareLocateFn.call(this, context)
|
|
@@ -1272,16 +1255,19 @@ class WebDriver extends Helper {
|
|
|
1272
1255
|
|
|
1273
1256
|
/**
|
|
1274
1257
|
* {{> fillField }}
|
|
1275
|
-
* {{ react }}
|
|
1276
1258
|
* {{ custom }}
|
|
1277
1259
|
*
|
|
1278
1260
|
*/
|
|
1279
1261
|
async fillField(field, value, context = null) {
|
|
1280
1262
|
const res = await findFields.call(this, field, context)
|
|
1281
1263
|
assertElementExists(res, field, 'Field')
|
|
1282
|
-
|
|
1283
|
-
const elem = usingFirstElement(res)
|
|
1264
|
+
const elem = selectElement(res, field, this)
|
|
1284
1265
|
highlightActiveElement.call(this, elem)
|
|
1266
|
+
|
|
1267
|
+
if (await fillRichEditor(this, elem, value)) {
|
|
1268
|
+
return
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1285
1271
|
try {
|
|
1286
1272
|
await elem.clearValue()
|
|
1287
1273
|
} catch (err) {
|
|
@@ -1298,13 +1284,11 @@ class WebDriver extends Helper {
|
|
|
1298
1284
|
|
|
1299
1285
|
/**
|
|
1300
1286
|
* {{> appendField }}
|
|
1301
|
-
* {{ react }}
|
|
1302
1287
|
*/
|
|
1303
1288
|
async appendField(field, value, context = null) {
|
|
1304
1289
|
const res = await findFields.call(this, field, context)
|
|
1305
1290
|
assertElementExists(res, field, 'Field')
|
|
1306
|
-
|
|
1307
|
-
const elem = usingFirstElement(res)
|
|
1291
|
+
const elem = selectElement(res, field, this)
|
|
1308
1292
|
highlightActiveElement.call(this, elem)
|
|
1309
1293
|
return elem.addValue(value.toString())
|
|
1310
1294
|
}
|
|
@@ -1316,8 +1300,7 @@ class WebDriver extends Helper {
|
|
|
1316
1300
|
async clearField(field, context = null) {
|
|
1317
1301
|
const res = await findFields.call(this, field, context)
|
|
1318
1302
|
assertElementExists(res, field, 'Field')
|
|
1319
|
-
|
|
1320
|
-
const elem = usingFirstElement(res)
|
|
1303
|
+
const elem = selectElement(res, field, this)
|
|
1321
1304
|
highlightActiveElement.call(this, elem)
|
|
1322
1305
|
return elem.clearValue(getElementId(elem))
|
|
1323
1306
|
}
|
|
@@ -1334,22 +1317,22 @@ class WebDriver extends Helper {
|
|
|
1334
1317
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1335
1318
|
const els = await locateFn(select)
|
|
1336
1319
|
assertElementExists(els, select, 'Selectable element')
|
|
1337
|
-
return proceedSelectOption.call(this,
|
|
1320
|
+
return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1338
1321
|
}
|
|
1339
1322
|
|
|
1340
1323
|
// Fuzzy: try combobox
|
|
1341
1324
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1342
1325
|
let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
|
|
1343
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1326
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1344
1327
|
|
|
1345
1328
|
// Fuzzy: try listbox
|
|
1346
1329
|
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1347
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1330
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1348
1331
|
|
|
1349
1332
|
// Fuzzy: try native select
|
|
1350
1333
|
const res = await findFields.call(this, select, context)
|
|
1351
1334
|
assertElementExists(res, select, 'Selectable field')
|
|
1352
|
-
return proceedSelectOption.call(this,
|
|
1335
|
+
return proceedSelectOption.call(this, selectElement(res, select, this), option)
|
|
1353
1336
|
}
|
|
1354
1337
|
|
|
1355
1338
|
/**
|
|
@@ -1358,7 +1341,7 @@ class WebDriver extends Helper {
|
|
|
1358
1341
|
* {{> attachFile }}
|
|
1359
1342
|
*/
|
|
1360
1343
|
async attachFile(locator, pathToFile, context = null) {
|
|
1361
|
-
let file = path.join(
|
|
1344
|
+
let file = path.join(store.codeceptDir, pathToFile)
|
|
1362
1345
|
if (!fileExists(file)) {
|
|
1363
1346
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1364
1347
|
}
|
|
@@ -1367,7 +1350,7 @@ class WebDriver extends Helper {
|
|
|
1367
1350
|
this.debug(`Uploading ${file}`)
|
|
1368
1351
|
|
|
1369
1352
|
if (res.length) {
|
|
1370
|
-
const el =
|
|
1353
|
+
const el = selectElement(res, locator, this)
|
|
1371
1354
|
const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
|
|
1372
1355
|
const type = await this.browser.execute(function (elem) { return elem.type }, el)
|
|
1373
1356
|
if (tag === 'INPUT' && type === 'file') {
|
|
@@ -1385,7 +1368,7 @@ class WebDriver extends Helper {
|
|
|
1385
1368
|
|
|
1386
1369
|
const targetRes = res.length ? res : await this._locate(locator)
|
|
1387
1370
|
assertElementExists(targetRes, locator, 'Element')
|
|
1388
|
-
const targetEl =
|
|
1371
|
+
const targetEl = selectElement(targetRes, locator, this)
|
|
1389
1372
|
const fileData = {
|
|
1390
1373
|
base64Content: base64EncodeFile(file),
|
|
1391
1374
|
fileName: path.basename(file),
|
|
@@ -1405,7 +1388,7 @@ class WebDriver extends Helper {
|
|
|
1405
1388
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1406
1389
|
|
|
1407
1390
|
assertElementExists(res, field, 'Checkable')
|
|
1408
|
-
const elem =
|
|
1391
|
+
const elem = selectElement(res, field, this)
|
|
1409
1392
|
const elementId = getElementId(elem)
|
|
1410
1393
|
highlightActiveElement.call(this, elem)
|
|
1411
1394
|
|
|
@@ -1426,7 +1409,7 @@ class WebDriver extends Helper {
|
|
|
1426
1409
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1427
1410
|
|
|
1428
1411
|
assertElementExists(res, field, 'Checkable')
|
|
1429
|
-
const elem =
|
|
1412
|
+
const elem = selectElement(res, field, this)
|
|
1430
1413
|
const elementId = getElementId(elem)
|
|
1431
1414
|
highlightActiveElement.call(this, elem)
|
|
1432
1415
|
|
|
@@ -1598,7 +1581,6 @@ class WebDriver extends Helper {
|
|
|
1598
1581
|
/**
|
|
1599
1582
|
* {{> see }}
|
|
1600
1583
|
*
|
|
1601
|
-
* {{ react }}
|
|
1602
1584
|
*/
|
|
1603
1585
|
async see(text, context = null) {
|
|
1604
1586
|
return proceedSee.call(this, 'assert', text, context)
|
|
@@ -1614,7 +1596,6 @@ class WebDriver extends Helper {
|
|
|
1614
1596
|
/**
|
|
1615
1597
|
* {{> dontSee }}
|
|
1616
1598
|
*
|
|
1617
|
-
* {{ react }}
|
|
1618
1599
|
*/
|
|
1619
1600
|
async dontSee(text, context = null) {
|
|
1620
1601
|
return proceedSee.call(this, 'negate', text, context)
|
|
@@ -1656,7 +1637,6 @@ class WebDriver extends Helper {
|
|
|
1656
1637
|
|
|
1657
1638
|
/**
|
|
1658
1639
|
* {{> seeElement }}
|
|
1659
|
-
* {{ react }}
|
|
1660
1640
|
*
|
|
1661
1641
|
*/
|
|
1662
1642
|
async seeElement(locator, context = null) {
|
|
@@ -1673,7 +1653,6 @@ class WebDriver extends Helper {
|
|
|
1673
1653
|
|
|
1674
1654
|
/**
|
|
1675
1655
|
* {{> dontSeeElement }}
|
|
1676
|
-
* {{ react }}
|
|
1677
1656
|
*/
|
|
1678
1657
|
async dontSeeElement(locator, context = null) {
|
|
1679
1658
|
const locateFn = prepareLocateFn.call(this, context)
|
|
@@ -1758,7 +1737,6 @@ class WebDriver extends Helper {
|
|
|
1758
1737
|
|
|
1759
1738
|
/**
|
|
1760
1739
|
* {{> seeNumberOfElements }}
|
|
1761
|
-
* {{ react }}
|
|
1762
1740
|
*/
|
|
1763
1741
|
async seeNumberOfElements(locator, num) {
|
|
1764
1742
|
const res = await this._locate(locator)
|
|
@@ -1767,7 +1745,6 @@ class WebDriver extends Helper {
|
|
|
1767
1745
|
|
|
1768
1746
|
/**
|
|
1769
1747
|
* {{> seeNumberOfVisibleElements }}
|
|
1770
|
-
* {{ react }}
|
|
1771
1748
|
*/
|
|
1772
1749
|
async seeNumberOfVisibleElements(locator, num) {
|
|
1773
1750
|
const res = await this.grabNumberOfVisibleElements(locator)
|
|
@@ -2243,6 +2220,7 @@ class WebDriver extends Helper {
|
|
|
2243
2220
|
* {{> pressKeyWithKeyNormalization }}
|
|
2244
2221
|
*/
|
|
2245
2222
|
async pressKey(key) {
|
|
2223
|
+
await checkFocusBeforePressKey(this, key)
|
|
2246
2224
|
const modifiers = []
|
|
2247
2225
|
if (Array.isArray(key)) {
|
|
2248
2226
|
for (let k of key) {
|
|
@@ -2289,6 +2267,8 @@ class WebDriver extends Helper {
|
|
|
2289
2267
|
* {{> type }}
|
|
2290
2268
|
*/
|
|
2291
2269
|
async type(keys, delay = null) {
|
|
2270
|
+
await checkFocusBeforeType(this)
|
|
2271
|
+
|
|
2292
2272
|
if (!Array.isArray(keys)) {
|
|
2293
2273
|
keys = keys.toString()
|
|
2294
2274
|
keys = keys.split('')
|
|
@@ -3301,6 +3281,19 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3301
3281
|
}
|
|
3302
3282
|
|
|
3303
3283
|
function usingFirstElement(els) {
|
|
3284
|
+
const rawIndex = store.currentStep?.opts?.elementIndex
|
|
3285
|
+
if (rawIndex != null && els.length > 1) {
|
|
3286
|
+
let elementIndex = rawIndex
|
|
3287
|
+
if (elementIndex === 'first') elementIndex = 1
|
|
3288
|
+
if (elementIndex === 'last') elementIndex = -1
|
|
3289
|
+
if (Number.isInteger(elementIndex) && elementIndex !== 0) {
|
|
3290
|
+
const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
|
|
3291
|
+
if (idx >= 0 && idx < els.length) {
|
|
3292
|
+
debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
|
|
3293
|
+
return els[idx]
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3304
3297
|
if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
|
|
3305
3298
|
return els[0]
|
|
3306
3299
|
}
|
|
@@ -3473,7 +3466,7 @@ function isModifierKey(key) {
|
|
|
3473
3466
|
}
|
|
3474
3467
|
|
|
3475
3468
|
function highlightActiveElement(element) {
|
|
3476
|
-
if (this.options.highlightElement &&
|
|
3469
|
+
if (this.options.highlightElement && store.debugMode) {
|
|
3477
3470
|
highlightElement(element, this.browser)
|
|
3478
3471
|
}
|
|
3479
3472
|
}
|
|
@@ -3484,9 +3477,6 @@ function prepareLocateFn(context) {
|
|
|
3484
3477
|
l = new Locator(l, 'css')
|
|
3485
3478
|
return this._locate(context, true).then(async res => {
|
|
3486
3479
|
assertElementExists(res, context, 'Context element')
|
|
3487
|
-
if (l.react) {
|
|
3488
|
-
return res[0].react$$(l.react, l.props || undefined)
|
|
3489
|
-
}
|
|
3490
3480
|
return res[0].$$(l.simplify())
|
|
3491
3481
|
})
|
|
3492
3482
|
}
|
|
@@ -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 }
|
|
@@ -0,0 +1,10 @@
|
|
|
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])
|
|
5
|
+
}
|
|
6
|
+
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
|
|
7
|
+
return matcher.locator(pwValue).all()
|
|
8
|
+
}
|
|
9
|
+
|
|
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
|
+
}
|