Skip to content

Commit 7939cf3

Browse files
committed
Initial commit
1 parent c78ff59 commit 7939cf3

12 files changed

+617
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.idea

README.md

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
## phantom-menace
2+
3+
Minimalist polyfill (minifill!) to make PhantomJS runners work with Headless Chrome.
4+
5+
## Install
6+
```shell
7+
$ npm install phantom-menace --save
8+
```
9+
## Usage
10+
In your runner script:
11+
12+
```js
13+
var {phantom, fs, system} = require('phantom-menace');
14+
```
15+
Then replace your `require('fs')` and `require('system')` with above. Cross your fingers and run it!
16+
17+
## Replacing `webpage`
18+
19+
No direct polyfill for phantom's [webpage](http://phantomjs.org/api/webpage/) module is provided. Instead you can use [chromate](https://github.com/moos/chromate) to load a target test page and listen for events.
20+
21+
```js
22+
// phantom runner to replace
23+
page = require('webpage').create();
24+
25+
page.onConsoleMessage = function (msg) {
26+
console.log(msg);
27+
};
28+
29+
page.onInitialized = function () {
30+
page.evaluate(addLogging);
31+
};
32+
33+
page.onCallback = handleResult;
34+
35+
page.open(url, function (status) { ... });
36+
37+
function handleResult(message) {
38+
var result,
39+
failed;
40+
41+
if (message) {
42+
if (message.name === 'QUnit.done') {
43+
result = message.data;
44+
failed = !result || !result.total || result.failed;
45+
46+
if (!result.total) {
47+
console.error('No tests were executed. Are you loading tests asynchronously?');
48+
}
49+
50+
exit(failed ? 1 : 0);
51+
}
52+
}
53+
};
54+
55+
```
56+
Replace that with an equivalent _phantom-menace_ runner using [chromate](https://github.com/moos/chromate):
57+
```js
58+
var Tab = require('chromate').Tab;
59+
var {phantom, fs, system} = require('phantom-menace');
60+
61+
url = 'file://' + fs.absolute(file); // must be absolute path
62+
63+
page = new Tab({ verbose: true });
64+
page.on('console', (msg) => console.log(msg));
65+
page.on('load', () => page.execute(addLogging));
66+
page.on('done', handleResult);
67+
68+
page.open(url)
69+
.then(() => page.evaluate('typeof QUnit').then(res => {
70+
if (res === 'undefined') {
71+
console.log('QUnit not found');
72+
page.close().then(exit);
73+
}
74+
}))
75+
.catch(err => console.log('Tab.open error', err));
76+
```
77+
78+
`addLogging` is the function that registers a QUnit 'done' event. In
79+
phantomjs world, it would look something like:
80+
81+
```js
82+
function addLogging() {
83+
QUnit.done(function (result) {
84+
console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
85+
86+
if (typeof window.callPhantom === 'function') {
87+
window.callPhantom({
88+
'name': 'QUnit.done',
89+
'data': result
90+
});
91+
}
92+
});
93+
}
94+
```
95+
96+
With chromate, replace `callPhantom` with `__chromate({event, data})`:
97+
```js
98+
if (typeof window.__chromate === 'function') {
99+
window.__chromate({event: 'done', data: result });
100+
}
101+
```
102+
and modify your `handleResult` function to receive:
103+
```js
104+
{ event: 'done',
105+
data: { failed: 0, passed: 150, total: 150, runtime: 18 } }
106+
```
107+
See [./bench](./bench) folder for sample runners.
108+
109+
## Benefits
110+
Headless Chrome is great. And fast. It pays to test your code in the same browser that your end-users use.
111+
112+
## Benchmark
113+
114+
A rudimentary benchmark test was run (see [bench/passing.html] for details)
115+
consisting of 150 tests.
116+
117+
```shell
118+
$ npm run bench
119+
```
120+
121+
The tests are run 10 times, i.e. 10 invocations of phantomjs (wi-fi off, see below) or Chrome headless, for a total of 1500 tests. Here are the result:
122+
| time | PhantomJS | Chrome | improvement |
123+
|--|--|--|--|
124+
| real | 0m9.555s | 0m4.440s | **2x** |
125+
| user | 0m6.832s | 0m2.037s | 3.3x |
126+
| sys | 0m1.603s | 0m0.443s | 3.6x |
127+
128+
If the instance of Chrome headless is reused, the improvements are even more dramatic, real time dropping to 2.87s (3.3x) and user time to 1.7s (4x).
129+
130+
### phantomjs/Qt wi-fi issue
131+
Latest version of PhantomJS (2.1) that is based on Qt is suffering [an issue](https://github.com/ariya/phantomjs/issues/14296) which results is severly degraded performance while wi-fi is turned on. This is quite a henderance when running tests on deverloper machines.
132+
133+
The improvements gained by Chrome headless against phantom when wi-fi is on is as follows:
134+
| time | PhantomJS | improvement with Chrome |
135+
|--|--|--|
136+
| real | 0m57.327s | **13x** |
137+
| user | 0m7.703s | 3.8x |
138+
| sys | 0m3.047s | 6.8x |
139+
140+
141+
142+
## Caveats
143+
144+
- This is *not* a drop-in replacement. It will require some fidgeting to make it work.
145+
- Many features are missing, including:
146+
- cookie support
147+
- many `webpage` module methods
148+
- `fs` module polyfill has been well tested, but is missing some methods.
149+
- `system` module parameters (e.g. `system.platform`) are based on nodejs's
150+
and may be different than phantom's.
151+
152+
If you find this useful, contributions are welcomed.
153+
154+
### License
155+
156+
MIT

bench/passing.html

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>QUnit Test Suite</title>
5+
<link rel="stylesheet" href="../node_modules/qunitjs/qunit/qunit.css" type="text/css" media="screen">
6+
<script type="text/javascript" src="../node_modules/qunitjs/qunit/qunit.js"></script>
7+
<!-- Your project file goes here -->
8+
<!-- <script type="text/javascript" src="myProject.js"></script> -->
9+
<!-- Your tests file goes here -->
10+
<!-- <script type="text/javascript" src="myTests.js"></script> -->
11+
12+
<!-- Adapted from https://github.com/jonkemp/qunit-phantomjs-runner -->
13+
</head>
14+
<body>
15+
<div id="qunit"></div>
16+
<div id="qunit-fixture"></div>
17+
18+
<script>
19+
20+
function run(n) {
21+
22+
// Let's test this function
23+
function isEven(val) {
24+
return val % 2 === 0;
25+
}
26+
27+
test(n + ' isEven()', function () {
28+
ok(isEven(0), 'Zero is an even number');
29+
ok(isEven(2), 'So is two');
30+
ok(isEven(-4), 'So is negative four');
31+
ok(!isEven(1), 'One is not an even number');
32+
ok(!isEven(-7), 'Neither is negative seven');
33+
});
34+
35+
// Let's test this function
36+
function multiply(a, b) {
37+
return a * b;
38+
}
39+
40+
test(n + ' multiply()', function () {
41+
ok(multiply(0, 1) === 0, 'Zero times one is zero');
42+
ok(multiply(2, 2) === 4, 'Two times two is four');
43+
ok(multiply(1, 3) === 3, 'One times three is three');
44+
ok(multiply(10, 5) === 50, 'Five times ten is fifty');
45+
ok(multiply(10, 10) === 100, 'Ten times ten is one hundred');
46+
});
47+
48+
test(n + ' querySelector', function () {
49+
equal(document.querySelectorAll('div').length, 3);
50+
equal(document.querySelectorAll('script').length, 2);
51+
equal(document.querySelectorAll('input.foo').length, 0);
52+
equal(document.querySelectorAll('span.bar').length, 0);
53+
equal(document.querySelectorAll('a.baz').length, 0);
54+
});
55+
56+
}
57+
58+
for (var n = 0; ++n <= 10;) {
59+
run(n);
60+
}
61+
</script>
62+
</body>
63+
</html>

bench/run.sh

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
3+
# usage: run.sh phantom|chrome [count|10]
4+
5+
P=`dirname "$0"`
6+
phantom="$P/../node_modules/.bin/phantomjs $P/runner-phantom.js"
7+
chrome="node $P/runner-chrome.js"
8+
test="${!1} $P/passing.html"
9+
repeat=${2-10}
10+
11+
echo "Running $test $repeat times"
12+
13+
for n in $(seq 1 $repeat)
14+
do
15+
echo "[$n]"
16+
$test
17+
done
18+
19+

bench/runner-chrome.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
var {Chrome, Tab} = require('chromate');
2+
var chrome;
3+
var timeout = 3000;
4+
5+
Chrome.start({
6+
canary: true
7+
}).then(run)
8+
.catch(err => {
9+
console.log('Could not start Chrome', err);
10+
});
11+
12+
function run(ch) {
13+
'use strict';
14+
15+
console.log('Chrome started')
16+
chrome = ch;
17+
18+
var {phantom, fs, system} = require('phantom-menace');
19+
20+
var url, page, timeout,
21+
args = system.args;
22+
23+
// arg[0]: scriptName, args[1...]: arguments
24+
if (args.length < 2) {
25+
console.error('Usage:\n node runner-chrome.js [url-of-your-qunit-testsuite]');
26+
exit(1);
27+
}
28+
29+
url = 'file://' + fs.absolute(args[1]);
30+
31+
page = new Tab({
32+
timeout: timeout
33+
// verbose: true
34+
});
35+
36+
function handleResult(message) {
37+
var result = message.data,
38+
failed;
39+
40+
console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
41+
42+
failed = !result || !result.total || result.failed;
43+
44+
if (!result.total) {
45+
console.error('No tests were executed. Are you loading tests asynchronously?');
46+
}
47+
48+
page.close();
49+
exit(failed ? 1 : 0);
50+
}
51+
52+
page.on('log', console.log);
53+
54+
page.on('load', function() {
55+
page.execute(addLogging);
56+
});
57+
58+
page.on('done', handleResult);
59+
60+
page.open(url)
61+
.then(() => page.evaluate('typeof QUnit').then(res => {
62+
if (res === 'undefined') {
63+
console.log('QUnit not found');
64+
page.close().then(exit);
65+
}
66+
}))
67+
.catch(err => console.log('Tab.open error', err));
68+
69+
function addLogging() {
70+
var currentTestAssertions = [];
71+
QUnit.testDone(function (result) {
72+
var i,
73+
len,
74+
name = '';
75+
76+
if (result.module) {
77+
name += result.module + ': ';
78+
}
79+
name += result.name;
80+
81+
if (result.failed) {
82+
console.log('\n' + 'Test failed: ' + name);
83+
84+
for (i = 0, len = currentTestAssertions.length; i < len; i++) {
85+
console.log(' ' + currentTestAssertions[i]);
86+
}
87+
}
88+
89+
currentTestAssertions.length = 0;
90+
});
91+
92+
QUnit.done(function (result) {
93+
console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.');
94+
95+
if (typeof window.__chromate === 'function') {
96+
window.__chromate({event: 'done', data: result });
97+
}
98+
});
99+
}
100+
101+
function exit(code) {
102+
Chrome.kill(chrome);
103+
phantom.exit(code);
104+
}
105+
}

0 commit comments

Comments
 (0)