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.
Files changed (173) hide show
  1. package/CHANGELOG.md +125 -37
  2. package/README.md +15 -22
  3. package/bin/codecept.js +4 -1
  4. package/docs/acceptance.md +44 -1
  5. package/docs/advanced.md +1 -1
  6. package/docs/angular.md +6 -9
  7. package/docs/basics.md +388 -75
  8. package/docs/bdd.md +4 -3
  9. package/docs/best.md +1 -1
  10. package/docs/books.md +31 -0
  11. package/docs/build/Appium.js +215 -176
  12. package/docs/build/Nightmare.js +618 -489
  13. package/docs/build/Polly.js +189 -0
  14. package/docs/build/Protractor.js +747 -608
  15. package/docs/build/Puppeteer.js +914 -633
  16. package/docs/build/REST.js +1 -1
  17. package/docs/build/TestCafe.js +1835 -0
  18. package/docs/build/WebDriver.js +861 -805
  19. package/docs/build/WebDriverIO.js +616 -617
  20. package/docs/changelog.md +410 -316
  21. package/docs/commands.md +6 -6
  22. package/docs/community-helpers.md +2 -0
  23. package/docs/detox.md +235 -0
  24. package/docs/examples.md +23 -0
  25. package/docs/helpers/ApiDataFactory.md +11 -10
  26. package/docs/helpers/Appium.md +130 -61
  27. package/docs/helpers/Detox.md +579 -0
  28. package/docs/helpers/FileSystem.md +2 -1
  29. package/docs/helpers/Mochawesome.md +1 -0
  30. package/docs/helpers/Nightmare.md +348 -128
  31. package/docs/helpers/Polly.md +85 -0
  32. package/docs/helpers/Protractor.md +451 -184
  33. package/docs/helpers/Puppeteer-firefox.md +55 -0
  34. package/docs/helpers/Puppeteer.md +619 -183
  35. package/docs/helpers/REST.md +17 -16
  36. package/docs/helpers/SeleniumWebdriver.md +9 -8
  37. package/docs/helpers/TestCafe.md +1168 -0
  38. package/docs/helpers/WebDriver.md +600 -291
  39. package/docs/helpers/WebDriverIO.md +393 -278
  40. package/docs/helpers.md +37 -18
  41. package/docs/locators.md +2 -0
  42. package/docs/mobile-react-native-locators.md +64 -0
  43. package/docs/mobile.md +5 -0
  44. package/docs/plugins.md +54 -13
  45. package/docs/puppeteer.md +74 -26
  46. package/docs/quickstart.md +47 -12
  47. package/docs/react.md +67 -0
  48. package/docs/reports.md +1 -1
  49. package/docs/{webapi/_keys.mustache → shared/keys.mustache} +0 -0
  50. package/docs/shared/react.mustache +1 -0
  51. package/docs/testcafe.md +157 -0
  52. package/docs/videos.md +19 -0
  53. package/docs/webapi/amOnPage.mustache +1 -1
  54. package/docs/webapi/appendField.mustache +2 -2
  55. package/docs/webapi/attachFile.mustache +2 -2
  56. package/docs/webapi/checkOption.mustache +2 -2
  57. package/docs/webapi/clearCookie.mustache +1 -1
  58. package/docs/webapi/clearField.mustache +1 -1
  59. package/docs/webapi/click.mustache +2 -2
  60. package/docs/webapi/clickLink.mustache +3 -3
  61. package/docs/webapi/dontSee.mustache +6 -3
  62. package/docs/webapi/dontSeeCheckboxIsChecked.mustache +7 -1
  63. package/docs/webapi/dontSeeCookie.mustache +5 -1
  64. package/docs/webapi/dontSeeCurrentUrlEquals.mustache +6 -1
  65. package/docs/webapi/dontSeeElement.mustache +5 -1
  66. package/docs/webapi/dontSeeElementInDOM.mustache +5 -1
  67. package/docs/webapi/dontSeeInCurrentUrl.mustache +1 -1
  68. package/docs/webapi/dontSeeInField.mustache +7 -2
  69. package/docs/webapi/dontSeeInSource.mustache +5 -1
  70. package/docs/webapi/dontSeeInTitle.mustache +5 -1
  71. package/docs/webapi/doubleClick.mustache +2 -2
  72. package/docs/webapi/downloadFile.mustache +2 -2
  73. package/docs/webapi/dragAndDrop.mustache +2 -2
  74. package/docs/webapi/dragSlider.mustache +2 -2
  75. package/docs/webapi/executeAsyncScript.mustache +1 -1
  76. package/docs/webapi/executeScript.mustache +1 -1
  77. package/docs/webapi/fillField.mustache +2 -2
  78. package/docs/webapi/grabAttributeFrom.mustache +3 -2
  79. package/docs/webapi/grabBrowserLogs.mustache +3 -1
  80. package/docs/webapi/grabCookie.mustache +2 -1
  81. package/docs/webapi/grabCssPropertyFrom.mustache +3 -2
  82. package/docs/webapi/grabCurrentUrl.mustache +3 -1
  83. package/docs/webapi/grabDataFromPerformanceTiming.mustache +19 -0
  84. package/docs/webapi/grabHTMLFrom.mustache +2 -1
  85. package/docs/webapi/grabNumberOfOpenTabs.mustache +4 -2
  86. package/docs/webapi/grabNumberOfVisibleElements.mustache +3 -2
  87. package/docs/webapi/grabPageScrollPosition.mustache +3 -1
  88. package/docs/webapi/grabSource.mustache +3 -1
  89. package/docs/webapi/grabTextFrom.mustache +2 -1
  90. package/docs/webapi/grabTitle.mustache +3 -1
  91. package/docs/webapi/grabValueFrom.mustache +2 -1
  92. package/docs/webapi/moveCursorTo.mustache +3 -3
  93. package/docs/webapi/pressKey.mustache +1 -1
  94. package/docs/webapi/resizeWindow.mustache +2 -2
  95. package/docs/webapi/rightClick.mustache +2 -2
  96. package/docs/webapi/saveScreenshot.mustache +3 -3
  97. package/docs/webapi/say.mustache +2 -2
  98. package/docs/webapi/scrollPageToBottom.mustache +1 -1
  99. package/docs/webapi/scrollPageToTop.mustache +1 -1
  100. package/docs/webapi/scrollTo.mustache +3 -3
  101. package/docs/webapi/see.mustache +2 -2
  102. package/docs/webapi/seeAttributesOnElements.mustache +3 -3
  103. package/docs/webapi/seeCheckboxIsChecked.mustache +2 -1
  104. package/docs/webapi/seeCookie.mustache +1 -1
  105. package/docs/webapi/seeCssPropertiesOnElements.mustache +2 -2
  106. package/docs/webapi/seeCurrentUrlEquals.mustache +1 -1
  107. package/docs/webapi/seeElement.mustache +1 -1
  108. package/docs/webapi/seeElementInDOM.mustache +1 -1
  109. package/docs/webapi/seeInCurrentUrl.mustache +1 -1
  110. package/docs/webapi/seeInField.mustache +2 -2
  111. package/docs/webapi/seeInSource.mustache +1 -1
  112. package/docs/webapi/seeInTitle.mustache +5 -1
  113. package/docs/webapi/seeNumberOfElements.mustache +10 -0
  114. package/docs/webapi/seeNumberOfVisibleElements.mustache +2 -2
  115. package/docs/webapi/selectOption.mustache +2 -2
  116. package/docs/webapi/setCookie.mustache +1 -1
  117. package/docs/webapi/switchTo.mustache +6 -1
  118. package/docs/webapi/uncheckOption.mustache +2 -2
  119. package/docs/webapi/wait.mustache +1 -2
  120. package/docs/webapi/waitForDetached.mustache +3 -3
  121. package/docs/webapi/waitForElement.mustache +2 -2
  122. package/docs/webapi/waitForEnabled.mustache +1 -1
  123. package/docs/webapi/waitForFunction.mustache +3 -3
  124. package/docs/webapi/waitForInvisible.mustache +3 -3
  125. package/docs/webapi/waitForText.mustache +3 -3
  126. package/docs/webapi/waitForValue.mustache +3 -3
  127. package/docs/webapi/waitForVisible.mustache +3 -3
  128. package/docs/webapi/waitInUrl.mustache +2 -2
  129. package/docs/webapi/waitNumberOfVisibleElements.mustache +3 -3
  130. package/docs/webapi/waitToHide.mustache +3 -3
  131. package/docs/webapi/waitUntil.mustache +3 -3
  132. package/docs/webapi/waitUrlEquals.mustache +2 -2
  133. package/docs/webdriver.md +453 -0
  134. package/lib/codecept.js +11 -9
  135. package/lib/command/definitions.js +183 -30
  136. package/lib/command/gherkin/snippets.js +29 -9
  137. package/lib/command/init.js +31 -9
  138. package/lib/command/run-multiple.js +46 -59
  139. package/lib/command/utils.js +1 -1
  140. package/lib/container.js +30 -4
  141. package/lib/data/dataScenarioConfig.js +18 -0
  142. package/lib/helper/Appium.js +24 -24
  143. package/lib/helper/Nightmare.js +81 -84
  144. package/lib/helper/Polly.js +189 -0
  145. package/lib/helper/Protractor.js +96 -86
  146. package/lib/helper/Puppeteer.js +238 -113
  147. package/lib/helper/REST.js +1 -1
  148. package/lib/helper/TestCafe.js +1257 -0
  149. package/lib/helper/WebDriver.js +217 -277
  150. package/lib/helper/WebDriverIO.js +75 -75
  151. package/lib/helper/clientscripts/nightmare.js +8 -0
  152. package/lib/helper/extras/React.js +55 -0
  153. package/lib/helper/testcafe/testControllerHolder.js +42 -0
  154. package/lib/helper/testcafe/testcafe-utils.js +63 -0
  155. package/lib/history.js +39 -0
  156. package/lib/hooks.js +25 -1
  157. package/lib/interfaces/gherkin.js +17 -1
  158. package/lib/interfaces/scenarioConfig.js +2 -2
  159. package/lib/listener/config.js +3 -3
  160. package/lib/locator.js +6 -0
  161. package/lib/pause.js +22 -1
  162. package/lib/plugin/allure.js +63 -0
  163. package/lib/plugin/autoLogin.js +65 -16
  164. package/lib/plugin/puppeteerCoverage.js +6 -1
  165. package/lib/plugin/stepByStepReport.js +4 -3
  166. package/lib/scenario.js +23 -17
  167. package/lib/step.js +5 -2
  168. package/lib/ui.js +1 -1
  169. package/lib/utils.js +70 -20
  170. package/package.json +20 -19
  171. package/translations/de-DE.js +69 -0
  172. package/translations/index.js +1 -0
  173. 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 isAsync(fn) {
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 = {};
@@ -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(Object.assign({}, oldConfig), config));
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
- eval(`I.${cmd}`); // eslint-disable-line no-eval
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);
@@ -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
 
@@ -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
- * admin: {
140
- * login: (I) => {
141
- * I.amOnPage('/login');
142
- * I.fillField('email', 'admin@site.com');
143
- * I.fillField('password', '123456');
144
- * I.click('Login');
145
- * }
146
- * check: (I) => {
147
- * I.amOnPage('/dashboard');
148
- * I.see('Admin', '.navbar');
149
- * },
150
- * fetch: () => {}, // empty function
151
- * restore: () => {}, // empty funciton
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
- await userSession.login(I);
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
- await userSession.restore(I, cookies);
222
- await userSession.check(I);
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
- fs.mkdirSync(coverageDir, { recursive: true });
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 { template, clearString, deleteDir } = require('../utils');
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
- dir = path.join(reportDir, `record_${clearString(test.title).substring(0, 20)}_${uuid}`);
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
- recorder.errHandler((err) => {
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
- recorder.session.start('teardown');
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
- event.emit(event.hook.started, suite);
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') return; // don't track meta steps inside generators
158
- if (fnName[1] === 'recorder') return; // don't track meta steps inside generators
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
@@ -138,7 +138,7 @@ module.exports = function (suite) {
138
138
  * Pending test case.
139
139
  */
140
140
  context.xScenario = context.Scenario.skip = function (title) {
141
- context.Scenario(title, {});
141
+ return context.Scenario(title, {});
142
142
  };
143
143
 
144
144
  addDataContext(context);