Repository: Codeception/CodeceptJS
Pull request #1786 (2019-07-19 10:45:15)

feat: async dynamic config
In some cases the decision about the config needs to be take asynchronously.

codacy-bot: 
![Codacy](https://app.codacy.com/assets/images/favicon.png) Here is an overview of what got changed by this pull request:


```diff

Complexity decreasing per file
==============================
+ lib/interfaces/scenarioConfig.js  -2
         

Clones removed
==============
+ lib/interfaces/scenarioConfig.js  -1
         
```


See the complete overview on [Codacy](https://app.codacy.com/app/DavertMik/CodeceptJS/pullRequest?prid=3882955&bid=13525682)
        

Patch:
diff --git a/RoboFile.php b/RoboFile.php
index 8160e07a..d1a134c1 100644
--- a/RoboFile.php
+++ b/RoboFile.php
@@ -159,7 +159,7 @@ class RoboFile extends \Robo\Tasks
         //issue
         $changelog = preg_replace(
             '~#(\d+)~',
-            '[#$1](https://github.com/Codeception/Codeception/issues/$1)',
+            '[#$1](https://github.com/Codeception/CodeceptJS/issues/$1)',
             $changelog
         );
 
@@ -203,4 +203,4 @@ class RoboFile extends \Robo\Tasks
         $this->_exec('npm publish');
         $this->yell('It is released!');
     }
-}
\ No newline at end of file
+}
diff --git a/docs/advanced.md b/docs/advanced.md
index c89d6500..9ffc9943 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -367,7 +367,7 @@ Scenario('should create data via v2 version of API', (I) => {
 ```
 
 Config can also be set by a function, in this case you can get a test object and specify config values based on it.
-This is very useful when running tests against cloud providers, like BrowserStack.
+This is very useful when running tests against cloud providers, like BrowserStack. This function can also be asynchronous.
 
 ```js
 Scenario('should report to BrowserStack', (I) => {
diff --git a/lib/command/definitions.js b/lib/command/definitions.js
index e519a216..380d19ed 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -182,15 +182,16 @@ module.exports = function (genPath, options) {
   const supports = container.support(); // return all support objects
   const exportPageObjects = [];
   const callbackParams = [];
-  for (const name in supports) {
-    if (name === 'I') continue;
-    callbackParams.push(`${name}:CodeceptJS.${name}`);
+  // See #1795 and #1799, there's no obvious way to use for-in with Proxies
+  Reflect.ownKeys(supports).forEach((name) => {
+    if (name === 'I') return;
+    callbackParams.push(`${String(name)}:CodeceptJS.${String(name)}`);
     const pageObject = supports[name];
     const pageMethods = addAllMethodsInObject(pageObject, {}, []);
     let pageObjectExport = pageObjectTemplate.replace('{{methods}}', pageMethods.join(''));
-    pageObjectExport = pageObjectExport.replace('{{name}}', name);
+    pageObjectExport = pageObjectExport.replace('{{name}}', String(name));
     exportPageObjects.push(pageObjectExport);
-  }
+  });
 
   let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
   definitionsTemplate = definitionsTemplate.replace('{{exportPageObjects}}', exportPageObjects.join('\n'));
diff --git a/lib/command/utils.js b/lib/command/utils.js
index c9cc65fa..ae488772 100644
--- a/lib/command/utils.js
+++ b/lib/command/utils.js
@@ -11,7 +11,7 @@ module.exports.getConfig = function (configFile) {
   try {
     return require('../config').load(configFile);
   } catch (err) {
-    fail(err.message);
+    fail(err.stack);
   }
 };
 
diff --git a/lib/container.js b/lib/container.js
index a0f85cb6..3473a379 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -162,6 +162,12 @@ function createHelpers(config) {
 
 function createSupportObjects(config) {
   const objects = container.support = new Proxy({}, {
+    has(target, key) {
+      return key in config;
+    },
+    ownKeys() {
+      return Reflect.ownKeys(config);
+    },
     get(target, key) {
       // configured but not in support object, yet: load the module
       if (key in config && !(key in target)) target[key] = lazyLoad(key);
diff --git a/lib/data/dataScenarioConfig.js b/lib/data/dataScenarioConfig.js
index 2ecf335a..9cb9173f 100644
--- a/lib/data/dataScenarioConfig.js
+++ b/lib/data/dataScenarioConfig.js
@@ -61,6 +61,24 @@ class DataScenarioConfig {
     this.scenarios.forEach(scenario => scenario.tag(tagName));
     return this;
   }
+
+  /**
+     * Pass in additional objects to inject into test
+     * @param {*} obj
+     */
+  inject(obj) {
+    this.scenarios.forEach(scenario => scenario.inject(obj));
+    return this;
+  }
+
+  /**
+     * Dynamically injects dependencies, see https://codecept.io/pageobjects/#dynamic-injection
+     * @param {*} dependencies
+     */
+  injectDependencies(dependencies) {
+    this.scenarios.forEach(scenario => scenario.injectDependencies(dependencies));
+    return this;
+  }
 }
 
 module.exports = DataScenarioConfig;
diff --git a/lib/helper/Polly.js b/lib/helper/Polly.js
index 859f2f8c..019bc405 100644
--- a/lib/helper/Polly.js
+++ b/lib/helper/Polly.js
@@ -67,9 +67,12 @@ class Polly extends Helper {
   // Connect Puppeteer helper to mock future requests.
   async _connectPuppeteer(title) {
     const adapter = require('@pollyjs/adapter-puppeteer');
-
     PollyJS.register(adapter);
+
     const { page } = this.helpers.Puppeteer;
+    if (!page) {
+      throw new Error('Looks like, there is no open tab');
+    }
     await page.setRequestInterception(true);
 
     this.polly = new PollyJS(title, {
@@ -148,16 +151,18 @@ class Polly extends Helper {
    */
   async stopMocking() {
     if (!this._checkIfMockingStarted()) return;
-
     await this._disconnectPuppeteer();
-    await this.polly.flush();
-    await this.polly.stop();
-    this.polly = undefined;
+
+    const { polly } = this;
+    if (!polly) return;
+    await polly.flush();
+    await polly.stop();
+    delete this.polly;
   }
 
   async _disconnectPuppeteer() {
     const { page } = this.helpers.Puppeteer;
-    await page.setRequestInterception(false);
+    if (page) await page.setRequestInterception(false);
   }
 }
 
diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js
index 90c2d317..c51ea3b2 100644
--- a/lib/helper/WebDriver.js
+++ b/lib/helper/WebDriver.js
@@ -15,7 +15,6 @@ const {
   chunkArray,
   convertCssPropertiesToCamelCase,
   screenshotOutputFolder,
-  fileToBase64Zip,
 } = require('../utils');
 const {
   isColorProperty,
@@ -852,12 +851,11 @@ class WebDriver extends Helper {
     assertElementExists(res, locator, 'File field');
     const el = usingFirstElement(res);
 
-    // Remote Uplaod (when running Selenium Server)
+    // Remote Upload (when running Selenium Server)
     if (this.options.remoteFileUpload) {
-      const fileCompressed = await fileToBase64Zip(file);
       try {
         this.debugSection('File', 'Uploading file to remote server');
-        file = await this.browser.uploadFile(fileCompressed);
+        file = await this.browser.uploadFile(file);
       } catch (err) {
         throw new Error(`File can't be transferred to remote server. Set \`remoteFileUpload: false\` in config to upload file locally.\n${err.message}`);
       }
diff --git a/lib/interfaces/scenarioConfig.js b/lib/interfaces/scenarioConfig.js
index 568ab7d6..05cbf8cd 100644
--- a/lib/interfaces/scenarioConfig.js
+++ b/lib/interfaces/scenarioConfig.js
@@ -58,13 +58,13 @@ class ScenarioConfig {
    * Configures a helper.
    * Helper name can be omitted and values will be applied to first helper.
    */
-  config(helper, obj) {
+  async config(helper, obj) {
     if (!obj) {
       obj = helper;
       helper = 0;
     }
     if (typeof obj === 'function') {
-      obj = obj(this.test);
+      obj = await obj(this.test);
     }
     if (!this.test.config) {
       this.test.config = {};
diff --git a/lib/step.js b/lib/step.js
index 1e770157..877f33aa 100644
--- a/lib/step.js
+++ b/lib/step.js
@@ -154,8 +154,11 @@ function detectMetaStep(stack) {
     if (isTest(line) || isBDD(line)) break;
     const fnName = line.match(/^at (\w+)\.(\w+)\s\(/);
     if (!fnName) continue;
-    if (fnName[1] === 'Generator') return; // don't track meta steps inside generators
-    if (fnName[1] === 'recorder') return; // don't track meta steps inside generators
+    if (fnName[1] === 'Generator'
+      || fnName[1] === 'recorder'
+      || fnName[1] === 'Runner'
+    ) { return; } // don't track meta steps inside generators
+
     if (fnName[1] === 'Object') {
       // detect PO name from includes
       for (const name in support) {
diff --git a/lib/ui.js b/lib/ui.js
index 9ebcbcfa..1a6a2f80 100644
--- a/lib/ui.js
+++ b/lib/ui.js
@@ -138,7 +138,7 @@ module.exports = function (suite) {
      * Pending test case.
      */
     context.xScenario = context.Scenario.skip = function (title) {
-      context.Scenario(title, {});
+      return context.Scenario(title, {});
     };
 
     addDataContext(context);
diff --git a/lib/utils.js b/lib/utils.js
index a0b6efd5..8f42b4cb 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -281,26 +281,6 @@ module.exports.screenshotOutputFolder = function (fileName) {
   return path.join(global.codecept_dir, fileName);
 };
 
-module.exports.fileToBase64Zip = async function (localPath) {
-  const archiver = require('archiver');
-
-  const zipData = [];
-  const source = fs.createReadStream(localPath);
-
-  return new Promise((resolve, reject) => {
-    archiver('zip')
-      .on('error', (e) => { throw new Error(e); })
-      .on('data', data => zipData.push(data))
-      .on('end', () => resolve(Buffer.concat(zipData).toString('base64')))
-      .append(source, { name: path.basename(localPath) })
-      .finalize((err) => {
-        if (err) {
-          reject(err);
-        }
-      });
-  });
-};
-
 module.exports.beautify = function (code) {
   const format = require('js-beautify').js;
   return format(code, { indent_size: 2, space_in_empty_paren: true });


Test patch:
diff --git a/test/acceptance/config_test.js b/test/acceptance/config_test.js
index 38128868..09067119 100644
--- a/test/acceptance/config_test.js
+++ b/test/acceptance/config_test.js
@@ -29,3 +29,11 @@ Scenario('change config 5 @WebDriverIO @Puppeteer @Protractor @Nightmare', (I) =
   I.dontSeeInCurrentUrl('github.com');
   I.seeInCurrentUrl('google.com');
 });
+
+Scenario('change config 6 @WebDriverIO @Puppeteer @Protractor @Nightmare', (I) => {
+  I.amOnPage('/');
+  I.seeInCurrentUrl('github.com');
+}).config(async (test) => {
+  await new Promise(r => setTimeout(r, 50));
+  return { url: 'https://github.com' };
+});
diff --git a/test/runner/list_test.js b/test/runner/list_test.js
index 816204dc..90785d37 100644
--- a/test/runner/list_test.js
+++ b/test/runner/list_test.js
@@ -47,4 +47,19 @@ describe('list/def commands', () => {
       done();
     });
   });
+
+  it('def should create definition file with support object', (done) => {
+    try {
+      fs.unlinkSync(`${codecept_dir}/steps.d.ts`);
+    } catch (e) {
+      // continue regardless of error
+    }
+    exec(`${runner} def --config ${codecept_dir}/codecept.inject.po.json`, () => {
+      const content = fs.readFileSync(`${codecept_dir}/steps.d.ts`).toString();
+      content.should.include('    openDir() : void');
+      content.should.include('  export interface MyPage {');
+      content.should.include('    hasFile() : void');
+      done();
+    });
+  });
 });