Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Integrate WDIO #1342

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
- Plays nice with CI and source control
- Run globally or locally as a standalone package app or `require('backstopjs')` right into your node app
- Incredibly easy to use: just 3 commands go a long long way!
- [BETA] Limited Support for WebDriver Protocol, based on wdio
- works as well with any wdio-(cloud)-service (see [wdio configuration]() )
- not compatible with puppeteer references pictures
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some issues with directly comparing, backstopjs puppeteer pictures with wdio pictures.

  • screensize is different a bit (still need to investigate if that what goes on, but due sickness&work I couldn't find the time)
  • puppeteer does a full page picture, webdriver captures only viewport => In theory, should be possible to merge the viewport images to a full page picture, wdio plugin is outdated
  • image interception is not possible without a proxy solution, due selenium limitation

Therefore I added a BETA support, so maybe some people get interessted to work on it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it is very very unlikely that you could ever render a reference image in puppeteer and then be able to test that against a test image generated in WDIO. Or vice versa. In addition to different high level rendering interpretations of the different application environments, there will also be a low-level rendering issue because different hardware will dither bitmap text differently. For example you can see this explanation.

TLDR; Screenshots generated by a single hardware, software and platform combination can only be compared against itself.


![BackstopJS cli report](http://garris.github.io/BackstopJS/assets/cli-report.png)

Expand Down Expand Up @@ -96,14 +99,17 @@ BackstopJS can create a default configuration file and project scaffolding in yo
```sh
$ backstop init
```


### Working with your config file

By default, BackstopJS places `backstop.json` in the root of your project. And also by default, BackstopJS looks for this file when invoked.

Pass a `--config=<configFilePathStr>` argument to test using a different config file.

**Propagation of Environment Variables**

To avoid having any hardcoded credentials, you can use "process.env." as prefix for any value in your json config. These values overwritten on runtime.
Alternativ you can use the `backstop.js` config file.

**JS based config file**

You may use a javascript based config file to allow connents in your config. Be sure to _export your config object as a node module_.
Expand Down Expand Up @@ -588,6 +594,30 @@ To use chrome headless you can currently use _puppeteer_ (https://github.com/Goo
```json
"engine": "puppeteer"
```
#### [BETA] WebDriverIO

To use WebDriverIO for Screenshot Comparison Testing you change the engine to

```json
"engine": "wdio"
```
To pass any required additional WDIO Configuration you can configure:
```json
"engineOptions": {
"wdio": {
[...]
}
}
```
Services can be configured like in wdio. Have a look at their documentation.
- [FEEDBACK WANTED] Sauce Service - https://webdriver.io/docs/sauce-service
- [TESTED] Browserstack - https://webdriver.io/docs/browserstack-service
- [TESTED] Selenium Standalone - https://webdriver.io/docs/selenium-standalone-service

The WDIO Setup is based on [wdio standalone - remote() function](https://webdriver.io/docs/setuptypes/#package-api-1) - it has slightly different configuration.
#### ⚠️ LIMITATIONS ⚠️
- Request blocking or interceptions are not possible without additional setup.
- you can proxy your whole browser session, configure proxy as capabilities https://github.com/lightbody/browsermob-proxy

### Setting Puppeteer option flags
Backstop sets two defaults for Puppeteer:
Expand Down
71 changes: 71 additions & 0 deletions backstop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"id": "backstop_default",
"viewports": [
{
"label": "phone",
"width": 320,
"height": 1768
}
],
"onBeforeScript": "wdio/onBefore.js",
"onReadyScript": "wdio/onReady.js",
"scenarios": [
{
"label": "BackstopJS Homepage",
"cookiePath": "backstop_data/engine_scripts/cookies.json",
"url": "https://garris.github.io/BackstopJS/",
"referenceUrl": "",
"readyEvent": "",
"readySelector": "",
"delay": 0,
"hideSelectors": [],
"removeSelectors": [],
"hoverSelector": "",
"clickSelector": "",
"postInteractionWait": 0,
"selectors": [],
"selectorExpansion": true,
"expect": 0,
"misMatchThreshold": 0.1,
"requireSameDimensions": true
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"engine_scripts": "backstop_data/engine_scripts",
"html_report": "backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
},
"report": [
"browser"
],
"engine": "wdio",
"engineOptions": {
"wdio": {
"logLevel": "trace",
"protocol": "http",
"hostname": "hub.browserstack.com",
"capabilities": {
"browserName": "chrome"
},
"user": "process.env.BROWSERSTACK_ACCESS_USER",
"key": "process.env.BROWSERSTACK_ACCESS_KEY",
"services": [
[
"browserstack", {
"browserstackLocal": true
}]
]
},
"puppeteer": {
"args": [
"--no-sandbox"
]
}
},
"asyncCaptureLimit": 5,
"asyncCompareLimit": 50,
"debug": false,
"debugWindow": false
}
14 changes: 14 additions & 0 deletions backstop_data/engine_scripts/cookies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"domain": ".www.yourdomain.com",
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "no_restriction"
}
]
Binary file added backstop_data/engine_scripts/imageStub.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions backstop_data/engine_scripts/puppet/clickAndHoverHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module.exports = async (page, scenario) => {
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
const scrollToSelector = scenario.scrollToSelector;
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]

if (keyPressSelector) {
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
await page.waitFor(keyPressSelectorItem.selector);
await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress);
}
}

if (hoverSelector) {
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
await page.waitFor(hoverSelectorIndex);
await page.hover(hoverSelectorIndex);
}
}

if (clickSelector) {
for (const clickSelectorIndex of [].concat(clickSelector)) {
await page.waitFor(clickSelectorIndex);
await page.click(clickSelectorIndex);
}
}

if (postInteractionWait) {
await page.waitFor(postInteractionWait);
}

if (scrollToSelector) {
await page.waitFor(scrollToSelector);
await page.evaluate(scrollToSelector => {
document.querySelector(scrollToSelector).scrollIntoView();
}, scrollToSelector);
}
};
65 changes: 65 additions & 0 deletions backstop_data/engine_scripts/puppet/ignoreCSP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* IGNORE CSP HEADERS
* Listen to all requests. If a request matches scenario.url
* then fetch the request again manually, strip out CSP headers
* and respond to the original request without CSP headers.
* Allows `ignoreHTTPSErrors: true` BUT... requires `debugWindow: true`
*
* see https://github.com/GoogleChrome/puppeteer/issues/1229#issuecomment-380133332
* this is the workaround until Page.setBypassCSP lands... https://github.com/GoogleChrome/puppeteer/pull/2324
*
* @param {REQUEST} request
* @return {VOID}
*
* Use this in an onBefore script E.G.
```
module.exports = async function(page, scenario) {
require('./removeCSP')(page, scenario);
}
```
*
*/

const fetch = require('node-fetch');
const https = require('https');
const agent = new https.Agent({
rejectUnauthorized: false
});

module.exports = async function (page, scenario) {
const intercept = async (request, targetUrl) => {
const requestUrl = request.url();

// FIND TARGET URL REQUEST
if (requestUrl === targetUrl) {
const cookiesList = await page.cookies(requestUrl);
const cookies = cookiesList.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
const headers = Object.assign(request.headers(), { cookie: cookies });
const options = {
headers: headers,
body: request.postData(),
method: request.method(),
follow: 20,
agent
};

const result = await fetch(requestUrl, options);

const buffer = await result.buffer();
let cleanedHeaders = result.headers._headers || {};
cleanedHeaders['content-security-policy'] = '';
await request.respond({
body: buffer,
headers: cleanedHeaders,
status: result.status
});
} else {
request.continue();
}
};

await page.setRequestInterception(true);
page.on('request', req => {
intercept(req, scenario.url);
});
};
37 changes: 37 additions & 0 deletions backstop_data/engine_scripts/puppet/interceptImages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* INTERCEPT IMAGES
* Listen to all requests. If a request matches IMAGE_URL_RE
* then stub the image with data from IMAGE_STUB_URL
*
* Use this in an onBefore script E.G.
```
module.exports = async function(page, scenario) {
require('./interceptImages')(page, scenario);
}
```
*
*/

const fs = require('fs');
const path = require('path');

const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i;
const IMAGE_STUB_URL = path.resolve(__dirname, '../../imageStub.jpg');
const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL);
const HEADERS_STUB = {};

module.exports = async function (page, scenario) {
const intercept = async (request, targetUrl) => {
if (IMAGE_URL_RE.test(request.url())) {
await request.respond({
body: IMAGE_DATA_BUFFER,
headers: HEADERS_STUB,
status: 200
});
} else {
request.continue();
}
};
await page.setRequestInterception(true);
page.on('request', intercept);
};
33 changes: 33 additions & 0 deletions backstop_data/engine_scripts/puppet/loadCookies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const fs = require('fs');

module.exports = async (page, scenario) => {
let cookies = [];
const cookiePath = scenario.cookiePath;

// READ COOKIES FROM FILE IF EXISTS
if (fs.existsSync(cookiePath)) {
cookies = JSON.parse(fs.readFileSync(cookiePath));
}

// MUNGE COOKIE DOMAIN
cookies = cookies.map(cookie => {
if (cookie.domain.startsWith('http://') || cookie.domain.startsWith('https://')) {
cookie.url = cookie.domain;
} else {
cookie.url = 'https://' + cookie.domain;
}
delete cookie.domain;
return cookie;
});

// SET COOKIES
const setCookies = async () => {
return Promise.all(
cookies.map(async (cookie) => {
await page.setCookie(cookie);
})
);
};
await setCookies();
console.log('Cookie state restored with:', JSON.stringify(cookies, null, 2));
};
3 changes: 3 additions & 0 deletions backstop_data/engine_scripts/puppet/onBefore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = async (page, scenario, vp) => {
await require('./loadCookies')(page, scenario);
};
6 changes: 6 additions & 0 deletions backstop_data/engine_scripts/puppet/onReady.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = async (page, scenario, vp) => {
console.log('SCENARIO > ' + scenario.label);
await require('./clickAndHoverHelper')(page, scenario);

// add more ready handlers here...
};
15 changes: 15 additions & 0 deletions backstop_data/engine_scripts/puppet/overrideCSS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const BACKSTOP_TEST_CSS_OVERRIDE = `html {background-image: none;}`;

module.exports = async (page, scenario) => {
// inject arbitrary css to override styles
await page.evaluate(`window._styleData = '${BACKSTOP_TEST_CSS_OVERRIDE}'`);
await page.evaluate(() => {
const style = document.createElement('style');
style.type = 'text/css';
const styleNode = document.createTextNode(window._styleData);
style.appendChild(styleNode);
document.head.appendChild(style);
});

console.log('BACKSTOP_TEST_CSS_OVERRIDE injected for: ' + scenario.label);
};
42 changes: 42 additions & 0 deletions backstop_data/engine_scripts/wdio/clickAndHoverHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module.exports = async (browser, scenario) => {
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
const scrollToSelector = scenario.scrollToSelector;
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]

if (keyPressSelector) {
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
await browser.$(keyPressSelectorItem.selector).waitForDisplayed();
const input = browser.$(keyPressSelectorItem.selector);
input.setValue(keyPressSelectorItem.keyPress);
}
}

if (hoverSelector) {
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
await browser.$(hoverSelectorIndex).waitForDisplayed();
browser.$(hoverSelectorIndex).moveTo();
}
}

if (clickSelector) {
for (const clickSelectorIndex of [].concat(clickSelector)) {
await browser.$(clickSelectorIndex).waitForDisplayed();
const clickElement = browser.$(clickSelectorIndex);
clickElement.click();
}
}

if (postInteractionWait) {
await browser.$(postInteractionWait).waitForDisplayed();
}

if (scrollToSelector) {
await browser.$(scrollToSelector).waitForDisplayed();

await browser.execute(scrollToSelector => {
document.querySelector(scrollToSelector).scrollIntoView();
}, scrollToSelector);
}
};
Loading