Repository: Codeception/CodeceptJS (js)

Issue:
I.say() does not join the Allure report.
#### What are you trying to achieve?
Get the `I.say` step in Allure report.
#### What do you get instead?
Miss `I.say` step inside Allure report.
> Console output with `npx codeceptjs run --plugins allure --steps`:

```bash
I click "[data-test-selector=submit-
Вижу ошибку 
I wait for element "[data-test-selector=username-error]", 4
```

> Test source code:

```js
# auth_test.js
Feature('Auth');
Scenario('Demo.', async (I) => {
    I.click(loginPage.submitButton);
    I.say('Вижу ошибку логина!');
    I.waitForElement(loginPage.error.login, 4);
});
```
> Allure report output:

![Screenshot from 2019-12-24 15-25-07](https://user-images.githubusercontent.com/18244131/71408342-67546c00-2670-11ea-881e-b1f2b1b6d088.png)

### Details

* CodeceptJS version: v2.3.3
* NodeJS Version: v13.3.0
* Operating System: Ubuntu, Linux 4.15.0-72-generic x86_64
* puppeteer: v1.20.0
* Configuration file: 

```js
  plugins: {
    allure: {
      enabled: true,
      outputDir: './output/allure-results'
    }
}
```

Pull request description:
(feat): add I say event to allure
## Motivation/Description of the PR
Resolves #2104 

I think the timestamps could be left as 0s or any suggestion on this would be nice

<img width="621" alt="Screenshot 2020-01-17 at 14 15 57" src="https://user-images.githubusercontent.com/7845001/72615029-01d46200-3934-11ea-889d-b7658fb12744.png">


## Type of change

- [ ] Breaking changes
- [x] New functionality
- [ ] Bug fix
- [ ] Documentation changes/updates
- [ ] Hot fix
- [ ] Markdown files fix - not related to source code

Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index c5ec2fae..af6b42ef 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -11,7 +11,7 @@ Applicable helpers:
 - [ ] TestCafe
 
 - Description of this PR, which problem it solves
-- A link to the corresponding issue (if applicable).
+- Resolves #issueId (if applicable).
 
 ## Type of change
 
@@ -29,4 +29,4 @@ Applicable helpers:
 - [ ] Tests have been added
 - [ ] Documentation has been added (Run `npm run docs`)
 - [ ] Lint checking (Run `npm run lint`)
-- [ ] Local tests are passed (Run `npm test`)
\ No newline at end of file
+- [ ] Local tests are passed (Run `npm test`)
diff --git a/docs/basics.md b/docs/basics.md
index 42f714f5..d1e08937 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -422,7 +422,8 @@ Try to perform your scenario step by step. Then copy succesful commands and inse
 
 ### Pause
 
-Test execution can be paused in any place of a test with `pause()` call.
+Test execution can be paused in any place of a test with `pause()` call. 
+Variables can also be passed to `pause({data: 'hi', func: () => console.log('hello')})` which can be accessed in Interactive shell.
 
 This launches the interactive console where you can call any action from the `I` object.
 
@@ -432,8 +433,17 @@ This launches the interactive console where you can call any action from the `I`
  - Use JavaScript syntax to try steps in action
  - Press TAB twice to see all available commands
  - Enter next to run the next step
+ - Prefix => to run js commands
+
+ Registered variables : data, func
+
  I.click
+ I.see(data)
+```
 
+To run any JS commands prefix `=>` and execute the command.
+```
+I.=> func()
 ```
 
 Type in different actions to try them, copy and paste successful ones into the test file.
diff --git a/docs/translation.md b/docs/translation.md
index bfdc595c..e0d85655 100644
--- a/docs/translation.md
+++ b/docs/translation.md
@@ -75,7 +75,7 @@ Add to config
 Now you can write test like this:
 
 ```js
-Scenario('Effettuare il Login su GitHub', (io) => {
+Caratteristica('Effettuare il Login su GitHub', (io) => {
     io.sono_sulla_pagina('https://github.com/login');
     io.compilo_il_campo("Username or email address", "giuseppe-santoro");
     io.compilo_il_campo("Password", "*********");
@@ -177,6 +177,14 @@ Create translation file like this:
 ```js
 module.exports = {
   I: '',
+  contexts: {
+    Feature: 'Feature',
+    Scenario: 'Szenario',
+    Before: 'Vor',
+    After: 'Nach',
+    BeforeSuite: 'vor_der_suite',
+    AfterSuite: 'nach_der_suite',
+  },
   actions: {
     click: 'Klicken',
     wait: 'Wartenn',
diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js
index bf1a1e2d..938eb6b4 100644
--- a/lib/command/run-workers.js
+++ b/lib/command/run-workers.js
@@ -22,7 +22,7 @@ const stats = {
 let numberOfWorkersClosed = 0;
 const hasFailures = () => stats.failures || errors.length;
 const pathToWorker = path.join(__dirname, 'workers', 'runTests.js');
-const finishedTests = [];
+const finishedTests = {};
 const errors = [];
 
 module.exports = function (workers, options) {
@@ -87,10 +87,12 @@ module.exports = function (workers, options) {
 
       switch (message.event) {
         case event.test.failed:
-          finishedTests.push(repackTest(message.data));
+          updateFinishedTests(repackTest(message.data));
           output.test.failed(repackTest(message.data));
           break;
-        case event.test.passed: output.test.passed(repackTest(message.data)); break;
+        case event.test.passed: output.test.passed(repackTest(message.data));
+          updateFinishedTests(repackTest(message.data));
+          break;
         case event.suite.before: output.suite.started(message.data); break;
         case event.all.after: appendStats(message.data); break;
       }
@@ -125,7 +127,10 @@ function printResults() {
   if (stats.failures) {
     output.print();
     output.print('-- FAILURES:');
-    Base.list(finishedTests.filter(t => t.err));
+    const failedList = Object.keys(finishedTests)
+      .filter(key => finishedTests[key].err)
+      .map(key => finishedTests[key]);
+    Base.list(failedList);
   }
 }
 
@@ -209,3 +214,12 @@ function simplifyObject(object) {
       return obj;
     }, {});
 }
+
+const updateFinishedTests = (test) => {
+  const { id } = test;
+  if (finishedTests[id]) {
+    const stats = { passes: 0, failures: -1, tests: 0 };
+    appendStats(stats);
+  }
+  finishedTests[id] = test;
+};
diff --git a/lib/pause.js b/lib/pause.js
index 91ae5f5e..550dd0c2 100644
--- a/lib/pause.js
+++ b/lib/pause.js
@@ -14,15 +14,16 @@ let rl;
 let nextStep;
 let finish;
 let next;
-
+let registeredVariables;
 /**
  * Pauses test execution and starts interactive shell
  */
-const pause = function () {
+const pause = function (passedObject) {
   if (store.dryRun) return;
 
   next = false;
   // add listener to all next steps to provide next() functionality
+  registeredVariables = setPassedVariables(passedObject);
   event.dispatcher.on(event.step.after, () => {
     recorder.add('Start next pause session', () => {
       if (!next) return;
@@ -40,6 +41,11 @@ function pauseSession() {
     output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`));
     output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`));
     output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`));
+    output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands`));
+    if (registeredVariables && registeredVariables.length > 0) {
+      output.print();
+      output.print(colors.yellow(` Registered variables : ${colors.bold(registeredVariables.join(', '))}`));
+    }
   }
   rl = readline.createInterface(process.stdin, process.stdout, completer);
 
@@ -70,15 +76,20 @@ function parseInput(cmd) {
   try {
     const locate = global.locate; // enable locate in this context
     const I = container.support('I');
+    if (cmd.trim().startsWith('=>')) {
+      cmd = cmd.trim().substring(2, cmd.length);
+    } else {
+      cmd = `I.${cmd}`;
+    }
+    const executeCommand = eval(cmd); // eslint-disable-line no-eval
 
-    const fullCommand = `I.${cmd}`;
-    const result = eval(fullCommand); // eslint-disable-line no-eval
+    const result = executeCommand instanceof Promise ? executeCommand : Promise.resolve(executeCommand);
     result.then((val) => {
-      if (cmd.startsWith('see') || cmd.startsWith('dontSee')) {
+      if (cmd.startsWith('I.see') || cmd.startsWith('I.dontSee')) {
         output.print(output.styles.success('  OK  '), cmd);
         return;
       }
-      if (cmd.startsWith('grab')) {
+      if (cmd.startsWith('I.grab')) {
         output.print(output.styles.debug(val));
       }
     }).catch((err) => {
@@ -86,7 +97,7 @@ function parseInput(cmd) {
       lastError = err.message;
     });
 
-    history.push(fullCommand); // add command to history when successful
+    history.push(cmd); // add command to history when successful
   } catch (err) {
     if (!lastError) output.print(output.styles.error(' ERROR '), err.message);
     lastError = err.message;
@@ -109,7 +120,7 @@ function askForStep() {
     nextStep = resolve;
     rl.setPrompt(' I.', 3);
     rl.resume();
-    rl.prompt();
+    rl.prompt([false]);
   }));
 }
 
@@ -125,4 +136,20 @@ function completer(line) {
   return [hits && hits.length ? hits : completions, line];
 }
 
+const setPassedVariables = (passedObject) => {
+  const registeredVariables = [];
+  if (passedObject) {
+    for (const key of Object.keys(passedObject)) {
+      registeredVariables.push(key);
+      if (typeof passedObject[key] === 'string') {
+        eval(`${key} = '${passedObject[key]}'`); // eslint-disable-line no-eval
+      } else {
+        eval(`${key} = ${passedObject[key]}`); // eslint-disable-line no-eval
+      }
+    }
+
+    return registeredVariables;
+  }
+};
+
 module.exports = pause;
diff --git a/lib/plugin/allure.js b/lib/plugin/allure.js
index b036e80d..93d497f3 100644
--- a/lib/plugin/allure.js
+++ b/lib/plugin/allure.js
@@ -244,6 +244,13 @@ module.exports = (config) => {
     }
   });
 
+  event.dispatcher.on(event.step.comment, (step) => {
+    reporter.startStep(step.toString());
+    currentStep = step;
+    reporter.endStep('passed');
+    currentStep = null;
+  });
+
   event.dispatcher.on(event.step.passed, (step) => {
     if (currentStep === step) {
       reporter.endStep('passed');
diff --git a/lib/ui.js b/lib/ui.js
index 45c3c735..54ae1734 100644
--- a/lib/ui.js
+++ b/lib/ui.js
@@ -6,6 +6,20 @@ const scenario = require('./scenario');
 const ScenarioConfig = require('./interfaces/scenarioConfig');
 const FeatureConfig = require('./interfaces/featureConfig');
 const addDataContext = require('./data/context');
+const container = require('./container');
+
+
+const setContextTranslation = (context) => {
+  const contexts = container.translation().value('contexts');
+
+  if (contexts) {
+    for (const key of Object.keys(contexts)) {
+      if (context[key]) {
+        context[contexts[key]] = context[key];
+      }
+    }
+  }
+};
 
 /**
  * Codecept-style interface:
@@ -167,6 +181,12 @@ module.exports = function (suite) {
       return context.Scenario(title, { ...opts, skipInfo });
     };
 
+    /**
+     * For translation
+     */
+
+    setContextTranslation(context);
+
     addDataContext(context);
   });
 
diff --git a/package.json b/package.json
index 896aed90..b3238a61 100644
--- a/package.json
+++ b/package.json
@@ -111,7 +111,7 @@
     "puppeteer": "^1.20.0",
     "qrcode-terminal": "^0.12.0",
     "rosie": "^1.6.0",
-    "runio.js": "^1.0.19",
+    "runio.js": "^1.0.20",
     "sinon": "^1.17.2",
     "sinon-chai": "^2.14.0",
     "testcafe": "^1.7.1",
diff --git a/translations/it-IT.js b/translations/it-IT.js
index 5619406f..ab769e96 100644
--- a/translations/it-IT.js
+++ b/translations/it-IT.js
@@ -1,5 +1,13 @@
 module.exports = {
   I: 'io',
+  contexts: {
+    Feature: 'Caratteristica',
+    Scenario: 'lo_scenario',
+    Before: 'Prima',
+    After: 'Dopo',
+    BeforeSuite: 'Prima_della_suite',
+    AfterSuite: 'Dopo_la_suite',
+  },
   actions: {
     amOutsideAngularApp: 'sono_fuori_dalla_applicazione_angular',
     amInsideAngularApp: 'sono_dentro_la_applicazione_angular',


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..5818c4c9
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,21 @@
+name: Run Unit tests
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-18.04
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - run: npm install
+    - run: npm test
diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js
index ba66ebee..12372f17 100644
--- a/lib/command/workers/runTests.js
+++ b/lib/command/workers/runTests.js
@@ -78,6 +78,7 @@ function initializeListeners() {
     }
 
     return {
+      id: test.id,
       workerIndex,
       title: test.title,
       status: test.status,
diff --git a/test/acceptance/mock_test.js b/test/acceptance/mock_test.js
index 7af4d302..71d6481b 100644
--- a/test/acceptance/mock_test.js
+++ b/test/acceptance/mock_test.js
@@ -58,7 +58,7 @@ Scenario(
     I.stopMocking();
 
     I.click('GET COMMENTS');
-    I.wait(2);
+    I.waitForText('laudantium', 3);
     I.dontSee('_uniqueId_u4805sd23', '#data');
   },
 );
diff --git a/test/data/sandbox/configs/translation/codecept.conf.js b/test/data/sandbox/configs/translation/codecept.conf.js
new file mode 100644
index 00000000..020f9ea7
--- /dev/null
+++ b/test/data/sandbox/configs/translation/codecept.conf.js
@@ -0,0 +1,9 @@
+exports.config = {
+  tests: './*_test.js',
+  output: './output',
+  include: {},
+  bootstrap: null,
+  mocha: {},
+  translation: 'it-IT',
+  name: 'translation-test',
+};
diff --git a/test/data/sandbox/configs/translation/translation_test.js b/test/data/sandbox/configs/translation/translation_test.js
new file mode 100644
index 00000000..b26651fd
--- /dev/null
+++ b/test/data/sandbox/configs/translation/translation_test.js
@@ -0,0 +1,18 @@
+Caratteristica('DevTo');
+
+Prima((I) => {
+  console.log('Before');
+});
+
+
+lo_scenario('Simple translation test', (io) => {
+  console.log('Simple test');
+});
+
+Scenario('Simple translation test 2', (I) => {
+  console.log('Simple test 2');
+});
+
+Dopo((I) => {
+  console.log('After');
+});
diff --git a/test/data/sandbox/workers/retry_test.workers.js b/test/data/sandbox/workers/retry_test.workers.js
new file mode 100644
index 00000000..2cbf435b
--- /dev/null
+++ b/test/data/sandbox/workers/retry_test.workers.js
@@ -0,0 +1,10 @@
+const assert = require('assert');
+
+Feature('Retry Workers');
+
+let tries = 0;
+
+Scenario('retry a test', { retries: 2 }, (I) => {
+  tries++;
+  assert.equal(tries, 2);
+});
diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js
index a9a63bc7..1cd05074 100644
--- a/test/runner/run_workers_test.js
+++ b/test/runner/run_workers_test.js
@@ -102,4 +102,13 @@ describe('CodeceptJS Workers Runner', function () {
       done();
     });
   });
+
+  it('should retry test', function (done) {
+    if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version');
+    exec(`${codecept_run} 2 --grep "retry"`, (err, stdout, stderr) => {
+      stdout.should.include('CodeceptJS'); // feature
+      stdout.should.include('OK  | 1 passed');
+      done();
+    });
+  });
 });
diff --git a/test/runner/translation_test.js b/test/runner/translation_test.js
new file mode 100644
index 00000000..bd847332
--- /dev/null
+++ b/test/runner/translation_test.js
@@ -0,0 +1,16 @@
+const path = require('path');
+const exec = require('child_process').exec;
+const assert = require('assert');
+
+const runner = path.join(__dirname, '/../../bin/codecept.js');
+const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/translation');
+const codecept_run = `${runner} run --config ${codecept_dir}/codecept.conf.js `;
+
+describe('Translation', () => {
+  it('Should run translated test file', (done) => {
+    exec(`${codecept_run}`, (err, stdout) => {
+      assert(!err);
+      done();
+    });
+  });
+});
diff --git a/test/unit/container_test.js b/test/unit/container_test.js
index 5daf3741..95d98689 100644
--- a/test/unit/container_test.js
+++ b/test/unit/container_test.js
@@ -38,6 +38,13 @@ describe('Container', () => {
       container.translation().I.should.eql('Я');
       container.translation().actionAliasFor('see').should.eql('вижу');
     });
+    it('should have translations for context', () => {
+      container.create({ translation: 'it-IT' });
+      container.translation().should.be.instanceOf(Translation);
+      container.translation().loaded.should.be.true;
+      container.translation().I.should.eql('io');
+      container.translation().value('contexts').Feature.should.eql('Caratteristica');
+    });
   });
 
   describe('#helpers', () => {