Repository: Codeception/CodeceptJS
Pull request #1806 (2019-07-27 21:22:35)

Preparing 2.2.1 release


Patch:
diff --git a/.travis.yml b/.travis.yml
index d26422c2..bef5afd8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
 language: node_js
 node_js:
 - 8
-- 10
+- 12
 sudo: required
 dist: trusty
 env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e9f2c60..27716adc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,31 @@
+## 2.2.1
+
+* [WebDriver] A [dedicated guide](https://codecept.io/webdriver) written.
+* [TestCade] A [dedicated guide](https://codecept.io/testcafe) written.
+* [Puppeteer][Nightmare][TestCafe] Window mode is enabled by default on `codeceptjs init`.
+* [TestCafe] Actions implemented by @hubidu
+  * `grabPageScrollPosition`
+  * `scrollPageToTop`
+  * `scrollPageToBottom`
+  * `scrollTo`
+  * `switchTo`
+* Intellisense improvements. Renamed `tsconfig.json` to `jsconfig.json` on init. Fixed autocompletion for Visual Studio Code.
+* [Polly] Take configuration values from Puppeteer. Fix #1766 by @VikalpP
+* [Polly] Add preconditions to check for puppeteer page availability by @VikalpP. Fixes #1767
+* [WebDriver] Use filename for `uploadFile` by @VikalpP. See #1797
+* [Puppeteer] Configure speed of input with `pressKeyDelay` option. By @hubidu
+* Fixed recursive loading of support objects by @davertmik.
+* Fixed support object definitions in steps.d.ts by @johnyb. Fixes #1795
+* Fixed `Data().Scenario().injectDependencies()` is not a function by @andrerleao
+* Fixed crash when using xScenario & Scenario.skip with tag by @VikalpP. Fixes #1751
+* Dynamic configuration of helpers can be performed with async function. See #1786 by @cviejo
+* Added TS definitions for internal objects by @Vorobeyko
+* BDD improvements:
+  * Fix for snippets command with a .feature file that has special characters by @asselin
+  * Fix `--path` option on `gherkin:snippets` command by @asselin. See #1790
+  * Added `--feature` option to `gherkin:snippets` to enable creating snippets for a subset of .feature files. See #1803 by @asselin.
+* Fixed: dynamic configs not reset after test. Fixes #1776 by @cviejo.
+
 ## 2.2.0
 
 * **EXPERIMENTAL** [**TestCafe** helper](https://codecept.io/helpers/TestCafe) introduced. TestCafe allows to run cross-browser tests it its own very fast engine. Supports all browsers including mobile. Thanks to @hubidu for implementation! Please test it and send us feedback.
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index 961bae45..5548d1ea 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -52,14 +52,14 @@ Make sure `Puppeteer` helper is enabled in `codecept.conf.js` config:
   helpers: {
     Puppeteer: {
       url: "http://localhost",
-      show: false
+      show: true
     }
   }
   // ..
 }
 ```
 
-Turn on the `show` option if you want to follow test progress in a window. This is very useful for debugging.
+> Turn off the `show` option if you want to run test in headless mode.
 
 Puppeteer uses different strategies to detect if a page is loaded. In configuration use `waitForNavigation` option for that:
 
@@ -69,6 +69,7 @@ By default it is set to `domcontentloaded` which waits for `DOMContentLoaded` ev
   helpers: {
     Puppeteer: {
       url: "http://localhost",
+      show: true,
       waitForNavigation: "networkidle0"
     }
   }
@@ -77,7 +78,7 @@ By default it is set to `domcontentloaded` which waits for `DOMContentLoaded` ev
 When a test runs faster than application it is recommended to increase `waitForAction` config value.
 It will wait for a small amount of time (100ms) by default after each user action is taken.
 
-> More options are listed in [helper reference](http://codecept.io/helpers/Puppeteer/).
+> ▶ More options are listed in [helper reference](http://codecept.io/helpers/Puppeteer/).
 
 ## Writing Tests
 
@@ -155,7 +156,7 @@ Scenario('get value of current tasks', async (I) => {
 
 ### Within
 
-In case some actions should be taken inside one element (a container or modal window) you can use `within` block to narrow the scope.
+In case some actions should be taken inside one element (a container or modal window or iframe) you can use `within` block to narrow the scope.
 Please take a note that you can't use within inside another within in Puppeteer helper:
 
 ```js
@@ -167,15 +168,58 @@ within('.todoapp', () => {
 I.see('0 items left', '.todo-count');
 ```
 
+> [▶ Learn more about basic commands](https://codecept.io/basics#writing-tests)
+
 CodeceptJS allows you to implement custom actions like `I.createTodo` or use **PageObjects**. Learn how to improve your tests in [PageObjects](http://codecept.io/pageobjects/) guide.
 
-`within` can also work with [iframes](/acceptance/#iframes)
+> [▶ Demo project is available on GitHub](https://github.com/DavertMik/codeceptjs-todomvc-puppeteer)
+
+## Mocking Requests
+
+Web application sends various requests to local services (Rest API, GraphQL) or to 3rd party services (CDNS, Google Analytics, etc).
+When you run tests with Puppeteer you can control those requests by mocking them. For instance, you can speed up your tests by blocking trackers, Google Analytics, and other services you don't control.
 
-When running steps inside a within block will be shown with a shift.
+Also you can replace real request with a one explicitly defined. This is useful when you want to isolate application testing from a backend. For instance, if you don't want to save data to database, and you know the request which performs save, you can mock the request, so application will treat this as valid response, but no data will be actually saved.
 
-![within](https://codecept.io/img/within.png)
+To mock requests enable additional helper [Polly](https://codecept.io/helpers/Polly) (which is based on Polly.js).
+
+```js
+helpers: {
+   Puppeteer: {
+     // regular Puppeteer config here
+   },
+   Polly: {}
+}
+```
+
+And install additional packages:
+
+```
+npm i @pollyjs/core @pollyjs/adapter-puppeteer --save-dev
+```
 
-> [Demo project is available on GitHub](https://github.com/DavertMik/codeceptjs-todomvc-puppeteer)
+After an installation function `mockRequest` will be added to `I` object. You can use it to explicitly define which requests to block and which response they should return instead:
+
+```js
+// block all Google Analytics calls
+I.mockRequest('/google-analytics/*path', 200);
+// return an empty successful response
+I.mockRequest('GET', '/api/users', 200);
+// block post requests to /api/users and return predefined object
+I.mockRequest('POST', '/api/users', { user: 'davert' });
+// return error request with body
+I.mockRequest('GET', '/api/users/1', 404, { error: 'User not found' });
+```
+
+> See [`mockRequest` API](https://codecept.io/helpers/Polly#mockrequest)
+
+To see `mockRequest` method in intellisense auto completion don't forget to run `codeceptjs def` command:
+
+```
+npx codeceptjs def
+```
+
+Mocking rules will be kept while a test is running. To stop mocking use `I.stopMocking()` command
 
 ## Extending
 
@@ -199,5 +243,7 @@ async renderPageToPdf() {
 }
 ```
 
-The same way you can also access `browser` object to implement more actions or handle events. [Learn more about Helpers](http://codecept.io/helpers/) in the corresponding guide.
+The same way you can also access `browser` object to implement more actions or handle events.
+
+> [▶ Learn more about Helpers](http://codecept.io/helpers/)
 
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 431036fc..b22e5bff 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -26,7 +26,7 @@ Here is a brief comparison of all tools you can use with CodeceptJS.
 
 * **[Choose Puppeteer](#Using-Puppeteer)** for simplest setup, fast tests, full browser control. Limited to Chrome and Firefox only. Cloud browsers via browserless.io.
 * **[Choose WebDriver](#Using-WebDriver)** or Protractor for classical Selenium. Rich ecosystem and cross browser support with cloud browsers via Sauce Labs, BrowserStack, TestingBot. **Selenium server requried** for local start.
-* **Choose TestCafe** for cheap and fast cross-browser tests. Has stability and feature limitation comparing to WebDriver.
+* **[Choose TestCafe](#Using-TestCafe)** for cheap and fast cross-browser tests. Has stability and feature limitation comparing to WebDriver.
 
 Each driver has its own pros and cons which can't be described in this paragraph. However, in CodeceptJS it is easy to switch between them. In most cases you just need to update a config to run tests differently.
 
@@ -256,3 +256,7 @@ My First Test --
 ## Using NightmareJS
 
 > [▶ Follow corresponding guide](https://codecept.io/nightmare/)
+
+## Using TestCafe
+
+> [▶ Follow corresponding guide](https://codecept.io/testcafe/)
diff --git a/docs/webdriver.md b/docs/webdriver.md
index 6f5ae8e1..d27a03c9 100644
--- a/docs/webdriver.md
+++ b/docs/webdriver.md
@@ -79,7 +79,7 @@ Configuration for WebDriver should be provided inside `codecept.conf.js` file un
 
 WebDriver protocol works over HTTP, so you need to have a Selenium Server to be running or other service that will launch a browser for you. That's why you may need to specify `host`, `port`, `protocol`, and `path` parameters.
 
-By default, those parameters are set to connect to local Selenium Server but they should be changed if you want to run tests via [Cloud Services](https://codecept.io/helpers/WebDriver#cloud-providers) (Sauce Labs, etc) or inside Docker container.
+By default, those parameters are set to connect to local Selenium Server but they should be changed if you want to run tests via [Cloud Services](https://codecept.io/helpers/WebDriver#cloud-providers). You may also need `user` and `key` parameters to authenticate on cloud service.
 
 If you want to run tests using raw ChromeDriver (which also supports WebDriver protocol) avoiding Selenium Server, you should provide following configuration:
 
@@ -112,6 +112,31 @@ desiredCapabilities: {
 
 There are also [browser and platform specific capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). Services like SauceLabs, BrowserStack or browser vendors can provide their own specific capabilities for more tuning.
 
+Here is a sample BrowserStack config for running tests on iOS mobile browser:
+
+```js
+ helpers: {
+   WebDriver: {
+     host: 'hub.browserstack.com',
+     path: '/wd/hub',
+     url: 'http://WEBSITE:8080/renderer',
+     user: 'xx', // credentials
+     key: 'xx', // credentials
+     browser: 'iphone',
+     desiredCapabilities: {
+       'os_version' : '11',
+       'device' : 'iPhone 8', // you can select device
+       'real_mobile' : 'true', // real or emulated
+       'browserstack.local' : 'true',
+       'browserstack.debug' : 'true',
+       'browserstack.networkLogs' : 'true',
+       'browserstack.appium_version' : '1.9.1',
+       'browserstack.user' : 'xx', // credentials
+       'browserstack.key' : 'xx' // credentials
+     }
+   }
+```
+
 There are also options specific to CodeceptJS. By default CodeceptJS runs tests in the same browser window but clears cookies and local storage after each test. This behavior can be changed with these options:
 
 ```js
@@ -124,7 +149,7 @@ keepCookies: true,
 
 > ▶ More config options available on [WebDriver helper reference](https://codecept.io/helpers/WebDriver#configuration)
 
-### Configuring WebDriver for CI
+### Configuring CI
 
 To develop tests it's fine to use local Selenium Server and window mode. Setting up WebDriver on remote CI (Continous Integration) server is different. If there is no desktop and no window mode on CI.
 
diff --git a/lib/container.js b/lib/container.js
index 3473a379..dae00e4d 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -161,28 +161,22 @@ 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);
-      return target[key];
-    },
-  });
+  const objects = {};
+
+  for (const name in config) {
+    objects[name] = {}; // placeholders
+  }
 
   if (!config.I) {
-    container.support.I = require('./actor')();
+    objects.I = require('./actor')();
 
     if (container.translation.I !== 'I') {
-      container.support[container.translation.I] = container.support.I;
+      objects[container.translation.I] = objects.I;
     }
   }
 
+  container.support = objects;
+
   function lazyLoad(name) {
     let newObj = getSupportObject(config, name);
     try {
@@ -196,6 +190,7 @@ function createSupportObjects(config) {
     }
     return newObj;
   }
+
   const asyncWrapper = function (f) {
     return function () {
       return f.apply(this, arguments).catch((e) => {
@@ -215,7 +210,32 @@ function createSupportObjects(config) {
     });
   });
 
-  return objects;
+  return 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 objects && !(key in target)) {
+        // load default I
+        if (key in objects && !(key in config)) {
+          return target[key] = objects[key];
+        }
+
+        // load new object
+        const object = lazyLoad(key);
+        // check that object is a real object and not an array
+        if (Object.prototype.toString.call(object) === '[object Object]') {
+          return target[key] = Object.assign(objects[key], object);
+        }
+        target[key] = object;
+      }
+      return target[key];
+    },
+  });
 }
 
 function createPlugins(config, options = {}) {
diff --git a/lib/helper/Polly.js b/lib/helper/Polly.js
index fca77809..2e1e6079 100644
--- a/lib/helper/Polly.js
+++ b/lib/helper/Polly.js
@@ -91,6 +91,7 @@ class Polly extends Helper {
    * I.mockRequest('GET', '/api/users', 200);
    * I.mockRequest('ANY', '/secretsRoutes/*', 403);
    * I.mockRequest('POST', '/secrets', { secrets: 'fakeSecrets' });
+   * I.mockRequest('GET', '/api/users/1', 404, 'User not found');
    * ```
    *
    * Multiple requests
@@ -98,8 +99,13 @@ class Polly extends Helper {
    * ```js
    * I.mockRequest('GET', ['/secrets', '/v2/secrets'], 403);
    * ```
+   * @param {string} method request method. Can be `GET`, `POST`, `PUT`, etc or `ANY`.
+   * @param {string|array} oneOrMoreUrls url(s) to mock. Can be exact URL, a pattern, or an array of URLs.
+   * @param {number|string|object} dataOrStatusCode status code when number provided. A response body otherwise
+   * @param {string|object} additionalData response body when a status code is set by previous parameter.
+   *
    */
-  async mockRequest(method, oneOrMoreUrls, dataOrStatusCode) {
+  async mockRequest(method, oneOrMoreUrls, dataOrStatusCode, additionalData = null) {
     await this._checkAndStartMocking();
     const puppeteerConfigUrl = this.helpers.Puppeteer && this.helpers.Puppeteer.options.url;
 
@@ -111,6 +117,9 @@ class Polly extends Helper {
 
     if (typeof dataOrStatusCode === 'number') {
       const statusCode = dataOrStatusCode;
+      if (additionalData) {
+        return handler.intercept((_, res) => res.status(statusCode).send(additionalData));
+      }
       return handler.intercept((_, res) => res.sendStatus(statusCode));
     }
     const data = dataOrStatusCode;
diff --git a/package.json b/package.json
index 0ef784d8..539ae6cb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "codeceptjs",
-  "version": "2.2.0",
+  "version": "2.2.1",
   "description": "Modern Era Acceptance Testing Framework for NodeJS",
   "keywords": [
     "acceptance",
diff --git a/website/pages/en/index.js b/website/pages/en/index.js
index 84b09c15..56b129c3 100644
--- a/website/pages/en/index.js
+++ b/website/pages/en/index.js
@@ -73,7 +73,7 @@ class Index extends React.Component {
     <div className="col-md-4">
       <img src="/img/Mind-Map-Paper.svg" alt="" />
       <h5>Driver Agnostic</h5>
-        Run your tests via <b>WebDriver, Puppeteer, Protractor, Appium</b>. The code is the same.
+        Run your tests via <b>WebDriver, Puppeteer, TestCafe, Protractor, Appium</b>. The code is the same.
         <a href="/basics#architecture" className="btn btn-light btn-sm">Learn More</a>
     </div>
     <div className="col-md-4">
diff --git a/website/sidebars.json b/website/sidebars.json
index 7a997cb4..5b8a50a7 100755
--- a/website/sidebars.json
+++ b/website/sidebars.json
@@ -3,15 +3,11 @@
     "Quickstart": [
       "quickstart"
     ],
-    "Tutorials": [
-      "books",
-      "videos",
-      "examples"
-    ],
     "Web Testing": [
       "basics",
       "webdriver",
       "puppeteer",
+      "testcafe",
       "angular",
       "nightmare",
       "visual",
@@ -31,6 +27,11 @@
       "hooks",
       "reports",
       "advanced"
+    ],
+    "Tutorials": [
+      "books",
+      "videos",
+      "examples"
     ]
   },
   "reference": {


Test patch:
diff --git a/docs/testcafe.md b/docs/testcafe.md
new file mode 100644
index 00000000..5a59bb8a
--- /dev/null
+++ b/docs/testcafe.md
@@ -0,0 +1,154 @@
+# TestCafe
+
+[TestCafe](https://devexpress.github.io/testcafe/) is another alternative engine for driving browsers. It is driven by unique technology which provides fast and simple cross browser testing for desktop and mobile browsers. Unlike WebDriver or Puppeteer, TestCafe doesn't control a browser at all. It is not a browser itself, like [Nightmare](https://codecept.io/nightmare) or Cypress. **TestCafe core is a proxy server** that runs behind the scene, and transforms all HTML and JS to include code that is needed for test automation.
+
+![](/img/testcafe.png)
+
+This is very smart idea. But to use TestCafe on daily basis you need to clearly understand its benefits and limitations:
+
+### Pros
+
+* **Fast**. Browser is controlled from inside a web page. This makes test run inside a browser as fast as your browser can render page with no extra network requests.
+* **Simple Cross-Browser Support.** Because TestCafe only launches browsers, it can **automate browser** on desktop or mobile. Unlike WebDriver, you don't need special version of browser and driver to prepare to run tests. Setup simplified. All you need is just a browser installed, and you are ready to go.
+* **Stable to Execution.** Because a test is executed inside a browser, the network latency effects are reduced. Unlike WebDriver you won't hit stale element exceptions, or element not interactable exceptions, as from within a web browser all DOM elements are accessible.
+
+## Cons
+
+* **Magic.** Browsers executed in TestCafe are not aware that they run in test mode. So at some edges automation control can be broken. It's also quite hard to debug possible issues, as you don't know how actually a web page is parsed to inject automation scripts.
+* **No Browser Control.** Because TestCafe do not control browser, you can't actually automate all users actions. For instance, TestCafe can't open new tabs or open a new browser window in incognito mode. There can be also some issues running tests on 3rd party servers or inside iframes.
+* **Simulated Events.** Events like `click` or `doubleClick` are simulated by JavaScript internally. Inside WebDriver or Puppeteer, where those events are dispatched by a browser, called native events. Native events are closer to real user experience. So in some cases simulated events wouldn't represent actual user experience, which can lead to false positive results. For instance, a button which can't be physically clicked by a user, would be clickable inside TestCafe.
+
+Anyway, TestCafe is a good option to start if you need cross browser testing. And here is the **reason to use TestCafe with CodeceptJS: if you hit an edge case or issue, you can easily switch your tests to WebDriver**. As all helpers in CodeceptJS share the same syntax.
+
+CodeceptJS is a rich testing frameworks which also provides features missing in original TestCafe:
+
+* [Cucumber integration](https://codecept.io/bdd)
+* [Real Page Objects](https://codecept.io/pageobjects)
+* [Data Management via API](https://codecept.io/data)
+* and others
+
+## Writing Tests
+
+To start using TestCafe with CodeceptJS install both via NPM
+
+> If you don't have `package.json` in your project, create it with `npm init -y`.
+
+```
+npm i codeceptjs testcafe --save-dev
+```
+
+Then you need to initialize a project, selecting TestCafe when asked:
+
+```
+npx codeceptjs init
+```
+
+A first test should be created with `codeceptjs gt` command
+
+```
+npx codeceptjs gt
+```
+
+In the next example we will [TodoMVC application](http://todomvc.com/examples/angularjs/#/). So let's create a test which will fill in todo list:
+
+```js
+Feature('TodoMVC');
+
+Scenario('create todo item', (I) => {
+  I.amOnPage('http://todomvc.com/examples/angularjs/#/');
+  I.fillField('.new-todo', todo)
+  I.pressKey('Enter');
+  I.seeNumberOfVisibleElements('.todo-list li', 1);
+  I.see('1 item left', '.todo-count');
+});
+```
+
+Same syntax is the same for all helpers in CodeceptJS so to learn more about available commands learn [CodeceptJS Basics](https://codecept.io/basics).
+
+> [▶ Complete list of TestCafe actions](https://codecept.io/helpers/TestCafe)
+
+## Page Objects
+
+Multiple tests can be refactored to share some logic and locators. It is recommended to use PageObjects for this. For instance, in example above, we could create special actions for creating todos and checking them. If we move such methods in a corresponding object a test would look even clearer:
+
+```js
+Scenario('Create a new todo item', async (I, TodosPage) => {
+  I.say('Given I have an empty todo list')
+
+  I.say('When I create a todo "foo"')
+  TodosPage.enterTodo('foo')
+
+  I.say('Then I see the new todo on my list')
+  TodosPage.seeNumberOfTodos(1)
+
+  I.saveScreenshot('create-todo-item.png')
+})
+
+Scenario('Create multiple todo items', async (I, TodosPage) => {
+  I.say('Given I have an empty todo list')
+
+  I.say('When I create todos "foo", "bar" and "baz"')
+  TodosPage.enterTodo('foo')
+  TodosPage.enterTodo('bar')
+  TodosPage.enterTodo('baz')
+
+  I.say('Then I have these 3 todos on my list')
+  TodosPage.seeNumberOfTodos(3)
+
+  I.saveScreenshot('create-multiple-todo-items.png')
+})
+```
+
+> ℹ [Source code of this example](https://github.com/hubidu/codeceptjs-testcafe-todomvc) is available on GitHub.
+
+A PageObject can be injected into a test by its name. Here is how TodosPage looks like:
+
+```js
+// inside todos_page.js
+const { I } = inject();
+
+module.exports = {
+    goto() {
+        I.amOnPage('http://todomvc.com/examples/angularjs/#/')
+    },
+
+    enterTodo(todo) {
+        I.fillField('.new-todo', todo)
+        I.pressKey('Enter')
+    },
+
+    seeNumberOfTodos(numberOfTodos) {
+        I.seeNumberOfVisibleElements('.todo-list li', numberOfTodos)
+    },
+}
+```
+
+> [▶ Read more about PageObjects in CodeceptJS](https://codecept.io/pageobjects)
+
+## Extending
+
+If you want to use TestCafe API inside your tests you can put them into actions of `I` object. To do so you can generate a new helper, access TestCafe helper, and get the test controller.
+
+Create a helper using `codecepjs gh` command.
+
+```
+npx codeceptjs gh
+```
+
+All methods of newly created class will be added to `I` object.
+
+```js
+const Helper = codeceptjs.helper;
+
+class MyTestCafe extends Helper {
+
+  slowlyFillField(field, text) {
+    // import test controller from TestCafe helper
+    const { t } = this.helpers.TestCafe;
+    // use TestCafe API here
+    return t.setTestSpeed(0.1)
+        .typeText(field, text);
+  }
+
+}
+```
diff --git a/lib/helper/TestCafe.js b/lib/helper/TestCafe.js
index e5115abe..8e1eae30 100644
--- a/lib/helper/TestCafe.js
+++ b/lib/helper/TestCafe.js
@@ -252,7 +252,7 @@ class TestCafe extends Helper {
   async _withinBegin(locator) {
     const els = await this._locate(locator);
     assertElementExists(els, locator);
-    this.context = els.nth(0);
+    this.context = await els.nth(0);
   }
 
   async _withinEnd() {
@@ -640,7 +640,7 @@ class TestCafe extends Helper {
     const el = await els.nth(0);
 
     return this.t
-      .expect(el.value).eql(value)
+      .expect(await el.value).eql(value)
       .catch(mapError);
   }
 
@@ -795,7 +795,7 @@ class TestCafe extends Helper {
    * {{> scrollPageToTop }}
    */
   scrollPageToTop() {
-    return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })();
+    return ClientFunction(() => window.scrollTo(0, 0)).with({ boundTestRun: this.t })().catch(mapError);
   }
 
   /**
@@ -809,7 +809,7 @@ class TestCafe extends Helper {
         body.scrollHeight, body.offsetHeight,
         html.clientHeight, html.scrollHeight, html.offsetHeight,
       ));
-    }).with({ boundTestRun: this.t })();
+    }).with({ boundTestRun: this.t })().catch(mapError);
   }
 
   /**
@@ -835,12 +835,12 @@ class TestCafe extends Helper {
       const x = (await el.offsetLeft) + offsetX;
       const y = (await el.offsetTop) + offsetY;
 
-      return scrollBy({ x, y });
+      return scrollBy({ x, y }).catch(mapError);
     }
 
     const x = offsetX;
     const y = offsetY;
-    return scrollBy({ x, y });
+    return scrollBy({ x, y }).catch(mapError);
   }
 
   /**
diff --git a/lib/helper/testcafe/testcafe-utils.js b/lib/helper/testcafe/testcafe-utils.js
index 3e32f5d1..aaf17b3e 100644
--- a/lib/helper/testcafe/testcafe-utils.js
+++ b/lib/helper/testcafe/testcafe-utils.js
@@ -28,7 +28,8 @@ const mapError = (testcafeError) => {
   if (testcafeError.errMsg) {
     throw new Error(testcafeError.errMsg);
   }
-  throw new Error(`Unknown TestCafe error ${testcafeError.toString()}`);
+  const errorInfo = `${testcafeError.callsite ? JSON.stringify(testcafeError.callsite) : ''} ${testcafeError.apiFnChain || JSON.stringify(testcafeError)}`;
+  throw new Error(`TestCafe Error: ${errorInfo}`);
 };
 
 
diff --git a/test/data/inject-fail-example/codecept.conf.js b/test/data/inject-fail-example/codecept.conf.js
index 3a68acb7..fcdf1bbb 100644
--- a/test/data/inject-fail-example/codecept.conf.js
+++ b/test/data/inject-fail-example/codecept.conf.js
@@ -7,6 +7,7 @@ exports.config = {
 
     page: './pages/page.js',
     notpage: './pages/notpage.js',
+    arraypage: './pages/arraypage.js',
 
   },
   bootstrap: null,
diff --git a/test/data/inject-fail-example/output/.gitignore b/test/data/inject-fail-example/output/.gitignore
new file mode 100644
index 00000000..e69de29b
diff --git a/test/data/inject-fail-example/pages/arraypage.js b/test/data/inject-fail-example/pages/arraypage.js
new file mode 100644
index 00000000..d1827341
--- /dev/null
+++ b/test/data/inject-fail-example/pages/arraypage.js
@@ -0,0 +1,1 @@
+module.exports = ['veni', 'vedi', 'vici'];
diff --git a/test/data/inject-fail-example/pages/notpage.js b/test/data/inject-fail-example/pages/notpage.js
index 6bac56f3..c1829cf6 100644
--- a/test/data/inject-fail-example/pages/notpage.js
+++ b/test/data/inject-fail-example/pages/notpage.js
@@ -1,4 +1,4 @@
-const { event, recorder } = require('codeceptjs');
+const { event, recorder } = codeceptjs;
 
 const { page } = inject();
 
diff --git a/test/data/inject-fail-example/pages/page.js b/test/data/inject-fail-example/pages/page.js
index ecf10975..c957c27d 100644
--- a/test/data/inject-fail-example/pages/page.js
+++ b/test/data/inject-fail-example/pages/page.js
@@ -1,9 +1,10 @@
-const { I, notpage } = inject();
+const { I, notpage, arraypage } = inject();
 
 module.exports = {
   type: (s) => {
-    console.log(s);
-    notpage.domainIds.push('asdasd');
+    console.log('type => ', s);
+    console.log('strategy', arraypage);
+    notpage.domainIds.push('newdomain');
     return notpage.domainIds;
   },
 
diff --git a/test/helper/TestCafe_test.js b/test/helper/TestCafe_test.js
index 3d665026..0ad18c98 100644
--- a/test/helper/TestCafe_test.js
+++ b/test/helper/TestCafe_test.js
@@ -8,7 +8,7 @@ const siteUrl = TestHelper.siteUrl();
 
 describe('TestCafe', function () {
   this.timeout(35000);
-  this.retries(0);
+  this.retries(1);
 
   before(() => {
     global.codecept_dir = path.join(__dirname, '/../data');
diff --git a/test/helper/webapi.js b/test/helper/webapi.js
index a7c0bbf1..ba33df05 100644
--- a/test/helper/webapi.js
+++ b/test/helper/webapi.js
@@ -918,7 +918,6 @@ module.exports.tests = function () {
     });
 
     it('should execute within block 2', () => {
-      if (isHelper('TestCafe')) return;
       return I.amOnPage('/form/example4')
         .then(() => I.fillField('Hasło', '12345'))
         .then(() => I._withinBegin({
@@ -932,36 +931,42 @@ module.exports.tests = function () {
         .then(input => assert.equal(input, '12345'));
     });
 
-    it('within should respect context in see', () => I.amOnPage('/form/example4')
-      .then(() => I.see('Rejestracja', 'fieldset'))
-      .then(() => I._withinBegin({
-        css: '.navbar-header',
-      }))
-      .then(() => I.see('Rejestracja', '.container fieldset'))
-      .catch((err) => {
-        if (!err) assert.fail('seen fieldset');
-      })
-      .then(() => I.see('Toggle navigation', '.container fieldset'))
-      .catch((err) => {
-        if (!err) assert.fail('seen fieldset');
-      }));
+    it('within should respect context in see', function () {
+      if (isHelper('TestCafe')) this.skip();
+      return I.amOnPage('/form/example4')
+        .then(() => I.see('Rejestracja', 'fieldset'))
+        .then(() => I._withinBegin({
+          css: '.navbar-header',
+        }))
+        .then(() => I.see('Rejestracja', '.container fieldset'))
+        .catch((err) => {
+          if (!err) assert.fail('seen fieldset');
+        })
+        .then(() => I.see('Toggle navigation', '.container fieldset'))
+        .catch((err) => {
+          if (!err) assert.fail('seen fieldset');
+        });
+    });
 
-    it('within should respect context in see when using nested frames', () => I.amOnPage('/iframe_nested')
-      .then(() => I._withinBegin({
-        frame: ['#wrapperId', '[name=content]'],
-      }))
-      .then(() => I.see('Kill & Destroy'))
-      .catch((err) => {
-        if (!err) assert.fail('seen "Kill & Destroy"');
-      })
-      .then(() => I.dontSee('Nested Iframe test'))
-      .catch((err) => {
-        if (!err) assert.fail('seen "Nested Iframe test"');
-      })
-      .then(() => I.dontSee('Iframe test'))
-      .catch((err) => {
-        if (!err) assert.fail('seen "Iframe test"');
-      }));
+    it('within should respect context in see when using nested frames', function () {
+      if (isHelper('TestCafe')) this.skip();
+      return I.amOnPage('/iframe_nested')
+        .then(() => I._withinBegin({
+          frame: ['#wrapperId', '[name=content]'],
+        }))
+        .then(() => I.see('Kill & Destroy'))
+        .catch((err) => {
+          if (!err) assert.fail('seen "Kill & Destroy"');
+        })
+        .then(() => I.dontSee('Nested Iframe test'))
+        .catch((err) => {
+          if (!err) assert.fail('seen "Nested Iframe test"');
+        })
+        .then(() => I.dontSee('Iframe test'))
+        .catch((err) => {
+          if (!err) assert.fail('seen "Iframe test"');
+        });
+    });
   });
 
   describe('scroll: #scrollTo, #scrollPageToTop, #scrollPageToBottom', () => {
diff --git a/test/runner/interface_test.js b/test/runner/interface_test.js
index 54b21a51..38dc706d 100644
--- a/test/runner/interface_test.js
+++ b/test/runner/interface_test.js
@@ -184,9 +184,10 @@ describe('CodeceptJS Interface', () => {
     });
   });
 
-  it.skip('should inject page objects via proxy', (done) => {
+  it('should inject page objects via proxy', (done) => {
     exec(`${config_run_config('../inject-fail-example')} --debug`, (err, stdout) => {
-      stdout.should.include('qw');
+      stdout.should.include('newdomain');
+      stdout.should.include("[ 'veni', 'vedi', 'vici' ]", 'array objects work');
       stdout.should.include('OK  | 1 passed');
       assert(!err);
       done();
diff --git a/test/unit/container_test.js b/test/unit/container_test.js
index ea4369a9..98d61320 100644
--- a/test/unit/container_test.js
+++ b/test/unit/container_test.js
@@ -128,7 +128,7 @@ describe('Container', () => {
         },
       });
       const dummyPage = require('../data/dummy_page');
-      container.support('dummyPage').should.be.equal(dummyPage);
+      container.support('dummyPage').should.be.eql(dummyPage);
     });
 
     it('should load I from path and execute _init', () => {