codeceptjs 3.5.14 → 3.5.15

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.
@@ -59,24 +59,39 @@ function printTests(files) {
59
59
 
60
60
  let numOfTests = 0;
61
61
  let numOfSuites = 0;
62
- const filteredSuites = [];
62
+ let outputString = '';
63
+ const filterBy = process.env.grep ? process.env.grep.toLowerCase() : undefined;
63
64
 
64
- for (const suite of mocha.suite.suites) {
65
- if (process.env.grep && suite.title.toLowerCase().includes(process.env.grep)) {
66
- filteredSuites.push(suite);
65
+ if (filterBy) {
66
+ for (const suite of mocha.suite.suites) {
67
+ const currentSuite = suite.title;
68
+ if (suite.title.toLowerCase().includes(filterBy)) {
69
+ outputString += `${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests\n`;
70
+ numOfSuites++;
71
+ }
72
+
73
+ for (test of suite.tests) {
74
+ if (test.title.toLowerCase().includes(filterBy)) {
75
+ numOfTests++;
76
+ outputString += `${colors.white.bold(test.parent.title)} -- ${output.styles.log(test.parent.file || '')} -- ${mocha.suite.suites.length} tests\n`;
77
+ outputString += ` ${output.styles.scenario(figures.checkboxOff)} ${test.title}\n`;
78
+ }
79
+ }
67
80
  }
68
- }
69
- const displayedSuites = process.env.grep ? filteredSuites : mocha.suite.suites;
70
- for (const suite of displayedSuites) {
71
- output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${suite.tests.length} tests`);
72
- numOfSuites++;
73
-
74
- for (const test of suite.tests) {
75
- numOfTests++;
76
- output.print(` ${output.styles.scenario(figures.checkboxOff)} ${test.title}`);
81
+ numOfSuites = countSuites(outputString);
82
+ } else {
83
+ for (const suite of mocha.suite.suites) {
84
+ output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${mocha.suite.suites.length} tests`);
85
+ numOfSuites++;
86
+
87
+ for (test of suite.tests) {
88
+ numOfTests++;
89
+ output.print(` ${output.styles.scenario(figures.checkboxOff)} ${test.title}`);
90
+ }
77
91
  }
78
92
  }
79
93
 
94
+ output.print(removeDuplicates(outputString));
80
95
  output.print('');
81
96
  output.success(` Total: ${numOfSuites} suites | ${numOfTests} tests `);
82
97
  printFooter();
@@ -87,3 +102,19 @@ function printFooter() {
87
102
  output.print();
88
103
  output.print('--- DRY MODE: No tests were executed ---');
89
104
  }
105
+
106
+ function removeDuplicates(inputString) {
107
+ const array = inputString.split('\n');
108
+ const uniqueLines = [...new Set(array)];
109
+ const resultString = uniqueLines.join('\n');
110
+
111
+ return resultString;
112
+ }
113
+
114
+ function countSuites(inputString) {
115
+ const array = inputString.split('\n');
116
+
117
+ const uniqueLines = [...new Set(array)];
118
+ const res = uniqueLines.filter(item => item.includes('-- '));
119
+ return res.length;
120
+ }
@@ -190,7 +190,7 @@ class ExpectHelper {
190
190
  * @param {*} targetData
191
191
  * @param {*} jsonSchema
192
192
  * @param {*} [customErrorMsg]
193
- * @param {*} ajvOptions Pass AJV options
193
+ * @param {*} [ajvOptions] Pass AJV options
194
194
  */
195
195
  expectJsonSchemaUsingAJV(
196
196
  targetData,
@@ -78,7 +78,7 @@ const consoleLogStore = new Console();
78
78
  * @prop {string} [browser=chrome] - can be changed to `firefox` when using [puppeteer-firefox](https://codecept.io/helpers/Puppeteer-firefox).
79
79
  * @prop {object} [chrome] - pass additional [Puppeteer run options](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#puppeteerlaunchoptions).
80
80
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
81
- */
81
+ */
82
82
  const config = {};
83
83
 
84
84
  /**
@@ -369,7 +369,7 @@ class Puppeteer extends Helper {
369
369
  this.debugSection('Incognito Tab', 'opened');
370
370
  this.activeSessionName = name;
371
371
 
372
- const bc = await this.browser.createIncognitoBrowserContext();
372
+ const bc = await this.browser.createBrowserContext();
373
373
  await bc.newPage();
374
374
 
375
375
  // Create a new page inside context.
@@ -599,14 +599,7 @@ class Puppeteer extends Helper {
599
599
  }
600
600
 
601
601
  async _evaluateHandeInContext(...args) {
602
- let context = await this._getContext();
603
-
604
- if (context.constructor.name === 'Frame') {
605
- // Currently there is no evalateHandle for the Frame object
606
- // https://github.com/GoogleChrome/puppeteer/issues/1051
607
- context = await context.executionContext();
608
- }
609
-
602
+ const context = await this._getContext();
610
603
  return context.evaluateHandle(...args);
611
604
  }
612
605
 
@@ -884,7 +877,8 @@ class Puppeteer extends Helper {
884
877
  * {{ react }}
885
878
  */
886
879
  async _locate(locator) {
887
- return findElements(await this.context, locator);
880
+ const context = await this.context;
881
+ return findElements.call(this, context, locator);
888
882
  }
889
883
 
890
884
  /**
@@ -910,7 +904,7 @@ class Puppeteer extends Helper {
910
904
  * ```
911
905
  */
912
906
  async _locateClickable(locator) {
913
- const context = await this._getContext();
907
+ const context = await this.context;
914
908
  return findClickable.call(this, context, locator);
915
909
  }
916
910
 
@@ -1687,8 +1681,8 @@ class Puppeteer extends Helper {
1687
1681
  * {{> executeScript }}
1688
1682
  */
1689
1683
  async executeScript(...args) {
1690
- let context = this.page;
1691
- if (this.context && this.context.constructor.name === 'Frame') {
1684
+ let context = await this._getContext();
1685
+ if (this.context && this.context.constructor.name === 'CdpFrame') {
1692
1686
  context = this.context; // switching to iframe context
1693
1687
  }
1694
1688
  return context.evaluate.apply(context, args);
@@ -1769,7 +1763,7 @@ class Puppeteer extends Helper {
1769
1763
  */
1770
1764
  async grabHTMLFromAll(locator) {
1771
1765
  const els = await this._locate(locator);
1772
- const values = await Promise.all(els.map(el => el.executionContext().evaluate(element => element.innerHTML, el)));
1766
+ const values = await Promise.all(els.map(el => el.evaluate(element => element.innerHTML, el)));
1773
1767
  return values;
1774
1768
  }
1775
1769
 
@@ -1792,7 +1786,7 @@ class Puppeteer extends Helper {
1792
1786
  */
1793
1787
  async grabCssPropertyFromAll(locator, cssProperty) {
1794
1788
  const els = await this._locate(locator);
1795
- const res = await Promise.all(els.map(el => el.executionContext().evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
1789
+ const res = await Promise.all(els.map(el => el.evaluate(el => JSON.parse(JSON.stringify(getComputedStyle(el))), el)));
1796
1790
  const cssValues = res.map(props => props[toCamelCase(cssProperty)]);
1797
1791
 
1798
1792
  return cssValues;
@@ -1854,35 +1848,34 @@ class Puppeteer extends Helper {
1854
1848
  * {{ react }}
1855
1849
  */
1856
1850
  async seeAttributesOnElements(locator, attributes) {
1857
- const res = await this._locate(locator);
1858
- assertElementExists(res, locator);
1851
+ const elements = await this._locate(locator);
1852
+ assertElementExists(elements, locator);
1859
1853
 
1860
- const elemAmount = res.length;
1861
- const commands = [];
1862
- res.forEach((el) => {
1863
- Object.keys(attributes).forEach((prop) => {
1864
- commands.push(el
1865
- .executionContext()
1866
- .evaluateHandle((el, attr) => el[attr] || el.getAttribute(attr), el, prop)
1867
- .then(el => el.jsonValue()));
1868
- });
1869
- });
1870
- let attrs = await Promise.all(commands);
1871
- const values = Object.keys(attributes).map(key => attributes[key]);
1872
- if (!Array.isArray(attrs)) attrs = [attrs];
1873
- let chunked = chunkArray(attrs, values.length);
1874
- chunked = chunked.filter((val) => {
1875
- for (let i = 0; i < val.length; ++i) {
1876
- const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
1877
- const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
1878
- // the attribute could be a boolean
1879
- if (typeof _actual === 'boolean') return _actual === _expected;
1880
- // if the attribute doesn't exist, returns false as well
1881
- if (!_actual || !_actual.includes(_expected)) return false;
1882
- }
1883
- return true;
1854
+ const expectedAttributes = Object.entries(attributes);
1855
+
1856
+ const valuesPromises = elements.map(async (element) => {
1857
+ const elementAttributes = {};
1858
+ await Promise.all(expectedAttributes.map(async ([attribute, expectedValue]) => {
1859
+ const actualValue = await element.evaluate((el, attr) => el[attr] || el.getAttribute(attr), attribute);
1860
+ elementAttributes[attribute] = actualValue;
1861
+ }));
1862
+ return elementAttributes;
1884
1863
  });
1885
- return equals(`all elements (${(new Locator(locator))}) to have attributes ${JSON.stringify(attributes)}`).assert(chunked.length, elemAmount);
1864
+
1865
+ const actualAttributes = await Promise.all(valuesPromises);
1866
+
1867
+ const matchingElements = actualAttributes.filter((attrs) => expectedAttributes.every(([attribute, expectedValue]) => {
1868
+ const actualValue = attrs[attribute];
1869
+ if (!actualValue) return false;
1870
+ if (actualValue.toString().match(new RegExp(expectedValue.toString()))) return true;
1871
+ return expectedValue === actualValue;
1872
+ }));
1873
+
1874
+ const elementsCount = elements.length;
1875
+ const matchingCount = matchingElements.length;
1876
+
1877
+ return equals(`all elements (${(new Locator(locator))}) to have attributes ${JSON.stringify(attributes)}`)
1878
+ .assert(matchingCount, elementsCount);
1886
1879
  }
1887
1880
 
1888
1881
  /**
@@ -2138,7 +2131,7 @@ class Puppeteer extends Helper {
2138
2131
  if (locator.isCSS()) {
2139
2132
  waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout });
2140
2133
  } else {
2141
- waiter = context.waitForXPath(locator.value, { timeout: waitTimeout });
2134
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout });
2142
2135
  }
2143
2136
  return waiter.catch((err) => {
2144
2137
  throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -2159,7 +2152,7 @@ class Puppeteer extends Helper {
2159
2152
  if (locator.isCSS()) {
2160
2153
  waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, visible: true });
2161
2154
  } else {
2162
- waiter = context.waitForXPath(locator.value, { timeout: waitTimeout, visible: true });
2155
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, visible: true });
2163
2156
  }
2164
2157
  return waiter.catch((err) => {
2165
2158
  throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -2178,7 +2171,7 @@ class Puppeteer extends Helper {
2178
2171
  if (locator.isCSS()) {
2179
2172
  waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
2180
2173
  } else {
2181
- waiter = context.waitForXPath(locator.value, { timeout: waitTimeout, hidden: true });
2174
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true });
2182
2175
  }
2183
2176
  return waiter.catch((err) => {
2184
2177
  throw new Error(`element (${locator.toString()}) still visible after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -2196,7 +2189,7 @@ class Puppeteer extends Helper {
2196
2189
  if (locator.isCSS()) {
2197
2190
  waiter = context.waitForSelector(locator.simplify(), { timeout: waitTimeout, hidden: true });
2198
2191
  } else {
2199
- waiter = context.waitForXPath(locator.value, { timeout: waitTimeout, hidden: true });
2192
+ waiter = _waitForElement.call(this, locator, { timeout: waitTimeout, hidden: true });
2200
2193
  }
2201
2194
  return waiter.catch((err) => {
2202
2195
  throw new Error(`element (${locator.toString()}) still not hidden after ${waitTimeout / 1000} sec\n${err.message}`);
@@ -2222,7 +2215,7 @@ class Puppeteer extends Helper {
2222
2215
  }
2223
2216
 
2224
2217
  async _getContext() {
2225
- if (this.context && this.context.constructor.name === 'Frame') {
2218
+ if (this.context && this.context.constructor.name === 'CdpFrame') {
2226
2219
  return this.context;
2227
2220
  }
2228
2221
  return this.page;
@@ -2345,35 +2338,37 @@ class Puppeteer extends Helper {
2345
2338
  async switchTo(locator) {
2346
2339
  if (Number.isInteger(locator)) {
2347
2340
  // Select by frame index of current context
2348
-
2349
- let childFrames = null;
2341
+ let frames = [];
2350
2342
  if (this.context && typeof this.context.childFrames === 'function') {
2351
- childFrames = this.context.childFrames();
2343
+ frames = await this.context.childFrames();
2352
2344
  } else {
2353
- childFrames = this.page.mainFrame().childFrames();
2345
+ frames = await this.page.mainFrame().childFrames();
2354
2346
  }
2355
2347
 
2356
- if (locator >= 0 && locator < childFrames.length) {
2357
- this.context = childFrames[locator];
2348
+ if (locator >= 0 && locator < frames.length) {
2349
+ this.context = frames[locator];
2358
2350
  } else {
2359
- throw new Error('Element #invalidIframeSelector was not found by text|CSS|XPath');
2351
+ throw new Error('Frame index out of bounds');
2360
2352
  }
2361
2353
  return;
2362
2354
  }
2355
+
2363
2356
  if (!locator) {
2364
- this.context = await this.page.mainFrame().$('body');
2357
+ this.context = await this.page.mainFrame();
2365
2358
  return;
2366
2359
  }
2367
2360
 
2368
- // iframe by selector
2361
+ // Select iframe by selector
2369
2362
  const els = await this._locate(locator);
2370
2363
  assertElementExists(els, locator);
2371
- const contentFrame = await els[0].contentFrame();
2364
+
2365
+ const iframeElement = els[0];
2366
+ const contentFrame = await iframeElement.contentFrame();
2372
2367
 
2373
2368
  if (contentFrame) {
2374
2369
  this.context = contentFrame;
2375
2370
  } else {
2376
- this.context = els[0];
2371
+ throw new Error('Element "#invalidIframeSelector" was not found by text|CSS|XPath');
2377
2372
  }
2378
2373
  }
2379
2374
 
@@ -2469,7 +2464,7 @@ class Puppeteer extends Helper {
2469
2464
  module.exports = Puppeteer;
2470
2465
 
2471
2466
  async function findElements(matcher, locator) {
2472
- if (locator.react) return findReact(matcher.executionContext(), locator);
2467
+ if (locator.react) return findReactElements.call(this, locator);
2473
2468
  locator = new Locator(locator, 'css');
2474
2469
  if (!locator.isXPath()) return matcher.$$(locator.simplify());
2475
2470
  // puppeteer version < 19.4.0 is no longer supported. This one is backward support.
@@ -2506,7 +2501,7 @@ async function proceedClick(locator, context = null, options = {}) {
2506
2501
  }
2507
2502
 
2508
2503
  async function findClickable(matcher, locator) {
2509
- if (locator.react) return findReact(matcher.executionContext(), locator);
2504
+ if (locator.react) return findReactElements.call(this, locator);
2510
2505
  locator = new Locator(locator);
2511
2506
  if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
2512
2507
 
@@ -2855,3 +2850,70 @@ function highlightActiveElement(element, context) {
2855
2850
  highlightElement(element, context);
2856
2851
  }
2857
2852
  }
2853
+
2854
+ function _waitForElement(locator, options) {
2855
+ try {
2856
+ return this.context.waitForXPath(locator.value, options);
2857
+ } catch (e) {
2858
+ return this.context.waitForSelector(`::-p-xpath(${locator.value})`, options);
2859
+ }
2860
+ }
2861
+
2862
+ async function findReactElements(locator, props = {}, state = {}) {
2863
+ const resqScript = await fs.promises.readFile(require.resolve('resq'), 'utf-8');
2864
+ await this.page.evaluate(resqScript.toString());
2865
+
2866
+ await this.page.evaluate(() => window.resq.waitToLoadReact());
2867
+ const arrayHandle = await this.page.evaluateHandle((obj) => {
2868
+ const { selector, props, state } = obj;
2869
+ let elements = window.resq.resq$$(selector);
2870
+ if (Object.keys(props).length) {
2871
+ elements = elements.byProps(props);
2872
+ }
2873
+ if (Object.keys(state).length) {
2874
+ elements = elements.byState(state);
2875
+ }
2876
+
2877
+ if (!elements.length) {
2878
+ return [];
2879
+ }
2880
+
2881
+ // resq returns an array of HTMLElements if the React component is a fragment
2882
+ // this avoids having nested arrays of nodes which the driver does not understand
2883
+ // [[div, div], [div, div]] => [div, div, div, div]
2884
+ let nodes = [];
2885
+
2886
+ elements.forEach((element) => {
2887
+ let { node, isFragment } = element;
2888
+
2889
+ if (!node) {
2890
+ isFragment = true;
2891
+ node = element.children;
2892
+ }
2893
+
2894
+ if (isFragment) {
2895
+ nodes = nodes.concat(node);
2896
+ } else {
2897
+ nodes.push(node);
2898
+ }
2899
+ });
2900
+
2901
+ return [...nodes];
2902
+ }, {
2903
+ selector: locator.react,
2904
+ props: locator.props || {},
2905
+ state: locator.state || {},
2906
+ });
2907
+
2908
+ const properties = await arrayHandle.getProperties();
2909
+ const result = [];
2910
+ for (const property of properties.values()) {
2911
+ const elementHandle = property.asElement();
2912
+ if (elementHandle) {
2913
+ result.push(elementHandle);
2914
+ }
2915
+ }
2916
+
2917
+ await arrayHandle.dispose();
2918
+ return result;
2919
+ }
@@ -1,44 +1,67 @@
1
1
  const debugModule = require('debug');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
2
+ const { CoverageReport } = require('monocart-coverage-reports');
5
3
  const Container = require('../container');
6
4
  const recorder = require('../recorder');
7
5
  const event = require('../event');
8
6
  const output = require('../output');
9
- const { clearString } = require('../utils');
7
+ const { deepMerge } = require('../utils');
10
8
 
11
9
  const defaultConfig = {
12
- coverageDir: 'output/coverage',
13
- uniqueFileName: true,
10
+ name: 'CodeceptJS Coverage Report',
11
+ outputDir: 'output/coverage',
14
12
  };
15
13
 
16
14
  const supportedHelpers = ['Puppeteer', 'Playwright'];
17
15
 
18
- function buildFileName(test, uniqueFileName) {
19
- let fileName = clearString(test.title);
20
-
21
- // This prevent data driven to be included in the failed screenshot file name
22
- if (fileName.indexOf('{') !== -1) {
23
- fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim();
24
- }
25
-
26
- if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') {
27
- fileName = clearString(`${test.title}_${test.ctx.test.title}`);
28
- }
29
-
30
- if (uniqueFileName) {
31
- const uuid = test.uuid
32
- || test.ctx.test.uuid
33
- || Math.floor(new Date().getTime() / 1000);
34
-
35
- fileName = `${fileName.substring(0, 10)}_${uuid}.coverage.json`;
36
- } else {
37
- fileName = `${fileName}.coverage.json`;
38
- }
39
-
40
- return fileName;
41
- }
16
+ const v8CoverageHelpers = {
17
+ Playwright: {
18
+ startCoverage: async (page) => {
19
+ await Promise.all([
20
+ page.coverage.startJSCoverage({
21
+ resetOnNavigation: false,
22
+ }),
23
+ page.coverage.startCSSCoverage({
24
+ resetOnNavigation: false,
25
+ }),
26
+ ]);
27
+ },
28
+ takeCoverage: async (page, coverageReport) => {
29
+ const [jsCoverage, cssCoverage] = await Promise.all([
30
+ page.coverage.stopJSCoverage(),
31
+ page.coverage.stopCSSCoverage(),
32
+ ]);
33
+ const coverageList = [...jsCoverage, ...cssCoverage];
34
+ await coverageReport.add(coverageList);
35
+ },
36
+ },
37
+ Puppeteer: {
38
+ startCoverage: async (page) => {
39
+ await Promise.all([
40
+ page.coverage.startJSCoverage({
41
+ resetOnNavigation: false,
42
+ includeRawScriptCoverage: true,
43
+ }),
44
+ page.coverage.startCSSCoverage({
45
+ resetOnNavigation: false,
46
+ }),
47
+ ]);
48
+ },
49
+ takeCoverage: async (page, coverageReport) => {
50
+ const [jsCoverage, cssCoverage] = await Promise.all([
51
+ page.coverage.stopJSCoverage(),
52
+ page.coverage.stopCSSCoverage(),
53
+ ]);
54
+ // to raw V8 script coverage
55
+ const coverageList = [...jsCoverage.map((it) => {
56
+ return {
57
+ source: it.text,
58
+ ...it.rawScriptCoverage,
59
+ };
60
+ }), ...cssCoverage];
61
+ await coverageReport.add(coverageList);
62
+ },
63
+ },
64
+ };
42
65
 
43
66
  /**
44
67
  * Dumps code coverage from Playwright/Puppeteer after every test.
@@ -49,94 +72,84 @@ function buildFileName(test, uniqueFileName) {
49
72
  * ```js
50
73
  * plugins: {
51
74
  * coverage: {
52
- * enabled: true
75
+ * enabled: true,
76
+ * debug: true,
77
+ * name: 'CodeceptJS Coverage Report',
78
+ * outputDir: 'output/coverage'
53
79
  * }
54
80
  * }
55
81
  * ```
56
82
  *
57
- * Possible config options:
83
+ * Possible config options, More could be found at [monocart-coverage-reports](https://github.com/cenfun/monocart-coverage-reports?tab=readme-ov-file#default-options)
84
+ *
85
+ * * `debug`: debug info. By default, false.
86
+ * * `name`: coverage report name.
87
+ * * `outputDir`: path to coverage report.
88
+ * * `sourceFilter`: filter the source files.
89
+ * * `sourcePath`: option to resolve a custom path.
58
90
  *
59
- * * `coverageDir`: directory to dump coverage files
60
- * * `uniqueFileName`: generate a unique filename by adding uuid
61
91
  */
62
92
  module.exports = function (config) {
93
+ config = deepMerge(defaultConfig, config);
94
+
95
+ if (config.debug) config.logging = 'debug';
96
+
63
97
  const helpers = Container.helpers();
64
98
  let coverageRunning = false;
65
- let helper;
66
99
 
67
- let debug;
68
- for (const helperName of supportedHelpers) {
69
- if (Object.keys(helpers).indexOf(helperName) > -1) {
70
- helper = helpers[helperName];
71
- debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
72
- }
100
+ const v8Names = Object.keys(v8CoverageHelpers);
101
+ const helperName = Object.keys(helpers).find((it) => v8Names.includes(it));
102
+ if (!helperName) {
103
+ console.error(`Coverage is only supported in ${supportedHelpers.join(' or ')}`);
104
+ // no helpers for screenshot
105
+ return;
73
106
  }
74
107
 
75
- if (!helper) {
76
- console.error('Coverage is only supported in Puppeteer, Playwright');
77
- return; // no helpers for screenshot
78
- }
108
+ config.name = `${config.name} - in ${helperName}`;
109
+ const debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
110
+
111
+ const helper = helpers[helperName];
112
+ const v8Helper = v8CoverageHelpers[helperName];
79
113
 
80
- const options = Object.assign(defaultConfig, helper.options, config);
114
+ const coverageOptions = {
115
+ ...config,
116
+ };
117
+ const coverageReport = new CoverageReport(coverageOptions);
118
+ coverageReport.cleanCache();
81
119
 
82
- event.dispatcher.on(event.all.before, async () => {
83
- output.debug('*** Collecting coverage for tests ****');
120
+ event.dispatcher.on(event.all.after, async () => {
121
+ output.print(`writing ${coverageOptions.outputDir}`);
122
+ await coverageReport.generate();
84
123
  });
85
124
 
86
- // Hack! we're going to try to "start" coverage before each step because this is
125
+ // we're going to try to "start" coverage before each step because this is
87
126
  // when the browser is already up and is ready to start coverage.
88
- event.dispatcher.on(event.step.before, async () => {
89
- recorder.add(
90
- 'starting coverage',
91
- async () => {
92
- try {
93
- if (!coverageRunning && helper.page && helper.page.coverage) {
94
- debug('--> starting coverage <--');
95
- coverageRunning = true;
96
- await helper.page.coverage.startJSCoverage();
97
- }
98
- } catch (err) {
99
- console.error(err);
100
- }
101
- },
102
- true,
103
- );
127
+ event.dispatcher.on(event.step.before, () => {
128
+ recorder.add('start coverage', async () => {
129
+ if (coverageRunning) {
130
+ return;
131
+ }
132
+ if (!helper.page || !helper.page.coverage) {
133
+ return;
134
+ }
135
+ coverageRunning = true;
136
+ debug('--> starting coverage <--');
137
+ await v8Helper.startCoverage(helper.page);
138
+ }, true);
104
139
  });
105
140
 
106
141
  // Save coverage data after every test run
107
- event.dispatcher.on(event.test.after, async (test) => {
108
- recorder.add(
109
- 'saving coverage',
110
- async () => {
111
- try {
112
- if (coverageRunning && helper.page && helper.page.coverage) {
113
- debug('--> stopping coverage <--');
114
- coverageRunning = false;
115
- const coverage = await helper.page.coverage.stopJSCoverage();
116
-
117
- const coverageDir = path.resolve(
118
- process.cwd(),
119
- options.coverageDir,
120
- );
121
-
122
- // Checking if coverageDir already exists, if not, create new one
123
-
124
- if (!fs.existsSync(coverageDir)) {
125
- fs.mkdirSync(coverageDir, { recursive: true });
126
- }
127
-
128
- const coveragePath = path.resolve(
129
- coverageDir,
130
- buildFileName(test, options.uniqueFileName),
131
- );
132
- output.print(`writing ${coveragePath}`);
133
- fs.writeFileSync(coveragePath, JSON.stringify(coverage));
134
- }
135
- } catch (err) {
136
- console.error(err);
137
- }
138
- },
139
- true,
140
- );
142
+ event.dispatcher.on(event.test.after, (test) => {
143
+ recorder.add('take coverage', async () => {
144
+ if (!coverageRunning) {
145
+ return;
146
+ }
147
+ if (!helper.page || !helper.page.coverage) {
148
+ return;
149
+ }
150
+ coverageRunning = false;
151
+ debug('--> stopping coverage <--');
152
+ await v8Helper.takeCoverage(helper.page, coverageReport);
153
+ }, true);
141
154
  });
142
155
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.5.14",
3
+ "version": "3.5.15",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -29,7 +29,7 @@
29
29
  "docs/webapi/**"
30
30
  ],
31
31
  "main": "lib/index.js",
32
- "typings": "typings/index.d.ts",
32
+ "types": "typings/index.d.ts",
33
33
  "bin": {
34
34
  "codeceptjs": "./bin/codecept.js"
35
35
  },
@@ -62,12 +62,13 @@
62
62
  "dev:graphql": "node test/data/graphql/index.js",
63
63
  "publish:site": "./runok.js publish:site",
64
64
  "update-contributor-faces": "./runok.js contributor:faces",
65
- "dtslint": "dtslint typings --localTs './node_modules/typescript/lib'",
65
+ "types-fix": "node typings/fixDefFiles.js",
66
+ "dtslint": "npm run types-fix && tsd",
66
67
  "prepare": "husky install",
67
68
  "prepare-release": "./runok.js versioning && ./runok.js get:commit-log"
68
69
  },
69
70
  "dependencies": {
70
- "@codeceptjs/configure": "0.10.0",
71
+ "@codeceptjs/configure": "1.0.1",
71
72
  "@codeceptjs/helper": "2.0.1",
72
73
  "@cucumber/cucumber-expressions": "17",
73
74
  "@cucumber/gherkin": "26",
@@ -76,7 +77,7 @@
76
77
  "acorn": "8.11.3",
77
78
  "arrify": "2.0.1",
78
79
  "axios": "1.6.7",
79
- "chai": "5.0.3",
80
+ "chai": "5.1.0",
80
81
  "chai-deep-match": "1.2.1",
81
82
  "chai-exclude": "2.1.0",
82
83
  "chai-json-schema": "1.5.1",
@@ -88,7 +89,7 @@
88
89
  "cross-spawn": "7.0.3",
89
90
  "css-to-xpath": "0.1.0",
90
91
  "csstoxpath": "1.6.0",
91
- "devtools": "8.29.1",
92
+ "devtools": "8.33.1",
92
93
  "envinfo": "7.11.0",
93
94
  "escape-string-regexp": "4.0.0",
94
95
  "figures": "3.2.0",
@@ -97,16 +98,17 @@
97
98
  "glob": "6.0.1",
98
99
  "html-minifier-terser": "7.2.0",
99
100
  "inquirer": "6.5.2",
100
- "joi": "17.12.1",
101
- "js-beautify": "1.14.11",
101
+ "joi": "17.12.2",
102
+ "js-beautify": "1.15.1",
102
103
  "lodash.clonedeep": "4.5.0",
103
104
  "lodash.merge": "4.6.2",
104
105
  "mkdirp": "1.0.4",
105
- "mocha": "10.2.0",
106
+ "mocha": "10.3.0",
107
+ "monocart-coverage-reports": "2.7.1",
106
108
  "ms": "2.1.3",
107
109
  "openai": "3.2.1",
108
110
  "ora-classic": "5.4.2",
109
- "pactum": "3.6.0",
111
+ "pactum": "3.6.1",
110
112
  "parse-function": "5.6.10",
111
113
  "parse5": "7.1.2",
112
114
  "promise-retry": "1.1.1",
@@ -115,7 +117,7 @@
115
117
  "uuid": "9.0"
116
118
  },
117
119
  "optionalDependencies": {
118
- "@codeceptjs/detox-helper": "1.0.2"
120
+ "@codeceptjs/detox-helper": "1.0.5"
119
121
  },
120
122
  "devDependencies": {
121
123
  "@codeceptjs/mock-request": "0.3.1",
@@ -125,7 +127,7 @@
125
127
  "@types/chai": "4.3.11",
126
128
  "@types/inquirer": "9.0.3",
127
129
  "@types/node": "20.11.16",
128
- "@wdio/sauce-service": "8.29.1",
130
+ "@wdio/sauce-service": "8.32.3",
129
131
  "@wdio/selenium-standalone-service": "8.3.2",
130
132
  "@wdio/utils": "8.28.8",
131
133
  "@xmldom/xmldom": "0.8.10",
@@ -134,14 +136,13 @@
134
136
  "chai-subset": "1.6.0",
135
137
  "contributor-faces": "1.1.0",
136
138
  "documentation": "12.3.0",
137
- "dtslint": "4.2.1",
138
139
  "electron": "28.2.1",
139
140
  "eslint": "8.56.0",
140
141
  "eslint-config-airbnb-base": "15.0.0",
141
142
  "eslint-plugin-import": "2.29.1",
142
143
  "eslint-plugin-mocha": "6.3.0",
143
144
  "expect": "29.7.0",
144
- "express": "4.18.2",
145
+ "express": "4.18.3",
145
146
  "graphql": "14.6.0",
146
147
  "husky": "8.0.3",
147
148
  "inquirer-test": "2.0.1",
@@ -149,7 +150,7 @@
149
150
  "jsdoc-typeof-plugin": "1.0.0",
150
151
  "json-server": "0.10.1",
151
152
  "playwright": "1.41.1",
152
- "puppeteer": "21.1.1",
153
+ "puppeteer": "22.4.1",
153
154
  "qrcode-terminal": "0.12.0",
154
155
  "rosie": "2.1.1",
155
156
  "runok": "0.9.3",
@@ -158,12 +159,13 @@
158
159
  "testcafe": "3.5.0",
159
160
  "ts-morph": "21.0.1",
160
161
  "ts-node": "10.9.2",
162
+ "tsd": "^0.30.7",
161
163
  "tsd-jsdoc": "2.5.0",
162
- "typedoc": "0.25.7",
164
+ "typedoc": "0.25.12",
163
165
  "typedoc-plugin-markdown": "3.17.1",
164
166
  "typescript": "5.3.3",
165
167
  "wdio-docker-service": "1.5.0",
166
- "webdriverio": "8.31.1",
168
+ "webdriverio": "8.33.1",
167
169
  "xml2js": "0.6.2",
168
170
  "xpath": "0.0.34"
169
171
  },
@@ -171,5 +173,11 @@
171
173
  "node": ">=16.0",
172
174
  "npm": ">=5.6.0"
173
175
  },
174
- "es6": true
175
- }
176
+ "es6": true,
177
+ "tsd": {
178
+ "directory": "typings",
179
+ "compilerOptions": {
180
+ "strict": false
181
+ }
182
+ }
183
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  declare namespace CodeceptJS {
10
10
  type WithTranslation<T> = T &
11
+ // @ts-ignore
11
12
  import("./utils").Translate<T, Translation.Actions>;
12
13
 
13
14
  type Cookie = {
@@ -1127,9 +1127,9 @@ declare namespace CodeceptJS {
1127
1127
  expectNotEndsWith(actualValue: any, expectedValueToNotEndWith: any, customErrorMsg?: any): Promise<any>;
1128
1128
  expectJsonSchema(targetData: any, jsonSchema: any, customErrorMsg?: any): Promise<any>;
1129
1129
  /**
1130
- * @param ajvOptions - Pass AJV options
1130
+ * @param [ajvOptions] - Pass AJV options
1131
1131
  */
1132
- expectJsonSchemaUsingAJV(targetData: any, jsonSchema: any, customErrorMsg?: any, ajvOptions: any): Promise<any>;
1132
+ expectJsonSchemaUsingAJV(targetData: any, jsonSchema: any, customErrorMsg?: any, ajvOptions?: any): Promise<any>;
1133
1133
  expectHasProperty(targetData: any, propertyName: any, customErrorMsg?: any): Promise<any>;
1134
1134
  expectHasAProperty(targetData: any, propertyName: any, customErrorMsg?: any): Promise<any>;
1135
1135
  expectToBeA(targetData: any, type: any, customErrorMsg?: any): Promise<any>;
@@ -1151,9 +1151,9 @@ declare namespace CodeceptJS {
1151
1151
  expectNotEndsWith(actualValue: any, expectedValueToNotEndWith: any, customErrorMsg?: any): void;
1152
1152
  expectJsonSchema(targetData: any, jsonSchema: any, customErrorMsg?: any): void;
1153
1153
  /**
1154
- * @param ajvOptions - Pass AJV options
1154
+ * @param [ajvOptions] - Pass AJV options
1155
1155
  */
1156
- expectJsonSchemaUsingAJV(targetData: any, jsonSchema: any, customErrorMsg?: any, ajvOptions: any): void;
1156
+ expectJsonSchemaUsingAJV(targetData: any, jsonSchema: any, customErrorMsg?: any, ajvOptions?: any): void;
1157
1157
  expectHasProperty(targetData: any, propertyName: any, customErrorMsg?: any): void;
1158
1158
  expectHasAProperty(targetData: any, propertyName: any, customErrorMsg?: any): void;
1159
1159
  expectToBeA(targetData: any, type: any, customErrorMsg?: any): void;
@@ -12184,14 +12184,14 @@ declare namespace CodeceptJS {
12184
12184
  * Detox provides a grey box testing for mobile applications, playing especially good for React Native apps.
12185
12185
  *
12186
12186
  * Detox plays quite differently from Appium. To establish detox testing you need to build a mobile application in a special way to inject Detox code.
12187
- * This why **Detox is grey box testing** solution, so you need an access to application source code, and a way to build and execute it on emulator.
12187
+ * This why **Detox is grey box testing** solution, so you need access to application source code, and a way to build and execute it on emulator.
12188
12188
  *
12189
12189
  * Comparing to Appium, Detox runs faster and more stable but requires an additional setup for build.
12190
12190
  *
12191
12191
  * ### Setup
12192
12192
  *
12193
- * 1. [Install and configure Detox for iOS](https://github.com/wix/Detox/blob/master/docs/Introduction.GettingStarted.md) and [Android](https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md)
12194
- * 2. [Build an application](https://github.com/wix/Detox/blob/master/docs/Introduction.GettingStarted.md#step-4-build-your-app-and-run-detox-tests) using `detox build` command.
12193
+ * 1. [Install and configure Detox](https://wix.github.io/Detox/docs/introduction/project-setup)
12194
+ * 2. [Build an application](https://wix.github.io/Detox/docs/introduction/project-setup#step-5-build-the-app) using `detox build` command.
12195
12195
  * 3. Install [CodeceptJS](https://codecept.io) and detox-helper:
12196
12196
  *
12197
12197
  * ```
@@ -12204,15 +12204,28 @@ declare namespace CodeceptJS {
12204
12204
  *
12205
12205
  * ```js
12206
12206
  * "detox": {
12207
- * "configurations": {
12208
- * "ios.sim.debug": {
12209
- * "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/example.app",
12210
- * "build": "xcodebuild -project ios/example.xcodeproj -scheme example -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
12211
- * "type": "ios.simulator",
12212
- * "name": "iPhone 7"
12213
- * }
12214
- * }
12215
- * }
12207
+ * "configurations": {
12208
+ * "ios.sim.debug": {
12209
+ * "device": "simulator",
12210
+ * "app": "ios.debug"
12211
+ * }
12212
+ * },
12213
+ * "apps": {
12214
+ * "ios.debug": {
12215
+ * "type": "ios.app",
12216
+ * "binaryPath": "../test/ios/build/Build/Products/Debug-iphonesimulator/MyTestApp.app",
12217
+ * "build": "xcodebuild -workspace ../test/ios/MyTestApp.xcworkspace -scheme MyTestApp -configuration Debug -sdk iphonesimulator -derivedDataPath ../test/ios/build"
12218
+ * }
12219
+ * },
12220
+ * "devices": {
12221
+ * "simulator": {
12222
+ * "type": "ios.simulator",
12223
+ * "device": {
12224
+ * "type": "iPhone 15"
12225
+ * }
12226
+ * }
12227
+ * }
12228
+ * }
12216
12229
  * ```
12217
12230
  *
12218
12231
  *
@@ -12307,6 +12320,14 @@ declare namespace CodeceptJS {
12307
12320
  * ```
12308
12321
  */
12309
12322
  setPortraitOrientation(): void;
12323
+ /**
12324
+ * Grab the device platform
12325
+ *
12326
+ * ```js
12327
+ * const platform = await I.grabPlatform();
12328
+ * ```
12329
+ */
12330
+ grabPlatform(): void;
12310
12331
  /**
12311
12332
  * Execute code only on iOS
12312
12333
  *
@@ -12396,6 +12417,19 @@ declare namespace CodeceptJS {
12396
12417
  * ```
12397
12418
  */
12398
12419
  click(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString | null): void;
12420
+ /**
12421
+ * Clicks on an element.
12422
+ * Element can be located by its label
12423
+ *
12424
+ * The second parameter is a context (id | type | accessibility id) to narrow the search.
12425
+ *
12426
+ *
12427
+ * ```js
12428
+ * I.tapByLabel('Login'); // locate by text
12429
+ * I.tapByLabel('Login', '#nav'); // locate by text inside #nav
12430
+ * ```
12431
+ */
12432
+ tapByLabel(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString | null): void;
12399
12433
  /**
12400
12434
  * Performs click on element with horizontal and vertical offset.
12401
12435
  * An element is located by text, id, accessibility id.
@@ -12446,6 +12480,17 @@ declare namespace CodeceptJS {
12446
12480
  * @param [context = null] - context element
12447
12481
  */
12448
12482
  seeElement(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString | null): void;
12483
+ /**
12484
+ * Checks if an element exists.
12485
+ *
12486
+ * ```js
12487
+ * I.checkIfElementExists('~edit'); // located by accessibility id
12488
+ * I.checkIfElementExists('~edit', '#menu'); // element inside #menu
12489
+ * ```
12490
+ * @param locator - element to locate
12491
+ * @param [context = null] - context element
12492
+ */
12493
+ checkIfElementExists(locator: CodeceptJS.LocatorOrString, context?: CodeceptJS.LocatorOrString | null): void;
12449
12494
  /**
12450
12495
  * Checks that element is not visible.
12451
12496
  * Use second parameter to narrow down the search.
@@ -12495,6 +12540,18 @@ declare namespace CodeceptJS {
12495
12540
  * @param value - value to fill
12496
12541
  */
12497
12542
  fillField(field: CodeceptJS.LocatorOrString, value: string): void;
12543
+ /**
12544
+ * Taps return key.
12545
+ * A field can be located by text, accessibility id, id.
12546
+ *
12547
+ * ```js
12548
+ * I.tapReturnKey('Username');
12549
+ * I.tapReturnKey('~name');
12550
+ * I.tapReturnKey({ android: 'NAME', ios: 'name' });
12551
+ * ```
12552
+ * @param field - an input element to fill in
12553
+ */
12554
+ tapReturnKey(field: CodeceptJS.LocatorOrString): void;
12498
12555
  /**
12499
12556
  * Clears a text field.
12500
12557
  * A field can be located by text, accessibility id, id.
@@ -12620,7 +12677,7 @@ declare namespace CodeceptJS {
12620
12677
  */
12621
12678
  waitForElementVisible(locator: CodeceptJS.LocatorOrString, sec?: number): void;
12622
12679
  /**
12623
- * Waits an elment to become not visible.
12680
+ * Waits an elmenet to become not visible.
12624
12681
  *
12625
12682
  * ```js
12626
12683
  * I.waitToHide('#message', 2); // wait for 2 seconds
@@ -12629,6 +12686,14 @@ declare namespace CodeceptJS {
12629
12686
  * @param [sec = 5] - number of seconds to wait
12630
12687
  */
12631
12688
  waitToHide(locator: CodeceptJS.LocatorOrString, sec?: number): void;
12689
+ /**
12690
+ * Scrolls within a scrollable container to an element.
12691
+ * @param targetLocator - Locator of the element to scroll to
12692
+ * @param containerLocator - Locator of the scrollable container
12693
+ * @param direction - 'up' or 'down'
12694
+ * @param [offset = 100] - Offset for scroll, can be adjusted based on need
12695
+ */
12696
+ scrollToElement(targetLocator: CodeceptJS.LocatorOrString, containerLocator: CodeceptJS.LocatorOrString, direction?: string, offset?: number): void;
12632
12697
  }
12633
12698
  /**
12634
12699
  * Abstract class.