codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.10
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 +2 -2
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -11011
- package/typings/types.d.ts +0 -13073
package/lib/helper/Playwright.js
CHANGED
|
@@ -23,21 +23,26 @@ import {
|
|
|
23
23
|
clearString,
|
|
24
24
|
requireWithFallback,
|
|
25
25
|
normalizeSpacesInString,
|
|
26
|
+
normalizePath,
|
|
27
|
+
resolveUrl,
|
|
26
28
|
relativeDir,
|
|
29
|
+
getMimeType,
|
|
30
|
+
base64EncodeFile,
|
|
27
31
|
} from '../utils.js'
|
|
28
32
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
29
33
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
34
|
+
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
30
35
|
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
|
|
31
36
|
import Popup from './extras/Popup.js'
|
|
32
37
|
import Console from './extras/Console.js'
|
|
33
38
|
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
39
|
+
import { dropFile } from './scripts/dropFile.js'
|
|
34
40
|
import WebElement from '../element/WebElement.js'
|
|
41
|
+
import { selectElement } from './extras/elementSelection.js'
|
|
35
42
|
|
|
36
43
|
let playwright
|
|
37
44
|
let perfTiming
|
|
38
45
|
let defaultSelectorEnginesInitialized = false
|
|
39
|
-
let registeredCustomLocatorStrategies = new Set()
|
|
40
|
-
let globalCustomLocatorStrategies = new Map()
|
|
41
46
|
|
|
42
47
|
// Use global object to track selector registration across workers
|
|
43
48
|
if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
@@ -100,7 +105,6 @@ const pathSeparator = path.sep
|
|
|
100
105
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
101
106
|
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
102
107
|
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
103
|
-
* @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(`[role="${selector}"]`) } }`
|
|
104
108
|
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
|
|
105
109
|
* passed directly to `browser.newContext`.
|
|
106
110
|
* If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
|
|
@@ -355,28 +359,6 @@ class Playwright extends Helper {
|
|
|
355
359
|
this.recordingWebSocketMessages = false
|
|
356
360
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
357
361
|
this.cdpSession = null
|
|
358
|
-
|
|
359
|
-
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
360
|
-
// This can happen in worker threads where config is serialized/deserialized
|
|
361
|
-
let validCustomLocators = null
|
|
362
|
-
if (typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null) {
|
|
363
|
-
// Check if it's an empty array or object with no function properties
|
|
364
|
-
const entries = Object.entries(config.customLocatorStrategies)
|
|
365
|
-
const hasFunctions = entries.some(([_, value]) => typeof value === 'function')
|
|
366
|
-
if (hasFunctions) {
|
|
367
|
-
validCustomLocators = config.customLocatorStrategies
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
this.customLocatorStrategies = validCustomLocators
|
|
372
|
-
this._customLocatorsRegistered = false
|
|
373
|
-
|
|
374
|
-
// Add custom locator strategies to global registry for early registration
|
|
375
|
-
if (this.customLocatorStrategies) {
|
|
376
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
377
|
-
globalCustomLocatorStrategies.set(strategyName, strategyFunction)
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
362
|
|
|
381
363
|
// Add test failure tracking to prevent false positives
|
|
382
364
|
this.testFailures = []
|
|
@@ -416,6 +398,8 @@ class Playwright extends Helper {
|
|
|
416
398
|
ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
|
|
417
399
|
highlightElement: false,
|
|
418
400
|
storageState: undefined,
|
|
401
|
+
onResponse: null,
|
|
402
|
+
strict: false,
|
|
419
403
|
}
|
|
420
404
|
|
|
421
405
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -522,16 +506,6 @@ class Playwright extends Helper {
|
|
|
522
506
|
}
|
|
523
507
|
}
|
|
524
508
|
|
|
525
|
-
// Ensure custom locators from this instance are in the global registry
|
|
526
|
-
// This is critical for worker threads where globalCustomLocatorStrategies is a new Map
|
|
527
|
-
if (this.customLocatorStrategies) {
|
|
528
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
529
|
-
if (!globalCustomLocatorStrategies.has(strategyName)) {
|
|
530
|
-
globalCustomLocatorStrategies.set(strategyName, strategyFunction)
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
509
|
// register an internal selector engine for reading value property of elements in a selector
|
|
536
510
|
try {
|
|
537
511
|
// Always wrap in try-catch since selectors might be registered globally across workers
|
|
@@ -562,54 +536,6 @@ class Playwright extends Helper {
|
|
|
562
536
|
// Ignore if already set
|
|
563
537
|
}
|
|
564
538
|
}
|
|
565
|
-
|
|
566
|
-
// Register all custom locator strategies from the global registry
|
|
567
|
-
for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
|
|
568
|
-
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
569
|
-
try {
|
|
570
|
-
// Create a selector engine factory function exactly like createValueEngine pattern
|
|
571
|
-
// Capture variables in closure to avoid reference issues
|
|
572
|
-
const createCustomEngine = ((name, func) => {
|
|
573
|
-
return () => {
|
|
574
|
-
return {
|
|
575
|
-
create() {
|
|
576
|
-
return null
|
|
577
|
-
},
|
|
578
|
-
query(root, selector) {
|
|
579
|
-
try {
|
|
580
|
-
if (!root) return null
|
|
581
|
-
const result = func(selector, root)
|
|
582
|
-
return Array.isArray(result) ? result[0] : result
|
|
583
|
-
} catch (error) {
|
|
584
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
585
|
-
return null
|
|
586
|
-
}
|
|
587
|
-
},
|
|
588
|
-
queryAll(root, selector) {
|
|
589
|
-
try {
|
|
590
|
-
if (!root) return []
|
|
591
|
-
const result = func(selector, root)
|
|
592
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
593
|
-
} catch (error) {
|
|
594
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
595
|
-
return []
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
})(strategyName, strategyFunction)
|
|
601
|
-
|
|
602
|
-
await playwright.selectors.register(strategyName, createCustomEngine)
|
|
603
|
-
registeredCustomLocatorStrategies.add(strategyName)
|
|
604
|
-
} catch (error) {
|
|
605
|
-
if (!error.message.includes('already registered')) {
|
|
606
|
-
console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
|
|
607
|
-
} else {
|
|
608
|
-
console.log(`Custom locator strategy '${strategyName}' already registered`)
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
539
|
} catch (e) {
|
|
614
540
|
console.warn(e)
|
|
615
541
|
}
|
|
@@ -794,10 +720,7 @@ class Playwright extends Helper {
|
|
|
794
720
|
await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
|
|
795
721
|
}
|
|
796
722
|
// Use timeout to prevent hanging (10s should be enough for browser cleanup)
|
|
797
|
-
await Promise.race([
|
|
798
|
-
this._stopBrowser(),
|
|
799
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)),
|
|
800
|
-
])
|
|
723
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))])
|
|
801
724
|
} catch (e) {
|
|
802
725
|
console.warn('Warning during browser restart in _after:', e.message)
|
|
803
726
|
// Force cleanup even on timeout
|
|
@@ -840,10 +763,7 @@ class Playwright extends Helper {
|
|
|
840
763
|
if (this.isRunning) {
|
|
841
764
|
try {
|
|
842
765
|
// Add timeout protection to prevent hanging
|
|
843
|
-
await Promise.race([
|
|
844
|
-
this._stopBrowser(),
|
|
845
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)),
|
|
846
|
-
])
|
|
766
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))])
|
|
847
767
|
} catch (e) {
|
|
848
768
|
console.warn('Warning during suite cleanup:', e.message)
|
|
849
769
|
// Track suite cleanup failures
|
|
@@ -928,7 +848,7 @@ class Playwright extends Helper {
|
|
|
928
848
|
}
|
|
929
849
|
|
|
930
850
|
async _finishTest() {
|
|
931
|
-
if (
|
|
851
|
+
if (this.isRunning) {
|
|
932
852
|
try {
|
|
933
853
|
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Test finish timeout')), 10000))])
|
|
934
854
|
} catch (e) {
|
|
@@ -954,10 +874,7 @@ class Playwright extends Helper {
|
|
|
954
874
|
if (this.isRunning) {
|
|
955
875
|
try {
|
|
956
876
|
// Add timeout protection to prevent hanging
|
|
957
|
-
await Promise.race([
|
|
958
|
-
this._stopBrowser(),
|
|
959
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)),
|
|
960
|
-
])
|
|
877
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))])
|
|
961
878
|
} catch (e) {
|
|
962
879
|
console.warn('Warning during final cleanup:', e.message)
|
|
963
880
|
// Force cleanup on timeout
|
|
@@ -970,10 +887,7 @@ class Playwright extends Helper {
|
|
|
970
887
|
if (this.browser) {
|
|
971
888
|
try {
|
|
972
889
|
// Add timeout protection to prevent hanging
|
|
973
|
-
await Promise.race([
|
|
974
|
-
this._stopBrowser(),
|
|
975
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)),
|
|
976
|
-
])
|
|
890
|
+
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))])
|
|
977
891
|
} catch (e) {
|
|
978
892
|
console.warn('Warning during forced cleanup:', e.message)
|
|
979
893
|
// Force cleanup on timeout
|
|
@@ -1288,30 +1202,6 @@ class Playwright extends Helper {
|
|
|
1288
1202
|
return this.browser
|
|
1289
1203
|
}
|
|
1290
1204
|
|
|
1291
|
-
_lookupCustomLocator(customStrategy) {
|
|
1292
|
-
if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
|
|
1293
|
-
return null
|
|
1294
|
-
}
|
|
1295
|
-
const strategy = this.customLocatorStrategies[customStrategy]
|
|
1296
|
-
return typeof strategy === 'function' ? strategy : null
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
_isCustomLocator(locator) {
|
|
1300
|
-
const locatorObj = new Locator(locator)
|
|
1301
|
-
if (locatorObj.isCustom()) {
|
|
1302
|
-
const customLocator = this._lookupCustomLocator(locatorObj.type)
|
|
1303
|
-
if (customLocator) {
|
|
1304
|
-
return true
|
|
1305
|
-
}
|
|
1306
|
-
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1307
|
-
}
|
|
1308
|
-
return false
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
_isCustomLocatorStrategyDefined() {
|
|
1312
|
-
return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
1205
|
/**
|
|
1316
1206
|
* Create a new browser context with a page. \
|
|
1317
1207
|
* Usually it should be run from a custom helper after call of `_startBrowser()`
|
|
@@ -1323,63 +1213,11 @@ class Playwright extends Helper {
|
|
|
1323
1213
|
}
|
|
1324
1214
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1325
1215
|
|
|
1326
|
-
// Register custom locator strategies for this context
|
|
1327
|
-
await this._registerCustomLocatorStrategies()
|
|
1328
|
-
|
|
1329
1216
|
const page = await this.browserContext.newPage()
|
|
1330
1217
|
targetCreatedHandler.call(this, page)
|
|
1331
1218
|
await this._setPage(page)
|
|
1332
1219
|
}
|
|
1333
1220
|
|
|
1334
|
-
async _registerCustomLocatorStrategies() {
|
|
1335
|
-
if (!this.customLocatorStrategies) return
|
|
1336
|
-
|
|
1337
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
1338
|
-
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
1339
|
-
try {
|
|
1340
|
-
const createCustomEngine = ((name, func) => {
|
|
1341
|
-
return () => {
|
|
1342
|
-
return {
|
|
1343
|
-
create(root, target) {
|
|
1344
|
-
return null
|
|
1345
|
-
},
|
|
1346
|
-
query(root, selector) {
|
|
1347
|
-
try {
|
|
1348
|
-
if (!root) return null
|
|
1349
|
-
const result = func(selector, root)
|
|
1350
|
-
return Array.isArray(result) ? result[0] : result
|
|
1351
|
-
} catch (error) {
|
|
1352
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1353
|
-
return null
|
|
1354
|
-
}
|
|
1355
|
-
},
|
|
1356
|
-
queryAll(root, selector) {
|
|
1357
|
-
try {
|
|
1358
|
-
if (!root) return []
|
|
1359
|
-
const result = func(selector, root)
|
|
1360
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1363
|
-
return []
|
|
1364
|
-
}
|
|
1365
|
-
},
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
})(strategyName, strategyFunction)
|
|
1369
|
-
|
|
1370
|
-
await playwright.selectors.register(strategyName, createCustomEngine)
|
|
1371
|
-
registeredCustomLocatorStrategies.add(strategyName)
|
|
1372
|
-
} catch (error) {
|
|
1373
|
-
if (!error.message.includes('already registered')) {
|
|
1374
|
-
console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
|
|
1375
|
-
} else {
|
|
1376
|
-
console.log(`Custom locator strategy '${strategyName}' already registered`)
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
1221
|
_getType() {
|
|
1384
1222
|
return this.browser._type
|
|
1385
1223
|
}
|
|
@@ -1390,7 +1228,7 @@ class Playwright extends Helper {
|
|
|
1390
1228
|
this.context = null
|
|
1391
1229
|
this.frame = null
|
|
1392
1230
|
popupStore.clear()
|
|
1393
|
-
|
|
1231
|
+
|
|
1394
1232
|
// Remove all event listeners to prevent hanging
|
|
1395
1233
|
if (this.browser) {
|
|
1396
1234
|
try {
|
|
@@ -1399,7 +1237,8 @@ class Playwright extends Helper {
|
|
|
1399
1237
|
// Ignore errors if browser is already closed
|
|
1400
1238
|
}
|
|
1401
1239
|
}
|
|
1402
|
-
|
|
1240
|
+
|
|
1241
|
+
// Close browserContext if recordHar is enabled
|
|
1403
1242
|
if (this.options.recordHar && this.browserContext) {
|
|
1404
1243
|
try {
|
|
1405
1244
|
await this.browserContext.close()
|
|
@@ -1408,22 +1247,17 @@ class Playwright extends Helper {
|
|
|
1408
1247
|
}
|
|
1409
1248
|
}
|
|
1410
1249
|
this.browserContext = null
|
|
1411
|
-
|
|
1250
|
+
|
|
1251
|
+
// Initiate browser close without waiting for it to complete
|
|
1252
|
+
// The browser process will be cleaned up when the Node process exits
|
|
1412
1253
|
if (this.browser) {
|
|
1413
1254
|
try {
|
|
1414
|
-
//
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
setTimeout(() => reject(new Error('Browser close timeout')), 5000)
|
|
1419
|
-
)
|
|
1420
|
-
])
|
|
1255
|
+
// Fire and forget - don't wait for close to complete
|
|
1256
|
+
this.browser.close().catch(() => {
|
|
1257
|
+
// Silently ignore any errors during async close
|
|
1258
|
+
})
|
|
1421
1259
|
} catch (e) {
|
|
1422
|
-
// Ignore
|
|
1423
|
-
if (!e.message?.includes('Browser close timeout')) {
|
|
1424
|
-
// Non-timeout error, can be ignored as well
|
|
1425
|
-
}
|
|
1426
|
-
// Force cleanup even on error
|
|
1260
|
+
// Ignore any synchronous errors
|
|
1427
1261
|
}
|
|
1428
1262
|
}
|
|
1429
1263
|
this.browser = null
|
|
@@ -1539,7 +1373,7 @@ class Playwright extends Helper {
|
|
|
1539
1373
|
acceptDownloads: true,
|
|
1540
1374
|
...this.options.emulate,
|
|
1541
1375
|
}
|
|
1542
|
-
|
|
1376
|
+
|
|
1543
1377
|
try {
|
|
1544
1378
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
1545
1379
|
} catch (err) {
|
|
@@ -1662,8 +1496,23 @@ class Playwright extends Helper {
|
|
|
1662
1496
|
*
|
|
1663
1497
|
*/
|
|
1664
1498
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
1665
|
-
|
|
1666
|
-
|
|
1499
|
+
let context = null
|
|
1500
|
+
if (typeof offsetX !== 'number') {
|
|
1501
|
+
context = offsetX
|
|
1502
|
+
offsetX = 0
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
let el
|
|
1506
|
+
if (context) {
|
|
1507
|
+
const contextEls = await this._locate(context)
|
|
1508
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1509
|
+
el = await findElements.call(this, contextEls[0], locator)
|
|
1510
|
+
assertElementExists(el, locator)
|
|
1511
|
+
el = el[0]
|
|
1512
|
+
} else {
|
|
1513
|
+
el = await this._locateElement(locator)
|
|
1514
|
+
assertElementExists(el, locator)
|
|
1515
|
+
}
|
|
1667
1516
|
|
|
1668
1517
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
1669
1518
|
const { x, y } = await clickablePoint(el)
|
|
@@ -1927,7 +1776,11 @@ class Playwright extends Helper {
|
|
|
1927
1776
|
*/
|
|
1928
1777
|
async _locateElement(locator) {
|
|
1929
1778
|
const context = await this._getContext()
|
|
1930
|
-
|
|
1779
|
+
const elements = await findElements.call(this, context, locator)
|
|
1780
|
+
if (elements.length === 0) {
|
|
1781
|
+
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1782
|
+
}
|
|
1783
|
+
return selectElement(elements, locator, this)
|
|
1931
1784
|
}
|
|
1932
1785
|
|
|
1933
1786
|
/**
|
|
@@ -1942,7 +1795,7 @@ class Playwright extends Helper {
|
|
|
1942
1795
|
const context = providedContext || (await this._getContext())
|
|
1943
1796
|
const els = await findCheckable.call(this, locator, context)
|
|
1944
1797
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1945
|
-
return els
|
|
1798
|
+
return selectElement(els, locator, this)
|
|
1946
1799
|
}
|
|
1947
1800
|
|
|
1948
1801
|
/**
|
|
@@ -2110,8 +1963,15 @@ class Playwright extends Helper {
|
|
|
2110
1963
|
* {{> seeElement }}
|
|
2111
1964
|
*
|
|
2112
1965
|
*/
|
|
2113
|
-
async seeElement(locator) {
|
|
2114
|
-
let els
|
|
1966
|
+
async seeElement(locator, context = null) {
|
|
1967
|
+
let els
|
|
1968
|
+
if (context) {
|
|
1969
|
+
const contextEls = await this._locate(context)
|
|
1970
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1971
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1972
|
+
} else {
|
|
1973
|
+
els = await this._locate(locator)
|
|
1974
|
+
}
|
|
2115
1975
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2116
1976
|
try {
|
|
2117
1977
|
return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2124,8 +1984,15 @@ class Playwright extends Helper {
|
|
|
2124
1984
|
* {{> dontSeeElement }}
|
|
2125
1985
|
*
|
|
2126
1986
|
*/
|
|
2127
|
-
async dontSeeElement(locator) {
|
|
2128
|
-
let els
|
|
1987
|
+
async dontSeeElement(locator, context = null) {
|
|
1988
|
+
let els
|
|
1989
|
+
if (context) {
|
|
1990
|
+
const contextEls = await this._locate(context)
|
|
1991
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
1992
|
+
els = await findElements.call(this, contextEls[0], locator)
|
|
1993
|
+
} else {
|
|
1994
|
+
els = await this._locate(locator)
|
|
1995
|
+
}
|
|
2129
1996
|
els = await Promise.all(els.map(el => el.isVisible()))
|
|
2130
1997
|
try {
|
|
2131
1998
|
return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT'))
|
|
@@ -2411,10 +2278,10 @@ class Playwright extends Helper {
|
|
|
2411
2278
|
* {{> fillField }}
|
|
2412
2279
|
*
|
|
2413
2280
|
*/
|
|
2414
|
-
async fillField(field, value) {
|
|
2415
|
-
const els = await findFields.call(this, field)
|
|
2281
|
+
async fillField(field, value, context = null) {
|
|
2282
|
+
const els = await findFields.call(this, field, context)
|
|
2416
2283
|
assertElementExists(els, field, 'Field')
|
|
2417
|
-
const el = els
|
|
2284
|
+
const el = selectElement(els, field, this)
|
|
2418
2285
|
|
|
2419
2286
|
await el.clear()
|
|
2420
2287
|
if (store.debugMode) this.debugSection('Focused', await elToString(el, 1))
|
|
@@ -2427,27 +2294,13 @@ class Playwright extends Helper {
|
|
|
2427
2294
|
}
|
|
2428
2295
|
|
|
2429
2296
|
/**
|
|
2430
|
-
*
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2433
|
-
* Examples:
|
|
2434
|
-
*
|
|
2435
|
-
* ```js
|
|
2436
|
-
* I.clearField('.text-area')
|
|
2437
|
-
*
|
|
2438
|
-
* // if this doesn't work use force option
|
|
2439
|
-
* I.clearField('#submit', { force: true })
|
|
2440
|
-
* ```
|
|
2441
|
-
* Use `force` to bypass the [actionability](https://playwright.dev/docs/actionability) checks.
|
|
2442
|
-
*
|
|
2443
|
-
* @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
|
|
2444
|
-
* @param {any} [options] [Additional options](https://playwright.dev/docs/api/class-locator#locator-clear) for available options object as 2nd argument.
|
|
2297
|
+
* {{> clearField }}
|
|
2445
2298
|
*/
|
|
2446
|
-
async clearField(locator,
|
|
2447
|
-
const els = await findFields.call(this, locator)
|
|
2299
|
+
async clearField(locator, context = null) {
|
|
2300
|
+
const els = await findFields.call(this, locator, context)
|
|
2448
2301
|
assertElementExists(els, locator, 'Field to clear')
|
|
2449
2302
|
|
|
2450
|
-
const el = els
|
|
2303
|
+
const el = selectElement(els, locator, this)
|
|
2451
2304
|
|
|
2452
2305
|
await highlightActiveElement.call(this, el)
|
|
2453
2306
|
|
|
@@ -2459,68 +2312,101 @@ class Playwright extends Helper {
|
|
|
2459
2312
|
/**
|
|
2460
2313
|
* {{> appendField }}
|
|
2461
2314
|
*/
|
|
2462
|
-
async appendField(field, value) {
|
|
2463
|
-
const els = await findFields.call(this, field)
|
|
2315
|
+
async appendField(field, value, context = null) {
|
|
2316
|
+
const els = await findFields.call(this, field, context)
|
|
2464
2317
|
assertElementExists(els, field, 'Field')
|
|
2465
|
-
|
|
2466
|
-
await
|
|
2467
|
-
await
|
|
2318
|
+
const el = selectElement(els, field, this)
|
|
2319
|
+
await highlightActiveElement.call(this, el)
|
|
2320
|
+
await el.press('End')
|
|
2321
|
+
await el.type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
2468
2322
|
return this._waitForAction()
|
|
2469
2323
|
}
|
|
2470
2324
|
|
|
2471
2325
|
/**
|
|
2472
2326
|
* {{> seeInField }}
|
|
2473
2327
|
*/
|
|
2474
|
-
async seeInField(field, value) {
|
|
2328
|
+
async seeInField(field, value, context = null) {
|
|
2475
2329
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2476
|
-
return proceedSeeInField.call(this, 'assert', field, _value)
|
|
2330
|
+
return proceedSeeInField.call(this, 'assert', field, _value, context)
|
|
2477
2331
|
}
|
|
2478
2332
|
|
|
2479
2333
|
/**
|
|
2480
2334
|
* {{> dontSeeInField }}
|
|
2481
2335
|
*/
|
|
2482
|
-
async dontSeeInField(field, value) {
|
|
2336
|
+
async dontSeeInField(field, value, context = null) {
|
|
2483
2337
|
const _value = typeof value === 'boolean' ? value : value.toString()
|
|
2484
|
-
return proceedSeeInField.call(this, 'negate', field, _value)
|
|
2338
|
+
return proceedSeeInField.call(this, 'negate', field, _value, context)
|
|
2485
2339
|
}
|
|
2486
2340
|
|
|
2487
2341
|
/**
|
|
2488
2342
|
* {{> attachFile }}
|
|
2489
2343
|
*
|
|
2490
2344
|
*/
|
|
2491
|
-
async attachFile(locator, pathToFile) {
|
|
2345
|
+
async attachFile(locator, pathToFile, context = null) {
|
|
2492
2346
|
const file = path.join(global.codecept_dir, pathToFile)
|
|
2493
2347
|
|
|
2494
2348
|
if (!fileExists(file)) {
|
|
2495
2349
|
throw new Error(`File at ${file} can not be found on local system`)
|
|
2496
2350
|
}
|
|
2497
|
-
const els = await findFields.call(this, locator)
|
|
2498
|
-
|
|
2499
|
-
|
|
2351
|
+
const els = await findFields.call(this, locator, context)
|
|
2352
|
+
if (els.length) {
|
|
2353
|
+
const el = selectElement(els, locator, this)
|
|
2354
|
+
const tag = await el.evaluate(el => el.tagName)
|
|
2355
|
+
const type = await el.evaluate(el => el.type)
|
|
2356
|
+
if (tag === 'INPUT' && type === 'file') {
|
|
2357
|
+
await el.setInputFiles(file)
|
|
2358
|
+
return this._waitForAction()
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const targetEls = els.length ? els : await this._locate(locator)
|
|
2363
|
+
assertElementExists(targetEls, locator, 'Element')
|
|
2364
|
+
const el = selectElement(targetEls, locator, this)
|
|
2365
|
+
const fileData = {
|
|
2366
|
+
base64Content: base64EncodeFile(file),
|
|
2367
|
+
fileName: path.basename(file),
|
|
2368
|
+
mimeType: getMimeType(path.basename(file)),
|
|
2369
|
+
}
|
|
2370
|
+
await el.evaluate(dropFile, fileData)
|
|
2500
2371
|
return this._waitForAction()
|
|
2501
2372
|
}
|
|
2502
2373
|
|
|
2503
2374
|
/**
|
|
2504
2375
|
* {{> selectOption }}
|
|
2505
2376
|
*/
|
|
2506
|
-
async selectOption(select, option) {
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
const el = els[0]
|
|
2377
|
+
async selectOption(select, option, context = null) {
|
|
2378
|
+
const pageContext = await this.context
|
|
2379
|
+
const matchedLocator = new Locator(select)
|
|
2510
2380
|
|
|
2511
|
-
|
|
2512
|
-
|
|
2381
|
+
let contextEl
|
|
2382
|
+
if (context) {
|
|
2383
|
+
const contextEls = await this._locate(context)
|
|
2384
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
2385
|
+
contextEl = contextEls[0]
|
|
2386
|
+
}
|
|
2513
2387
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2388
|
+
// Strict locator
|
|
2389
|
+
if (!matchedLocator.isFuzzy()) {
|
|
2390
|
+
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2391
|
+
const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator)
|
|
2392
|
+
assertElementExists(els, select, 'Selectable element')
|
|
2393
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2518
2394
|
}
|
|
2519
2395
|
|
|
2520
|
-
|
|
2396
|
+
// Fuzzy: try combobox
|
|
2397
|
+
this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`)
|
|
2398
|
+
const comboboxSearchCtx = contextEl || pageContext
|
|
2399
|
+
let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value })
|
|
2400
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2521
2401
|
|
|
2522
|
-
|
|
2523
|
-
|
|
2402
|
+
// Fuzzy: try listbox
|
|
2403
|
+
els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value })
|
|
2404
|
+
if (els?.length) return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2405
|
+
|
|
2406
|
+
// Fuzzy: try native select
|
|
2407
|
+
els = await findFields.call(this, select, context)
|
|
2408
|
+
assertElementExists(els, select, 'Selectable element')
|
|
2409
|
+
return proceedSelect.call(this, pageContext, selectElement(els, select, this), option)
|
|
2524
2410
|
}
|
|
2525
2411
|
|
|
2526
2412
|
/**
|
|
@@ -2561,6 +2447,26 @@ class Playwright extends Helper {
|
|
|
2561
2447
|
urlEquals(this.options.url).negate(url, await this._getPageUrl())
|
|
2562
2448
|
}
|
|
2563
2449
|
|
|
2450
|
+
/**
|
|
2451
|
+
* {{> seeCurrentPathEquals }}
|
|
2452
|
+
*/
|
|
2453
|
+
async seeCurrentPathEquals(path) {
|
|
2454
|
+
const currentUrl = await this._getPageUrl()
|
|
2455
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2456
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2457
|
+
return equals('url path').assert(normalizePath(path), normalizePath(actualPath))
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/**
|
|
2461
|
+
* {{> dontSeeCurrentPathEquals }}
|
|
2462
|
+
*/
|
|
2463
|
+
async dontSeeCurrentPathEquals(path) {
|
|
2464
|
+
const currentUrl = await this._getPageUrl()
|
|
2465
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
2466
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
2467
|
+
return equals('url path').negate(normalizePath(path), normalizePath(actualPath))
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2564
2470
|
/**
|
|
2565
2471
|
* {{> see }}
|
|
2566
2472
|
*
|
|
@@ -2778,23 +2684,12 @@ class Playwright extends Helper {
|
|
|
2778
2684
|
_contextLocator(locator) {
|
|
2779
2685
|
const locatorObj = new Locator(locator, 'css')
|
|
2780
2686
|
|
|
2781
|
-
// Handle custom locators differently
|
|
2782
|
-
if (locatorObj.isCustom()) {
|
|
2783
|
-
return buildCustomLocatorString(locatorObj)
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
2687
|
locator = buildLocatorString(locatorObj)
|
|
2787
2688
|
|
|
2788
2689
|
if (this.contextLocator) {
|
|
2789
2690
|
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
// Instead, we'll need to handle this differently in the calling methods
|
|
2793
|
-
return locator
|
|
2794
|
-
} else {
|
|
2795
|
-
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2796
|
-
locator = `${contextLocator} >> ${locator}`
|
|
2797
|
-
}
|
|
2691
|
+
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2692
|
+
locator = `${contextLocator} >> ${locator}`
|
|
2798
2693
|
}
|
|
2799
2694
|
|
|
2800
2695
|
return locator
|
|
@@ -2805,43 +2700,28 @@ class Playwright extends Helper {
|
|
|
2805
2700
|
*
|
|
2806
2701
|
*/
|
|
2807
2702
|
async grabTextFrom(locator) {
|
|
2808
|
-
|
|
2809
|
-
if (
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
this.debugSection('Text', text)
|
|
2815
|
-
return text
|
|
2816
|
-
}
|
|
2703
|
+
const roleElements = await handleRoleLocator(this.page, locator)
|
|
2704
|
+
if (roleElements && roleElements.length > 0) {
|
|
2705
|
+
const text = await roleElements[0].textContent()
|
|
2706
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2707
|
+
this.debugSection('Text', text)
|
|
2708
|
+
return text
|
|
2817
2709
|
}
|
|
2818
2710
|
|
|
2819
2711
|
const locatorObj = new Locator(locator, 'css')
|
|
2820
2712
|
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
throw new Error(`Element not found: ${locatorObj.toString()}`)
|
|
2826
|
-
}
|
|
2827
|
-
const text = await elements[0].textContent()
|
|
2828
|
-
assertElementExists(text, locatorObj.toString())
|
|
2713
|
+
locator = this._contextLocator(locator)
|
|
2714
|
+
try {
|
|
2715
|
+
const text = await this.page.textContent(locator)
|
|
2716
|
+
assertElementExists(text, locator)
|
|
2829
2717
|
this.debugSection('Text', text)
|
|
2830
2718
|
return text
|
|
2831
|
-
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
assertElementExists(text, locator)
|
|
2836
|
-
this.debugSection('Text', text)
|
|
2837
|
-
return text
|
|
2838
|
-
} catch (error) {
|
|
2839
|
-
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2840
|
-
if (error.message && error.message.includes('Timeout')) {
|
|
2841
|
-
throw new ElementNotFound(locator, 'text')
|
|
2842
|
-
}
|
|
2843
|
-
throw error
|
|
2719
|
+
} catch (error) {
|
|
2720
|
+
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2721
|
+
if (error.message && error.message.includes('Timeout')) {
|
|
2722
|
+
throw new ElementNotFound(locator, 'text')
|
|
2844
2723
|
}
|
|
2724
|
+
throw error
|
|
2845
2725
|
}
|
|
2846
2726
|
}
|
|
2847
2727
|
|
|
@@ -3183,14 +3063,14 @@ class Playwright extends Helper {
|
|
|
3183
3063
|
this.debugSection('Response', await response.text())
|
|
3184
3064
|
|
|
3185
3065
|
// hook to allow JSON response handle this
|
|
3186
|
-
if (this.
|
|
3066
|
+
if (this.options.onResponse) {
|
|
3187
3067
|
const axiosResponse = {
|
|
3188
3068
|
data: await response.json(),
|
|
3189
3069
|
status: response.status(),
|
|
3190
3070
|
statusText: response.statusText(),
|
|
3191
3071
|
headers: response.headers(),
|
|
3192
3072
|
}
|
|
3193
|
-
this.
|
|
3073
|
+
this.options.onResponse(axiosResponse)
|
|
3194
3074
|
}
|
|
3195
3075
|
|
|
3196
3076
|
return response
|
|
@@ -3406,16 +3286,7 @@ class Playwright extends Helper {
|
|
|
3406
3286
|
|
|
3407
3287
|
const context = await this._getContext()
|
|
3408
3288
|
try {
|
|
3409
|
-
|
|
3410
|
-
// For custom locators, we need to use our custom element finding logic
|
|
3411
|
-
const elements = await findCustomElements.call(this, context, locator)
|
|
3412
|
-
if (elements.length === 0) {
|
|
3413
|
-
throw new Error(`Custom locator ${locator.type}=${locator.value} not found`)
|
|
3414
|
-
}
|
|
3415
|
-
await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3416
|
-
} else {
|
|
3417
|
-
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3418
|
-
}
|
|
3289
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
3419
3290
|
} catch (e) {
|
|
3420
3291
|
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
3421
3292
|
}
|
|
@@ -3433,26 +3304,6 @@ class Playwright extends Helper {
|
|
|
3433
3304
|
const context = await this._getContext()
|
|
3434
3305
|
let count = 0
|
|
3435
3306
|
|
|
3436
|
-
// Handle custom locators
|
|
3437
|
-
if (locator.isCustom()) {
|
|
3438
|
-
let waiter
|
|
3439
|
-
do {
|
|
3440
|
-
const elements = await findCustomElements.call(this, context, locator)
|
|
3441
|
-
if (elements.length > 0) {
|
|
3442
|
-
waiter = await elements[0].isVisible()
|
|
3443
|
-
} else {
|
|
3444
|
-
waiter = false
|
|
3445
|
-
}
|
|
3446
|
-
if (!waiter) {
|
|
3447
|
-
await this.wait(1)
|
|
3448
|
-
count += 1000
|
|
3449
|
-
}
|
|
3450
|
-
} while (!waiter && count <= waitTimeout)
|
|
3451
|
-
|
|
3452
|
-
if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`)
|
|
3453
|
-
return
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
3307
|
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
3457
3308
|
let waiter
|
|
3458
3309
|
if (this.frame) {
|
|
@@ -3570,6 +3421,7 @@ class Playwright extends Helper {
|
|
|
3570
3421
|
*/
|
|
3571
3422
|
async waitInUrl(urlPart, sec = null) {
|
|
3572
3423
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3424
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3573
3425
|
|
|
3574
3426
|
return this.page
|
|
3575
3427
|
.waitForFunction(
|
|
@@ -3577,13 +3429,13 @@ class Playwright extends Helper {
|
|
|
3577
3429
|
const currUrl = decodeURIComponent(decodeURIComponent(decodeURIComponent(window.location.href)))
|
|
3578
3430
|
return currUrl.indexOf(urlPart) > -1
|
|
3579
3431
|
},
|
|
3580
|
-
|
|
3432
|
+
expectedUrl,
|
|
3581
3433
|
{ timeout: waitTimeout },
|
|
3582
3434
|
)
|
|
3583
3435
|
.catch(async e => {
|
|
3584
|
-
const currUrl = await this._getPageUrl()
|
|
3436
|
+
const currUrl = await this._getPageUrl()
|
|
3585
3437
|
if (/Timeout/i.test(e.message)) {
|
|
3586
|
-
throw new Error(`expected url to include ${
|
|
3438
|
+
throw new Error(`expected url to include ${expectedUrl}, but found ${currUrl}`)
|
|
3587
3439
|
} else {
|
|
3588
3440
|
throw e
|
|
3589
3441
|
}
|
|
@@ -3595,29 +3447,50 @@ class Playwright extends Helper {
|
|
|
3595
3447
|
*/
|
|
3596
3448
|
async waitUrlEquals(urlPart, sec = null) {
|
|
3597
3449
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3450
|
+
const expectedUrl = resolveUrl(urlPart, this.options.url)
|
|
3598
3451
|
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3452
|
+
try {
|
|
3453
|
+
await this.page.waitForURL(
|
|
3454
|
+
url => url.href === expectedUrl,
|
|
3455
|
+
{ timeout: waitTimeout },
|
|
3456
|
+
)
|
|
3457
|
+
} catch (e) {
|
|
3458
|
+
const currUrl = await this._getPageUrl()
|
|
3459
|
+
if (/Timeout/i.test(e.message)) {
|
|
3460
|
+
throw new Error(`expected url to be ${expectedUrl}, but found ${currUrl}`)
|
|
3461
|
+
} else {
|
|
3462
|
+
throw e
|
|
3463
|
+
}
|
|
3602
3464
|
}
|
|
3465
|
+
}
|
|
3603
3466
|
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3467
|
+
/**
|
|
3468
|
+
* {{> waitCurrentPathEquals }}
|
|
3469
|
+
*/
|
|
3470
|
+
async waitCurrentPathEquals(path, sec = null) {
|
|
3471
|
+
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
3472
|
+
const normalizedPath = normalizePath(path)
|
|
3473
|
+
|
|
3474
|
+
try {
|
|
3475
|
+
await this.page.waitForFunction(
|
|
3476
|
+
expectedPath => {
|
|
3477
|
+
const actualPath = window.location.pathname
|
|
3478
|
+
const normalizePath = p => (p === '' || p === '/' ? '/' : p.replace(/\/+/g, '/').replace(/\/$/, '') || '/')
|
|
3479
|
+
return normalizePath(actualPath) === expectedPath
|
|
3609
3480
|
},
|
|
3610
|
-
|
|
3481
|
+
normalizedPath,
|
|
3611
3482
|
{ timeout: waitTimeout },
|
|
3612
3483
|
)
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3484
|
+
} catch (e) {
|
|
3485
|
+
const currentUrl = await this._getPageUrl()
|
|
3486
|
+
const baseUrl = this.options.url || 'http://localhost'
|
|
3487
|
+
const actualPath = new URL(currentUrl, baseUrl).pathname
|
|
3488
|
+
if (/Timeout/i.test(e.message)) {
|
|
3489
|
+
throw new Error(`expected path to be ${normalizedPath}, but found ${normalizePath(actualPath)}`)
|
|
3490
|
+
} else {
|
|
3491
|
+
throw e
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3621
3494
|
}
|
|
3622
3495
|
|
|
3623
3496
|
/**
|
|
@@ -3632,15 +3505,6 @@ class Playwright extends Helper {
|
|
|
3632
3505
|
if (context) {
|
|
3633
3506
|
const locator = new Locator(context, 'css')
|
|
3634
3507
|
try {
|
|
3635
|
-
if (locator.isCustom()) {
|
|
3636
|
-
// For custom locators, find the elements first then check for text within them
|
|
3637
|
-
const elements = await findCustomElements.call(this, contextObject, locator)
|
|
3638
|
-
if (elements.length === 0) {
|
|
3639
|
-
throw new Error(`Context element not found: ${locator.toString()}`)
|
|
3640
|
-
}
|
|
3641
|
-
return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
3642
|
-
}
|
|
3643
|
-
|
|
3644
3508
|
if (!locator.isXPath()) {
|
|
3645
3509
|
return contextObject
|
|
3646
3510
|
.locator(`${locator.simplify()} >> text=${text}`)
|
|
@@ -3883,7 +3747,7 @@ class Playwright extends Helper {
|
|
|
3883
3747
|
if (!locator.isXPath()) {
|
|
3884
3748
|
try {
|
|
3885
3749
|
await context
|
|
3886
|
-
.locator(
|
|
3750
|
+
.locator(locator.simplify())
|
|
3887
3751
|
.first()
|
|
3888
3752
|
.waitFor({ timeout: waitTimeout, state: 'detached' })
|
|
3889
3753
|
} catch (e) {
|
|
@@ -4308,40 +4172,48 @@ class Playwright extends Helper {
|
|
|
4308
4172
|
|
|
4309
4173
|
export default Playwright
|
|
4310
4174
|
|
|
4311
|
-
function
|
|
4312
|
-
// Note: this.debug not available in standalone function, using console.log
|
|
4313
|
-
console.log(`Building custom locator string: ${locator.type}=${locator.value}`)
|
|
4314
|
-
return `${locator.type}=${locator.value}`
|
|
4315
|
-
}
|
|
4316
|
-
|
|
4317
|
-
function buildLocatorString(locator) {
|
|
4318
|
-
if (locator.isCustom()) {
|
|
4319
|
-
return buildCustomLocatorString(locator)
|
|
4320
|
-
}
|
|
4175
|
+
export function buildLocatorString(locator) {
|
|
4321
4176
|
if (locator.isXPath()) {
|
|
4322
|
-
|
|
4177
|
+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
|
|
4178
|
+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
|
|
4179
|
+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
|
|
4180
|
+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
|
|
4181
|
+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
|
|
4182
|
+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
|
|
4183
|
+
return `xpath=${value}`
|
|
4184
|
+
}
|
|
4185
|
+
if (locator.isShadow()) {
|
|
4186
|
+
// Convert shadow locator to CSS with >> chaining operator
|
|
4187
|
+
// Playwright pierces shadow DOM by default, >> chains selectors
|
|
4188
|
+
// { shadow: ['my-app', 'my-form', 'button'] } => 'my-app >> my-form >> button'
|
|
4189
|
+
return locator.value.join(' >> ')
|
|
4323
4190
|
}
|
|
4324
4191
|
return locator.simplify()
|
|
4325
4192
|
}
|
|
4326
4193
|
|
|
4327
|
-
/**
|
|
4328
|
-
* Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4329
|
-
*/
|
|
4330
|
-
function isRoleLocatorObject(locator) {
|
|
4331
|
-
return locator && typeof locator === 'object' && locator.role && !locator.type
|
|
4332
|
-
}
|
|
4333
|
-
|
|
4334
4194
|
/**
|
|
4335
4195
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4196
|
+
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
4336
4197
|
* Returns elements array if role locator, null otherwise
|
|
4337
4198
|
*/
|
|
4338
4199
|
async function handleRoleLocator(context, locator) {
|
|
4339
|
-
|
|
4340
|
-
|
|
4200
|
+
const loc = new Locator(locator)
|
|
4201
|
+
if (!loc.isRole()) return null
|
|
4202
|
+
|
|
4203
|
+
const roleObj = loc.locator || {}
|
|
4204
|
+
const options = {}
|
|
4205
|
+
if (roleObj.text) options.name = roleObj.text
|
|
4206
|
+
if (roleObj.name) options.name = roleObj.name
|
|
4207
|
+
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4208
|
+
|
|
4209
|
+
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
async function findByRole(context, locator) {
|
|
4213
|
+
if (!locator || !locator.role) return null
|
|
4341
4214
|
const options = {}
|
|
4342
|
-
if (locator.
|
|
4215
|
+
if (locator.name) options.name = locator.name
|
|
4343
4216
|
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4344
|
-
|
|
4345
4217
|
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4346
4218
|
}
|
|
4347
4219
|
|
|
@@ -4350,7 +4222,7 @@ async function findElements(matcher, locator) {
|
|
|
4350
4222
|
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4351
4223
|
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4352
4224
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4353
|
-
|
|
4225
|
+
|
|
4354
4226
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4355
4227
|
if (isVueLocator) return findVue(matcher, locator)
|
|
4356
4228
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
@@ -4361,119 +4233,11 @@ async function findElements(matcher, locator) {
|
|
|
4361
4233
|
|
|
4362
4234
|
locator = new Locator(locator, 'css')
|
|
4363
4235
|
|
|
4364
|
-
// Handle custom locators directly instead of relying on Playwright selector engines
|
|
4365
|
-
if (locator.isCustom()) {
|
|
4366
|
-
return findCustomElements.call(this, matcher, locator)
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
// Check if we have a custom context locator and need to search within it
|
|
4370
|
-
if (this.contextLocator) {
|
|
4371
|
-
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
4372
|
-
if (contextLocatorObj.isCustom()) {
|
|
4373
|
-
// Find the context elements first
|
|
4374
|
-
const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj)
|
|
4375
|
-
if (contextElements.length === 0) {
|
|
4376
|
-
return []
|
|
4377
|
-
}
|
|
4378
|
-
|
|
4379
|
-
// Search within the first context element
|
|
4380
|
-
const locatorString = buildLocatorString(locator)
|
|
4381
|
-
return contextElements[0].locator(locatorString).all()
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
|
|
4385
4236
|
const locatorString = buildLocatorString(locator)
|
|
4386
4237
|
|
|
4387
4238
|
return matcher.locator(locatorString).all()
|
|
4388
4239
|
}
|
|
4389
4240
|
|
|
4390
|
-
async function findCustomElements(matcher, locator) {
|
|
4391
|
-
// Always prioritize this.customLocatorStrategies which is set in constructor from config
|
|
4392
|
-
// and persists in every worker thread instance
|
|
4393
|
-
let strategyFunction = null
|
|
4394
|
-
|
|
4395
|
-
if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
|
|
4396
|
-
strategyFunction = this.customLocatorStrategies[locator.type]
|
|
4397
|
-
} else if (globalCustomLocatorStrategies.has(locator.type)) {
|
|
4398
|
-
// Fallback to global registry (populated in constructor and _init)
|
|
4399
|
-
strategyFunction = globalCustomLocatorStrategies.get(locator.type)
|
|
4400
|
-
}
|
|
4401
|
-
|
|
4402
|
-
if (!strategyFunction) {
|
|
4403
|
-
throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
|
-
// Execute the custom locator function in the browser context using page.evaluate
|
|
4407
|
-
const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page()
|
|
4408
|
-
|
|
4409
|
-
const elements = await page.evaluate(
|
|
4410
|
-
({ strategyCode, selector }) => {
|
|
4411
|
-
const strategy = new Function('return ' + strategyCode)()
|
|
4412
|
-
const result = strategy(selector, document)
|
|
4413
|
-
|
|
4414
|
-
// Convert NodeList or single element to array
|
|
4415
|
-
if (result && result.nodeType) {
|
|
4416
|
-
return [result]
|
|
4417
|
-
} else if (result && result.length !== undefined) {
|
|
4418
|
-
return Array.from(result)
|
|
4419
|
-
} else if (Array.isArray(result)) {
|
|
4420
|
-
return result
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
return []
|
|
4424
|
-
},
|
|
4425
|
-
{
|
|
4426
|
-
strategyCode: strategyFunction.toString(),
|
|
4427
|
-
selector: locator.value,
|
|
4428
|
-
},
|
|
4429
|
-
)
|
|
4430
|
-
|
|
4431
|
-
// Convert the found elements back to Playwright locators
|
|
4432
|
-
if (elements.length === 0) {
|
|
4433
|
-
return []
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
// Create CSS selectors for the found elements and return as locators
|
|
4437
|
-
const locators = []
|
|
4438
|
-
const timestamp = Date.now()
|
|
4439
|
-
|
|
4440
|
-
for (let i = 0; i < elements.length; i++) {
|
|
4441
|
-
// Use a unique attribute approach to target specific elements
|
|
4442
|
-
const uniqueAttr = `data-codecept-custom-${timestamp}-${i}`
|
|
4443
|
-
|
|
4444
|
-
await page.evaluate(
|
|
4445
|
-
({ index, uniqueAttr, strategyCode, selector }) => {
|
|
4446
|
-
// Re-execute the strategy to find elements and mark the specific one
|
|
4447
|
-
const strategy = new Function('return ' + strategyCode)()
|
|
4448
|
-
const result = strategy(selector, document)
|
|
4449
|
-
|
|
4450
|
-
let elementsArray = []
|
|
4451
|
-
if (result && result.nodeType) {
|
|
4452
|
-
elementsArray = [result]
|
|
4453
|
-
} else if (result && result.length !== undefined) {
|
|
4454
|
-
elementsArray = Array.from(result)
|
|
4455
|
-
} else if (Array.isArray(result)) {
|
|
4456
|
-
elementsArray = result
|
|
4457
|
-
}
|
|
4458
|
-
|
|
4459
|
-
if (elementsArray[index]) {
|
|
4460
|
-
elementsArray[index].setAttribute(uniqueAttr, 'true')
|
|
4461
|
-
}
|
|
4462
|
-
},
|
|
4463
|
-
{
|
|
4464
|
-
index: i,
|
|
4465
|
-
uniqueAttr,
|
|
4466
|
-
strategyCode: strategyFunction.toString(),
|
|
4467
|
-
selector: locator.value,
|
|
4468
|
-
},
|
|
4469
|
-
)
|
|
4470
|
-
|
|
4471
|
-
locators.push(page.locator(`[${uniqueAttr}="true"]`))
|
|
4472
|
-
}
|
|
4473
|
-
|
|
4474
|
-
return locators
|
|
4475
|
-
}
|
|
4476
|
-
|
|
4477
4241
|
async function findElement(matcher, locator) {
|
|
4478
4242
|
if (locator.react) return findReact(matcher, locator)
|
|
4479
4243
|
if (locator.vue) return findVue(matcher, locator)
|
|
@@ -4511,16 +4275,22 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4511
4275
|
assertElementExists(els, locator, 'Clickable element')
|
|
4512
4276
|
}
|
|
4513
4277
|
|
|
4514
|
-
|
|
4515
|
-
|
|
4278
|
+
const opts = store.currentStep?.opts
|
|
4279
|
+
let element
|
|
4280
|
+
if (opts?.elementIndex != null) {
|
|
4281
|
+
element = selectElement(els, locator, this)
|
|
4282
|
+
} else {
|
|
4283
|
+
const strict = (opts?.exact === false || opts?.strictMode === false) ? false : (this.options.strict || opts?.exact === true || opts?.strictMode === true)
|
|
4284
|
+
if (strict) assertOnlyOneElement(els, locator, this)
|
|
4285
|
+
element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
await highlightActiveElement.call(this, element)
|
|
4289
|
+
if (store.debugMode) this.debugSection('Clicked', await elToString(element, 1))
|
|
4516
4290
|
|
|
4517
|
-
/*
|
|
4518
|
-
using the force true options itself but instead dispatching a click
|
|
4519
|
-
*/
|
|
4520
4291
|
if (options.force) {
|
|
4521
|
-
await
|
|
4292
|
+
await element.dispatchEvent('click')
|
|
4522
4293
|
} else {
|
|
4523
|
-
const element = els.length > 1 ? (await getVisibleElements(els))[0] : els[0]
|
|
4524
4294
|
await element.click(options)
|
|
4525
4295
|
}
|
|
4526
4296
|
const promises = []
|
|
@@ -4535,7 +4305,10 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4535
4305
|
async function findClickable(matcher, locator) {
|
|
4536
4306
|
const matchedLocator = new Locator(locator)
|
|
4537
4307
|
|
|
4538
|
-
if (!matchedLocator.isFuzzy())
|
|
4308
|
+
if (!matchedLocator.isFuzzy()) {
|
|
4309
|
+
const els = await findElements.call(this, matcher, matchedLocator)
|
|
4310
|
+
return els
|
|
4311
|
+
}
|
|
4539
4312
|
|
|
4540
4313
|
let els
|
|
4541
4314
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
@@ -4635,38 +4408,92 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4635
4408
|
return truth(`checkable ${option}`, 'to be checked')[assertType](selected)
|
|
4636
4409
|
}
|
|
4637
4410
|
|
|
4638
|
-
async function findFields(locator) {
|
|
4639
|
-
|
|
4640
|
-
if (
|
|
4641
|
-
const
|
|
4642
|
-
|
|
4643
|
-
|
|
4411
|
+
async function findFields(locator, context = null) {
|
|
4412
|
+
let contextEl
|
|
4413
|
+
if (context) {
|
|
4414
|
+
const contextEls = await this._locate(context)
|
|
4415
|
+
assertElementExists(contextEls, context, 'Context element')
|
|
4416
|
+
contextEl = contextEls[0]
|
|
4644
4417
|
}
|
|
4645
4418
|
|
|
4419
|
+
const locateFn = contextEl
|
|
4420
|
+
? loc => findElements.call(this, contextEl, loc)
|
|
4421
|
+
: loc => this._locate(loc)
|
|
4422
|
+
|
|
4423
|
+
const matcher = contextEl || (await this.page)
|
|
4424
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4425
|
+
if (roleElements) return roleElements
|
|
4426
|
+
|
|
4646
4427
|
const matchedLocator = new Locator(locator)
|
|
4647
4428
|
if (!matchedLocator.isFuzzy()) {
|
|
4648
|
-
return
|
|
4429
|
+
return locateFn(matchedLocator)
|
|
4649
4430
|
}
|
|
4650
4431
|
const literal = xpathLocator.literal(locator)
|
|
4651
4432
|
|
|
4652
|
-
let els = await
|
|
4433
|
+
let els = await locateFn({ xpath: Locator.field.labelEquals(literal) })
|
|
4653
4434
|
if (els.length) {
|
|
4654
4435
|
return els
|
|
4655
4436
|
}
|
|
4656
4437
|
|
|
4657
|
-
els = await
|
|
4438
|
+
els = await locateFn({ xpath: Locator.field.labelContains(literal) })
|
|
4658
4439
|
if (els.length) {
|
|
4659
4440
|
return els
|
|
4660
4441
|
}
|
|
4661
|
-
els = await
|
|
4442
|
+
els = await locateFn({ xpath: Locator.field.byName(literal) })
|
|
4662
4443
|
if (els.length) {
|
|
4663
4444
|
return els
|
|
4664
4445
|
}
|
|
4665
|
-
return
|
|
4446
|
+
return locateFn({ css: locator })
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
async function proceedSelect(context, el, option) {
|
|
4450
|
+
const role = await el.getAttribute('role')
|
|
4451
|
+
const options = Array.isArray(option) ? option : [option]
|
|
4452
|
+
|
|
4453
|
+
if (role === 'combobox') {
|
|
4454
|
+
this.debugSection('SelectOption', 'Expanding combobox')
|
|
4455
|
+
await highlightActiveElement.call(this, el)
|
|
4456
|
+
const [ariaOwns, ariaControls] = await Promise.all([el.getAttribute('aria-owns'), el.getAttribute('aria-controls')])
|
|
4457
|
+
await el.click()
|
|
4458
|
+
await this._waitForAction()
|
|
4459
|
+
|
|
4460
|
+
const listboxId = ariaOwns || ariaControls
|
|
4461
|
+
let listbox = listboxId ? context.locator(`#${listboxId}`).first() : null
|
|
4462
|
+
if (!listbox || !(await listbox.count())) listbox = context.getByRole('listbox').first()
|
|
4463
|
+
|
|
4464
|
+
for (const opt of options) {
|
|
4465
|
+
const optEl = listbox.getByRole('option', { name: opt }).first()
|
|
4466
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4467
|
+
await highlightActiveElement.call(this, optEl)
|
|
4468
|
+
await optEl.click()
|
|
4469
|
+
}
|
|
4470
|
+
return this._waitForAction()
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
if (role === 'listbox') {
|
|
4474
|
+
for (const opt of options) {
|
|
4475
|
+
const optEl = el.getByRole('option', { name: opt }).first()
|
|
4476
|
+
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4477
|
+
await highlightActiveElement.call(this, optEl)
|
|
4478
|
+
await optEl.click()
|
|
4479
|
+
}
|
|
4480
|
+
return this._waitForAction()
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
await highlightActiveElement.call(this, el)
|
|
4484
|
+
let optionToSelect = option
|
|
4485
|
+
try {
|
|
4486
|
+
optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim()
|
|
4487
|
+
} catch (e) {
|
|
4488
|
+
optionToSelect = option
|
|
4489
|
+
}
|
|
4490
|
+
if (!Array.isArray(option)) option = [optionToSelect]
|
|
4491
|
+
await el.selectOption(option)
|
|
4492
|
+
return this._waitForAction()
|
|
4666
4493
|
}
|
|
4667
4494
|
|
|
4668
|
-
async function proceedSeeInField(assertType, field, value) {
|
|
4669
|
-
const els = await findFields.call(this, field)
|
|
4495
|
+
async function proceedSeeInField(assertType, field, value, context) {
|
|
4496
|
+
const els = await findFields.call(this, field, context)
|
|
4670
4497
|
assertElementExists(els, field, 'Field')
|
|
4671
4498
|
const el = els[0]
|
|
4672
4499
|
const tag = await el.evaluate(e => e.tagName)
|
|
@@ -4780,6 +4607,13 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4780
4607
|
}
|
|
4781
4608
|
}
|
|
4782
4609
|
|
|
4610
|
+
function assertOnlyOneElement(elements, locator, helper) {
|
|
4611
|
+
if (elements.length > 1) {
|
|
4612
|
+
const webElements = elements.map(el => new WebElement(el, helper))
|
|
4613
|
+
throw new MultipleElementsFound(locator, webElements)
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4783
4617
|
function $XPath(element, selector) {
|
|
4784
4618
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
4785
4619
|
const res = []
|
|
@@ -4967,7 +4801,7 @@ async function refreshContextSession() {
|
|
|
4967
4801
|
this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
|
|
4968
4802
|
return
|
|
4969
4803
|
}
|
|
4970
|
-
|
|
4804
|
+
|
|
4971
4805
|
const currentUrl = await this.grabCurrentUrl()
|
|
4972
4806
|
|
|
4973
4807
|
if (currentUrl.startsWith('http')) {
|