codeceptjs 3.1.0 → 3.2.0

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 (71) hide show
  1. package/CHANGELOG.md +129 -3
  2. package/README.md +2 -3
  3. package/bin/codecept.js +1 -0
  4. package/docs/advanced.md +94 -60
  5. package/docs/basics.md +1 -1
  6. package/docs/bdd.md +55 -1
  7. package/docs/build/Appium.js +106 -34
  8. package/docs/build/FileSystem.js +1 -0
  9. package/docs/build/Nightmare.js +48 -48
  10. package/docs/build/Playwright.js +97 -94
  11. package/docs/build/Protractor.js +68 -81
  12. package/docs/build/Puppeteer.js +91 -93
  13. package/docs/build/REST.js +1 -0
  14. package/docs/build/TestCafe.js +44 -44
  15. package/docs/build/WebDriver.js +71 -95
  16. package/docs/changelog.md +144 -2
  17. package/docs/commands.md +21 -7
  18. package/docs/configuration.md +15 -2
  19. package/docs/custom-helpers.md +1 -36
  20. package/docs/helpers/Appium.md +97 -95
  21. package/docs/helpers/FileSystem.md +1 -1
  22. package/docs/helpers/Playwright.md +16 -18
  23. package/docs/helpers/Puppeteer.md +18 -18
  24. package/docs/helpers/REST.md +3 -1
  25. package/docs/helpers/WebDriver.md +3 -19
  26. package/docs/mobile-react-native-locators.md +3 -0
  27. package/docs/playwright.md +40 -0
  28. package/docs/plugins.md +185 -68
  29. package/docs/reports.md +23 -5
  30. package/lib/actor.js +20 -2
  31. package/lib/codecept.js +15 -2
  32. package/lib/command/info.js +1 -1
  33. package/lib/config.js +13 -1
  34. package/lib/container.js +3 -1
  35. package/lib/data/dataTableArgument.js +35 -0
  36. package/lib/helper/Appium.js +49 -4
  37. package/lib/helper/FileSystem.js +1 -0
  38. package/lib/helper/Playwright.js +35 -22
  39. package/lib/helper/Protractor.js +2 -14
  40. package/lib/helper/Puppeteer.js +20 -19
  41. package/lib/helper/REST.js +1 -0
  42. package/lib/helper/WebDriver.js +2 -16
  43. package/lib/index.js +2 -0
  44. package/lib/interfaces/featureConfig.js +3 -0
  45. package/lib/interfaces/gherkin.js +7 -1
  46. package/lib/interfaces/scenarioConfig.js +4 -0
  47. package/lib/listener/helpers.js +1 -0
  48. package/lib/listener/steps.js +21 -3
  49. package/lib/listener/timeout.js +71 -0
  50. package/lib/locator.js +3 -0
  51. package/lib/mochaFactory.js +13 -9
  52. package/lib/plugin/allure.js +6 -1
  53. package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
  54. package/lib/plugin/customLocator.js +2 -2
  55. package/lib/plugin/retryTo.js +130 -0
  56. package/lib/plugin/screenshotOnFail.js +1 -0
  57. package/lib/plugin/stepByStepReport.js +7 -0
  58. package/lib/plugin/stepTimeout.js +90 -0
  59. package/lib/plugin/subtitles.js +88 -0
  60. package/lib/plugin/tryTo.js +1 -1
  61. package/lib/recorder.js +21 -8
  62. package/lib/step.js +7 -2
  63. package/lib/store.js +2 -0
  64. package/lib/ui.js +2 -2
  65. package/package.json +6 -7
  66. package/typings/index.d.ts +8 -1
  67. package/typings/types.d.ts +198 -82
  68. package/docs/angular.md +0 -325
  69. package/docs/helpers/Protractor.md +0 -1658
  70. package/docs/webapi/waitUntil.mustache +0 -11
  71. package/typings/Protractor.d.ts +0 -16
@@ -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/recorder.js CHANGED
@@ -3,6 +3,8 @@ const promiseRetry = require('promise-retry');
3
3
 
4
4
  const { log } = require('./output');
5
5
 
6
+ const MAX_TASKS = 100;
7
+
6
8
  let promise;
7
9
  let running = false;
8
10
  let errFn;
@@ -116,7 +118,7 @@ module.exports = {
116
118
  * @inner
117
119
  */
118
120
  start(name) {
119
- log(`${currentQueue()}Starting <${name}> session`);
121
+ debug(`${currentQueue()}Starting <${name}> session`);
120
122
  tasks.push('--->');
121
123
  oldPromises.push(promise);
122
124
  this.running = true;
@@ -130,7 +132,7 @@ module.exports = {
130
132
  */
131
133
  restore(name) {
132
134
  tasks.push('<---');
133
- log(`${currentQueue()}Finalize <${name}> session`);
135
+ debug(`${currentQueue()}Finalize <${name}> session`);
134
136
  this.running = false;
135
137
  sessionId = null;
136
138
  this.catch(errFn);
@@ -158,10 +160,11 @@ module.exports = {
158
160
  * undefined: `add(fn)` -> `false` and `add('step',fn)` -> `true`
159
161
  * true: it will retries if `retryOpts` set.
160
162
  * false: ignore `retryOpts` and won't retry.
163
+ * @param {number} [timeout]
161
164
  * @return {Promise<*> | undefined}
162
165
  * @inner
163
166
  */
164
- add(taskName, fn = undefined, force = false, retry = undefined) {
167
+ add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
165
168
  if (typeof taskName === 'function') {
166
169
  fn = taskName;
167
170
  taskName = fn.toString();
@@ -172,19 +175,21 @@ module.exports = {
172
175
  return;
173
176
  }
174
177
  tasks.push(taskName);
175
- debug(`${currentQueue()}Queued | ${taskName}`);
178
+ if (process.env.DEBUG) debug(`${currentQueue()}Queued | ${taskName}`);
176
179
 
177
180
  return promise = Promise.resolve(promise).then((res) => {
178
181
  const retryOpts = this.retries.slice(-1).pop();
179
182
  // no retries or unnamed tasks
180
183
  if (!retryOpts || !taskName || !retry) {
181
- return Promise.resolve(res).then(fn);
184
+ const [promise, timer] = getTimeoutPromise(timeout, taskName);
185
+ return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer));
182
186
  }
183
187
 
184
188
  const retryRules = this.retries.slice().reverse();
185
189
  return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
186
190
  if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
187
- return Promise.resolve(res).then(fn).catch((err) => {
191
+ const [promise, timer] = getTimeoutPromise(timeout, taskName);
192
+ return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => {
188
193
  for (const retryObj of retryRules) {
189
194
  if (!retryObj.when) return retry(err);
190
195
  if (retryObj.when && retryObj.when(err)) return retry(err);
@@ -296,7 +301,7 @@ module.exports = {
296
301
  * @inner
297
302
  */
298
303
  stop() {
299
- debug(this.toString());
304
+ if (process.env.DEBUG) debug(this.toString());
300
305
  log(`${currentQueue()}Stopping recording promises`);
301
306
  running = false;
302
307
  },
@@ -318,7 +323,7 @@ module.exports = {
318
323
  * @inner
319
324
  */
320
325
  scheduled() {
321
- return tasks.join('\n');
326
+ return tasks.slice(-MAX_TASKS).join('\n');
322
327
  },
323
328
 
324
329
  /**
@@ -341,6 +346,14 @@ module.exports = {
341
346
 
342
347
  };
343
348
 
349
+ function getTimeoutPromise(timeoutMs, taskName) {
350
+ let timer;
351
+ if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`);
352
+ return [new Promise((done, reject) => {
353
+ timer = setTimeout(() => { reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)); }, timeoutMs || 2e9);
354
+ }), timer];
355
+ }
356
+
344
357
  function currentQueue() {
345
358
  let session = '';
346
359
  if (sessionId) session = `<${sessionId}> `;
package/lib/step.js CHANGED
@@ -38,6 +38,9 @@ class Step {
38
38
  this.metaStep = undefined;
39
39
  /** @member {string} */
40
40
  this.stack = '';
41
+ /** @member {number} */
42
+ this.totalTimeout = undefined;
43
+
41
44
  this.setTrace();
42
45
  }
43
46
 
@@ -212,16 +215,18 @@ class MetaStep extends Step {
212
215
  step.metaStep = this;
213
216
  };
214
217
  event.dispatcher.prependListener(event.step.before, registerStep);
218
+ let rethrownError = null;
215
219
  try {
216
220
  this.startTime = Date.now();
217
221
  result = fn.apply(this.context, this.args);
218
222
  } catch (error) {
219
- this.status = 'failed';
223
+ this.setStatus('failed');
224
+ rethrownError = error;
220
225
  } finally {
221
226
  this.endTime = Date.now();
222
-
223
227
  event.dispatcher.removeListener(event.step.before, registerStep);
224
228
  }
229
+ if (rethrownError) { throw rethrownError; }
225
230
  return result;
226
231
  }
227
232
  }
package/lib/store.js CHANGED
@@ -5,6 +5,8 @@
5
5
  const store = {
6
6
  /** @type {boolean} */
7
7
  debugMode: false,
8
+ /** @type {boolean} */
9
+ timeouts: true,
8
10
  };
9
11
 
10
12
  module.exports = store;
package/lib/ui.js CHANGED
@@ -65,7 +65,7 @@ module.exports = function (suite) {
65
65
 
66
66
  suite.addTest(scenario.test(test));
67
67
  if (opts.retries) test.retries(opts.retries);
68
- if (opts.timeout) test.timeout(opts.timeout);
68
+ if (opts.timeout) test.totalTimeout = opts.timeout;
69
69
  test.opts = opts;
70
70
 
71
71
  return new ScenarioConfig(test);
@@ -103,7 +103,7 @@ module.exports = function (suite) {
103
103
  suite.timeout(0);
104
104
 
105
105
  if (opts.retries) suite.retries(opts.retries);
106
- if (opts.timeout) suite.timeout(opts.timeout);
106
+ if (opts.timeout) suite.totalTimeout = opts.timeout;
107
107
 
108
108
  suite.tags = title.match(/(\@[a-zA-Z0-9-_]+)/g) || []; // match tags from title
109
109
  suite.file = file;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -10,7 +10,6 @@
10
10
  "webdriver",
11
11
  "testcafe",
12
12
  "playwright",
13
- "protractor",
14
13
  "bdd",
15
14
  "tdd",
16
15
  "testing"
@@ -84,7 +83,8 @@
84
83
  "promise-retry": "^1.1.1",
85
84
  "requireg": "^0.2.2",
86
85
  "resq": "^1.10.0",
87
- "sprintf-js": "^1.1.1"
86
+ "sprintf-js": "^1.1.1",
87
+ "uuid": "^8.3.2"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@codeceptjs/detox-helper": "^1.0.2",
@@ -102,7 +102,7 @@
102
102
  "chai-subset": "^1.6.0",
103
103
  "contributor-faces": "^1.0.3",
104
104
  "documentation": "^12.3.0",
105
- "dtslint": "^3.6.12",
105
+ "dtslint": "^4.1.6",
106
106
  "electron": "^12.0.0",
107
107
  "eslint": "^6.8.0",
108
108
  "eslint-config-airbnb-base": "^14.2.1",
@@ -121,7 +121,6 @@
121
121
  "nightmare": "^3.0.2",
122
122
  "nodemon": "^1.19.4",
123
123
  "playwright": "^1.9.1",
124
- "protractor": "^5.4.4",
125
124
  "puppeteer": "^10.0.0",
126
125
  "qrcode-terminal": "^0.12.0",
127
126
  "rosie": "^1.6.0",
@@ -132,9 +131,9 @@
132
131
  "testcafe": "^1.9.4",
133
132
  "ts-morph": "^3.1.3",
134
133
  "tsd-jsdoc": "^2.5.0",
135
- "typescript": "^3.7.5",
134
+ "typescript": "^4.4.3",
136
135
  "wdio-docker-service": "^1.5.0",
137
- "webdriverio": "^6.10.7",
136
+ "webdriverio": "^7.14.1",
138
137
  "xml2js": "^0.4.23",
139
138
  "xmldom": "^0.1.31",
140
139
  "xpath": "0.0.27"
@@ -66,7 +66,7 @@ declare namespace CodeceptJS {
66
66
  type StringOrSecret = string | CodeceptJS.Secret;
67
67
 
68
68
  interface HookCallback {
69
- (args: SupportObject): void;
69
+ (args: SupportObject): void | Promise<void>;
70
70
  }
71
71
  interface Scenario extends IScenario {
72
72
  only: IScenario;
@@ -111,6 +111,7 @@ declare const pause: typeof CodeceptJS.pause;
111
111
  declare const within: typeof CodeceptJS.within;
112
112
  declare const session: typeof CodeceptJS.session;
113
113
  declare const DataTable: typeof CodeceptJS.DataTable;
114
+ declare const DataTableArgument: typeof CodeceptJS.DataTableArgument;
114
115
  declare const codeceptjs: typeof CodeceptJS.index;
115
116
  declare const locate: typeof CodeceptJS.Locator.build;
116
117
  declare function inject(): CodeceptJS.SupportObject;
@@ -160,6 +161,7 @@ declare namespace NodeJS {
160
161
  within: typeof within;
161
162
  session: typeof session;
162
163
  DataTable: typeof DataTable;
164
+ DataTableArgument: typeof DataTableArgument;
163
165
  locate: typeof locate;
164
166
  inject: typeof inject;
165
167
  secret: typeof secret;
@@ -193,6 +195,7 @@ declare namespace Mocha {
193
195
  }
194
196
 
195
197
  interface Test extends Runnable {
198
+ artifacts: [],
196
199
  tags: any[];
197
200
  }
198
201
  }
@@ -200,3 +203,7 @@ declare namespace Mocha {
200
203
  declare module "codeceptjs" {
201
204
  export = codeceptjs;
202
205
  }
206
+
207
+ declare module "@codeceptjs/helper" {
208
+ export = CodeceptJS.Helper;
209
+ }