codeceptjs 3.7.4 → 3.7.5-beta.2
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 +45 -0
- package/bin/codecept.js +25 -0
- package/bin/test-server.js +53 -0
- package/lib/codecept.js +41 -0
- package/lib/command/init.js +5 -0
- package/lib/command/run-failed-tests.js +216 -0
- package/lib/command/run-workers.js +16 -1
- package/lib/command/workers/runTests.js +220 -14
- package/lib/element/WebElement.js +327 -0
- package/lib/helper/JSONResponse.js +23 -4
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +396 -57
- package/lib/helper/Puppeteer.js +107 -28
- package/lib/helper/WebDriver.js +18 -4
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +12 -0
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +8 -10
- package/lib/plugin/failedTestsTracker.js +197 -0
- package/lib/plugin/htmlReporter.js +2955 -0
- package/lib/recorder.js +9 -0
- package/lib/test-server.js +323 -0
- package/lib/utils/mask_data.js +53 -0
- package/lib/utils.js +34 -2
- package/lib/workers.js +135 -9
- package/package.json +8 -6
- package/typings/index.d.ts +17 -4
- package/typings/promiseBasedTypes.d.ts +14 -0
- package/typings/types.d.ts +18 -0
package/lib/helper/Playwright.js
CHANGED
|
@@ -33,10 +33,13 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection
|
|
|
33
33
|
const Popup = require('./extras/Popup')
|
|
34
34
|
const Console = require('./extras/Console')
|
|
35
35
|
const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator')
|
|
36
|
+
const WebElement = require('../element/WebElement')
|
|
36
37
|
|
|
37
38
|
let playwright
|
|
38
39
|
let perfTiming
|
|
39
40
|
let defaultSelectorEnginesInitialized = false
|
|
41
|
+
let registeredCustomLocatorStrategies = new Set()
|
|
42
|
+
let globalCustomLocatorStrategies = new Map()
|
|
40
43
|
|
|
41
44
|
const popupStore = new Popup()
|
|
42
45
|
const consoleLogStore = new Console()
|
|
@@ -95,6 +98,7 @@ const pathSeparator = path.sep
|
|
|
95
98
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
96
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).
|
|
97
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}\"]\`) } }`
|
|
98
102
|
*/
|
|
99
103
|
const config = {}
|
|
100
104
|
|
|
@@ -343,9 +347,23 @@ class Playwright extends Helper {
|
|
|
343
347
|
this.recordingWebSocketMessages = false
|
|
344
348
|
this.recordedWebSocketMessagesAtLeastOnce = false
|
|
345
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
|
+
}
|
|
346
359
|
|
|
347
360
|
// override defaults with config
|
|
348
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)
|
|
349
367
|
}
|
|
350
368
|
|
|
351
369
|
_validateConfig(config) {
|
|
@@ -462,12 +480,61 @@ class Playwright extends Helper {
|
|
|
462
480
|
|
|
463
481
|
async _init() {
|
|
464
482
|
// register an internal selector engine for reading value property of elements in a selector
|
|
465
|
-
if (defaultSelectorEnginesInitialized) return
|
|
466
|
-
defaultSelectorEnginesInitialized = true
|
|
467
483
|
try {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
538
|
} catch (e) {
|
|
472
539
|
console.warn(e)
|
|
473
540
|
}
|
|
@@ -826,6 +893,9 @@ class Playwright extends Helper {
|
|
|
826
893
|
}
|
|
827
894
|
|
|
828
895
|
async _startBrowser() {
|
|
896
|
+
// Ensure custom locator strategies are registered before browser launch
|
|
897
|
+
await this._init()
|
|
898
|
+
|
|
829
899
|
if (this.isElectron) {
|
|
830
900
|
this.browser = await playwright._electron.launch(this.playwrightOptions)
|
|
831
901
|
} else if (this.isRemoteBrowser && this.isCDPConnection) {
|
|
@@ -861,6 +931,30 @@ class Playwright extends Helper {
|
|
|
861
931
|
return this.browser
|
|
862
932
|
}
|
|
863
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
|
+
|
|
864
958
|
/**
|
|
865
959
|
* Create a new browser context with a page. \
|
|
866
960
|
* Usually it should be run from a custom helper after call of `_startBrowser()`
|
|
@@ -868,11 +962,64 @@ class Playwright extends Helper {
|
|
|
868
962
|
*/
|
|
869
963
|
async _createContextPage(contextOptions) {
|
|
870
964
|
this.browserContext = await this.browser.newContext(contextOptions)
|
|
965
|
+
|
|
966
|
+
// Register custom locator strategies for this context
|
|
967
|
+
await this._registerCustomLocatorStrategies()
|
|
968
|
+
|
|
871
969
|
const page = await this.browserContext.newPage()
|
|
872
970
|
targetCreatedHandler.call(this, page)
|
|
873
971
|
await this._setPage(page)
|
|
874
972
|
}
|
|
875
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
|
+
|
|
876
1023
|
_getType() {
|
|
877
1024
|
return this.browser._type
|
|
878
1025
|
}
|
|
@@ -884,7 +1031,10 @@ class Playwright extends Helper {
|
|
|
884
1031
|
this.frame = null
|
|
885
1032
|
popupStore.clear()
|
|
886
1033
|
if (this.options.recordHar) await this.browserContext.close()
|
|
1034
|
+
this.browserContext = null
|
|
887
1035
|
await this.browser.close()
|
|
1036
|
+
this.browser = null
|
|
1037
|
+
this.isRunning = false
|
|
888
1038
|
}
|
|
889
1039
|
|
|
890
1040
|
async _evaluateHandeInContext(...args) {
|
|
@@ -1265,9 +1415,9 @@ class Playwright extends Helper {
|
|
|
1265
1415
|
async _locate(locator) {
|
|
1266
1416
|
const context = await this._getContext()
|
|
1267
1417
|
|
|
1268
|
-
if (this.frame) return findElements(this.frame, locator)
|
|
1418
|
+
if (this.frame) return findElements.call(this, this.frame, locator)
|
|
1269
1419
|
|
|
1270
|
-
const els = await findElements(context, locator)
|
|
1420
|
+
const els = await findElements.call(this, context, locator)
|
|
1271
1421
|
|
|
1272
1422
|
if (store.debugMode) {
|
|
1273
1423
|
const previewElements = els.slice(0, 3)
|
|
@@ -1341,7 +1491,8 @@ class Playwright extends Helper {
|
|
|
1341
1491
|
*
|
|
1342
1492
|
*/
|
|
1343
1493
|
async grabWebElements(locator) {
|
|
1344
|
-
|
|
1494
|
+
const elements = await this._locate(locator)
|
|
1495
|
+
return elements.map(element => new WebElement(element, this))
|
|
1345
1496
|
}
|
|
1346
1497
|
|
|
1347
1498
|
/**
|
|
@@ -1349,7 +1500,8 @@ class Playwright extends Helper {
|
|
|
1349
1500
|
*
|
|
1350
1501
|
*/
|
|
1351
1502
|
async grabWebElement(locator) {
|
|
1352
|
-
|
|
1503
|
+
const element = await this._locateElement(locator)
|
|
1504
|
+
return new WebElement(element, this)
|
|
1353
1505
|
}
|
|
1354
1506
|
|
|
1355
1507
|
/**
|
|
@@ -2060,11 +2212,25 @@ class Playwright extends Helper {
|
|
|
2060
2212
|
* @param {*} locator
|
|
2061
2213
|
*/
|
|
2062
2214
|
_contextLocator(locator) {
|
|
2063
|
-
|
|
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)
|
|
2064
2223
|
|
|
2065
2224
|
if (this.contextLocator) {
|
|
2066
|
-
const
|
|
2067
|
-
|
|
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
|
+
}
|
|
2068
2234
|
}
|
|
2069
2235
|
|
|
2070
2236
|
return locator
|
|
@@ -2075,11 +2241,25 @@ class Playwright extends Helper {
|
|
|
2075
2241
|
*
|
|
2076
2242
|
*/
|
|
2077
2243
|
async grabTextFrom(locator) {
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
+
}
|
|
2083
2263
|
}
|
|
2084
2264
|
|
|
2085
2265
|
/**
|
|
@@ -2092,7 +2272,6 @@ class Playwright extends Helper {
|
|
|
2092
2272
|
for (const el of els) {
|
|
2093
2273
|
texts.push(await el.innerText())
|
|
2094
2274
|
}
|
|
2095
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2096
2275
|
return texts
|
|
2097
2276
|
}
|
|
2098
2277
|
|
|
@@ -2111,7 +2290,6 @@ class Playwright extends Helper {
|
|
|
2111
2290
|
*/
|
|
2112
2291
|
async grabValueFromAll(locator) {
|
|
2113
2292
|
const els = await findFields.call(this, locator)
|
|
2114
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2115
2293
|
return Promise.all(els.map(el => el.inputValue()))
|
|
2116
2294
|
}
|
|
2117
2295
|
|
|
@@ -2130,7 +2308,6 @@ class Playwright extends Helper {
|
|
|
2130
2308
|
*/
|
|
2131
2309
|
async grabHTMLFromAll(locator) {
|
|
2132
2310
|
const els = await this._locate(locator)
|
|
2133
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2134
2311
|
return Promise.all(els.map(el => el.innerHTML()))
|
|
2135
2312
|
}
|
|
2136
2313
|
|
|
@@ -2151,7 +2328,6 @@ class Playwright extends Helper {
|
|
|
2151
2328
|
*/
|
|
2152
2329
|
async grabCssPropertyFromAll(locator, cssProperty) {
|
|
2153
2330
|
const els = await this._locate(locator)
|
|
2154
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2155
2331
|
const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
|
|
2156
2332
|
|
|
2157
2333
|
return cssValues
|
|
@@ -2262,7 +2438,6 @@ class Playwright extends Helper {
|
|
|
2262
2438
|
*/
|
|
2263
2439
|
async grabAttributeFromAll(locator, attr) {
|
|
2264
2440
|
const els = await this._locate(locator)
|
|
2265
|
-
this.debug(`Matched ${els.length} elements`)
|
|
2266
2441
|
const array = []
|
|
2267
2442
|
|
|
2268
2443
|
for (let index = 0; index < els.length; index++) {
|
|
@@ -2282,7 +2457,6 @@ class Playwright extends Helper {
|
|
|
2282
2457
|
const res = await this._locateElement(locator)
|
|
2283
2458
|
assertElementExists(res, locator)
|
|
2284
2459
|
const elem = res
|
|
2285
|
-
this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
|
|
2286
2460
|
return elem.screenshot({ path: outputFile, type: 'png' })
|
|
2287
2461
|
}
|
|
2288
2462
|
|
|
@@ -2577,7 +2751,16 @@ class Playwright extends Helper {
|
|
|
2577
2751
|
|
|
2578
2752
|
const context = await this._getContext()
|
|
2579
2753
|
try {
|
|
2580
|
-
|
|
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
|
+
}
|
|
2581
2764
|
} catch (e) {
|
|
2582
2765
|
throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
|
|
2583
2766
|
}
|
|
@@ -2591,9 +2774,30 @@ class Playwright extends Helper {
|
|
|
2591
2774
|
async waitForVisible(locator, sec) {
|
|
2592
2775
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2593
2776
|
locator = new Locator(locator, 'css')
|
|
2777
|
+
|
|
2594
2778
|
const context = await this._getContext()
|
|
2595
2779
|
let count = 0
|
|
2596
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
|
+
|
|
2597
2801
|
// we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
|
|
2598
2802
|
let waiter
|
|
2599
2803
|
if (this.frame) {
|
|
@@ -2620,6 +2824,7 @@ class Playwright extends Helper {
|
|
|
2620
2824
|
async waitForInvisible(locator, sec) {
|
|
2621
2825
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2622
2826
|
locator = new Locator(locator, 'css')
|
|
2827
|
+
|
|
2623
2828
|
const context = await this._getContext()
|
|
2624
2829
|
let waiter
|
|
2625
2830
|
let count = 0
|
|
@@ -2650,6 +2855,7 @@ class Playwright extends Helper {
|
|
|
2650
2855
|
async waitToHide(locator, sec) {
|
|
2651
2856
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
|
|
2652
2857
|
locator = new Locator(locator, 'css')
|
|
2858
|
+
|
|
2653
2859
|
const context = await this._getContext()
|
|
2654
2860
|
let waiter
|
|
2655
2861
|
let count = 0
|
|
@@ -2771,52 +2977,77 @@ class Playwright extends Helper {
|
|
|
2771
2977
|
if (context) {
|
|
2772
2978
|
const locator = new Locator(context, 'css')
|
|
2773
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
|
+
|
|
2774
2989
|
if (!locator.isXPath()) {
|
|
2775
2990
|
return contextObject
|
|
2776
|
-
.locator(`${locator.
|
|
2991
|
+
.locator(`${locator.simplify()} >> text=${text}`)
|
|
2777
2992
|
.first()
|
|
2778
2993
|
.waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2994
|
+
.catch(e => {
|
|
2995
|
+
throw new Error(errorMessage)
|
|
2996
|
+
})
|
|
2779
2997
|
}
|
|
2780
2998
|
|
|
2781
2999
|
if (locator.isXPath()) {
|
|
2782
|
-
return contextObject
|
|
2783
|
-
(
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3000
|
+
return contextObject
|
|
3001
|
+
.waitForFunction(
|
|
3002
|
+
([locator, text, $XPath]) => {
|
|
3003
|
+
eval($XPath)
|
|
3004
|
+
const el = $XPath(null, locator)
|
|
3005
|
+
if (!el.length) return false
|
|
3006
|
+
return el[0].innerText.indexOf(text) > -1
|
|
3007
|
+
},
|
|
3008
|
+
[locator.value, text, $XPath.toString()],
|
|
3009
|
+
{ timeout: waitTimeout },
|
|
3010
|
+
)
|
|
3011
|
+
.catch(e => {
|
|
3012
|
+
throw new Error(errorMessage)
|
|
3013
|
+
})
|
|
2792
3014
|
}
|
|
2793
3015
|
} catch (e) {
|
|
2794
3016
|
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2795
3017
|
}
|
|
2796
3018
|
}
|
|
2797
3019
|
|
|
3020
|
+
// Based on original implementation but fixed to check title text and remove problematic promiseRetry
|
|
3021
|
+
// Original used timeoutGap for waitForFunction to give it slightly more time than the locator
|
|
2798
3022
|
const timeoutGap = waitTimeout + 1000
|
|
2799
3023
|
|
|
2800
|
-
// We add basic timeout to make sure we don't wait forever
|
|
2801
|
-
// We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
|
|
2802
|
-
// or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
|
|
2803
|
-
// If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
|
|
2804
3024
|
return Promise.race([
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
this.page.waitForFunction(
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
if (
|
|
3025
|
+
// Strategy 1: waitForFunction that checks both body AND title text
|
|
3026
|
+
// Use this.page instead of contextObject because FrameLocator doesn't have waitForFunction
|
|
3027
|
+
// Original only checked document.body.innerText, missing title text like "TestEd"
|
|
3028
|
+
this.page.waitForFunction(
|
|
3029
|
+
function (text) {
|
|
3030
|
+
// Check body text (original behavior)
|
|
3031
|
+
if (document.body && document.body.innerText && document.body.innerText.indexOf(text) > -1) {
|
|
3032
|
+
return true
|
|
3033
|
+
}
|
|
3034
|
+
// Check document title (fixes the TestEd in title issue)
|
|
3035
|
+
if (document.title && document.title.indexOf(text) > -1) {
|
|
3036
|
+
return true
|
|
3037
|
+
}
|
|
3038
|
+
return false
|
|
2816
3039
|
},
|
|
2817
|
-
|
|
3040
|
+
text,
|
|
3041
|
+
{ timeout: timeoutGap },
|
|
2818
3042
|
),
|
|
2819
|
-
|
|
3043
|
+
// Strategy 2: Native Playwright text locator (replaces problematic promiseRetry)
|
|
3044
|
+
contextObject
|
|
3045
|
+
.locator(`:has-text(${JSON.stringify(text)})`)
|
|
3046
|
+
.first()
|
|
3047
|
+
.waitFor({ timeout: waitTimeout }),
|
|
3048
|
+
]).catch(err => {
|
|
3049
|
+
throw new Error(errorMessage)
|
|
3050
|
+
})
|
|
2820
3051
|
}
|
|
2821
3052
|
|
|
2822
3053
|
/**
|
|
@@ -3402,9 +3633,15 @@ class Playwright extends Helper {
|
|
|
3402
3633
|
|
|
3403
3634
|
module.exports = Playwright
|
|
3404
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
|
+
|
|
3405
3642
|
function buildLocatorString(locator) {
|
|
3406
3643
|
if (locator.isCustom()) {
|
|
3407
|
-
return
|
|
3644
|
+
return buildCustomLocatorString(locator)
|
|
3408
3645
|
}
|
|
3409
3646
|
if (locator.isXPath()) {
|
|
3410
3647
|
return `xpath=${locator.value}`
|
|
@@ -3416,15 +3653,119 @@ async function findElements(matcher, locator) {
|
|
|
3416
3653
|
if (locator.react) return findReact(matcher, locator)
|
|
3417
3654
|
if (locator.vue) return findVue(matcher, locator)
|
|
3418
3655
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3656
|
+
|
|
3419
3657
|
locator = new Locator(locator, 'css')
|
|
3420
3658
|
|
|
3421
|
-
|
|
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
|
|
3422
3762
|
}
|
|
3423
3763
|
|
|
3424
3764
|
async function findElement(matcher, locator) {
|
|
3425
3765
|
if (locator.react) return findReact(matcher, locator)
|
|
3426
3766
|
if (locator.vue) return findVue(matcher, locator)
|
|
3427
3767
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
3768
|
+
|
|
3428
3769
|
locator = new Locator(locator, 'css')
|
|
3429
3770
|
|
|
3430
3771
|
return matcher.locator(buildLocatorString(locator)).first()
|
|
@@ -3745,9 +4086,7 @@ async function targetCreatedHandler(page) {
|
|
|
3745
4086
|
if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
|
|
3746
4087
|
try {
|
|
3747
4088
|
await page.setViewportSize(parseWindowSize(this.options.windowSize))
|
|
3748
|
-
} catch (err) {
|
|
3749
|
-
this.debug('Target can be already closed, ignoring...')
|
|
3750
|
-
}
|
|
4089
|
+
} catch (err) {}
|
|
3751
4090
|
}
|
|
3752
4091
|
}
|
|
3753
4092
|
|