codeceptjs 3.0.6 → 3.1.2

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +92 -8
  2. package/README.md +9 -1
  3. package/bin/codecept.js +28 -17
  4. package/docs/build/Appium.js +69 -0
  5. package/docs/build/GraphQL.js +9 -10
  6. package/docs/build/Playwright.js +271 -63
  7. package/docs/build/Protractor.js +2 -0
  8. package/docs/build/Puppeteer.js +56 -18
  9. package/docs/build/REST.js +16 -3
  10. package/docs/build/WebDriver.js +82 -16
  11. package/docs/changelog.md +93 -9
  12. package/docs/configuration.md +15 -2
  13. package/docs/email.md +8 -8
  14. package/docs/examples.md +3 -3
  15. package/docs/helpers/Appium.md +66 -68
  16. package/docs/helpers/MockRequest.md +3 -3
  17. package/docs/helpers/Playwright.md +269 -203
  18. package/docs/helpers/Puppeteer.md +17 -1
  19. package/docs/helpers/REST.md +23 -9
  20. package/docs/helpers/WebDriver.md +3 -2
  21. package/docs/locators.md +27 -0
  22. package/docs/mobile.md +2 -1
  23. package/docs/nightmare.md +0 -5
  24. package/docs/parallel.md +14 -7
  25. package/docs/playwright.md +178 -11
  26. package/docs/plugins.md +61 -69
  27. package/docs/react.md +1 -1
  28. package/docs/reports.md +5 -4
  29. package/lib/actor.js +1 -2
  30. package/lib/codecept.js +13 -2
  31. package/lib/command/definitions.js +8 -1
  32. package/lib/command/interactive.js +4 -2
  33. package/lib/command/run-multiple/collection.js +4 -0
  34. package/lib/container.js +3 -3
  35. package/lib/helper/Appium.js +41 -0
  36. package/lib/helper/GraphQL.js +9 -10
  37. package/lib/helper/Playwright.js +218 -70
  38. package/lib/helper/Protractor.js +2 -0
  39. package/lib/helper/Puppeteer.js +56 -18
  40. package/lib/helper/REST.js +12 -0
  41. package/lib/helper/WebDriver.js +82 -16
  42. package/lib/helper/errors/ConnectionRefused.js +1 -1
  43. package/lib/helper/extras/Popup.js +1 -1
  44. package/lib/helper/extras/React.js +44 -32
  45. package/lib/interfaces/gherkin.js +1 -0
  46. package/lib/listener/exit.js +2 -4
  47. package/lib/listener/helpers.js +3 -4
  48. package/lib/locator.js +7 -0
  49. package/lib/mochaFactory.js +11 -6
  50. package/lib/output.js +5 -2
  51. package/lib/plugin/allure.js +7 -18
  52. package/lib/plugin/commentStep.js +1 -1
  53. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  54. package/lib/plugin/customLocator.js +2 -2
  55. package/lib/plugin/screenshotOnFail.js +5 -0
  56. package/lib/plugin/subtitles.js +88 -0
  57. package/lib/plugin/tryTo.js +1 -1
  58. package/lib/step.js +4 -2
  59. package/lib/ui.js +6 -2
  60. package/package.json +5 -4
  61. package/typings/index.d.ts +44 -21
  62. package/typings/types.d.ts +137 -16
@@ -17,12 +17,6 @@ class MochaFactory {
17
17
  output.process(opts.child);
18
18
  mocha.ui(scenarioUi);
19
19
 
20
- // process.on('unhandledRejection', (reason) => {
21
- // output.error('Unhandled rejection');
22
- // console.log(Error.captureStackTrace(reason));
23
- // output.error(reason);
24
- // });
25
-
26
20
  Mocha.Runner.prototype.uncaught = function (err) {
27
21
  if (err) {
28
22
  if (err.toString().indexOf('ECONNREFUSED') >= 0) {
@@ -51,19 +45,30 @@ class MochaFactory {
51
45
 
52
46
  // add ids for each test and check uniqueness
53
47
  const dupes = [];
48
+ let missingFeatureInFile = [];
54
49
  const seenTests = [];
55
50
  mocha.suite.eachTest(test => {
56
51
  test.id = genTestId(test);
52
+
57
53
  const name = test.fullTitle();
58
54
  if (seenTests.includes(test.id)) {
59
55
  dupes.push(name);
60
56
  }
61
57
  seenTests.push(test.id);
58
+
59
+ if (name.slice(0, name.indexOf(':')) === '') {
60
+ missingFeatureInFile.push(test.file);
61
+ }
62
62
  });
63
63
  if (dupes.length) {
64
64
  // ideally this should be no-op and throw (breaking change)...
65
65
  output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`);
66
66
  }
67
+
68
+ if (missingFeatureInFile.length) {
69
+ missingFeatureInFile = [...new Set(missingFeatureInFile)];
70
+ output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`);
71
+ }
67
72
  }
68
73
  };
69
74
 
package/lib/output.js CHANGED
@@ -190,6 +190,9 @@ module.exports = {
190
190
  * @param {string} [color]
191
191
  */
192
192
  say(message, color = 'cyan') {
193
+ if (colors[color] === undefined) {
194
+ color = 'cyan';
195
+ }
193
196
  if (outputLevel >= 1) print(` ${colors[color].bold(message)}`);
194
197
  },
195
198
 
@@ -232,8 +235,8 @@ function print(...msg) {
232
235
  }
233
236
 
234
237
  function truncate(msg, gap = 0) {
235
- if (msg.indexOf('\n') > 0) {
236
- return msg; // don't cut multi line steps
238
+ if (msg.indexOf('\n') > 0 || outputLevel >= 3) {
239
+ return msg; // don't cut multi line steps or on verbose log level
237
240
  }
238
241
  const width = (process.stdout.columns || 200) - gap - 4;
239
242
  if (msg.length > width) {
@@ -83,7 +83,6 @@ module.exports = (config) => {
83
83
 
84
84
  let currentMetaStep = [];
85
85
  let currentStep;
86
- let isHookSteps = false;
87
86
 
88
87
  reporter.pendingCase = function (testName, timestamp, opts = {}) {
89
88
  reporter.startCase(testName, timestamp);
@@ -191,14 +190,6 @@ module.exports = (config) => {
191
190
  }
192
191
  });
193
192
 
194
- event.dispatcher.on(event.hook.started, () => {
195
- isHookSteps = true;
196
- });
197
-
198
- event.dispatcher.on(event.hook.passed, () => {
199
- isHookSteps = false;
200
- });
201
-
202
193
  event.dispatcher.on(event.suite.after, () => {
203
194
  reporter.endSuite();
204
195
  });
@@ -258,15 +249,13 @@ module.exports = (config) => {
258
249
  });
259
250
 
260
251
  event.dispatcher.on(event.step.started, (step) => {
261
- if (isHookSteps === false) {
262
- startMetaStep(step.metaStep);
263
- if (currentStep !== step) {
264
- // In multi-session scenarios, actors' names will be highlighted with ANSI
265
- // escape sequences which are invalid XML values
266
- step.actor = step.actor.replace(ansiRegExp(), '');
267
- reporter.startStep(step.toString());
268
- currentStep = step;
269
- }
252
+ startMetaStep(step.metaStep);
253
+ if (currentStep !== step) {
254
+ // In multi-session scenarios, actors' names will be highlighted with ANSI
255
+ // escape sequences which are invalid XML values
256
+ step.actor = step.actor.replace(ansiRegExp(), '');
257
+ reporter.startStep(step.toString());
258
+ currentStep = step;
270
259
  }
271
260
  });
272
261
 
@@ -41,7 +41,7 @@ const defaultGlobalName = '__';
41
41
  * ### Config
42
42
  *
43
43
  * * `enabled` - (default: false) enable a plugin
44
- * * `regusterGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value.
44
+ * * `registerGlobal` - (default: false) register `__` template literal function globally. You can override function global name by providing a name as a value.
45
45
  *
46
46
  * ### Examples
47
47
  *
@@ -1,4 +1,4 @@
1
- const debug = require('debug')('codeceptjs:plugin:puppeteerCoverage');
1
+ const debugModule = require('debug');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
 
@@ -13,7 +13,7 @@ const defaultConfig = {
13
13
  uniqueFileName: true,
14
14
  };
15
15
 
16
- const supportedHelpers = ['Puppeteer'];
16
+ const supportedHelpers = ['Puppeteer', 'Playwright'];
17
17
 
18
18
  function buildFileName(test, uniqueFileName) {
19
19
  let fileName = clearString(test.title);
@@ -41,15 +41,14 @@ function buildFileName(test, uniqueFileName) {
41
41
  }
42
42
 
43
43
  /**
44
- * Dumps puppeteers code coverage after every test.
44
+ * Dumps code coverage from Playwright/Puppeteer after every test.
45
45
  *
46
46
  * #### Configuration
47
47
  *
48
- * Configuration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended).
49
48
  *
50
49
  * ```js
51
50
  * plugins: {
52
- * puppeteerCoverage: {
51
+ * playwrightCoverage: {
53
52
  * enabled: true
54
53
  * }
55
54
  * }
@@ -59,33 +58,22 @@ function buildFileName(test, uniqueFileName) {
59
58
  *
60
59
  * * `coverageDir`: directory to dump coverage files
61
60
  * * `uniqueFileName`: generate a unique filename by adding uuid
62
- *
63
- * First of all, your mileage may vary!
64
- *
65
- * To work, you need the client javascript code to be NOT uglified. They need to be built in "development" mode.
66
- * And the end of your tests, you'll get a directory full of coverage per test run. Now what?
67
- * You'll need to convert the coverage code to something istanbul can read. Good news is someone wrote the code
68
- * for you (see puppeteer-to-istanbul link below). Then using istanbul you need to combine the converted
69
- * coverage and create a report. Good luck!
70
- *
71
- * Links:
72
- * * https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage
73
- * * https://github.com/istanbuljs/puppeteer-to-istanbul
74
- * * https://github.com/gotwarlost/istanbul
75
61
  */
76
62
  module.exports = function (config) {
77
63
  const helpers = Container.helpers();
78
64
  let coverageRunning = false;
79
65
  let helper;
80
66
 
67
+ let debug;
81
68
  for (const helperName of supportedHelpers) {
82
69
  if (Object.keys(helpers).indexOf(helperName) > -1) {
83
70
  helper = helpers[helperName];
71
+ debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
84
72
  }
85
73
  }
86
74
 
87
75
  if (!helper) {
88
- console.error('Coverage is only supported in Puppeteer');
76
+ console.error('Coverage is only supported in Puppeteer, Playwright');
89
77
  return; // no helpers for screenshot
90
78
  }
91
79
 
@@ -102,7 +90,7 @@ module.exports = function (config) {
102
90
  'starting coverage',
103
91
  async () => {
104
92
  try {
105
- if (!coverageRunning) {
93
+ if (!coverageRunning && helper.page && helper.page.coverage) {
106
94
  debug('--> starting coverage <--');
107
95
  coverageRunning = true;
108
96
  await helper.page.coverage.startJSCoverage();
@@ -115,13 +103,13 @@ module.exports = function (config) {
115
103
  );
116
104
  });
117
105
 
118
- // Save puppeteer coverage data after every test run
106
+ // Save coverage data after every test run
119
107
  event.dispatcher.on(event.test.after, async (test) => {
120
108
  recorder.add(
121
109
  'saving coverage',
122
110
  async () => {
123
111
  try {
124
- if (coverageRunning) {
112
+ if (coverageRunning && helper.page && helper.page.coverage) {
125
113
  debug('--> stopping coverage <--');
126
114
  coverageRunning = false;
127
115
  const coverage = await helper.page.coverage.stopJSCoverage();
@@ -38,7 +38,7 @@ const defaultConfig = {
38
38
  * // in codecept.conf.js
39
39
  * plugins: {
40
40
  * customLocator: {
41
- * enabled: true
41
+ * enabled: true,
42
42
  * attribute: 'data-test'
43
43
  * }
44
44
  * }
@@ -57,7 +57,7 @@ const defaultConfig = {
57
57
  * // in codecept.conf.js
58
58
  * plugins: {
59
59
  * customLocator: {
60
- * enabled: true
60
+ * enabled: true,
61
61
  * prefix: '=',
62
62
  * attribute: 'data-qa'
63
63
  * }
@@ -107,6 +107,11 @@ module.exports = function (config) {
107
107
  if (allureReporter) {
108
108
  allureReporter.addAttachment('Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), 'image/png');
109
109
  }
110
+
111
+ const cucumberReporter = Container.plugins('cucumberJsonReporter');
112
+ if (cucumberReporter) {
113
+ cucumberReporter.addScreenshot(test.artifacts.screenshot);
114
+ }
110
115
  } catch (err) {
111
116
  output.plugin(err);
112
117
  if (
@@ -0,0 +1,88 @@
1
+ const { v4: uuidv4 } = require('uuid');
2
+ const fsPromise = require('fs').promises;
3
+ const path = require('path');
4
+ const event = require('../event');
5
+
6
+ // This will convert a given timestamp in milliseconds to
7
+ // an SRT recognized timestamp, ie HH:mm:ss,SSS
8
+ function formatTimestamp(timestampInMs) {
9
+ const date = new Date(0, 0, 0, 0, 0, 0, timestampInMs);
10
+ const hours = date.getHours();
11
+ const minutes = date.getMinutes();
12
+ const seconds = date.getSeconds();
13
+ const ms = timestampInMs - (hours * 3600000 + minutes * 60000 + seconds * 1000);
14
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
15
+ }
16
+
17
+ let steps = {};
18
+ let testStartedAt;
19
+ /**
20
+ * Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test
21
+ *
22
+ * #### Configuration
23
+ * ```js
24
+ * plugins: {
25
+ * subtitles: {
26
+ * enabled: true
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ module.exports = function () {
32
+ event.dispatcher.on(event.test.before, (_) => {
33
+ testStartedAt = Date.now();
34
+ steps = {};
35
+ });
36
+
37
+ event.dispatcher.on(event.step.started, (step) => {
38
+ const stepStartedAt = Date.now();
39
+ step.id = uuidv4();
40
+
41
+ let title = `${step.actor}.${step.name}(${step.args ? step.args.join(',') : ''})`;
42
+ if (title.length > 100) {
43
+ title = `${title.substring(0, 100)}...`;
44
+ }
45
+
46
+ steps[step.id] = {
47
+ start: formatTimestamp(stepStartedAt - testStartedAt),
48
+ startedAt: stepStartedAt,
49
+ title,
50
+ };
51
+ });
52
+
53
+ event.dispatcher.on(event.step.finished, (step) => {
54
+ if (step && step.id && steps[step.id]) {
55
+ steps[step.id].end = formatTimestamp(Date.now() - testStartedAt);
56
+ }
57
+ });
58
+
59
+ event.dispatcher.on(event.test.after, async (test) => {
60
+ if (test && test.artifacts && test.artifacts.video) {
61
+ const stepsSortedByStartTime = Object.values(steps);
62
+ stepsSortedByStartTime.sort((stepA, stepB) => {
63
+ return stepA.startedAt - stepB.startedAt;
64
+ });
65
+
66
+ let subtitle = '';
67
+
68
+ // For an SRT file, every subtitle has to be in the format as mentioned below:
69
+ //
70
+ // 1
71
+ // HH:mm:ss,SSS --> HH:mm:ss,SSS
72
+ // [title]
73
+ stepsSortedByStartTime.forEach((step, index) => {
74
+ if (step.end) {
75
+ subtitle = `${subtitle}${index + 1}
76
+ ${step.start} --> ${step.end}
77
+ ${step.title}
78
+
79
+ `;
80
+ }
81
+ });
82
+
83
+ const { dir: artifactsDirectory, name: fileName } = path.parse(test.artifacts.video);
84
+ await fsPromise.writeFile(`${artifactsDirectory}/${fileName}.srt`, subtitle);
85
+ test.artifacts.subtitle = `${artifactsDirectory}/${fileName}.srt`;
86
+ }
87
+ });
88
+ };
@@ -89,7 +89,7 @@ function tryTo(callback) {
89
89
  recorder.session.catch((err) => {
90
90
  result = false;
91
91
  const msg = err.inspect ? err.inspect() : err.toString();
92
- debug(`Unsuccesful try > ${msg}`);
92
+ debug(`Unsuccessful try > ${msg}`);
93
93
  recorder.session.restore('tryTo');
94
94
  return result;
95
95
  });
package/lib/step.js CHANGED
@@ -212,16 +212,18 @@ class MetaStep extends Step {
212
212
  step.metaStep = this;
213
213
  };
214
214
  event.dispatcher.prependListener(event.step.before, registerStep);
215
+ let rethrownError = null;
215
216
  try {
216
217
  this.startTime = Date.now();
217
218
  result = fn.apply(this.context, this.args);
218
219
  } catch (error) {
219
- this.status = 'failed';
220
+ this.setStatus('failed');
221
+ rethrownError = error;
220
222
  } finally {
221
223
  this.endTime = Date.now();
222
-
223
224
  event.dispatcher.removeListener(event.step.before, registerStep);
224
225
  }
226
+ if (rethrownError) { throw rethrownError; }
225
227
  return result;
226
228
  }
227
229
  }
package/lib/ui.js CHANGED
@@ -176,8 +176,12 @@ module.exports = function (suite) {
176
176
  * @kind constant
177
177
  * @type {CodeceptJS.IScenario}
178
178
  */
179
- context.xScenario = context.Scenario.skip = function (title) {
180
- return context.Scenario(title, {});
179
+ context.xScenario = context.Scenario.skip = function (title, opts = {}, fn) {
180
+ if (typeof opts === 'function' && !fn) {
181
+ opts = {};
182
+ }
183
+
184
+ return context.Scenario(title, opts);
181
185
  };
182
186
 
183
187
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.0.6",
3
+ "version": "3.1.2",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -84,8 +84,8 @@
84
84
  "promise-retry": "^1.1.1",
85
85
  "requireg": "^0.2.2",
86
86
  "resq": "^1.10.0",
87
- "semver": "^6.2.0",
88
- "sprintf-js": "^1.1.1"
87
+ "sprintf-js": "^1.1.1",
88
+ "uuid": "^8.3.2"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@codeceptjs/detox-helper": "^1.0.2",
@@ -123,10 +123,11 @@
123
123
  "nodemon": "^1.19.4",
124
124
  "playwright": "^1.9.1",
125
125
  "protractor": "^5.4.4",
126
- "puppeteer": "^8.0.0",
126
+ "puppeteer": "^10.0.0",
127
127
  "qrcode-terminal": "^0.12.0",
128
128
  "rosie": "^1.6.0",
129
129
  "runok": "^0.9.2",
130
+ "semver": "^6.3.0",
130
131
  "sinon": "^9.2.2",
131
132
  "sinon-chai": "^3.5.0",
132
133
  "testcafe": "^1.9.4",
@@ -8,13 +8,13 @@ declare namespace CodeceptJS {
8
8
  import("./utils").Translate<T, Translation.Actions>;
9
9
 
10
10
  type Cookie = {
11
- name: string
12
- value: string
13
- }
11
+ name: string;
12
+ value: string;
13
+ };
14
14
 
15
15
  interface PageScrollPosition {
16
- x: number,
17
- y: number
16
+ x: number;
17
+ y: number;
18
18
  }
19
19
 
20
20
  // Could get extended by user generated typings
@@ -23,7 +23,7 @@ declare namespace CodeceptJS {
23
23
  interface IHook {}
24
24
  interface IScenario {}
25
25
  interface IFeature {
26
- (title: string): FeatureConfig
26
+ (title: string): FeatureConfig;
27
27
  }
28
28
  interface CallbackOrder extends Array<any> {}
29
29
  interface SupportObject {
@@ -51,25 +51,48 @@ declare namespace CodeceptJS {
51
51
  | { frame: string }
52
52
  | { android: string }
53
53
  | { ios: string }
54
- | { android: string, ios: string }
55
- | { react: string };
56
-
57
- interface CustomLocators { }
58
- type LocatorOrString = string | ILocator | Locator | CustomLocators[keyof CustomLocators];
54
+ | { android: string; ios: string }
55
+ | { react: string }
56
+ | { shadow: string }
57
+ | { custom: string };
58
+
59
+ interface CustomLocators {}
60
+ type LocatorOrString =
61
+ | string
62
+ | ILocator
63
+ | Locator
64
+ | CustomLocators[keyof CustomLocators];
59
65
 
60
66
  type StringOrSecret = string | CodeceptJS.Secret;
61
67
 
62
- interface HookCallback { (args: SupportObject): void; }
63
- interface Scenario extends IScenario { only: IScenario, skip: IScenario, todo: IScenario}
64
- interface Feature extends IFeature { skip: IFeature }
65
- interface IData { Scenario: IScenario, only: { Scenario: IScenario } }
68
+ interface HookCallback {
69
+ (args: SupportObject): void;
70
+ }
71
+ interface Scenario extends IScenario {
72
+ only: IScenario;
73
+ skip: IScenario;
74
+ todo: IScenario;
75
+ }
76
+ interface Feature extends IFeature {
77
+ skip: IFeature;
78
+ }
79
+ interface IData {
80
+ Scenario: IScenario;
81
+ only: { Scenario: IScenario };
82
+ }
66
83
 
67
84
  interface IScenario {
68
85
  // Scenario.todo can be called only with a title.
69
86
  (title: string, callback?: HookCallback): ScenarioConfig;
70
- (title: string, opts: { [key: string]: any }, callback: HookCallback): ScenarioConfig;
87
+ (
88
+ title: string,
89
+ opts: { [key: string]: any },
90
+ callback: HookCallback
91
+ ): ScenarioConfig;
92
+ }
93
+ interface IHook {
94
+ (callback: HookCallback): void;
71
95
  }
72
- interface IHook { (callback: HookCallback): void; }
73
96
 
74
97
  interface Globals {
75
98
  codeceptjs: typeof codeceptjs;
@@ -164,12 +187,12 @@ declare namespace Mocha {
164
187
  }
165
188
 
166
189
  interface Suite extends SuiteRunnable {
167
- tags: any[]
168
- comment: string
169
- feature: any
190
+ tags: any[];
191
+ comment: string;
192
+ feature: any;
170
193
  }
171
194
 
172
- interface Test extends Runnable {
195
+ interface Test extends Runnable {
173
196
  tags: any[];
174
197
  }
175
198
  }