Compare commits

...

7 Commits

Author SHA1 Message Date
Román Jiménez
9a510a8f18 update README 2017-08-29 18:12:00 +02:00
Román Jiménez
ae839dbd3f display slow tests after suite has finished 2017-08-28 19:44:19 +02:00
Román Jiménez
9aec3c2cd8 add some error handling & customizable poll frequency 2017-08-28 19:03:14 +02:00
Román Jiménez
7dcf2425de remove unnecessary argument 2017-08-25 19:24:02 +02:00
Román Jiménez
0a156892cc use same events as phantom & forward them 2017-08-25 18:44:31 +02:00
Román Jiménez
1b1a899f0e remove some lint 2017-08-25 18:27:25 +02:00
Román Jiménez
c949b89055 wip chrome support 2017-08-25 18:09:32 +02:00
6 changed files with 271 additions and 10 deletions

View File

@ -1,6 +1,6 @@
# grunt-contrib-jasmine v1.1.0 [![Build Status: Linux](https://travis-ci.org/gruntjs/grunt-contrib-jasmine.svg?branch=master)](https://travis-ci.org/gruntjs/grunt-contrib-jasmine) [![Build Status: Windows](https://ci.appveyor.com/api/projects/status/5985958by5rhnh31/branch/master?svg=true)](https://ci.appveyor.com/project/gruntjs/grunt-contrib-jasmine/branch/master)
> Run jasmine specs headlessly through PhantomJS
> Run jasmine specs headlessly
@ -24,7 +24,7 @@ grunt.loadNpmTasks('grunt-contrib-jasmine');
## Jasmine task
_Run this task with the `grunt jasmine` command._
Automatically builds and maintains your spec runner and runs your tests headlessly through PhantomJS.
Automatically builds and maintains your spec runner and runs your tests headlessly.
#### Run specs locally or on a remote server
@ -98,7 +98,7 @@ Automatically deleted upon normal runs.
Type: `String`
Default: `_SpecRunner.html`
The auto-generated specfile that phantomjs will use to run your tests.
The auto-generated specfile that the browser will use to run your tests.
Automatically deleted upon normal runs. Use the `:build` flag to generate a SpecRunner manually e.g.
`grunt jasmine:myTask:build`
@ -130,7 +130,7 @@ Specify a custom JUnit template instead of using the default `junitTemplate`.
Type: `String`
Default: `''`
The host you want PhantomJS to connect against to run your tests.
The host you want the browser to connect against to run your tests.
e.g. if using an ad hoc server from within grunt
@ -175,6 +175,33 @@ Default: `false`
Display a list of all failed tests and their failure messages
#### options.browser
Type: `String`
Default: `phantomjs`
Which headless browser to use. Supported options are:
* `phantomjs` runs your tests through PhantomJS
* `chrome` runs your tests through Chrome. See the headless option
#### options.headless
Type: `Boolean`
Default: `false`
Whether Chrome should be started headlessly or not. Requires Chrome 59
#### options.chromePoll
Type: `Number`
Default: `100`
Frequency (in milliseconds) in which the plugin polls Chrome for updates on the specs.
#### options.reportSlowerThan
Type: `Number`
Default: `-1`
If a spec is takes longer than this option it will be printed out after the run ends. Unit is milliseconds.
### Flags
Name: `build`

View File

@ -21,6 +21,7 @@
"jasmine-core": "~2.4.0",
"lodash": "~2.4.1",
"rimraf": "^2.1.4",
"simple-headless-chrome": "^4.3.1",
"sprintf-js": "~1.0.3"
},
"devDependencies": {

62
simplechrome.js Normal file
View File

@ -0,0 +1,62 @@
var Chrome = require('simple-headless-chrome');
var grunt = require('grunt');
var browser = new Chrome({
headless: true,
});
function consume (tab) {
return tab.evaluate(function () {
return ChromeQueue.splice(0, ChromeQueue.length);
});
}
var duration;
var finishedResult = {
failed: 0
};
var fails = [];
// All of this would be much more straightforward with async/await
browser.init()
.then(browser => browser.newTab({ privateTab: false }))
.then(tab => {
return tab.goTo("http://localhost:8088/_SpecRunner-affected.html")
// Return a promise that resolves when we get the FINISHED event from our ChromeReporter
.then(() => new Promise(resolve => {
// Periodically evaluate
var interval = setInterval(function () {
consume(tab)
.then(result => {
var events = result.result.value;
events.forEach(e => {
if (e.type === 'JASMINE_STARTED') {
duration = e.when;
}
if (e.type === 'FINISHED') {
duration = e.when - duration;
}
if (e.type === 'SPEC_FINISHED') {
if (e.payload.status === 'passed') {
process.stdout.write('.')
} else if (e.payload.status === 'pending') {
process.stdout.write('*');
} else if (e.payload.status === 'failed') {
process.stdout.write('x');
}
if (e.payload.failedExpectations.length > 0) {
finishedResult.failed++;
fails.concat(e.payload.failedExpectations);
}
}
})
if (events.length > 0 && events[events.length - 1].type === 'FINISHED') {
clearInterval(interval);
resolve();
}
})
}, 100);
}))
.then(() => { grunt.log.writeln('\nFinished testing (' + (duration / 1000) + ' seconds)')})
.then(() => browser.close())
}
)

View File

@ -1,3 +1,4 @@
/* global Promise:true */
/*
* grunt-contrib-jasmine
* http://gruntjs.com/
@ -19,6 +20,8 @@ module.exports = function(grunt) {
var phantomjs = require('grunt-lib-phantomjs').init(grunt),
chalk = require('chalk'),
_ = require('lodash');
var Chrome = require('simple-headless-chrome');
// local lib
var jasmine = require('./lib/jasmine').init(grunt, phantomjs);
@ -71,6 +74,10 @@ module.exports = function(grunt) {
// Merge task-specific options with these defaults.
var options = this.options({
browser: 'phantomjs',
headless: false,
chromePoll: 100,
reportSlowerThan: -1,
version: '2.2.0',
timeout: 10000,
styles: [],
@ -108,7 +115,14 @@ module.exports = function(grunt) {
}
var done = this.async();
phantomRunner(options, function(err, status) {
var runner = phantomRunner;
if (options.browser === 'chrome') {
runner = chromeRunner;
}
runner(options, function(err, status) {
var success = !err && status.failed === 0;
if (err) {
@ -127,7 +141,7 @@ module.exports = function(grunt) {
});
function phantomRunner(options, cb) {
function __fileOrHost (options) {
var file = options.outfile;
if (options.host) {
@ -135,8 +149,91 @@ module.exports = function(grunt) {
options.host += '/';
}
file = options.host + options.outfile;
} else {
file = path.resolve(file);
file = 'file://' + file;
}
return file;
}
function chromeRunner(options, cb) {
var file = __fileOrHost(options);
var intervalId;
var lastEvent;
var exitReason = null;
var message = 'Testing Jasmine specs via Chrome';
if (options.headless) {
message += ' (headless)';
}
grunt.verbose.subhead(message).or.writeln(message);
grunt.log.writeln('');
var browser = new Chrome({
headless: options.headless,
});
var emitChromeEvent = function (e) {
phantomjs.emit(e.type, e.payload);
};
function consume (tab) {
return tab.evaluate('function () { return ChromeQueue.splice(0, ChromeQueue.length); }');
}
function somethingWrong (what) {
cb(what.message, status);
}
// All of this would be much more straightforward with async/await
browser.init()
.then(function (browser) {
return browser.newTab()
.then(function (tab) {
lastEvent = new Date();
return tab.goTo(file)
// Return a promise that resolves when we get the FINISHED event from our ChromeReporter
.then(function () {
return new Promise(function (resolve) {
// Periodically evaluate
intervalId = setInterval(function () {
var date = new Date();
consume(tab)
.then(function (result) {
var events = result.result.value;
if (!events || events.length === 0) {
if (date - lastEvent > options.timeout) {
clearInterval(intervalId);
exitReason = 'Chrome has timed out';
resolve();
}
} else {
lastEvent = date;
events.forEach(emitChromeEvent);
// Exit condition
if (events.length > 0 && events[events.length - 1].type === 'jasmine.jasmineDone') {
clearInterval(intervalId);
resolve();
}
}
});
}, options.chromePoll);
});
})
.then(function () {
return browser.close();
})
.then(function () { cb(exitReason, status); });
});
}, somethingWrong);
}
function phantomRunner(options, cb) {
var file = __fileOrHost(options);
grunt.verbose.subhead('Testing Jasmine specs via PhantomJS').or.writeln('Testing Jasmine specs via PhantomJS');
grunt.log.writeln('');
@ -173,11 +270,10 @@ module.exports = function(grunt) {
tabstop = 2,
thisRun = {},
suites = {},
slowSpecs = [],
currentSuite;
status = {
failed: 0
};
status.failed = 0;
function indent(times) {
return new Array(+times * tabstop).join(' ');
@ -272,6 +368,11 @@ module.exports = function(grunt) {
failureMessages: []
};
if (options.reportSlowerThan !== -1 &&
specMetaData.duration > options.reportSlowerThan) {
slowSpecs.push(specMetaData);
}
suites[currentSuite].tests++;
var color = 'yellow',
@ -371,6 +472,14 @@ module.exports = function(grunt) {
logSummary(thisRun.summary);
}
if (options.reportSlowerThan !== -1 && slowSpecs.length > 0) {
grunt.log.writeln(chalk.yellow('Some specs were slower than ' + options.reportSlowerThan + ' ms:'));
grunt.log.writeln();
_.forEach(slowSpecs, function (slowSpec) {
grunt.log.writeln(chalk.yellow(slowSpec.fullName) + chalk.red(' (' + slowSpec.duration + ' ms)'));
})
}
if (options.junit && options.junit.path) {
writeJunitXml(suites);
}

View File

@ -0,0 +1,56 @@
/* global window:true, jasmine:true */
(function () {
window.ChromeQueue = [];
function pushToQueue (msg) {
window.ChromeQueue.push(msg);
}
var ChromeReporter = {
lastSpecStartTime: null,
jasmineStarted: function (suiteInfo) {
console.log('Jasmine started: ', new Date().toString());
pushToQueue({
type: 'jasmine.jasmineStarted',
when: new Date().getTime(),
payload: suiteInfo
});
},
suiteStarted: function (result) {
pushToQueue({
type: 'jasmine.suiteStarted',
payload: result
});
},
specStarted: function (result) {
this.lastSpecStartTime = new Date();
pushToQueue({
type: 'jasmine.specStarted',
payload: result
});
},
specDone: function (result) {
result.duration = new Date() - this.lastSpecStartTime;
pushToQueue({
type: 'jasmine.specDone',
payload: result
});
},
suiteDone: function (result) {
pushToQueue({
type: 'jasmine.suiteDone',
payload: result
});
},
jasmineDone: function () {
pushToQueue({
type: 'jasmine.jasmineDone'
});
}
};
var env = jasmine.getEnv();
env.addReporter(ChromeReporter);
})();

View File

@ -54,13 +54,19 @@ exports.init = function(grunt, phantomjs) {
}
}
exports.copyTempFile(path.join(__dirname, '/../jasmine/reporters/PhantomReporter.js'), path.join(tempDir, 'reporter.js'));
var reporter = 'PhantomReporter.js';
if (options.browser === 'chrome') {
reporter = 'ChromeReporter.js';
}
exports.copyTempFile(path.join(__dirname, '/../jasmine/reporters/' + reporter), path.join(tempDir, 'reporter.js'));
[].concat(jasmineRequire.files.cssFiles, jasmineRequire.files.jsFiles).forEach(function(name) {
var srcPath = path.join(jasmineRequire.files.path, name);
exports.copyTempFile(srcPath, path.join(tempDir, name));
});
// IMHO This is broken, it will copy all boot files but later only inject boot.js, I don't see the point
jasmineRequire.files.bootFiles.forEach(function(name) {
var srcPath = path.join(jasmineRequire.files.bootDir, name);
exports.copyTempFile(srcPath, path.join(tempDir, name));