Repository: Codeception/CodeceptJS (js)

Issue:
Polly: Cannot read property 'substr' of undefined
#### What are you trying to achieve?

I call I.mockRequest('GET', '/api/users', 200); in my test

#### What do you get instead?

> Provide console output if related. Use `--verbose` mode for more details.

```bash
 Cannot read property 'substr' of undefined

  Scenario Steps:

  - I.mockRequest("GET", "/api/users", 200) at Test.Scenario (versicherungscenter\fremdvertrag-hinzufuegen\versicherungsnehmer-formular.test.js:66:5)
```
### Details

The cause is that this.options.url is not set and therefore baseUrl is undefined on line 137 in Polly Helper.

* CodeceptJS version: 2.2.0
* NodeJS Version: 10.x
* Operating System: Windows
* Polly


Pull request:
refactor: Use puppeteer config for Polly
**Problem:** No config `url` specific for Polly helper.
**Solution:** Use Puppeteer helper configs.

resolves #1766

Patch:
diff --git a/lib/command/definitions.js b/lib/command/definitions.js
index 380d19ed..72db5e58 100644
--- a/lib/command/definitions.js
+++ b/lib/command/definitions.js
@@ -9,7 +9,7 @@ const fs = require('fs');
 const path = require('path');
 
 const template = `
-type ICodeceptCallback = (i: CodeceptJS.{{I}}{{callbackParams}}) => void;
+type ICodeceptCallback = (i?: CodeceptJS.{{I}}, current?:any{{callbackParams}}) => void;
 
 declare class FeatureConfig {
   retry(times: number): FeatureConfig
@@ -41,6 +41,134 @@ interface ILocator {
 
 type LocatorOrString = string | ILocator | Locator;
 
+declare class Container {
+  create(config: Object, opts: Object): void
+  plugins(name?: string): Object
+  support(name?: string): Object
+  helpers(name?: string): Object
+  translation(): Object
+  mocha(): Object
+  append(newContainer: Object): void
+  clear(newHelpers: Object, newSupport: Object, newPlugins: Object): void
+}
+
+declare class RecorderSession {
+  running: boolean
+    start(name: string): void
+    restore(name: string): void
+    catch(fn: CallableFunction): void
+}
+
+declare class Recorder {
+  retries: Object[]
+  start(): void
+  isRunning(): boolean
+  startUnlessRunning(): void
+  errHandler(fn: CallableFunction): void
+  reset(): void
+  session: RecorderSession
+  add(taskName, fn?: CallableFunction, force?: boolean, retry?: boolean): Promise<any>
+  retry(opts: Object): Promise<any>
+  catch(customErrFn: CallableFunction): Promise<any>
+  catchWithoutStop(customErrFn: CallableFunction ): Promise<any>
+  throw(err: Error): Promise<any>
+  saveFirstAsyncError(err: Error): void
+  getAsyncErr(): Promise<Error>
+  cleanAsyncErr(): void
+  stop():void
+  promise(): Promise<any>
+  scheduled(): string[]
+  toString(): string
+  add(hookName: string, fn: CallableFunction, force?: boolean): void
+  catch(customErrFn: CallableFunction)
+}
+
+declare class CodeceptJSEvent {
+  dispatcher: EventEmitter
+  test: {
+    started: string
+    before: string
+    after: string
+    passed: string
+    failed: string
+    finished: string
+  }
+  suite: {
+    before: string,
+    after: string,
+  }
+  hook: {
+    started: string,
+    passed: string,
+  }
+  step: {
+    before: string,
+    after: string,
+    started: string,
+    passed: string,
+    failed: string,
+    finished: string,
+  }
+  all: {
+    before: string,
+    after: string,
+    result: string,
+  }
+  multiple: {
+    before: string,
+    after: string,
+  }
+  emit(event: string, param: string)
+  cleanDispatcher(): void
+}
+
+declare class Output {
+  colors: any
+  styles: {
+    error: any,
+    success: any,
+    scenario: any,
+    basic: any,
+    debug: any,
+    log: any,
+  }
+
+  print(msg: string): void
+  stepShift: number
+  level(level: number): number
+  process(process: string): string
+  debug(msg: string): void
+  log(msg: string): void
+  error(msg: string): void
+  success(msg: string): void
+  plugin(name: string, msg: string): void
+  step(step: any): void
+  suite: {
+    started: Function
+  }
+  test: {
+    started(test: string): void
+    passed(test: string): void
+    failed(test: string): void
+    skipped(test: string): void
+  }
+  scenario: {
+    started(test: string): void
+    passed(test: string): void
+    failed(test: string): void
+  }
+  say(message: string, color?: string): void
+  result(passed: number, failed: number, skipped: number, duration: string): void
+}
+
+declare class Config {
+  create(newConfig: Object): Object
+  load(configFile: string): Config
+  get(key: string, val: any): any
+  append(additionalConfig: Object): Object
+  reset(): Object
+}
+
 declare class Helper {
   /** Abstract method to provide required config options */
   static _config(): any;
@@ -129,7 +257,9 @@ declare function BeforeSuite(callback: ICodeceptCallback): void;
 declare function After(callback: ICodeceptCallback): void;
 declare function AfterSuite(callback: ICodeceptCallback): void;
 
-declare function inject(): { [key: string]: any };
+declare function inject(): {
+{{injectPageObjects}}
+};
 declare function locate(selector: LocatorOrString): Locator;
 declare function within(selector: LocatorOrString, callback: Function): Promise<any>;
 declare function session(selector: LocatorOrString, callback: Function): Promise<any>;
@@ -140,6 +270,12 @@ declare function secret(secret: any): string;
 declare const codeceptjs: any;
 
 declare namespace CodeceptJS {
+  export const container: Container
+  export const recorder: Recorder
+  export const event: CodeceptJSEvent
+  export const output: Output
+  export const config: Config
+
   export interface {{I}} {
 {{methods}}
   }
@@ -151,6 +287,8 @@ declare module "codeceptjs" {
 }
 `;
 
+const injectSupportTemplate = '  {{name}}: CodeceptJS.{{name}}';
+
 const pageObjectTemplate = `
   export interface {{name}} {
 {{methods}}
@@ -181,20 +319,26 @@ module.exports = function (genPath, options) {
 
   const supports = container.support(); // return all support objects
   const exportPageObjects = [];
+  const injectPageObjects = [];
   const callbackParams = [];
   // 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)}`);
+    if (name === 'I') {
+      injectPageObjects.push(injectSupportTemplate.replace(/{{name}}/g, String(name)));
+      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}}', String(name));
+    injectPageObjects.push(injectSupportTemplate.replace(/{{name}}/g, String(name)));
     exportPageObjects.push(pageObjectExport);
   });
 
   let definitionsTemplate = template.replace('{{methods}}', methods.join(''));
   definitionsTemplate = definitionsTemplate.replace('{{exportPageObjects}}', exportPageObjects.join('\n'));
+  definitionsTemplate = definitionsTemplate.replace('{{injectPageObjects}}', injectPageObjects.join('\n'));
   definitionsTemplate = definitionsTemplate.replace('{{callbackParams}}', `, ${[...callbackParams, '...args: any'].join(', ')}`);
   if (translations) {
     definitionsTemplate = definitionsTemplate.replace(/\{\{I\}\}/g, translations.I);
diff --git a/lib/helper/Polly.js b/lib/helper/Polly.js
index 019bc405..fca77809 100644
--- a/lib/helper/Polly.js
+++ b/lib/helper/Polly.js
@@ -58,7 +58,7 @@ class Polly extends Helper {
 
   // Start mocking network requests/responses
   async _startMocking(title = 'Test') {
-    if (!this.helpers && !this.helpers.Puppeteer) {
+    if (!(this.helpers && this.helpers.Puppeteer)) {
       throw new Error('Puppeteer is the only supported helper right now');
     }
     await this._connectPuppeteer(title);
@@ -76,14 +76,12 @@ class Polly extends Helper {
     await page.setRequestInterception(true);
 
     this.polly = new PollyJS(title, {
+      mode: 'passthrough',
       adapters: ['puppeteer'],
       adapterOptions: {
         puppeteer: { page },
       },
     });
-
-    // By default let pass through all network requests
-    if (this.polly) this.polly.server.any().passthrough();
   }
 
   /**
@@ -103,10 +101,12 @@ class Polly extends Helper {
    */
   async mockRequest(method, oneOrMoreUrls, dataOrStatusCode) {
     await this._checkAndStartMocking();
+    const puppeteerConfigUrl = this.helpers.Puppeteer && this.helpers.Puppeteer.options.url;
+
     const handler = this._getRouteHandler(
       method,
       oneOrMoreUrls,
-      this.options.url,
+      this.options.url || puppeteerConfigUrl,
     );
 
     if (typeof dataOrStatusCode === 'number') {
diff --git a/lib/utils.js b/lib/utils.js
index 8f42b4cb..4b7e507b 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -302,7 +302,7 @@ function joinUrl(baseUrl, url) {
   return shouldAppendBaseUrl(url) ? `${baseUrl}/${trimUrl(url)}` : url;
 }
 
-module.exports.appendBaseUrl = function (baseUrl, oneOrMoreUrls) {
+module.exports.appendBaseUrl = function (baseUrl = '', oneOrMoreUrls) {
   // Remove '/' if it's at the end of baseUrl
   const lastChar = baseUrl.substr(-1);
   if (lastChar === '/') {


Test patch:
diff --git a/lib/helper/TestCafe.js b/lib/helper/TestCafe.js
index 266d839e..56eb9cbf 100644
--- a/lib/helper/TestCafe.js
+++ b/lib/helper/TestCafe.js
@@ -780,10 +780,76 @@ class TestCafe extends Helper {
     return ClientFunction(() => document.location.href).with({ boundTestRun: this.t })();
   }
 
+  /**
+   * {{> grabPageScrollPosition }}
+   */
+  async grabPageScrollPosition() {
+    return ClientFunction(() => ({ x: window.pageXOffset, y: window.pageYOffset })).with({ boundTestRun: this.t })();
+  }
+
+  /**
+   * {{> scrollPageToTop }}
+   */
+  scrollPageToTop() {
+    return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })();
+  }
+
+  /**
+   * {{> scrollPageToBottom }}
+   */
+  scrollPageToBottom() {
+    return ClientFunction(() => {
+      const body = document.body;
+      const html = document.documentElement;
+      window.scrollTo(0, Math.max(
+        body.scrollHeight, body.offsetHeight,
+        html.clientHeight, html.scrollHeight, html.offsetHeight,
+      ));
+    }).with({ boundTestRun: this.t })();
+  }
+
+  /**
+   * {{> scrollTo }}
+   */
+  async scrollTo(locator, offsetX = 0, offsetY = 0) {
+    if (typeof locator === 'number' && typeof offsetX === 'number') {
+      offsetY = offsetX;
+      offsetX = locator;
+      locator = null;
+    }
+
+    const scrollBy = ClientFunction((offset) => {
+      if (window && window.scrollBy && offset) {
+        window.scrollBy(offset.x, offset.y);
+      }
+    }).with({ boundTestRun: this.t });
+
+    if (locator) {
+      const els = await this._locate(locator);
+      assertElementExists(els, locator, 'Element');
+      const el = await els.nth(0);
+      const x = (await el.offsetLeft) + offsetX;
+      const y = (await el.offsetTop) + offsetY;
+
+      return scrollBy({ x, y });
+    }
+
+    const x = offsetX;
+    const y = offsetY;
+    return scrollBy({ x, y });
+  }
+
   /**
    * {{> switchTo }}
    */
   async switchTo(locator) {
+    if (Number.isInteger(locator)) {
+      throw new Error('Not supported switching to iframe by number');
+    }
+
+    if (!locator) {
+      return this.t.switchToMainWindow();
+    }
     return this.t.switchToIframe(findElements.call(this, this.context, locator));
   }
 
diff --git a/lib/helper/testcafe/testcafe-utils.js b/lib/helper/testcafe/testcafe-utils.js
index 65963c7d..3e32f5d1 100644
--- a/lib/helper/testcafe/testcafe-utils.js
+++ b/lib/helper/testcafe/testcafe-utils.js
@@ -9,7 +9,7 @@ const createTestFile = () => {
   assert(global.output_dir, 'global.output_dir must be set');
 
   const testFile = path.join(global.output_dir, `${Date.now()}_test.js`);
-  const testControllerHolderDir = __dirname;
+  const testControllerHolderDir = __dirname.replace(/\\/g, '/');
 
   fs.writeFileSync(
     testFile,
diff --git a/test/acceptance/codecept.Puppeteer.js b/test/acceptance/codecept.Puppeteer.js
index a699546a..83890d6a 100644
--- a/test/acceptance/codecept.Puppeteer.js
+++ b/test/acceptance/codecept.Puppeteer.js
@@ -15,9 +15,7 @@ module.exports.config = {
         ],
       },
     },
-    Polly: {
-      url: TestHelper.siteUrl(),
-    },
+    Polly: {},
   },
   include: {},
   bootstrap: false,
diff --git a/test/acceptance/mock_test.js b/test/acceptance/mock_test.js
index d6111c07..3bd8e1fd 100644
--- a/test/acceptance/mock_test.js
+++ b/test/acceptance/mock_test.js
@@ -8,30 +8,24 @@ const fetchUsers = response => response.url() === 'https://jsonplaceholder.typic
 
 Scenario('change statusCode @Puppeteer', (I) => {
   I.amOnPage('/form/fetch_call');
-  I.click('GET POSTS');
-  I.waitForResponse(fetchPost, 3);
   I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', 404);
   I.click('GET POSTS');
-  I.see('Can not load data!');
+  I.waitForText('Can not load data!', 1, '#data');
   I.stopMocking();
 });
 
 Scenario('change response data @Puppeteer', (I) => {
   I.amOnPage('/form/fetch_call');
-  I.click('GET COMMENTS');
-  I.waitForResponse(fetchComments, 3);
   I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
     modified: 'This is modified from mocking',
   });
   I.click('GET COMMENTS');
-  I.see('This is modified from mocking', '#data');
+  I.waitForText('This is modified from mocking', 1, '#data');
   I.stopMocking();
 });
 
 Scenario('change response data for multiple requests @Puppeteer', (I) => {
   I.amOnPage('/form/fetch_call');
-  I.click('GET USERS');
-  I.waitForResponse(fetchUsers, 3);
   I.mockRequest(
     'GET',
     [
@@ -44,11 +38,11 @@ Scenario('change response data for multiple requests @Puppeteer', (I) => {
     },
   );
   I.click('GET POSTS');
-  I.see('MY CUSTOM DATA', '#data');
+  I.waitForText('MY CUSTOM DATA', 1, '#data');
   I.click('GET COMMENTS');
-  I.see('MY CUSTOM DATA', '#data');
+  I.waitForText('MY CUSTOM DATA', 1, '#data');
   I.click('GET USERS');
-  I.see('MY CUSTOM DATA', '#data');
+  I.waitForText('MY CUSTOM DATA', 1, '#data');
   I.stopMocking();
 });
 
@@ -56,15 +50,15 @@ Scenario(
   'should request for original data after mocking stopped @Puppeteer',
   (I) => {
     I.amOnPage('/form/fetch_call');
-    I.click('GET COMMENTS');
     I.mockRequest('GET', 'https://jsonplaceholder.typicode.com/*', {
-      comment: 'CUSTOM',
+      comment: 'CUSTOM _uniqueId_u4805sd23',
     });
     I.click('GET COMMENTS');
-    I.see('CUSTOM', '#data');
+    I.waitForText('_uniqueId_u4805sd23', 1, '#data');
     I.stopMocking();
 
     I.click('GET COMMENTS');
-    I.dontSee('CUSTOM', '#data');
+    I.wait(2);
+    I.dontSee('_uniqueId_u4805sd23', '#data');
   },
 );
diff --git a/test/data/sandbox/codecept.inject.po.json b/test/data/sandbox/codecept.inject.po.json
index a914e7f3..5dde0213 100644
--- a/test/data/sandbox/codecept.inject.po.json
+++ b/test/data/sandbox/codecept.inject.po.json
@@ -7,7 +7,8 @@
   },
   "include": {
     "I": "./support/custom_steps.js",
-    "MyPage": "./support/my_page.js"
+    "MyPage": "./support/my_page.js",
+    "SecondPage": "./support/second_page.js"
   },
   "bootstrap": false,
   "mocha": {},
diff --git a/test/data/sandbox/support/second_page.js b/test/data/sandbox/support/second_page.js
new file mode 100644
index 00000000..adfd44c9
--- /dev/null
+++ b/test/data/sandbox/support/second_page.js
@@ -0,0 +1,5 @@
+module.exports = {
+  secondPageMethod() {
+    console.log('secondPageMethod');
+  },
+};
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index 399925b9..a7c0bbf1 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -980,7 +980,6 @@ module.exports.tests = function () {
     });
 
     it('should scroll to an element', async () => {
-      if (isHelper('TestCafe')) return;
       await I.amOnPage('/form/scroll');
       await I.resizeWindow(500, 700);
       const { x, y } = await I.grabPageScrollPosition();
@@ -990,7 +989,6 @@ module.exports.tests = function () {
     });
 
     it('should scroll to coordinates', async () => {
-      if (isHelper('TestCafe')) return;
       await I.amOnPage('/form/scroll');
       await I.resizeWindow(500, 700);
       const { x, y } = await I.grabPageScrollPosition();
@@ -1001,7 +999,6 @@ module.exports.tests = function () {
     });
 
     it('should scroll to bottom of page', async () => {
-      if (isHelper('TestCafe')) return;
       await I.amOnPage('/form/scroll');
       await I.resizeWindow(500, 700);
       const { y } = await I.grabPageScrollPosition();
@@ -1012,7 +1009,6 @@ module.exports.tests = function () {
     });
 
     it('should scroll to top of page', async () => {
-      if (isHelper('TestCafe')) return;
       await I.amOnPage('/form/scroll');
       await I.resizeWindow(500, 700);
       await I.scrollPageToBottom();
diff --git a/test/runner/list_test.js b/test/runner/list_test.js
index 90785d37..825c26e0 100644
--- a/test/runner/list_test.js
+++ b/test/runner/list_test.js
@@ -62,4 +62,63 @@ describe('list/def commands', () => {
       done();
     });
   });
+
+  it('def should create definition file with internal 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('  export const recorder: Recorder');
+      content.should.include('  export const event: CodeceptJSEvent');
+      content.should.include('  export const output: Output');
+      content.should.include('  export const config: Config');
+      content.should.include('  export const container: Container');
+      done();
+    });
+  });
+
+  it('def should create definition file with inject which contains support objects', (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('declare function inject(): {');
+      content.should.include(' MyPage: CodeceptJS.MyPage');
+      content.should.include(' SecondPage: CodeceptJS.SecondPage');
+      done();
+    });
+  });
+
+  it('def should create definition file with inject which contains I 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('declare function inject(): {');
+      content.should.include(' I: CodeceptJS.I');
+      done();
+    });
+  });
+
+  it('def should create definition file with callback params', (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('type ICodeceptCallback = (i?: CodeceptJS.I, current?:any, MyPage?:CodeceptJS.MyPage, SecondPage?:CodeceptJS.SecondPage, ...args: any) => void;');
+      done();
+    });
+  });
 });