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
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -38,6 +38,7 @@ const { highlightElement } = require('./scripts/highlightElement')
|
|
|
38
38
|
const { blurElement } = require('./scripts/blurElement')
|
|
39
39
|
const { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion')
|
|
40
40
|
const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
|
|
41
|
+
const WebElement = require('../element/WebElement')
|
|
41
42
|
|
|
42
43
|
let puppeteer
|
|
43
44
|
let perfTiming
|
|
@@ -633,9 +634,11 @@ class Puppeteer extends Helper {
|
|
|
633
634
|
return
|
|
634
635
|
}
|
|
635
636
|
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
637
|
+
const el = await this._locateElement(locator)
|
|
638
|
+
if (!el) {
|
|
639
|
+
throw new ElementNotFound(locator, 'Element for within context')
|
|
640
|
+
}
|
|
641
|
+
this.context = el
|
|
639
642
|
|
|
640
643
|
this.withinLocator = new Locator(locator)
|
|
641
644
|
}
|
|
@@ -729,11 +732,13 @@ class Puppeteer extends Helper {
|
|
|
729
732
|
* {{ react }}
|
|
730
733
|
*/
|
|
731
734
|
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
|
|
732
|
-
const
|
|
733
|
-
|
|
735
|
+
const el = await this._locateElement(locator)
|
|
736
|
+
if (!el) {
|
|
737
|
+
throw new ElementNotFound(locator, 'Element to move cursor to')
|
|
738
|
+
}
|
|
734
739
|
|
|
735
740
|
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
|
|
736
|
-
const { x, y } = await getClickablePoint(
|
|
741
|
+
const { x, y } = await getClickablePoint(el)
|
|
737
742
|
await this.page.mouse.move(x + offsetX, y + offsetY)
|
|
738
743
|
return this._waitForAction()
|
|
739
744
|
}
|
|
@@ -743,9 +748,10 @@ class Puppeteer extends Helper {
|
|
|
743
748
|
*
|
|
744
749
|
*/
|
|
745
750
|
async focus(locator) {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
751
|
+
const el = await this._locateElement(locator)
|
|
752
|
+
if (!el) {
|
|
753
|
+
throw new ElementNotFound(locator, 'Element to focus')
|
|
754
|
+
}
|
|
749
755
|
|
|
750
756
|
await el.click()
|
|
751
757
|
await el.focus()
|
|
@@ -757,10 +763,12 @@ class Puppeteer extends Helper {
|
|
|
757
763
|
*
|
|
758
764
|
*/
|
|
759
765
|
async blur(locator) {
|
|
760
|
-
const
|
|
761
|
-
|
|
766
|
+
const el = await this._locateElement(locator)
|
|
767
|
+
if (!el) {
|
|
768
|
+
throw new ElementNotFound(locator, 'Element to blur')
|
|
769
|
+
}
|
|
762
770
|
|
|
763
|
-
await blurElement(
|
|
771
|
+
await blurElement(el, this.page)
|
|
764
772
|
return this._waitForAction()
|
|
765
773
|
}
|
|
766
774
|
|
|
@@ -809,11 +817,12 @@ class Puppeteer extends Helper {
|
|
|
809
817
|
}
|
|
810
818
|
|
|
811
819
|
if (locator) {
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
820
|
+
const el = await this._locateElement(locator)
|
|
821
|
+
if (!el) {
|
|
822
|
+
throw new ElementNotFound(locator, 'Element to scroll into view')
|
|
823
|
+
}
|
|
815
824
|
await el.evaluate(el => el.scrollIntoView())
|
|
816
|
-
const elementCoordinates = await getClickablePoint(
|
|
825
|
+
const elementCoordinates = await getClickablePoint(el)
|
|
817
826
|
await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
|
|
818
827
|
} else {
|
|
819
828
|
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
|
|
@@ -881,6 +890,21 @@ class Puppeteer extends Helper {
|
|
|
881
890
|
return findElements.call(this, context, locator)
|
|
882
891
|
}
|
|
883
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Get single element by different locator types, including strict locator
|
|
895
|
+
* Should be used in custom helpers:
|
|
896
|
+
*
|
|
897
|
+
* ```js
|
|
898
|
+
* const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
|
|
899
|
+
* ```
|
|
900
|
+
*
|
|
901
|
+
* {{ react }}
|
|
902
|
+
*/
|
|
903
|
+
async _locateElement(locator) {
|
|
904
|
+
const context = await this.context
|
|
905
|
+
return findElement.call(this, context, locator)
|
|
906
|
+
}
|
|
907
|
+
|
|
884
908
|
/**
|
|
885
909
|
* Find a checkbox by providing human-readable text:
|
|
886
910
|
* NOTE: Assumes the checkable element exists
|
|
@@ -892,7 +916,9 @@ class Puppeteer extends Helper {
|
|
|
892
916
|
async _locateCheckable(locator, providedContext = null) {
|
|
893
917
|
const context = providedContext || (await this._getContext())
|
|
894
918
|
const els = await findCheckable.call(this, locator, context)
|
|
895
|
-
|
|
919
|
+
if (!els || els.length === 0) {
|
|
920
|
+
throw new ElementNotFound(locator, 'Checkbox or radio')
|
|
921
|
+
}
|
|
896
922
|
return els[0]
|
|
897
923
|
}
|
|
898
924
|
|
|
@@ -924,7 +950,20 @@ class Puppeteer extends Helper {
|
|
|
924
950
|
*
|
|
925
951
|
*/
|
|
926
952
|
async grabWebElements(locator) {
|
|
927
|
-
|
|
953
|
+
const elements = await this._locate(locator)
|
|
954
|
+
return elements.map(element => new WebElement(element, this))
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* {{> grabWebElement }}
|
|
959
|
+
*
|
|
960
|
+
*/
|
|
961
|
+
async grabWebElement(locator) {
|
|
962
|
+
const elements = await this._locate(locator)
|
|
963
|
+
if (elements.length === 0) {
|
|
964
|
+
throw new ElementNotFound(locator, 'Element')
|
|
965
|
+
}
|
|
966
|
+
return new WebElement(elements[0], this)
|
|
928
967
|
}
|
|
929
968
|
|
|
930
969
|
/**
|
|
@@ -2110,10 +2149,12 @@ class Puppeteer extends Helper {
|
|
|
2110
2149
|
* {{> waitForClickable }}
|
|
2111
2150
|
*/
|
|
2112
2151
|
async waitForClickable(locator, waitTimeout) {
|
|
2113
|
-
const
|
|
2114
|
-
|
|
2152
|
+
const el = await this._locateElement(locator)
|
|
2153
|
+
if (!el) {
|
|
2154
|
+
throw new ElementNotFound(locator, 'Element to wait for clickable')
|
|
2155
|
+
}
|
|
2115
2156
|
|
|
2116
|
-
return this.waitForFunction(isElementClickable, [
|
|
2157
|
+
return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
|
|
2117
2158
|
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
|
|
2118
2159
|
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
|
|
2119
2160
|
} else {
|
|
@@ -2687,9 +2728,18 @@ class Puppeteer extends Helper {
|
|
|
2687
2728
|
|
|
2688
2729
|
module.exports = Puppeteer
|
|
2689
2730
|
|
|
2731
|
+
/**
|
|
2732
|
+
* Find elements using Puppeteer's native element discovery methods
|
|
2733
|
+
* Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
|
|
2734
|
+
* @param {Object} matcher - Puppeteer context to search within
|
|
2735
|
+
* @param {Object|string} locator - Locator specification
|
|
2736
|
+
* @returns {Promise<Array>} Array of ElementHandle objects
|
|
2737
|
+
*/
|
|
2690
2738
|
async function findElements(matcher, locator) {
|
|
2691
2739
|
if (locator.react) return findReactElements.call(this, locator)
|
|
2692
2740
|
locator = new Locator(locator, 'css')
|
|
2741
|
+
|
|
2742
|
+
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
|
|
2693
2743
|
if (!locator.isXPath()) return matcher.$$(locator.simplify())
|
|
2694
2744
|
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
2695
2745
|
if (puppeteer.default?.defaultBrowserRevision) {
|
|
@@ -2698,6 +2748,31 @@ async function findElements(matcher, locator) {
|
|
|
2698
2748
|
return matcher.$x(locator.value)
|
|
2699
2749
|
}
|
|
2700
2750
|
|
|
2751
|
+
/**
|
|
2752
|
+
* Find a single element using Puppeteer's native element discovery methods
|
|
2753
|
+
* Note: Puppeteer Locator API doesn't have .first() method like Playwright
|
|
2754
|
+
* @param {Object} matcher - Puppeteer context to search within
|
|
2755
|
+
* @param {Object|string} locator - Locator specification
|
|
2756
|
+
* @returns {Promise<Object>} Single ElementHandle object
|
|
2757
|
+
*/
|
|
2758
|
+
async function findElement(matcher, locator) {
|
|
2759
|
+
if (locator.react) return findReactElements.call(this, locator)
|
|
2760
|
+
locator = new Locator(locator, 'css')
|
|
2761
|
+
|
|
2762
|
+
// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
|
|
2763
|
+
if (!locator.isXPath()) {
|
|
2764
|
+
const elements = await matcher.$$(locator.simplify())
|
|
2765
|
+
return elements[0]
|
|
2766
|
+
}
|
|
2767
|
+
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
|
|
2768
|
+
if (puppeteer.default?.defaultBrowserRevision) {
|
|
2769
|
+
const elements = await matcher.$$(`xpath/${locator.value}`)
|
|
2770
|
+
return elements[0]
|
|
2771
|
+
}
|
|
2772
|
+
const elements = await matcher.$x(locator.value)
|
|
2773
|
+
return elements[0]
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2701
2776
|
async function proceedClick(locator, context = null, options = {}) {
|
|
2702
2777
|
let matcher = await this.context
|
|
2703
2778
|
if (context) {
|
|
@@ -2843,15 +2918,19 @@ async function findFields(locator) {
|
|
|
2843
2918
|
}
|
|
2844
2919
|
|
|
2845
2920
|
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
|
|
2846
|
-
const src = await this.
|
|
2847
|
-
|
|
2921
|
+
const src = await this._locateElement(sourceLocator)
|
|
2922
|
+
if (!src) {
|
|
2923
|
+
throw new ElementNotFound(sourceLocator, 'Source Element')
|
|
2924
|
+
}
|
|
2848
2925
|
|
|
2849
|
-
const dst = await this.
|
|
2850
|
-
|
|
2926
|
+
const dst = await this._locateElement(destinationLocator)
|
|
2927
|
+
if (!dst) {
|
|
2928
|
+
throw new ElementNotFound(destinationLocator, 'Destination Element')
|
|
2929
|
+
}
|
|
2851
2930
|
|
|
2852
|
-
// Note: Using public api .getClickablePoint
|
|
2853
|
-
const dragSource = await getClickablePoint(src
|
|
2854
|
-
const dragDestination = await getClickablePoint(dst
|
|
2931
|
+
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
|
|
2932
|
+
const dragSource = await getClickablePoint(src)
|
|
2933
|
+
const dragDestination = await getClickablePoint(dst)
|
|
2855
2934
|
|
|
2856
2935
|
// Drag start point
|
|
2857
2936
|
await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -21,6 +21,7 @@ const { focusElement } = require('./scripts/focusElement')
|
|
|
21
21
|
const { blurElement } = require('./scripts/blurElement')
|
|
22
22
|
const { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } = require('./errors/ElementAssertion')
|
|
23
23
|
const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions')
|
|
24
|
+
const WebElement = require('../element/WebElement')
|
|
24
25
|
|
|
25
26
|
const SHADOW = 'shadow'
|
|
26
27
|
const webRoot = 'body'
|
|
@@ -936,7 +937,20 @@ class WebDriver extends Helper {
|
|
|
936
937
|
*
|
|
937
938
|
*/
|
|
938
939
|
async grabWebElements(locator) {
|
|
939
|
-
|
|
940
|
+
const elements = await this._locate(locator)
|
|
941
|
+
return elements.map(element => new WebElement(element, this))
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* {{> grabWebElement }}
|
|
946
|
+
*
|
|
947
|
+
*/
|
|
948
|
+
async grabWebElement(locator) {
|
|
949
|
+
const elements = await this._locate(locator)
|
|
950
|
+
if (elements.length === 0) {
|
|
951
|
+
throw new ElementNotFound(locator, 'Element')
|
|
952
|
+
}
|
|
953
|
+
return new WebElement(elements[0], this)
|
|
940
954
|
}
|
|
941
955
|
|
|
942
956
|
/**
|
|
@@ -981,7 +995,7 @@ class WebDriver extends Helper {
|
|
|
981
995
|
* {{ react }}
|
|
982
996
|
*/
|
|
983
997
|
async click(locator, context = null) {
|
|
984
|
-
|
|
998
|
+
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
985
999
|
const locateFn = prepareLocateFn.call(this, context)
|
|
986
1000
|
|
|
987
1001
|
const res = await findClickable.call(this, locator, locateFn)
|
|
@@ -1200,7 +1214,7 @@ class WebDriver extends Helper {
|
|
|
1200
1214
|
* {{> checkOption }}
|
|
1201
1215
|
*/
|
|
1202
1216
|
async checkOption(field, context = null) {
|
|
1203
|
-
|
|
1217
|
+
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
1204
1218
|
const locateFn = prepareLocateFn.call(this, context)
|
|
1205
1219
|
|
|
1206
1220
|
const res = await findCheckable.call(this, field, locateFn)
|
|
@@ -1220,7 +1234,7 @@ class WebDriver extends Helper {
|
|
|
1220
1234
|
* {{> uncheckOption }}
|
|
1221
1235
|
*/
|
|
1222
1236
|
async uncheckOption(field, context = null) {
|
|
1223
|
-
|
|
1237
|
+
const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
|
|
1224
1238
|
const locateFn = prepareLocateFn.call(this, context)
|
|
1225
1239
|
|
|
1226
1240
|
const res = await findCheckable.call(this, field, locateFn)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const event = require('../event')
|
|
2
|
+
const { enhanceMochaTest } = require('../mocha/test')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Enhance retried tests by copying CodeceptJS-specific properties from the original test
|
|
6
|
+
* This fixes the issue where Mocha's shallow clone during retries loses CodeceptJS properties
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function () {
|
|
9
|
+
event.dispatcher.on(event.test.before, test => {
|
|
10
|
+
// Check if this test is a retry (has a reference to the original test)
|
|
11
|
+
const originalTest = test.retriedTest && test.retriedTest()
|
|
12
|
+
|
|
13
|
+
if (originalTest) {
|
|
14
|
+
// This is a retried test - copy CodeceptJS-specific properties from the original
|
|
15
|
+
copyCodeceptJSProperties(originalTest, test)
|
|
16
|
+
|
|
17
|
+
// Ensure the test is enhanced with CodeceptJS functionality
|
|
18
|
+
enhanceMochaTest(test)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Copy CodeceptJS-specific properties from the original test to the retried test
|
|
25
|
+
* @param {CodeceptJS.Test} originalTest - The original test object
|
|
26
|
+
* @param {CodeceptJS.Test} retriedTest - The retried test object
|
|
27
|
+
*/
|
|
28
|
+
function copyCodeceptJSProperties(originalTest, retriedTest) {
|
|
29
|
+
// Copy CodeceptJS-specific properties
|
|
30
|
+
if (originalTest.opts !== undefined) {
|
|
31
|
+
retriedTest.opts = originalTest.opts ? { ...originalTest.opts } : {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (originalTest.tags !== undefined) {
|
|
35
|
+
retriedTest.tags = originalTest.tags ? [...originalTest.tags] : []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (originalTest.notes !== undefined) {
|
|
39
|
+
retriedTest.notes = originalTest.notes ? [...originalTest.notes] : []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (originalTest.meta !== undefined) {
|
|
43
|
+
retriedTest.meta = originalTest.meta ? { ...originalTest.meta } : {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (originalTest.artifacts !== undefined) {
|
|
47
|
+
retriedTest.artifacts = originalTest.artifacts ? [...originalTest.artifacts] : []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (originalTest.steps !== undefined) {
|
|
51
|
+
retriedTest.steps = originalTest.steps ? [...originalTest.steps] : []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (originalTest.config !== undefined) {
|
|
55
|
+
retriedTest.config = originalTest.config ? { ...originalTest.config } : {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (originalTest.inject !== undefined) {
|
|
59
|
+
retriedTest.inject = originalTest.inject ? { ...originalTest.inject } : {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Copy methods that might be missing
|
|
63
|
+
if (originalTest.addNote && !retriedTest.addNote) {
|
|
64
|
+
retriedTest.addNote = function (type, note) {
|
|
65
|
+
this.notes = this.notes || []
|
|
66
|
+
this.notes.push({ type, text: note })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (originalTest.applyOptions && !retriedTest.applyOptions) {
|
|
71
|
+
retriedTest.applyOptions = originalTest.applyOptions.bind(retriedTest)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (originalTest.simplify && !retriedTest.simplify) {
|
|
75
|
+
retriedTest.simplify = originalTest.simplify.bind(retriedTest)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Preserve the uid if it exists
|
|
79
|
+
if (originalTest.uid !== undefined) {
|
|
80
|
+
retriedTest.uid = originalTest.uid
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mark as enhanced
|
|
84
|
+
retriedTest.codeceptjs = true
|
|
85
|
+
}
|
package/lib/listener/steps.js
CHANGED
|
@@ -3,10 +3,14 @@ const event = require('../event')
|
|
|
3
3
|
const store = require('../store')
|
|
4
4
|
const output = require('../output')
|
|
5
5
|
const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks')
|
|
6
|
+
const recorder = require('../recorder')
|
|
6
7
|
|
|
7
8
|
let currentTest
|
|
8
9
|
let currentHook
|
|
9
10
|
|
|
11
|
+
// Session names that should not contribute steps to the main test trace
|
|
12
|
+
const EXCLUDED_SESSIONS = ['tryTo', 'hopeThat']
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Register steps inside tests
|
|
12
16
|
*/
|
|
@@ -75,6 +79,14 @@ module.exports = function () {
|
|
|
75
79
|
return currentHook.steps.push(step)
|
|
76
80
|
}
|
|
77
81
|
if (!currentTest || !currentTest.steps) return
|
|
82
|
+
|
|
83
|
+
// Check if we're in a session that should be excluded from main test steps
|
|
84
|
+
const currentSessionId = recorder.getCurrentSessionId()
|
|
85
|
+
if (currentSessionId && EXCLUDED_SESSIONS.includes(currentSessionId)) {
|
|
86
|
+
// Skip adding this step to the main test steps
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
currentTest.steps.push(step)
|
|
79
91
|
})
|
|
80
92
|
|
|
@@ -121,9 +121,19 @@ module.exports.injected = function (fn, suite, hookName) {
|
|
|
121
121
|
const errHandler = err => {
|
|
122
122
|
recorder.session.start('teardown')
|
|
123
123
|
recorder.cleanAsyncErr()
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
if (['before', 'beforeSuite'].includes(hookName)) {
|
|
125
|
+
suiteTestFailedHookError(suite, err, hookName)
|
|
126
|
+
}
|
|
127
|
+
if (hookName === 'after') {
|
|
128
|
+
suiteTestFailedHookError(suite, err, hookName)
|
|
129
|
+
suite.eachTest(test => {
|
|
130
|
+
event.emit(event.test.after, test)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
if (hookName === 'afterSuite') {
|
|
134
|
+
suiteTestFailedHookError(suite, err, hookName)
|
|
135
|
+
event.emit(event.suite.after, suite)
|
|
136
|
+
}
|
|
127
137
|
recorder.add(() => doneFn(err))
|
|
128
138
|
}
|
|
129
139
|
|
package/lib/mocha/cli.js
CHANGED
|
@@ -200,7 +200,7 @@ class Cli extends Base {
|
|
|
200
200
|
|
|
201
201
|
// explicitly show file with error
|
|
202
202
|
if (test.file) {
|
|
203
|
-
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')}
|
|
203
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} file://${test.file}\n`
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
const steps = test.steps || (test.ctx && test.ctx.test.steps)
|
package/lib/mocha/gherkin.js
CHANGED
|
@@ -107,7 +107,7 @@ module.exports = (text, file) => {
|
|
|
107
107
|
)
|
|
108
108
|
continue
|
|
109
109
|
}
|
|
110
|
-
if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline
|
|
110
|
+
if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline === child.scenario.keyword : child.scenario.keyword === 'Scenario Outline')) {
|
|
111
111
|
for (const examples of child.scenario.examples) {
|
|
112
112
|
const fields = examples.tableHeader.cells.map(c => c.value)
|
|
113
113
|
for (const example of examples.tableBody) {
|
package/lib/mocha/test.js
CHANGED
|
@@ -77,6 +77,12 @@ function deserializeTest(test) {
|
|
|
77
77
|
test.parent = Object.assign(new Suite(test.parent?.title || 'Suite'), test.parent)
|
|
78
78
|
enhanceMochaSuite(test.parent)
|
|
79
79
|
if (test.steps) test.steps = test.steps.map(step => Object.assign(new Step(step.title), step))
|
|
80
|
+
|
|
81
|
+
// Restore the custom fullTitle function to maintain consistency with original test
|
|
82
|
+
if (test.parent) {
|
|
83
|
+
test.fullTitle = () => `${test.parent.title}: ${test.title}`
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
return test
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -135,9 +141,19 @@ function cloneTest(test) {
|
|
|
135
141
|
return deserializeTest(serializeTest(test))
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Get a filename from the test object
|
|
146
|
+
* @param {CodeceptJS.Test} test
|
|
147
|
+
* @param {Object} options
|
|
148
|
+
* @param {string} options.suffix Add a suffix to the filename
|
|
149
|
+
* @param {boolean} options.unique Add a unique suffix to the file
|
|
150
|
+
*
|
|
151
|
+
* @returns {string} the filename
|
|
152
|
+
*/
|
|
153
|
+
function testToFileName(test, { suffix = '', unique = false } = {}) {
|
|
139
154
|
let fileName = test.title
|
|
140
155
|
|
|
156
|
+
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
|
|
141
157
|
if (suffix) fileName = `${fileName}_${suffix}`
|
|
142
158
|
// remove tags with empty string (disable for now)
|
|
143
159
|
// fileName = fileName.replace(/\@\w+/g, '')
|
|
@@ -151,6 +167,7 @@ function testToFileName(test, suffix = '') {
|
|
|
151
167
|
// fileName = `${clearString(test.parent.title)}_${fileName}`
|
|
152
168
|
// }
|
|
153
169
|
fileName = clearString(fileName).slice(0, 100)
|
|
170
|
+
|
|
154
171
|
return fileName
|
|
155
172
|
}
|
|
156
173
|
|
package/lib/mocha/ui.js
CHANGED
|
@@ -103,6 +103,19 @@ module.exports = function (suite) {
|
|
|
103
103
|
return new FeatureConfig(suite)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Exclusive test suite - runs only this feature.
|
|
108
|
+
* @global
|
|
109
|
+
* @kind constant
|
|
110
|
+
* @type {CodeceptJS.IFeature}
|
|
111
|
+
*/
|
|
112
|
+
context.Feature.only = function (title, opts) {
|
|
113
|
+
const reString = `^${escapeRe(`${title}:`)}`
|
|
114
|
+
mocha.grep(new RegExp(reString))
|
|
115
|
+
process.env.FEATURE_ONLY = true
|
|
116
|
+
return context.Feature(title, opts)
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
/**
|
|
107
120
|
* Pending test suite.
|
|
108
121
|
* @global
|
package/lib/output.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const colors = require('chalk')
|
|
2
2
|
const figures = require('figures')
|
|
3
|
-
const {
|
|
3
|
+
const { maskData, shouldMaskData, getMaskConfig } = require('./utils/mask_data')
|
|
4
4
|
|
|
5
5
|
const styles = {
|
|
6
6
|
error: colors.bgRed.white.bold,
|
|
@@ -59,7 +59,7 @@ module.exports = {
|
|
|
59
59
|
* @param {string} msg
|
|
60
60
|
*/
|
|
61
61
|
debug(msg) {
|
|
62
|
-
const _msg =
|
|
62
|
+
const _msg = shouldMaskData() ? maskData(msg, getMaskConfig()) : msg
|
|
63
63
|
if (outputLevel >= 2) {
|
|
64
64
|
print(' '.repeat(this.stepShift), styles.debug(`${figures.pointerSmall} ${_msg}`))
|
|
65
65
|
}
|
|
@@ -70,7 +70,7 @@ module.exports = {
|
|
|
70
70
|
* @param {string} msg
|
|
71
71
|
*/
|
|
72
72
|
log(msg) {
|
|
73
|
-
const _msg =
|
|
73
|
+
const _msg = shouldMaskData() ? maskData(msg, getMaskConfig()) : msg
|
|
74
74
|
if (outputLevel >= 3) {
|
|
75
75
|
print(' '.repeat(this.stepShift), styles.log(truncate(` ${_msg}`, this.spaceShift)))
|
|
76
76
|
}
|
|
@@ -81,7 +81,8 @@ module.exports = {
|
|
|
81
81
|
* @param {string} msg
|
|
82
82
|
*/
|
|
83
83
|
error(msg) {
|
|
84
|
-
|
|
84
|
+
const _msg = shouldMaskData() ? maskData(msg, getMaskConfig()) : msg
|
|
85
|
+
print(styles.error(_msg))
|
|
85
86
|
},
|
|
86
87
|
|
|
87
88
|
/**
|
|
@@ -89,7 +90,8 @@ module.exports = {
|
|
|
89
90
|
* @param {string} msg
|
|
90
91
|
*/
|
|
91
92
|
success(msg) {
|
|
92
|
-
|
|
93
|
+
const _msg = shouldMaskData() ? maskData(msg, getMaskConfig()) : msg
|
|
94
|
+
print(styles.success(_msg))
|
|
93
95
|
},
|
|
94
96
|
|
|
95
97
|
/**
|
|
@@ -124,7 +126,7 @@ module.exports = {
|
|
|
124
126
|
stepLine += colors.grey(step.comment.split('\n').join('\n' + ' '.repeat(4)))
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
const _stepLine =
|
|
129
|
+
const _stepLine = shouldMaskData() ? maskData(stepLine, getMaskConfig()) : stepLine
|
|
128
130
|
print(' '.repeat(this.stepShift), truncate(_stepLine, this.spaceShift))
|
|
129
131
|
},
|
|
130
132
|
|
|
@@ -278,7 +280,3 @@ function truncate(msg, gap = 0) {
|
|
|
278
280
|
}
|
|
279
281
|
return msg
|
|
280
282
|
}
|
|
281
|
-
|
|
282
|
-
function isMaskedData() {
|
|
283
|
-
return global.maskSensitiveData === true || false
|
|
284
|
-
}
|
package/lib/pause.js
CHANGED
|
@@ -175,7 +175,12 @@ async function parseInput(cmd) {
|
|
|
175
175
|
output.print(output.styles.success(' OK '), cmd)
|
|
176
176
|
}
|
|
177
177
|
if (cmd?.startsWith('I.grab')) {
|
|
178
|
-
|
|
178
|
+
try {
|
|
179
|
+
output.print(output.styles.debug(JSON.stringify(val, null, 2)))
|
|
180
|
+
} catch (err) {
|
|
181
|
+
output.print(output.styles.error(' ERROR '), 'Failed to stringify result:', err.message)
|
|
182
|
+
output.print(output.styles.error(' RAW VALUE '), String(val))
|
|
183
|
+
}
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
history.push(cmd) // add command to history when successful
|