codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -27
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1187 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +230 -0
- package/docs/continuous-integration.md +497 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +26 -23
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -0
- package/lib/command/run.js +1 -1
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +4 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +228 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -3
- package/lib/mocha/cli.js +14 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +453 -0
- package/lib/plugin/analyze.js +1 -1
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +53 -49
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +77 -3
- package/lib/workers.js +52 -22
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
package/lib/helper/WebDriver.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
let webdriverio
|
|
2
2
|
|
|
3
|
+
import fs from 'fs'
|
|
3
4
|
import assert from 'assert'
|
|
4
5
|
import path from 'path'
|
|
5
6
|
import crypto from 'crypto'
|
|
@@ -9,21 +10,39 @@ import promiseRetry from 'promise-retry'
|
|
|
9
10
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
10
11
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
11
12
|
import store from '../store.js'
|
|
13
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
12
14
|
import output from '../output.js'
|
|
13
15
|
const { debug } = output
|
|
14
16
|
import { empty } from '../assert/empty.js'
|
|
15
17
|
import { truth } from '../assert/truth.js'
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
xpathLocator,
|
|
20
|
+
fileExists,
|
|
21
|
+
decodeUrl,
|
|
22
|
+
chunkArray,
|
|
23
|
+
convertCssPropertiesToCamelCase,
|
|
24
|
+
screenshotOutputFolder,
|
|
25
|
+
getNormalizedKeyAttributeValue,
|
|
26
|
+
modifierKeys,
|
|
27
|
+
normalizePath,
|
|
28
|
+
resolveUrl,
|
|
29
|
+
getMimeType,
|
|
30
|
+
base64EncodeFile,
|
|
31
|
+
} from '../utils.js'
|
|
17
32
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
18
33
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
34
|
+
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
19
35
|
import ConnectionRefused from './errors/ConnectionRefused.js'
|
|
20
36
|
import Locator from '../locator.js'
|
|
21
37
|
import { highlightElement } from './scripts/highlightElement.js'
|
|
22
38
|
import { focusElement } from './scripts/focusElement.js'
|
|
23
39
|
import { blurElement } from './scripts/blurElement.js'
|
|
24
40
|
import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
|
|
41
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
25
42
|
import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
|
|
26
43
|
import WebElement from '../element/WebElement.js'
|
|
44
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
45
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
27
46
|
|
|
28
47
|
const SHADOW = 'shadow'
|
|
29
48
|
const webRoot = 'body'
|
|
@@ -489,6 +508,7 @@ class WebDriver extends Helper {
|
|
|
489
508
|
keepBrowserState: false,
|
|
490
509
|
deprecationWarnings: false,
|
|
491
510
|
highlightElement: false,
|
|
511
|
+
strict: false,
|
|
492
512
|
}
|
|
493
513
|
|
|
494
514
|
// override defaults with config
|
|
@@ -1076,7 +1096,7 @@ class WebDriver extends Helper {
|
|
|
1076
1096
|
} else {
|
|
1077
1097
|
assertElementExists(res, locator, 'Clickable element')
|
|
1078
1098
|
}
|
|
1079
|
-
const elem =
|
|
1099
|
+
const elem = selectElement(res, locator, this)
|
|
1080
1100
|
highlightActiveElement.call(this, elem)
|
|
1081
1101
|
return this.browser[clickMethod](getElementId(elem))
|
|
1082
1102
|
}
|
|
@@ -1095,7 +1115,7 @@ class WebDriver extends Helper {
|
|
|
1095
1115
|
} else {
|
|
1096
1116
|
assertElementExists(res, locator, 'Clickable element')
|
|
1097
1117
|
}
|
|
1098
|
-
const elem =
|
|
1118
|
+
const elem = selectElement(res, locator, this)
|
|
1099
1119
|
highlightActiveElement.call(this, elem)
|
|
1100
1120
|
|
|
1101
1121
|
return this.executeScript(el => {
|
|
@@ -1123,7 +1143,7 @@ class WebDriver extends Helper {
|
|
|
1123
1143
|
assertElementExists(res, locator, 'Clickable element')
|
|
1124
1144
|
}
|
|
1125
1145
|
|
|
1126
|
-
const elem =
|
|
1146
|
+
const elem = selectElement(res, locator, this)
|
|
1127
1147
|
highlightActiveElement.call(this, elem)
|
|
1128
1148
|
return elem.doubleClick()
|
|
1129
1149
|
}
|
|
@@ -1143,7 +1163,7 @@ class WebDriver extends Helper {
|
|
|
1143
1163
|
assertElementExists(res, locator, 'Clickable element')
|
|
1144
1164
|
}
|
|
1145
1165
|
|
|
1146
|
-
const el =
|
|
1166
|
+
const el = selectElement(res, locator, this)
|
|
1147
1167
|
|
|
1148
1168
|
await el.moveTo()
|
|
1149
1169
|
|
|
@@ -1255,11 +1275,16 @@ class WebDriver extends Helper {
|
|
|
1255
1275
|
* {{ custom }}
|
|
1256
1276
|
*
|
|
1257
1277
|
*/
|
|
1258
|
-
async fillField(field, value) {
|
|
1259
|
-
const res = await findFields.call(this, field)
|
|
1278
|
+
async fillField(field, value, context = null) {
|
|
1279
|
+
const res = await findFields.call(this, field, context)
|
|
1260
1280
|
assertElementExists(res, field, 'Field')
|
|
1261
|
-
const elem =
|
|
1281
|
+
const elem = selectElement(res, field, this)
|
|
1262
1282
|
highlightActiveElement.call(this, elem)
|
|
1283
|
+
|
|
1284
|
+
if (await fillRichEditor(this, elem, value)) {
|
|
1285
|
+
return
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1263
1288
|
try {
|
|
1264
1289
|
await elem.clearValue()
|
|
1265
1290
|
} catch (err) {
|
|
@@ -1278,10 +1303,10 @@ class WebDriver extends Helper {
|
|
|
1278
1303
|
* {{> appendField }}
|
|
1279
1304
|
* {{ react }}
|
|
1280
1305
|
*/
|
|
1281
|
-
async appendField(field, value) {
|
|
1282
|
-
const res = await findFields.call(this, field)
|
|
1306
|
+
async appendField(field, value, context = null) {
|
|
1307
|
+
const res = await findFields.call(this, field, context)
|
|
1283
1308
|
assertElementExists(res, field, 'Field')
|
|
1284
|
-
const elem =
|
|
1309
|
+
const elem = selectElement(res, field, this)
|
|
1285
1310
|
highlightActiveElement.call(this, elem)
|
|
1286
1311
|
return elem.addValue(value.toString())
|
|
1287
1312
|
}
|
|
@@ -1290,10 +1315,10 @@ class WebDriver extends Helper {
|
|
|
1290
1315
|
* {{> clearField }}
|
|
1291
1316
|
*
|
|
1292
1317
|
*/
|
|
1293
|
-
async clearField(field) {
|
|
1294
|
-
const res = await findFields.call(this, field)
|
|
1318
|
+
async clearField(field, context = null) {
|
|
1319
|
+
const res = await findFields.call(this, field, context)
|
|
1295
1320
|
assertElementExists(res, field, 'Field')
|
|
1296
|
-
const elem =
|
|
1321
|
+
const elem = selectElement(res, field, this)
|
|
1297
1322
|
highlightActiveElement.call(this, elem)
|
|
1298
1323
|
return elem.clearValue(getElementId(elem))
|
|
1299
1324
|
}
|
|
@@ -1301,30 +1326,31 @@ class WebDriver extends Helper {
|
|
|
1301
1326
|
/**
|
|
1302
1327
|
* {{> selectOption }}
|
|
1303
1328
|
*/
|
|
1304
|
-
async selectOption(select, option) {
|
|
1329
|
+
async selectOption(select, option, context = null) {
|
|
1330
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1305
1331
|
const matchedLocator = new Locator(select)
|
|
1306
1332
|
|
|
1307
1333
|
// Strict locator
|
|
1308
1334
|
if (!matchedLocator.isFuzzy()) {
|
|
1309
1335
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
1310
|
-
const els = await
|
|
1336
|
+
const els = await locateFn(select)
|
|
1311
1337
|
assertElementExists(els, select, 'Selectable element')
|
|
1312
|
-
return proceedSelectOption.call(this,
|
|
1338
|
+
return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1313
1339
|
}
|
|
1314
1340
|
|
|
1315
1341
|
// Fuzzy: try combobox
|
|
1316
1342
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
1317
1343
|
let els = await this._locateByRole({ role: 'combobox', text: matchedLocator.value })
|
|
1318
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1344
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1319
1345
|
|
|
1320
1346
|
// Fuzzy: try listbox
|
|
1321
1347
|
els = await this._locateByRole({ role: 'listbox', text: matchedLocator.value })
|
|
1322
|
-
if (els?.length) return proceedSelectOption.call(this,
|
|
1348
|
+
if (els?.length) return proceedSelectOption.call(this, selectElement(els, select, this), option)
|
|
1323
1349
|
|
|
1324
1350
|
// Fuzzy: try native select
|
|
1325
|
-
const res = await findFields.call(this, select)
|
|
1351
|
+
const res = await findFields.call(this, select, context)
|
|
1326
1352
|
assertElementExists(res, select, 'Selectable field')
|
|
1327
|
-
return proceedSelectOption.call(this,
|
|
1353
|
+
return proceedSelectOption.call(this, selectElement(res, select, this), option)
|
|
1328
1354
|
}
|
|
1329
1355
|
|
|
1330
1356
|
/**
|
|
@@ -1332,28 +1358,41 @@ class WebDriver extends Helper {
|
|
|
1332
1358
|
*
|
|
1333
1359
|
* {{> attachFile }}
|
|
1334
1360
|
*/
|
|
1335
|
-
async attachFile(locator, pathToFile) {
|
|
1336
|
-
let file = path.join(
|
|
1361
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
1362
|
+
let file = path.join(store.codeceptDir, pathToFile)
|
|
1337
1363
|
if (!fileExists(file)) {
|
|
1338
1364
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
1339
1365
|
}
|
|
1340
1366
|
|
|
1341
|
-
const res = await findFields.call(this, locator)
|
|
1367
|
+
const res = await findFields.call(this, locator, context)
|
|
1342
1368
|
this.debug(`Uploading ${file}`)
|
|
1343
|
-
assertElementExists(res, locator, 'File field')
|
|
1344
|
-
const el = usingFirstElement(res)
|
|
1345
1369
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1370
|
+
if (res.length) {
|
|
1371
|
+
const el = selectElement(res, locator, this)
|
|
1372
|
+
const tag = await this.browser.execute(function (elem) { return elem.tagName }, el)
|
|
1373
|
+
const type = await this.browser.execute(function (elem) { return elem.type }, el)
|
|
1374
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
1375
|
+
if (this.options.remoteFileUpload) {
|
|
1376
|
+
try {
|
|
1377
|
+
this.debugSection('File', 'Uploading file to remote server')
|
|
1378
|
+
file = await this.browser.uploadFile(file)
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return el.addValue(file)
|
|
1353
1384
|
}
|
|
1354
1385
|
}
|
|
1355
1386
|
|
|
1356
|
-
|
|
1387
|
+
const targetRes = res.length ? res : await this._locate(locator)
|
|
1388
|
+
assertElementExists(targetRes, locator, 'Element')
|
|
1389
|
+
const targetEl = selectElement(targetRes, locator, this)
|
|
1390
|
+
const fileData = {
|
|
1391
|
+
base64Content: base64EncodeFile(file),
|
|
1392
|
+
fileName: path.basename(file),
|
|
1393
|
+
mimeType: getMimeType(path.basename(file)),
|
|
1394
|
+
}
|
|
1395
|
+
return this.browser.execute(dropFile, targetEl, fileData)
|
|
1357
1396
|
}
|
|
1358
1397
|
|
|
1359
1398
|
/**
|
|
@@ -1367,7 +1406,7 @@ class WebDriver extends Helper {
|
|
|
1367
1406
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1368
1407
|
|
|
1369
1408
|
assertElementExists(res, field, 'Checkable')
|
|
1370
|
-
const elem =
|
|
1409
|
+
const elem = selectElement(res, field, this)
|
|
1371
1410
|
const elementId = getElementId(elem)
|
|
1372
1411
|
highlightActiveElement.call(this, elem)
|
|
1373
1412
|
|
|
@@ -1388,7 +1427,7 @@ class WebDriver extends Helper {
|
|
|
1388
1427
|
const res = await findCheckable.call(this, field, locateFn)
|
|
1389
1428
|
|
|
1390
1429
|
assertElementExists(res, field, 'Checkable')
|
|
1391
|
-
const elem =
|
|
1430
|
+
const elem = selectElement(res, field, this)
|
|
1392
1431
|
const elementId = getElementId(elem)
|
|
1393
1432
|
highlightActiveElement.call(this, elem)
|
|
1394
1433
|
|
|
@@ -1586,18 +1625,18 @@ class WebDriver extends Helper {
|
|
|
1586
1625
|
* {{> seeInField }}
|
|
1587
1626
|
*
|
|
1588
1627
|
*/
|
|
1589
|
-
async seeInField(field, value) {
|
|
1628
|
+
async seeInField(field, value, context = null) {
|
|
1590
1629
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1591
|
-
return proceedSeeField.call(this, 'assert', field, _value)
|
|
1630
|
+
return proceedSeeField.call(this, 'assert', field, _value, context)
|
|
1592
1631
|
}
|
|
1593
1632
|
|
|
1594
1633
|
/**
|
|
1595
1634
|
* {{> dontSeeInField }}
|
|
1596
1635
|
*
|
|
1597
1636
|
*/
|
|
1598
|
-
async dontSeeInField(field, value) {
|
|
1637
|
+
async dontSeeInField(field, value, context = null) {
|
|
1599
1638
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
1600
|
-
return proceedSeeField.call(this, 'negate', field, _value)
|
|
1639
|
+
return proceedSeeField.call(this, 'negate', field, _value, context)
|
|
1601
1640
|
}
|
|
1602
1641
|
|
|
1603
1642
|
/**
|
|
@@ -1621,8 +1660,9 @@ class WebDriver extends Helper {
|
|
|
1621
1660
|
* {{ react }}
|
|
1622
1661
|
*
|
|
1623
1662
|
*/
|
|
1624
|
-
async seeElement(locator) {
|
|
1625
|
-
const
|
|
1663
|
+
async seeElement(locator, context = null) {
|
|
1664
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1665
|
+
const res = context ? await locateFn(locator) : await this._locate(locator, true)
|
|
1626
1666
|
assertElementExists(res, locator)
|
|
1627
1667
|
const selected = await forEachAsync(res, async el => el.isDisplayed())
|
|
1628
1668
|
try {
|
|
@@ -1636,8 +1676,9 @@ class WebDriver extends Helper {
|
|
|
1636
1676
|
* {{> dontSeeElement }}
|
|
1637
1677
|
* {{ react }}
|
|
1638
1678
|
*/
|
|
1639
|
-
async dontSeeElement(locator) {
|
|
1640
|
-
const
|
|
1679
|
+
async dontSeeElement(locator, context = null) {
|
|
1680
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
1681
|
+
const res = context ? await locateFn(locator) : await this._locate(locator, false)
|
|
1641
1682
|
if (!res || res.length === 0) {
|
|
1642
1683
|
return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false)
|
|
1643
1684
|
}
|
|
@@ -1851,7 +1892,7 @@ class WebDriver extends Helper {
|
|
|
1851
1892
|
const currentUrl = await this.browser.getUrl()
|
|
1852
1893
|
const baseUrl = this.options.url || 'http://localhost'
|
|
1853
1894
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1854
|
-
return equals('url path').assert(path, actualPath)
|
|
1895
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
1855
1896
|
}
|
|
1856
1897
|
|
|
1857
1898
|
/**
|
|
@@ -1861,7 +1902,7 @@ class WebDriver extends Helper {
|
|
|
1861
1902
|
const currentUrl = await this.browser.getUrl()
|
|
1862
1903
|
const baseUrl = this.options.url || 'http://localhost'
|
|
1863
1904
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
1864
|
-
return equals('url path').negate(path, actualPath)
|
|
1905
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
1865
1906
|
}
|
|
1866
1907
|
|
|
1867
1908
|
/**
|
|
@@ -1936,8 +1977,22 @@ class WebDriver extends Helper {
|
|
|
1936
1977
|
* {{> moveCursorTo }}
|
|
1937
1978
|
*/
|
|
1938
1979
|
async moveCursorTo(locator, xOffset, yOffset) {
|
|
1939
|
-
|
|
1940
|
-
|
|
1980
|
+
let context = null
|
|
1981
|
+
if (typeof xOffset !== 'number' && xOffset !== undefined) {
|
|
1982
|
+
context = xOffset
|
|
1983
|
+
xOffset = undefined
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
let res
|
|
1987
|
+
if (context) {
|
|
1988
|
+
const contextRes = await this._locate(withStrictLocator(context), true)
|
|
1989
|
+
assertElementExists(contextRes, context, 'Context element')
|
|
1990
|
+
res = await contextRes[0].$$(withStrictLocator(locator))
|
|
1991
|
+
assertElementExists(res, locator)
|
|
1992
|
+
} else {
|
|
1993
|
+
res = await this._locate(withStrictLocator(locator), true)
|
|
1994
|
+
assertElementExists(res, locator)
|
|
1995
|
+
}
|
|
1941
1996
|
const elem = usingFirstElement(res)
|
|
1942
1997
|
try {
|
|
1943
1998
|
await elem.moveTo({ xOffset, yOffset })
|
|
@@ -2189,6 +2244,7 @@ class WebDriver extends Helper {
|
|
|
2189
2244
|
* {{> pressKeyWithKeyNormalization }}
|
|
2190
2245
|
*/
|
|
2191
2246
|
async pressKey(key) {
|
|
2247
|
+
await checkFocusBeforePressKey(this, key)
|
|
2192
2248
|
const modifiers = []
|
|
2193
2249
|
if (Array.isArray(key)) {
|
|
2194
2250
|
for (let k of key) {
|
|
@@ -2235,6 +2291,8 @@ class WebDriver extends Helper {
|
|
|
2235
2291
|
* {{> type }}
|
|
2236
2292
|
*/
|
|
2237
2293
|
async type(keys, delay = null) {
|
|
2294
|
+
await checkFocusBeforeType(this)
|
|
2295
|
+
|
|
2238
2296
|
if (!Array.isArray(keys)) {
|
|
2239
2297
|
keys = keys.toString()
|
|
2240
2298
|
keys = keys.split('')
|
|
@@ -2487,6 +2545,7 @@ class WebDriver extends Helper {
|
|
|
2487
2545
|
async waitInUrl(urlPart, sec = null) {
|
|
2488
2546
|
const client = this.browser
|
|
2489
2547
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2548
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2490
2549
|
let currUrl = ''
|
|
2491
2550
|
|
|
2492
2551
|
return client
|
|
@@ -2494,7 +2553,7 @@ class WebDriver extends Helper {
|
|
|
2494
2553
|
function () {
|
|
2495
2554
|
return this.getUrl().then(res => {
|
|
2496
2555
|
currUrl = decodeUrl(res)
|
|
2497
|
-
return currUrl.indexOf(
|
|
2556
|
+
return currUrl.indexOf(expectedUrl) > -1
|
|
2498
2557
|
})
|
|
2499
2558
|
},
|
|
2500
2559
|
{ timeout: aSec * 1000 },
|
|
@@ -2502,7 +2561,7 @@ class WebDriver extends Helper {
|
|
|
2502
2561
|
.catch(e => {
|
|
2503
2562
|
e = wrapError(e)
|
|
2504
2563
|
if (e.message.indexOf('timeout')) {
|
|
2505
|
-
throw new Error(`expected url to include ${
|
|
2564
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
2506
2565
|
}
|
|
2507
2566
|
throw e
|
|
2508
2567
|
})
|
|
@@ -2513,22 +2572,47 @@ class WebDriver extends Helper {
|
|
|
2513
2572
|
*/
|
|
2514
2573
|
async waitUrlEquals(urlPart, sec = null) {
|
|
2515
2574
|
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2516
|
-
const
|
|
2517
|
-
if (urlPart.indexOf('http') < 0) {
|
|
2518
|
-
urlPart = baseUrl + urlPart
|
|
2519
|
-
}
|
|
2575
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
2520
2576
|
let currUrl = ''
|
|
2521
2577
|
return this.browser
|
|
2522
2578
|
.waitUntil(function () {
|
|
2523
2579
|
return this.getUrl().then(res => {
|
|
2524
2580
|
currUrl = decodeUrl(res)
|
|
2525
|
-
return currUrl ===
|
|
2581
|
+
return currUrl === expectedUrl
|
|
2526
2582
|
})
|
|
2527
2583
|
}, aSec * 1000)
|
|
2528
2584
|
.catch(e => {
|
|
2529
2585
|
e = wrapError(e)
|
|
2530
2586
|
if (e.message.indexOf('timeout')) {
|
|
2531
|
-
throw new Error(`expected url to be ${
|
|
2587
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
2588
|
+
}
|
|
2589
|
+
throw e
|
|
2590
|
+
})
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/**
|
|
2594
|
+
* {{> waitCurrentPathEquals }}
|
|
2595
|
+
*/
|
|
2596
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
2597
|
+
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
2598
|
+
const normalizedPath = normalizePath(path)
|
|
2599
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2600
|
+
let actualPath = ''
|
|
2601
|
+
|
|
2602
|
+
return this.browser
|
|
2603
|
+
.waitUntil(
|
|
2604
|
+
async () => {
|
|
2605
|
+
const currUrl = await this.browser.getUrl()
|
|
2606
|
+
const url = new URL(currUrl, baseUrl)
|
|
2607
|
+
actualPath = url.pathname
|
|
2608
|
+
return normalizePath(actualPath) === normalizedPath
|
|
2609
|
+
},
|
|
2610
|
+
{ timeout: aSec * 1000 },
|
|
2611
|
+
)
|
|
2612
|
+
.catch(e => {
|
|
2613
|
+
e = wrapError(e)
|
|
2614
|
+
if (e.message.indexOf('timeout')) {
|
|
2615
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
2532
2616
|
}
|
|
2533
2617
|
throw e
|
|
2534
2618
|
})
|
|
@@ -3010,32 +3094,33 @@ async function findClickable(locator, locateFn) {
|
|
|
3010
3094
|
return await locateFn(locator.value) // by css or xpath
|
|
3011
3095
|
}
|
|
3012
3096
|
|
|
3013
|
-
async function findFields(locator) {
|
|
3097
|
+
async function findFields(locator, context = null) {
|
|
3098
|
+
const locateFn = prepareLocateFn.call(this, context)
|
|
3014
3099
|
locator = new Locator(locator)
|
|
3015
3100
|
|
|
3016
3101
|
if (this._isCustomLocator(locator)) {
|
|
3017
|
-
return
|
|
3102
|
+
return locateFn(locator)
|
|
3018
3103
|
}
|
|
3019
3104
|
|
|
3020
|
-
if (locator.isAccessibilityId() && !this.isWeb) return
|
|
3021
|
-
if (locator.isRole()) return
|
|
3022
|
-
if (!locator.isFuzzy()) return
|
|
3105
|
+
if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator)
|
|
3106
|
+
if (locator.isRole()) return locateFn(locator)
|
|
3107
|
+
if (!locator.isFuzzy()) return locateFn(locator)
|
|
3023
3108
|
|
|
3024
3109
|
const literal = xpathLocator.literal(locator.value)
|
|
3025
|
-
let els = await
|
|
3110
|
+
let els = await locateFn(Locator.field.labelEquals(literal))
|
|
3026
3111
|
if (els.length) return els
|
|
3027
3112
|
|
|
3028
|
-
els = await
|
|
3113
|
+
els = await locateFn(Locator.field.labelContains(literal))
|
|
3029
3114
|
if (els.length) return els
|
|
3030
3115
|
|
|
3031
|
-
els = await
|
|
3116
|
+
els = await locateFn(Locator.field.byName(literal))
|
|
3032
3117
|
if (els.length) return els
|
|
3033
3118
|
|
|
3034
|
-
return await
|
|
3119
|
+
return await locateFn(locator.value) // by css or xpath
|
|
3035
3120
|
}
|
|
3036
3121
|
|
|
3037
|
-
async function proceedSeeField(assertType, field, value) {
|
|
3038
|
-
const res = await findFields.call(this, field)
|
|
3122
|
+
async function proceedSeeField(assertType, field, value, context) {
|
|
3123
|
+
const res = await findFields.call(this, field, context)
|
|
3039
3124
|
assertElementExists(res, field, 'Field')
|
|
3040
3125
|
const elem = usingFirstElement(res)
|
|
3041
3126
|
const elemId = getElementId(elem)
|
|
@@ -3220,10 +3305,30 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
3220
3305
|
}
|
|
3221
3306
|
|
|
3222
3307
|
function usingFirstElement(els) {
|
|
3308
|
+
const rawIndex = store.currentStep?.opts?.elementIndex
|
|
3309
|
+
if (rawIndex != null && els.length > 1) {
|
|
3310
|
+
let elementIndex = rawIndex
|
|
3311
|
+
if (elementIndex === 'first') elementIndex = 1
|
|
3312
|
+
if (elementIndex === 'last') elementIndex = -1
|
|
3313
|
+
if (Number.isInteger(elementIndex) && elementIndex !== 0) {
|
|
3314
|
+
const idx = elementIndex > 0 ? elementIndex - 1 : els.length + elementIndex
|
|
3315
|
+
if (idx >= 0 && idx < els.length) {
|
|
3316
|
+
debug(`[Elements] Using element #${rawIndex} out of ${els.length}`)
|
|
3317
|
+
return els[idx]
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3223
3321
|
if (els.length > 1) debug(`[Elements] Using first element out of ${els.length}`)
|
|
3224
3322
|
return els[0]
|
|
3225
3323
|
}
|
|
3226
3324
|
|
|
3325
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
3326
|
+
if (elements.length > 1) {
|
|
3327
|
+
const webElements = Array.from(elements).map(el => new WebElement(el, helper))
|
|
3328
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3227
3332
|
function getElementId(el) {
|
|
3228
3333
|
// W3C WebDriver web element identifier
|
|
3229
3334
|
// https://w3c.github.io/webdriver/#dfn-web-element-identifier
|
|
@@ -3385,7 +3490,7 @@ function isModifierKey(key) {
|
|
|
3385
3490
|
}
|
|
3386
3491
|
|
|
3387
3492
|
function highlightActiveElement(element) {
|
|
3388
|
-
if (this.options.highlightElement &&
|
|
3493
|
+
if (this.options.highlightElement && store.debugMode) {
|
|
3389
3494
|
highlightElement(element, this.browser)
|
|
3390
3495
|
}
|
|
3391
3496
|
}
|
|
@@ -1,40 +1,45 @@
|
|
|
1
1
|
import Locator from '../../locator.js'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Error thrown when strict mode is enabled and multiple elements are found
|
|
5
|
-
* for a single-element locator operation (click, fillField, etc.)
|
|
6
|
-
*/
|
|
7
3
|
class MultipleElementsFound extends Error {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
super(`Multiple elements (${elements.length}) found for "${locator}". Call fetchDetails() for full information.`)
|
|
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.`)
|
|
14
9
|
this.name = 'MultipleElementsFound'
|
|
15
10
|
this.locator = locator
|
|
16
|
-
this.
|
|
17
|
-
this.count =
|
|
11
|
+
this.webElements = webElements
|
|
12
|
+
this.count = webElements.length
|
|
18
13
|
this._detailsFetched = false
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
/**
|
|
22
|
-
* Fetch detailed information about the found elements asynchronously
|
|
23
|
-
* This updates the error message with XPath and element previews
|
|
24
|
-
*/
|
|
25
16
|
async fetchDetails() {
|
|
26
17
|
if (this._detailsFetched) return
|
|
27
18
|
|
|
28
19
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
if (this.count > 10) {
|
|
35
|
+
items.push(` ... and ${this.count - 10} more`)
|
|
36
|
+
}
|
|
35
37
|
|
|
36
|
-
this.
|
|
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') +
|
|
38
43
|
`\nUse a more specific locator or use grabWebElements() to handle multiple elements.`
|
|
39
44
|
} catch (err) {
|
|
40
45
|
this.message = `Multiple elements (${this.count}) found. Failed to fetch details: ${err.message}`
|
|
@@ -42,94 +47,6 @@ class MultipleElementsFound extends Error {
|
|
|
42
47
|
|
|
43
48
|
this._detailsFetched = true
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Generate a formatted list of found elements with their XPath and preview
|
|
48
|
-
* @param {Array<HTMLElement>} elements
|
|
49
|
-
* @param {number} count
|
|
50
|
-
* @returns {Promise<string>}
|
|
51
|
-
*/
|
|
52
|
-
async _generateElementList(elements, count) {
|
|
53
|
-
const items = []
|
|
54
|
-
const maxToShow = Math.min(count, 10)
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < maxToShow; i++) {
|
|
57
|
-
const el = elements[i]
|
|
58
|
-
try {
|
|
59
|
-
const info = await this._getElementInfo(el)
|
|
60
|
-
items.push(` ${i + 1}. ${info.xpath} (${info.preview})`)
|
|
61
|
-
} catch (err) {
|
|
62
|
-
// Element might be detached or inaccessible
|
|
63
|
-
items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (count > 10) {
|
|
68
|
-
items.push(` ... and ${count - 10} more`)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return items.join('\n')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get XPath and preview for an element by running JavaScript in browser context
|
|
76
|
-
* @param {HTMLElement} element
|
|
77
|
-
* @returns {Promise<{xpath: string, preview: string}>}
|
|
78
|
-
*/
|
|
79
|
-
async _getElementInfo(element) {
|
|
80
|
-
return element.evaluate((el) => {
|
|
81
|
-
// Generate a unique XPath for this element
|
|
82
|
-
const getUniqueXPath = (element) => {
|
|
83
|
-
if (element.id) {
|
|
84
|
-
return `//*[@id="${element.id}"]`
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const parts = []
|
|
88
|
-
let current = element
|
|
89
|
-
|
|
90
|
-
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
91
|
-
let index = 0
|
|
92
|
-
let sibling = current.previousSibling
|
|
93
|
-
|
|
94
|
-
while (sibling) {
|
|
95
|
-
if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
|
|
96
|
-
index++
|
|
97
|
-
}
|
|
98
|
-
sibling = sibling.previousSibling
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const tagName = current.tagName.toLowerCase()
|
|
102
|
-
const pathIndex = index > 0 ? `[${index + 1}]` : ''
|
|
103
|
-
parts.unshift(`${tagName}${pathIndex}`)
|
|
104
|
-
|
|
105
|
-
current = current.parentElement
|
|
106
|
-
|
|
107
|
-
// Stop at body to keep XPath reasonable
|
|
108
|
-
if (current && current.tagName === 'BODY') {
|
|
109
|
-
parts.unshift('body')
|
|
110
|
-
break
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return '/' + parts.join('/')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Get a preview of the element (tag, classes, id)
|
|
118
|
-
const getPreview = (element) => {
|
|
119
|
-
const tag = element.tagName.toLowerCase()
|
|
120
|
-
const id = element.id ? `#${element.id}` : ''
|
|
121
|
-
const classes = element.className
|
|
122
|
-
? '.' + element.className.split(' ').filter(c => c).join('.')
|
|
123
|
-
: ''
|
|
124
|
-
return `${tag}${id}${classes || ''}`
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
xpath: getUniqueXPath(el),
|
|
129
|
-
preview: getPreview(el),
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
50
|
}
|
|
134
51
|
|
|
135
52
|
export default MultipleElementsFound
|