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.
- package/CHANGELOG.md +129 -3
- package/README.md +2 -3
- package/bin/codecept.js +1 -0
- package/docs/advanced.md +94 -60
- package/docs/basics.md +1 -1
- package/docs/bdd.md +55 -1
- package/docs/build/Appium.js +106 -34
- package/docs/build/FileSystem.js +1 -0
- package/docs/build/Nightmare.js +48 -48
- package/docs/build/Playwright.js +97 -94
- package/docs/build/Protractor.js +68 -81
- package/docs/build/Puppeteer.js +91 -93
- package/docs/build/REST.js +1 -0
- package/docs/build/TestCafe.js +44 -44
- package/docs/build/WebDriver.js +71 -95
- package/docs/changelog.md +144 -2
- package/docs/commands.md +21 -7
- package/docs/configuration.md +15 -2
- package/docs/custom-helpers.md +1 -36
- package/docs/helpers/Appium.md +97 -95
- package/docs/helpers/FileSystem.md +1 -1
- package/docs/helpers/Playwright.md +16 -18
- package/docs/helpers/Puppeteer.md +18 -18
- package/docs/helpers/REST.md +3 -1
- package/docs/helpers/WebDriver.md +3 -19
- package/docs/mobile-react-native-locators.md +3 -0
- package/docs/playwright.md +40 -0
- package/docs/plugins.md +185 -68
- package/docs/reports.md +23 -5
- package/lib/actor.js +20 -2
- package/lib/codecept.js +15 -2
- package/lib/command/info.js +1 -1
- package/lib/config.js +13 -1
- package/lib/container.js +3 -1
- package/lib/data/dataTableArgument.js +35 -0
- package/lib/helper/Appium.js +49 -4
- package/lib/helper/FileSystem.js +1 -0
- package/lib/helper/Playwright.js +35 -22
- package/lib/helper/Protractor.js +2 -14
- package/lib/helper/Puppeteer.js +20 -19
- package/lib/helper/REST.js +1 -0
- package/lib/helper/WebDriver.js +2 -16
- package/lib/index.js +2 -0
- package/lib/interfaces/featureConfig.js +3 -0
- package/lib/interfaces/gherkin.js +7 -1
- package/lib/interfaces/scenarioConfig.js +4 -0
- package/lib/listener/helpers.js +1 -0
- package/lib/listener/steps.js +21 -3
- package/lib/listener/timeout.js +71 -0
- package/lib/locator.js +3 -0
- package/lib/mochaFactory.js +13 -9
- package/lib/plugin/allure.js +6 -1
- package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
- package/lib/plugin/customLocator.js +2 -2
- package/lib/plugin/retryTo.js +130 -0
- package/lib/plugin/screenshotOnFail.js +1 -0
- package/lib/plugin/stepByStepReport.js +7 -0
- package/lib/plugin/stepTimeout.js +90 -0
- package/lib/plugin/subtitles.js +88 -0
- package/lib/plugin/tryTo.js +1 -1
- package/lib/recorder.js +21 -8
- package/lib/step.js +7 -2
- package/lib/store.js +2 -0
- package/lib/ui.js +2 -2
- package/package.json +6 -7
- package/typings/index.d.ts +8 -1
- package/typings/types.d.ts +198 -82
- package/docs/angular.md +0 -325
- package/docs/helpers/Protractor.md +0 -1658
- package/docs/webapi/waitUntil.mustache +0 -11
- 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
|
+
};
|
package/lib/plugin/tryTo.js
CHANGED
|
@@ -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(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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": "^
|
|
134
|
+
"typescript": "^4.4.3",
|
|
136
135
|
"wdio-docker-service": "^1.5.0",
|
|
137
|
-
"webdriverio": "^
|
|
136
|
+
"webdriverio": "^7.14.1",
|
|
138
137
|
"xml2js": "^0.4.23",
|
|
139
138
|
"xmldom": "^0.1.31",
|
|
140
139
|
"xpath": "0.0.27"
|
package/typings/index.d.ts
CHANGED
|
@@ -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
|
+
}
|