codeceptjs 3.7.3 → 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 +3 -0
- package/bin/test-server.js +53 -0
- package/lib/codecept.js +46 -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/container.js +16 -3
- package/lib/element/WebElement.js +327 -0
- package/lib/helper/JSONResponse.js +23 -4
- package/lib/helper/Mochawesome.js +30 -9
- package/lib/helper/Playwright.js +74 -38
- 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/asyncWrapper.js +13 -3
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +18 -1
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +8 -10
- package/lib/pause.js +6 -1
- package/lib/plugin/commentStep.js +1 -1
- package/lib/plugin/htmlReporter.js +2955 -0
- package/lib/plugin/screenshotOnFail.js +1 -9
- 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/workerStorage.js +2 -1
- package/lib/workers.js +135 -9
- package/package.json +41 -37
- package/typings/index.d.ts +17 -4
- package/typings/promiseBasedTypes.d.ts +53 -688
- package/typings/types.d.ts +125 -691
|
@@ -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
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
let addMochawesomeContext
|
|
2
1
|
let currentTest
|
|
3
2
|
let currentSuite
|
|
4
3
|
|
|
@@ -16,7 +15,8 @@ class Mochawesome extends Helper {
|
|
|
16
15
|
disableScreenshots: false,
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
this._addContext = require('mochawesome/addContext')
|
|
19
|
+
|
|
20
20
|
this._createConfig(config)
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -37,35 +37,56 @@ 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) {
|
|
44
57
|
if (this.options.disableScreenshots) return
|
|
45
58
|
let fileName
|
|
46
59
|
// Get proper name if we are fail on hook
|
|
47
|
-
if (test.ctx
|
|
60
|
+
if (test.ctx?.test?.type === 'hook') {
|
|
48
61
|
currentTest = { test: test.ctx.test }
|
|
49
62
|
// ignore retries if we are in hook
|
|
50
63
|
test._retries = -1
|
|
51
64
|
fileName = clearString(`${test.title}_${currentTest.test.title}`)
|
|
52
65
|
} else {
|
|
53
66
|
currentTest = { test }
|
|
54
|
-
fileName =
|
|
67
|
+
fileName = testToFileName(test)
|
|
55
68
|
}
|
|
56
69
|
if (this.options.uniqueScreenshotNames) {
|
|
57
|
-
|
|
58
|
-
fileName = `${fileName.substring(0, 10)}_${uuid}`
|
|
70
|
+
fileName = testToFileName(test, { unique: true })
|
|
59
71
|
}
|
|
60
72
|
if (test._retries < 1 || test._retries === test.retryNum) {
|
|
61
73
|
fileName = `${fileName}.failed.png`
|
|
62
|
-
return
|
|
74
|
+
return this._addContext(currentTest, fileName)
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
addMochawesomeContext(context) {
|
|
67
79
|
if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
|
|
68
|
-
|
|
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)
|
|
69
90
|
}
|
|
70
91
|
}
|
|
71
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
|
/**
|
|
@@ -2377,15 +2380,19 @@ class Playwright extends Helper {
|
|
|
2377
2380
|
if (this.options.recordVideo && this.page && this.page.video()) {
|
|
2378
2381
|
test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
|
|
2379
2382
|
for (const sessionName in this.sessionPages) {
|
|
2380
|
-
|
|
2383
|
+
if (sessionName === '') continue
|
|
2384
|
+
test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`)
|
|
2381
2385
|
}
|
|
2382
2386
|
}
|
|
2383
2387
|
|
|
2384
2388
|
if (this.options.trace) {
|
|
2385
2389
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
|
|
2386
2390
|
for (const sessionName in this.sessionPages) {
|
|
2387
|
-
if (
|
|
2388
|
-
|
|
2391
|
+
if (sessionName === '') continue
|
|
2392
|
+
const sessionPage = this.sessionPages[sessionName]
|
|
2393
|
+
const sessionContext = sessionPage.context()
|
|
2394
|
+
if (!sessionContext || !sessionContext.tracing) continue
|
|
2395
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.failed`)
|
|
2389
2396
|
}
|
|
2390
2397
|
}
|
|
2391
2398
|
|
|
@@ -2399,7 +2406,8 @@ class Playwright extends Helper {
|
|
|
2399
2406
|
if (this.options.keepVideoForPassedTests) {
|
|
2400
2407
|
test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
|
|
2401
2408
|
for (const sessionName of Object.keys(this.sessionPages)) {
|
|
2402
|
-
|
|
2409
|
+
if (sessionName === '') continue
|
|
2410
|
+
test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`)
|
|
2403
2411
|
}
|
|
2404
2412
|
} else {
|
|
2405
2413
|
this.page
|
|
@@ -2414,8 +2422,11 @@ class Playwright extends Helper {
|
|
|
2414
2422
|
if (this.options.trace) {
|
|
2415
2423
|
test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
|
|
2416
2424
|
for (const sessionName in this.sessionPages) {
|
|
2417
|
-
if (
|
|
2418
|
-
|
|
2425
|
+
if (sessionName === '') continue
|
|
2426
|
+
const sessionPage = this.sessionPages[sessionName]
|
|
2427
|
+
const sessionContext = sessionPage.context()
|
|
2428
|
+
if (!sessionContext || !sessionContext.tracing) continue
|
|
2429
|
+
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.passed`)
|
|
2419
2430
|
}
|
|
2420
2431
|
}
|
|
2421
2432
|
} else {
|
|
@@ -2768,47 +2779,63 @@ class Playwright extends Helper {
|
|
|
2768
2779
|
.locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
|
|
2769
2780
|
.first()
|
|
2770
2781
|
.waitFor({ timeout: waitTimeout, state: 'visible' })
|
|
2782
|
+
.catch(e => {
|
|
2783
|
+
throw new Error(errorMessage)
|
|
2784
|
+
})
|
|
2771
2785
|
}
|
|
2772
2786
|
|
|
2773
2787
|
if (locator.isXPath()) {
|
|
2774
|
-
return contextObject
|
|
2775
|
-
(
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
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
|
+
})
|
|
2784
2802
|
}
|
|
2785
2803
|
} catch (e) {
|
|
2786
2804
|
throw new Error(`${errorMessage}\n${e.message}`)
|
|
2787
2805
|
}
|
|
2788
2806
|
}
|
|
2789
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
|
|
2790
2810
|
const timeoutGap = waitTimeout + 1000
|
|
2791
2811
|
|
|
2792
|
-
// We add basic timeout to make sure we don't wait forever
|
|
2793
|
-
// We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
|
|
2794
|
-
// or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
|
|
2795
|
-
// 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
|
|
2796
2812
|
return Promise.race([
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
this.page.waitForFunction(
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
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
|
|
2808
2827
|
},
|
|
2809
|
-
|
|
2828
|
+
text,
|
|
2829
|
+
{ timeout: timeoutGap },
|
|
2810
2830
|
),
|
|
2811
|
-
|
|
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
|
+
})
|
|
2812
2839
|
}
|
|
2813
2840
|
|
|
2814
2841
|
/**
|
|
@@ -3883,9 +3910,18 @@ function saveVideoForPage(page, name) {
|
|
|
3883
3910
|
async function saveTraceForContext(context, name) {
|
|
3884
3911
|
if (!context) return
|
|
3885
3912
|
if (!context.tracing) return
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3913
|
+
try {
|
|
3914
|
+
const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
|
|
3915
|
+
await context.tracing.stop({ path: fileName })
|
|
3916
|
+
return fileName
|
|
3917
|
+
} catch (err) {
|
|
3918
|
+
// Handle the case where tracing was not started or context is invalid
|
|
3919
|
+
if (err.message && err.message.includes('Must start tracing before stopping')) {
|
|
3920
|
+
// Tracing was never started on this context, silently skip
|
|
3921
|
+
return null
|
|
3922
|
+
}
|
|
3923
|
+
throw err
|
|
3924
|
+
}
|
|
3889
3925
|
}
|
|
3890
3926
|
|
|
3891
3927
|
async function highlightActiveElement(element) {
|