|
| 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 |
0 commit comments