codeceptjs 4.0.0-rc.2 → 4.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -27
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +230 -0
- package/docs/continuous-integration.md +497 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +27 -24
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +6 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +233 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -3
- package/lib/mocha/cli.js +14 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +456 -0
- package/lib/plugin/analyze.js +6 -5
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +32 -22
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +77 -3
- package/lib/workers.js +63 -25
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
package/lib/helper/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,7 +24,11 @@ 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'
|
|
@@ -31,17 +36,16 @@ import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
|
31
36
|
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
|
|
32
37
|
import Popup from './extras/Popup.js'
|
|
33
38
|
import Console from './extras/Console.js'
|
|
34
|
-
import { findReact,
|
|
39
|
+
import { findReact, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
40
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
35
41
|
import WebElement from '../element/WebElement.js'
|
|
42
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
43
|
+
import { fillRichEditor } from './extras/richTextEditor.js'
|
|
36
44
|
|
|
37
45
|
let playwright
|
|
38
46
|
let perfTiming
|
|
39
47
|
let defaultSelectorEnginesInitialized = false
|
|
40
48
|
|
|
41
|
-
// Use global object to track selector registration across workers
|
|
42
|
-
if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
43
|
-
global.__playwrightSelectorsRegistered = false
|
|
44
|
-
}
|
|
45
49
|
|
|
46
50
|
const popupStore = new Popup()
|
|
47
51
|
const consoleLogStore = new Console()
|
|
@@ -442,7 +446,7 @@ class Playwright extends Helper {
|
|
|
442
446
|
this.options.recordVideo = { size }
|
|
443
447
|
}
|
|
444
448
|
if (this.options.recordVideo && !this.options.recordVideo.dir) {
|
|
445
|
-
this.options.recordVideo.dir = `${
|
|
449
|
+
this.options.recordVideo.dir = `${store.outputDir}/videos/`
|
|
446
450
|
}
|
|
447
451
|
this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint
|
|
448
452
|
this.isElectron = this.options.browser === 'electron'
|
|
@@ -504,18 +508,18 @@ class Playwright extends Helper {
|
|
|
504
508
|
try {
|
|
505
509
|
// Always wrap in try-catch since selectors might be registered globally across workers
|
|
506
510
|
// Check global flag to avoid re-registration in worker processes
|
|
507
|
-
if (!
|
|
511
|
+
if (!defaultSelectorEnginesInitialized) {
|
|
508
512
|
try {
|
|
509
513
|
await playwright.selectors.register('__value', createValueEngine)
|
|
510
514
|
await playwright.selectors.register('__disabled', createDisabledEngine)
|
|
511
|
-
|
|
515
|
+
defaultSelectorEnginesInitialized = true
|
|
512
516
|
defaultSelectorEnginesInitialized = true
|
|
513
517
|
} catch (e) {
|
|
514
518
|
if (!e.message.includes('already registered')) {
|
|
515
519
|
throw e
|
|
516
520
|
}
|
|
517
521
|
// Selector already registered globally by another worker
|
|
518
|
-
|
|
522
|
+
defaultSelectorEnginesInitialized = true
|
|
519
523
|
defaultSelectorEnginesInitialized = true
|
|
520
524
|
}
|
|
521
525
|
} else {
|
|
@@ -608,7 +612,7 @@ class Playwright extends Helper {
|
|
|
608
612
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo
|
|
609
613
|
if (this.options.recordHar) {
|
|
610
614
|
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har'
|
|
611
|
-
const fileName = `${`${
|
|
615
|
+
const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`
|
|
612
616
|
const dir = path.dirname(fileName)
|
|
613
617
|
if (!fileExists(dir)) fs.mkdirSync(dir)
|
|
614
618
|
this.options.recordHar.path = fileName
|
|
@@ -751,6 +755,11 @@ class Playwright extends Helper {
|
|
|
751
755
|
}
|
|
752
756
|
|
|
753
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
|
+
|
|
754
763
|
// Stop browser after suite completes
|
|
755
764
|
// For restart strategies: stop after each suite
|
|
756
765
|
// For session mode (restart:false): stop after the last suite
|
|
@@ -1490,8 +1499,23 @@ class Playwright extends Helper {
|
|
|
1490
1499
|
*
|
|
1491
1500
|
*/
|
|
1492
1501
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
}
|
|
1495
1519
|
|
|
1496
1520
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1497
1521
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1615,7 +1639,7 @@ class Playwright extends Helper {
|
|
|
1615
1639
|
* @returns Promise<void>
|
|
1616
1640
|
*/
|
|
1617
1641
|
async replayFromHar(harFilePath, opts) {
|
|
1618
|
-
const file = path.join(
|
|
1642
|
+
const file = path.join(store.codeceptDir, harFilePath)
|
|
1619
1643
|
|
|
1620
1644
|
if (!fileExists(file)) {
|
|
1621
1645
|
throw new Error(`File at ${file} cannot be found on local system`)
|
|
@@ -1759,8 +1783,7 @@ class Playwright extends Helper {
|
|
|
1759
1783
|
if (elements.length === 0) {
|
|
1760
1784
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1761
1785
|
}
|
|
1762
|
-
|
|
1763
|
-
return elements[0]
|
|
1786
|
+
return selectElement(elements, locator, this)
|
|
1764
1787
|
}
|
|
1765
1788
|
|
|
1766
1789
|
/**
|
|
@@ -1775,8 +1798,7 @@ class Playwright extends Helper {
|
|
|
1775
1798
|
const context = providedContext || (await this._getContext())
|
|
1776
1799
|
const els = await findCheckable.call(this, locator, context)
|
|
1777
1800
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1778
|
-
|
|
1779
|
-
return els[0]
|
|
1801
|
+
return selectElement(els, locator, this)
|
|
1780
1802
|
}
|
|
1781
1803
|
|
|
1782
1804
|
/**
|
|
@@ -1944,8 +1966,15 @@ class Playwright extends Helper {
|
|
|
1944
1966
|
* {{> seeElement }}
|
|
1945
1967
|
*
|
|
1946
1968
|
*/
|
|
1947
|
-
async seeElement(locator) {
|
|
1948
|
-
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
|
+
}
|
|
1949
1978
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1950
1979
|
try {
|
|
1951
1980
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -1958,8 +1987,15 @@ class Playwright extends Helper {
|
|
|
1958
1987
|
* {{> dontSeeElement }}
|
|
1959
1988
|
*
|
|
1960
1989
|
*/
|
|
1961
|
-
async dontSeeElement(locator) {
|
|
1962
|
-
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
|
+
}
|
|
1963
1999
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1964
2000
|
try {
|
|
1965
2001
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2014,7 +2050,7 @@ class Playwright extends Helper {
|
|
|
2014
2050
|
const filePath = await download.path()
|
|
2015
2051
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
2016
2052
|
|
|
2017
|
-
const downloadPath = path.join(
|
|
2053
|
+
const downloadPath = path.join(store.outputDir, fileName)
|
|
2018
2054
|
if (!fs.existsSync(path.dirname(downloadPath))) {
|
|
2019
2055
|
fs.mkdirSync(path.dirname(downloadPath), '0777')
|
|
2020
2056
|
}
|
|
@@ -2198,6 +2234,7 @@ class Playwright extends Helper {
|
|
|
2198
2234
|
* {{> pressKeyWithKeyNormalization }}
|
|
2199
2235
|
*/
|
|
2200
2236
|
async pressKey(key) {
|
|
2237
|
+
await checkFocusBeforePressKey(this, key)
|
|
2201
2238
|
const modifiers = []
|
|
2202
2239
|
if (Array.isArray(key)) {
|
|
2203
2240
|
for (let k of key) {
|
|
@@ -2226,6 +2263,8 @@ class Playwright extends Helper {
|
|
|
2226
2263
|
* {{> type }}
|
|
2227
2264
|
*/
|
|
2228
2265
|
async type(keys, delay = null) {
|
|
2266
|
+
await checkFocusBeforeType(this)
|
|
2267
|
+
|
|
2229
2268
|
// Always use page.keyboard.type for any string (including single character and national characters).
|
|
2230
2269
|
if (!Array.isArray(keys)) {
|
|
2231
2270
|
keys = keys.toString()
|
|
@@ -2245,45 +2284,33 @@ class Playwright extends Helper {
|
|
|
2245
2284
|
* {{> fillField }}
|
|
2246
2285
|
*
|
|
2247
2286
|
*/
|
|
2248
|
-
async fillField(field, value) {
|
|
2249
|
-
const els = await findFields.call(this, field)
|
|
2287
|
+
async fillField(field, value, context = null) {
|
|
2288
|
+
const els = await findFields.call(this, field, context)
|
|
2250
2289
|
assertElementExists(els, field, 'Field')
|
|
2251
|
-
|
|
2252
|
-
|
|
2290
|
+
const el = selectElement(els, field, this)
|
|
2291
|
+
|
|
2292
|
+
await highlightActiveElement.call(this, el)
|
|
2293
|
+
|
|
2294
|
+
if (await fillRichEditor(this, el, value)) {
|
|
2295
|
+
return this._waitForAction()
|
|
2296
|
+
}
|
|
2253
2297
|
|
|
2254
2298
|
await el.clear()
|
|
2255
2299
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
2256
2300
|
|
|
2257
|
-
await highlightActiveElement.call(this, el)
|
|
2258
|
-
|
|
2259
2301
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2260
2302
|
|
|
2261
2303
|
return this._waitForAction()
|
|
2262
2304
|
}
|
|
2263
2305
|
|
|
2264
2306
|
/**
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
* Examples:
|
|
2269
|
-
*
|
|
2270
|
-
* ```js
|
|
2271
|
-
* I.clearField('.text-area')
|
|
2272
|
-
*
|
|
2273
|
-
* // if this doesn't work use force option
|
|
2274
|
-
* I.clearField('#submit', { force: true })
|
|
2275
|
-
* ```
|
|
2276
|
-
* Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
2277
|
-
*
|
|
2278
|
-
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
2279
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
2307
|
+
* {{> clearField }}
|
|
2280
2308
|
*/
|
|
2281
|
-
async clearField(locator,
|
|
2282
|
-
const els = await findFields.call(this, locator)
|
|
2309
|
+
async clearField(locator, context = null) {
|
|
2310
|
+
const els = await findFields.call(this, locator, context)
|
|
2283
2311
|
assertElementExists(els, locator, 'Field to clear')
|
|
2284
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
2285
2312
|
|
|
2286
|
-
const el = els
|
|
2313
|
+
const el = selectElement(els, locator, this)
|
|
2287
2314
|
|
|
2288
2315
|
await highlightActiveElement.call(this, el)
|
|
2289
2316
|
|
|
@@ -2295,76 +2322,101 @@ class Playwright extends Helper {
|
|
|
2295
2322
|
/**
|
|
2296
2323
|
* {{> appendField }}
|
|
2297
2324
|
*/
|
|
2298
|
-
async appendField(field, value) {
|
|
2299
|
-
const els = await findFields.call(this, field)
|
|
2325
|
+
async appendField(field, value, context = null) {
|
|
2326
|
+
const els = await findFields.call(this, field, context)
|
|
2300
2327
|
assertElementExists(els, field, 'Field')
|
|
2301
|
-
|
|
2302
|
-
await highlightActiveElement.call(this,
|
|
2303
|
-
await
|
|
2304
|
-
await
|
|
2328
|
+
const el = selectElement(els, field, this)
|
|
2329
|
+
await highlightActiveElement.call(this, el)
|
|
2330
|
+
await el.press('End')
|
|
2331
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2305
2332
|
return this._waitForAction()
|
|
2306
2333
|
}
|
|
2307
2334
|
|
|
2308
2335
|
/**
|
|
2309
2336
|
* {{> seeInField }}
|
|
2310
2337
|
*/
|
|
2311
|
-
async seeInField(field, value) {
|
|
2338
|
+
async seeInField(field, value, context = null) {
|
|
2312
2339
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2313
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2340
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2314
2341
|
}
|
|
2315
2342
|
|
|
2316
2343
|
/**
|
|
2317
2344
|
* {{> dontSeeInField }}
|
|
2318
2345
|
*/
|
|
2319
|
-
async dontSeeInField(field, value) {
|
|
2346
|
+
async dontSeeInField(field, value, context = null) {
|
|
2320
2347
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2321
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2348
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2322
2349
|
}
|
|
2323
2350
|
|
|
2324
2351
|
/**
|
|
2325
2352
|
* {{> attachFile }}
|
|
2326
2353
|
*
|
|
2327
2354
|
*/
|
|
2328
|
-
async attachFile(locator, pathToFile) {
|
|
2329
|
-
const file = path.join(
|
|
2355
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2356
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
2330
2357
|
|
|
2331
2358
|
if (!fileExists(file)) {
|
|
2332
2359
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2333
2360
|
}
|
|
2334
|
-
const els = await findFields.call(this, locator)
|
|
2335
|
-
|
|
2336
|
-
|
|
2361
|
+
const els = await findFields.call(this, locator, context)
|
|
2362
|
+
if (els.length) {
|
|
2363
|
+
const el = selectElement(els, locator, this)
|
|
2364
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2365
|
+
const type = await el.evaluate(el => el.type)
|
|
2366
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2367
|
+
await el.setInputFiles(file)
|
|
2368
|
+
return this._waitForAction()
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2373
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2374
|
+
const el = selectElement(targetEls, locator, this)
|
|
2375
|
+
const fileData = {
|
|
2376
|
+
base64Content: base64EncodeFile(file),
|
|
2377
|
+
fileName: path.basename(file),
|
|
2378
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2379
|
+
}
|
|
2380
|
+
await el.evaluate(dropFile, fileData)
|
|
2337
2381
|
return this._waitForAction()
|
|
2338
2382
|
}
|
|
2339
2383
|
|
|
2340
2384
|
/**
|
|
2341
2385
|
* {{> selectOption }}
|
|
2342
2386
|
*/
|
|
2343
|
-
async selectOption(select, option) {
|
|
2344
|
-
const
|
|
2387
|
+
async selectOption(select, option, context = null) {
|
|
2388
|
+
const pageContext = await this.context
|
|
2345
2389
|
const matchedLocator = new Locator(select)
|
|
2346
2390
|
|
|
2391
|
+
let contextEl
|
|
2392
|
+
if (context) {
|
|
2393
|
+
const contextEls = await this._locate(context)
|
|
2394
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2395
|
+
contextEl = contextEls[0]
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2347
2398
|
// Strict locator
|
|
2348
2399
|
if (!matchedLocator.isFuzzy()) {
|
|
2349
2400
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2350
|
-
const els = await this._locate(matchedLocator)
|
|
2401
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2351
2402
|
assertElementExists(els, select, 'Selectable element')
|
|
2352
|
-
return proceedSelect.call(this,
|
|
2403
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2353
2404
|
}
|
|
2354
2405
|
|
|
2355
2406
|
// Fuzzy: try combobox
|
|
2356
2407
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2357
|
-
|
|
2358
|
-
|
|
2408
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2409
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2410
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2359
2411
|
|
|
2360
2412
|
// Fuzzy: try listbox
|
|
2361
|
-
els = await findByRole(
|
|
2362
|
-
if (els?.length) return proceedSelect.call(this,
|
|
2413
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2414
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2363
2415
|
|
|
2364
2416
|
// Fuzzy: try native select
|
|
2365
|
-
els = await findFields.call(this, select)
|
|
2417
|
+
els = await findFields.call(this, select, context)
|
|
2366
2418
|
assertElementExists(els, select, 'Selectable element')
|
|
2367
|
-
return proceedSelect.call(this,
|
|
2419
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2368
2420
|
}
|
|
2369
2421
|
|
|
2370
2422
|
/**
|
|
@@ -2412,7 +2464,7 @@ class Playwright extends Helper {
|
|
|
2412
2464
|
const currentUrl = await this._getPageUrl()
|
|
2413
2465
|
const baseUrl = this.options.url || 'http://localhost'
|
|
2414
2466
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2415
|
-
return equals('url path').assert(path, actualPath)
|
|
2467
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2416
2468
|
}
|
|
2417
2469
|
|
|
2418
2470
|
/**
|
|
@@ -2422,7 +2474,7 @@ class Playwright extends Helper {
|
|
|
2422
2474
|
const currentUrl = await this._getPageUrl()
|
|
2423
2475
|
const baseUrl = this.options.url || 'http://localhost'
|
|
2424
2476
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2425
|
-
return equals('url path').negate(path, actualPath)
|
|
2477
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2426
2478
|
}
|
|
2427
2479
|
|
|
2428
2480
|
/**
|
|
@@ -2627,8 +2679,11 @@ class Playwright extends Helper {
|
|
|
2627
2679
|
* @returns {Promise<any>}
|
|
2628
2680
|
*/
|
|
2629
2681
|
async executeScript(fn, arg) {
|
|
2682
|
+
if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
|
|
2683
|
+
if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
|
|
2684
|
+
return arg.evaluate(fn)
|
|
2685
|
+
}
|
|
2630
2686
|
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2631
|
-
// switching to iframe context
|
|
2632
2687
|
return this.context.locator(':root').evaluate(fn, arg)
|
|
2633
2688
|
}
|
|
2634
2689
|
return this.page.evaluate.apply(this.page, [fn, arg])
|
|
@@ -2658,15 +2713,12 @@ class Playwright extends Helper {
|
|
|
2658
2713
|
*
|
|
2659
2714
|
*/
|
|
2660
2715
|
async grabTextFrom(locator) {
|
|
2661
|
-
|
|
2662
|
-
if (
|
|
2663
|
-
const
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
this.debugSection('Text', text)
|
|
2668
|
-
return text
|
|
2669
|
-
}
|
|
2716
|
+
const roleElements = await handleRoleLocator(this.page, locator)
|
|
2717
|
+
if (roleElements && roleElements.length > 0) {
|
|
2718
|
+
const text = await roleElements[0].textContent()
|
|
2719
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2720
|
+
this.debugSection('Text', text)
|
|
2721
|
+
return text
|
|
2670
2722
|
}
|
|
2671
2723
|
|
|
2672
2724
|
const locatorObj = new Locator(locator, 'css')
|
|
@@ -2894,7 +2946,7 @@ class Playwright extends Helper {
|
|
|
2894
2946
|
const els = await this._locate(matchedLocator)
|
|
2895
2947
|
assertElementExists(els, locator)
|
|
2896
2948
|
const snapshot = await els[0].ariaSnapshot()
|
|
2897
|
-
this.debugSection('Aria Snapshot', snapshot)
|
|
2949
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
2898
2950
|
return snapshot
|
|
2899
2951
|
}
|
|
2900
2952
|
|
|
@@ -3382,6 +3434,7 @@ class Playwright extends Helper {
|
|
|
3382
3434
|
*/
|
|
3383
3435
|
async waitInUrl(urlPart, sec = null) {
|
|
3384
3436
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3437
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3385
3438
|
|
|
3386
3439
|
return this.page
|
|
3387
3440
|
.waitForFunction(
|
|
@@ -3389,13 +3442,13 @@ class Playwright extends Helper {
|
|
|
3389
3442
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3390
3443
|
return currUrl.indexOf(urlPart) > -1
|
|
3391
3444
|
},
|
|
3392
|
-
|
|
3445
|
+
expectedUrl,
|
|
3393
3446
|
{ timeout: waitTimeout },
|
|
3394
3447
|
)
|
|
3395
3448
|
.catch(async e => {
|
|
3396
|
-
const currUrl = await this._getPageUrl()
|
|
3449
|
+
const currUrl = await this._getPageUrl()
|
|
3397
3450
|
if (/Timeout/i.test(e.message)) {
|
|
3398
|
-
throw new Error(`expected url to include ${
|
|
3451
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3399
3452
|
} else {
|
|
3400
3453
|
throw e
|
|
3401
3454
|
}
|
|
@@ -3407,26 +3460,46 @@ class Playwright extends Helper {
|
|
|
3407
3460
|
*/
|
|
3408
3461
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3409
3462
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3410
|
-
|
|
3411
|
-
const baseUrl = this.options.url
|
|
3412
|
-
let expectedUrl = urlPart
|
|
3413
|
-
if (urlPart.indexOf('http') < 0) {
|
|
3414
|
-
expectedUrl = baseUrl + urlPart
|
|
3415
|
-
}
|
|
3463
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3416
3464
|
|
|
3417
3465
|
try {
|
|
3418
3466
|
await this.page.waitForURL(
|
|
3419
|
-
url => url.href
|
|
3467
|
+
url => url.href === expectedUrl,
|
|
3420
3468
|
{ timeout: waitTimeout },
|
|
3421
3469
|
)
|
|
3422
3470
|
} catch (e) {
|
|
3423
3471
|
const currUrl = await this._getPageUrl()
|
|
3424
3472
|
if (/Timeout/i.test(e.message)) {
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3473
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
3474
|
+
} else {
|
|
3475
|
+
throw e
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
/**
|
|
3481
|
+
* {{> waitCurrentPathEquals }}
|
|
3482
|
+
*/
|
|
3483
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
3484
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3485
|
+
const normalizedPath = normalizePath(path)
|
|
3486
|
+
|
|
3487
|
+
try {
|
|
3488
|
+
await this.page.waitForFunction(
|
|
3489
|
+
expectedPath => {
|
|
3490
|
+
const actualPath = window.location.pathname
|
|
3491
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
3492
|
+
return normalizePath(actualPath) === expectedPath
|
|
3493
|
+
},
|
|
3494
|
+
normalizedPath,
|
|
3495
|
+
{ timeout: waitTimeout },
|
|
3496
|
+
)
|
|
3497
|
+
} catch (e) {
|
|
3498
|
+
const currentUrl = await this._getPageUrl()
|
|
3499
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
3500
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
3501
|
+
if (/Timeout/i.test(e.message)) {
|
|
3502
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
3430
3503
|
} else {
|
|
3431
3504
|
throw e
|
|
3432
3505
|
}
|
|
@@ -4112,9 +4185,15 @@ class Playwright extends Helper {
|
|
|
4112
4185
|
|
|
4113
4186
|
export default Playwright
|
|
4114
4187
|
|
|
4115
|
-
function buildLocatorString(locator) {
|
|
4188
|
+
export function buildLocatorString(locator) {
|
|
4116
4189
|
if (locator.isXPath()) {
|
|
4117
|
-
|
|
4190
|
+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
|
|
4191
|
+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
|
|
4192
|
+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
|
|
4193
|
+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
|
|
4194
|
+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
|
|
4195
|
+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
|
|
4196
|
+
return `xpath=${value}`
|
|
4118
4197
|
}
|
|
4119
4198
|
if (locator.isShadow()) {
|
|
4120
4199
|
// Convert shadow locator to CSS with >> chaining operator
|
|
@@ -4125,25 +4204,22 @@ function buildLocatorString(locator) {
|
|
|
4125
4204
|
return locator.simplify()
|
|
4126
4205
|
}
|
|
4127
4206
|
|
|
4128
|
-
/**
|
|
4129
|
-
* Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4130
|
-
*/
|
|
4131
|
-
function isRoleLocatorObject(locator) {
|
|
4132
|
-
return locator && typeof locator === 'object' && locator.role && !locator.type
|
|
4133
|
-
}
|
|
4134
|
-
|
|
4135
4207
|
/**
|
|
4136
4208
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4209
|
+
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
4137
4210
|
* Returns elements array if role locator, null otherwise
|
|
4138
4211
|
*/
|
|
4139
4212
|
async function handleRoleLocator(context, locator) {
|
|
4140
|
-
|
|
4213
|
+
const loc = new Locator(locator)
|
|
4214
|
+
if (!loc.isRole()) return null
|
|
4141
4215
|
|
|
4216
|
+
const roleObj = loc.locator || {}
|
|
4142
4217
|
const options = {}
|
|
4143
|
-
if (
|
|
4144
|
-
if (
|
|
4218
|
+
if (roleObj.text) options.name = roleObj.text
|
|
4219
|
+
if (roleObj.name) options.name = roleObj.name
|
|
4220
|
+
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4145
4221
|
|
|
4146
|
-
return context.getByRole(
|
|
4222
|
+
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4147
4223
|
}
|
|
4148
4224
|
|
|
4149
4225
|
async function findByRole(context, locator) {
|
|
@@ -4155,13 +4231,10 @@ async function findByRole(context, locator) {
|
|
|
4155
4231
|
}
|
|
4156
4232
|
|
|
4157
4233
|
async function findElements(matcher, locator) {
|
|
4158
|
-
// Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
|
|
4159
4234
|
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4160
|
-
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4161
4235
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4162
4236
|
|
|
4163
4237
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4164
|
-
if (isVueLocator) return findVue(matcher, locator)
|
|
4165
4238
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4166
4239
|
|
|
4167
4240
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
@@ -4177,7 +4250,6 @@ async function findElements(matcher, locator) {
|
|
|
4177
4250
|
|
|
4178
4251
|
async function findElement(matcher, locator) {
|
|
4179
4252
|
if (locator.react) return findReact(matcher, locator)
|
|
4180
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
4181
4253
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4182
4254
|
|
|
4183
4255
|
locator = new Locator(locator, 'css')
|
|
@@ -4212,16 +4284,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4212
4284
|
assertElementExists(els, locator, 'Clickable element')
|
|
4213
4285
|
}
|
|
4214
4286
|
|
|
4215
|
-
|
|
4216
|
-
|
|
4287
|
+
const opts = store.currentStep?.opts
|
|
4288
|
+
let element
|
|
4289
|
+
if (opts?.elementIndex != null) {
|
|
4290
|
+
element = selectElement(els, locator, this)
|
|
4291
|
+
} else {
|
|
4292
|
+
const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
|
|
4293
|
+
if (strict) assertOnlyOneElement(els, locator, this)
|
|
4294
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
await highlightActiveElement.call(this, element)
|
|
4298
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4217
4299
|
|
|
4218
|
-
/*
|
|
4219
|
-
using the force true options itself but instead dispatching a click
|
|
4220
|
-
*/
|
|
4221
4300
|
if (options.force) {
|
|
4222
|
-
await
|
|
4301
|
+
await element.dispatchEvent('click')
|
|
4223
4302
|
} else {
|
|
4224
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4225
4303
|
await element.click(options)
|
|
4226
4304
|
}
|
|
4227
4305
|
const promises = []
|
|
@@ -4238,7 +4316,6 @@ async function findClickable(matcher, locator) {
|
|
|
4238
4316
|
|
|
4239
4317
|
if (!matchedLocator.isFuzzy()) {
|
|
4240
4318
|
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4241
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4242
4319
|
return els
|
|
4243
4320
|
}
|
|
4244
4321
|
|
|
@@ -4247,42 +4324,27 @@ async function findClickable(matcher, locator) {
|
|
|
4247
4324
|
|
|
4248
4325
|
try {
|
|
4249
4326
|
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4250
|
-
if (els.length)
|
|
4251
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4252
|
-
return els
|
|
4253
|
-
}
|
|
4327
|
+
if (els.length) return els
|
|
4254
4328
|
} catch (err) {
|
|
4255
4329
|
// getByRole not supported or failed
|
|
4256
4330
|
}
|
|
4257
4331
|
|
|
4258
4332
|
try {
|
|
4259
4333
|
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4260
|
-
if (els.length)
|
|
4261
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4262
|
-
return els
|
|
4263
|
-
}
|
|
4334
|
+
if (els.length) return els
|
|
4264
4335
|
} catch (err) {
|
|
4265
4336
|
// getByRole not supported or failed
|
|
4266
4337
|
}
|
|
4267
4338
|
|
|
4268
4339
|
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
4269
|
-
if (els.length)
|
|
4270
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4271
|
-
return els
|
|
4272
|
-
}
|
|
4340
|
+
if (els.length) return els
|
|
4273
4341
|
|
|
4274
4342
|
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
4275
|
-
if (els.length)
|
|
4276
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4277
|
-
return els
|
|
4278
|
-
}
|
|
4343
|
+
if (els.length) return els
|
|
4279
4344
|
|
|
4280
4345
|
try {
|
|
4281
4346
|
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
4282
|
-
if (els.length)
|
|
4283
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4284
|
-
return els
|
|
4285
|
-
}
|
|
4347
|
+
if (els.length) return els
|
|
4286
4348
|
} catch (err) {
|
|
4287
4349
|
// Do nothing
|
|
4288
4350
|
}
|
|
@@ -4355,34 +4417,42 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4355
4417
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4356
4418
|
}
|
|
4357
4419
|
|
|
4358
|
-
async function findFields(locator) {
|
|
4359
|
-
|
|
4360
|
-
if (
|
|
4361
|
-
const
|
|
4362
|
-
|
|
4363
|
-
|
|
4420
|
+
async function findFields(locator, context = null) {
|
|
4421
|
+
let contextEl
|
|
4422
|
+
if (context) {
|
|
4423
|
+
const contextEls = await this._locate(context)
|
|
4424
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4425
|
+
contextEl = contextEls[0]
|
|
4364
4426
|
}
|
|
4365
4427
|
|
|
4428
|
+
const locateFn = contextEl
|
|
4429
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4430
|
+
: loc => this._locate(loc)
|
|
4431
|
+
|
|
4432
|
+
const matcher = contextEl || (await this.page)
|
|
4433
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4434
|
+
if (roleElements) return roleElements
|
|
4435
|
+
|
|
4366
4436
|
const matchedLocator = new Locator(locator)
|
|
4367
4437
|
if (!matchedLocator.isFuzzy()) {
|
|
4368
|
-
return
|
|
4438
|
+
return locateFn(matchedLocator)
|
|
4369
4439
|
}
|
|
4370
4440
|
const literal = xpathLocator.literal(locator)
|
|
4371
4441
|
|
|
4372
|
-
let els = await
|
|
4442
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4373
4443
|
if (els.length) {
|
|
4374
4444
|
return els
|
|
4375
4445
|
}
|
|
4376
4446
|
|
|
4377
|
-
els = await
|
|
4447
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4378
4448
|
if (els.length) {
|
|
4379
4449
|
return els
|
|
4380
4450
|
}
|
|
4381
|
-
els = await
|
|
4451
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4382
4452
|
if (els.length) {
|
|
4383
4453
|
return els
|
|
4384
4454
|
}
|
|
4385
|
-
return
|
|
4455
|
+
return locateFn({ css: locator })
|
|
4386
4456
|
}
|
|
4387
4457
|
|
|
4388
4458
|
async function proceedSelect(context, el, option) {
|
|
@@ -4431,8 +4501,8 @@ async function proceedSelect(context, el, option) {
|
|
|
4431
4501
|
return this._waitForAction()
|
|
4432
4502
|
}
|
|
4433
4503
|
|
|
4434
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
4435
|
-
const els = await findFields.call(this, field)
|
|
4504
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4505
|
+
const els = await findFields.call(this, field, context)
|
|
4436
4506
|
assertElementExists(els, field, 'Field')
|
|
4437
4507
|
const el = els[0]
|
|
4438
4508
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4546,9 +4616,10 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4546
4616
|
}
|
|
4547
4617
|
}
|
|
4548
4618
|
|
|
4549
|
-
function assertOnlyOneElement(elements, locator) {
|
|
4619
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4550
4620
|
if (elements.length > 1) {
|
|
4551
|
-
|
|
4621
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4622
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4552
4623
|
}
|
|
4553
4624
|
}
|
|
4554
4625
|
|
|
@@ -4757,7 +4828,7 @@ async function refreshContextSession() {
|
|
|
4757
4828
|
|
|
4758
4829
|
function saveVideoForPage(page, name) {
|
|
4759
4830
|
if (!page.video()) return null
|
|
4760
|
-
const fileName = `${`${
|
|
4831
|
+
const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
|
|
4761
4832
|
page
|
|
4762
4833
|
.video()
|
|
4763
4834
|
.saveAs(fileName)
|
|
@@ -4774,7 +4845,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4774
4845
|
if (!context) return
|
|
4775
4846
|
if (!context.tracing) return
|
|
4776
4847
|
try {
|
|
4777
|
-
const fileName = `${`${
|
|
4848
|
+
const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
4778
4849
|
await context.tracing.stop({ path: fileName })
|
|
4779
4850
|
return fileName
|
|
4780
4851
|
} catch (err) {
|