codeceptjs 4.0.2-beta.8 → 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 +91 -37
- 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
package/lib/helper/Playwright.js
CHANGED
|
@@ -7,6 +7,7 @@ import promiseRetry from 'promise-retry'
|
|
|
7
7
|
import Locator from '../locator.js'
|
|
8
8
|
import recorder from '../recorder.js'
|
|
9
9
|
import store from '../store.js'
|
|
10
|
+
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
10
11
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
11
12
|
import { urlEquals, equals } from '../assert/equal.js'
|
|
12
13
|
import { empty } from '../assert/empty.js'
|
|
@@ -23,56 +24,28 @@ import {
|
|
|
23
24
|
clearString,
|
|
24
25
|
requireWithFallback,
|
|
25
26
|
normalizeSpacesInString,
|
|
27
|
+
normalizePath,
|
|
28
|
+
resolveUrl,
|
|
26
29
|
relativeDir,
|
|
30
|
+
getMimeType,
|
|
31
|
+
base64EncodeFile,
|
|
27
32
|
} from '../utils.js'
|
|
28
33
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
29
34
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
35
|
+
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
30
36
|
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
|
|
31
37
|
import Popup from './extras/Popup.js'
|
|
32
38
|
import Console from './extras/Console.js'
|
|
33
|
-
import {
|
|
39
|
+
import { findByPlaywrightLocator } from './extras/PlaywrightLocator.js'
|
|
40
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
34
41
|
import WebElement from '../element/WebElement.js'
|
|
42
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
43
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
35
44
|
|
|
36
45
|
let playwright
|
|
37
46
|
let perfTiming
|
|
38
47
|
let defaultSelectorEnginesInitialized = false
|
|
39
|
-
let registeredCustomLocatorStrategies = new Set()
|
|
40
|
-
let globalCustomLocatorStrategies = new Map()
|
|
41
48
|
|
|
42
|
-
// Use global object to track selector registration across workers
|
|
43
|
-
if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
44
|
-
global.__playwrightSelectorsRegistered = false
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Creates a Playwright selector engine factory for a custom locator strategy.
|
|
49
|
-
* @param {string} name - Strategy name for error messages
|
|
50
|
-
* @param {Function} func - The locator function (selector, root) => Element|Element[]
|
|
51
|
-
* @returns {Function} Selector engine factory
|
|
52
|
-
*/
|
|
53
|
-
function createCustomSelectorEngine(name, func) {
|
|
54
|
-
return () => ({
|
|
55
|
-
create: () => null,
|
|
56
|
-
query(root, selector) {
|
|
57
|
-
if (!root) return null
|
|
58
|
-
try {
|
|
59
|
-
const result = func(selector, root)
|
|
60
|
-
return Array.isArray(result) ? result[0] : result
|
|
61
|
-
} catch (e) {
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
queryAll(root, selector) {
|
|
66
|
-
if (!root) return []
|
|
67
|
-
try {
|
|
68
|
-
const result = func(selector, root)
|
|
69
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
70
|
-
} catch (e) {
|
|
71
|
-
return []
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
49
|
|
|
77
50
|
const popupStore = new Popup()
|
|
78
51
|
const consoleLogStore = new Console()
|
|
@@ -130,7 +103,6 @@ const pathSeparator = path.sep
|
|
|
130
103
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
131
104
|
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
132
105
|
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
133
|
-
* @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(`[role="${selector}"]`) } }`
|
|
134
106
|
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
|
|
135
107
|
* passed directly to `browser.newContext`.
|
|
136
108
|
* If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
|
|
@@ -386,18 +358,6 @@ class Playwright extends Helper {
|
|
|
386
358
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
387
359
|
this.cdpSession = null
|
|
388
360
|
|
|
389
|
-
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
390
|
-
// This can happen in worker threads where config is serialized/deserialized
|
|
391
|
-
this.customLocatorStrategies = this._parseCustomLocatorStrategies(config.customLocatorStrategies)
|
|
392
|
-
this._customLocatorsRegistered = false
|
|
393
|
-
|
|
394
|
-
// Add custom locator strategies to global registry for early registration
|
|
395
|
-
if (this.customLocatorStrategies) {
|
|
396
|
-
for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
|
|
397
|
-
globalCustomLocatorStrategies.set(name, func)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
361
|
// Add test failure tracking to prevent false positives
|
|
402
362
|
this.testFailures = []
|
|
403
363
|
this.hasCleanupError = false
|
|
@@ -437,6 +397,7 @@ class Playwright extends Helper {
|
|
|
437
397
|
highlightElement: false,
|
|
438
398
|
storageState: undefined,
|
|
439
399
|
onResponse: null,
|
|
400
|
+
strict: false,
|
|
440
401
|
}
|
|
441
402
|
|
|
442
403
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -485,7 +446,7 @@ class Playwright extends Helper {
|
|
|
485
446
|
this.options.recordVideo = { size }
|
|
486
447
|
}
|
|
487
448
|
if (this.options.recordVideo && !this.options.recordVideo.dir) {
|
|
488
|
-
this.options.recordVideo.dir = `${
|
|
449
|
+
this.options.recordVideo.dir = `${store.outputDir}/videos/`
|
|
489
450
|
}
|
|
490
451
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
491
452
|
this.isElectron = this.options.browser === 'electron'
|
|
@@ -543,32 +504,22 @@ class Playwright extends Helper {
|
|
|
543
504
|
}
|
|
544
505
|
}
|
|
545
506
|
|
|
546
|
-
// Ensure custom locators from this instance are in the global registry
|
|
547
|
-
// This is critical for worker threads where globalCustomLocatorStrategies is a new Map
|
|
548
|
-
if (this.customLocatorStrategies) {
|
|
549
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
550
|
-
if (!globalCustomLocatorStrategies.has(strategyName)) {
|
|
551
|
-
globalCustomLocatorStrategies.set(strategyName, strategyFunction)
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
507
|
// register an internal selector engine for reading value property of elements in a selector
|
|
557
508
|
try {
|
|
558
509
|
// Always wrap in try-catch since selectors might be registered globally across workers
|
|
559
510
|
// Check global flag to avoid re-registration in worker processes
|
|
560
|
-
if (!
|
|
511
|
+
if (!defaultSelectorEnginesInitialized) {
|
|
561
512
|
try {
|
|
562
513
|
await playwright.selectors.register('__value', createValueEngine)
|
|
563
514
|
await playwright.selectors.register('__disabled', createDisabledEngine)
|
|
564
|
-
|
|
515
|
+
defaultSelectorEnginesInitialized = true
|
|
565
516
|
defaultSelectorEnginesInitialized = true
|
|
566
517
|
} catch (e) {
|
|
567
518
|
if (!e.message.includes('already registered')) {
|
|
568
519
|
throw e
|
|
569
520
|
}
|
|
570
521
|
// Selector already registered globally by another worker
|
|
571
|
-
|
|
522
|
+
defaultSelectorEnginesInitialized = true
|
|
572
523
|
defaultSelectorEnginesInitialized = true
|
|
573
524
|
}
|
|
574
525
|
} else {
|
|
@@ -583,28 +534,11 @@ class Playwright extends Helper {
|
|
|
583
534
|
// Ignore if already set
|
|
584
535
|
}
|
|
585
536
|
}
|
|
586
|
-
|
|
587
|
-
// Register all custom locator strategies from the global registry
|
|
588
|
-
await this._registerGlobalCustomLocators()
|
|
589
537
|
} catch (e) {
|
|
590
538
|
console.warn(e)
|
|
591
539
|
}
|
|
592
540
|
}
|
|
593
541
|
|
|
594
|
-
async _registerGlobalCustomLocators() {
|
|
595
|
-
for (const [name, func] of globalCustomLocatorStrategies.entries()) {
|
|
596
|
-
if (registeredCustomLocatorStrategies.has(name)) continue
|
|
597
|
-
try {
|
|
598
|
-
await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
|
|
599
|
-
registeredCustomLocatorStrategies.add(name)
|
|
600
|
-
} catch (e) {
|
|
601
|
-
if (!e.message.includes('already registered')) {
|
|
602
|
-
this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
542
|
_beforeSuite() {
|
|
609
543
|
// Skip browser start in dry-run mode (used by check command)
|
|
610
544
|
if (store.dryRun) {
|
|
@@ -678,7 +612,7 @@ class Playwright extends Helper {
|
|
|
678
612
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
|
|
679
613
|
if (this.options.recordHar) {
|
|
680
614
|
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
|
|
681
|
-
const fileName = `${`${
|
|
615
|
+
const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
|
|
682
616
|
const dir = path.dirname(fileName)
|
|
683
617
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
684
618
|
this.options.recordHar.path = fileName
|
|
@@ -821,6 +755,11 @@ class Playwright extends Helper {
|
|
|
821
755
|
}
|
|
822
756
|
|
|
823
757
|
async _afterSuite() {
|
|
758
|
+
// Reset leftover test-level cleanup state (e.g. screenshot failures)
|
|
759
|
+
// so only errors from this suite teardown are evaluated below.
|
|
760
|
+
this.hasCleanupError = false
|
|
761
|
+
this.testFailures = []
|
|
762
|
+
|
|
824
763
|
// Stop browser after suite completes
|
|
825
764
|
// For restart strategies: stop after each suite
|
|
826
765
|
// For session mode (restart:false): stop after the last suite
|
|
@@ -1266,33 +1205,6 @@ class Playwright extends Helper {
|
|
|
1266
1205
|
return this.browser
|
|
1267
1206
|
}
|
|
1268
1207
|
|
|
1269
|
-
_hasCustomLocatorStrategies() {
|
|
1270
|
-
return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
_parseCustomLocatorStrategies(strategies) {
|
|
1274
|
-
if (typeof strategies !== 'object' || strategies === null) return null
|
|
1275
|
-
const hasValidFunctions = Object.values(strategies).some(v => typeof v === 'function')
|
|
1276
|
-
return hasValidFunctions ? strategies : null
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
_lookupCustomLocator(customStrategy) {
|
|
1280
|
-
if (!this._hasCustomLocatorStrategies()) return null
|
|
1281
|
-
const strategy = this.customLocatorStrategies[customStrategy]
|
|
1282
|
-
return typeof strategy === 'function' ? strategy : null
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
_isCustomLocator(locator) {
|
|
1286
|
-
const locatorObj = new Locator(locator)
|
|
1287
|
-
if (!locatorObj.isCustom()) return false
|
|
1288
|
-
if (this._lookupCustomLocator(locatorObj.type)) return true
|
|
1289
|
-
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
_isCustomLocatorStrategyDefined() {
|
|
1293
|
-
return this._hasCustomLocatorStrategies()
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
1208
|
/**
|
|
1297
1209
|
* Create a new browser context with a page. \
|
|
1298
1210
|
* Usually it should be run from a custom helper after call of `_startBrowser()`
|
|
@@ -1304,30 +1216,11 @@ class Playwright extends Helper {
|
|
|
1304
1216
|
}
|
|
1305
1217
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1306
1218
|
|
|
1307
|
-
// Register custom locator strategies for this context
|
|
1308
|
-
await this._registerCustomLocatorStrategies()
|
|
1309
|
-
|
|
1310
1219
|
const page = await this.browserContext.newPage()
|
|
1311
1220
|
targetCreatedHandler.call(this, page)
|
|
1312
1221
|
await this._setPage(page)
|
|
1313
1222
|
}
|
|
1314
1223
|
|
|
1315
|
-
async _registerCustomLocatorStrategies() {
|
|
1316
|
-
if (!this._hasCustomLocatorStrategies()) return
|
|
1317
|
-
|
|
1318
|
-
for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
|
|
1319
|
-
if (registeredCustomLocatorStrategies.has(name)) continue
|
|
1320
|
-
try {
|
|
1321
|
-
await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
|
|
1322
|
-
registeredCustomLocatorStrategies.add(name)
|
|
1323
|
-
} catch (e) {
|
|
1324
|
-
if (!e.message.includes('already registered')) {
|
|
1325
|
-
this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
1224
|
_getType() {
|
|
1332
1225
|
return this.browser._type
|
|
1333
1226
|
}
|
|
@@ -1606,8 +1499,23 @@ class Playwright extends Helper {
|
|
|
1606
1499
|
*
|
|
1607
1500
|
*/
|
|
1608
1501
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1609
|
-
|
|
1610
|
-
|
|
1502
|
+
let context = null
|
|
1503
|
+
if (typeof offsetX !== 'number') {
|
|
1504
|
+
context = offsetX
|
|
1505
|
+
offsetX = 0
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
let el
|
|
1509
|
+
if (context) {
|
|
1510
|
+
const contextEls = await this._locate(context)
|
|
1511
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1512
|
+
el = await findElements.call(this, contextEls[0], locator)
|
|
1513
|
+
assertElementExists(el, locator)
|
|
1514
|
+
el = el[0]
|
|
1515
|
+
} else {
|
|
1516
|
+
el = await this._locateElement(locator)
|
|
1517
|
+
assertElementExists(el, locator)
|
|
1518
|
+
}
|
|
1611
1519
|
|
|
1612
1520
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1613
1521
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1731,7 +1639,7 @@ class Playwright extends Helper {
|
|
|
1731
1639
|
* @returns Promise<void>
|
|
1732
1640
|
*/
|
|
1733
1641
|
async replayFromHar(harFilePath, opts) {
|
|
1734
|
-
const file = path.join(
|
|
1642
|
+
const file = path.join(store.codeceptDir, harFilePath)
|
|
1735
1643
|
|
|
1736
1644
|
if (!fileExists(file)) {
|
|
1737
1645
|
throw new Error(`File at ${file} cannot be found on local system`)
|
|
@@ -1871,7 +1779,11 @@ class Playwright extends Helper {
|
|
|
1871
1779
|
*/
|
|
1872
1780
|
async _locateElement(locator) {
|
|
1873
1781
|
const context = await this._getContext()
|
|
1874
|
-
|
|
1782
|
+
const elements = await findElements.call(this, context, locator)
|
|
1783
|
+
if (elements.length === 0) {
|
|
1784
|
+
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1785
|
+
}
|
|
1786
|
+
return selectElement(elements, locator, this)
|
|
1875
1787
|
}
|
|
1876
1788
|
|
|
1877
1789
|
/**
|
|
@@ -1886,7 +1798,7 @@ class Playwright extends Helper {
|
|
|
1886
1798
|
const context = providedContext || (await this._getContext())
|
|
1887
1799
|
const els = await findCheckable.call(this, locator, context)
|
|
1888
1800
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1889
|
-
return els
|
|
1801
|
+
return selectElement(els, locator, this)
|
|
1890
1802
|
}
|
|
1891
1803
|
|
|
1892
1804
|
/**
|
|
@@ -2054,8 +1966,15 @@ class Playwright extends Helper {
|
|
|
2054
1966
|
* {{> seeElement }}
|
|
2055
1967
|
*
|
|
2056
1968
|
*/
|
|
2057
|
-
async seeElement(locator) {
|
|
2058
|
-
let els
|
|
1969
|
+
async seeElement(locator, context = null) {
|
|
1970
|
+
let els
|
|
1971
|
+
if (context) {
|
|
1972
|
+
const contextEls = await this._locate(context)
|
|
1973
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1974
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1975
|
+
} else {
|
|
1976
|
+
els = await this._locate(locator)
|
|
1977
|
+
}
|
|
2059
1978
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2060
1979
|
try {
|
|
2061
1980
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2068,8 +1987,15 @@ class Playwright extends Helper {
|
|
|
2068
1987
|
* {{> dontSeeElement }}
|
|
2069
1988
|
*
|
|
2070
1989
|
*/
|
|
2071
|
-
async dontSeeElement(locator) {
|
|
2072
|
-
let els
|
|
1990
|
+
async dontSeeElement(locator, context = null) {
|
|
1991
|
+
let els
|
|
1992
|
+
if (context) {
|
|
1993
|
+
const contextEls = await this._locate(context)
|
|
1994
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1995
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1996
|
+
} else {
|
|
1997
|
+
els = await this._locate(locator)
|
|
1998
|
+
}
|
|
2073
1999
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2074
2000
|
try {
|
|
2075
2001
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2124,7 +2050,7 @@ class Playwright extends Helper {
|
|
|
2124
2050
|
const filePath = await download.path()
|
|
2125
2051
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
2126
2052
|
|
|
2127
|
-
const downloadPath = path.join(
|
|
2053
|
+
const downloadPath = path.join(store.outputDir, fileName)
|
|
2128
2054
|
if (!fs.existsSync(path.dirname(downloadPath))) {
|
|
2129
2055
|
fs.mkdirSync(path.dirname(downloadPath), '0777')
|
|
2130
2056
|
}
|
|
@@ -2155,15 +2081,6 @@ class Playwright extends Helper {
|
|
|
2155
2081
|
return proceedClick.call(this, locator, context, options)
|
|
2156
2082
|
}
|
|
2157
2083
|
|
|
2158
|
-
/**
|
|
2159
|
-
* Clicks link and waits for navigation (deprecated)
|
|
2160
|
-
*/
|
|
2161
|
-
async clickLink(locator, context = null) {
|
|
2162
|
-
console.log('clickLink deprecated: Playwright automatically waits for navigation to happen.')
|
|
2163
|
-
console.log('Replace I.clickLink with I.click')
|
|
2164
|
-
return this.click(locator, context)
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
2084
|
/**
|
|
2168
2085
|
* {{> forceClick }}
|
|
2169
2086
|
*/
|
|
@@ -2308,6 +2225,7 @@ class Playwright extends Helper {
|
|
|
2308
2225
|
* {{> pressKeyWithKeyNormalization }}
|
|
2309
2226
|
*/
|
|
2310
2227
|
async pressKey(key) {
|
|
2228
|
+
await checkFocusBeforePressKey(this, key)
|
|
2311
2229
|
const modifiers = []
|
|
2312
2230
|
if (Array.isArray(key)) {
|
|
2313
2231
|
for (let k of key) {
|
|
@@ -2336,6 +2254,8 @@ class Playwright extends Helper {
|
|
|
2336
2254
|
* {{> type }}
|
|
2337
2255
|
*/
|
|
2338
2256
|
async type(keys, delay = null) {
|
|
2257
|
+
await checkFocusBeforeType(this)
|
|
2258
|
+
|
|
2339
2259
|
// Always use page.keyboard.type for any string (including single character and national characters).
|
|
2340
2260
|
if (!Array.isArray(keys)) {
|
|
2341
2261
|
keys = keys.toString()
|
|
@@ -2355,43 +2275,33 @@ class Playwright extends Helper {
|
|
|
2355
2275
|
* {{> fillField }}
|
|
2356
2276
|
*
|
|
2357
2277
|
*/
|
|
2358
|
-
async fillField(field, value) {
|
|
2359
|
-
const els = await findFields.call(this, field)
|
|
2278
|
+
async fillField(field, value, context = null) {
|
|
2279
|
+
const els = await findFields.call(this, field, context)
|
|
2360
2280
|
assertElementExists(els, field, 'Field')
|
|
2361
|
-
const el = els
|
|
2281
|
+
const el = selectElement(els, field, this)
|
|
2282
|
+
|
|
2283
|
+
await highlightActiveElement.call(this, el)
|
|
2284
|
+
|
|
2285
|
+
if (await fillRichEditor(this, el, value)) {
|
|
2286
|
+
return this._waitForAction()
|
|
2287
|
+
}
|
|
2362
2288
|
|
|
2363
2289
|
await el.clear()
|
|
2364
2290
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
2365
2291
|
|
|
2366
|
-
await highlightActiveElement.call(this, el)
|
|
2367
|
-
|
|
2368
2292
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2369
2293
|
|
|
2370
2294
|
return this._waitForAction()
|
|
2371
2295
|
}
|
|
2372
2296
|
|
|
2373
2297
|
/**
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2377
|
-
* Examples:
|
|
2378
|
-
*
|
|
2379
|
-
* ```js
|
|
2380
|
-
* I.clearField('.text-area')
|
|
2381
|
-
*
|
|
2382
|
-
* // if this doesn't work use force option
|
|
2383
|
-
* I.clearField('#submit', { force: true })
|
|
2384
|
-
* ```
|
|
2385
|
-
* Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
2386
|
-
*
|
|
2387
|
-
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
2388
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
2298
|
+
* {{> clearField }}
|
|
2389
2299
|
*/
|
|
2390
|
-
async clearField(locator,
|
|
2391
|
-
const els = await findFields.call(this, locator)
|
|
2300
|
+
async clearField(locator, context = null) {
|
|
2301
|
+
const els = await findFields.call(this, locator, context)
|
|
2392
2302
|
assertElementExists(els, locator, 'Field to clear')
|
|
2393
2303
|
|
|
2394
|
-
const el = els
|
|
2304
|
+
const el = selectElement(els, locator, this)
|
|
2395
2305
|
|
|
2396
2306
|
await highlightActiveElement.call(this, el)
|
|
2397
2307
|
|
|
@@ -2403,68 +2313,101 @@ class Playwright extends Helper {
|
|
|
2403
2313
|
/**
|
|
2404
2314
|
* {{> appendField }}
|
|
2405
2315
|
*/
|
|
2406
|
-
async appendField(field, value) {
|
|
2407
|
-
const els = await findFields.call(this, field)
|
|
2316
|
+
async appendField(field, value, context = null) {
|
|
2317
|
+
const els = await findFields.call(this, field, context)
|
|
2408
2318
|
assertElementExists(els, field, 'Field')
|
|
2409
|
-
|
|
2410
|
-
await
|
|
2411
|
-
await
|
|
2319
|
+
const el = selectElement(els, field, this)
|
|
2320
|
+
await highlightActiveElement.call(this, el)
|
|
2321
|
+
await el.press('End')
|
|
2322
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2412
2323
|
return this._waitForAction()
|
|
2413
2324
|
}
|
|
2414
2325
|
|
|
2415
2326
|
/**
|
|
2416
2327
|
* {{> seeInField }}
|
|
2417
2328
|
*/
|
|
2418
|
-
async seeInField(field, value) {
|
|
2329
|
+
async seeInField(field, value, context = null) {
|
|
2419
2330
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2420
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2331
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2421
2332
|
}
|
|
2422
2333
|
|
|
2423
2334
|
/**
|
|
2424
2335
|
* {{> dontSeeInField }}
|
|
2425
2336
|
*/
|
|
2426
|
-
async dontSeeInField(field, value) {
|
|
2337
|
+
async dontSeeInField(field, value, context = null) {
|
|
2427
2338
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2428
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2339
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2429
2340
|
}
|
|
2430
2341
|
|
|
2431
2342
|
/**
|
|
2432
2343
|
* {{> attachFile }}
|
|
2433
2344
|
*
|
|
2434
2345
|
*/
|
|
2435
|
-
async attachFile(locator, pathToFile) {
|
|
2436
|
-
const file = path.join(
|
|
2346
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2347
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
2437
2348
|
|
|
2438
2349
|
if (!fileExists(file)) {
|
|
2439
2350
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2440
2351
|
}
|
|
2441
|
-
const els = await findFields.call(this, locator)
|
|
2442
|
-
|
|
2443
|
-
|
|
2352
|
+
const els = await findFields.call(this, locator, context)
|
|
2353
|
+
if (els.length) {
|
|
2354
|
+
const el = selectElement(els, locator, this)
|
|
2355
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2356
|
+
const type = await el.evaluate(el => el.type)
|
|
2357
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2358
|
+
await el.setInputFiles(file)
|
|
2359
|
+
return this._waitForAction()
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2364
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2365
|
+
const el = selectElement(targetEls, locator, this)
|
|
2366
|
+
const fileData = {
|
|
2367
|
+
base64Content: base64EncodeFile(file),
|
|
2368
|
+
fileName: path.basename(file),
|
|
2369
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2370
|
+
}
|
|
2371
|
+
await el.evaluate(dropFile, fileData)
|
|
2444
2372
|
return this._waitForAction()
|
|
2445
2373
|
}
|
|
2446
2374
|
|
|
2447
2375
|
/**
|
|
2448
2376
|
* {{> selectOption }}
|
|
2449
2377
|
*/
|
|
2450
|
-
async selectOption(select, option) {
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
const el = els[0]
|
|
2378
|
+
async selectOption(select, option, context = null) {
|
|
2379
|
+
const pageContext = await this.context
|
|
2380
|
+
const matchedLocator = new Locator(select)
|
|
2454
2381
|
|
|
2455
|
-
|
|
2456
|
-
|
|
2382
|
+
let contextEl
|
|
2383
|
+
if (context) {
|
|
2384
|
+
const contextEls = await this._locate(context)
|
|
2385
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2386
|
+
contextEl = contextEls[0]
|
|
2387
|
+
}
|
|
2457
2388
|
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2389
|
+
// Strict locator
|
|
2390
|
+
if (!matchedLocator.isFuzzy()) {
|
|
2391
|
+
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2392
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2393
|
+
assertElementExists(els, select, 'Selectable element')
|
|
2394
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2462
2395
|
}
|
|
2463
2396
|
|
|
2464
|
-
|
|
2397
|
+
// Fuzzy: try combobox
|
|
2398
|
+
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2399
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2400
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2401
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2465
2402
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2403
|
+
// Fuzzy: try listbox
|
|
2404
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2405
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2406
|
+
|
|
2407
|
+
// Fuzzy: try native select
|
|
2408
|
+
els = await findFields.call(this, select, context)
|
|
2409
|
+
assertElementExists(els, select, 'Selectable element')
|
|
2410
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2468
2411
|
}
|
|
2469
2412
|
|
|
2470
2413
|
/**
|
|
@@ -2505,6 +2448,26 @@ class Playwright extends Helper {
|
|
|
2505
2448
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
2506
2449
|
}
|
|
2507
2450
|
|
|
2451
|
+
/**
|
|
2452
|
+
* {{> seeCurrentPathEquals }}
|
|
2453
|
+
*/
|
|
2454
|
+
async seeCurrentPathEquals(path) {
|
|
2455
|
+
const currentUrl = await this._getPageUrl()
|
|
2456
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2457
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2458
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
/**
|
|
2462
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
2463
|
+
*/
|
|
2464
|
+
async dontSeeCurrentPathEquals(path) {
|
|
2465
|
+
const currentUrl = await this._getPageUrl()
|
|
2466
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2467
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2468
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2508
2471
|
/**
|
|
2509
2472
|
* {{> see }}
|
|
2510
2473
|
*
|
|
@@ -2707,8 +2670,11 @@ class Playwright extends Helper {
|
|
|
2707
2670
|
* @returns {Promise<any>}
|
|
2708
2671
|
*/
|
|
2709
2672
|
async executeScript(fn, arg) {
|
|
2710
|
-
if (
|
|
2711
|
-
|
|
2673
|
+
if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
|
|
2674
|
+
if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
|
|
2675
|
+
return arg.evaluate(fn)
|
|
2676
|
+
}
|
|
2677
|
+
if (this.context && typeof this.context.url !== 'function' && typeof this.context.innerText !== 'function') {
|
|
2712
2678
|
return this.context.locator(':root').evaluate(fn, arg)
|
|
2713
2679
|
}
|
|
2714
2680
|
return this.page.evaluate.apply(this.page, [fn, arg])
|
|
@@ -2722,23 +2688,12 @@ class Playwright extends Helper {
|
|
|
2722
2688
|
_contextLocator(locator) {
|
|
2723
2689
|
const locatorObj = new Locator(locator, 'css')
|
|
2724
2690
|
|
|
2725
|
-
// Handle custom locators differently
|
|
2726
|
-
if (locatorObj.isCustom()) {
|
|
2727
|
-
return buildCustomLocatorString(locatorObj)
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
2691
|
locator = buildLocatorString(locatorObj)
|
|
2731
2692
|
|
|
2732
2693
|
if (this.contextLocator) {
|
|
2733
2694
|
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
// Instead, we'll need to handle this differently in the calling methods
|
|
2737
|
-
return locator
|
|
2738
|
-
} else {
|
|
2739
|
-
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2740
|
-
locator = `${contextLocator} >> ${locator}`
|
|
2741
|
-
}
|
|
2695
|
+
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2696
|
+
locator = `${contextLocator} >> ${locator}`
|
|
2742
2697
|
}
|
|
2743
2698
|
|
|
2744
2699
|
return locator
|
|
@@ -2749,43 +2704,28 @@ class Playwright extends Helper {
|
|
|
2749
2704
|
*
|
|
2750
2705
|
*/
|
|
2751
2706
|
async grabTextFrom(locator) {
|
|
2752
|
-
|
|
2753
|
-
if (
|
|
2754
|
-
const
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
this.debugSection('Text', text)
|
|
2759
|
-
return text
|
|
2760
|
-
}
|
|
2707
|
+
const roleElements = await handleRoleLocator(this.page, locator)
|
|
2708
|
+
if (roleElements && roleElements.length > 0) {
|
|
2709
|
+
const text = await roleElements[0].textContent()
|
|
2710
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2711
|
+
this.debugSection('Text', text)
|
|
2712
|
+
return text
|
|
2761
2713
|
}
|
|
2762
2714
|
|
|
2763
2715
|
const locatorObj = new Locator(locator, 'css')
|
|
2764
2716
|
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
throw new Error(`Element not found: ${locatorObj.toString()}`)
|
|
2770
|
-
}
|
|
2771
|
-
const text = await elements[0].textContent()
|
|
2772
|
-
assertElementExists(text, locatorObj.toString())
|
|
2717
|
+
locator = this._contextLocator(locator)
|
|
2718
|
+
try {
|
|
2719
|
+
const text = await this.page.textContent(locator)
|
|
2720
|
+
assertElementExists(text, locator)
|
|
2773
2721
|
this.debugSection('Text', text)
|
|
2774
2722
|
return text
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
assertElementExists(text, locator)
|
|
2780
|
-
this.debugSection('Text', text)
|
|
2781
|
-
return text
|
|
2782
|
-
} catch (error) {
|
|
2783
|
-
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2784
|
-
if (error.message && error.message.includes('Timeout')) {
|
|
2785
|
-
throw new ElementNotFound(locator, 'text')
|
|
2786
|
-
}
|
|
2787
|
-
throw error
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2725
|
+
if (error.message && error.message.includes('Timeout')) {
|
|
2726
|
+
throw new ElementNotFound(locator, 'text')
|
|
2788
2727
|
}
|
|
2728
|
+
throw error
|
|
2789
2729
|
}
|
|
2790
2730
|
}
|
|
2791
2731
|
|
|
@@ -2997,7 +2937,7 @@ class Playwright extends Helper {
|
|
|
2997
2937
|
const els = await this._locate(matchedLocator)
|
|
2998
2938
|
assertElementExists(els, locator)
|
|
2999
2939
|
const snapshot = await els[0].ariaSnapshot()
|
|
3000
|
-
this.debugSection('Aria Snapshot', snapshot)
|
|
2940
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
3001
2941
|
return snapshot
|
|
3002
2942
|
}
|
|
3003
2943
|
|
|
@@ -3350,24 +3290,13 @@ class Playwright extends Helper {
|
|
|
3350
3290
|
|
|
3351
3291
|
const context = await this._getContext()
|
|
3352
3292
|
try {
|
|
3353
|
-
|
|
3354
|
-
// For custom locators, we need to use our custom element finding logic
|
|
3355
|
-
const elements = await findCustomElements.call(this, context, locator)
|
|
3356
|
-
if (elements.length === 0) {
|
|
3357
|
-
throw new Error(`Custom locator ${locator.type}=${locator.value} not found`)
|
|
3358
|
-
}
|
|
3359
|
-
await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3360
|
-
} else {
|
|
3361
|
-
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3362
|
-
}
|
|
3293
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3363
3294
|
} catch (e) {
|
|
3364
3295
|
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
3365
3296
|
}
|
|
3366
3297
|
}
|
|
3367
3298
|
|
|
3368
3299
|
/**
|
|
3369
|
-
* This method accepts [React selectors](https://codecept.io/react).
|
|
3370
|
-
*
|
|
3371
3300
|
* {{> waitForVisible }}
|
|
3372
3301
|
*/
|
|
3373
3302
|
async waitForVisible(locator, sec) {
|
|
@@ -3377,26 +3306,6 @@ class Playwright extends Helper {
|
|
|
3377
3306
|
const context = await this._getContext()
|
|
3378
3307
|
let count = 0
|
|
3379
3308
|
|
|
3380
|
-
// Handle custom locators
|
|
3381
|
-
if (locator.isCustom()) {
|
|
3382
|
-
let waiter
|
|
3383
|
-
do {
|
|
3384
|
-
const elements = await findCustomElements.call(this, context, locator)
|
|
3385
|
-
if (elements.length > 0) {
|
|
3386
|
-
waiter = await elements[0].isVisible()
|
|
3387
|
-
} else {
|
|
3388
|
-
waiter = false
|
|
3389
|
-
}
|
|
3390
|
-
if (!waiter) {
|
|
3391
|
-
await this.wait(1)
|
|
3392
|
-
count += 1000
|
|
3393
|
-
}
|
|
3394
|
-
} while (!waiter && count <= waitTimeout)
|
|
3395
|
-
|
|
3396
|
-
if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`)
|
|
3397
|
-
return
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
3309
|
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
3401
3310
|
let waiter
|
|
3402
3311
|
if (this.frame) {
|
|
@@ -3500,7 +3409,7 @@ class Playwright extends Helper {
|
|
|
3500
3409
|
}
|
|
3501
3410
|
|
|
3502
3411
|
async _getContext() {
|
|
3503
|
-
if (
|
|
3412
|
+
if (this.context) {
|
|
3504
3413
|
return this.context
|
|
3505
3414
|
}
|
|
3506
3415
|
if (this.frame) {
|
|
@@ -3514,6 +3423,7 @@ class Playwright extends Helper {
|
|
|
3514
3423
|
*/
|
|
3515
3424
|
async waitInUrl(urlPart, sec = null) {
|
|
3516
3425
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3426
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3517
3427
|
|
|
3518
3428
|
return this.page
|
|
3519
3429
|
.waitForFunction(
|
|
@@ -3521,13 +3431,13 @@ class Playwright extends Helper {
|
|
|
3521
3431
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3522
3432
|
return currUrl.indexOf(urlPart) > -1
|
|
3523
3433
|
},
|
|
3524
|
-
|
|
3434
|
+
expectedUrl,
|
|
3525
3435
|
{ timeout: waitTimeout },
|
|
3526
3436
|
)
|
|
3527
3437
|
.catch(async e => {
|
|
3528
|
-
const currUrl = await this._getPageUrl()
|
|
3438
|
+
const currUrl = await this._getPageUrl()
|
|
3529
3439
|
if (/Timeout/i.test(e.message)) {
|
|
3530
|
-
throw new Error(`expected url to include ${
|
|
3440
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3531
3441
|
} else {
|
|
3532
3442
|
throw e
|
|
3533
3443
|
}
|
|
@@ -3539,29 +3449,50 @@ class Playwright extends Helper {
|
|
|
3539
3449
|
*/
|
|
3540
3450
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3541
3451
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3452
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3542
3453
|
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3454
|
+
try {
|
|
3455
|
+
await this.page.waitForURL(
|
|
3456
|
+
url => url.href === expectedUrl,
|
|
3457
|
+
{ timeout: waitTimeout },
|
|
3458
|
+
)
|
|
3459
|
+
} catch (e) {
|
|
3460
|
+
const currUrl = await this._getPageUrl()
|
|
3461
|
+
if (/Timeout/i.test(e.message)) {
|
|
3462
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
3463
|
+
} else {
|
|
3464
|
+
throw e
|
|
3465
|
+
}
|
|
3546
3466
|
}
|
|
3467
|
+
}
|
|
3547
3468
|
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3469
|
+
/**
|
|
3470
|
+
* {{> waitCurrentPathEquals }}
|
|
3471
|
+
*/
|
|
3472
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
3473
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3474
|
+
const normalizedPath = normalizePath(path)
|
|
3475
|
+
|
|
3476
|
+
try {
|
|
3477
|
+
await this.page.waitForFunction(
|
|
3478
|
+
expectedPath => {
|
|
3479
|
+
const actualPath = window.location.pathname
|
|
3480
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
3481
|
+
return normalizePath(actualPath) === expectedPath
|
|
3553
3482
|
},
|
|
3554
|
-
|
|
3483
|
+
normalizedPath,
|
|
3555
3484
|
{ timeout: waitTimeout },
|
|
3556
3485
|
)
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3486
|
+
} catch (e) {
|
|
3487
|
+
const currentUrl = await this._getPageUrl()
|
|
3488
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
3489
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
3490
|
+
if (/Timeout/i.test(e.message)) {
|
|
3491
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
3492
|
+
} else {
|
|
3493
|
+
throw e
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3565
3496
|
}
|
|
3566
3497
|
|
|
3567
3498
|
/**
|
|
@@ -3576,15 +3507,6 @@ class Playwright extends Helper {
|
|
|
3576
3507
|
if (context) {
|
|
3577
3508
|
const locator = new Locator(context, 'css')
|
|
3578
3509
|
try {
|
|
3579
|
-
if (locator.isCustom()) {
|
|
3580
|
-
// For custom locators, find the elements first then check for text within them
|
|
3581
|
-
const elements = await findCustomElements.call(this, contextObject, locator)
|
|
3582
|
-
if (elements.length === 0) {
|
|
3583
|
-
throw new Error(`Context element not found: ${locator.toString()}`)
|
|
3584
|
-
}
|
|
3585
|
-
return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
3510
|
if (!locator.isXPath()) {
|
|
3589
3511
|
return contextObject
|
|
3590
3512
|
.locator(`${locator.simplify()} >> text=${text}`)
|
|
@@ -3827,7 +3749,7 @@ class Playwright extends Helper {
|
|
|
3827
3749
|
if (!locator.isXPath()) {
|
|
3828
3750
|
try {
|
|
3829
3751
|
await context
|
|
3830
|
-
.locator(
|
|
3752
|
+
.locator(locator.simplify())
|
|
3831
3753
|
.first()
|
|
3832
3754
|
.waitFor({ timeout: waitTimeout, state: 'detached' })
|
|
3833
3755
|
} catch (e) {
|
|
@@ -4252,51 +4174,54 @@ class Playwright extends Helper {
|
|
|
4252
4174
|
|
|
4253
4175
|
export default Playwright
|
|
4254
4176
|
|
|
4255
|
-
function
|
|
4256
|
-
// Note: this.debug not available in standalone function, using console.log
|
|
4257
|
-
console.log(`Building custom locator string: ${locator.type}=${locator.value}`)
|
|
4258
|
-
return `${locator.type}=${locator.value}`
|
|
4259
|
-
}
|
|
4260
|
-
|
|
4261
|
-
function buildLocatorString(locator) {
|
|
4262
|
-
if (locator.isCustom()) {
|
|
4263
|
-
return buildCustomLocatorString(locator)
|
|
4264
|
-
}
|
|
4177
|
+
export function buildLocatorString(locator) {
|
|
4265
4178
|
if (locator.isXPath()) {
|
|
4266
|
-
|
|
4179
|
+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
|
|
4180
|
+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
|
|
4181
|
+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
|
|
4182
|
+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
|
|
4183
|
+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
|
|
4184
|
+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
|
|
4185
|
+
return `xpath=${value}`
|
|
4186
|
+
}
|
|
4187
|
+
if (locator.isShadow()) {
|
|
4188
|
+
// Convert shadow locator to CSS with >> chaining operator
|
|
4189
|
+
// Playwright pierces shadow DOM by default, >> chains selectors
|
|
4190
|
+
// { shadow: ['my-app', 'my-form', 'button'] } => 'my-app >> my-form >> button'
|
|
4191
|
+
return locator.value.join(' >> ')
|
|
4267
4192
|
}
|
|
4268
4193
|
return locator.simplify()
|
|
4269
4194
|
}
|
|
4270
4195
|
|
|
4271
|
-
/**
|
|
4272
|
-
* Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4273
|
-
*/
|
|
4274
|
-
function isRoleLocatorObject(locator) {
|
|
4275
|
-
return locator && typeof locator === 'object' && locator.role && !locator.type
|
|
4276
|
-
}
|
|
4277
|
-
|
|
4278
4196
|
/**
|
|
4279
4197
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4198
|
+
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
4280
4199
|
* Returns elements array if role locator, null otherwise
|
|
4281
4200
|
*/
|
|
4282
4201
|
async function handleRoleLocator(context, locator) {
|
|
4283
|
-
|
|
4202
|
+
const loc = new Locator(locator)
|
|
4203
|
+
if (!loc.isRole()) return null
|
|
4284
4204
|
|
|
4205
|
+
const roleObj = loc.locator || {}
|
|
4285
4206
|
const options = {}
|
|
4286
|
-
if (
|
|
4287
|
-
if (
|
|
4207
|
+
if (roleObj.text) options.name = roleObj.text
|
|
4208
|
+
if (roleObj.name) options.name = roleObj.name
|
|
4209
|
+
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4288
4210
|
|
|
4211
|
+
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
async function findByRole(context, locator) {
|
|
4215
|
+
if (!locator || !locator.role) return null
|
|
4216
|
+
const options = {}
|
|
4217
|
+
if (locator.name) options.name = locator.name
|
|
4218
|
+
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4289
4219
|
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4290
4220
|
}
|
|
4291
4221
|
|
|
4292
4222
|
async function findElements(matcher, locator) {
|
|
4293
|
-
// Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
|
|
4294
|
-
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4295
|
-
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4296
4223
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4297
4224
|
|
|
4298
|
-
if (isReactLocator) return findReact(matcher, locator)
|
|
4299
|
-
if (isVueLocator) return findVue(matcher, locator)
|
|
4300
4225
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4301
4226
|
|
|
4302
4227
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
@@ -4305,122 +4230,12 @@ async function findElements(matcher, locator) {
|
|
|
4305
4230
|
|
|
4306
4231
|
locator = new Locator(locator, 'css')
|
|
4307
4232
|
|
|
4308
|
-
// Handle custom locators directly instead of relying on Playwright selector engines
|
|
4309
|
-
if (locator.isCustom()) {
|
|
4310
|
-
return findCustomElements.call(this, matcher, locator)
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
// Check if we have a custom context locator and need to search within it
|
|
4314
|
-
if (this.contextLocator) {
|
|
4315
|
-
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
4316
|
-
if (contextLocatorObj.isCustom()) {
|
|
4317
|
-
// Find the context elements first
|
|
4318
|
-
const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj)
|
|
4319
|
-
if (contextElements.length === 0) {
|
|
4320
|
-
return []
|
|
4321
|
-
}
|
|
4322
|
-
|
|
4323
|
-
// Search within the first context element
|
|
4324
|
-
const locatorString = buildLocatorString(locator)
|
|
4325
|
-
return contextElements[0].locator(locatorString).all()
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
|
|
4329
4233
|
const locatorString = buildLocatorString(locator)
|
|
4330
4234
|
|
|
4331
4235
|
return matcher.locator(locatorString).all()
|
|
4332
4236
|
}
|
|
4333
4237
|
|
|
4334
|
-
async function findCustomElements(matcher, locator) {
|
|
4335
|
-
// Always prioritize this.customLocatorStrategies which is set in constructor from config
|
|
4336
|
-
// and persists in every worker thread instance
|
|
4337
|
-
let strategyFunction = null
|
|
4338
|
-
|
|
4339
|
-
if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
|
|
4340
|
-
strategyFunction = this.customLocatorStrategies[locator.type]
|
|
4341
|
-
} else if (globalCustomLocatorStrategies.has(locator.type)) {
|
|
4342
|
-
// Fallback to global registry (populated in constructor and _init)
|
|
4343
|
-
strategyFunction = globalCustomLocatorStrategies.get(locator.type)
|
|
4344
|
-
}
|
|
4345
|
-
|
|
4346
|
-
if (!strategyFunction) {
|
|
4347
|
-
throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
// Execute the custom locator function in the browser context using page.evaluate
|
|
4351
|
-
const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page()
|
|
4352
|
-
|
|
4353
|
-
const elements = await page.evaluate(
|
|
4354
|
-
({ strategyCode, selector }) => {
|
|
4355
|
-
const strategy = new Function('return ' + strategyCode)()
|
|
4356
|
-
const result = strategy(selector, document)
|
|
4357
|
-
|
|
4358
|
-
// Convert NodeList or single element to array
|
|
4359
|
-
if (result && result.nodeType) {
|
|
4360
|
-
return [result]
|
|
4361
|
-
} else if (result && result.length !== undefined) {
|
|
4362
|
-
return Array.from(result)
|
|
4363
|
-
} else if (Array.isArray(result)) {
|
|
4364
|
-
return result
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
return []
|
|
4368
|
-
},
|
|
4369
|
-
{
|
|
4370
|
-
strategyCode: strategyFunction.toString(),
|
|
4371
|
-
selector: locator.value,
|
|
4372
|
-
},
|
|
4373
|
-
)
|
|
4374
|
-
|
|
4375
|
-
// Convert the found elements back to Playwright locators
|
|
4376
|
-
if (elements.length === 0) {
|
|
4377
|
-
return []
|
|
4378
|
-
}
|
|
4379
|
-
|
|
4380
|
-
// Create CSS selectors for the found elements and return as locators
|
|
4381
|
-
const locators = []
|
|
4382
|
-
const timestamp = Date.now()
|
|
4383
|
-
|
|
4384
|
-
for (let i = 0; i < elements.length; i++) {
|
|
4385
|
-
// Use a unique attribute approach to target specific elements
|
|
4386
|
-
const uniqueAttr = `data-codecept-custom-${timestamp}-${i}`
|
|
4387
|
-
|
|
4388
|
-
await page.evaluate(
|
|
4389
|
-
({ index, uniqueAttr, strategyCode, selector }) => {
|
|
4390
|
-
// Re-execute the strategy to find elements and mark the specific one
|
|
4391
|
-
const strategy = new Function('return ' + strategyCode)()
|
|
4392
|
-
const result = strategy(selector, document)
|
|
4393
|
-
|
|
4394
|
-
let elementsArray = []
|
|
4395
|
-
if (result && result.nodeType) {
|
|
4396
|
-
elementsArray = [result]
|
|
4397
|
-
} else if (result && result.length !== undefined) {
|
|
4398
|
-
elementsArray = Array.from(result)
|
|
4399
|
-
} else if (Array.isArray(result)) {
|
|
4400
|
-
elementsArray = result
|
|
4401
|
-
}
|
|
4402
|
-
|
|
4403
|
-
if (elementsArray[index]) {
|
|
4404
|
-
elementsArray[index].setAttribute(uniqueAttr, 'true')
|
|
4405
|
-
}
|
|
4406
|
-
},
|
|
4407
|
-
{
|
|
4408
|
-
index: i,
|
|
4409
|
-
uniqueAttr,
|
|
4410
|
-
strategyCode: strategyFunction.toString(),
|
|
4411
|
-
selector: locator.value,
|
|
4412
|
-
},
|
|
4413
|
-
)
|
|
4414
|
-
|
|
4415
|
-
locators.push(page.locator(`[${uniqueAttr}="true"]`))
|
|
4416
|
-
}
|
|
4417
|
-
|
|
4418
|
-
return locators
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
4238
|
async function findElement(matcher, locator) {
|
|
4422
|
-
if (locator.react) return findReact(matcher, locator)
|
|
4423
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
4424
4239
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4425
4240
|
|
|
4426
4241
|
locator = new Locator(locator, 'css')
|
|
@@ -4455,16 +4270,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4455
4270
|
assertElementExists(els, locator, 'Clickable element')
|
|
4456
4271
|
}
|
|
4457
4272
|
|
|
4458
|
-
|
|
4459
|
-
|
|
4273
|
+
const opts = store.currentStep?.opts
|
|
4274
|
+
let element
|
|
4275
|
+
if (opts?.elementIndex != null) {
|
|
4276
|
+
element = selectElement(els, locator, this)
|
|
4277
|
+
} else {
|
|
4278
|
+
const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
|
|
4279
|
+
if (strict) assertOnlyOneElement(els, locator, this)
|
|
4280
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
await highlightActiveElement.call(this, element)
|
|
4284
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4460
4285
|
|
|
4461
|
-
/*
|
|
4462
|
-
using the force true options itself but instead dispatching a click
|
|
4463
|
-
*/
|
|
4464
4286
|
if (options.force) {
|
|
4465
|
-
await
|
|
4287
|
+
await element.dispatchEvent('click')
|
|
4466
4288
|
} else {
|
|
4467
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4468
4289
|
await element.click(options)
|
|
4469
4290
|
}
|
|
4470
4291
|
const promises = []
|
|
@@ -4479,7 +4300,10 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4479
4300
|
async function findClickable(matcher, locator) {
|
|
4480
4301
|
const matchedLocator = new Locator(locator)
|
|
4481
4302
|
|
|
4482
|
-
if (!matchedLocator.isFuzzy())
|
|
4303
|
+
if (!matchedLocator.isFuzzy()) {
|
|
4304
|
+
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4305
|
+
return els
|
|
4306
|
+
}
|
|
4483
4307
|
|
|
4484
4308
|
let els
|
|
4485
4309
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
@@ -4521,7 +4345,9 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
4521
4345
|
if (!context) {
|
|
4522
4346
|
const el = await this.context
|
|
4523
4347
|
|
|
4524
|
-
allText = el.
|
|
4348
|
+
allText = typeof el.url !== 'function' && typeof el.innerText === 'function'
|
|
4349
|
+
? [await el.innerText()]
|
|
4350
|
+
: [await el.locator('body').innerText()]
|
|
4525
4351
|
|
|
4526
4352
|
description = 'web application'
|
|
4527
4353
|
} else {
|
|
@@ -4579,38 +4405,92 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4579
4405
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4580
4406
|
}
|
|
4581
4407
|
|
|
4582
|
-
async function findFields(locator) {
|
|
4583
|
-
|
|
4584
|
-
if (
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
4408
|
+
async function findFields(locator, context = null) {
|
|
4409
|
+
let contextEl
|
|
4410
|
+
if (context) {
|
|
4411
|
+
const contextEls = await this._locate(context)
|
|
4412
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4413
|
+
contextEl = contextEls[0]
|
|
4588
4414
|
}
|
|
4589
4415
|
|
|
4416
|
+
const locateFn = contextEl
|
|
4417
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4418
|
+
: loc => this._locate(loc)
|
|
4419
|
+
|
|
4420
|
+
const matcher = contextEl || (await this.page)
|
|
4421
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4422
|
+
if (roleElements) return roleElements
|
|
4423
|
+
|
|
4590
4424
|
const matchedLocator = new Locator(locator)
|
|
4591
4425
|
if (!matchedLocator.isFuzzy()) {
|
|
4592
|
-
return
|
|
4426
|
+
return locateFn(matchedLocator)
|
|
4593
4427
|
}
|
|
4594
4428
|
const literal = xpathLocator.literal(locator)
|
|
4595
4429
|
|
|
4596
|
-
let els = await
|
|
4430
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4597
4431
|
if (els.length) {
|
|
4598
4432
|
return els
|
|
4599
4433
|
}
|
|
4600
4434
|
|
|
4601
|
-
els = await
|
|
4435
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4602
4436
|
if (els.length) {
|
|
4603
4437
|
return els
|
|
4604
4438
|
}
|
|
4605
|
-
els = await
|
|
4439
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4606
4440
|
if (els.length) {
|
|
4607
4441
|
return els
|
|
4608
4442
|
}
|
|
4609
|
-
return
|
|
4443
|
+
return locateFn({ css: locator })
|
|
4610
4444
|
}
|
|
4611
4445
|
|
|
4612
|
-
async function
|
|
4613
|
-
const
|
|
4446
|
+
async function proceedSelect(context, el, option) {
|
|
4447
|
+
const role = await el.getAttribute('role')
|
|
4448
|
+
const options = Array.isArray(option) ? option : [option]
|
|
4449
|
+
|
|
4450
|
+
if (role === 'combobox') {
|
|
4451
|
+
this.debugSection('SelectOption', 'Expanding combobox')
|
|
4452
|
+
await highlightActiveElement.call(this, el)
|
|
4453
|
+
const [ariaOwns, ariaControls] = await Promise.all([el.getAttribute('aria-owns'), el.getAttribute('aria-controls')])
|
|
4454
|
+
await el.click()
|
|
4455
|
+
await this._waitForAction()
|
|
4456
|
+
|
|
4457
|
+
const listboxId = ariaOwns || ariaControls
|
|
4458
|
+
let listbox = listboxId ? context.locator(`#${listboxId}`).first() : null
|
|
4459
|
+
if (!listbox || !(await listbox.count())) listbox = context.getByRole('listbox').first()
|
|
4460
|
+
|
|
4461
|
+
for (const opt of options) {
|
|
4462
|
+
const optEl = listbox.getByRole('option', { name: opt }).first()
|
|
4463
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4464
|
+
await highlightActiveElement.call(this, optEl)
|
|
4465
|
+
await optEl.click()
|
|
4466
|
+
}
|
|
4467
|
+
return this._waitForAction()
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
if (role === 'listbox') {
|
|
4471
|
+
for (const opt of options) {
|
|
4472
|
+
const optEl = el.getByRole('option', { name: opt }).first()
|
|
4473
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4474
|
+
await highlightActiveElement.call(this, optEl)
|
|
4475
|
+
await optEl.click()
|
|
4476
|
+
}
|
|
4477
|
+
return this._waitForAction()
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
await highlightActiveElement.call(this, el)
|
|
4481
|
+
let optionToSelect = option
|
|
4482
|
+
try {
|
|
4483
|
+
optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim()
|
|
4484
|
+
} catch (e) {
|
|
4485
|
+
optionToSelect = option
|
|
4486
|
+
}
|
|
4487
|
+
if (!Array.isArray(option)) option = [optionToSelect]
|
|
4488
|
+
await el.selectOption(option)
|
|
4489
|
+
return this._waitForAction()
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4493
|
+
const els = await findFields.call(this, field, context)
|
|
4614
4494
|
assertElementExists(els, field, 'Field')
|
|
4615
4495
|
const el = els[0]
|
|
4616
4496
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4724,6 +4604,13 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4724
4604
|
}
|
|
4725
4605
|
}
|
|
4726
4606
|
|
|
4607
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4608
|
+
if (elements.length > 1) {
|
|
4609
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4610
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4727
4614
|
function $XPath(element, selector) {
|
|
4728
4615
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
4729
4616
|
const res = []
|
|
@@ -4743,12 +4630,16 @@ async function targetCreatedHandler(page) {
|
|
|
4743
4630
|
.catch(() => null)
|
|
4744
4631
|
.then(async () => {
|
|
4745
4632
|
if (this.context && this.context._type === 'Frame') {
|
|
4746
|
-
// we are inside iframe
|
|
4633
|
+
// we are inside iframe via Frame object — refresh handle
|
|
4747
4634
|
const frameEl = await this.context.frameElement()
|
|
4748
4635
|
this.context = await frameEl.contentFrame()
|
|
4749
4636
|
this.contextLocator = null
|
|
4750
4637
|
return
|
|
4751
4638
|
}
|
|
4639
|
+
if (this.context && this.context.constructor && this.context.constructor.name === 'FrameLocator') {
|
|
4640
|
+
// we are inside iframe via FrameLocator — keep it across load events
|
|
4641
|
+
return
|
|
4642
|
+
}
|
|
4752
4643
|
// if context element was in iframe - keep it
|
|
4753
4644
|
// if (await this.context.ownerFrame()) return;
|
|
4754
4645
|
this.context = page
|
|
@@ -4929,7 +4820,7 @@ async function refreshContextSession() {
|
|
|
4929
4820
|
|
|
4930
4821
|
function saveVideoForPage(page, name) {
|
|
4931
4822
|
if (!page.video()) return null
|
|
4932
|
-
const fileName = `${`${
|
|
4823
|
+
const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
|
|
4933
4824
|
page
|
|
4934
4825
|
.video()
|
|
4935
4826
|
.saveAs(fileName)
|
|
@@ -4946,7 +4837,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4946
4837
|
if (!context) return
|
|
4947
4838
|
if (!context.tracing) return
|
|
4948
4839
|
try {
|
|
4949
|
-
const fileName = `${`${
|
|
4840
|
+
const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
4950
4841
|
await context.tracing.stop({ path: fileName })
|
|
4951
4842
|
return fileName
|
|
4952
4843
|
} catch (err) {
|