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.
@@ -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
- // connect to REST helper
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
- this._assertContains(actual[key], expected[key])
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
- currentTest = { test }
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
- return this._addContext(currentTest, context)
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
 
@@ -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
- return this._locate(locator)
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
- return this._locateElement(locator)
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.waitForFunction(
2783
- ([locator, text, $XPath]) => {
2784
- eval($XPath)
2785
- const el = $XPath(null, locator)
2786
- if (!el.length) return false
2787
- return el[0].innerText.indexOf(text) > -1
2788
- },
2789
- [locator.value, text, $XPath.toString()],
2790
- { timeout: waitTimeout },
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
- new Promise((_, reject) => {
2806
- setTimeout(() => reject(errorMessage), waitTimeout)
2807
- }),
2808
- this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
2809
- promiseRetry(
2810
- async retry => {
2811
- const textPresent = await contextObject
2812
- .locator(`:has-text(${JSON.stringify(text)})`)
2813
- .first()
2814
- .isVisible()
2815
- if (!textPresent) retry(errorMessage)
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
- { retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
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
  /**