codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.8.esm-aria
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 +46 -3
- package/bin/codecept.js +9 -0
- package/bin/test-server.js +64 -0
- package/docs/webapi/click.mustache +5 -1
- package/lib/ai.js +66 -102
- package/lib/codecept.js +99 -24
- package/lib/command/generate.js +33 -1
- package/lib/command/init.js +7 -3
- package/lib/command/run-workers.js +31 -2
- package/lib/command/run.js +15 -0
- package/lib/command/workers/runTests.js +331 -58
- package/lib/config.js +16 -5
- package/lib/container.js +15 -13
- package/lib/effects.js +1 -1
- package/lib/element/WebElement.js +327 -0
- package/lib/event.js +10 -1
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +34 -6
- package/lib/helper/Appium.js +156 -42
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +48 -40
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +841 -153
- package/lib/helper/Puppeteer.js +263 -67
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +105 -16
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
- package/lib/helper/network/actions.js +8 -6
- package/lib/listener/config.js +11 -3
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/helpers.js +8 -2
- 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/factory.js +3 -0
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +62 -18
- package/lib/plugin/coverage.js +16 -3
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/recorder.js +28 -3
- package/lib/result.js +100 -23
- package/lib/retryCoordinator.js +207 -0
- package/lib/step/base.js +1 -1
- package/lib/step/comment.js +2 -2
- package/lib/step/meta.js +1 -1
- package/lib/template/heal.js +1 -1
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +87 -6
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +179 -23
- package/package.json +58 -47
- package/typings/index.d.ts +19 -7
- package/typings/promiseBasedTypes.d.ts +5525 -3759
- package/typings/types.d.ts +5791 -3781
package/lib/helper/Appium.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import * as webdriverio from 'webdriverio'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
5
|
+
|
|
6
|
+
import Webdriver from './WebDriver.js'
|
|
7
|
+
import AssertionFailedError from '../assert/error.js'
|
|
8
|
+
import { truth } from '../assert/truth.js'
|
|
9
|
+
import recorder from '../recorder.js'
|
|
10
|
+
import Locator from '../locator.js'
|
|
11
|
+
import ConnectionRefused from './errors/ConnectionRefused.js'
|
|
12
|
+
import ElementNotFound from './errors/ElementNotFound.js'
|
|
13
|
+
import { dontSeeElementError } from './errors/ElementAssertion.js'
|
|
13
14
|
|
|
14
15
|
const mobileRoot = '//*'
|
|
15
16
|
const webRoot = 'body'
|
|
@@ -181,7 +182,6 @@ class Appium extends Webdriver {
|
|
|
181
182
|
this.appiumV2 = config.appiumV2 || true
|
|
182
183
|
this.axios = axios.create()
|
|
183
184
|
|
|
184
|
-
webdriverio = require('webdriverio')
|
|
185
185
|
if (!config.appiumV2) {
|
|
186
186
|
console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Appium 2.x is used by default.')
|
|
187
187
|
console.log('More info: https://bit.ly/appium-v2-migration')
|
|
@@ -261,11 +261,13 @@ class Appium extends Webdriver {
|
|
|
261
261
|
|
|
262
262
|
this.platform = null
|
|
263
263
|
if (config.capabilities[`${vendorPrefix.appium}:platformName`]) {
|
|
264
|
-
|
|
264
|
+
config.capabilities[`${vendorPrefix.appium}:platformName`] = config.capabilities[`${vendorPrefix.appium}:platformName`].toLowerCase()
|
|
265
|
+
this.platform = config.capabilities[`${vendorPrefix.appium}:platformName`]
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
if (config.capabilities.platformName) {
|
|
268
|
-
|
|
269
|
+
config.capabilities.platformName = config.capabilities.platformName.toLowerCase()
|
|
270
|
+
this.platform = config.capabilities.platformName
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
return config
|
|
@@ -275,7 +277,7 @@ class Appium extends Webdriver {
|
|
|
275
277
|
const _convertedCaps = {}
|
|
276
278
|
for (const [key, value] of Object.entries(capabilities)) {
|
|
277
279
|
if (!key.startsWith(vendorPrefix.appium)) {
|
|
278
|
-
if (key !== 'platformName' && key !== 'bstack:options') {
|
|
280
|
+
if (key !== 'platformName' && key !== 'bstack:options' && key !== 'sauce:options') {
|
|
279
281
|
_convertedCaps[`${vendorPrefix.appium}:${key}`] = value
|
|
280
282
|
} else {
|
|
281
283
|
_convertedCaps[`${key}`] = value
|
|
@@ -389,6 +391,29 @@ class Appium extends Webdriver {
|
|
|
389
391
|
return `${protocol}://${hostname}:${port}${normalizedPath}/session/${this.browser.sessionId}`
|
|
390
392
|
}
|
|
391
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Helper method to safely call isDisplayed() on mobile elements.
|
|
396
|
+
* Handles the case where webdriverio tries to use execute/sync which isn't supported in Appium.
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
async _isDisplayedSafe(element) {
|
|
400
|
+
if (this.isWeb) {
|
|
401
|
+
// For web contexts, use the normal isDisplayed
|
|
402
|
+
return element.isDisplayed()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
return await element.isDisplayed()
|
|
407
|
+
} catch (err) {
|
|
408
|
+
// If isDisplayed fails due to execute/sync not being supported in native mobile contexts,
|
|
409
|
+
// fall back to assuming the element is displayed (since we found it)
|
|
410
|
+
if (err.message && err.message.includes('Method is not implemented')) {
|
|
411
|
+
return true
|
|
412
|
+
}
|
|
413
|
+
throw err
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
392
417
|
/**
|
|
393
418
|
* Execute code only on iOS
|
|
394
419
|
*
|
|
@@ -617,6 +642,7 @@ class Appium extends Webdriver {
|
|
|
617
642
|
*/
|
|
618
643
|
async resetApp() {
|
|
619
644
|
onlyForApps.call(this)
|
|
645
|
+
this.isWeb = false // Reset to native context after app reset
|
|
620
646
|
return this.axios({
|
|
621
647
|
method: 'post',
|
|
622
648
|
url: `${this._buildAppiumEndpoint()}/appium/app/reset`,
|
|
@@ -1132,7 +1158,7 @@ class Appium extends Webdriver {
|
|
|
1132
1158
|
],
|
|
1133
1159
|
},
|
|
1134
1160
|
])
|
|
1135
|
-
await this.browser.pause(
|
|
1161
|
+
await this.browser.pause(2000)
|
|
1136
1162
|
}
|
|
1137
1163
|
|
|
1138
1164
|
/**
|
|
@@ -1294,28 +1320,26 @@ class Appium extends Webdriver {
|
|
|
1294
1320
|
let currentSource
|
|
1295
1321
|
return browser
|
|
1296
1322
|
.waitUntil(
|
|
1297
|
-
() => {
|
|
1323
|
+
async () => {
|
|
1298
1324
|
if (err) {
|
|
1299
1325
|
return new Error(`Scroll to the end and element ${searchableLocator} was not found`)
|
|
1300
1326
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
})
|
|
1318
|
-
})
|
|
1327
|
+
const els = await browser.$$(parseLocator.call(this, searchableLocator))
|
|
1328
|
+
if (els.length) {
|
|
1329
|
+
const displayed = await this._isDisplayedSafe(els[0])
|
|
1330
|
+
if (displayed) {
|
|
1331
|
+
return true
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
await this[direction](scrollLocator, offset, speed)
|
|
1336
|
+
const source = await this.browser.getPageSource()
|
|
1337
|
+
if (source === currentSource) {
|
|
1338
|
+
err = true
|
|
1339
|
+
} else {
|
|
1340
|
+
currentSource = source
|
|
1341
|
+
return false
|
|
1342
|
+
}
|
|
1319
1343
|
},
|
|
1320
1344
|
timeout * 1000,
|
|
1321
1345
|
errorMsg,
|
|
@@ -1521,7 +1545,26 @@ class Appium extends Webdriver {
|
|
|
1521
1545
|
*/
|
|
1522
1546
|
async dontSeeElement(locator) {
|
|
1523
1547
|
if (this.isWeb) return super.dontSeeElement(locator)
|
|
1524
|
-
|
|
1548
|
+
|
|
1549
|
+
// For mobile native apps, use safe isDisplayed wrapper
|
|
1550
|
+
const parsedLocator = parseLocator.call(this, locator)
|
|
1551
|
+
const res = await this._locate(parsedLocator, false)
|
|
1552
|
+
|
|
1553
|
+
if (!res || res.length === 0) {
|
|
1554
|
+
return truth(`elements of ${Locator.build(parsedLocator)}`, 'to be seen').negate(false)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const selected = []
|
|
1558
|
+
for (const el of res) {
|
|
1559
|
+
const displayed = await this._isDisplayedSafe(el)
|
|
1560
|
+
if (displayed) selected.push(true)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
try {
|
|
1564
|
+
return truth(`elements of ${Locator.build(parsedLocator)}`, 'to be seen').negate(selected)
|
|
1565
|
+
} catch (err) {
|
|
1566
|
+
throw err
|
|
1567
|
+
}
|
|
1525
1568
|
}
|
|
1526
1569
|
|
|
1527
1570
|
/**
|
|
@@ -1575,7 +1618,18 @@ class Appium extends Webdriver {
|
|
|
1575
1618
|
*/
|
|
1576
1619
|
async grabNumberOfVisibleElements(locator) {
|
|
1577
1620
|
if (this.isWeb) return super.grabNumberOfVisibleElements(locator)
|
|
1578
|
-
|
|
1621
|
+
|
|
1622
|
+
// For mobile native apps, use safe isDisplayed wrapper
|
|
1623
|
+
const parsedLocator = parseLocator.call(this, locator)
|
|
1624
|
+
const res = await this._locate(parsedLocator)
|
|
1625
|
+
|
|
1626
|
+
const selected = []
|
|
1627
|
+
for (const el of res) {
|
|
1628
|
+
const displayed = await this._isDisplayedSafe(el)
|
|
1629
|
+
if (displayed) selected.push(true)
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
return selected.length
|
|
1579
1633
|
}
|
|
1580
1634
|
|
|
1581
1635
|
/**
|
|
@@ -1654,7 +1708,26 @@ class Appium extends Webdriver {
|
|
|
1654
1708
|
*/
|
|
1655
1709
|
async seeElement(locator) {
|
|
1656
1710
|
if (this.isWeb) return super.seeElement(locator)
|
|
1657
|
-
|
|
1711
|
+
|
|
1712
|
+
// For mobile native apps, use safe isDisplayed wrapper
|
|
1713
|
+
const parsedLocator = parseLocator.call(this, locator)
|
|
1714
|
+
const res = await this._locate(parsedLocator, true)
|
|
1715
|
+
|
|
1716
|
+
if (!res || res.length === 0) {
|
|
1717
|
+
throw new ElementNotFound(parsedLocator)
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const selected = []
|
|
1721
|
+
for (const el of res) {
|
|
1722
|
+
const displayed = await this._isDisplayedSafe(el)
|
|
1723
|
+
if (displayed) selected.push(true)
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
try {
|
|
1727
|
+
return truth(`elements of ${Locator.build(parsedLocator)}`, 'to be seen').assert(selected)
|
|
1728
|
+
} catch (e) {
|
|
1729
|
+
dontSeeElementError(parsedLocator)
|
|
1730
|
+
}
|
|
1658
1731
|
}
|
|
1659
1732
|
|
|
1660
1733
|
/**
|
|
@@ -1701,7 +1774,29 @@ class Appium extends Webdriver {
|
|
|
1701
1774
|
*/
|
|
1702
1775
|
async waitForVisible(locator, sec = null) {
|
|
1703
1776
|
if (this.isWeb) return super.waitForVisible(locator, sec)
|
|
1704
|
-
|
|
1777
|
+
|
|
1778
|
+
// For mobile native apps, use safe isDisplayed wrapper
|
|
1779
|
+
const parsedLocator = parseLocator.call(this, locator)
|
|
1780
|
+
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
1781
|
+
|
|
1782
|
+
return this.browser.waitUntil(
|
|
1783
|
+
async () => {
|
|
1784
|
+
const res = await this._res(parsedLocator)
|
|
1785
|
+
if (!res || res.length === 0) return false
|
|
1786
|
+
|
|
1787
|
+
const selected = []
|
|
1788
|
+
for (const el of res) {
|
|
1789
|
+
const displayed = await this._isDisplayedSafe(el)
|
|
1790
|
+
if (displayed) selected.push(true)
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
return selected.length > 0
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
timeout: aSec * 1000,
|
|
1797
|
+
timeoutMsg: `element (${Locator.build(parsedLocator)}) still not visible after ${aSec} sec`,
|
|
1798
|
+
},
|
|
1799
|
+
)
|
|
1705
1800
|
}
|
|
1706
1801
|
|
|
1707
1802
|
/**
|
|
@@ -1710,7 +1805,26 @@ class Appium extends Webdriver {
|
|
|
1710
1805
|
*/
|
|
1711
1806
|
async waitForInvisible(locator, sec = null) {
|
|
1712
1807
|
if (this.isWeb) return super.waitForInvisible(locator, sec)
|
|
1713
|
-
|
|
1808
|
+
|
|
1809
|
+
// For mobile native apps, use safe isDisplayed wrapper
|
|
1810
|
+
const parsedLocator = parseLocator.call(this, locator)
|
|
1811
|
+
const aSec = sec || this.options.waitForTimeoutInSeconds
|
|
1812
|
+
|
|
1813
|
+
return this.browser.waitUntil(
|
|
1814
|
+
async () => {
|
|
1815
|
+
const res = await this._res(parsedLocator)
|
|
1816
|
+
if (!res || res.length === 0) return true
|
|
1817
|
+
|
|
1818
|
+
const selected = []
|
|
1819
|
+
for (const el of res) {
|
|
1820
|
+
const displayed = await this._isDisplayedSafe(el)
|
|
1821
|
+
if (displayed) selected.push(true)
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
return selected.length === 0
|
|
1825
|
+
},
|
|
1826
|
+
{ timeout: aSec * 1000, timeoutMsg: `element (${Locator.build(parsedLocator)}) still visible after ${aSec} sec` },
|
|
1827
|
+
)
|
|
1714
1828
|
}
|
|
1715
1829
|
|
|
1716
1830
|
/**
|
|
@@ -1786,4 +1900,4 @@ function onlyForApps(expectedPlatform) {
|
|
|
1786
1900
|
}
|
|
1787
1901
|
}
|
|
1788
1902
|
|
|
1789
|
-
|
|
1903
|
+
export default Appium
|
package/lib/helper/GraphQL.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import HelperModule from '@codeceptjs/helper'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* GraphQL helper allows to send additional requests to a GraphQl endpoint during acceptance tests.
|
|
@@ -227,4 +227,4 @@ class GraphQL extends Helper {
|
|
|
227
227
|
this.haveRequestHeaders({ Authorization: `Bearer ${accessToken}` })
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
export default GraphQL
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import path from 'path'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import HelperModule from '@codeceptjs/helper'
|
|
4
|
+
import GraphQL from './GraphQL.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Helper for managing remote data using GraphQL queries.
|
|
@@ -305,4 +305,4 @@ class GraphQLDataFactory extends Helper {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
export default GraphQLDataFactory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Helper from '@codeceptjs/helper'
|
|
2
2
|
import assert from 'assert'
|
|
3
|
-
import
|
|
3
|
+
import { z } from 'zod'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* This helper allows performing assertions on JSON responses paired with following helpers:
|
|
@@ -69,17 +69,13 @@ class JSONResponse extends Helper {
|
|
|
69
69
|
|
|
70
70
|
_beforeSuite() {
|
|
71
71
|
this.response = null
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
} catch (e) {
|
|
81
|
-
// Temporary workaround for ESM transition - helpers access issue
|
|
82
|
-
console.log('[JSONResponse] Warning: Could not connect to REST helper during ESM transition:', e.message)
|
|
72
|
+
if (!this.helpers[this.options.requestHelper]) {
|
|
73
|
+
throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
|
|
74
|
+
}
|
|
75
|
+
const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse
|
|
76
|
+
this.helpers[this.options.requestHelper].config.onResponse = response => {
|
|
77
|
+
this.response = response
|
|
78
|
+
if (typeof origOnResponse === 'function') origOnResponse(response)
|
|
83
79
|
}
|
|
84
80
|
}
|
|
85
81
|
|
|
@@ -87,16 +83,7 @@ class JSONResponse extends Helper {
|
|
|
87
83
|
this.response = null
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
// In ESM, joi is already imported at the top
|
|
93
|
-
// The import will fail at module load time if joi is missing
|
|
94
|
-
return null
|
|
95
|
-
} catch (e) {
|
|
96
|
-
return ['joi']
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
86
|
+
|
|
100
87
|
/**
|
|
101
88
|
* Checks that response code is equal to the provided one
|
|
102
89
|
*
|
|
@@ -308,28 +295,28 @@ class JSONResponse extends Helper {
|
|
|
308
295
|
}
|
|
309
296
|
|
|
310
297
|
/**
|
|
311
|
-
* Validates JSON structure of response using [
|
|
312
|
-
* See [
|
|
298
|
+
* Validates JSON structure of response using [Zod library](https://zod.dev).
|
|
299
|
+
* See [Zod API](https://zod.dev/) for complete reference on usage.
|
|
313
300
|
*
|
|
314
|
-
* Use pre-initialized
|
|
301
|
+
* Use pre-initialized Zod instance by passing function callback:
|
|
315
302
|
*
|
|
316
303
|
* ```js
|
|
317
304
|
* // response.data is { name: 'jon', id: 1 }
|
|
318
305
|
*
|
|
319
|
-
* I.seeResponseMatchesJsonSchema(
|
|
320
|
-
* return
|
|
321
|
-
* name:
|
|
322
|
-
* id:
|
|
306
|
+
* I.seeResponseMatchesJsonSchema(z => {
|
|
307
|
+
* return z.object({
|
|
308
|
+
* name: z.string(),
|
|
309
|
+
* id: z.number()
|
|
323
310
|
* })
|
|
324
311
|
* });
|
|
325
312
|
*
|
|
326
313
|
* // or pass a valid schema
|
|
327
|
-
*
|
|
314
|
+
* import { z } from 'zod';
|
|
328
315
|
*
|
|
329
|
-
* I.seeResponseMatchesJsonSchema(
|
|
330
|
-
* name:
|
|
331
|
-
* id:
|
|
332
|
-
* });
|
|
316
|
+
* I.seeResponseMatchesJsonSchema(z.object({
|
|
317
|
+
* name: z.string(),
|
|
318
|
+
* id: z.number()
|
|
319
|
+
* }));
|
|
333
320
|
* ```
|
|
334
321
|
*
|
|
335
322
|
* @param {any} fnOrSchema
|
|
@@ -338,14 +325,17 @@ class JSONResponse extends Helper {
|
|
|
338
325
|
this._checkResponseReady()
|
|
339
326
|
let schema = fnOrSchema
|
|
340
327
|
if (typeof fnOrSchema === 'function') {
|
|
341
|
-
schema = fnOrSchema(
|
|
328
|
+
schema = fnOrSchema(z)
|
|
342
329
|
const body = fnOrSchema.toString()
|
|
343
330
|
fnOrSchema.toString = () => `${body.split('\n')[1]}...`
|
|
344
331
|
}
|
|
345
|
-
if (!schema) throw new Error('Empty
|
|
346
|
-
if (!
|
|
347
|
-
schema.toString = () => schema.
|
|
348
|
-
|
|
332
|
+
if (!schema) throw new Error('Empty Zod schema provided, see https://zod.dev/ for details')
|
|
333
|
+
if (!(schema instanceof z.ZodType)) throw new Error('Invalid Zod schema provided, see https://zod.dev/ for details')
|
|
334
|
+
schema.toString = () => schema._def.description || JSON.stringify(schema._def)
|
|
335
|
+
const result = schema.parse(this.response.data)
|
|
336
|
+
if (!result) {
|
|
337
|
+
throw new Error('Schema validation failed')
|
|
338
|
+
}
|
|
349
339
|
}
|
|
350
340
|
|
|
351
341
|
_checkResponseReady() {
|
|
@@ -356,7 +346,25 @@ class JSONResponse extends Helper {
|
|
|
356
346
|
for (const key in expected) {
|
|
357
347
|
assert(key in actual, `Key "${key}" not found in ${JSON.stringify(actual)}`)
|
|
358
348
|
if (typeof expected[key] === 'object' && expected[key] !== null) {
|
|
359
|
-
|
|
349
|
+
if (Array.isArray(expected[key])) {
|
|
350
|
+
// Handle array comparison: each expected element should have a match in actual array
|
|
351
|
+
assert(Array.isArray(actual[key]), `Expected array for key "${key}", but got ${typeof actual[key]}`)
|
|
352
|
+
for (const expectedItem of expected[key]) {
|
|
353
|
+
let found = false
|
|
354
|
+
for (const actualItem of actual[key]) {
|
|
355
|
+
try {
|
|
356
|
+
this._assertContains(actualItem, expectedItem)
|
|
357
|
+
found = true
|
|
358
|
+
break
|
|
359
|
+
} catch (err) {
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
assert(found, `No matching element found in array for ${JSON.stringify(expectedItem)}`)
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
this._assertContains(actual[key], expected[key])
|
|
367
|
+
}
|
|
360
368
|
} else {
|
|
361
369
|
assert.deepStrictEqual(actual[key], expected[key], `Values for key "${key}" don't match`)
|
|
362
370
|
}
|
|
@@ -40,7 +40,20 @@ class Mochawesome extends Helper {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
_test(test) {
|
|
43
|
-
|
|
43
|
+
// If this is a retried test, we want to add context to the retried test
|
|
44
|
+
// but also potentially preserve context from the original test
|
|
45
|
+
const originalTest = test.retriedTest && test.retriedTest()
|
|
46
|
+
if (originalTest) {
|
|
47
|
+
// This is a retried test - use the retried test for context
|
|
48
|
+
currentTest = { test }
|
|
49
|
+
|
|
50
|
+
// Optionally copy context from original test if it exists
|
|
51
|
+
// Note: mochawesome context is stored in test.ctx, but we need to be careful
|
|
52
|
+
// not to break the mocha context structure
|
|
53
|
+
} else {
|
|
54
|
+
// Normal test (not a retry)
|
|
55
|
+
currentTest = { test }
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
_failed(test) {
|
|
@@ -67,7 +80,16 @@ class Mochawesome extends Helper {
|
|
|
67
80
|
|
|
68
81
|
addMochawesomeContext(context) {
|
|
69
82
|
if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
|
|
70
|
-
|
|
83
|
+
|
|
84
|
+
// For retried tests, make sure we're adding context to the current (retried) test
|
|
85
|
+
// not the original test
|
|
86
|
+
let targetTest = currentTest
|
|
87
|
+
if (currentTest.test && currentTest.test.retriedTest && currentTest.test.retriedTest()) {
|
|
88
|
+
// This test has been retried, make sure we're using the current test for context
|
|
89
|
+
targetTest = { test: currentTest.test }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this._addContext(targetTest, context)
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
95
|
|