Repository: Codeception/CodeceptJS
Pull request #1784 (2019-07-18 16:08:19)

fix: puppeteer scroll to locator
**Problem** 
With the puppeteer helper using `I.scrollTo()` on an element that's well below the viewport won't scroll to the element. The browser will remain in the upper area of the page instead.

**Solution**
The `elementCoordinates` returned by `_clickablePoint()` are **viewport** coordinates  (not window coordinates). Since we have already scrolled to the element via `_scrollIntoViewIfNeeded()`, we need scrollBy (not scrollTo) those coordinates to bring the element to the top the way webdriver does.


**Problem 2** 
When switching to an iframe, scroll is not correctly applied. 

**Solution 2**
Using `executeScript` instead of `this.page.evaluate`, since `executeScript` uses the current context (`_locate`, `_scrollIntoViewIfNeeded` and `_clickablePoint` use the it as well).

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


```diff

Clones added
============
- lib/helper/Puppeteer.js  1
         
```


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

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..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,19 +319,26 @@ module.exports = function (genPath, options) {
 
   const supports = container.support(); // return all support objects
   const exportPageObjects = [];
+  const injectPageObjects = [];
   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') {
+      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}}', name);
+    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/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..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);
@@ -67,20 +67,21 @@ 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, {
+      mode: 'passthrough',
       adapters: ['puppeteer'],
       adapterOptions: {
         puppeteer: { page },
       },
     });
-
-    // By default let pass through all network requests
-    if (this.polly) this.polly.server.any().passthrough();
   }
 
   /**
@@ -100,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') {
@@ -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/Puppeteer.js b/lib/helper/Puppeteer.js
index b6436ea8..941f8220 100644
--- a/lib/helper/Puppeteer.js
+++ b/lib/helper/Puppeteer.js
@@ -599,7 +599,7 @@ class Puppeteer extends Helper {
    * {{> scrollPageToTop }}
    */
   scrollPageToTop() {
-    return this.page.evaluate(() => {
+    return this.executeScript(() => {
       window.scrollTo(0, 0);
     });
   }
@@ -608,7 +608,7 @@ class Puppeteer extends Helper {
    * {{> scrollPageToBottom }}
    */
   scrollPageToBottom() {
-    return this.page.evaluate(() => {
+    return this.executeScript(() => {
       const body = document.body;
       const html = document.documentElement;
       window.scrollTo(0, Math.max(
@@ -627,20 +627,16 @@ class Puppeteer extends Helper {
       offsetX = locator;
       locator = null;
     }
-    let x = 0;
-    let y = 0;
+
     if (locator) {
       const els = await this._locate(locator);
       assertElementExists(els, locator, 'Element');
       await els[0]._scrollIntoViewIfNeeded();
       const elementCoordinates = await els[0]._clickablePoint();
-      x = elementCoordinates.x;
-      y = elementCoordinates.y;
+      await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY);
+    } else {
+      await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY);
     }
-
-    await this.page.evaluate((x, y) => {
-      window.scrollTo(x, y);
-    }, x + offsetX, y + offsetY);
     return this._waitForAction();
   }
 
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..4b7e507b 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 });
@@ -322,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/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/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 816204dc..825c26e0 100644
--- a/test/runner/list_test.js
+++ b/test/runner/list_test.js
@@ -47,4 +47,78 @@ 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();
+    });
+  });
+
+  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();
+    });
+  });
 });