Skip to content

Commit

Permalink
Try out browser testing (mapbox#9245)
Browse files Browse the repository at this point in the history
  • Loading branch information
kkaefer authored and mike-unearth committed Mar 18, 2020
1 parent 2afca08 commit 178379b
Show file tree
Hide file tree
Showing 11 changed files with 394 additions and 3 deletions.
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

0 comments on commit 178379b

Please sign in to comment.