Skip to content
This repository was archived by the owner on Oct 10, 2021. It is now read-only.

Commit cb0c85a

Browse files
committed
allow testing in Selenium locally using --selenium
1 parent 1a3391c commit cb0c85a

File tree

9 files changed

+391
-5
lines changed

9 files changed

+391
-5
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ When iterating on your tests during development, simply use zuul `--local` mode
2424

2525
See the [quickstart](https://github.com/defunctzombie/zuul/wiki/quickstart) page on the wiki for more details.
2626

27+
### Automated browser tests
28+
29+
You can test in PhantomJS using:
30+
31+
zuul --phantom
32+
33+
Note that PhantomJS must be installed separately, e.g. `npm install phantomjs`.
34+
35+
You can also test using Selenium against any browser you have installed locally. For instance:
36+
37+
zuul --selenium
38+
39+
will test in the default browser (Firefox). To test in another browser, you can do:
40+
41+
zuul --selenium --browser-name chrome
42+
43+
Or:
44+
45+
zuul --selenium --browser-name firefox --browser-version 41.0.1
46+
2747
### Cross browser testing via Saucelabs
2848

2949
The reason we go through all this trouble in the first place is to seamlessly run our tests against all those browsers we don't have installed. Luckily, [saucelabs](https://saucelabs.com/) runs some browsers and we can easily task zuul to test on those.

bin/zuul

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ program
2525
.option('--phantom-remote-debugger-port [port]', 'connect phantom to remote debugger')
2626
.option('--phantom-remote-debugger-autorun', 'run tests automatically when --phantom-remote-debugger-port is specified')
2727
.option('--electron', 'run tests in electron. electron must be installed separately.')
28+
.option('--selenium', 'run tests in Selenium, locally. Default browser: Chrome. Use --browser-name and --browser-version for others')
2829
.option('--tunnel-host <host url>', 'specify a localtunnel server to use for forwarding')
2930
.option('--sauce-connect [tunnel-identifier]', 'use saucelabs with sauce connect instead of localtunnel. Optionally specify the tunnel-identifier')
3031
.option('--server <the server script>', 'specify a server script to be run')
@@ -48,6 +49,7 @@ var config = {
4849
phantomRemoteDebuggerPort: program.phantomRemoteDebuggerPort,
4950
phantomRemoteDebuggerAutorun: program.phantomRemoteDebuggerAutorun,
5051
electron: program.electron,
52+
selenium: program.selenium,
5153
prj_dir: process.cwd(),
5254
tunnel_host: program.tunnelHost,
5355
sauce_connect: program.sauceConnect,
@@ -127,12 +129,14 @@ if (config.files.length === 0) {
127129
return process.exit(1);
128130
}
129131

130-
if ((program.browserVersion || program.browserPlatform) && !program.browserName) {
132+
if (((program.browserVersion || program.browserPlatform) && !program.browserName) &&
133+
!program.selenium) {
131134
console.error('the browser name needs to be specified (via --browser-name)');
132135
return process.exit(1);
133136
}
134137

135-
if ((program.browserName || program.browserPlatform) && !program.browserVersion) {
138+
if (((program.browserName || program.browserPlatform) && !program.browserVersion) &&
139+
!program.selenium) {
136140
console.error('the browser version needs to be specified (via --browser-version)');
137141
return process.exit(1);
138142
}
@@ -188,7 +192,7 @@ if (config.local) {
188192
return zuul.run(function(passed) {
189193
});
190194
}
191-
else if (config.phantom || config.electron) {
195+
else if (config.phantom || config.electron || config.selenium) {
192196
return zuul.run(function(passed) {
193197
process.exit(passed ? 0 : 1);
194198
});

lib/Selenium.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
var path = require('path');
2+
var EventEmitter = require('events').EventEmitter;
3+
var debug = require('debug')('zuul:selenium');
4+
var wd = require('wd');
5+
6+
var SELENIUM_VERSION = '2.52.0';
7+
8+
var setup_test_instance = require('./setup');
9+
require('colors');
10+
11+
function Selenium(opt) {
12+
if (!(this instanceof Selenium)) {
13+
return new Selenium(opt);
14+
}
15+
16+
var self = this;
17+
self._opt = opt;
18+
self._browserName = (opt.browsers && opt.browsers[0] && opt.browsers[0].name) || null;
19+
self._browserVersion = (opt.browsers && opt.browsers[0] && opt.browsers[0].version) || null;
20+
self.status = {
21+
passed: 0,
22+
failed: 0
23+
};
24+
}
25+
26+
Selenium.prototype.__proto__ = EventEmitter.prototype;
27+
28+
Selenium.prototype.start = function() {
29+
var self = this;
30+
31+
var seleniumClient;
32+
var selenium = require('selenium-standalone');
33+
34+
function finish() {
35+
if (self._finished) {
36+
return;
37+
}
38+
self._finished = true;
39+
reporter.removeAllListeners();
40+
if (seleniumClient) {
41+
seleniumClient.quit();
42+
}
43+
}
44+
45+
self.controller = setup_test_instance(self._opt, function(err, url) {
46+
if (err) {
47+
console.log('Error: %s'.red, err);
48+
self.emit('done', {
49+
passed: false
50+
});
51+
finish();
52+
}
53+
54+
debug('url %s', url);
55+
56+
var reporter = new EventEmitter();
57+
58+
reporter.on('console', function(msg) {
59+
console.log.apply(console, msg.args);
60+
});
61+
62+
reporter.on('test', function(test) {
63+
console.log('starting', test.name.white);
64+
});
65+
66+
reporter.on('test_end', function(test) {
67+
if (!test.passed) {
68+
console.log('failed', test.name.red);
69+
return self.status.failed++;
70+
}
71+
72+
console.log('passed:', test.name.green);
73+
self.status.passed++;
74+
});
75+
76+
reporter.on('assertion', function(assertion) {
77+
console.log('Error: %s'.red, assertion.message);
78+
assertion.frames.forEach(function(frame) {
79+
console.log(' %s %s:%d'.grey, frame.func, frame.filename, frame.line);
80+
});
81+
console.log();
82+
});
83+
84+
reporter.on('done', function() {
85+
finish();
86+
});
87+
88+
self.emit('init', url);
89+
self.emit('start', reporter);
90+
91+
var opts = {version: SELENIUM_VERSION};
92+
selenium.install(opts, function(err) {
93+
if (err) {
94+
console.log('Error: %s'.red, new Error(
95+
'Failed to install selenium'));
96+
self.emit('done', {
97+
passed: false
98+
});
99+
finish();
100+
return;
101+
}
102+
selenium.start(opts, function() {
103+
seleniumClient = wd.promiseChainRemote();
104+
onSeleniumReady();
105+
});
106+
});
107+
108+
function onSeleniumClientReady() {
109+
var lastMessage = Date.now();
110+
var script = 'window.zuul_msg_bus ? ' +
111+
'window.zuul_msg_bus.splice(0, window.zuul_msg_bus.length) : ' +
112+
'[]';
113+
114+
var interval = setInterval(poll, 2000);
115+
116+
function onDoneMessage() {
117+
clearInterval(interval);
118+
seleniumClient.quit(function () {
119+
seleniumClient = null;
120+
self.emit('done', {
121+
passed: self.status.passed,
122+
failed: self.status.failed
123+
});
124+
finish();
125+
});
126+
}
127+
128+
function onGetMessages(err, messages) {
129+
if (err) {
130+
self.emit('error', err);
131+
} else if (messages.length) {
132+
lastMessage = Date.now();
133+
messages.forEach(function (msg) {
134+
debug('msg: %j', msg);
135+
if (msg.type === 'done') {
136+
onDoneMessage();
137+
} else {
138+
reporter.emit(msg.type, msg);
139+
}
140+
});
141+
} else if ((Date.now() - lastMessage) > testTimeout) {
142+
clearInterval(interval);
143+
console.log('Error: %s'.red, new Error(
144+
'selenium timeout after ' + testTimeout + ' ms'));
145+
self.emit('done', {
146+
passed: false
147+
});
148+
finish();
149+
}
150+
}
151+
152+
function poll() {
153+
seleniumClient.eval(script, onGetMessages);
154+
}
155+
}
156+
157+
function onSeleniumReady() {
158+
// TODO: maybe these should be configurable
159+
var testTimeout = 120000;
160+
var tunnelId = process.env.TRAVIS_JOB_NUMBER || 'tunnel-' + Date.now();
161+
var opts = {
162+
tunnelTimeout: testTimeout,
163+
name: self._browserName + ' - ' + tunnelId,
164+
'max-duration': 60 * 45,
165+
'command-timeout': 599,
166+
'idle-timeout': 599,
167+
'tunnel-identifier': tunnelId
168+
};
169+
if (self._browserName) {
170+
opts.browserName = self._browserName;
171+
}
172+
if (self._browserVersion) {
173+
opts.version = self._browserVersion;
174+
}
175+
176+
seleniumClient.init(opts).get(url, onSeleniumClientReady);
177+
}
178+
});
179+
};
180+
181+
Selenium.prototype.shutdown = function() {
182+
if (self.controller) {
183+
self.controller.shutdown();
184+
}
185+
};
186+
187+
module.exports = Selenium;

lib/zuul.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var setup_test_instance = require('./setup');
1010
var SauceBrowser = require('./SauceBrowser');
1111
var PhantomBrowser = require('./PhantomBrowser');
1212
var Electron = require('./Electron');
13+
var Selenium = require('./Selenium');
1314

1415
module.exports = Zuul;
1516

@@ -125,6 +126,14 @@ Zuul.prototype.run = function(done) {
125126
});
126127
return electron.start();
127128
}
129+
if (config.selenium) {
130+
var selenium = Selenium(config);
131+
self.emit('browser', selenium);
132+
selenium.once('done', function(results) {
133+
done(results.failed === 0 && results.passed > 0);
134+
});
135+
return selenium.start();
136+
}
128137

129138
var batch = new Batch();
130139
batch.concurrency(self._concurrency);

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"lodash": "3.10.1",
2929
"opener": "1.4.0",
3030
"osenv": "0.0.3",
31+
"selenium-standalone": "^5.0.0",
3132
"shallow-copy": "0.0.1",
3233
"shell-quote": "1.4.1",
3334
"stack-mapper": "0.2.2",
@@ -36,7 +37,7 @@
3637
"tap-finished": "0.0.1",
3738
"tap-parser": "0.7.0",
3839
"watchify": "3.7.0",
39-
"wd": "0.3.11",
40+
"wd": "0.4.0",
4041
"xtend": "2.1.2",
4142
"yamljs": "0.1.4",
4243
"zuul-localtunnel": "1.1.0"
@@ -70,4 +71,4 @@
7071
"scripts": {
7172
"test": "DEBUG=zuul* mocha --ui qunit --timeout 0 --bail -- test/index.js"
7273
}
73-
}
74+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
var Zuul = require('../../');
2+
3+
var after = require('after');
4+
var assert = require('assert');
5+
6+
test('jasmine - phantom', function(done) {
7+
done = after(3, done);
8+
9+
var config = {
10+
ui: 'jasmine',
11+
prj_dir: __dirname + '/../fixtures/jasmine',
12+
selenium: true,
13+
concurrency: 1,
14+
files: [__dirname + '/../fixtures/jasmine/test.js']
15+
};
16+
17+
var zuul = Zuul(config);
18+
19+
// each browser we test will emit as a browser
20+
zuul.on('browser', function(browser) {
21+
browser.on('init', function() {
22+
done();
23+
});
24+
25+
browser.on('done', function(results) {
26+
assert.equal(results.passed, 1);
27+
assert.equal(results.failed, 1);
28+
done();
29+
});
30+
});
31+
32+
zuul.run(function(passed) {
33+
assert.ok(!passed);
34+
done();
35+
});
36+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
var Zuul = require('../../');
2+
3+
var after = require('after');
4+
var assert = require('assert');
5+
6+
test('mocha-qunit - phantom', function(done) {
7+
done = after(3, done);
8+
9+
var config = {
10+
ui: 'mocha-qunit',
11+
prj_dir: __dirname + '/../fixtures/mocha-qunit',
12+
selenium: true,
13+
concurrency: 1,
14+
files: [__dirname + '/../fixtures/mocha-qunit/test.js']
15+
};
16+
var zuul = Zuul(config);
17+
18+
zuul.on('browser', function(browser) {
19+
browser.once('start', function(reporter) {
20+
reporter.once('done', function(results) {
21+
assert.equal(results.passed, false);
22+
assert.equal(results.stats.passed, 1);
23+
assert.equal(results.stats.failed, 1);
24+
done();
25+
});
26+
});
27+
28+
browser.on('done', function(results) {
29+
assert.equal(results.passed, 1);
30+
assert.equal(results.failed, 1);
31+
done();
32+
});
33+
});
34+
35+
zuul.run(function(passed) {
36+
assert.ok(!passed);
37+
done();
38+
});
39+
});

0 commit comments

Comments
 (0)