Skip to content

Commit 4d37e2d

Browse files
committed
tests: switch from PhatomJS to headless Chromium
1 parent 54d0ebe commit 4d37e2d

File tree

5 files changed

+140
-30
lines changed

5 files changed

+140
-30
lines changed

gulpfile.js

-9
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,4 @@ gulp.task("test-watch", ["watch-dev"], done => {
193193
}, done).start();
194194
});
195195

196-
gulp.task("test-debug", ["watch-dev"], done => {
197-
new karma.Server({
198-
configFile: path.join(__dirname, "karma.conf.js"),
199-
browsers: ["PhantomJSCustom", "Chrome"],
200-
reporters: ["progress"],
201-
debug: true
202-
}, done).start();
203-
});
204-
205196
gulp.task("default", ["watch"]);

karma.conf.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
process.env.CHROMIUM_BIN = require("puppeteer").executablePath();
2+
3+
require("./tests/chromium.callback.js");
4+
15
module.exports = function(config) {
26
config.set({
37
basePath: "",
@@ -17,7 +21,7 @@ module.exports = function(config) {
1721
{ pattern: "tests/*/**/*.qml", included: false },
1822
{ pattern: "tests/*/**/*.png", included: false }
1923
],
20-
browsers: ["PhantomJSCustom"],
24+
browsers: ["ChromiumHeadlessCustom"],
2125
reporters: ["spec", "coverage"],
2226
coverageReporter: {
2327
type: "lcov",
@@ -26,11 +30,13 @@ module.exports = function(config) {
2630
browserDisconnectTolerance: 5, // required for phantomjs in windows
2731
browserNoActivityTimeout: 100000, // required for phantomjs in windows
2832
customLaunchers: {
29-
PhantomJSCustom: {
30-
base: "PhantomJS",
31-
options: {
32-
onCallback: require("./tests/phantom.callback.js")
33-
}
33+
ChromiumHeadlessCustom: {
34+
base: process.env.NOT_HEADLESS ? "Chromium" : "ChromiumHeadless",
35+
flags: [
36+
"--no-sandbox", // FIXME
37+
"--force-device-scale-factor=1",
38+
"--remote-debugging-port=9222"
39+
]
3440
}
3541
}
3642
});

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"babel-plugin-istanbul": "^3.1.2",
1010
"babel-plugin-transform-class-properties": "^6.11.5",
1111
"babel-preset-es2015": "^6.6.0",
12+
"bhttp": "^1.2.4",
1213
"eslint": "^3.3.1",
1314
"eslint-plugin-babel": "^3.3.0",
1415
"gulp": "^3.6.2",
@@ -24,12 +25,14 @@
2425
"jasmine-core": "^2.4.1",
2526
"jsdom": "^9.3.0",
2627
"karma": "^1.2.0",
28+
"karma-chrome-launcher": "^2.2.0",
2729
"karma-coverage": "^1.1.1",
2830
"karma-jasmine": "^1.0.2",
29-
"karma-phantomjs-launcher": "^1.0.2",
31+
"karma-phantomjs-launcher": "^1.0.4",
3032
"karma-spec-reporter": "~0.0.26",
33+
"mkdirp": "^0.5.1",
3134
"nyc": "^10.0.0",
32-
"phantomjs-prebuilt": "^2.1.4",
35+
"puppeteer": "^1.0.0",
3336
"qmlweb-parser": "^0.3.2",
3437
"remark-cli": "^2.0.0",
3538
"remark-lint": "^5.0.0",

tests/Render/runner.js

+22-13
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
(function() {
2-
if (!window.top.callPhantom) {
3-
console.log("Render tests require PhantomJS");
2+
if (!window.top.callPhantom && !window.top.chromeScreenshot) {
3+
console.log("Render tests require Puppeteer or PhantomJS");
44
return;
55
}
66

77
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
88

99
function screenshot(div, options) {
10-
if (!window.top.callPhantom) {
11-
return undefined;
12-
}
13-
1410
var rect0 = div.getBoundingClientRect();
1511
var rect1 = window.parent.document.getElementById("context")
1612
.getBoundingClientRect();
@@ -21,13 +17,26 @@
2117
left: rect0.left + rect1.left
2218
};
2319

24-
var base64 = window.top.callPhantom("render", {
25-
offset: offset,
26-
fileName: options && options.fileName || undefined
27-
});
28-
var image = document.createElement("img");
29-
image.src = "data:image/png;base64," + base64;
30-
return image;
20+
var image;
21+
if (window.top.callPhantom) {
22+
var base64 = window.top.callPhantom("render", {
23+
offset: offset,
24+
fileName: options && options.fileName || undefined
25+
});
26+
image = document.createElement("img");
27+
image.src = "data:image/png;base64," + base64;
28+
return image;
29+
} else if (window.top.chromeScreenshot) {
30+
image = document.createElement("img");
31+
window.top.chromeScreenshot({
32+
offset: offset,
33+
fileName: options && options.fileName || undefined
34+
}).then(function(data) {
35+
image.src = "data:image/png;base64," + data;
36+
});
37+
return image;
38+
}
39+
throw new Error("Screenshots are not supported on this platform");
3140
}
3241

3342
function image2canvas(img) {

tests/chromium.callback.js

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"use strict";
2+
3+
/* eslint-env es6 */
4+
5+
const http = require("http");
6+
const url = require("url");
7+
const querystring = require("querystring");
8+
const crypto = require("crypto");
9+
const path = require("path");
10+
const puppeteer = require("puppeteer");
11+
const bhttp = require("bhttp");
12+
const mkdirp = require("mkdirp");
13+
14+
const secret = crypto.randomBytes(64).toString("base64");
15+
const port = 9100;
16+
17+
function sleep(ms) {
18+
return new Promise(resolve => setTimeout(resolve, ms));
19+
}
20+
21+
function mkdirpAsync(dir) {
22+
return new Promise(resolve => mkdirp(dir, resolve));
23+
}
24+
25+
async function attempt() {
26+
await sleep(100);
27+
try {
28+
const res = await bhttp.get("http://localhost:9222/json");
29+
for (const item of res.body) {
30+
if (item.title === "Karma") return item;
31+
}
32+
} catch (e) {
33+
// Do nothing on a purpose
34+
}
35+
return null;
36+
}
37+
38+
async function main() {
39+
let info;
40+
while (!info) {
41+
info = await attempt();
42+
}
43+
const browser = await puppeteer.connect({
44+
browserWSEndpoint: info.webSocketDebuggerUrl
45+
});
46+
const pages = await browser.pages();
47+
const page = pages[0];
48+
page.evaluate(`
49+
window.top.chromeScreenshot = async function(options = {}) {
50+
const response = await fetch(
51+
"http://localhost:${port}/screenshot" +
52+
"?rand=" + Math.random().toString(36) +
53+
"&options=" + encodeURIComponent(JSON.stringify(options)) +
54+
"&secret=${encodeURIComponent(secret)}"
55+
);
56+
const base64 = await response.text();
57+
return base64;
58+
};
59+
`);
60+
http.createServer((req, res) => {
61+
(async () => {
62+
const parsed = url.parse(req.url);
63+
if (parsed.pathname !== "/screenshot") throw new Error(404);
64+
const query = querystring.parse(parsed.query);
65+
if (query.secret !== secret) throw new Error(403);
66+
const options = query.options ? JSON.parse(query.options) : {};
67+
const fileName = options.fileName || "";
68+
if (fileName && !/^[A-Za-z0-9_/]{1,50}\.png$/.test(fileName)) {
69+
throw new Error(422);
70+
}
71+
const offset = options.offset || null;
72+
const args = { omitBackground: true };
73+
if (options.fileName && process.env.QMLWEB_SAVE_RENDER) {
74+
const filepath = `tmp/Render/${options.fileName}`;
75+
await mkdirpAsync(path.dirname(filepath));
76+
args.path = filepath;
77+
}
78+
if (offset) {
79+
args.clip = {
80+
x: parseInt(offset.left, 10),
81+
y: parseInt(offset.top, 10),
82+
width: parseInt(offset.width, 10),
83+
height: parseInt(offset.height, 10)
84+
};
85+
}
86+
return page.screenshot(args);
87+
})().then(data => {
88+
res.setHeader("Access-Control-Allow-Origin", "*");
89+
res.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET");
90+
res.setHeader("Content-Type", "text/plain");
91+
res.end(data.toString("base64"));
92+
}).catch(e => {
93+
const code = /^[0-9]{3}$/.test(e.message) ? parseInt(e.message, 10) : 500;
94+
res.writeHead(code, { "Content-Type": "text/plain" });
95+
res.end(`Error ${code}`);
96+
});
97+
}).listen(port);
98+
}
99+
100+
main().catch(console.error);
101+

0 commit comments

Comments
 (0)