codeceptjs 3.7.4 → 3.7.5-beta.1
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 +2 -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-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 +48 -29
- 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/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 +53 -0
- package/typings/types.d.ts +68 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified WebElement class that wraps native element instances from different helpers
|
|
5
|
+
* and provides a consistent API across all supported helpers (Playwright, WebDriver, Puppeteer).
|
|
6
|
+
*/
|
|
7
|
+
class WebElement {
|
|
8
|
+
constructor(element, helper) {
|
|
9
|
+
this.element = element
|
|
10
|
+
this.helper = helper
|
|
11
|
+
this.helperType = this._detectHelperType(helper)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_detectHelperType(helper) {
|
|
15
|
+
if (!helper) return 'unknown'
|
|
16
|
+
|
|
17
|
+
const className = helper.constructor.name
|
|
18
|
+
if (className === 'Playwright') return 'playwright'
|
|
19
|
+
if (className === 'WebDriver') return 'webdriver'
|
|
20
|
+
if (className === 'Puppeteer') return 'puppeteer'
|
|
21
|
+
|
|
22
|
+
return 'unknown'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the native element instance
|
|
27
|
+
* @returns {ElementHandle|WebElement|ElementHandle} Native element
|
|
28
|
+
*/
|
|
29
|
+
getNativeElement() {
|
|
30
|
+
return this.element
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the helper instance
|
|
35
|
+
* @returns {Helper} Helper instance
|
|
36
|
+
*/
|
|
37
|
+
getHelper() {
|
|
38
|
+
return this.helper
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get text content of the element
|
|
43
|
+
* @returns {Promise<string>} Element text content
|
|
44
|
+
*/
|
|
45
|
+
async getText() {
|
|
46
|
+
switch (this.helperType) {
|
|
47
|
+
case 'playwright':
|
|
48
|
+
return this.element.textContent()
|
|
49
|
+
case 'webdriver':
|
|
50
|
+
return this.element.getText()
|
|
51
|
+
case 'puppeteer':
|
|
52
|
+
return this.element.evaluate(el => el.textContent)
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get attribute value of the element
|
|
60
|
+
* @param {string} name Attribute name
|
|
61
|
+
* @returns {Promise<string|null>} Attribute value
|
|
62
|
+
*/
|
|
63
|
+
async getAttribute(name) {
|
|
64
|
+
switch (this.helperType) {
|
|
65
|
+
case 'playwright':
|
|
66
|
+
return this.element.getAttribute(name)
|
|
67
|
+
case 'webdriver':
|
|
68
|
+
return this.element.getAttribute(name)
|
|
69
|
+
case 'puppeteer':
|
|
70
|
+
return this.element.evaluate((el, attrName) => el.getAttribute(attrName), name)
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get property value of the element
|
|
78
|
+
* @param {string} name Property name
|
|
79
|
+
* @returns {Promise<any>} Property value
|
|
80
|
+
*/
|
|
81
|
+
async getProperty(name) {
|
|
82
|
+
switch (this.helperType) {
|
|
83
|
+
case 'playwright':
|
|
84
|
+
return this.element.evaluate((el, propName) => el[propName], name)
|
|
85
|
+
case 'webdriver':
|
|
86
|
+
return this.element.getProperty(name)
|
|
87
|
+
case 'puppeteer':
|
|
88
|
+
return this.element.evaluate((el, propName) => el[propName], name)
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get innerHTML of the element
|
|
96
|
+
* @returns {Promise<string>} Element innerHTML
|
|
97
|
+
*/
|
|
98
|
+
async getInnerHTML() {
|
|
99
|
+
switch (this.helperType) {
|
|
100
|
+
case 'playwright':
|
|
101
|
+
return this.element.innerHTML()
|
|
102
|
+
case 'webdriver':
|
|
103
|
+
return this.element.getProperty('innerHTML')
|
|
104
|
+
case 'puppeteer':
|
|
105
|
+
return this.element.evaluate(el => el.innerHTML)
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get value of the element (for input elements)
|
|
113
|
+
* @returns {Promise<string>} Element value
|
|
114
|
+
*/
|
|
115
|
+
async getValue() {
|
|
116
|
+
switch (this.helperType) {
|
|
117
|
+
case 'playwright':
|
|
118
|
+
return this.element.inputValue()
|
|
119
|
+
case 'webdriver':
|
|
120
|
+
return this.element.getValue()
|
|
121
|
+
case 'puppeteer':
|
|
122
|
+
return this.element.evaluate(el => el.value)
|
|
123
|
+
default:
|
|
124
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if element is visible
|
|
130
|
+
* @returns {Promise<boolean>} True if element is visible
|
|
131
|
+
*/
|
|
132
|
+
async isVisible() {
|
|
133
|
+
switch (this.helperType) {
|
|
134
|
+
case 'playwright':
|
|
135
|
+
return this.element.isVisible()
|
|
136
|
+
case 'webdriver':
|
|
137
|
+
return this.element.isDisplayed()
|
|
138
|
+
case 'puppeteer':
|
|
139
|
+
return this.element.evaluate(el => {
|
|
140
|
+
const style = window.getComputedStyle(el)
|
|
141
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
|
142
|
+
})
|
|
143
|
+
default:
|
|
144
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if element is enabled
|
|
150
|
+
* @returns {Promise<boolean>} True if element is enabled
|
|
151
|
+
*/
|
|
152
|
+
async isEnabled() {
|
|
153
|
+
switch (this.helperType) {
|
|
154
|
+
case 'playwright':
|
|
155
|
+
return this.element.isEnabled()
|
|
156
|
+
case 'webdriver':
|
|
157
|
+
return this.element.isEnabled()
|
|
158
|
+
case 'puppeteer':
|
|
159
|
+
return this.element.evaluate(el => !el.disabled)
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if element exists in DOM
|
|
167
|
+
* @returns {Promise<boolean>} True if element exists
|
|
168
|
+
*/
|
|
169
|
+
async exists() {
|
|
170
|
+
try {
|
|
171
|
+
switch (this.helperType) {
|
|
172
|
+
case 'playwright':
|
|
173
|
+
// For Playwright, if we have the element, it exists
|
|
174
|
+
return await this.element.evaluate(el => !!el)
|
|
175
|
+
case 'webdriver':
|
|
176
|
+
// For WebDriver, if we have the element, it exists
|
|
177
|
+
return true
|
|
178
|
+
case 'puppeteer':
|
|
179
|
+
// For Puppeteer, if we have the element, it exists
|
|
180
|
+
return await this.element.evaluate(el => !!el)
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get bounding box of the element
|
|
191
|
+
* @returns {Promise<Object>} Bounding box with x, y, width, height properties
|
|
192
|
+
*/
|
|
193
|
+
async getBoundingBox() {
|
|
194
|
+
switch (this.helperType) {
|
|
195
|
+
case 'playwright':
|
|
196
|
+
return this.element.boundingBox()
|
|
197
|
+
case 'webdriver':
|
|
198
|
+
const rect = await this.element.getRect()
|
|
199
|
+
return {
|
|
200
|
+
x: rect.x,
|
|
201
|
+
y: rect.y,
|
|
202
|
+
width: rect.width,
|
|
203
|
+
height: rect.height,
|
|
204
|
+
}
|
|
205
|
+
case 'puppeteer':
|
|
206
|
+
return this.element.boundingBox()
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Click the element
|
|
214
|
+
* @param {Object} options Click options
|
|
215
|
+
* @returns {Promise<void>}
|
|
216
|
+
*/
|
|
217
|
+
async click(options = {}) {
|
|
218
|
+
switch (this.helperType) {
|
|
219
|
+
case 'playwright':
|
|
220
|
+
return this.element.click(options)
|
|
221
|
+
case 'webdriver':
|
|
222
|
+
return this.element.click()
|
|
223
|
+
case 'puppeteer':
|
|
224
|
+
return this.element.click(options)
|
|
225
|
+
default:
|
|
226
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Type text into the element
|
|
232
|
+
* @param {string} text Text to type
|
|
233
|
+
* @param {Object} options Type options
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
async type(text, options = {}) {
|
|
237
|
+
switch (this.helperType) {
|
|
238
|
+
case 'playwright':
|
|
239
|
+
return this.element.type(text, options)
|
|
240
|
+
case 'webdriver':
|
|
241
|
+
return this.element.setValue(text)
|
|
242
|
+
case 'puppeteer':
|
|
243
|
+
return this.element.type(text, options)
|
|
244
|
+
default:
|
|
245
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Find first child element matching the locator
|
|
251
|
+
* @param {string|Object} locator Element locator
|
|
252
|
+
* @returns {Promise<WebElement|null>} WebElement instance or null if not found
|
|
253
|
+
*/
|
|
254
|
+
async $(locator) {
|
|
255
|
+
let childElement
|
|
256
|
+
|
|
257
|
+
switch (this.helperType) {
|
|
258
|
+
case 'playwright':
|
|
259
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
260
|
+
break
|
|
261
|
+
case 'webdriver':
|
|
262
|
+
try {
|
|
263
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
break
|
|
268
|
+
case 'puppeteer':
|
|
269
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
270
|
+
break
|
|
271
|
+
default:
|
|
272
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return childElement ? new WebElement(childElement, this.helper) : null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Find all child elements matching the locator
|
|
280
|
+
* @param {string|Object} locator Element locator
|
|
281
|
+
* @returns {Promise<WebElement[]>} Array of WebElement instances
|
|
282
|
+
*/
|
|
283
|
+
async $$(locator) {
|
|
284
|
+
let childElements
|
|
285
|
+
|
|
286
|
+
switch (this.helperType) {
|
|
287
|
+
case 'playwright':
|
|
288
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
289
|
+
break
|
|
290
|
+
case 'webdriver':
|
|
291
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
292
|
+
break
|
|
293
|
+
case 'puppeteer':
|
|
294
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
295
|
+
break
|
|
296
|
+
default:
|
|
297
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return childElements.map(el => new WebElement(el, this.helper))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Normalize locator for element search
|
|
305
|
+
* @param {string|Object} locator Locator to normalize
|
|
306
|
+
* @returns {string} Normalized CSS selector
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
_normalizeLocator(locator) {
|
|
310
|
+
if (typeof locator === 'string') {
|
|
311
|
+
return locator
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof locator === 'object') {
|
|
315
|
+
// Handle CodeceptJS locator objects
|
|
316
|
+
if (locator.css) return locator.css
|
|
317
|
+
if (locator.xpath) return locator.xpath
|
|
318
|
+
if (locator.id) return `#${locator.id}`
|
|
319
|
+
if (locator.name) return `[name="${locator.name}"]`
|
|
320
|
+
if (locator.className) return `.${locator.className}`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return locator.toString()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = WebElement
|
|
@@ -72,10 +72,11 @@ class JSONResponse extends Helper {
|
|
|
72
72
|
if (!this.helpers[this.options.requestHelper]) {
|
|
73
73
|
throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse;
|
|
76
76
|
this.helpers[this.options.requestHelper].config.onResponse = response => {
|
|
77
|
-
this.response = response
|
|
78
|
-
|
|
77
|
+
this.response = response;
|
|
78
|
+
if (typeof origOnResponse === 'function') origOnResponse(response);
|
|
79
|
+
};
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
_before() {
|
|
@@ -349,7 +350,25 @@ class JSONResponse extends Helper {
|
|
|
349
350
|
for (const key in expected) {
|
|
350
351
|
assert(key in actual, `Key "${key}" not found in ${JSON.stringify(actual)}`)
|
|
351
352
|
if (typeof expected[key] === 'object' && expected[key] !== null) {
|
|
352
|
-
|
|
353
|
+
if (Array.isArray(expected[key])) {
|
|
354
|
+
// Handle array comparison: each expected element should have a match in actual array
|
|
355
|
+
assert(Array.isArray(actual[key]), `Expected array for key "${key}", but got ${typeof actual[key]}`)
|
|
356
|
+
for (const expectedItem of expected[key]) {
|
|
357
|
+
let found = false
|
|
358
|
+
for (const actualItem of actual[key]) {
|
|
359
|
+
try {
|
|
360
|
+
this._assertContains(actualItem, expectedItem)
|
|
361
|
+
found = true
|
|
362
|
+
break
|
|
363
|
+
} catch (err) {
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
assert(found, `No matching element found in array for ${JSON.stringify(expectedItem)}`)
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
this._assertContains(actual[key], expected[key])
|
|
371
|
+
}
|
|
353
372
|
} else {
|
|
354
373
|
assert.deepStrictEqual(actual[key], expected[key], `Values for key "${key}" don't match`)
|
|
355
374
|
}
|
|
@@ -37,7 +37,20 @@ class Mochawesome extends Helper {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
_test(test) {
|
|
40
|
-
|
|
40
|
+
// If this is a retried test, we want to add context to the retried test
|
|
41
|
+
// but also potentially preserve context from the original test
|
|
42
|
+
const originalTest = test.retriedTest && test.retriedTest()
|
|
43
|
+
if (originalTest) {
|
|
44
|
+
// This is a retried test - use the retried test for context
|
|
45
|
+
currentTest = { test }
|
|
46
|
+
|
|
47
|
+
// Optionally copy context from original test if it exists
|
|
48
|
+
// Note: mochawesome context is stored in test.ctx, but we need to be careful
|
|
49
|
+
// not to break the mocha context structure
|
|
50
|
+
} else {
|
|
51
|
+
// Normal test (not a retry)
|
|
52
|
+
currentTest = { test }
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
_failed(test) {
|
|
@@ -64,7 +77,16 @@ class Mochawesome extends Helper {
|
|
|
64
77
|
|
|
65
78
|
addMochawesomeContext(context) {
|
|
66
79
|
if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
|
|
67
|
-
|
|
80
|
+
|
|
81
|
+
// For retried tests, make sure we're adding context to the current (retried) test
|
|
82
|
+
// not the original test
|
|
83
|
+
let targetTest = currentTest
|
|
84
|
+
if (currentTest.test && currentTest.test.retriedTest && currentTest.test.retriedTest()) {
|
|
85
|
+
// This test has been retried, make sure we're using the current test for context
|
|
86
|
+
targetTest = { test: currentTest.test }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this._addContext(targetTest, context)
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -33,6 +33,7 @@ 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
|
|
@@ -1341,7 +1342,8 @@ class Playwright extends Helper {
|
|
|
1341
1342
|
*
|
|
1342
1343
|
*/
|
|
1343
1344
|
async grabWebElements(locator) {
|
|
1344
|
-
|
|
1345
|
+
const elements = await this._locate(locator)
|
|
1346
|
+
return elements.map(element => new WebElement(element, this))
|
|
1345
1347
|
}
|
|
1346
1348
|
|
|
1347
1349
|
/**
|
|
@@ -1349,7 +1351,8 @@ class Playwright extends Helper {
|
|
|
1349
1351
|
*
|
|
1350
1352
|
*/
|
|
1351
1353
|
async grabWebElement(locator) {
|
|
1352
|
-
|
|
1354
|
+
const element = await this._locateElement(locator)
|
|
1355
|
+
return new WebElement(element, this)
|
|
1353
1356
|
}
|
|
1354
1357
|
|
|
1355
1358
|
/**
|
|
@@ -2776,47 +2779,63 @@ class Playwright extends Helper {
|
|
|
2776
2779
|
.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
|
|
2777
2780
|
.first()
|
|
2778
2781
|
.waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2782
|
+
.catch(e => {
|
|
2783
|
+
throw new Error(errorMessage)
|
|
2784
|
+
})
|
|
2779
2785
|
}
|
|
2780
2786
|
|
|
2781
2787
|
if (locator.isXPath()) {
|
|
2782
|
-
return contextObject
|
|
2783
|
-
(
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2788
|
+
return contextObject
|
|
2789
|
+
.waitForFunction(
|
|
2790
|
+
([locator, text, $XPath]) => {
|
|
2791
|
+
eval($XPath)
|
|
2792
|
+
const el = $XPath(null, locator)
|
|
2793
|
+
if (!el.length) return false
|
|
2794
|
+
return el[0].innerText.indexOf(text) > -1
|
|
2795
|
+
},
|
|
2796
|
+
[locator.value, text, $XPath.toString()],
|
|
2797
|
+
{ timeout: waitTimeout },
|
|
2798
|
+
)
|
|
2799
|
+
.catch(e => {
|
|
2800
|
+
throw new Error(errorMessage)
|
|
2801
|
+
})
|
|
2792
2802
|
}
|
|
2793
2803
|
} catch (e) {
|
|
2794
2804
|
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2795
2805
|
}
|
|
2796
2806
|
}
|
|
2797
2807
|
|
|
2808
|
+
// Based on original implementation but fixed to check title text and remove problematic promiseRetry
|
|
2809
|
+
// Original used timeoutGap for waitForFunction to give it slightly more time than the locator
|
|
2798
2810
|
const timeoutGap = waitTimeout + 1000
|
|
2799
2811
|
|
|
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
2812
|
return Promise.race([
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
this.page.waitForFunction(
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
if (
|
|
2813
|
+
// Strategy 1: waitForFunction that checks both body AND title text
|
|
2814
|
+
// Use this.page instead of contextObject because FrameLocator doesn't have waitForFunction
|
|
2815
|
+
// Original only checked document.body.innerText, missing title text like "TestEd"
|
|
2816
|
+
this.page.waitForFunction(
|
|
2817
|
+
function (text) {
|
|
2818
|
+
// Check body text (original behavior)
|
|
2819
|
+
if (document.body && document.body.innerText && document.body.innerText.indexOf(text) > -1) {
|
|
2820
|
+
return true
|
|
2821
|
+
}
|
|
2822
|
+
// Check document title (fixes the TestEd in title issue)
|
|
2823
|
+
if (document.title && document.title.indexOf(text) > -1) {
|
|
2824
|
+
return true
|
|
2825
|
+
}
|
|
2826
|
+
return false
|
|
2816
2827
|
},
|
|
2817
|
-
|
|
2828
|
+
text,
|
|
2829
|
+
{ timeout: timeoutGap },
|
|
2818
2830
|
),
|
|
2819
|
-
|
|
2831
|
+
// Strategy 2: Native Playwright text locator (replaces problematic promiseRetry)
|
|
2832
|
+
contextObject
|
|
2833
|
+
.locator(`:has-text(${JSON.stringify(text)})`)
|
|
2834
|
+
.first()
|
|
2835
|
+
.waitFor({ timeout: waitTimeout }),
|
|
2836
|
+
]).catch(err => {
|
|
2837
|
+
throw new Error(errorMessage)
|
|
2838
|
+
})
|
|
2820
2839
|
}
|
|
2821
2840
|
|
|
2822
2841
|
/**
|