codeceptjs 4.0.0-rc.2 → 4.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -27
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1187 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +230 -0
- package/docs/continuous-integration.md +497 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +26 -23
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -0
- package/lib/command/run.js +1 -1
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +4 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +228 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -3
- package/lib/mocha/cli.js +14 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +453 -0
- package/lib/plugin/analyze.js +1 -1
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +53 -49
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +77 -3
- package/lib/workers.js +52 -22
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
package/lib/helper/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
|
|
@@ -1490,8 +1494,23 @@ class Playwright extends Helper {
|
|
|
1490
1494
|
*
|
|
1491
1495
|
*/
|
|
1492
1496
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1497
|
+
let context = null
|
|
1498
|
+
if (typeof offsetX !== 'number') {
|
|
1499
|
+
context = offsetX
|
|
1500
|
+
offsetX = 0
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
let el
|
|
1504
|
+
if (context) {
|
|
1505
|
+
const contextEls = await this._locate(context)
|
|
1506
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1507
|
+
el = await findElements.call(this, contextEls[0], locator)
|
|
1508
|
+
assertElementExists(el, locator)
|
|
1509
|
+
el = el[0]
|
|
1510
|
+
} else {
|
|
1511
|
+
el = await this._locateElement(locator)
|
|
1512
|
+
assertElementExists(el, locator)
|
|
1513
|
+
}
|
|
1495
1514
|
|
|
1496
1515
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1497
1516
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1615,7 +1634,7 @@ class Playwright extends Helper {
|
|
|
1615
1634
|
* @returns Promise<void>
|
|
1616
1635
|
*/
|
|
1617
1636
|
async replayFromHar(harFilePath, opts) {
|
|
1618
|
-
const file = path.join(
|
|
1637
|
+
const file = path.join(store.codeceptDir, harFilePath)
|
|
1619
1638
|
|
|
1620
1639
|
if (!fileExists(file)) {
|
|
1621
1640
|
throw new Error(`File at ${file} cannot be found on local system`)
|
|
@@ -1759,8 +1778,7 @@ class Playwright extends Helper {
|
|
|
1759
1778
|
if (elements.length === 0) {
|
|
1760
1779
|
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1761
1780
|
}
|
|
1762
|
-
|
|
1763
|
-
return elements[0]
|
|
1781
|
+
return selectElement(elements, locator, this)
|
|
1764
1782
|
}
|
|
1765
1783
|
|
|
1766
1784
|
/**
|
|
@@ -1775,8 +1793,7 @@ class Playwright extends Helper {
|
|
|
1775
1793
|
const context = providedContext || (await this._getContext())
|
|
1776
1794
|
const els = await findCheckable.call(this, locator, context)
|
|
1777
1795
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1778
|
-
|
|
1779
|
-
return els[0]
|
|
1796
|
+
return selectElement(els, locator, this)
|
|
1780
1797
|
}
|
|
1781
1798
|
|
|
1782
1799
|
/**
|
|
@@ -1944,8 +1961,15 @@ class Playwright extends Helper {
|
|
|
1944
1961
|
* {{> seeElement }}
|
|
1945
1962
|
*
|
|
1946
1963
|
*/
|
|
1947
|
-
async seeElement(locator) {
|
|
1948
|
-
let els
|
|
1964
|
+
async seeElement(locator, context = null) {
|
|
1965
|
+
let els
|
|
1966
|
+
if (context) {
|
|
1967
|
+
const contextEls = await this._locate(context)
|
|
1968
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1969
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1970
|
+
} else {
|
|
1971
|
+
els = await this._locate(locator)
|
|
1972
|
+
}
|
|
1949
1973
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1950
1974
|
try {
|
|
1951
1975
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -1958,8 +1982,15 @@ class Playwright extends Helper {
|
|
|
1958
1982
|
* {{> dontSeeElement }}
|
|
1959
1983
|
*
|
|
1960
1984
|
*/
|
|
1961
|
-
async dontSeeElement(locator) {
|
|
1962
|
-
let els
|
|
1985
|
+
async dontSeeElement(locator, context = null) {
|
|
1986
|
+
let els
|
|
1987
|
+
if (context) {
|
|
1988
|
+
const contextEls = await this._locate(context)
|
|
1989
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1990
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1991
|
+
} else {
|
|
1992
|
+
els = await this._locate(locator)
|
|
1993
|
+
}
|
|
1963
1994
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
1964
1995
|
try {
|
|
1965
1996
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2014,7 +2045,7 @@ class Playwright extends Helper {
|
|
|
2014
2045
|
const filePath = await download.path()
|
|
2015
2046
|
fileName = fileName || `downloads/${path.basename(filePath)}`
|
|
2016
2047
|
|
|
2017
|
-
const downloadPath = path.join(
|
|
2048
|
+
const downloadPath = path.join(store.outputDir, fileName)
|
|
2018
2049
|
if (!fs.existsSync(path.dirname(downloadPath))) {
|
|
2019
2050
|
fs.mkdirSync(path.dirname(downloadPath), '0777')
|
|
2020
2051
|
}
|
|
@@ -2198,6 +2229,7 @@ class Playwright extends Helper {
|
|
|
2198
2229
|
* {{> pressKeyWithKeyNormalization }}
|
|
2199
2230
|
*/
|
|
2200
2231
|
async pressKey(key) {
|
|
2232
|
+
await checkFocusBeforePressKey(this, key)
|
|
2201
2233
|
const modifiers = []
|
|
2202
2234
|
if (Array.isArray(key)) {
|
|
2203
2235
|
for (let k of key) {
|
|
@@ -2226,6 +2258,8 @@ class Playwright extends Helper {
|
|
|
2226
2258
|
* {{> type }}
|
|
2227
2259
|
*/
|
|
2228
2260
|
async type(keys, delay = null) {
|
|
2261
|
+
await checkFocusBeforeType(this)
|
|
2262
|
+
|
|
2229
2263
|
// Always use page.keyboard.type for any string (including single character and national characters).
|
|
2230
2264
|
if (!Array.isArray(keys)) {
|
|
2231
2265
|
keys = keys.toString()
|
|
@@ -2245,45 +2279,33 @@ class Playwright extends Helper {
|
|
|
2245
2279
|
* {{> fillField }}
|
|
2246
2280
|
*
|
|
2247
2281
|
*/
|
|
2248
|
-
async fillField(field, value) {
|
|
2249
|
-
const els = await findFields.call(this, field)
|
|
2282
|
+
async fillField(field, value, context = null) {
|
|
2283
|
+
const els = await findFields.call(this, field, context)
|
|
2250
2284
|
assertElementExists(els, field, 'Field')
|
|
2251
|
-
|
|
2252
|
-
|
|
2285
|
+
const el = selectElement(els, field, this)
|
|
2286
|
+
|
|
2287
|
+
await highlightActiveElement.call(this, el)
|
|
2288
|
+
|
|
2289
|
+
if (await fillRichEditor(this, el, value)) {
|
|
2290
|
+
return this._waitForAction()
|
|
2291
|
+
}
|
|
2253
2292
|
|
|
2254
2293
|
await el.clear()
|
|
2255
2294
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
2256
2295
|
|
|
2257
|
-
await highlightActiveElement.call(this, el)
|
|
2258
|
-
|
|
2259
2296
|
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2260
2297
|
|
|
2261
2298
|
return this._waitForAction()
|
|
2262
2299
|
}
|
|
2263
2300
|
|
|
2264
2301
|
/**
|
|
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.
|
|
2302
|
+
* {{> clearField }}
|
|
2280
2303
|
*/
|
|
2281
|
-
async clearField(locator,
|
|
2282
|
-
const els = await findFields.call(this, locator)
|
|
2304
|
+
async clearField(locator, context = null) {
|
|
2305
|
+
const els = await findFields.call(this, locator, context)
|
|
2283
2306
|
assertElementExists(els, locator, 'Field to clear')
|
|
2284
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
2285
2307
|
|
|
2286
|
-
const el = els
|
|
2308
|
+
const el = selectElement(els, locator, this)
|
|
2287
2309
|
|
|
2288
2310
|
await highlightActiveElement.call(this, el)
|
|
2289
2311
|
|
|
@@ -2295,76 +2317,101 @@ class Playwright extends Helper {
|
|
|
2295
2317
|
/**
|
|
2296
2318
|
* {{> appendField }}
|
|
2297
2319
|
*/
|
|
2298
|
-
async appendField(field, value) {
|
|
2299
|
-
const els = await findFields.call(this, field)
|
|
2320
|
+
async appendField(field, value, context = null) {
|
|
2321
|
+
const els = await findFields.call(this, field, context)
|
|
2300
2322
|
assertElementExists(els, field, 'Field')
|
|
2301
|
-
|
|
2302
|
-
await highlightActiveElement.call(this,
|
|
2303
|
-
await
|
|
2304
|
-
await
|
|
2323
|
+
const el = selectElement(els, field, this)
|
|
2324
|
+
await highlightActiveElement.call(this, el)
|
|
2325
|
+
await el.press('End')
|
|
2326
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2305
2327
|
return this._waitForAction()
|
|
2306
2328
|
}
|
|
2307
2329
|
|
|
2308
2330
|
/**
|
|
2309
2331
|
* {{> seeInField }}
|
|
2310
2332
|
*/
|
|
2311
|
-
async seeInField(field, value) {
|
|
2333
|
+
async seeInField(field, value, context = null) {
|
|
2312
2334
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2313
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2335
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2314
2336
|
}
|
|
2315
2337
|
|
|
2316
2338
|
/**
|
|
2317
2339
|
* {{> dontSeeInField }}
|
|
2318
2340
|
*/
|
|
2319
|
-
async dontSeeInField(field, value) {
|
|
2341
|
+
async dontSeeInField(field, value, context = null) {
|
|
2320
2342
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2321
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2343
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2322
2344
|
}
|
|
2323
2345
|
|
|
2324
2346
|
/**
|
|
2325
2347
|
* {{> attachFile }}
|
|
2326
2348
|
*
|
|
2327
2349
|
*/
|
|
2328
|
-
async attachFile(locator, pathToFile) {
|
|
2329
|
-
const file = path.join(
|
|
2350
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2351
|
+
const file = path.join(store.codeceptDir, pathToFile)
|
|
2330
2352
|
|
|
2331
2353
|
if (!fileExists(file)) {
|
|
2332
2354
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2333
2355
|
}
|
|
2334
|
-
const els = await findFields.call(this, locator)
|
|
2335
|
-
|
|
2336
|
-
|
|
2356
|
+
const els = await findFields.call(this, locator, context)
|
|
2357
|
+
if (els.length) {
|
|
2358
|
+
const el = selectElement(els, locator, this)
|
|
2359
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2360
|
+
const type = await el.evaluate(el => el.type)
|
|
2361
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2362
|
+
await el.setInputFiles(file)
|
|
2363
|
+
return this._waitForAction()
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2368
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2369
|
+
const el = selectElement(targetEls, locator, this)
|
|
2370
|
+
const fileData = {
|
|
2371
|
+
base64Content: base64EncodeFile(file),
|
|
2372
|
+
fileName: path.basename(file),
|
|
2373
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2374
|
+
}
|
|
2375
|
+
await el.evaluate(dropFile, fileData)
|
|
2337
2376
|
return this._waitForAction()
|
|
2338
2377
|
}
|
|
2339
2378
|
|
|
2340
2379
|
/**
|
|
2341
2380
|
* {{> selectOption }}
|
|
2342
2381
|
*/
|
|
2343
|
-
async selectOption(select, option) {
|
|
2344
|
-
const
|
|
2382
|
+
async selectOption(select, option, context = null) {
|
|
2383
|
+
const pageContext = await this.context
|
|
2345
2384
|
const matchedLocator = new Locator(select)
|
|
2346
2385
|
|
|
2386
|
+
let contextEl
|
|
2387
|
+
if (context) {
|
|
2388
|
+
const contextEls = await this._locate(context)
|
|
2389
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2390
|
+
contextEl = contextEls[0]
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2347
2393
|
// Strict locator
|
|
2348
2394
|
if (!matchedLocator.isFuzzy()) {
|
|
2349
2395
|
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2350
|
-
const els = await this._locate(matchedLocator)
|
|
2396
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2351
2397
|
assertElementExists(els, select, 'Selectable element')
|
|
2352
|
-
return proceedSelect.call(this,
|
|
2398
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2353
2399
|
}
|
|
2354
2400
|
|
|
2355
2401
|
// Fuzzy: try combobox
|
|
2356
2402
|
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2357
|
-
|
|
2358
|
-
|
|
2403
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2404
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2405
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2359
2406
|
|
|
2360
2407
|
// Fuzzy: try listbox
|
|
2361
|
-
els = await findByRole(
|
|
2362
|
-
if (els?.length) return proceedSelect.call(this,
|
|
2408
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2409
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2363
2410
|
|
|
2364
2411
|
// Fuzzy: try native select
|
|
2365
|
-
els = await findFields.call(this, select)
|
|
2412
|
+
els = await findFields.call(this, select, context)
|
|
2366
2413
|
assertElementExists(els, select, 'Selectable element')
|
|
2367
|
-
return proceedSelect.call(this,
|
|
2414
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2368
2415
|
}
|
|
2369
2416
|
|
|
2370
2417
|
/**
|
|
@@ -2412,7 +2459,7 @@ class Playwright extends Helper {
|
|
|
2412
2459
|
const currentUrl = await this._getPageUrl()
|
|
2413
2460
|
const baseUrl = this.options.url || 'http://localhost'
|
|
2414
2461
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2415
|
-
return equals('url path').assert(path, actualPath)
|
|
2462
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2416
2463
|
}
|
|
2417
2464
|
|
|
2418
2465
|
/**
|
|
@@ -2422,7 +2469,7 @@ class Playwright extends Helper {
|
|
|
2422
2469
|
const currentUrl = await this._getPageUrl()
|
|
2423
2470
|
const baseUrl = this.options.url || 'http://localhost'
|
|
2424
2471
|
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2425
|
-
return equals('url path').negate(path, actualPath)
|
|
2472
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2426
2473
|
}
|
|
2427
2474
|
|
|
2428
2475
|
/**
|
|
@@ -2627,8 +2674,11 @@ class Playwright extends Helper {
|
|
|
2627
2674
|
* @returns {Promise<any>}
|
|
2628
2675
|
*/
|
|
2629
2676
|
async executeScript(fn, arg) {
|
|
2677
|
+
if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
|
|
2678
|
+
if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
|
|
2679
|
+
return arg.evaluate(fn)
|
|
2680
|
+
}
|
|
2630
2681
|
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2631
|
-
// switching to iframe context
|
|
2632
2682
|
return this.context.locator(':root').evaluate(fn, arg)
|
|
2633
2683
|
}
|
|
2634
2684
|
return this.page.evaluate.apply(this.page, [fn, arg])
|
|
@@ -2658,15 +2708,12 @@ class Playwright extends Helper {
|
|
|
2658
2708
|
*
|
|
2659
2709
|
*/
|
|
2660
2710
|
async grabTextFrom(locator) {
|
|
2661
|
-
|
|
2662
|
-
if (
|
|
2663
|
-
const
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
this.debugSection('Text', text)
|
|
2668
|
-
return text
|
|
2669
|
-
}
|
|
2711
|
+
const roleElements = await handleRoleLocator(this.page, locator)
|
|
2712
|
+
if (roleElements && roleElements.length > 0) {
|
|
2713
|
+
const text = await roleElements[0].textContent()
|
|
2714
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2715
|
+
this.debugSection('Text', text)
|
|
2716
|
+
return text
|
|
2670
2717
|
}
|
|
2671
2718
|
|
|
2672
2719
|
const locatorObj = new Locator(locator, 'css')
|
|
@@ -2894,7 +2941,7 @@ class Playwright extends Helper {
|
|
|
2894
2941
|
const els = await this._locate(matchedLocator)
|
|
2895
2942
|
assertElementExists(els, locator)
|
|
2896
2943
|
const snapshot = await els[0].ariaSnapshot()
|
|
2897
|
-
this.debugSection('Aria Snapshot', snapshot)
|
|
2944
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
2898
2945
|
return snapshot
|
|
2899
2946
|
}
|
|
2900
2947
|
|
|
@@ -3382,6 +3429,7 @@ class Playwright extends Helper {
|
|
|
3382
3429
|
*/
|
|
3383
3430
|
async waitInUrl(urlPart, sec = null) {
|
|
3384
3431
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3432
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3385
3433
|
|
|
3386
3434
|
return this.page
|
|
3387
3435
|
.waitForFunction(
|
|
@@ -3389,13 +3437,13 @@ class Playwright extends Helper {
|
|
|
3389
3437
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3390
3438
|
return currUrl.indexOf(urlPart) > -1
|
|
3391
3439
|
},
|
|
3392
|
-
|
|
3440
|
+
expectedUrl,
|
|
3393
3441
|
{ timeout: waitTimeout },
|
|
3394
3442
|
)
|
|
3395
3443
|
.catch(async e => {
|
|
3396
|
-
const currUrl = await this._getPageUrl()
|
|
3444
|
+
const currUrl = await this._getPageUrl()
|
|
3397
3445
|
if (/Timeout/i.test(e.message)) {
|
|
3398
|
-
throw new Error(`expected url to include ${
|
|
3446
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3399
3447
|
} else {
|
|
3400
3448
|
throw e
|
|
3401
3449
|
}
|
|
@@ -3407,26 +3455,46 @@ class Playwright extends Helper {
|
|
|
3407
3455
|
*/
|
|
3408
3456
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3409
3457
|
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
|
-
}
|
|
3458
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3416
3459
|
|
|
3417
3460
|
try {
|
|
3418
3461
|
await this.page.waitForURL(
|
|
3419
|
-
url => url.href
|
|
3462
|
+
url => url.href === expectedUrl,
|
|
3420
3463
|
{ timeout: waitTimeout },
|
|
3421
3464
|
)
|
|
3422
3465
|
} catch (e) {
|
|
3423
3466
|
const currUrl = await this._getPageUrl()
|
|
3424
3467
|
if (/Timeout/i.test(e.message)) {
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3468
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
3469
|
+
} else {
|
|
3470
|
+
throw e
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
/**
|
|
3476
|
+
* {{> waitCurrentPathEquals }}
|
|
3477
|
+
*/
|
|
3478
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
3479
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3480
|
+
const normalizedPath = normalizePath(path)
|
|
3481
|
+
|
|
3482
|
+
try {
|
|
3483
|
+
await this.page.waitForFunction(
|
|
3484
|
+
expectedPath => {
|
|
3485
|
+
const actualPath = window.location.pathname
|
|
3486
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
3487
|
+
return normalizePath(actualPath) === expectedPath
|
|
3488
|
+
},
|
|
3489
|
+
normalizedPath,
|
|
3490
|
+
{ timeout: waitTimeout },
|
|
3491
|
+
)
|
|
3492
|
+
} catch (e) {
|
|
3493
|
+
const currentUrl = await this._getPageUrl()
|
|
3494
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
3495
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
3496
|
+
if (/Timeout/i.test(e.message)) {
|
|
3497
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
3430
3498
|
} else {
|
|
3431
3499
|
throw e
|
|
3432
3500
|
}
|
|
@@ -4112,9 +4180,15 @@ class Playwright extends Helper {
|
|
|
4112
4180
|
|
|
4113
4181
|
export default Playwright
|
|
4114
4182
|
|
|
4115
|
-
function buildLocatorString(locator) {
|
|
4183
|
+
export function buildLocatorString(locator) {
|
|
4116
4184
|
if (locator.isXPath()) {
|
|
4117
|
-
|
|
4185
|
+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
|
|
4186
|
+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
|
|
4187
|
+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
|
|
4188
|
+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
|
|
4189
|
+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
|
|
4190
|
+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
|
|
4191
|
+
return `xpath=${value}`
|
|
4118
4192
|
}
|
|
4119
4193
|
if (locator.isShadow()) {
|
|
4120
4194
|
// Convert shadow locator to CSS with >> chaining operator
|
|
@@ -4125,25 +4199,22 @@ function buildLocatorString(locator) {
|
|
|
4125
4199
|
return locator.simplify()
|
|
4126
4200
|
}
|
|
4127
4201
|
|
|
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
4202
|
/**
|
|
4136
4203
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4204
|
+
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
4137
4205
|
* Returns elements array if role locator, null otherwise
|
|
4138
4206
|
*/
|
|
4139
4207
|
async function handleRoleLocator(context, locator) {
|
|
4140
|
-
|
|
4208
|
+
const loc = new Locator(locator)
|
|
4209
|
+
if (!loc.isRole()) return null
|
|
4141
4210
|
|
|
4211
|
+
const roleObj = loc.locator || {}
|
|
4142
4212
|
const options = {}
|
|
4143
|
-
if (
|
|
4144
|
-
if (
|
|
4213
|
+
if (roleObj.text) options.name = roleObj.text
|
|
4214
|
+
if (roleObj.name) options.name = roleObj.name
|
|
4215
|
+
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4145
4216
|
|
|
4146
|
-
return context.getByRole(
|
|
4217
|
+
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4147
4218
|
}
|
|
4148
4219
|
|
|
4149
4220
|
async function findByRole(context, locator) {
|
|
@@ -4155,13 +4226,10 @@ async function findByRole(context, locator) {
|
|
|
4155
4226
|
}
|
|
4156
4227
|
|
|
4157
4228
|
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
4229
|
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
4230
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4162
4231
|
|
|
4163
4232
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4164
|
-
if (isVueLocator) return findVue(matcher, locator)
|
|
4165
4233
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4166
4234
|
|
|
4167
4235
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
@@ -4177,7 +4245,6 @@ async function findElements(matcher, locator) {
|
|
|
4177
4245
|
|
|
4178
4246
|
async function findElement(matcher, locator) {
|
|
4179
4247
|
if (locator.react) return findReact(matcher, locator)
|
|
4180
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
4181
4248
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4182
4249
|
|
|
4183
4250
|
locator = new Locator(locator, 'css')
|
|
@@ -4212,16 +4279,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4212
4279
|
assertElementExists(els, locator, 'Clickable element')
|
|
4213
4280
|
}
|
|
4214
4281
|
|
|
4215
|
-
|
|
4216
|
-
|
|
4282
|
+
const opts = store.currentStep?.opts
|
|
4283
|
+
let element
|
|
4284
|
+
if (opts?.elementIndex != null) {
|
|
4285
|
+
element = selectElement(els, locator, this)
|
|
4286
|
+
} else {
|
|
4287
|
+
const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
|
|
4288
|
+
if (strict) assertOnlyOneElement(els, locator, this)
|
|
4289
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
await highlightActiveElement.call(this, element)
|
|
4293
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4217
4294
|
|
|
4218
|
-
/*
|
|
4219
|
-
using the force true options itself but instead dispatching a click
|
|
4220
|
-
*/
|
|
4221
4295
|
if (options.force) {
|
|
4222
|
-
await
|
|
4296
|
+
await element.dispatchEvent('click')
|
|
4223
4297
|
} else {
|
|
4224
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4225
4298
|
await element.click(options)
|
|
4226
4299
|
}
|
|
4227
4300
|
const promises = []
|
|
@@ -4238,7 +4311,6 @@ async function findClickable(matcher, locator) {
|
|
|
4238
4311
|
|
|
4239
4312
|
if (!matchedLocator.isFuzzy()) {
|
|
4240
4313
|
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4241
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4242
4314
|
return els
|
|
4243
4315
|
}
|
|
4244
4316
|
|
|
@@ -4247,42 +4319,27 @@ async function findClickable(matcher, locator) {
|
|
|
4247
4319
|
|
|
4248
4320
|
try {
|
|
4249
4321
|
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
|
-
}
|
|
4322
|
+
if (els.length) return els
|
|
4254
4323
|
} catch (err) {
|
|
4255
4324
|
// getByRole not supported or failed
|
|
4256
4325
|
}
|
|
4257
4326
|
|
|
4258
4327
|
try {
|
|
4259
4328
|
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
|
-
}
|
|
4329
|
+
if (els.length) return els
|
|
4264
4330
|
} catch (err) {
|
|
4265
4331
|
// getByRole not supported or failed
|
|
4266
4332
|
}
|
|
4267
4333
|
|
|
4268
4334
|
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
|
-
}
|
|
4335
|
+
if (els.length) return els
|
|
4273
4336
|
|
|
4274
4337
|
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
|
-
}
|
|
4338
|
+
if (els.length) return els
|
|
4279
4339
|
|
|
4280
4340
|
try {
|
|
4281
4341
|
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
|
-
}
|
|
4342
|
+
if (els.length) return els
|
|
4286
4343
|
} catch (err) {
|
|
4287
4344
|
// Do nothing
|
|
4288
4345
|
}
|
|
@@ -4355,34 +4412,42 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4355
4412
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4356
4413
|
}
|
|
4357
4414
|
|
|
4358
|
-
async function findFields(locator) {
|
|
4359
|
-
|
|
4360
|
-
if (
|
|
4361
|
-
const
|
|
4362
|
-
|
|
4363
|
-
|
|
4415
|
+
async function findFields(locator, context = null) {
|
|
4416
|
+
let contextEl
|
|
4417
|
+
if (context) {
|
|
4418
|
+
const contextEls = await this._locate(context)
|
|
4419
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4420
|
+
contextEl = contextEls[0]
|
|
4364
4421
|
}
|
|
4365
4422
|
|
|
4423
|
+
const locateFn = contextEl
|
|
4424
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4425
|
+
: loc => this._locate(loc)
|
|
4426
|
+
|
|
4427
|
+
const matcher = contextEl || (await this.page)
|
|
4428
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4429
|
+
if (roleElements) return roleElements
|
|
4430
|
+
|
|
4366
4431
|
const matchedLocator = new Locator(locator)
|
|
4367
4432
|
if (!matchedLocator.isFuzzy()) {
|
|
4368
|
-
return
|
|
4433
|
+
return locateFn(matchedLocator)
|
|
4369
4434
|
}
|
|
4370
4435
|
const literal = xpathLocator.literal(locator)
|
|
4371
4436
|
|
|
4372
|
-
let els = await
|
|
4437
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4373
4438
|
if (els.length) {
|
|
4374
4439
|
return els
|
|
4375
4440
|
}
|
|
4376
4441
|
|
|
4377
|
-
els = await
|
|
4442
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4378
4443
|
if (els.length) {
|
|
4379
4444
|
return els
|
|
4380
4445
|
}
|
|
4381
|
-
els = await
|
|
4446
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4382
4447
|
if (els.length) {
|
|
4383
4448
|
return els
|
|
4384
4449
|
}
|
|
4385
|
-
return
|
|
4450
|
+
return locateFn({ css: locator })
|
|
4386
4451
|
}
|
|
4387
4452
|
|
|
4388
4453
|
async function proceedSelect(context, el, option) {
|
|
@@ -4431,8 +4496,8 @@ async function proceedSelect(context, el, option) {
|
|
|
4431
4496
|
return this._waitForAction()
|
|
4432
4497
|
}
|
|
4433
4498
|
|
|
4434
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
4435
|
-
const els = await findFields.call(this, field)
|
|
4499
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4500
|
+
const els = await findFields.call(this, field, context)
|
|
4436
4501
|
assertElementExists(els, field, 'Field')
|
|
4437
4502
|
const el = els[0]
|
|
4438
4503
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4546,9 +4611,10 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4546
4611
|
}
|
|
4547
4612
|
}
|
|
4548
4613
|
|
|
4549
|
-
function assertOnlyOneElement(elements, locator) {
|
|
4614
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4550
4615
|
if (elements.length > 1) {
|
|
4551
|
-
|
|
4616
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4617
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4552
4618
|
}
|
|
4553
4619
|
}
|
|
4554
4620
|
|
|
@@ -4757,7 +4823,7 @@ async function refreshContextSession() {
|
|
|
4757
4823
|
|
|
4758
4824
|
function saveVideoForPage(page, name) {
|
|
4759
4825
|
if (!page.video()) return null
|
|
4760
|
-
const fileName = `${`${
|
|
4826
|
+
const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm`
|
|
4761
4827
|
page
|
|
4762
4828
|
.video()
|
|
4763
4829
|
.saveAs(fileName)
|
|
@@ -4774,7 +4840,7 @@ async function saveTraceForContext(context, name) {
|
|
|
4774
4840
|
if (!context) return
|
|
4775
4841
|
if (!context.tracing) return
|
|
4776
4842
|
try {
|
|
4777
|
-
const fileName = `${`${
|
|
4843
|
+
const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
4778
4844
|
await context.tracing.stop({ path: fileName })
|
|
4779
4845
|
return fileName
|
|
4780
4846
|
} catch (err) {
|