Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
- 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]() )

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

Expand Down Expand Up @@ -96,14 +98,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 +593,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