codeceptjs 4.0.1-beta.25 → 4.0.1-beta.26
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/bin/codecept.js +1 -2
- package/lib/command/{shell.js → interactive.js} +3 -31
- package/lib/config.js +3 -2
- package/lib/container.js +17 -3
- package/lib/helper/Playwright.js +186 -263
- package/lib/helper/Puppeteer.js +33 -115
- package/lib/helper/WebDriver.js +22 -114
- package/lib/helper/extras/PlaywrightLocator.js +34 -13
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/locator.js +31 -117
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/utils/typescript.js +61 -34
- package/package.json +8 -8
- package/typings/index.d.ts +1 -1
- package/typings/promiseBasedTypes.d.ts +5475 -3929
- package/typings/types.d.ts +5767 -4092
- package/lib/helper/errors/MultipleElementsFound.js +0 -135
package/lib/helper/Playwright.js
CHANGED
|
@@ -27,11 +27,10 @@ import {
|
|
|
27
27
|
} from '../utils.js'
|
|
28
28
|
import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
|
|
29
29
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
30
|
-
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
31
30
|
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
|
|
32
31
|
import Popup from './extras/Popup.js'
|
|
33
32
|
import Console from './extras/Console.js'
|
|
34
|
-
import { findReact, findVue } from './extras/
|
|
33
|
+
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
35
34
|
import WebElement from '../element/WebElement.js'
|
|
36
35
|
|
|
37
36
|
let playwright
|
|
@@ -45,6 +44,36 @@ if (typeof global.__playwrightSelectorsRegistered === 'undefined') {
|
|
|
45
44
|
global.__playwrightSelectorsRegistered = false
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Creates a Playwright selector engine factory for a custom locator strategy.
|
|
49
|
+
* @param {string} name - Strategy name for error messages
|
|
50
|
+
* @param {Function} func - The locator function (selector, root) => Element|Element[]
|
|
51
|
+
* @returns {Function} Selector engine factory
|
|
52
|
+
*/
|
|
53
|
+
function createCustomSelectorEngine(name, func) {
|
|
54
|
+
return () => ({
|
|
55
|
+
create: () => null,
|
|
56
|
+
query(root, selector) {
|
|
57
|
+
if (!root) return null
|
|
58
|
+
try {
|
|
59
|
+
const result = func(selector, root)
|
|
60
|
+
return Array.isArray(result) ? result[0] : result
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
queryAll(root, selector) {
|
|
66
|
+
if (!root) return []
|
|
67
|
+
try {
|
|
68
|
+
const result = func(selector, root)
|
|
69
|
+
return Array.isArray(result) ? result : result ? [result] : []
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
const popupStore = new Popup()
|
|
49
78
|
const consoleLogStore = new Console()
|
|
50
79
|
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
|
|
@@ -108,9 +137,6 @@ const pathSeparator = path.sep
|
|
|
108
137
|
* those cookies are used instead and the configured `storageState` is ignored (no merge).
|
|
109
138
|
* May include session cookies, auth tokens, localStorage and (if captured with
|
|
110
139
|
* `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
|
|
111
|
-
* @prop {boolean} [strict=false] - throw error when multiple elements match a single-element locator.
|
|
112
|
-
* When enabled, methods like `click`, `fillField`, `selectOption`, etc. will throw a
|
|
113
|
-
* `MultipleElementsFound` error if more than one element matches the locator.
|
|
114
140
|
*/
|
|
115
141
|
const config = {}
|
|
116
142
|
|
|
@@ -362,23 +388,13 @@ class Playwright extends Helper {
|
|
|
362
388
|
|
|
363
389
|
// Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
|
|
364
390
|
// This can happen in worker threads where config is serialized/deserialized
|
|
365
|
-
|
|
366
|
-
if (typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null) {
|
|
367
|
-
// Check if it's an empty array or object with no function properties
|
|
368
|
-
const entries = Object.entries(config.customLocatorStrategies)
|
|
369
|
-
const hasFunctions = entries.some(([_, value]) => typeof value === 'function')
|
|
370
|
-
if (hasFunctions) {
|
|
371
|
-
validCustomLocators = config.customLocatorStrategies
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.customLocatorStrategies = validCustomLocators
|
|
391
|
+
this.customLocatorStrategies = this._parseCustomLocatorStrategies(config.customLocatorStrategies)
|
|
376
392
|
this._customLocatorsRegistered = false
|
|
377
393
|
|
|
378
394
|
// Add custom locator strategies to global registry for early registration
|
|
379
395
|
if (this.customLocatorStrategies) {
|
|
380
|
-
for (const [
|
|
381
|
-
globalCustomLocatorStrategies.set(
|
|
396
|
+
for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
|
|
397
|
+
globalCustomLocatorStrategies.set(name, func)
|
|
382
398
|
}
|
|
383
399
|
}
|
|
384
400
|
|
|
@@ -421,7 +437,6 @@ class Playwright extends Helper {
|
|
|
421
437
|
highlightElement: false,
|
|
422
438
|
storageState: undefined,
|
|
423
439
|
onResponse: null,
|
|
424
|
-
strict: false, // Throw error when multiple elements match single-element locator
|
|
425
440
|
}
|
|
426
441
|
|
|
427
442
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -570,54 +585,23 @@ class Playwright extends Helper {
|
|
|
570
585
|
}
|
|
571
586
|
|
|
572
587
|
// Register all custom locator strategies from the global registry
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const createCustomEngine = ((name, func) => {
|
|
579
|
-
return () => {
|
|
580
|
-
return {
|
|
581
|
-
create() {
|
|
582
|
-
return null
|
|
583
|
-
},
|
|
584
|
-
query(root, selector) {
|
|
585
|
-
try {
|
|
586
|
-
if (!root) return null
|
|
587
|
-
const result = func(selector, root)
|
|
588
|
-
return Array.isArray(result) ? result[0] : result
|
|
589
|
-
} catch (error) {
|
|
590
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
591
|
-
return null
|
|
592
|
-
}
|
|
593
|
-
},
|
|
594
|
-
queryAll(root, selector) {
|
|
595
|
-
try {
|
|
596
|
-
if (!root) return []
|
|
597
|
-
const result = func(selector, root)
|
|
598
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
599
|
-
} catch (error) {
|
|
600
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
601
|
-
return []
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
})(strategyName, strategyFunction)
|
|
588
|
+
await this._registerGlobalCustomLocators()
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.warn(e)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
607
593
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
594
|
+
async _registerGlobalCustomLocators() {
|
|
595
|
+
for (const [name, func] of globalCustomLocatorStrategies.entries()) {
|
|
596
|
+
if (registeredCustomLocatorStrategies.has(name)) continue
|
|
597
|
+
try {
|
|
598
|
+
await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
|
|
599
|
+
registeredCustomLocatorStrategies.add(name)
|
|
600
|
+
} catch (e) {
|
|
601
|
+
if (!e.message.includes('already registered')) {
|
|
602
|
+
this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
|
|
617
603
|
}
|
|
618
604
|
}
|
|
619
|
-
} catch (e) {
|
|
620
|
-
console.warn(e)
|
|
621
605
|
}
|
|
622
606
|
}
|
|
623
607
|
|
|
@@ -1282,28 +1266,31 @@ class Playwright extends Helper {
|
|
|
1282
1266
|
return this.browser
|
|
1283
1267
|
}
|
|
1284
1268
|
|
|
1269
|
+
_hasCustomLocatorStrategies() {
|
|
1270
|
+
return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
_parseCustomLocatorStrategies(strategies) {
|
|
1274
|
+
if (typeof strategies !== 'object' || strategies === null) return null
|
|
1275
|
+
const hasValidFunctions = Object.values(strategies).some(v => typeof v === 'function')
|
|
1276
|
+
return hasValidFunctions ? strategies : null
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1285
1279
|
_lookupCustomLocator(customStrategy) {
|
|
1286
|
-
if (
|
|
1287
|
-
return null
|
|
1288
|
-
}
|
|
1280
|
+
if (!this._hasCustomLocatorStrategies()) return null
|
|
1289
1281
|
const strategy = this.customLocatorStrategies[customStrategy]
|
|
1290
1282
|
return typeof strategy === 'function' ? strategy : null
|
|
1291
1283
|
}
|
|
1292
1284
|
|
|
1293
1285
|
_isCustomLocator(locator) {
|
|
1294
1286
|
const locatorObj = new Locator(locator)
|
|
1295
|
-
if (locatorObj.isCustom())
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
return true
|
|
1299
|
-
}
|
|
1300
|
-
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1301
|
-
}
|
|
1302
|
-
return false
|
|
1287
|
+
if (!locatorObj.isCustom()) return false
|
|
1288
|
+
if (this._lookupCustomLocator(locatorObj.type)) return true
|
|
1289
|
+
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
1303
1290
|
}
|
|
1304
1291
|
|
|
1305
1292
|
_isCustomLocatorStrategyDefined() {
|
|
1306
|
-
return
|
|
1293
|
+
return this._hasCustomLocatorStrategies()
|
|
1307
1294
|
}
|
|
1308
1295
|
|
|
1309
1296
|
/**
|
|
@@ -1326,49 +1313,16 @@ class Playwright extends Helper {
|
|
|
1326
1313
|
}
|
|
1327
1314
|
|
|
1328
1315
|
async _registerCustomLocatorStrategies() {
|
|
1329
|
-
if (!this.
|
|
1330
|
-
|
|
1331
|
-
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
1332
|
-
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
1333
|
-
try {
|
|
1334
|
-
const createCustomEngine = ((name, func) => {
|
|
1335
|
-
return () => {
|
|
1336
|
-
return {
|
|
1337
|
-
create(root, target) {
|
|
1338
|
-
return null
|
|
1339
|
-
},
|
|
1340
|
-
query(root, selector) {
|
|
1341
|
-
try {
|
|
1342
|
-
if (!root) return null
|
|
1343
|
-
const result = func(selector, root)
|
|
1344
|
-
return Array.isArray(result) ? result[0] : result
|
|
1345
|
-
} catch (error) {
|
|
1346
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1347
|
-
return null
|
|
1348
|
-
}
|
|
1349
|
-
},
|
|
1350
|
-
queryAll(root, selector) {
|
|
1351
|
-
try {
|
|
1352
|
-
if (!root) return []
|
|
1353
|
-
const result = func(selector, root)
|
|
1354
|
-
return Array.isArray(result) ? result : result ? [result] : []
|
|
1355
|
-
} catch (error) {
|
|
1356
|
-
console.warn(`Error in custom locator "${name}":`, error)
|
|
1357
|
-
return []
|
|
1358
|
-
}
|
|
1359
|
-
},
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
})(strategyName, strategyFunction)
|
|
1316
|
+
if (!this._hasCustomLocatorStrategies()) return
|
|
1363
1317
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
}
|
|
1318
|
+
for (const [name, func] of Object.entries(this.customLocatorStrategies)) {
|
|
1319
|
+
if (registeredCustomLocatorStrategies.has(name)) continue
|
|
1320
|
+
try {
|
|
1321
|
+
await playwright.selectors.register(name, createCustomSelectorEngine(name, func))
|
|
1322
|
+
registeredCustomLocatorStrategies.add(name)
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
if (!e.message.includes('already registered')) {
|
|
1325
|
+
this.debugSection('Custom Locator', `Failed to register '${name}': ${e.message}`)
|
|
1372
1326
|
}
|
|
1373
1327
|
}
|
|
1374
1328
|
}
|
|
@@ -1917,12 +1871,7 @@ class Playwright extends Helper {
|
|
|
1917
1871
|
*/
|
|
1918
1872
|
async _locateElement(locator) {
|
|
1919
1873
|
const context = await this._getContext()
|
|
1920
|
-
|
|
1921
|
-
if (elements.length === 0) {
|
|
1922
|
-
throw new ElementNotFound(locator, 'Element', 'was not found')
|
|
1923
|
-
}
|
|
1924
|
-
if (this.options.strict) assertOnlyOneElement(elements, locator)
|
|
1925
|
-
return elements[0]
|
|
1874
|
+
return findElement(context, locator)
|
|
1926
1875
|
}
|
|
1927
1876
|
|
|
1928
1877
|
/**
|
|
@@ -1937,7 +1886,6 @@ class Playwright extends Helper {
|
|
|
1937
1886
|
const context = providedContext || (await this._getContext())
|
|
1938
1887
|
const els = await findCheckable.call(this, locator, context)
|
|
1939
1888
|
assertElementExists(els[0], locator, 'Checkbox or radio')
|
|
1940
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
1941
1889
|
return els[0]
|
|
1942
1890
|
}
|
|
1943
1891
|
|
|
@@ -2410,7 +2358,6 @@ class Playwright extends Helper {
|
|
|
2410
2358
|
async fillField(field, value) {
|
|
2411
2359
|
const els = await findFields.call(this, field)
|
|
2412
2360
|
assertElementExists(els, field, 'Field')
|
|
2413
|
-
if (this.options.strict) assertOnlyOneElement(els, field)
|
|
2414
2361
|
const el = els[0]
|
|
2415
2362
|
|
|
2416
2363
|
await el.clear()
|
|
@@ -2443,7 +2390,6 @@ class Playwright extends Helper {
|
|
|
2443
2390
|
async clearField(locator, options = {}) {
|
|
2444
2391
|
const els = await findFields.call(this, locator)
|
|
2445
2392
|
assertElementExists(els, locator, 'Field to clear')
|
|
2446
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
2447
2393
|
|
|
2448
2394
|
const el = els[0]
|
|
2449
2395
|
|
|
@@ -2460,7 +2406,6 @@ class Playwright extends Helper {
|
|
|
2460
2406
|
async appendField(field, value) {
|
|
2461
2407
|
const els = await findFields.call(this, field)
|
|
2462
2408
|
assertElementExists(els, field, 'Field')
|
|
2463
|
-
if (this.options.strict) assertOnlyOneElement(els, field)
|
|
2464
2409
|
await highlightActiveElement.call(this, els[0])
|
|
2465
2410
|
await els[0].press('End')
|
|
2466
2411
|
await els[0].type(value.toString(), { delay: this.options.pressKeyDelay })
|
|
@@ -2503,28 +2448,23 @@ class Playwright extends Helper {
|
|
|
2503
2448
|
* {{> selectOption }}
|
|
2504
2449
|
*/
|
|
2505
2450
|
async selectOption(select, option) {
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
if (!matchedLocator.isFuzzy()) {
|
|
2510
|
-
this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`)
|
|
2511
|
-
const els = await this._locate(matchedLocator)
|
|
2512
|
-
assertElementExists(els, select, 'Selectable element')
|
|
2513
|
-
return proceedSelect.call(this, context, els[0], option)
|
|
2514
|
-
}
|
|
2451
|
+
const els = await findFields.call(this, select)
|
|
2452
|
+
assertElementExists(els, select, 'Selectable field')
|
|
2453
|
+
const el = els[0]
|
|
2515
2454
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2455
|
+
await highlightActiveElement.call(this, el)
|
|
2456
|
+
let optionToSelect = ''
|
|
2518
2457
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2458
|
+
try {
|
|
2459
|
+
optionToSelect = (await el.locator('option', { hasText: option }).textContent()).trim()
|
|
2460
|
+
} catch (e) {
|
|
2461
|
+
optionToSelect = option
|
|
2462
|
+
}
|
|
2521
2463
|
|
|
2522
|
-
|
|
2523
|
-
if (els.length) return proceedSelect.call(this, context, els[0], option)
|
|
2464
|
+
if (!Array.isArray(option)) option = [optionToSelect]
|
|
2524
2465
|
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
return proceedSelect.call(this, context, els[0], option)
|
|
2466
|
+
await el.selectOption(option)
|
|
2467
|
+
return this._waitForAction()
|
|
2528
2468
|
}
|
|
2529
2469
|
|
|
2530
2470
|
/**
|
|
@@ -2571,10 +2511,6 @@ class Playwright extends Helper {
|
|
|
2571
2511
|
*
|
|
2572
2512
|
*/
|
|
2573
2513
|
async see(text, context = null) {
|
|
2574
|
-
// If only one argument passed and it's an object without custom toString(), treat as locator
|
|
2575
|
-
if (!context && text && typeof text === 'object' && !Array.isArray(text) && text.toString === Object.prototype.toString) {
|
|
2576
|
-
return this.seeElement(text)
|
|
2577
|
-
}
|
|
2578
2514
|
return proceedSee.call(this, 'assert', text, context)
|
|
2579
2515
|
}
|
|
2580
2516
|
|
|
@@ -2813,6 +2749,17 @@ class Playwright extends Helper {
|
|
|
2813
2749
|
*
|
|
2814
2750
|
*/
|
|
2815
2751
|
async grabTextFrom(locator) {
|
|
2752
|
+
// Handle role locators with text/exact options
|
|
2753
|
+
if (isRoleLocatorObject(locator)) {
|
|
2754
|
+
const elements = await handleRoleLocator(this.page, locator)
|
|
2755
|
+
if (elements && elements.length > 0) {
|
|
2756
|
+
const text = await elements[0].textContent()
|
|
2757
|
+
assertElementExists(text, JSON.stringify(locator))
|
|
2758
|
+
this.debugSection('Text', text)
|
|
2759
|
+
return text
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2816
2763
|
const locatorObj = new Locator(locator, 'css')
|
|
2817
2764
|
|
|
2818
2765
|
if (locatorObj.isCustom()) {
|
|
@@ -2825,32 +2772,21 @@ class Playwright extends Helper {
|
|
|
2825
2772
|
assertElementExists(text, locatorObj.toString())
|
|
2826
2773
|
this.debugSection('Text', text)
|
|
2827
2774
|
return text
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
if (roleElements && roleElements.length > 0) {
|
|
2834
|
-
const text = await roleElements[0].textContent()
|
|
2835
|
-
assertElementExists(text, JSON.stringify(locator))
|
|
2775
|
+
} else {
|
|
2776
|
+
locator = this._contextLocator(locator)
|
|
2777
|
+
try {
|
|
2778
|
+
const text = await this.page.textContent(locator)
|
|
2779
|
+
assertElementExists(text, locator)
|
|
2836
2780
|
this.debugSection('Text', text)
|
|
2837
2781
|
return text
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2784
|
+
if (error.message && error.message.includes('Timeout')) {
|
|
2785
|
+
throw new ElementNotFound(locator, 'text')
|
|
2786
|
+
}
|
|
2787
|
+
throw error
|
|
2838
2788
|
}
|
|
2839
2789
|
}
|
|
2840
|
-
|
|
2841
|
-
locator = this._contextLocator(locator)
|
|
2842
|
-
try {
|
|
2843
|
-
const text = await this.page.textContent(locator)
|
|
2844
|
-
assertElementExists(text, locator)
|
|
2845
|
-
this.debugSection('Text', text)
|
|
2846
|
-
return text
|
|
2847
|
-
} catch (error) {
|
|
2848
|
-
// Convert Playwright timeout errors to ElementNotFound for consistency
|
|
2849
|
-
if (error.message && error.message.includes('Timeout')) {
|
|
2850
|
-
throw new ElementNotFound(locator, 'text')
|
|
2851
|
-
}
|
|
2852
|
-
throw error
|
|
2853
|
-
}
|
|
2854
2790
|
}
|
|
2855
2791
|
|
|
2856
2792
|
/**
|
|
@@ -4329,26 +4265,50 @@ function buildLocatorString(locator) {
|
|
|
4329
4265
|
if (locator.isXPath()) {
|
|
4330
4266
|
return `xpath=${locator.value}`
|
|
4331
4267
|
}
|
|
4332
|
-
|
|
4268
|
+
return locator.simplify()
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
/**
|
|
4272
|
+
* Checks if a locator is a role locator object (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4273
|
+
*/
|
|
4274
|
+
function isRoleLocatorObject(locator) {
|
|
4275
|
+
return locator && typeof locator === 'object' && locator.role && !locator.type
|
|
4333
4276
|
}
|
|
4334
4277
|
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4278
|
+
/**
|
|
4279
|
+
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4280
|
+
* Returns elements array if role locator, null otherwise
|
|
4281
|
+
*/
|
|
4282
|
+
async function handleRoleLocator(context, locator) {
|
|
4283
|
+
if (!isRoleLocatorObject(locator)) return null
|
|
4284
|
+
|
|
4285
|
+
const options = {}
|
|
4286
|
+
if (locator.text) options.name = locator.text
|
|
4287
|
+
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4288
|
+
|
|
4289
|
+
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4340
4290
|
}
|
|
4341
4291
|
|
|
4342
4292
|
async function findElements(matcher, locator) {
|
|
4343
|
-
|
|
4344
|
-
const
|
|
4293
|
+
// Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
|
|
4294
|
+
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4295
|
+
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4296
|
+
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4297
|
+
|
|
4298
|
+
if (isReactLocator) return findReact(matcher, locator)
|
|
4299
|
+
if (isVueLocator) return findVue(matcher, locator)
|
|
4300
|
+
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4301
|
+
|
|
4302
|
+
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4303
|
+
const roleElements = await handleRoleLocator(matcher, locator)
|
|
4345
4304
|
if (roleElements) return roleElements
|
|
4346
4305
|
|
|
4347
|
-
|
|
4348
|
-
const isVueLocator = matchedLocator.type === 'vue'
|
|
4306
|
+
locator = new Locator(locator, 'css')
|
|
4349
4307
|
|
|
4350
|
-
|
|
4351
|
-
if (
|
|
4308
|
+
// Handle custom locators directly instead of relying on Playwright selector engines
|
|
4309
|
+
if (locator.isCustom()) {
|
|
4310
|
+
return findCustomElements.call(this, matcher, locator)
|
|
4311
|
+
}
|
|
4352
4312
|
|
|
4353
4313
|
// Check if we have a custom context locator and need to search within it
|
|
4354
4314
|
if (this.contextLocator) {
|
|
@@ -4361,12 +4321,12 @@ async function findElements(matcher, locator) {
|
|
|
4361
4321
|
}
|
|
4362
4322
|
|
|
4363
4323
|
// Search within the first context element
|
|
4364
|
-
const locatorString = buildLocatorString(
|
|
4324
|
+
const locatorString = buildLocatorString(locator)
|
|
4365
4325
|
return contextElements[0].locator(locatorString).all()
|
|
4366
4326
|
}
|
|
4367
4327
|
}
|
|
4368
4328
|
|
|
4369
|
-
const locatorString = buildLocatorString(
|
|
4329
|
+
const locatorString = buildLocatorString(locator)
|
|
4370
4330
|
|
|
4371
4331
|
return matcher.locator(locatorString).all()
|
|
4372
4332
|
}
|
|
@@ -4458,6 +4418,16 @@ async function findCustomElements(matcher, locator) {
|
|
|
4458
4418
|
return locators
|
|
4459
4419
|
}
|
|
4460
4420
|
|
|
4421
|
+
async function findElement(matcher, locator) {
|
|
4422
|
+
if (locator.react) return findReact(matcher, locator)
|
|
4423
|
+
if (locator.vue) return findVue(matcher, locator)
|
|
4424
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4425
|
+
|
|
4426
|
+
locator = new Locator(locator, 'css')
|
|
4427
|
+
|
|
4428
|
+
return matcher.locator(buildLocatorString(locator)).first()
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4461
4431
|
async function getVisibleElements(elements) {
|
|
4462
4432
|
const visibleElements = []
|
|
4463
4433
|
for (const element of elements) {
|
|
@@ -4507,42 +4477,41 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
4507
4477
|
}
|
|
4508
4478
|
|
|
4509
4479
|
async function findClickable(matcher, locator) {
|
|
4510
|
-
|
|
4511
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
4480
|
+
const matchedLocator = new Locator(locator)
|
|
4512
4481
|
|
|
4513
|
-
|
|
4514
|
-
if (!locator.isFuzzy()) {
|
|
4515
|
-
const els = await findElements.call(this, matcher, locator)
|
|
4516
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4517
|
-
return els
|
|
4518
|
-
}
|
|
4482
|
+
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)
|
|
4519
4483
|
|
|
4520
4484
|
let els
|
|
4521
|
-
const literal = xpathLocator.literal(
|
|
4485
|
+
const literal = xpathLocator.literal(matchedLocator.value)
|
|
4522
4486
|
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
if (
|
|
4526
|
-
|
|
4487
|
+
try {
|
|
4488
|
+
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4489
|
+
if (els.length) return els
|
|
4490
|
+
} catch (err) {
|
|
4491
|
+
// getByRole not supported or failed
|
|
4527
4492
|
}
|
|
4528
4493
|
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
if (
|
|
4532
|
-
|
|
4494
|
+
try {
|
|
4495
|
+
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4496
|
+
if (els.length) return els
|
|
4497
|
+
} catch (err) {
|
|
4498
|
+
// getByRole not supported or failed
|
|
4533
4499
|
}
|
|
4534
4500
|
|
|
4501
|
+
els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
|
|
4502
|
+
if (els.length) return els
|
|
4503
|
+
|
|
4504
|
+
els = await findElements.call(this, matcher, Locator.clickable.wide(literal))
|
|
4505
|
+
if (els.length) return els
|
|
4506
|
+
|
|
4535
4507
|
try {
|
|
4536
4508
|
els = await findElements.call(this, matcher, Locator.clickable.self(literal))
|
|
4537
|
-
if (els.length)
|
|
4538
|
-
if (this.options.strict) assertOnlyOneElement(els, locator)
|
|
4539
|
-
return els
|
|
4540
|
-
}
|
|
4509
|
+
if (els.length) return els
|
|
4541
4510
|
} catch (err) {
|
|
4542
4511
|
// Do nothing
|
|
4543
4512
|
}
|
|
4544
4513
|
|
|
4545
|
-
return findElements.call(this, matcher,
|
|
4514
|
+
return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
|
|
4546
4515
|
}
|
|
4547
4516
|
|
|
4548
4517
|
async function proceedSee(assertType, text, context, strict = false) {
|
|
@@ -4582,17 +4551,14 @@ async function findCheckable(locator, context) {
|
|
|
4582
4551
|
}
|
|
4583
4552
|
|
|
4584
4553
|
// Handle role locators with text/exact options
|
|
4585
|
-
const roleElements = await
|
|
4554
|
+
const roleElements = await handleRoleLocator(contextEl, locator)
|
|
4586
4555
|
if (roleElements) return roleElements
|
|
4587
4556
|
|
|
4588
|
-
const matchedLocator = Locator
|
|
4557
|
+
const matchedLocator = new Locator(locator)
|
|
4589
4558
|
if (!matchedLocator.isFuzzy()) {
|
|
4590
4559
|
return findElements.call(this, contextEl, matchedLocator)
|
|
4591
4560
|
}
|
|
4592
4561
|
|
|
4593
|
-
const checkboxByRole = await findByRole(contextEl, { role: 'checkbox', name: matchedLocator.value })
|
|
4594
|
-
if (checkboxByRole) return checkboxByRole
|
|
4595
|
-
|
|
4596
4562
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
4597
4563
|
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
4598
4564
|
if (els.length) {
|
|
@@ -4614,15 +4580,17 @@ async function proceedIsChecked(assertType, option) {
|
|
|
4614
4580
|
}
|
|
4615
4581
|
|
|
4616
4582
|
async function findFields(locator) {
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4583
|
+
// Handle role locators with text/exact options
|
|
4584
|
+
if (isRoleLocatorObject(locator)) {
|
|
4585
|
+
const page = await this.page
|
|
4586
|
+
const roleElements = await handleRoleLocator(page, locator)
|
|
4587
|
+
if (roleElements) return roleElements
|
|
4588
|
+
}
|
|
4620
4589
|
|
|
4621
4590
|
const matchedLocator = new Locator(locator)
|
|
4622
4591
|
if (!matchedLocator.isFuzzy()) {
|
|
4623
4592
|
return this._locate(matchedLocator)
|
|
4624
4593
|
}
|
|
4625
|
-
|
|
4626
4594
|
const literal = xpathLocator.literal(locator)
|
|
4627
4595
|
|
|
4628
4596
|
let els = await this._locate({ xpath: Locator.field.labelEquals(literal) })
|
|
@@ -4641,45 +4609,6 @@ async function findFields(locator) {
|
|
|
4641
4609
|
return this._locate({ css: locator })
|
|
4642
4610
|
}
|
|
4643
4611
|
|
|
4644
|
-
async function proceedSelect(context, el, option) {
|
|
4645
|
-
const role = await el.getAttribute('role')
|
|
4646
|
-
const options = Array.isArray(option) ? option : [option]
|
|
4647
|
-
|
|
4648
|
-
if (role === 'combobox' || role === 'button') {
|
|
4649
|
-
this.debugSection('SelectOption', 'Expanding combobox')
|
|
4650
|
-
await highlightActiveElement.call(this, el)
|
|
4651
|
-
const [ariaOwns, ariaControls] = await Promise.all([el.getAttribute('aria-owns'), el.getAttribute('aria-controls')])
|
|
4652
|
-
await el.click()
|
|
4653
|
-
await this._waitForAction()
|
|
4654
|
-
|
|
4655
|
-
const listboxId = ariaOwns || ariaControls
|
|
4656
|
-
let listbox = listboxId ? context.locator(`#${listboxId}`).first() : null
|
|
4657
|
-
if (!listbox || !(await listbox.count())) listbox = context.getByRole('listbox').first()
|
|
4658
|
-
|
|
4659
|
-
for (const opt of options) {
|
|
4660
|
-
const optEl = listbox.getByRole('option', { name: opt }).first()
|
|
4661
|
-
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4662
|
-
await highlightActiveElement.call(this, optEl)
|
|
4663
|
-
await optEl.click()
|
|
4664
|
-
}
|
|
4665
|
-
return this._waitForAction()
|
|
4666
|
-
}
|
|
4667
|
-
|
|
4668
|
-
if (role === 'listbox') {
|
|
4669
|
-
for (const opt of options) {
|
|
4670
|
-
const optEl = el.getByRole('option', { name: opt }).first()
|
|
4671
|
-
this.debugSection('SelectOption', `Clicking: "${opt}"`)
|
|
4672
|
-
await highlightActiveElement.call(this, optEl)
|
|
4673
|
-
await optEl.click()
|
|
4674
|
-
}
|
|
4675
|
-
return this._waitForAction()
|
|
4676
|
-
}
|
|
4677
|
-
|
|
4678
|
-
await highlightActiveElement.call(this, el)
|
|
4679
|
-
await el.selectOption(option)
|
|
4680
|
-
return this._waitForAction()
|
|
4681
|
-
}
|
|
4682
|
-
|
|
4683
4612
|
async function proceedSeeInField(assertType, field, value) {
|
|
4684
4613
|
const els = await findFields.call(this, field)
|
|
4685
4614
|
assertElementExists(els, field, 'Field')
|
|
@@ -4795,12 +4724,6 @@ function assertElementExists(res, locator, prefix, suffix) {
|
|
|
4795
4724
|
}
|
|
4796
4725
|
}
|
|
4797
4726
|
|
|
4798
|
-
function assertOnlyOneElement(elements, locator) {
|
|
4799
|
-
if (elements.length > 1) {
|
|
4800
|
-
throw new MultipleElementsFound(locator, elements)
|
|
4801
|
-
}
|
|
4802
|
-
}
|
|
4803
|
-
|
|
4804
4727
|
function $XPath(element, selector) {
|
|
4805
4728
|
const found = document.evaluate(selector, element || document.body, null, 5, null)
|
|
4806
4729
|
const res = []
|
|
@@ -5037,7 +4960,7 @@ async function saveTraceForContext(context, name) {
|
|
|
5037
4960
|
}
|
|
5038
4961
|
|
|
5039
4962
|
async function highlightActiveElement(element) {
|
|
5040
|
-
if (this.options.highlightElement || store.onPause
|
|
4963
|
+
if ((this.options.highlightElement || store.onPause) && store.debugMode) {
|
|
5041
4964
|
await element.evaluate(el => {
|
|
5042
4965
|
const prevStyle = el.style.boxShadow
|
|
5043
4966
|
el.style.boxShadow = '0px 0px 4px 3px rgba(147, 51, 234, 0.8)' // Bright purple that works on both dark/light modes
|