codeceptjs 4.0.1-beta.9 → 4.0.1
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 +17 -4
- 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 +14 -10
- 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 -2
- package/lib/command/run-workers.js +14 -16
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +117 -9
- package/lib/config.js +98 -19
- package/lib/container.js +188 -19
- 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 +367 -516
- package/lib/helper/Puppeteer.js +343 -197
- 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 +6 -15
- 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 +13 -28
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/test.js +4 -2
- package/lib/mocha/ui.js +5 -6
- package/lib/output.js +2 -2
- 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 +23 -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 +12 -4
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/loaderCheck.js +41 -3
- 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 +261 -49
- package/lib/utils.js +77 -3
- package/lib/workers.js +123 -17
- package/package.json +48 -43
- package/typings/index.d.ts +120 -9
- package/typings/promiseBasedTypes.d.ts +3243 -6057
- package/typings/types.d.ts +3541 -6506
- 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,26 +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
49
|
|
|
47
50
|
const popupStore = new Popup()
|
|
48
51
|
const consoleLogStore = new Console()
|
|
@@ -100,7 +103,6 @@ const pathSeparator = path.sep
|
|
|
100
103
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
101
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).
|
|
102
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).
|
|
103
|
-
* @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}"]`) } }`
|
|
104
106
|
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
|
|
105
107
|
* passed directly to `browser.newContext`.
|
|
106
108
|
* If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
|
|
@@ -356,28 +358,6 @@ class Playwright extends Helper {
|
|
|
356
358
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
357
359
|
this.cdpSession = null
|
|
358
360
|
|
|
359
|
-
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
360
|
-
// This can happen in worker threads where config is serialized/deserialized
|
|
361
|
-
let validCustomLocators = null
|
|
362
|
-
if (typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null) {
|
|
363
|
-
// Check if it's an empty array or object with no function properties
|
|
364
|
-
const entries = Object.entries(config.customLocatorStrategies)
|
|
365
|
-
const hasFunctions = entries.some(([_, value]) => typeof value === 'function')
|
|
366
|
-
if (hasFunctions) {
|
|
367
|
-
validCustomLocators = config.customLocatorStrategies
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
this.customLocatorStrategies = validCustomLocators
|
|
372
|
-
this._customLocatorsRegistered = false
|
|
373
|
-
|
|
374
|
-
// Add custom locator strategies to global registry for early registration
|
|
375
|
-
if (this.customLocatorStrategies) {
|
|
376
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
377
|
-
globalCustomLocatorStrategies.set(strategyName, strategyFunction)
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
361
|
// Add test failure tracking to prevent false positives
|
|
382
362
|
this.testFailures = []
|
|
383
363
|
this.hasCleanupError = false
|
|
@@ -417,6 +397,7 @@ class Playwright extends Helper {
|
|
|
417
397
|
highlightElement: false,
|
|
418
398
|
storageState: undefined,
|
|
419
399
|
onResponse: null,
|
|
400
|
+
strict: false,
|
|
420
401
|
}
|
|
421
402
|
|
|
422
403
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -465,7 +446,7 @@ class Playwright extends Helper {
|
|
|
465
446
|
this.options.recordVideo = { size }
|
|
466
447
|
}
|
|
467
448
|
if (this.options.recordVideo && !this.options.recordVideo.dir) {
|
|
468
|
-
this.options.recordVideo.dir = `${
|
|
449
|
+
this.options.recordVideo.dir = `${store.outputDir}/videos/`
|
|
469
450
|
}
|
|
470
451
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
471
452
|
this.isElectron = this.options.browser === 'electron'
|
|
@@ -523,32 +504,22 @@ class Playwright extends Helper {
|
|
|
523
504
|
}
|
|
524
505
|
}
|
|
525
506
|
|
|
526
|
-
// Ensure custom locators from this instance are in the global registry
|
|
527
|
-
// This is critical for worker threads where globalCustomLocatorStrategies is a new Map
|
|
528
|
-
if (this.customLocatorStrategies) {
|
|
529
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
530
|
-
if (!globalCustomLocatorStrategies.has(strategyName)) {
|
|
531
|
-
globalCustomLocatorStrategies.set(strategyName, strategyFunction)
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
507
|
// register an internal selector engine for reading value property of elements in a selector
|
|
537
508
|
try {
|
|
538
509
|
// Always wrap in try-catch since selectors might be registered globally across workers
|
|
539
510
|
// Check global flag to avoid re-registration in worker processes
|
|
540
|
-
if (!
|
|
511
|
+
if (!defaultSelectorEnginesInitialized) {
|
|
541
512
|
try {
|
|
542
513
|
await playwright.selectors.register('__value', createValueEngine)
|
|
543
514
|
await playwright.selectors.register('__disabled', createDisabledEngine)
|
|
544
|
-
|
|
515
|
+
defaultSelectorEnginesInitialized = true
|
|
545
516
|
defaultSelectorEnginesInitialized = true
|
|
546
517
|
} catch (e) {
|
|
547
518
|
if (!e.message.includes('already registered')) {
|
|
548
519
|
throw e
|
|
549
520
|
}
|
|
550
521
|
// Selector already registered globally by another worker
|
|
551
|
-
|
|
522
|
+
defaultSelectorEnginesInitialized = true
|
|
552
523
|
defaultSelectorEnginesInitialized = true
|
|
553
524
|
}
|
|
554
525
|
} else {
|
|
@@ -563,54 +534,6 @@ class Playwright extends Helper {
|
|
|
563
534
|
// Ignore if already set
|
|
564
535
|
}
|
|
565
536
|
}
|
|
566
|
-
|
|
567
|
-
// Register all custom locator strategies from the global registry
|
|
568
|
-
for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
|
|
569
|
-
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
570
|
-
try {
|
|
571
|
-
// Create a selector engine factory function exactly like createValueEngine pattern
|
|
572
|
-
// Capture variables in closure to avoid reference issues
|
|
573
|
-
const createCustomEngine = ((name, func) => {
|
|
574
|
-
return () => {
|
|
575
|
-
return {
|
|
576
|
-
create() {
|
|
577
|
-
return null
|
|
578
|
-
},
|
|
579
|
-
query(root, selector) {
|
|
580
|
-
try {
|
|
581
|
-
if (!root) return null
|
|
582
|
-
const result = func(selector, root)
|
|
583
|
-
return Array.isArray(result) ? result[0] : result
|
|
584
|
-
} catch (error) {
|
|
585
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
586
|
-
return null
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
queryAll(root, selector) {
|
|
590
|
-
try {
|
|
591
|
-
if (!root) return []
|
|
592
|
-
const result = func(selector, root)
|
|
593
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
594
|
-
} catch (error) {
|
|
595
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
596
|
-
return []
|
|
597
|
-
}
|
|
598
|
-
},
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
})(strategyName, strategyFunction)
|
|
602
|
-
|
|
603
|
-
await playwright.selectors.register(strategyName, createCustomEngine)
|
|
604
|
-
registeredCustomLocatorStrategies.add(strategyName)
|
|
605
|
-
} catch (error) {
|
|
606
|
-
if (!error.message.includes('already registered')) {
|
|
607
|
-
console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
|
|
608
|
-
} else {
|
|
609
|
-
console.log(`Custom locator strategy '${strategyName}' already registered`)
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
537
|
} catch (e) {
|
|
615
538
|
console.warn(e)
|
|
616
539
|
}
|
|
@@ -689,7 +612,7 @@ class Playwright extends Helper {
|
|
|
689
612
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
|
|
690
613
|
if (this.options.recordHar) {
|
|
691
614
|
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
|
|
692
|
-
const fileName = `${`${
|
|
615
|
+
const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
|
|
693
616
|
const dir = path.dirname(fileName)
|
|
694
617
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
695
618
|
this.options.recordHar.path = fileName
|
|
@@ -832,6 +755,11 @@ class Playwright extends Helper {
|
|
|
832
755
|
}
|
|
833
756
|
|
|
834
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
|
+
|
|
835
763
|
// Stop browser after suite completes
|
|
836
764
|
// For restart strategies: stop after each suite
|
|
837
765
|
// For session mode (restart:false): stop after the last suite
|
|
@@ -923,7 +851,7 @@ class Playwright extends Helper {
|
|
|
923
851
|
}
|
|
924
852
|
|
|
925
853
|
async _finishTest() {
|
|
926
|
-
if (
|
|
854
|
+
if (this.isRunning) {
|
|
927
855
|
try {
|
|
928
856
|
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Test finish timeout')), 10000))])
|
|
929
857
|
} catch (e) {
|
|
@@ -1277,30 +1205,6 @@ class Playwright extends Helper {
|
|
|
1277
1205
|
return this.browser
|
|
1278
1206
|
}
|
|
1279
1207
|
|
|
1280
|
-
_lookupCustomLocator(customStrategy) {
|
|
1281
|
-
if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
|
|
1282
|
-
return null
|
|
1283
|
-
}
|
|
1284
|
-
const strategy = this.customLocatorStrategies[customStrategy]
|
|
1285
|
-
return typeof strategy === 'function' ? strategy : null
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
_isCustomLocator(locator) {
|
|
1289
|
-
const locatorObj = new Locator(locator)
|
|
1290
|
-
if (locatorObj.isCustom()) {
|
|
1291
|
-
const customLocator = this._lookupCustomLocator(locatorObj.type)
|
|
1292
|
-
if (customLocator) {
|
|
1293
|
-
return true
|
|
1294
|
-
}
|
|
1295
|
-
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1296
|
-
}
|
|
1297
|
-
return false
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
_isCustomLocatorStrategyDefined() {
|
|
1301
|
-
return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
1208
|
/**
|
|
1305
1209
|
* Create a new browser context with a page. \
|
|
1306
1210
|
* Usually it should be run from a custom helper after call of `_startBrowser()`
|
|
@@ -1312,63 +1216,11 @@ class Playwright extends Helper {
|
|
|
1312
1216
|
}
|
|
1313
1217
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1314
1218
|
|
|
1315
|
-
// Register custom locator strategies for this context
|
|
1316
|
-
await this._registerCustomLocatorStrategies()
|
|
1317
|
-
|
|
1318
1219
|
const page = await this.browserContext.newPage()
|
|
1319
1220
|
targetCreatedHandler.call(this, page)
|
|
1320
1221
|
await this._setPage(page)
|
|
1321
1222
|
}
|
|
1322
1223
|
|
|
1323
|
-
async _registerCustomLocatorStrategies() {
|
|
1324
|
-
if (!this.customLocatorStrategies) return
|
|
1325
|
-
|
|
1326
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
1327
|
-
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
1328
|
-
try {
|
|
1329
|
-
const createCustomEngine = ((name, func) => {
|
|
1330
|
-
return () => {
|
|
1331
|
-
return {
|
|
1332
|
-
create(root, target) {
|
|
1333
|
-
return null
|
|
1334
|
-
},
|
|
1335
|
-
query(root, selector) {
|
|
1336
|
-
try {
|
|
1337
|
-
if (!root) return null
|
|
1338
|
-
const result = func(selector, root)
|
|
1339
|
-
return Array.isArray(result) ? result[0] : result
|
|
1340
|
-
} catch (error) {
|
|
1341
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1342
|
-
return null
|
|
1343
|
-
}
|
|
1344
|
-
},
|
|
1345
|
-
queryAll(root, selector) {
|
|
1346
|
-
try {
|
|
1347
|
-
if (!root) return []
|
|
1348
|
-
const result = func(selector, root)
|
|
1349
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
1350
|
-
} catch (error) {
|
|
1351
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1352
|
-
return []
|
|
1353
|
-
}
|
|
1354
|
-
},
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
})(strategyName, strategyFunction)
|
|
1358
|
-
|
|
1359
|
-
await playwright.selectors.register(strategyName, createCustomEngine)
|
|
1360
|
-
registeredCustomLocatorStrategies.add(strategyName)
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
if (!error.message.includes('already registered')) {
|
|
1363
|
-
console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
|
|
1364
|
-
} else {
|
|
1365
|
-
console.log(`Custom locator strategy '${strategyName}' already registered`)
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
1224
|
_getType() {
|
|
1373
1225
|
return this.browser._type
|
|
1374
1226
|
}
|
|
@@ -1389,6 +1241,7 @@ class Playwright extends Helper {
|
|
|
1389
1241
|
}
|
|
1390
1242
|
}
|
|
1391
1243
|
|
|
1244
|
+
// Close browserContext if recordHar is enabled
|
|
1392
1245
|
if (this.options.recordHar && this.browserContext) {
|
|
1393
1246
|
try {
|
|
1394
1247
|
await this.browserContext.close()
|
|
@@ -1398,16 +1251,16 @@ class Playwright extends Helper {
|
|
|
1398
1251
|
}
|
|
1399
1252
|
this.browserContext = null
|
|
1400
1253
|
|
|
1254
|
+
// Initiate browser close without waiting for it to complete
|
|
1255
|
+
// The browser process will be cleaned up when the Node process exits
|
|
1401
1256
|
if (this.browser) {
|
|
1402
1257
|
try {
|
|
1403
|
-
//
|
|
1404
|
-
|
|
1258
|
+
// Fire and forget - don't wait for close to complete
|
|
1259
|
+
this.browser.close().catch(() => {
|
|
1260
|
+
// Silently ignore any errors during async close
|
|
1261
|
+
})
|
|
1405
1262
|
} catch (e) {
|
|
1406
|
-
// Ignore
|
|
1407
|
-
if (!e.message?.includes('Browser close timeout')) {
|
|
1408
|
-
// Non-timeout error, can be ignored as well
|
|
1409
|
-
}
|
|
1410
|
-
// Force cleanup even on error
|
|
1263
|
+
// Ignore any synchronous errors
|
|
1411
1264
|
}
|
|
1412
1265
|
}
|
|
1413
1266
|
this.browser = null
|
|
@@ -1646,8 +1499,23 @@ class Playwright extends Helper {
|
|
|
1646
1499
|
*
|
|
1647
1500
|
*/
|
|
1648
1501
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1649
|
-
|
|
1650
|
-
|
|
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
|
+
}
|
|
1651
1519
|
|
|
1652
1520
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1653
1521
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1771,7 +1639,7 @@ class Playwright extends Helper {
|
|
|
1771
1639
|
* @returns Promise<void>
|
|
1772
1640
|
*/
|
|
1773
1641
|
async replayFromHar(harFilePath, opts) {
|
|
1774
|
-
const file = path.join(
|
|
1642
|
+
const file = path.join(store.codeceptDir, harFilePath)
|
|
1775
1643
|
|
|
1776
1644
|
if (!fileExists(file)) {
|
|
1777
1645
|
throw new Error(`File at ${file} cannot be found on local system`)
|
|
@@ -1911,7 +1779,11 @@ class Playwright extends Helper {
|
|
|
1911
1779
|
*/
|
|
1912
1780
|
async _locateElement(locator) {
|
|
1913
1781
|
const context = await this._getContext()
|
|
1914
|
-
|
|
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)
|
|
1915
1787
|
}
|
|
1916
1788
|
|
|
1917
1789
|
/**
|
|
@@ -1926,7 +1798,7 @@ class Playwright extends Helper {
|
|
|
1926
1798
|
const context = providedContext || (await this._getContext())
|
|
1927
1799
|
const els = await findCheckable.call(this, locator, context)
|
|
1928
1800
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1929
|
-
return els
|
|
1801
|
+
return selectElement(els, locator, this)
|
|
1930
1802
|
}
|
|
1931
1803
|
|
|
1932
1804
|
/**
|
|
@@ -2094,8 +1966,15 @@ class Playwright extends Helper {
|
|
|
2094
1966
|
* {{> seeElement }}
|
|
2095
1967
|
*
|
|
2096
1968
|
*/
|
|
2097
|
-
async seeElement(locator) {
|
|
2098
|
-
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
|
+
}
|
|
2099
1978
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2100
1979
|
try {
|
|
2101
1980
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2108,8 +1987,15 @@ class Playwright extends Helper {
|
|
|
2108
1987
|
* {{> dontSeeElement }}
|
|
2109
1988
|
*
|
|
2110
1989
|
*/
|
|
2111
|
-
async dontSeeElement(locator) {
|
|
2112
|
-
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
|
+
}
|
|
2113
1999
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2114
2000
|
try {
|
|
2115
2001
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2164,7 +2050,7 @@ class Playwright extends Helper {
|
|
|
2164
2050
|
const filePath = await download.path()
|
|
2165
2051
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
2166
2052
|
|
|
2167
|
-
const downloadPath = path.join(
|
|
2053
|
+
const downloadPath = path.join(store.outputDir, fileName)
|
|
2168
2054
|
if (!fs.existsSync(path.dirname(downloadPath))) {
|
|
2169
2055
|
fs.mkdirSync(path.dirname(downloadPath), '0777')
|
|
2170
2056
|
}
|
|
@@ -2195,15 +2081,6 @@ class Playwright extends Helper {
|
|
|
2195
2081
|
return proceedClick.call(this, locator, context, options)
|
|
2196
2082
|
}
|
|
2197
2083
|
|
|
2198
|
-
/**
|
|
2199
|
-
* Clicks link and waits for navigation (deprecated)
|
|
2200
|
-
*/
|
|
2201
|
-
async clickLink(locator, context = null) {
|
|
2202
|
-
console.log('clickLink deprecated: Playwright automatically waits for navigation to happen.')
|
|
2203
|
-
console.log('Replace I.clickLink with I.click')
|
|
2204
|
-
return this.click(locator, context)
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
2084
|
/**
|
|
2208
2085
|
* {{> forceClick }}
|
|
2209
2086
|
*/
|
|
@@ -2348,6 +2225,7 @@ class Playwright extends Helper {
|
|
|
2348
2225
|
* {{> pressKeyWithKeyNormalization }}
|
|
2349
2226
|
*/
|
|
2350
2227
|
async pressKey(key) {
|
|
2228
|
+
await checkFocusBeforePressKey(this, key)
|
|
2351
2229
|
const modifiers = []
|
|
2352
2230
|
if (Array.isArray(key)) {
|
|
2353
2231
|
for (let k of key) {
|
|
@@ -2376,6 +2254,8 @@ class Playwright extends Helper {
|
|
|
2376
2254
|
* {{> type }}
|
|
2377
2255
|
*/
|
|
2378
2256
|
async type(keys, delay = null) {
|
|
2257
|
+
await checkFocusBeforeType(this)
|
|
2258
|
+
|
|
2379
2259
|
// Always use page.keyboard.type for any string (including single character and national characters).
|
|
2380
2260
|
if (!Array.isArray(keys)) {
|
|
2381
2261
|
keys = keys.toString()
|
|
@@ -2395,43 +2275,33 @@ class Playwright extends Helper {
|
|
|
2395
2275
|
* {{> fillField }}
|
|
2396
2276
|
*
|
|
2397
2277
|
*/
|
|
2398
|
-
async fillField(field, value) {
|
|
2399
|
-
const els = await findFields.call(this, field)
|
|
2278
|
+
async fillField(field, value, context = null) {
|
|
2279
|
+
const els = await findFields.call(this, field, context)
|
|
2400
2280
|
assertElementExists(els, field, 'Field')
|
|
2401
|
-
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
|
+
}
|
|
2402
2288
|
|
|
2403
2289
|
await el.clear()
|
|
2404
2290
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
2405
2291
|
|
|
2406
|
-
await highlightActiveElement.call(this, el)
|
|
2407
|
-
|
|
2408
2292
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2409
2293
|
|
|
2410
2294
|
return this._waitForAction()
|
|
2411
2295
|
}
|
|
2412
2296
|
|
|
2413
2297
|
/**
|
|
2414
|
-
*
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
* Examples:
|
|
2418
|
-
*
|
|
2419
|
-
* ```js
|
|
2420
|
-
* I.clearField('.text-area')
|
|
2421
|
-
*
|
|
2422
|
-
* // if this doesn't work use force option
|
|
2423
|
-
* I.clearField('#submit', { force: true })
|
|
2424
|
-
* ```
|
|
2425
|
-
* Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
2426
|
-
*
|
|
2427
|
-
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
2428
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
2298
|
+
* {{> clearField }}
|
|
2429
2299
|
*/
|
|
2430
|
-
async clearField(locator,
|
|
2431
|
-
const els = await findFields.call(this, locator)
|
|
2300
|
+
async clearField(locator, context = null) {
|
|
2301
|
+
const els = await findFields.call(this, locator, context)
|
|
2432
2302
|
assertElementExists(els, locator, 'Field to clear')
|
|
2433
2303
|
|
|
2434
|
-
const el = els
|
|
2304
|
+
const el = selectElement(els, locator, this)
|
|
2435
2305
|
|
|
2436
2306
|
await highlightActiveElement.call(this, el)
|
|
2437
2307
|
|
|
@@ -2443,68 +2313,101 @@ class Playwright extends Helper {
|
|
|
2443
2313
|
/**
|
|
2444
2314
|
* {{> appendField }}
|
|
2445
2315
|
*/
|
|
2446
|
-
async appendField(field, value) {
|
|
2447
|
-
const els = await findFields.call(this, field)
|
|
2316
|
+
async appendField(field, value, context = null) {
|
|
2317
|
+
const els = await findFields.call(this, field, context)
|
|
2448
2318
|
assertElementExists(els, field, 'Field')
|
|
2449
|
-
|
|
2450
|
-
await
|
|
2451
|
-
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 })
|
|
2452
2323
|
return this._waitForAction()
|
|
2453
2324
|
}
|
|
2454
2325
|
|
|
2455
2326
|
/**
|
|
2456
2327
|
* {{> seeInField }}
|
|
2457
2328
|
*/
|
|
2458
|
-
async seeInField(field, value) {
|
|
2329
|
+
async seeInField(field, value, context = null) {
|
|
2459
2330
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2460
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2331
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2461
2332
|
}
|
|
2462
2333
|
|
|
2463
2334
|
/**
|
|
2464
2335
|
* {{> dontSeeInField }}
|
|
2465
2336
|
*/
|
|
2466
|
-
async dontSeeInField(field, value) {
|
|
2337
|
+
async dontSeeInField(field, value, context = null) {
|
|
2467
2338
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2468
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2339
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2469
2340
|
}
|
|
2470
2341
|
|
|
2471
2342
|
/**
|
|
2472
2343
|
* {{> attachFile }}
|
|
2473
2344
|
*
|
|
2474
2345
|
*/
|
|
2475
|
-
async attachFile(locator, pathToFile) {
|
|
2476
|
-
const file = path.join(
|
|
2346
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2347
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
2477
2348
|
|
|
2478
2349
|
if (!fileExists(file)) {
|
|
2479
2350
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2480
2351
|
}
|
|
2481
|
-
const els = await findFields.call(this, locator)
|
|
2482
|
-
|
|
2483
|
-
|
|
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)
|
|
2484
2372
|
return this._waitForAction()
|
|
2485
2373
|
}
|
|
2486
2374
|
|
|
2487
2375
|
/**
|
|
2488
2376
|
* {{> selectOption }}
|
|
2489
2377
|
*/
|
|
2490
|
-
async selectOption(select, option) {
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
const el = els[0]
|
|
2378
|
+
async selectOption(select, option, context = null) {
|
|
2379
|
+
const pageContext = await this.context
|
|
2380
|
+
const matchedLocator = new Locator(select)
|
|
2494
2381
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2382
|
+
let contextEl
|
|
2383
|
+
if (context) {
|
|
2384
|
+
const contextEls = await this._locate(context)
|
|
2385
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2386
|
+
contextEl = contextEls[0]
|
|
2387
|
+
}
|
|
2497
2388
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
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)
|
|
2502
2395
|
}
|
|
2503
2396
|
|
|
2504
|
-
|
|
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)
|
|
2505
2402
|
|
|
2506
|
-
|
|
2507
|
-
|
|
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)
|
|
2508
2411
|
}
|
|
2509
2412
|
|
|
2510
2413
|
/**
|
|
@@ -2545,6 +2448,26 @@ class Playwright extends Helper {
|
|
|
2545
2448
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
2546
2449
|
}
|
|
2547
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
|
+
|
|
2548
2471
|
/**
|
|
2549
2472
|
* {{> see }}
|
|
2550
2473
|
*
|
|
@@ -2747,8 +2670,11 @@ class Playwright extends Helper {
|
|
|
2747
2670
|
* @returns {Promise<any>}
|
|
2748
2671
|
*/
|
|
2749
2672
|
async executeScript(fn, arg) {
|
|
2750
|
-
if (
|
|
2751
|
-
|
|
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') {
|
|
2752
2678
|
return this.context.locator(':root').evaluate(fn, arg)
|
|
2753
2679
|
}
|
|
2754
2680
|
return this.page.evaluate.apply(this.page, [fn, arg])
|
|
@@ -2762,23 +2688,12 @@ class Playwright extends Helper {
|
|
|
2762
2688
|
_contextLocator(locator) {
|
|
2763
2689
|
const locatorObj = new Locator(locator, 'css')
|
|
2764
2690
|
|
|
2765
|
-
// Handle custom locators differently
|
|
2766
|
-
if (locatorObj.isCustom()) {
|
|
2767
|
-
return buildCustomLocatorString(locatorObj)
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
2691
|
locator = buildLocatorString(locatorObj)
|
|
2771
2692
|
|
|
2772
2693
|
if (this.contextLocator) {
|
|
2773
2694
|
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
// Instead, we'll need to handle this differently in the calling methods
|
|
2777
|
-
return locator
|
|
2778
|
-
} else {
|
|
2779
|
-
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2780
|
-
locator = `${contextLocator} >> ${locator}`
|
|
2781
|
-
}
|
|
2695
|
+
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2696
|
+
locator = `${contextLocator} >> ${locator}`
|
|
2782
2697
|
}
|
|
2783
2698
|
|
|
2784
2699
|
return locator
|
|
@@ -2789,43 +2704,28 @@ class Playwright extends Helper {
|
|
|
2789
2704
|
*
|
|
2790
2705
|
*/
|
|
2791
2706
|
async grabTextFrom(locator) {
|
|
2792
|
-
|
|
2793
|
-
if (
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
this.debugSection('Text', text)
|
|
2799
|
-
return text
|
|
2800
|
-
}
|
|
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
|
|
2801
2713
|
}
|
|
2802
2714
|
|
|
2803
2715
|
const locatorObj = new Locator(locator, 'css')
|
|
2804
2716
|
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
throw new Error(`Element not found: ${locatorObj.toString()}`)
|
|
2810
|
-
}
|
|
2811
|
-
const text = await elements[0].textContent()
|
|
2812
|
-
assertElementExists(text, locatorObj.toString())
|
|
2717
|
+
locator = this._contextLocator(locator)
|
|
2718
|
+
try {
|
|
2719
|
+
const text = await this.page.textContent(locator)
|
|
2720
|
+
assertElementExists(text, locator)
|
|
2813
2721
|
this.debugSection('Text', text)
|
|
2814
2722
|
return text
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
assertElementExists(text, locator)
|
|
2820
|
-
this.debugSection('Text', text)
|
|
2821
|
-
return text
|
|
2822
|
-
} catch (error) {
|
|
2823
|
-
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2824
|
-
if (error.message && error.message.includes('Timeout')) {
|
|
2825
|
-
throw new ElementNotFound(locator, 'text')
|
|
2826
|
-
}
|
|
2827
|
-
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')
|
|
2828
2727
|
}
|
|
2728
|
+
throw error
|
|
2829
2729
|
}
|
|
2830
2730
|
}
|
|
2831
2731
|
|
|
@@ -3037,7 +2937,7 @@ class Playwright extends Helper {
|
|
|
3037
2937
|
const els = await this._locate(matchedLocator)
|
|
3038
2938
|
assertElementExists(els, locator)
|
|
3039
2939
|
const snapshot = await els[0].ariaSnapshot()
|
|
3040
|
-
this.debugSection('Aria Snapshot', snapshot)
|
|
2940
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
3041
2941
|
return snapshot
|
|
3042
2942
|
}
|
|
3043
2943
|
|
|
@@ -3390,24 +3290,13 @@ class Playwright extends Helper {
|
|
|
3390
3290
|
|
|
3391
3291
|
const context = await this._getContext()
|
|
3392
3292
|
try {
|
|
3393
|
-
|
|
3394
|
-
// For custom locators, we need to use our custom element finding logic
|
|
3395
|
-
const elements = await findCustomElements.call(this, context, locator)
|
|
3396
|
-
if (elements.length === 0) {
|
|
3397
|
-
throw new Error(`Custom locator ${locator.type}=${locator.value} not found`)
|
|
3398
|
-
}
|
|
3399
|
-
await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3400
|
-
} else {
|
|
3401
|
-
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3402
|
-
}
|
|
3293
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3403
3294
|
} catch (e) {
|
|
3404
3295
|
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
3405
3296
|
}
|
|
3406
3297
|
}
|
|
3407
3298
|
|
|
3408
3299
|
/**
|
|
3409
|
-
* This method accepts [React selectors](https://codecept.io/react).
|
|
3410
|
-
*
|
|
3411
3300
|
* {{> waitForVisible }}
|
|
3412
3301
|
*/
|
|
3413
3302
|
async waitForVisible(locator, sec) {
|
|
@@ -3417,26 +3306,6 @@ class Playwright extends Helper {
|
|
|
3417
3306
|
const context = await this._getContext()
|
|
3418
3307
|
let count = 0
|
|
3419
3308
|
|
|
3420
|
-
// Handle custom locators
|
|
3421
|
-
if (locator.isCustom()) {
|
|
3422
|
-
let waiter
|
|
3423
|
-
do {
|
|
3424
|
-
const elements = await findCustomElements.call(this, context, locator)
|
|
3425
|
-
if (elements.length > 0) {
|
|
3426
|
-
waiter = await elements[0].isVisible()
|
|
3427
|
-
} else {
|
|
3428
|
-
waiter = false
|
|
3429
|
-
}
|
|
3430
|
-
if (!waiter) {
|
|
3431
|
-
await this.wait(1)
|
|
3432
|
-
count += 1000
|
|
3433
|
-
}
|
|
3434
|
-
} while (!waiter && count <= waitTimeout)
|
|
3435
|
-
|
|
3436
|
-
if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`)
|
|
3437
|
-
return
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
3309
|
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
3441
3310
|
let waiter
|
|
3442
3311
|
if (this.frame) {
|
|
@@ -3540,7 +3409,7 @@ class Playwright extends Helper {
|
|
|
3540
3409
|
}
|
|
3541
3410
|
|
|
3542
3411
|
async _getContext() {
|
|
3543
|
-
if (
|
|
3412
|
+
if (this.context) {
|
|
3544
3413
|
return this.context
|
|
3545
3414
|
}
|
|
3546
3415
|
if (this.frame) {
|
|
@@ -3554,6 +3423,7 @@ class Playwright extends Helper {
|
|
|
3554
3423
|
*/
|
|
3555
3424
|
async waitInUrl(urlPart, sec = null) {
|
|
3556
3425
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3426
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3557
3427
|
|
|
3558
3428
|
return this.page
|
|
3559
3429
|
.waitForFunction(
|
|
@@ -3561,13 +3431,13 @@ class Playwright extends Helper {
|
|
|
3561
3431
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3562
3432
|
return currUrl.indexOf(urlPart) > -1
|
|
3563
3433
|
},
|
|
3564
|
-
|
|
3434
|
+
expectedUrl,
|
|
3565
3435
|
{ timeout: waitTimeout },
|
|
3566
3436
|
)
|
|
3567
3437
|
.catch(async e => {
|
|
3568
|
-
const currUrl = await this._getPageUrl()
|
|
3438
|
+
const currUrl = await this._getPageUrl()
|
|
3569
3439
|
if (/Timeout/i.test(e.message)) {
|
|
3570
|
-
throw new Error(`expected url to include ${
|
|
3440
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3571
3441
|
} else {
|
|
3572
3442
|
throw e
|
|
3573
3443
|
}
|
|
@@ -3579,29 +3449,50 @@ class Playwright extends Helper {
|
|
|
3579
3449
|
*/
|
|
3580
3450
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3581
3451
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3452
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3582
3453
|
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
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
|
+
}
|
|
3586
3466
|
}
|
|
3467
|
+
}
|
|
3587
3468
|
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
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
|
|
3593
3482
|
},
|
|
3594
|
-
|
|
3483
|
+
normalizedPath,
|
|
3595
3484
|
{ timeout: waitTimeout },
|
|
3596
3485
|
)
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
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
|
+
}
|
|
3605
3496
|
}
|
|
3606
3497
|
|
|
3607
3498
|
/**
|
|
@@ -3616,15 +3507,6 @@ class Playwright extends Helper {
|
|
|
3616
3507
|
if (context) {
|
|
3617
3508
|
const locator = new Locator(context, 'css')
|
|
3618
3509
|
try {
|
|
3619
|
-
if (locator.isCustom()) {
|
|
3620
|
-
// For custom locators, find the elements first then check for text within them
|
|
3621
|
-
const elements = await findCustomElements.call(this, contextObject, locator)
|
|
3622
|
-
if (elements.length === 0) {
|
|
3623
|
-
throw new Error(`Context element not found: ${locator.toString()}`)
|
|
3624
|
-
}
|
|
3625
|
-
return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
3626
|
-
}
|
|
3627
|
-
|
|
3628
3510
|
if (!locator.isXPath()) {
|
|
3629
3511
|
return contextObject
|
|
3630
3512
|
.locator(`${locator.simplify()} >> text=${text}`)
|
|
@@ -3867,7 +3749,7 @@ class Playwright extends Helper {
|
|
|
3867
3749
|
if (!locator.isXPath()) {
|
|
3868
3750
|
try {
|
|
3869
3751
|
await context
|
|
3870
|
-
.locator(
|
|
3752
|
+
.locator(locator.simplify())
|
|
3871
3753
|
.first()
|
|
3872
3754
|
.waitFor({ timeout: waitTimeout, state: 'detached' })
|
|
3873
3755
|
} catch (e) {
|
|
@@ -4292,51 +4174,54 @@ class Playwright extends Helper {
|
|
|
4292
4174
|
|
|
4293
4175
|
export default Playwright
|
|
4294
4176
|
|
|
4295
|
-
function
|
|
4296
|
-
// Note: this.debug not available in standalone function, using console.log
|
|
4297
|
-
console.log(`Building custom locator string: ${locator.type}=${locator.value}`)
|
|
4298
|
-
return `${locator.type}=${locator.value}`
|
|
4299
|
-
}
|
|
4300
|
-
|
|
4301
|
-
function buildLocatorString(locator) {
|
|
4302
|
-
if (locator.isCustom()) {
|
|
4303
|
-
return buildCustomLocatorString(locator)
|
|
4304
|
-
}
|
|
4177
|
+
export function buildLocatorString(locator) {
|
|
4305
4178
|
if (locator.isXPath()) {
|
|
4306
|
-
|
|
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(' >> ')
|
|
4307
4192
|
}
|
|
4308
4193
|
return locator.simplify()
|
|
4309
4194
|
}
|
|
4310
4195
|
|
|
4311
|
-
/**
|
|
4312
|
-
* Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4313
|
-
*/
|
|
4314
|
-
function isRoleLocatorObject(locator) {
|
|
4315
|
-
return locator && typeof locator === 'object' && locator.role && !locator.type
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
4196
|
/**
|
|
4319
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.
|
|
4320
4199
|
* Returns elements array if role locator, null otherwise
|
|
4321
4200
|
*/
|
|
4322
4201
|
async function handleRoleLocator(context, locator) {
|
|
4323
|
-
|
|
4202
|
+
const loc = new Locator(locator)
|
|
4203
|
+
if (!loc.isRole()) return null
|
|
4324
4204
|
|
|
4205
|
+
const roleObj = loc.locator || {}
|
|
4325
4206
|
const options = {}
|
|
4326
|
-
if (
|
|
4327
|
-
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
|
|
4328
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
|
|
4329
4219
|
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4330
4220
|
}
|
|
4331
4221
|
|
|
4332
4222
|
async function findElements(matcher, locator) {
|
|
4333
|
-
// Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
|
|
4334
|
-
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4335
|
-
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4336
4223
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4337
4224
|
|
|
4338
|
-
if (isReactLocator) return findReact(matcher, locator)
|
|
4339
|
-
if (isVueLocator) return findVue(matcher, locator)
|
|
4340
4225
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4341
4226
|
|
|
4342
4227
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
@@ -4345,122 +4230,12 @@ async function findElements(matcher, locator) {
|
|
|
4345
4230
|
|
|
4346
4231
|
locator = new Locator(locator, 'css')
|
|
4347
4232
|
|
|
4348
|
-
// Handle custom locators directly instead of relying on Playwright selector engines
|
|
4349
|
-
if (locator.isCustom()) {
|
|
4350
|
-
return findCustomElements.call(this, matcher, locator)
|
|
4351
|
-
}
|
|
4352
|
-
|
|
4353
|
-
// Check if we have a custom context locator and need to search within it
|
|
4354
|
-
if (this.contextLocator) {
|
|
4355
|
-
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
4356
|
-
if (contextLocatorObj.isCustom()) {
|
|
4357
|
-
// Find the context elements first
|
|
4358
|
-
const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj)
|
|
4359
|
-
if (contextElements.length === 0) {
|
|
4360
|
-
return []
|
|
4361
|
-
}
|
|
4362
|
-
|
|
4363
|
-
// Search within the first context element
|
|
4364
|
-
const locatorString = buildLocatorString(locator)
|
|
4365
|
-
return contextElements[0].locator(locatorString).all()
|
|
4366
|
-
}
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
4233
|
const locatorString = buildLocatorString(locator)
|
|
4370
4234
|
|
|
4371
4235
|
return matcher.locator(locatorString).all()
|
|
4372
4236
|
}
|
|
4373
4237
|
|
|
4374
|
-
async function findCustomElements(matcher, locator) {
|
|
4375
|
-
// Always prioritize this.customLocatorStrategies which is set in constructor from config
|
|
4376
|
-
// and persists in every worker thread instance
|
|
4377
|
-
let strategyFunction = null
|
|
4378
|
-
|
|
4379
|
-
if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
|
|
4380
|
-
strategyFunction = this.customLocatorStrategies[locator.type]
|
|
4381
|
-
} else if (globalCustomLocatorStrategies.has(locator.type)) {
|
|
4382
|
-
// Fallback to global registry (populated in constructor and _init)
|
|
4383
|
-
strategyFunction = globalCustomLocatorStrategies.get(locator.type)
|
|
4384
|
-
}
|
|
4385
|
-
|
|
4386
|
-
if (!strategyFunction) {
|
|
4387
|
-
throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
|
|
4388
|
-
}
|
|
4389
|
-
|
|
4390
|
-
// Execute the custom locator function in the browser context using page.evaluate
|
|
4391
|
-
const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page()
|
|
4392
|
-
|
|
4393
|
-
const elements = await page.evaluate(
|
|
4394
|
-
({ strategyCode, selector }) => {
|
|
4395
|
-
const strategy = new Function('return ' + strategyCode)()
|
|
4396
|
-
const result = strategy(selector, document)
|
|
4397
|
-
|
|
4398
|
-
// Convert NodeList or single element to array
|
|
4399
|
-
if (result && result.nodeType) {
|
|
4400
|
-
return [result]
|
|
4401
|
-
} else if (result && result.length !== undefined) {
|
|
4402
|
-
return Array.from(result)
|
|
4403
|
-
} else if (Array.isArray(result)) {
|
|
4404
|
-
return result
|
|
4405
|
-
}
|
|
4406
|
-
|
|
4407
|
-
return []
|
|
4408
|
-
},
|
|
4409
|
-
{
|
|
4410
|
-
strategyCode: strategyFunction.toString(),
|
|
4411
|
-
selector: locator.value,
|
|
4412
|
-
},
|
|
4413
|
-
)
|
|
4414
|
-
|
|
4415
|
-
// Convert the found elements back to Playwright locators
|
|
4416
|
-
if (elements.length === 0) {
|
|
4417
|
-
return []
|
|
4418
|
-
}
|
|
4419
|
-
|
|
4420
|
-
// Create CSS selectors for the found elements and return as locators
|
|
4421
|
-
const locators = []
|
|
4422
|
-
const timestamp = Date.now()
|
|
4423
|
-
|
|
4424
|
-
for (let i = 0; i < elements.length; i++) {
|
|
4425
|
-
// Use a unique attribute approach to target specific elements
|
|
4426
|
-
const uniqueAttr = `data-codecept-custom-${timestamp}-${i}`
|
|
4427
|
-
|
|
4428
|
-
await page.evaluate(
|
|
4429
|
-
({ index, uniqueAttr, strategyCode, selector }) => {
|
|
4430
|
-
// Re-execute the strategy to find elements and mark the specific one
|
|
4431
|
-
const strategy = new Function('return ' + strategyCode)()
|
|
4432
|
-
const result = strategy(selector, document)
|
|
4433
|
-
|
|
4434
|
-
let elementsArray = []
|
|
4435
|
-
if (result && result.nodeType) {
|
|
4436
|
-
elementsArray = [result]
|
|
4437
|
-
} else if (result && result.length !== undefined) {
|
|
4438
|
-
elementsArray = Array.from(result)
|
|
4439
|
-
} else if (Array.isArray(result)) {
|
|
4440
|
-
elementsArray = result
|
|
4441
|
-
}
|
|
4442
|
-
|
|
4443
|
-
if (elementsArray[index]) {
|
|
4444
|
-
elementsArray[index].setAttribute(uniqueAttr, 'true')
|
|
4445
|
-
}
|
|
4446
|
-
},
|
|
4447
|
-
{
|
|
4448
|
-
index: i,
|
|
4449
|
-
uniqueAttr,
|
|
4450
|
-
strategyCode: strategyFunction.toString(),
|
|
4451
|
-
selector: locator.value,
|
|
4452
|
-
},
|
|
4453
|
-
)
|
|
4454
|
-
|
|
4455
|
-
locators.push(page.locator(`[${uniqueAttr}="true"]`))
|
|
4456
|
-
}
|
|
4457
|
-
|
|
4458
|
-
return locators
|
|
4459
|
-
}
|
|
4460
|
-
|
|
4461
4238
|
async function findElement(matcher, locator) {
|
|
4462
|
-
if (locator.react) return findReact(matcher, locator)
|
|
4463
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
4464
4239
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4465
4240
|
|
|
4466
4241
|
locator = new Locator(locator, 'css')
|
|
@@ -4495,16 +4270,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4495
4270
|
assertElementExists(els, locator, 'Clickable element')
|
|
4496
4271
|
}
|
|
4497
4272
|
|
|
4498
|
-
|
|
4499
|
-
|
|
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))
|
|
4500
4285
|
|
|
4501
|
-
/*
|
|
4502
|
-
using the force true options itself but instead dispatching a click
|
|
4503
|
-
*/
|
|
4504
4286
|
if (options.force) {
|
|
4505
|
-
await
|
|
4287
|
+
await element.dispatchEvent('click')
|
|
4506
4288
|
} else {
|
|
4507
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4508
4289
|
await element.click(options)
|
|
4509
4290
|
}
|
|
4510
4291
|
const promises = []
|
|
@@ -4519,7 +4300,10 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4519
4300
|
async function findClickable(matcher, locator) {
|
|
4520
4301
|
const matchedLocator = new Locator(locator)
|
|
4521
4302
|
|
|
4522
|
-
if (!matchedLocator.isFuzzy())
|
|
4303
|
+
if (!matchedLocator.isFuzzy()) {
|
|
4304
|
+
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4305
|
+
return els
|
|
4306
|
+
}
|
|
4523
4307
|
|
|
4524
4308
|
let els
|
|
4525
4309
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
@@ -4561,7 +4345,9 @@ async function proceedSee(assertType, text, context, strict = false) {
|
|
|
4561
4345
|
if (!context) {
|
|
4562
4346
|
const el = await this.context
|
|
4563
4347
|
|
|
4564
|
-
allText = el.
|
|
4348
|
+
allText = typeof el.url !== 'function' && typeof el.innerText === 'function'
|
|
4349
|
+
? [await el.innerText()]
|
|
4350
|
+
: [await el.locator('body').innerText()]
|
|
4565
4351
|
|
|
4566
4352
|
description = 'web application'
|
|
4567
4353
|
} else {
|
|
@@ -4619,38 +4405,92 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4619
4405
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4620
4406
|
}
|
|
4621
4407
|
|
|
4622
|
-
async function findFields(locator) {
|
|
4623
|
-
|
|
4624
|
-
if (
|
|
4625
|
-
const
|
|
4626
|
-
|
|
4627
|
-
|
|
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]
|
|
4628
4414
|
}
|
|
4629
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
|
+
|
|
4630
4424
|
const matchedLocator = new Locator(locator)
|
|
4631
4425
|
if (!matchedLocator.isFuzzy()) {
|
|
4632
|
-
return
|
|
4426
|
+
return locateFn(matchedLocator)
|
|
4633
4427
|
}
|
|
4634
4428
|
const literal = xpathLocator.literal(locator)
|
|
4635
4429
|
|
|
4636
|
-
let els = await
|
|
4430
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4637
4431
|
if (els.length) {
|
|
4638
4432
|
return els
|
|
4639
4433
|
}
|
|
4640
4434
|
|
|
4641
|
-
els = await
|
|
4435
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4642
4436
|
if (els.length) {
|
|
4643
4437
|
return els
|
|
4644
4438
|
}
|
|
4645
|
-
els = await
|
|
4439
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4646
4440
|
if (els.length) {
|
|
4647
4441
|
return els
|
|
4648
4442
|
}
|
|
4649
|
-
return
|
|
4443
|
+
return locateFn({ css: locator })
|
|
4650
4444
|
}
|
|
4651
4445
|
|
|
4652
|
-
async function
|
|
4653
|
-
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)
|
|
4654
4494
|
assertElementExists(els, field, 'Field')
|
|
4655
4495
|
const el = els[0]
|
|
4656
4496
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4764,6 +4604,13 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4764
4604
|
}
|
|
4765
4605
|
}
|
|
4766
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
|
+
|
|
4767
4614
|
function $XPath(element, selector) {
|
|
4768
4615
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
4769
4616
|
const res = []
|
|
@@ -4783,12 +4630,16 @@ async function targetCreatedHandler(page) {
|
|
|
4783
4630
|
.catch(() => null)
|
|
4784
4631
|
.then(async () => {
|
|
4785
4632
|
if (this.context && this.context._type === 'Frame') {
|
|
4786
|
-
// we are inside iframe
|
|
4633
|
+
// we are inside iframe via Frame object — refresh handle
|
|
4787
4634
|
const frameEl = await this.context.frameElement()
|
|
4788
4635
|
this.context = await frameEl.contentFrame()
|
|
4789
4636
|
this.contextLocator = null
|
|
4790
4637
|
return
|
|
4791
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
|
+
}
|
|
4792
4643
|
// if context element was in iframe - keep it
|
|
4793
4644
|
// if (await this.context.ownerFrame()) return;
|
|
4794
4645
|
this.context = page
|
|
@@ -4969,7 +4820,7 @@ async function refreshContextSession() {
|
|
|
4969
4820
|
|
|
4970
4821
|
function saveVideoForPage(page, name) {
|
|
4971
4822
|
if (!page.video()) return null
|
|
4972
|
-
const fileName = `${`${
|
|
4823
|
+
const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
|
|
4973
4824
|
page
|
|
4974
4825
|
.video()
|
|
4975
4826
|
.saveAs(fileName)
|
|
@@ -4986,7 +4837,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4986
4837
|
if (!context) return
|
|
4987
4838
|
if (!context.tracing) return
|
|
4988
4839
|
try {
|
|
4989
|
-
const fileName = `${`${
|
|
4840
|
+
const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
4990
4841
|
await context.tracing.stop({ path: fileName })
|
|
4991
4842
|
return fileName
|
|
4992
4843
|
} catch (err) {
|