codeceptjs 2.1.3 → 2.2.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/CHANGELOG.md +125 -37
- package/README.md +15 -22
- package/bin/codecept.js +4 -1
- package/docs/acceptance.md +44 -1
- package/docs/advanced.md +1 -1
- package/docs/angular.md +6 -9
- package/docs/basics.md +388 -75
- package/docs/bdd.md +4 -3
- package/docs/best.md +1 -1
- package/docs/books.md +31 -0
- package/docs/build/Appium.js +215 -176
- package/docs/build/Nightmare.js +618 -489
- package/docs/build/Polly.js +189 -0
- package/docs/build/Protractor.js +747 -608
- package/docs/build/Puppeteer.js +914 -633
- package/docs/build/REST.js +1 -1
- package/docs/build/TestCafe.js +1835 -0
- package/docs/build/WebDriver.js +861 -805
- package/docs/build/WebDriverIO.js +616 -617
- package/docs/changelog.md +410 -316
- package/docs/commands.md +6 -6
- package/docs/community-helpers.md +2 -0
- package/docs/detox.md +235 -0
- package/docs/examples.md +23 -0
- package/docs/helpers/ApiDataFactory.md +11 -10
- package/docs/helpers/Appium.md +130 -61
- package/docs/helpers/Detox.md +579 -0
- package/docs/helpers/FileSystem.md +2 -1
- package/docs/helpers/Mochawesome.md +1 -0
- package/docs/helpers/Nightmare.md +348 -128
- package/docs/helpers/Polly.md +85 -0
- package/docs/helpers/Protractor.md +451 -184
- package/docs/helpers/Puppeteer-firefox.md +55 -0
- package/docs/helpers/Puppeteer.md +619 -183
- package/docs/helpers/REST.md +17 -16
- package/docs/helpers/SeleniumWebdriver.md +9 -8
- package/docs/helpers/TestCafe.md +1168 -0
- package/docs/helpers/WebDriver.md +600 -291
- package/docs/helpers/WebDriverIO.md +393 -278
- package/docs/helpers.md +37 -18
- package/docs/locators.md +2 -0
- package/docs/mobile-react-native-locators.md +64 -0
- package/docs/mobile.md +5 -0
- package/docs/plugins.md +54 -13
- package/docs/puppeteer.md +74 -26
- package/docs/quickstart.md +47 -12
- package/docs/react.md +67 -0
- package/docs/reports.md +1 -1
- package/docs/{webapi/_keys.mustache → shared/keys.mustache} +0 -0
- package/docs/shared/react.mustache +1 -0
- package/docs/testcafe.md +157 -0
- package/docs/videos.md +19 -0
- package/docs/webapi/amOnPage.mustache +1 -1
- package/docs/webapi/appendField.mustache +2 -2
- package/docs/webapi/attachFile.mustache +2 -2
- package/docs/webapi/checkOption.mustache +2 -2
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/clearField.mustache +1 -1
- package/docs/webapi/click.mustache +2 -2
- package/docs/webapi/clickLink.mustache +3 -3
- package/docs/webapi/dontSee.mustache +6 -3
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +7 -1
- package/docs/webapi/dontSeeCookie.mustache +5 -1
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +6 -1
- package/docs/webapi/dontSeeElement.mustache +5 -1
- package/docs/webapi/dontSeeElementInDOM.mustache +5 -1
- package/docs/webapi/dontSeeInCurrentUrl.mustache +1 -1
- package/docs/webapi/dontSeeInField.mustache +7 -2
- package/docs/webapi/dontSeeInSource.mustache +5 -1
- package/docs/webapi/dontSeeInTitle.mustache +5 -1
- package/docs/webapi/doubleClick.mustache +2 -2
- package/docs/webapi/downloadFile.mustache +2 -2
- package/docs/webapi/dragAndDrop.mustache +2 -2
- package/docs/webapi/dragSlider.mustache +2 -2
- package/docs/webapi/executeAsyncScript.mustache +1 -1
- package/docs/webapi/executeScript.mustache +1 -1
- package/docs/webapi/fillField.mustache +2 -2
- package/docs/webapi/grabAttributeFrom.mustache +3 -2
- package/docs/webapi/grabBrowserLogs.mustache +3 -1
- package/docs/webapi/grabCookie.mustache +2 -1
- package/docs/webapi/grabCssPropertyFrom.mustache +3 -2
- package/docs/webapi/grabCurrentUrl.mustache +3 -1
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +19 -0
- package/docs/webapi/grabHTMLFrom.mustache +2 -1
- package/docs/webapi/grabNumberOfOpenTabs.mustache +4 -2
- package/docs/webapi/grabNumberOfVisibleElements.mustache +3 -2
- package/docs/webapi/grabPageScrollPosition.mustache +3 -1
- package/docs/webapi/grabSource.mustache +3 -1
- package/docs/webapi/grabTextFrom.mustache +2 -1
- package/docs/webapi/grabTitle.mustache +3 -1
- package/docs/webapi/grabValueFrom.mustache +2 -1
- package/docs/webapi/moveCursorTo.mustache +3 -3
- package/docs/webapi/pressKey.mustache +1 -1
- package/docs/webapi/resizeWindow.mustache +2 -2
- package/docs/webapi/rightClick.mustache +2 -2
- package/docs/webapi/saveScreenshot.mustache +3 -3
- package/docs/webapi/say.mustache +2 -2
- package/docs/webapi/scrollPageToBottom.mustache +1 -1
- package/docs/webapi/scrollPageToTop.mustache +1 -1
- package/docs/webapi/scrollTo.mustache +3 -3
- package/docs/webapi/see.mustache +2 -2
- package/docs/webapi/seeAttributesOnElements.mustache +3 -3
- package/docs/webapi/seeCheckboxIsChecked.mustache +2 -1
- package/docs/webapi/seeCookie.mustache +1 -1
- package/docs/webapi/seeCssPropertiesOnElements.mustache +2 -2
- package/docs/webapi/seeCurrentUrlEquals.mustache +1 -1
- package/docs/webapi/seeElement.mustache +1 -1
- package/docs/webapi/seeElementInDOM.mustache +1 -1
- package/docs/webapi/seeInCurrentUrl.mustache +1 -1
- package/docs/webapi/seeInField.mustache +2 -2
- package/docs/webapi/seeInSource.mustache +1 -1
- package/docs/webapi/seeInTitle.mustache +5 -1
- package/docs/webapi/seeNumberOfElements.mustache +10 -0
- package/docs/webapi/seeNumberOfVisibleElements.mustache +2 -2
- package/docs/webapi/selectOption.mustache +2 -2
- package/docs/webapi/setCookie.mustache +1 -1
- package/docs/webapi/switchTo.mustache +6 -1
- package/docs/webapi/uncheckOption.mustache +2 -2
- package/docs/webapi/wait.mustache +1 -2
- package/docs/webapi/waitForDetached.mustache +3 -3
- package/docs/webapi/waitForElement.mustache +2 -2
- package/docs/webapi/waitForEnabled.mustache +1 -1
- package/docs/webapi/waitForFunction.mustache +3 -3
- package/docs/webapi/waitForInvisible.mustache +3 -3
- package/docs/webapi/waitForText.mustache +3 -3
- package/docs/webapi/waitForValue.mustache +3 -3
- package/docs/webapi/waitForVisible.mustache +3 -3
- package/docs/webapi/waitInUrl.mustache +2 -2
- package/docs/webapi/waitNumberOfVisibleElements.mustache +3 -3
- package/docs/webapi/waitToHide.mustache +3 -3
- package/docs/webapi/waitUntil.mustache +3 -3
- package/docs/webapi/waitUrlEquals.mustache +2 -2
- package/docs/webdriver.md +453 -0
- package/lib/codecept.js +11 -9
- package/lib/command/definitions.js +183 -30
- package/lib/command/gherkin/snippets.js +29 -9
- package/lib/command/init.js +31 -9
- package/lib/command/run-multiple.js +46 -59
- package/lib/command/utils.js +1 -1
- package/lib/container.js +30 -4
- package/lib/data/dataScenarioConfig.js +18 -0
- package/lib/helper/Appium.js +24 -24
- package/lib/helper/Nightmare.js +81 -84
- package/lib/helper/Polly.js +189 -0
- package/lib/helper/Protractor.js +96 -86
- package/lib/helper/Puppeteer.js +238 -113
- package/lib/helper/REST.js +1 -1
- package/lib/helper/TestCafe.js +1257 -0
- package/lib/helper/WebDriver.js +217 -277
- package/lib/helper/WebDriverIO.js +75 -75
- package/lib/helper/clientscripts/nightmare.js +8 -0
- package/lib/helper/extras/React.js +55 -0
- package/lib/helper/testcafe/testControllerHolder.js +42 -0
- package/lib/helper/testcafe/testcafe-utils.js +63 -0
- package/lib/history.js +39 -0
- package/lib/hooks.js +25 -1
- package/lib/interfaces/gherkin.js +17 -1
- package/lib/interfaces/scenarioConfig.js +2 -2
- package/lib/listener/config.js +3 -3
- package/lib/locator.js +6 -0
- package/lib/pause.js +22 -1
- package/lib/plugin/allure.js +63 -0
- package/lib/plugin/autoLogin.js +65 -16
- package/lib/plugin/puppeteerCoverage.js +6 -1
- package/lib/plugin/stepByStepReport.js +4 -3
- package/lib/scenario.js +23 -17
- package/lib/step.js +5 -2
- package/lib/ui.js +1 -1
- package/lib/utils.js +70 -20
- package/package.json +20 -19
- package/translations/de-DE.js +69 -0
- package/translations/index.js +1 -0
- package/docs/video.md +0 -26
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const testControllerHolder = {
|
|
2
|
+
|
|
3
|
+
testController: undefined,
|
|
4
|
+
captureResolver: undefined,
|
|
5
|
+
getResolver: undefined,
|
|
6
|
+
|
|
7
|
+
capture(t) {
|
|
8
|
+
testControllerHolder.testController = t;
|
|
9
|
+
|
|
10
|
+
if (testControllerHolder.getResolver) {
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
testControllerHolder.getResolver(t);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
testControllerHolder.captureResolver = resolve;
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
free() {
|
|
22
|
+
testControllerHolder.testController = undefined;
|
|
23
|
+
|
|
24
|
+
if (testControllerHolder.captureResolver) {
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
testControllerHolder.captureResolver();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
get() {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
if (testControllerHolder.testController) {
|
|
33
|
+
resolve(testControllerHolder.testController);
|
|
34
|
+
} else {
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
testControllerHolder.getResolver = resolve;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = testControllerHolder;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { ClientFunction } = require('testcafe');
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { getParamNames } = require('../../utils');
|
|
7
|
+
|
|
8
|
+
const createTestFile = () => {
|
|
9
|
+
assert(global.output_dir, 'global.output_dir must be set');
|
|
10
|
+
|
|
11
|
+
const testFile = path.join(global.output_dir, `${Date.now()}_test.js`);
|
|
12
|
+
const testControllerHolderDir = __dirname.replace(/\\/g, '/');
|
|
13
|
+
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
testFile,
|
|
16
|
+
`import testControllerHolder from "${testControllerHolderDir}/testControllerHolder.js";\n\n
|
|
17
|
+
fixture("fixture")\n
|
|
18
|
+
test\n
|
|
19
|
+
("test", testControllerHolder.capture)`,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return testFile;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// TODO Better error mapping (actual, expected)
|
|
26
|
+
const mapError = (testcafeError) => {
|
|
27
|
+
// console.log('TODO map error better', JSON.stringify(testcafeError, null, 2));
|
|
28
|
+
if (testcafeError.errMsg) {
|
|
29
|
+
throw new Error(testcafeError.errMsg);
|
|
30
|
+
}
|
|
31
|
+
const errorInfo = `${testcafeError.callsite ? JSON.stringify(testcafeError.callsite) : ''} ${testcafeError.apiFnChain || JSON.stringify(testcafeError)}`;
|
|
32
|
+
throw new Error(`TestCafe Error: ${errorInfo}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
function createClientFunction(func, args) {
|
|
37
|
+
if (!args || !args.length) {
|
|
38
|
+
return ClientFunction(func);
|
|
39
|
+
}
|
|
40
|
+
const paramNames = getParamNames(func);
|
|
41
|
+
const dependencies = {};
|
|
42
|
+
paramNames.forEach((param, i) => dependencies[param] = args[i]);
|
|
43
|
+
|
|
44
|
+
return ClientFunction(getFuncBody(func), { dependencies });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getFuncBody(func) {
|
|
48
|
+
let fnStr = func.toString();
|
|
49
|
+
const arrowIndex = fnStr.indexOf('=>');
|
|
50
|
+
if (arrowIndex >= 0) {
|
|
51
|
+
fnStr = fnStr.slice(arrowIndex + 2);
|
|
52
|
+
// eslint-disable-next-line no-new-func
|
|
53
|
+
// eslint-disable-next-line no-eval
|
|
54
|
+
return eval(`() => ${fnStr}`);
|
|
55
|
+
}
|
|
56
|
+
// TODO: support general functions
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
createTestFile,
|
|
61
|
+
mapError,
|
|
62
|
+
createClientFunction,
|
|
63
|
+
};
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const output = require('./output');
|
|
5
|
+
const colors = require('chalk');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* REPL history records REPL commands and stores them in
|
|
9
|
+
* a file (~history) when session ends.
|
|
10
|
+
*/
|
|
11
|
+
class ReplHistory {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.commands = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
push(cmd) {
|
|
17
|
+
this.commands.push(cmd);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pop() {
|
|
21
|
+
this.commands.pop();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
save() {
|
|
25
|
+
if (this.commands.length === 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const historyFile = path.join(global.output_dir, 'cli-history');
|
|
30
|
+
const commandSnippet = `\n\n<<< Recorded commands on ${new Date()}\n${this.commands.join('\n')}`;
|
|
31
|
+
fs.appendFileSync(historyFile, commandSnippet);
|
|
32
|
+
|
|
33
|
+
output.print(colors.yellow(` Commands have been saved to ${historyFile}`));
|
|
34
|
+
|
|
35
|
+
this.commands = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = new ReplHistory();
|
package/lib/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const getParamNames = require('./utils').getParamNames;
|
|
2
2
|
const fsPath = require('path');
|
|
3
3
|
const fileExists = require('./utils').fileExists;
|
|
4
|
+
const output = require('./output');
|
|
4
5
|
|
|
5
6
|
module.exports = function (hook, done, stage) {
|
|
6
7
|
stage = stage || 'bootstrap';
|
|
@@ -39,6 +40,8 @@ function loadCustomHook(module) {
|
|
|
39
40
|
|
|
40
41
|
function callSync(callable, done) {
|
|
41
42
|
if (isAsync(callable)) {
|
|
43
|
+
callAsync(callable, done, hasArguments(callable));
|
|
44
|
+
} else if (hasArguments(callable)) {
|
|
42
45
|
callable(done);
|
|
43
46
|
} else {
|
|
44
47
|
callable();
|
|
@@ -46,7 +49,28 @@ function callSync(callable, done) {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function
|
|
52
|
+
function callAsync(callable, done, hasArgs = false) {
|
|
53
|
+
let called = new Promise(() => {});
|
|
54
|
+
|
|
55
|
+
if (done) {
|
|
56
|
+
if (hasArgs) called = callable(done);
|
|
57
|
+
else called = callable().then(() => done());
|
|
58
|
+
} else {
|
|
59
|
+
called = callable();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
called.catch((err) => {
|
|
63
|
+
output.print('');
|
|
64
|
+
output.error(err.message);
|
|
65
|
+
output.print('');
|
|
66
|
+
output.print(output.colors.grey(err.stack.replace(err.message, '')));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isAsync = fn => fn.constructor.name === 'AsyncFunction';
|
|
72
|
+
|
|
73
|
+
function hasArguments(fn) {
|
|
50
74
|
const params = getParamNames(fn);
|
|
51
75
|
return params && params.length;
|
|
52
76
|
}
|
|
@@ -73,7 +73,7 @@ module.exports = (text) => {
|
|
|
73
73
|
}
|
|
74
74
|
const tags = child.tags.map(t => t.name);
|
|
75
75
|
const title = `${child.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
|
|
76
|
-
const test = new Test(title, async () => runSteps(exampleSteps));
|
|
76
|
+
const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
|
|
77
77
|
test.tags = suite.tags.concat(tags);
|
|
78
78
|
suite.addTest(scenario.test(test));
|
|
79
79
|
}
|
|
@@ -99,3 +99,19 @@ function transformTable(table) {
|
|
|
99
99
|
}
|
|
100
100
|
return str;
|
|
101
101
|
}
|
|
102
|
+
function addExampleInTable(exampleSteps, placeholders) {
|
|
103
|
+
const steps = JSON.parse(JSON.stringify(exampleSteps));
|
|
104
|
+
for (const placeholder in placeholders) {
|
|
105
|
+
steps.map((step) => {
|
|
106
|
+
step = Object.assign({}, step);
|
|
107
|
+
if (step.argument && step.argument.type === 'DataTable') {
|
|
108
|
+
for (const id in step.argument.rows) {
|
|
109
|
+
const cells = step.argument.rows[id].cells;
|
|
110
|
+
cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return step;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return steps;
|
|
117
|
+
}
|
|
@@ -58,13 +58,13 @@ class ScenarioConfig {
|
|
|
58
58
|
* Configures a helper.
|
|
59
59
|
* Helper name can be omitted and values will be applied to first helper.
|
|
60
60
|
*/
|
|
61
|
-
config(helper, obj) {
|
|
61
|
+
async config(helper, obj) {
|
|
62
62
|
if (!obj) {
|
|
63
63
|
obj = helper;
|
|
64
64
|
helper = 0;
|
|
65
65
|
}
|
|
66
66
|
if (typeof obj === 'function') {
|
|
67
|
-
obj = obj(this.test);
|
|
67
|
+
obj = await obj(this.test);
|
|
68
68
|
}
|
|
69
69
|
if (!this.test.config) {
|
|
70
70
|
this.test.config = {};
|
package/lib/listener/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const event = require('../event');
|
|
2
2
|
const container = require('../container');
|
|
3
3
|
const recorder = require('../recorder');
|
|
4
|
-
const { deepMerge, ucfirst } = require('../utils');
|
|
4
|
+
const { deepMerge, deepClone, ucfirst } = require('../utils');
|
|
5
5
|
const { debug } = require('../output');
|
|
6
6
|
/**
|
|
7
7
|
* Enable Helpers to listen to test events
|
|
@@ -17,7 +17,7 @@ module.exports = function () {
|
|
|
17
17
|
function updateHelperConfig(helper, config) {
|
|
18
18
|
const oldConfig = Object.assign({}, helper.options);
|
|
19
19
|
try {
|
|
20
|
-
helper._setConfig(deepMerge(
|
|
20
|
+
helper._setConfig(deepMerge(deepClone(oldConfig), config));
|
|
21
21
|
debug(`[${ucfirst(type)} Config] ${helper.constructor.name} ${JSON.stringify(config)}`);
|
|
22
22
|
} catch (err) {
|
|
23
23
|
recorder.throw(err);
|
|
@@ -34,7 +34,7 @@ module.exports = function () {
|
|
|
34
34
|
for (let name in context.config) {
|
|
35
35
|
const config = context.config[name];
|
|
36
36
|
if (name === '0') { // first helper
|
|
37
|
-
name = Object.keys(helpers);
|
|
37
|
+
name = Object.keys(helpers)[0];
|
|
38
38
|
}
|
|
39
39
|
const helper = helpers[name];
|
|
40
40
|
updateHelperConfig(helper, config);
|
package/lib/locator.js
CHANGED
|
@@ -207,6 +207,9 @@ Locator.clickable = {
|
|
|
207
207
|
`.//label[contains(normalize-space(string(.)), ${literal})]`,
|
|
208
208
|
`.//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = ${literal}]`,
|
|
209
209
|
`.//button[./@name = ${literal}]`,
|
|
210
|
+
`.//*[@aria-label = ${literal}]`,
|
|
211
|
+
`.//*[@title = ${literal}]`,
|
|
212
|
+
`.//*[@aria-labelledby = //*[normalize-space(string(.)) = ${literal}]/@id ]`,
|
|
210
213
|
]),
|
|
211
214
|
|
|
212
215
|
self: literal => `./self::*[contains(normalize-space(string(.)), ${literal}) or contains(normalize-space(@value), ${literal})]`,
|
|
@@ -220,6 +223,9 @@ Locator.field = {
|
|
|
220
223
|
labelContains: literal => xpathLocator.combine([
|
|
221
224
|
`.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = ${literal}) or ./@id = //label[contains(normalize-space(string(.)), ${literal})]/@for) or ./@placeholder = ${literal})]`,
|
|
222
225
|
`.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
|
|
226
|
+
`.//*[@aria-label = ${literal}]`,
|
|
227
|
+
`.//*[@title = ${literal}]`,
|
|
228
|
+
`.//*[@aria-labelledby = //*[normalize-space(string(.)) = ${literal}]/@id ]`,
|
|
223
229
|
]),
|
|
224
230
|
byName: literal => `.//*[self::input | self::textarea | self::select][@name = ${literal}]`,
|
|
225
231
|
byText: literal => xpathLocator.combine([
|
package/lib/pause.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const container = require('./container');
|
|
2
|
+
const history = require('./history');
|
|
2
3
|
const store = require('./store');
|
|
3
4
|
const recorder = require('./recorder');
|
|
4
5
|
const event = require('./event');
|
|
@@ -58,18 +59,38 @@ function parseInput(cmd) {
|
|
|
58
59
|
finish();
|
|
59
60
|
recorder.session.restore();
|
|
60
61
|
rl.close();
|
|
62
|
+
history.save();
|
|
61
63
|
return nextStep();
|
|
62
64
|
}
|
|
63
65
|
store.debugMode = true;
|
|
64
66
|
try {
|
|
65
67
|
const locate = global.locate; // enable locate in this context
|
|
66
68
|
const I = container.support('I');
|
|
67
|
-
|
|
69
|
+
|
|
70
|
+
const fullCommand = `I.${cmd}`;
|
|
71
|
+
const result = eval(fullCommand); // eslint-disable-line no-eval
|
|
72
|
+
result.then((val) => {
|
|
73
|
+
if (cmd.startsWith('see') || cmd.startsWith('dontSee')) {
|
|
74
|
+
output.print(output.styles.success(' OK '), cmd);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (cmd.startsWith('grab')) {
|
|
78
|
+
output.print(output.styles.debug(val));
|
|
79
|
+
}
|
|
80
|
+
}).catch((err) => {
|
|
81
|
+
if (err.message) output.print(output.styles.error(' ERROR '), err.message);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
history.push(fullCommand); // add command to history when successful
|
|
68
85
|
} catch (err) {
|
|
69
86
|
output.print(output.styles.error(' ERROR '), err.message);
|
|
70
87
|
}
|
|
71
88
|
recorder.session.catch((err) => {
|
|
72
89
|
const msg = err.cliMessage ? err.cliMessage() : err.message;
|
|
90
|
+
|
|
91
|
+
// pop latest command from history because it failed
|
|
92
|
+
history.pop();
|
|
93
|
+
|
|
73
94
|
return output.print(output.styles.error(' FAIL '), msg);
|
|
74
95
|
});
|
|
75
96
|
recorder.add('ask for next step', askForStep);
|
package/lib/plugin/allure.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const event = require('../event');
|
|
2
2
|
const Allure = require('allure-js-commons');
|
|
3
3
|
const logger = require('../output');
|
|
4
|
+
const ansiRegExp = require('../utils').ansiRegExp;
|
|
4
5
|
|
|
5
6
|
const defaultConfig = {
|
|
6
7
|
outputDir: global.output_dir,
|
|
@@ -61,6 +62,12 @@ const defaultConfig = {
|
|
|
61
62
|
*
|
|
62
63
|
* * `addAttachment(name, buffer, type)` - add an attachment to current test / suite
|
|
63
64
|
* * `addLabel(name, value)` - adds a label to current test
|
|
65
|
+
* * `severity(value)` - adds severity label
|
|
66
|
+
* * `epic(value)` - adds epic label
|
|
67
|
+
* * `feature(value)` - adds feature label
|
|
68
|
+
* * `story(value)` - adds story label
|
|
69
|
+
* * `issue(value)` - adds issue label
|
|
70
|
+
* * `setDescription(description, type)` - sets a description
|
|
64
71
|
*
|
|
65
72
|
*/
|
|
66
73
|
module.exports = (config) => {
|
|
@@ -77,6 +84,60 @@ module.exports = (config) => {
|
|
|
77
84
|
reporter.addAttachment(name, buffer, type);
|
|
78
85
|
};
|
|
79
86
|
|
|
87
|
+
this.setDescription = (description, type) => {
|
|
88
|
+
reporter.setDescription(description, type);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.createStep = (name, stepFunc = () => {}) => {
|
|
92
|
+
let result;
|
|
93
|
+
let status = 'passed';
|
|
94
|
+
reporter.startStep(name);
|
|
95
|
+
try {
|
|
96
|
+
result = stepFunc(this.arguments);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
status = 'broken';
|
|
99
|
+
throw error;
|
|
100
|
+
} finally {
|
|
101
|
+
if (!!result
|
|
102
|
+
&& (typeof result === 'object' || typeof result === 'function')
|
|
103
|
+
&& typeof result.then === 'function'
|
|
104
|
+
) {
|
|
105
|
+
result.then(() => reporter.endStep('passed'), () => reporter.endStep('broken'));
|
|
106
|
+
} else {
|
|
107
|
+
reporter.endStep(status);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.createAttachment = (name, content, type) => {
|
|
114
|
+
if (typeof content === 'function') {
|
|
115
|
+
const attachmentName = name;
|
|
116
|
+
const buffer = content.apply(this, arguments);
|
|
117
|
+
return createAttachment(attachmentName, buffer, type);
|
|
118
|
+
} reporter.addAttachment(name, content, type);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.severity = (severity) => {
|
|
122
|
+
this.addLabel('severity', severity);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
this.epic = (epic) => {
|
|
126
|
+
this.addLabel('epic', epic);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
this.feature = (feature) => {
|
|
130
|
+
this.addLabel('feature', feature);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
this.story = (story) => {
|
|
134
|
+
this.addLabel('story', story);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this.issue = (issue) => {
|
|
138
|
+
this.addLabel('issue', issue);
|
|
139
|
+
};
|
|
140
|
+
|
|
80
141
|
this.addLabel = (name, value) => {
|
|
81
142
|
const currentTest = reporter.getCurrentTest();
|
|
82
143
|
if (currentTest) {
|
|
@@ -133,6 +194,8 @@ module.exports = (config) => {
|
|
|
133
194
|
currentMetaStep.forEach(() => reporter.endStep('failed'));
|
|
134
195
|
currentMetaStep = [];
|
|
135
196
|
}
|
|
197
|
+
|
|
198
|
+
err.message = err.message.replace(ansiRegExp(), '');
|
|
136
199
|
reporter.endCase('failed', err);
|
|
137
200
|
});
|
|
138
201
|
|
package/lib/plugin/autoLogin.js
CHANGED
|
@@ -5,6 +5,7 @@ const container = require('../container');
|
|
|
5
5
|
const store = require('../store');
|
|
6
6
|
const recorder = require('../recorder');
|
|
7
7
|
const debug = require('../output').debug;
|
|
8
|
+
const isAsyncFunction = require('../utils').isAsyncFunction;
|
|
8
9
|
|
|
9
10
|
const defaultUser = {
|
|
10
11
|
fetch: I => I.grabCookie(),
|
|
@@ -136,19 +137,21 @@ const defaultConfig = {
|
|
|
136
137
|
* },
|
|
137
138
|
* plugins: {
|
|
138
139
|
* autoLogin: {
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* I
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* I
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
140
|
+
* users: {
|
|
141
|
+
* admin: {
|
|
142
|
+
* login: (I) => {
|
|
143
|
+
* I.amOnPage('/login');
|
|
144
|
+
* I.fillField('email', 'admin@site.com');
|
|
145
|
+
* I.fillField('password', '123456');
|
|
146
|
+
* I.click('Login');
|
|
147
|
+
* },
|
|
148
|
+
* check: (I) => {
|
|
149
|
+
* I.amOnPage('/dashboard');
|
|
150
|
+
* I.see('Admin', '.navbar');
|
|
151
|
+
* },
|
|
152
|
+
* fetch: () => {}, // empty function
|
|
153
|
+
* restore: () => {}, // empty funciton
|
|
154
|
+
* }
|
|
152
155
|
* }
|
|
153
156
|
* }
|
|
154
157
|
* }
|
|
@@ -176,6 +179,40 @@ const defaultConfig = {
|
|
|
176
179
|
* }
|
|
177
180
|
* ```
|
|
178
181
|
*
|
|
182
|
+
* #### Tips: Using async function in the autoLogin
|
|
183
|
+
*
|
|
184
|
+
* If you use async functions in the autoLogin plugin, login function should be used with `await` keyword.
|
|
185
|
+
*
|
|
186
|
+
* ```js
|
|
187
|
+
* autoLogin: {
|
|
188
|
+
* enabled: true,
|
|
189
|
+
* saveToFile: true,
|
|
190
|
+
* inject: 'login',
|
|
191
|
+
* users: {
|
|
192
|
+
* admin: {
|
|
193
|
+
* login: async (I) => { // If you use async function in the autoLogin plugin
|
|
194
|
+
* const phrase = await I.grabTextFrom('#phrase')
|
|
195
|
+
* I.fillField('username', 'admin'),
|
|
196
|
+
* I.fillField('password', 'password')
|
|
197
|
+
* I.fillField('phrase', phrase)
|
|
198
|
+
* },
|
|
199
|
+
* check: (I) => {
|
|
200
|
+
* I.amOnPage('/');
|
|
201
|
+
* I.see('Admin');
|
|
202
|
+
* }
|
|
203
|
+
* }
|
|
204
|
+
* }
|
|
205
|
+
* }
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* ```js
|
|
209
|
+
* Scenario('login', async (I, login) => {
|
|
210
|
+
* await login('admin') // you should use `await`
|
|
211
|
+
* })
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
*
|
|
215
|
+
*
|
|
179
216
|
*/
|
|
180
217
|
module.exports = function (config) {
|
|
181
218
|
config = Object.assign(defaultConfig, config);
|
|
@@ -200,9 +237,16 @@ module.exports = function (config) {
|
|
|
200
237
|
const userSession = config.users[name];
|
|
201
238
|
const I = container.support('I');
|
|
202
239
|
const cookies = store[`${name}_session`];
|
|
240
|
+
const shouldAwait = isAsyncFunction(userSession.login)
|
|
241
|
+
|| isAsyncFunction(userSession.restore)
|
|
242
|
+
|| isAsyncFunction(userSession.check);
|
|
203
243
|
|
|
204
244
|
const loginAndSave = async () => {
|
|
205
|
-
|
|
245
|
+
if (shouldAwait) {
|
|
246
|
+
await userSession.login(I);
|
|
247
|
+
} else {
|
|
248
|
+
userSession.login(I);
|
|
249
|
+
}
|
|
206
250
|
store.debugMode = true;
|
|
207
251
|
const cookies = await userSession.fetch(I);
|
|
208
252
|
if (config.saveToFile) {
|
|
@@ -218,8 +262,13 @@ module.exports = function (config) {
|
|
|
218
262
|
store.debugMode = true;
|
|
219
263
|
|
|
220
264
|
recorder.session.start('check login');
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
if (shouldAwait) {
|
|
266
|
+
await userSession.restore(I, cookies);
|
|
267
|
+
await userSession.check(I);
|
|
268
|
+
} else {
|
|
269
|
+
userSession.restore(I, cookies);
|
|
270
|
+
userSession.check(I);
|
|
271
|
+
}
|
|
223
272
|
recorder.session.catch((err) => {
|
|
224
273
|
debug(`Failed auto login for ${name} due to ${err}`);
|
|
225
274
|
debug('Logging in again');
|
|
@@ -130,7 +130,12 @@ module.exports = function (config) {
|
|
|
130
130
|
process.cwd(),
|
|
131
131
|
options.coverageDir,
|
|
132
132
|
);
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
// Checking if coverageDir already exists, if not, create new one
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(coverageDir)) {
|
|
137
|
+
fs.mkdirSync(coverageDir, { recursive: true });
|
|
138
|
+
}
|
|
134
139
|
|
|
135
140
|
const coveragePath = path.resolve(
|
|
136
141
|
coverageDir,
|
|
@@ -7,7 +7,8 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const figures = require('figures');
|
|
9
9
|
const colors = require('chalk');
|
|
10
|
-
const
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { template, deleteDir } = require('../utils');
|
|
11
12
|
|
|
12
13
|
const supportedHelpers = [
|
|
13
14
|
'WebDriverIO',
|
|
@@ -84,13 +85,13 @@ module.exports = function (config) {
|
|
|
84
85
|
let slides = {};
|
|
85
86
|
let error;
|
|
86
87
|
let savedStep = null;
|
|
87
|
-
const uuid = Math.floor(new Date().getTime() / 1000);
|
|
88
88
|
const recordedTests = {};
|
|
89
89
|
const pad = '0000';
|
|
90
90
|
const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output;
|
|
91
91
|
|
|
92
92
|
event.dispatcher.on(event.test.before, (test) => {
|
|
93
|
-
|
|
93
|
+
const md5hash = crypto.createHash('md5').update(test.file + test.title).digest('hex');
|
|
94
|
+
dir = path.join(reportDir, `record_${md5hash}`);
|
|
94
95
|
mkdirp.sync(dir);
|
|
95
96
|
stepNum = 0;
|
|
96
97
|
error = null;
|
package/lib/scenario.js
CHANGED
|
@@ -95,21 +95,32 @@ module.exports.test = (test) => {
|
|
|
95
95
|
*/
|
|
96
96
|
module.exports.injected = function (fn, suite, hookName) {
|
|
97
97
|
return function (done) {
|
|
98
|
-
|
|
98
|
+
const errHandler = (err) => {
|
|
99
99
|
recorder.session.start('teardown');
|
|
100
100
|
recorder.cleanAsyncErr();
|
|
101
101
|
event.emit(event.test.failed, suite, err);
|
|
102
102
|
if (hookName === 'after') event.emit(event.test.after, suite);
|
|
103
103
|
if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
|
|
104
104
|
recorder.add(() => done(err));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
recorder.errHandler((err) => {
|
|
108
|
+
errHandler(err);
|
|
105
109
|
});
|
|
106
110
|
|
|
107
111
|
if (!fn) throw new Error('fn is not defined');
|
|
108
112
|
|
|
113
|
+
event.emit(event.hook.started, suite);
|
|
114
|
+
if (!recorder.isRunning()) {
|
|
115
|
+
recorder.start();
|
|
116
|
+
recorder.errHandler((err) => {
|
|
117
|
+
errHandler(err);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.test.body = fn.toString();
|
|
122
|
+
|
|
109
123
|
if (isAsyncFunction(fn)) {
|
|
110
|
-
event.emit(event.hook.started, suite);
|
|
111
|
-
recorder.startUnlessRunning();
|
|
112
|
-
this.test.body = fn.toString();
|
|
113
124
|
fn.apply(this, getInjectedArguments(fn)).then(() => {
|
|
114
125
|
recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
|
|
115
126
|
recorder.add(`finish ${hookName} hook`, () => done());
|
|
@@ -118,28 +129,23 @@ module.exports.injected = function (fn, suite, hookName) {
|
|
|
118
129
|
recorder.throw(e);
|
|
119
130
|
recorder.catch((e) => {
|
|
120
131
|
const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr();
|
|
121
|
-
|
|
122
|
-
recorder.cleanAsyncErr();
|
|
123
|
-
event.emit(event.test.failed, suite, err);
|
|
124
|
-
if (hookName === 'after') event.emit(event.test.after, suite);
|
|
125
|
-
if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
|
|
126
|
-
recorder.add(() => done(err));
|
|
132
|
+
errHandler(err);
|
|
127
133
|
});
|
|
128
134
|
});
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
try {
|
|
133
|
-
|
|
134
|
-
recorder.startUnlessRunning();
|
|
135
|
-
this.test.body = fn.toString();
|
|
136
|
-
const res = fn.apply(this, getInjectedArguments(fn));
|
|
137
|
-
} catch (err) {
|
|
138
|
-
recorder.throw(err);
|
|
139
|
-
} finally {
|
|
139
|
+
fn.apply(this, getInjectedArguments(fn));
|
|
140
140
|
recorder.add('fire hook.passed', () => event.emit(event.hook.passed, suite));
|
|
141
141
|
recorder.add(`finish ${hookName} hook`, () => done());
|
|
142
142
|
recorder.catch();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
recorder.throw(err);
|
|
145
|
+
recorder.catch((e) => {
|
|
146
|
+
const err = (recorder.getAsyncErr() === null) ? e : recorder.getAsyncErr();
|
|
147
|
+
errHandler(err);
|
|
148
|
+
});
|
|
143
149
|
}
|
|
144
150
|
};
|
|
145
151
|
};
|
package/lib/step.js
CHANGED
|
@@ -154,8 +154,11 @@ function detectMetaStep(stack) {
|
|
|
154
154
|
if (isTest(line) || isBDD(line)) break;
|
|
155
155
|
const fnName = line.match(/^at (\w+)\.(\w+)\s\(/);
|
|
156
156
|
if (!fnName) continue;
|
|
157
|
-
if (fnName[1] === 'Generator'
|
|
158
|
-
|
|
157
|
+
if (fnName[1] === 'Generator'
|
|
158
|
+
|| fnName[1] === 'recorder'
|
|
159
|
+
|| fnName[1] === 'Runner'
|
|
160
|
+
) { return; } // don't track meta steps inside generators
|
|
161
|
+
|
|
159
162
|
if (fnName[1] === 'Object') {
|
|
160
163
|
// detect PO name from includes
|
|
161
164
|
for (const name in support) {
|
package/lib/ui.js
CHANGED