codeceptjs 3.7.5-beta.1 → 3.7.5-beta.11
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 +23 -0
- package/lib/command/run-failed-tests.js +276 -0
- package/lib/helper/Playwright.js +348 -28
- package/lib/mocha/test.js +1 -0
- package/lib/plugin/failedTestsTracker.js +413 -0
- package/lib/workers.js +23 -3
- package/package.json +1 -1
- package/typings/promiseBasedTypes.d.ts +10 -49
- package/typings/types.d.ts +10 -60
package/lib/helper/Playwright.js
CHANGED
|
@@ -38,6 +38,8 @@ const WebElement = require('../element/WebElement')
|
|
|
38
38
|
let playwright
|
|
39
39
|
let perfTiming
|
|
40
40
|
let defaultSelectorEnginesInitialized = false
|
|
41
|
+
let registeredCustomLocatorStrategies = new Set()
|
|
42
|
+
let globalCustomLocatorStrategies = new Map()
|
|
41
43
|
|
|
42
44
|
const popupStore = new Popup()
|
|
43
45
|
const consoleLogStore = new Console()
|
|
@@ -96,6 +98,7 @@ const pathSeparator = path.sep
|
|
|
96
98
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
97
99
|
* @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).
|
|
98
100
|
* @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).
|
|
101
|
+
* @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}\"]\`) } }`
|
|
99
102
|
*/
|
|
100
103
|
const config = {}
|
|
101
104
|
|
|
@@ -344,9 +347,23 @@ class Playwright extends Helper {
|
|
|
344
347
|
this.recordingWebSocketMessages = false
|
|
345
348
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
346
349
|
this.cdpSession = null
|
|
350
|
+
this.customLocatorStrategies = typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null ? config.customLocatorStrategies : null
|
|
351
|
+
this._customLocatorsRegistered = false
|
|
352
|
+
|
|
353
|
+
// Add custom locator strategies to global registry for early registration
|
|
354
|
+
if (this.customLocatorStrategies) {
|
|
355
|
+
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
356
|
+
globalCustomLocatorStrategies.set(strategyName, strategyFunction)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
347
359
|
|
|
348
360
|
// override defaults with config
|
|
349
361
|
this._setConfig(config)
|
|
362
|
+
|
|
363
|
+
// Call _init() to register selector engines - use setTimeout to avoid blocking constructor
|
|
364
|
+
setTimeout(() => {
|
|
365
|
+
this._init().catch(console.error)
|
|
366
|
+
}, 0)
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
_validateConfig(config) {
|
|
@@ -463,12 +480,61 @@ class Playwright extends Helper {
|
|
|
463
480
|
|
|
464
481
|
async _init() {
|
|
465
482
|
// register an internal selector engine for reading value property of elements in a selector
|
|
466
|
-
if (defaultSelectorEnginesInitialized) return
|
|
467
|
-
defaultSelectorEnginesInitialized = true
|
|
468
483
|
try {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
484
|
+
if (!defaultSelectorEnginesInitialized) {
|
|
485
|
+
await playwright.selectors.register('__value', createValueEngine)
|
|
486
|
+
await playwright.selectors.register('__disabled', createDisabledEngine)
|
|
487
|
+
if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
|
|
488
|
+
defaultSelectorEnginesInitialized = true
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Register all custom locator strategies from the global registry
|
|
492
|
+
for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
|
|
493
|
+
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
494
|
+
try {
|
|
495
|
+
// Create a selector engine factory function exactly like createValueEngine pattern
|
|
496
|
+
// Capture variables in closure to avoid reference issues
|
|
497
|
+
const createCustomEngine = ((name, func) => {
|
|
498
|
+
return () => {
|
|
499
|
+
return {
|
|
500
|
+
create() {
|
|
501
|
+
return null
|
|
502
|
+
},
|
|
503
|
+
query(root, selector) {
|
|
504
|
+
try {
|
|
505
|
+
if (!root) return null
|
|
506
|
+
const result = func(selector, root)
|
|
507
|
+
return Array.isArray(result) ? result[0] : result
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.warn(`Error in custom locator "${name}":`, error)
|
|
510
|
+
return null
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
queryAll(root, selector) {
|
|
514
|
+
try {
|
|
515
|
+
if (!root) return []
|
|
516
|
+
const result = func(selector, root)
|
|
517
|
+
return Array.isArray(result) ? result : result ? [result] : []
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.warn(`Error in custom locator "${name}":`, error)
|
|
520
|
+
return []
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
})(strategyName, strategyFunction)
|
|
526
|
+
|
|
527
|
+
await playwright.selectors.register(strategyName, createCustomEngine)
|
|
528
|
+
registeredCustomLocatorStrategies.add(strategyName)
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (!error.message.includes('already registered')) {
|
|
531
|
+
console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
|
|
532
|
+
} else {
|
|
533
|
+
console.log(`Custom locator strategy '${strategyName}' already registered`)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
472
538
|
} catch (e) {
|
|
473
539
|
console.warn(e)
|
|
474
540
|
}
|
|
@@ -827,6 +893,9 @@ class Playwright extends Helper {
|
|
|
827
893
|
}
|
|
828
894
|
|
|
829
895
|
async _startBrowser() {
|
|
896
|
+
// Ensure custom locator strategies are registered before browser launch
|
|
897
|
+
await this._init()
|
|
898
|
+
|
|
830
899
|
if (this.isElectron) {
|
|
831
900
|
this.browser = await playwright._electron.launch(this.playwrightOptions)
|
|
832
901
|
} else if (this.isRemoteBrowser && this.isCDPConnection) {
|
|
@@ -862,6 +931,30 @@ class Playwright extends Helper {
|
|
|
862
931
|
return this.browser
|
|
863
932
|
}
|
|
864
933
|
|
|
934
|
+
_lookupCustomLocator(customStrategy) {
|
|
935
|
+
if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
|
|
936
|
+
return null
|
|
937
|
+
}
|
|
938
|
+
const strategy = this.customLocatorStrategies[customStrategy]
|
|
939
|
+
return typeof strategy === 'function' ? strategy : null
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
_isCustomLocator(locator) {
|
|
943
|
+
const locatorObj = new Locator(locator)
|
|
944
|
+
if (locatorObj.isCustom()) {
|
|
945
|
+
const customLocator = this._lookupCustomLocator(locatorObj.type)
|
|
946
|
+
if (customLocator) {
|
|
947
|
+
return true
|
|
948
|
+
}
|
|
949
|
+
throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
|
|
950
|
+
}
|
|
951
|
+
return false
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
_isCustomLocatorStrategyDefined() {
|
|
955
|
+
return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
|
|
956
|
+
}
|
|
957
|
+
|
|
865
958
|
/**
|
|
866
959
|
* Create a new browser context with a page. \
|
|
867
960
|
* Usually it should be run from a custom helper after call of `_startBrowser()`
|
|
@@ -869,11 +962,64 @@ class Playwright extends Helper {
|
|
|
869
962
|
*/
|
|
870
963
|
async _createContextPage(contextOptions) {
|
|
871
964
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
965
|
+
|
|
966
|
+
// Register custom locator strategies for this context
|
|
967
|
+
await this._registerCustomLocatorStrategies()
|
|
968
|
+
|
|
872
969
|
const page = await this.browserContext.newPage()
|
|
873
970
|
targetCreatedHandler.call(this, page)
|
|
874
971
|
await this._setPage(page)
|
|
875
972
|
}
|
|
876
973
|
|
|
974
|
+
async _registerCustomLocatorStrategies() {
|
|
975
|
+
if (!this.customLocatorStrategies) return
|
|
976
|
+
|
|
977
|
+
for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
|
|
978
|
+
if (!registeredCustomLocatorStrategies.has(strategyName)) {
|
|
979
|
+
try {
|
|
980
|
+
const createCustomEngine = ((name, func) => {
|
|
981
|
+
return () => {
|
|
982
|
+
return {
|
|
983
|
+
create(root, target) {
|
|
984
|
+
return null
|
|
985
|
+
},
|
|
986
|
+
query(root, selector) {
|
|
987
|
+
try {
|
|
988
|
+
if (!root) return null
|
|
989
|
+
const result = func(selector, root)
|
|
990
|
+
return Array.isArray(result) ? result[0] : result
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.warn(`Error in custom locator "${name}":`, error)
|
|
993
|
+
return null
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
queryAll(root, selector) {
|
|
997
|
+
try {
|
|
998
|
+
if (!root) return []
|
|
999
|
+
const result = func(selector, root)
|
|
1000
|
+
return Array.isArray(result) ? result : result ? [result] : []
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
console.warn(`Error in custom locator "${name}":`, error)
|
|
1003
|
+
return []
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
})(strategyName, strategyFunction)
|
|
1009
|
+
|
|
1010
|
+
await playwright.selectors.register(strategyName, createCustomEngine)
|
|
1011
|
+
registeredCustomLocatorStrategies.add(strategyName)
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (!error.message.includes('already registered')) {
|
|
1014
|
+
console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
|
|
1015
|
+
} else {
|
|
1016
|
+
console.log(`Custom locator strategy '${strategyName}' already registered`)
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
877
1023
|
_getType() {
|
|
878
1024
|
return this.browser._type
|
|
879
1025
|
}
|
|
@@ -885,7 +1031,10 @@ class Playwright extends Helper {
|
|
|
885
1031
|
this.frame = null
|
|
886
1032
|
popupStore.clear()
|
|
887
1033
|
if (this.options.recordHar) await this.browserContext.close()
|
|
1034
|
+
this.browserContext = null
|
|
888
1035
|
await this.browser.close()
|
|
1036
|
+
this.browser = null
|
|
1037
|
+
this.isRunning = false
|
|
889
1038
|
}
|
|
890
1039
|
|
|
891
1040
|
async _evaluateHandeInContext(...args) {
|
|
@@ -1266,9 +1415,9 @@ class Playwright extends Helper {
|
|
|
1266
1415
|
async _locate(locator) {
|
|
1267
1416
|
const context = await this._getContext()
|
|
1268
1417
|
|
|
1269
|
-
if (this.frame) return findElements(this.frame, locator)
|
|
1418
|
+
if (this.frame) return findElements.call(this, this.frame, locator)
|
|
1270
1419
|
|
|
1271
|
-
const els = await findElements(context, locator)
|
|
1420
|
+
const els = await findElements.call(this, context, locator)
|
|
1272
1421
|
|
|
1273
1422
|
if (store.debugMode) {
|
|
1274
1423
|
const previewElements = els.slice(0, 3)
|
|
@@ -2063,11 +2212,25 @@ class Playwright extends Helper {
|
|
|
2063
2212
|
* @param {*} locator
|
|
2064
2213
|
*/
|
|
2065
2214
|
_contextLocator(locator) {
|
|
2066
|
-
|
|
2215
|
+
const locatorObj = new Locator(locator, 'css')
|
|
2216
|
+
|
|
2217
|
+
// Handle custom locators differently
|
|
2218
|
+
if (locatorObj.isCustom()) {
|
|
2219
|
+
return buildCustomLocatorString(locatorObj)
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
locator = buildLocatorString(locatorObj)
|
|
2067
2223
|
|
|
2068
2224
|
if (this.contextLocator) {
|
|
2069
|
-
const
|
|
2070
|
-
|
|
2225
|
+
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
2226
|
+
if (contextLocatorObj.isCustom()) {
|
|
2227
|
+
// For custom context locators, we can't use the >> syntax
|
|
2228
|
+
// Instead, we'll need to handle this differently in the calling methods
|
|
2229
|
+
return locator
|
|
2230
|
+
} else {
|
|
2231
|
+
const contextLocator = buildLocatorString(contextLocatorObj)
|
|
2232
|
+
locator = `${contextLocator} >> ${locator}`
|
|
2233
|
+
}
|
|
2071
2234
|
}
|
|
2072
2235
|
|
|
2073
2236
|
return locator
|
|
@@ -2078,11 +2241,25 @@ class Playwright extends Helper {
|
|
|
2078
2241
|
*
|
|
2079
2242
|
*/
|
|
2080
2243
|
async grabTextFrom(locator) {
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2244
|
+
const locatorObj = new Locator(locator, 'css')
|
|
2245
|
+
|
|
2246
|
+
if (locatorObj.isCustom()) {
|
|
2247
|
+
// For custom locators, find the element first
|
|
2248
|
+
const elements = await findCustomElements.call(this, this.page, locatorObj)
|
|
2249
|
+
if (elements.length === 0) {
|
|
2250
|
+
throw new Error(`Element not found: ${locatorObj.toString()}`)
|
|
2251
|
+
}
|
|
2252
|
+
const text = await elements[0].textContent()
|
|
2253
|
+
assertElementExists(text, locatorObj.toString())
|
|
2254
|
+
this.debugSection('Text', text)
|
|
2255
|
+
return text
|
|
2256
|
+
} else {
|
|
2257
|
+
locator = this._contextLocator(locator)
|
|
2258
|
+
const text = await this.page.textContent(locator)
|
|
2259
|
+
assertElementExists(text, locator)
|
|
2260
|
+
this.debugSection('Text', text)
|
|
2261
|
+
return text
|
|
2262
|
+
}
|
|
2086
2263
|
}
|
|
2087
2264
|
|
|
2088
2265
|
/**
|
|
@@ -2095,7 +2272,6 @@ class Playwright extends Helper {
|
|
|
2095
2272
|
for (const el of els) {
|
|
2096
2273
|
texts.push(await el.innerText())
|
|
2097
2274
|
}
|
|
2098
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2099
2275
|
return texts
|
|
2100
2276
|
}
|
|
2101
2277
|
|
|
@@ -2114,7 +2290,6 @@ class Playwright extends Helper {
|
|
|
2114
2290
|
*/
|
|
2115
2291
|
async grabValueFromAll(locator) {
|
|
2116
2292
|
const els = await findFields.call(this, locator)
|
|
2117
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2118
2293
|
return Promise.all(els.map(el => el.inputValue()))
|
|
2119
2294
|
}
|
|
2120
2295
|
|
|
@@ -2133,7 +2308,6 @@ class Playwright extends Helper {
|
|
|
2133
2308
|
*/
|
|
2134
2309
|
async grabHTMLFromAll(locator) {
|
|
2135
2310
|
const els = await this._locate(locator)
|
|
2136
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2137
2311
|
return Promise.all(els.map(el => el.innerHTML()))
|
|
2138
2312
|
}
|
|
2139
2313
|
|
|
@@ -2154,7 +2328,6 @@ class Playwright extends Helper {
|
|
|
2154
2328
|
*/
|
|
2155
2329
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
2156
2330
|
const els = await this._locate(locator)
|
|
2157
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2158
2331
|
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
|
|
2159
2332
|
|
|
2160
2333
|
return cssValues
|
|
@@ -2265,7 +2438,6 @@ class Playwright extends Helper {
|
|
|
2265
2438
|
*/
|
|
2266
2439
|
async grabAttributeFromAll(locator, attr) {
|
|
2267
2440
|
const els = await this._locate(locator)
|
|
2268
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2269
2441
|
const array = []
|
|
2270
2442
|
|
|
2271
2443
|
for (let index = 0; index < els.length; index++) {
|
|
@@ -2285,7 +2457,6 @@ class Playwright extends Helper {
|
|
|
2285
2457
|
const res = await this._locateElement(locator)
|
|
2286
2458
|
assertElementExists(res, locator)
|
|
2287
2459
|
const elem = res
|
|
2288
|
-
this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
|
|
2289
2460
|
return elem.screenshot({ path: outputFile, type: 'png' })
|
|
2290
2461
|
}
|
|
2291
2462
|
|
|
@@ -2580,7 +2751,16 @@ class Playwright extends Helper {
|
|
|
2580
2751
|
|
|
2581
2752
|
const context = await this._getContext()
|
|
2582
2753
|
try {
|
|
2583
|
-
|
|
2754
|
+
if (locator.isCustom()) {
|
|
2755
|
+
// For custom locators, we need to use our custom element finding logic
|
|
2756
|
+
const elements = await findCustomElements.call(this, context, locator)
|
|
2757
|
+
if (elements.length === 0) {
|
|
2758
|
+
throw new Error(`Custom locator ${locator.type}=${locator.value} not found`)
|
|
2759
|
+
}
|
|
2760
|
+
await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
2761
|
+
} else {
|
|
2762
|
+
await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
|
|
2763
|
+
}
|
|
2584
2764
|
} catch (e) {
|
|
2585
2765
|
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
2586
2766
|
}
|
|
@@ -2594,9 +2774,30 @@ class Playwright extends Helper {
|
|
|
2594
2774
|
async waitForVisible(locator, sec) {
|
|
2595
2775
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2596
2776
|
locator = new Locator(locator, 'css')
|
|
2777
|
+
|
|
2597
2778
|
const context = await this._getContext()
|
|
2598
2779
|
let count = 0
|
|
2599
2780
|
|
|
2781
|
+
// Handle custom locators
|
|
2782
|
+
if (locator.isCustom()) {
|
|
2783
|
+
let waiter
|
|
2784
|
+
do {
|
|
2785
|
+
const elements = await findCustomElements.call(this, context, locator)
|
|
2786
|
+
if (elements.length > 0) {
|
|
2787
|
+
waiter = await elements[0].isVisible()
|
|
2788
|
+
} else {
|
|
2789
|
+
waiter = false
|
|
2790
|
+
}
|
|
2791
|
+
if (!waiter) {
|
|
2792
|
+
await this.wait(1)
|
|
2793
|
+
count += 1000
|
|
2794
|
+
}
|
|
2795
|
+
} while (!waiter && count <= waitTimeout)
|
|
2796
|
+
|
|
2797
|
+
if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`)
|
|
2798
|
+
return
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2600
2801
|
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2601
2802
|
let waiter
|
|
2602
2803
|
if (this.frame) {
|
|
@@ -2623,6 +2824,7 @@ class Playwright extends Helper {
|
|
|
2623
2824
|
async waitForInvisible(locator, sec) {
|
|
2624
2825
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2625
2826
|
locator = new Locator(locator, 'css')
|
|
2827
|
+
|
|
2626
2828
|
const context = await this._getContext()
|
|
2627
2829
|
let waiter
|
|
2628
2830
|
let count = 0
|
|
@@ -2653,6 +2855,7 @@ class Playwright extends Helper {
|
|
|
2653
2855
|
async waitToHide(locator, sec) {
|
|
2654
2856
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2655
2857
|
locator = new Locator(locator, 'css')
|
|
2858
|
+
|
|
2656
2859
|
const context = await this._getContext()
|
|
2657
2860
|
let waiter
|
|
2658
2861
|
let count = 0
|
|
@@ -2774,9 +2977,18 @@ class Playwright extends Helper {
|
|
|
2774
2977
|
if (context) {
|
|
2775
2978
|
const locator = new Locator(context, 'css')
|
|
2776
2979
|
try {
|
|
2980
|
+
if (locator.isCustom()) {
|
|
2981
|
+
// For custom locators, find the elements first then check for text within them
|
|
2982
|
+
const elements = await findCustomElements.call(this, contextObject, locator)
|
|
2983
|
+
if (elements.length === 0) {
|
|
2984
|
+
throw new Error(`Context element not found: ${locator.toString()}`)
|
|
2985
|
+
}
|
|
2986
|
+
return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2777
2989
|
if (!locator.isXPath()) {
|
|
2778
2990
|
return contextObject
|
|
2779
|
-
.locator(`${locator.
|
|
2991
|
+
.locator(`${locator.simplify()} >> text=${text}`)
|
|
2780
2992
|
.first()
|
|
2781
2993
|
.waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2782
2994
|
.catch(e => {
|
|
@@ -3421,9 +3633,15 @@ class Playwright extends Helper {
|
|
|
3421
3633
|
|
|
3422
3634
|
module.exports = Playwright
|
|
3423
3635
|
|
|
3636
|
+
function buildCustomLocatorString(locator) {
|
|
3637
|
+
// Note: this.debug not available in standalone function, using console.log
|
|
3638
|
+
console.log(`Building custom locator string: ${locator.type}=${locator.value}`)
|
|
3639
|
+
return `${locator.type}=${locator.value}`
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3424
3642
|
function buildLocatorString(locator) {
|
|
3425
3643
|
if (locator.isCustom()) {
|
|
3426
|
-
return
|
|
3644
|
+
return buildCustomLocatorString(locator)
|
|
3427
3645
|
}
|
|
3428
3646
|
if (locator.isXPath()) {
|
|
3429
3647
|
return `xpath=${locator.value}`
|
|
@@ -3435,15 +3653,119 @@ async function findElements(matcher, locator) {
|
|
|
3435
3653
|
if (locator.react) return findReact(matcher, locator)
|
|
3436
3654
|
if (locator.vue) return findVue(matcher, locator)
|
|
3437
3655
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3656
|
+
|
|
3438
3657
|
locator = new Locator(locator, 'css')
|
|
3439
3658
|
|
|
3440
|
-
|
|
3659
|
+
// Handle custom locators directly instead of relying on Playwright selector engines
|
|
3660
|
+
if (locator.isCustom()) {
|
|
3661
|
+
return findCustomElements.call(this, matcher, locator)
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
// Check if we have a custom context locator and need to search within it
|
|
3665
|
+
if (this.contextLocator) {
|
|
3666
|
+
const contextLocatorObj = new Locator(this.contextLocator, 'css')
|
|
3667
|
+
if (contextLocatorObj.isCustom()) {
|
|
3668
|
+
// Find the context elements first
|
|
3669
|
+
const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj)
|
|
3670
|
+
if (contextElements.length === 0) {
|
|
3671
|
+
return []
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
// Search within the first context element
|
|
3675
|
+
const locatorString = buildLocatorString(locator)
|
|
3676
|
+
return contextElements[0].locator(locatorString).all()
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
const locatorString = buildLocatorString(locator)
|
|
3681
|
+
|
|
3682
|
+
return matcher.locator(locatorString).all()
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
async function findCustomElements(matcher, locator) {
|
|
3686
|
+
const customLocatorStrategies = this.customLocatorStrategies || globalCustomLocatorStrategies
|
|
3687
|
+
const strategyFunction = customLocatorStrategies.get ? customLocatorStrategies.get(locator.type) : customLocatorStrategies[locator.type]
|
|
3688
|
+
|
|
3689
|
+
if (!strategyFunction) {
|
|
3690
|
+
throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// Execute the custom locator function in the browser context using page.evaluate
|
|
3694
|
+
const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page()
|
|
3695
|
+
|
|
3696
|
+
const elements = await page.evaluate(
|
|
3697
|
+
({ strategyCode, selector }) => {
|
|
3698
|
+
const strategy = new Function('return ' + strategyCode)()
|
|
3699
|
+
const result = strategy(selector, document)
|
|
3700
|
+
|
|
3701
|
+
// Convert NodeList or single element to array
|
|
3702
|
+
if (result && result.nodeType) {
|
|
3703
|
+
return [result]
|
|
3704
|
+
} else if (result && result.length !== undefined) {
|
|
3705
|
+
return Array.from(result)
|
|
3706
|
+
} else if (Array.isArray(result)) {
|
|
3707
|
+
return result
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
return []
|
|
3711
|
+
},
|
|
3712
|
+
{
|
|
3713
|
+
strategyCode: strategyFunction.toString(),
|
|
3714
|
+
selector: locator.value,
|
|
3715
|
+
},
|
|
3716
|
+
)
|
|
3717
|
+
|
|
3718
|
+
// Convert the found elements back to Playwright locators
|
|
3719
|
+
if (elements.length === 0) {
|
|
3720
|
+
return []
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
// Create CSS selectors for the found elements and return as locators
|
|
3724
|
+
const locators = []
|
|
3725
|
+
const timestamp = Date.now()
|
|
3726
|
+
|
|
3727
|
+
for (let i = 0; i < elements.length; i++) {
|
|
3728
|
+
// Use a unique attribute approach to target specific elements
|
|
3729
|
+
const uniqueAttr = `data-codecept-custom-${timestamp}-${i}`
|
|
3730
|
+
|
|
3731
|
+
await page.evaluate(
|
|
3732
|
+
({ index, uniqueAttr, strategyCode, selector }) => {
|
|
3733
|
+
// Re-execute the strategy to find elements and mark the specific one
|
|
3734
|
+
const strategy = new Function('return ' + strategyCode)()
|
|
3735
|
+
const result = strategy(selector, document)
|
|
3736
|
+
|
|
3737
|
+
let elementsArray = []
|
|
3738
|
+
if (result && result.nodeType) {
|
|
3739
|
+
elementsArray = [result]
|
|
3740
|
+
} else if (result && result.length !== undefined) {
|
|
3741
|
+
elementsArray = Array.from(result)
|
|
3742
|
+
} else if (Array.isArray(result)) {
|
|
3743
|
+
elementsArray = result
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
if (elementsArray[index]) {
|
|
3747
|
+
elementsArray[index].setAttribute(uniqueAttr, 'true')
|
|
3748
|
+
}
|
|
3749
|
+
},
|
|
3750
|
+
{
|
|
3751
|
+
index: i,
|
|
3752
|
+
uniqueAttr,
|
|
3753
|
+
strategyCode: strategyFunction.toString(),
|
|
3754
|
+
selector: locator.value,
|
|
3755
|
+
},
|
|
3756
|
+
)
|
|
3757
|
+
|
|
3758
|
+
locators.push(page.locator(`[${uniqueAttr}="true"]`))
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
return locators
|
|
3441
3762
|
}
|
|
3442
3763
|
|
|
3443
3764
|
async function findElement(matcher, locator) {
|
|
3444
3765
|
if (locator.react) return findReact(matcher, locator)
|
|
3445
3766
|
if (locator.vue) return findVue(matcher, locator)
|
|
3446
3767
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3768
|
+
|
|
3447
3769
|
locator = new Locator(locator, 'css')
|
|
3448
3770
|
|
|
3449
3771
|
return matcher.locator(buildLocatorString(locator)).first()
|
|
@@ -3764,9 +4086,7 @@ async function targetCreatedHandler(page) {
|
|
|
3764
4086
|
if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
|
|
3765
4087
|
try {
|
|
3766
4088
|
await page.setViewportSize(parseWindowSize(this.options.windowSize))
|
|
3767
|
-
} catch (err) {
|
|
3768
|
-
this.debug('Target can be already closed, ignoring...')
|
|
3769
|
-
}
|
|
4089
|
+
} catch (err) {}
|
|
3770
4090
|
}
|
|
3771
4091
|
}
|
|
3772
4092
|
|