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

Add browser testing with Selenium/WebDriver #9245

Merged
merged 1 commit into from
Feb 13, 2020
Merged
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
26 changes: 26 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ workflows:
filters:
tags:
only: /.*/
- test-browser:
requires:
- prepare
filters:
tags:
only: /.*/
- deploy-benchmarks:
requires:
- lint
Expand Down Expand Up @@ -218,6 +224,26 @@ jobs:
- store_artifacts:
path: "test/integration/query-tests/index.html"

test-browser:
<<: *defaults
steps:
- attach_workspace:
at: .
- run: yarn run build-dev
- run: yarn run build-token
- run:
name: Test Chrome
environment:
SELENIUM_BROWSER: chrome
TAP_COLORS: 1
command: yarn run test-browser
- run:
name: Test Firefox
environment:
SELENIUM_BROWSER: firefox
TAP_COLORS: 1
command: yarn run test-browser

test-expressions:
<<: *defaults
steps:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-terser": "^5.1.2",
"rollup-plugin-unassert": "^0.3.0",
"selenium-webdriver": "^4.0.0-alpha.5",
"shuffle-seed": "^1.1.6",
"sinon": "^7.3.2",
"st": "^1.2.2",
Expand Down Expand Up @@ -141,6 +142,7 @@
"test-suite-clean": "find test/integration/{render,query, expressions}-tests -mindepth 2 -type d -exec test -e \"{}/actual.png\" \\; -not \\( -exec test -e \"{}/style.json\" \\; \\) -print | xargs -t rm -r",
"test-unit": "build/run-tap --reporter classic --no-coverage test/unit",
"test-build": "build/run-tap --no-coverage test/build/**/*.test.js",
"test-browser": "build/run-tap --reporter spec --no-coverage test/browser/**/*.test.js",
"test-render": "node --max-old-space-size=2048 test/render.test.js",
"test-query-node": "node test/query.test.js",
"watch-query": "testem -f test/integration/testem.js",
Expand Down
4 changes: 4 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ See [`test/integration/README.md`](https://github.com/mapbox/mapbox-gl-js/blob/m
- **You must not make network requests in test cases.** This rule holds in cases when result isn't used or is expected to fail. You may use `window.useFakeXMLHttpRequest` and `window.server` per the [Sinon API](http://sinonjs.org/docs/#server) to simulate network requests. This ensures that tests are reliable, able to be run in an isolated environment, and performant.
- **You should use clear [input space partitioning](http://crystal.uta.edu/~ylei/cse4321/data/isp.pdf) schemes.** Look for edge cases! This ensures that tests suites are comprehensive and easy to understand.

## Browser Tests

See [`test/browser/README.md`](https://github.com/mapbox/mapbox-gl-js/blob/master/test/browser/README.md).

## Spies, Stubs, and Mocks

The test object is augmented with methods from Sinon.js for [spies](http://sinonjs.org/docs/#spies), [stubs](http://sinonjs.org/docs/#stubs), and [mocks](http://sinonjs.org/docs/#mocks). For example, to use Sinon's spy API, call `t.spy(...)` within a test.
Expand Down
29 changes: 29 additions & 0 deletions test/browser/drag.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {test} from '../util/test';
import browser from './util/browser';
import {Origin} from 'selenium-webdriver';
import {equalWithPrecision} from '../util';

test("dragging", async t => {
const {driver} = browser;

await t.test("drag to the left", async t => {
const canvas = await browser.getMapCanvas(`${browser.basePath}/test/browser/fixtures/land.html`);

// Perform drag action, wait a bit the end to avoid the momentum mode.
await driver
.actions()
.move(canvas)
.press()
.move({x: 100 / browser.scaleFactor, y: 0, origin: Origin.POINTER})
.pause(200)
.release()
.perform();

const center = await driver.executeScript(() => {
/* eslint-disable no-undef */
return map.getCenter();
});
equalWithPrecision(t, center.lng, -35.15625, 0.001);
equalWithPrecision(t, center.lat, 0, 0.0000001);
});
});
58 changes: 58 additions & 0 deletions test/browser/fixtures/land.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../../../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>

<body>
<div id='map'></div>

<script src='../../../dist/mapbox-gl-dev.js'></script>
<script src='../../../debug/access_token_generated.js'></script>

<script>

var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 1,
fadeDuration: 0,
center: [0, 0],
style: {
version: 8,
sources: {
land: {
type: 'geojson',
data: `${location.origin}/test/browser/fixtures/land.json`
}
},
layers: [
{
id: 'background',
type: 'background',
paint: {
'background-color': '#72d0f2'
}
},
{
id: 'land',
type: 'fill',
source: 'land',
paint: {
'fill-color': '#f0e9e1'
}
}
]
}
});

</script>

</body>
</html>
1 change: 1 addition & 0 deletions test/browser/fixtures/land.json

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions test/browser/util/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import tap from 'tap';
import address from 'address';
import st from 'st';
import http from 'http';

import webdriver from 'selenium-webdriver';
const {Builder, By} = webdriver;

import chrome from 'selenium-webdriver/chrome';
import firefox from 'selenium-webdriver/firefox';
import safari from 'selenium-webdriver/safari';

import doubleClick from './doubleclick';
import mouseWheel from './mousewheel';

const defaultViewportSize = {width: 800, height: 600};

const chromeOptions = new chrome.Options().windowSize(defaultViewportSize);
const firefoxOptions = new firefox.Options().windowSize(defaultViewportSize);
const safariOptions = new safari.Options();

if (process.env.SELENIUM_BROWSER && process.env.SELENIUM_BROWSER.split(/:/, 3)[2] === 'android') {
chromeOptions.androidChrome().setPageLoadStrategy('normal');
}

const ip = address.ip();
const port = 9968;

const browser = {
driver: null,
pixelRatio: 1,
scaleFactor: 1,
basePath: `http://${ip}:${port}`,
getMapCanvas,
doubleClick,
mouseWheel
};

export default browser;

async function getMapCanvas(url) {
await browser.driver.get(url);

await browser.driver.executeAsyncScript(callback => {
/* eslint-disable no-undef */
if (map.loaded()) {
callback();
} else {
map.once("load", () => callback());
}
});

return browser.driver.findElement(By.className('mapboxgl-canvas'));
}

let server = null;

tap.test('start server', t => {
server = http.createServer(
st(process.cwd())
).listen(port, ip, err => {
if (err) {
t.error(err);
t.bailout();
} else {
t.ok(true, `Listening at ${ip}:${port}`);
}
t.end();
});
});

tap.test("start browser", async t => {
try {
// eslint-disable-next-line require-atomic-updates
browser.driver = await new Builder()
.forBrowser("chrome")
.setChromeOptions(chromeOptions)
.setFirefoxOptions(firefoxOptions)
.setSafariOptions(safariOptions)
.build();
} catch (err) {
t.error(err);
t.bailout();
}

const capabilities = await browser.driver.getCapabilities();
t.ok(true, `platform: ${capabilities.getPlatform()}`);
t.ok(true, `browser: ${capabilities.getBrowserName()}`);
t.ok(true, `version: ${capabilities.getBrowserVersion()}`);

if (capabilities.getBrowserName() === 'Safari') {
browser.scaleFactor = 2;
}

const metrics = await browser.driver.executeScript(size => {
/* eslint-disable no-undef */
return {
width: outerWidth - innerWidth / devicePixelRatio + size.width,
height: outerHeight - innerHeight / devicePixelRatio + size.height,
pixelRatio: devicePixelRatio
};
}, defaultViewportSize);
browser.pixelRatio = metrics.pixelRatio;
(await browser.driver.manage().window()).setRect({
width: metrics.width,
height: metrics.height
});
});

tap.tearDown(async () => {
if (browser.driver) {
await browser.driver.quit();
}

if (server) {
server.close();
}
});
30 changes: 30 additions & 0 deletions test/browser/util/doubleclick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Simulates a double click. Unfortunately, Safari doesn't properly recognize double
// clicks when sent as two subsequent clicks via the WebDriver API. Therefore, we'll
// manually dispatch a double click event for a particular location.

// Adapted from https://stackoverflow.com/a/47287595/331379
export default (element, x, y) => {
// Disables modern JS features to maintain IE11/ES5 support.
/* eslint-disable no-var, no-undef, object-shorthand */
var box = element.getBoundingClientRect();
var clientX = box.left + (typeof x !== "undefined" ? x : box.width / 2);
var clientY = box.top + (typeof y !== "undefined" ? y : box.height / 2);
var target = element.ownerDocument.elementFromPoint(clientX, clientY);

for (var e = target; e; e = e.parentElement) {
if (e === element) {
target.dispatchEvent(
new MouseEvent("dblclick", {
view: window,
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY
})
);
return null;
}
}

return "Element is not interactable";
};
45 changes: 45 additions & 0 deletions test/browser/util/mousewheel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Adapted from https://stackoverflow.com/a/47287595/331379
export default (element, deltaY, x, y) => {
// Disables modern JS features to maintain IE11/ES5 support.
/* eslint-disable no-var, no-undef, object-shorthand */
var box = element.getBoundingClientRect();
var clientX = box.left + (typeof x !== "undefined" ? x : box.width / 2);
var clientY = box.top + (typeof y !== "undefined" ? y : box.height / 2);
var target = element.ownerDocument.elementFromPoint(clientX, clientY);

for (var e = target; e; e = e.parentElement) {
if (e === element) {
target.dispatchEvent(
new MouseEvent("mouseover", {
view: window,
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY
})
);
target.dispatchEvent(
new MouseEvent("mousemove", {
view: window,
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY
})
);
target.dispatchEvent(
new WheelEvent("wheel", {
view: window,
bubbles: true,
cancelable: true,
clientX: clientX,
clientY: clientY,
deltaY: deltaY
})
);
return null;
}
}

return "Element is not interactable";
};
21 changes: 21 additions & 0 deletions test/browser/zoom.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {test} from '../util/test';
import browser from './util/browser';

test("zooming", async t => {
const {driver} = browser;

await t.test("double click at the center", async t => {
const canvas = await browser.getMapCanvas(`${browser.basePath}/test/browser/fixtures/land.html`);

// Double-click on the center of the map.
await driver.executeScript(browser.doubleClick, canvas);

// Wait until the map has settled, then report the zoom level back.
const zoom = await driver.executeAsyncScript(callback => {
/* eslint-disable no-undef */
map.once('idle', () => callback(map.getZoom()));
});

t.equals(zoom, 2, 'zoomed in by 1 zoom level');
});
});
Loading