├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── gulpfile.js ├── package.json ├── public └── jasmine_favicon.png ├── spec ├── fixtures │ ├── dummy_spec.js │ ├── gulpfile.js │ └── mutable_spec.js ├── index_spec.js ├── lib │ ├── server_spec.js │ └── spec_runner_spec.js ├── spec_helper.js ├── support │ ├── deferred.js │ ├── jasmine_webdriver.js │ ├── selenium.js │ ├── unhandled_rejection_helper.js │ └── webdriver_helper.js └── webpack │ └── jasmine-plugin_spec.js ├── src ├── index.js ├── lib │ ├── boot.js │ ├── drivers │ │ ├── chrome.js │ │ ├── phantomjs.js │ │ ├── phantomjs1.js │ │ └── slimerjs.js │ ├── headless.js │ ├── helper.js │ ├── reporters │ │ ├── add_profile_reporter.js │ │ └── add_sourcemapped_stacktrace_reporter.js │ ├── runners │ │ ├── chrome_evaluate.js │ │ ├── chrome_runner │ │ ├── phantom_runner.js │ │ └── slimer_runner.js │ ├── server.js │ ├── spec_runner.js │ └── stylesheets │ │ └── sourcemapped_stacktrace_reporter.css └── webpack │ └── jasmine-plugin.js ├── tasks ├── build.js ├── default.js ├── lint.js ├── publish.js └── spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["node_modules"], 3 | "presets": [["es2015", {"loose": true}], "stage-0"], 4 | "plugins": [ 5 | "add-module-exports", 6 | "transform-runtime" 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "phantomjs": true, 6 | "node": true, 7 | "jasmine": true 8 | }, 9 | 10 | "ecmaFeatures": { 11 | "modules": true 12 | }, 13 | 14 | "parser": "babel-eslint", 15 | 16 | "plugins": ["jasmine"], 17 | 18 | "rules": { 19 | "camelcase": 0, 20 | "curly": 0, 21 | "eol-last": 0, 22 | "jasmine/no-spec-dupes": 0, 23 | "jasmine/no-suite-dupes": 0, 24 | "jasmine/no-disabled-tests": 1, 25 | "jasmine/no-focused-tests": 2, 26 | "no-undef": 0, 27 | "no-path-concat": 0, 28 | "no-process-exit": 0, 29 | "no-shadow": 0, 30 | "no-underscore-dangle": 0, 31 | "no-unused-expressions": 0, 32 | "quotes": [1, "single"], 33 | "strict": 0 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | .idea/* 4 | yarn-error.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "6" 5 | env: 6 | - CXX=g++-4.8 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | before_install: 14 | # phantomjs and slimerjs need access to TMPDIR 15 | - export TMPDIR=/tmp 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | before_script: 19 | - selenium-standalone install 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Copyright 2015 Pivotal Software, Inc. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-jasmine-browser 2 | 3 | Run jasmine tests in a browser or headless browser using gulp. 4 | 5 | [![Build Status](https://travis-ci.org/jasmine/gulp-jasmine-browser.svg?branch=master)](https://travis-ci.org/jasmine/gulp-jasmine-browser) 6 | 7 | ## Discontinued 8 | The `gulp-jasmine-browser` package is discontinued. There will be no further releases. We recommend migrating to the 9 | [jasmine-browser-runner](https://github.com/jasmine/jasmine-browser) package. 10 | 11 | ## Installing 12 | `gulp-jasmine-browser` is available as an 13 | [npm package](https://www.npmjs.com/package/gulp-jasmine-browser). 14 | 15 | ## Usage 16 | 17 | Gulp Jasmine Browser currently works with any synchronous method of loading files. The beginning examples all assume basic script loading. 18 | If you are using CommonJS to load files, you may want to skip to [Usage with Webpack](#usage-with-webpack) 19 | 20 | ### Create a Jasmine server to run specs in a browser 21 | 22 | In `gulpfile.js` 23 | 24 | ```js 25 | var gulp = require('gulp'); 26 | var jasmineBrowser = require('gulp-jasmine-browser'); 27 | 28 | gulp.task('jasmine', function() { 29 | return gulp.src(['src/**/*.js', 'spec/**/*_spec.js']) 30 | .pipe(jasmineBrowser.specRunner()) 31 | .pipe(jasmineBrowser.server({port: 8888})); 32 | }); 33 | ``` 34 | In `gulp.src` include all files you need for testing other than jasmine itself. 35 | This should include your spec files, and may also include your production JavaScript and 36 | CSS files. 37 | 38 | The jasmine server will run on the `port` given to `server`, or will default to port 8888. 39 | 40 | ### Watching for file changes 41 | 42 | To have the server automatically refresh when files change, you will want something like [gulp-watch](https://github.com/floatdrop/gulp-watch). 43 | 44 | ```js 45 | var gulp = require('gulp'); 46 | var jasmineBrowser = require('gulp-jasmine-browser'); 47 | var watch = require('gulp-watch'); 48 | 49 | gulp.task('jasmine', function() { 50 | var filesForTest = ['src/**/*.js', 'spec/**/*_spec.js']; 51 | return gulp.src(filesForTest) 52 | .pipe(watch(filesForTest)) 53 | .pipe(jasmineBrowser.specRunner()) 54 | .pipe(jasmineBrowser.server({port: 8888})); 55 | }); 56 | ``` 57 | 58 | If you are using Webpack or Browserify, you may want to use their watching mechanisms instead of this example. 59 | 60 | ### Run jasmine tests headlessly 61 | 62 | In `gulpfile.js` 63 | 64 | For Headless Chrome 65 | ```js 66 | var gulp = require('gulp'); 67 | var jasmineBrowser = require('gulp-jasmine-browser'); 68 | 69 | gulp.task('jasmine-chrome', function() { 70 | return gulp.src(['src/**/*.js', 'spec/**/*_spec.js']) 71 | .pipe(jasmineBrowser.specRunner({console: true})) 72 | .pipe(jasmineBrowser.headless({driver: 'chrome'})); 73 | }); 74 | ``` 75 | 76 | To use this driver, [puppeteer](https://www.npmjs.com/package/puppeteer) must be installed in your project. 77 | 78 | For PhantomJs 79 | ```js 80 | var gulp = require('gulp'); 81 | var jasmineBrowser = require('gulp-jasmine-browser'); 82 | 83 | gulp.task('jasmine-phantom', function() { 84 | return gulp.src(['src/**/*.js', 'spec/**/*_spec.js']) 85 | .pipe(jasmineBrowser.specRunner({console: true})) 86 | .pipe(jasmineBrowser.headless({driver: 'phantomjs'})); 87 | }); 88 | ``` 89 | 90 | To use this driver, the PhantomJS npm [package](https://www.npmjs.com/package/phantomjs) must be installed in your project. 91 | 92 | GulpJasmineBrowser assumes that if the package is not installed `phantomjs` is already installed and in your path. 93 | It is only tested with PhantomJS 2. 94 | 95 | For SlimerJs 96 | ```js 97 | var gulp = require('gulp'); 98 | var jasmineBrowser = require('gulp-jasmine-browser'); 99 | 100 | gulp.task('jasmine-slimer', function() { 101 | return gulp.src(['src/**/*.js', 'spec/**/*_spec.js']) 102 | .pipe(jasmineBrowser.specRunner({console: true})) 103 | .pipe(jasmineBrowser.headless({driver: 'slimerjs'})); 104 | }); 105 | ``` 106 | 107 | To use this driver, the SlimerJS npm [package](https://www.npmjs.com/package/slimerjs) must be installed in your project. 108 | 109 | Note the `{console: true}` passed into specRunner. 110 | 111 | 112 | 113 | ### Usage with Webpack 114 | 115 | If you would like to compile your front end assets with Webpack, for example to use 116 | commonjs style require statements, you can pipe the compiled assets into 117 | GulpJasmineBrowser. 118 | 119 | In `gulpfile.js` 120 | 121 | ```js 122 | var gulp = require('gulp'); 123 | var jasmineBrowser = require('gulp-jasmine-browser'); 124 | var webpack = require('webpack-stream'); 125 | 126 | gulp.task('jasmine', function() { 127 | return gulp.src(['spec/**/*_spec.js']) 128 | .pipe(webpack({watch: true, output: {filename: 'spec.js'}})) 129 | .pipe(jasmineBrowser.specRunner()) 130 | .pipe(jasmineBrowser.server()); 131 | }); 132 | ``` 133 | 134 | When using webpack, it is helpful to delay the jasmine server when the webpack bundle becomes invalid (to prevent serving 135 | javascript that is out of date). Adding the plugin to your webpack configuration, and adding the whenReady function to 136 | the server configuration enables this behavior. 137 | 138 | ```js 139 | var gulp = require('gulp'); 140 | var jasmineBrowser = require('gulp-jasmine-browser'); 141 | var webpack = require('webpack-stream'); 142 | 143 | gulp.task('jasmine', function() { 144 | var JasminePlugin = require('gulp-jasmine-browser/webpack/jasmine-plugin'); 145 | var plugin = new JasminePlugin(); 146 | return gulp.src(['spec/**/*_spec.js']) 147 | .pipe(webpack({watch: true, output: {filename: 'spec.js'}, plugins: [plugin]})) 148 | .pipe(jasmineBrowser.specRunner()) 149 | .pipe(jasmineBrowser.server({whenReady: plugin.whenReady})); 150 | }); 151 | ``` 152 | ### Options 153 | #### for specRunner 154 | ##### console 155 | Generates a console reporter for the spec runner that should be used with a headless browser. 156 | 157 | ##### profile 158 | Prints out timing information for your slowest specs after Jasmine is done. 159 | If used in the browser, this will print into the developer console. In headless mode, this will print to the terminal. 160 | 161 | #### for server and headless server 162 | 163 | ##### catch 164 | If true, the headless server catches exceptions raised while running tests 165 | 166 | ##### driver 167 | Sets the driver used by the headless server 168 | 169 | ##### findOpenPort 170 | To force the headless port to use a specific port you can pass an option to the headless configuration so it does not search for an open port. 171 | ```js 172 | gulp.task('jasmine', function() { 173 | var port = 8080; 174 | return gulp.src(['spec/**/*_spec.js']) 175 | .pipe(jasmineBrowser.specRunner()) 176 | .pipe(jasmineBrowser.headless({port: 8080, findOpenPort: false})); 177 | }); 178 | ``` 179 | 180 | ##### onCoverage 181 | Called with the `__coverage__` from the browser, can be used with code coverage like [istanbul](http://gotwarlost.github.io/istanbul/) 182 | 183 | ##### port 184 | Sets the port for the server 185 | 186 | ##### random 187 | If true, the headless server runs the tests in random order 188 | 189 | ##### reporter 190 | Provide a [custom reporter](http://jasmine.github.io/2.1/custom_reporter.html) for the output, defaults to the jasmine 191 | terminal reporter. 192 | 193 | ##### seed 194 | Sets the randomization seed if randomization is turned on 195 | 196 | ##### sourcemappedStacktrace 197 | **EXPERIMENTAL** asynchronously loads the sourcemapped stacktraces for better stacktraces in chrome and firefox. 198 | 199 | ##### spec 200 | Only runs specs that match the given string 201 | 202 | ##### throwFailures 203 | If true, the headless server fails tests on the first failed expectation 204 | 205 | ## Development 206 | ### Getting Started 207 | The application requires the following external dependencies: 208 | * [Node](https://nodejs.org/) 209 | * [Gulp](http://gulpjs.com/) 210 | * [PhantomJS](http://phantomjs.org/) - if you want to run tests with Phantom, see your options under 'Usage.' 211 | 212 | The rest of the dependencies are handled through: 213 | ```bash 214 | npm install 215 | ``` 216 | 217 | Run tests with: 218 | ```bash 219 | npm test 220 | ``` 221 | 222 | Note: `npm test` need a webdriver server up and running. An easy way of accomplish that is by using `webdriver-manager`: 223 | 224 | ```bash 225 | npm install --global webdriver-manager 226 | webdriver-manager update 227 | webdriver-manager start 228 | ``` 229 | 230 | (c) Copyright 2016 Pivotal Software, Inc. All Rights Reserved. 231 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('babel-polyfill'); 3 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 4 | require('./tasks/build'); 5 | require('./tasks/lint'); 6 | require('./tasks/publish'); 7 | require('./tasks/spec'); 8 | require('./tasks/default'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-jasmine-browser", 3 | "version": "4.1.0", 4 | "license": "MIT", 5 | "description": "", 6 | "keywords": [ 7 | "gulp", 8 | "gulpplugin", 9 | "jasmine", 10 | "test", 11 | "testing", 12 | "spec" 13 | ], 14 | "homepage": "https://github.com/jasmine/gulp-jasmine-browser", 15 | "bugs": "https://github.com/jasmine/gulp-jasmine-browser/issues", 16 | "main": "index.js", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/jasmine/gulp-jasmine-browser" 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.4.0", 23 | "babel-eslint": "^6.1.2", 24 | "babel-plugin-add-module-exports": "^0.2.1", 25 | "babel-plugin-transform-object-assign": "^6.3.13", 26 | "babel-plugin-transform-runtime": "^6.4.0", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-stage-0": "^6.3.13", 29 | "cheerio": "^0.20.0", 30 | "debug": "^2.2.0", 31 | "del": "^2.2.0", 32 | "eslint-plugin-jasmine": "^1.8.1", 33 | "gulp": "^4.0.0", 34 | "gulp-babel": "^6.1.1", 35 | "gulp-eslint": "^3.0.1", 36 | "gulp-if": "^2.0.0", 37 | "gulp-jasmine": "^4.0.0", 38 | "gulp-plumber": "^1.0.0", 39 | "jasmine-async-suite": "^0.0.7", 40 | "merge-stream": "^1.0.0", 41 | "methods": "^1.1.1", 42 | "npm": "^5.0.4", 43 | "phantomjs": "^2.1.7", 44 | "phantomjs-prebuilt": "^2.1.16", 45 | "puppeteer": "^1.3.0", 46 | "require-dir": "^0.3.2", 47 | "selenium-standalone": "^6.5.0", 48 | "slimerjs": "^0.906.2", 49 | "supertest": "^3.0.0", 50 | "wait-on": "^2.0.2", 51 | "webdriverio": "^4.12.0", 52 | "webpack-stream": "^3.2.0" 53 | }, 54 | "dependencies": { 55 | "babel-polyfill": "^6.3.14", 56 | "babel-runtime": "^6.3.19", 57 | "express": "^4.13.3", 58 | "flat-map": "^1.0.0", 59 | "jasmine-json-stream-reporter": "^0.3.1", 60 | "jasmine-profile-reporter": "^0.0.2", 61 | "jasmine-terminal-reporter": "^1.0.2", 62 | "lodash.once": "^4.0.0", 63 | "mime": "^1.3.4", 64 | "multipipe": "^2.0.1", 65 | "portastic": "^1.0.1", 66 | "qs": "^6.5.1", 67 | "serve-favicon": "^2.3.0", 68 | "sourcemapped-stacktrace": "^1.0.1", 69 | "split2": "^2.1.0", 70 | "thenify": "^3.1.1", 71 | "through2": "^2.0.0", 72 | "through2-reduce": "^1.1.1", 73 | "vinyl": "^1.2.0", 74 | "ws": "^3.3.1" 75 | }, 76 | "peerDependencies": { 77 | "fancy-log": "^1.3.2", 78 | "gulp": "^4.0.0", 79 | "jasmine-core": "^3.1.0", 80 | "plugin-error": "^1.0.1" 81 | }, 82 | "scripts": { 83 | "test": "gulp" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasmine/gulp-jasmine-browser/606f0b50dd39b3527f5da08685a53884d3e3f9ad/public/jasmine_favicon.png -------------------------------------------------------------------------------- /spec/fixtures/dummy_spec.js: -------------------------------------------------------------------------------- 1 | describe('dummy', function() { 2 | it('makes a basic passing assertion', function() { 3 | console.log('A message from the page.'); 4 | expect(true).toBe(true); 5 | }); 6 | 7 | it('makes a basic failing assertion', function() { 8 | expect(true).toBe(false); 9 | }); 10 | }); -------------------------------------------------------------------------------- /spec/fixtures/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const webpack = require('webpack-stream'); 3 | const jasmineBrowser = require('../../dist/index'); 4 | 5 | gulp.task('phantomjs', function() { 6 | return gulp.src('dummy_spec.js') 7 | .pipe(jasmineBrowser.specRunner({console: true})) 8 | .pipe(jasmineBrowser.headless({driver: 'phantomjs', showColors: false})); 9 | }); 10 | 11 | gulp.task('slimerjs', function() { 12 | return gulp.src('dummy_spec.js') 13 | .pipe(jasmineBrowser.specRunner({console: true})) 14 | .pipe(jasmineBrowser.headless({driver: 'slimerjs', showColors: false})); 15 | }); 16 | 17 | gulp.task('chrome', () => { 18 | return gulp.src('dummy_spec.js') 19 | .pipe(jasmineBrowser.specRunner({console: true})) 20 | .pipe(jasmineBrowser.headless({driver: 'chrome', showColors: false})); 21 | }); 22 | 23 | gulp.task('server', function() { 24 | return gulp.src('dummy_spec.js') 25 | .pipe(jasmineBrowser.specRunner({console: false})) 26 | .pipe(jasmineBrowser.server()); 27 | }); 28 | 29 | gulp.task('webpack-server', function() { 30 | return gulp.src('mutable_spec.js') 31 | .pipe(webpack({watch: true, output: {filename: 'spec.js'}})) 32 | .pipe(jasmineBrowser.specRunner({console: false})) 33 | .pipe(jasmineBrowser.server()); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/fixtures/mutable_spec.js: -------------------------------------------------------------------------------- 1 | it('makes a basic failing assertion', function() { expect(true).toBe(false); }); -------------------------------------------------------------------------------- /spec/index_spec.js: -------------------------------------------------------------------------------- 1 | import {describeWithWebdriver, describeWithoutTravisCI, visit} from './spec_helper'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import childProcess from 'child_process'; 5 | 6 | describe('gulp-jasmine-browser', function() { 7 | const gulpTimeout = 15000; 8 | 9 | let processes; 10 | 11 | beforeEach(function() { 12 | processes = []; 13 | }); 14 | 15 | function gulp(task) { 16 | let resolveCompleted; 17 | const completed = new Promise(resolve => resolveCompleted = resolve); 18 | 19 | const gulpPath = path.resolve('node_modules', '.bin', 'gulp'); 20 | const gulpFile = path.resolve(__dirname, 'fixtures', 'gulpfile.js'); 21 | const process = childProcess.exec([gulpPath, '--gulpfile', gulpFile, task].join(' '), 22 | {timeout: gulpTimeout}, (error, stdout, stderr) => resolveCompleted({error, stdout, stderr})); 23 | 24 | const closed = new Promise(resolve => process.on('close', resolve)); 25 | 26 | processes.push({process, closed}); 27 | 28 | return { 29 | completed, 30 | process 31 | }; 32 | } 33 | 34 | afterEach.async(async function() { 35 | (await Promise.all(processes)).filter(p => p.process).map(p => (p.process.kill(), p.closed)); 36 | }); 37 | 38 | describeWithoutTravisCI('when running a headless browser', () => { 39 | it.async('can run tests via PhantomJS', async function() { 40 | const {error, stdout, stderr} = await gulp('phantomjs').completed; 41 | expect(error).toBeTruthy(); 42 | expect(stderr).toContain('Error: 1 failure'); 43 | expect(stdout).toContain('.F'); 44 | expect(stdout).toContain('A message from the page'); 45 | }); 46 | 47 | it.async('can run tests via SlimerJS', async function() { 48 | const {error, stdout, stderr} = await gulp('slimerjs').completed; 49 | expect(error).toBeTruthy(); 50 | expect(stderr).toContain('Error: 1 failure'); 51 | expect(stdout).toContain('.F'); 52 | expect(stdout).toContain('A message from the page'); 53 | }); 54 | 55 | it.async('can run tests via headless chrome', async () => { 56 | const {error, stdout, stderr} = await gulp('chrome').completed; 57 | expect(error).toBeTruthy(); 58 | expect(stderr).toContain('Error: 1 failure'); 59 | expect(stdout).toContain('.F'); 60 | expect(stdout).toContain('A message from the page'); 61 | }); 62 | }); 63 | 64 | describeWithoutTravisCI('when running in a browser', function() { 65 | describeWithWebdriver('when running with webdriver', () => { 66 | let page; 67 | it.async('allows running tests in a browser', async function() { 68 | gulp('server'); 69 | page = (await visit('http://localhost:8888')).page; 70 | await page.waitForExist('.jasmine-bar.jasmine-failed'); 71 | const text = await page.getText('.jasmine-bar.jasmine-failed'); 72 | expect(text).toMatch('2 specs, 1 failure'); 73 | }); 74 | 75 | it.async('allows re-running tests in a browser', async function() { 76 | gulp('server'); 77 | page = (await visit('http://localhost:8888')).page; 78 | await page.url('http://localhost:8888'); 79 | await page.refresh(); 80 | await page.waitForExist('.jasmine-bar.jasmine-failed'); 81 | const text = await page.getText('.jasmine-bar.jasmine-failed'); 82 | expect(text).toMatch('2 specs, 1 failure'); 83 | }); 84 | 85 | describe('when the file is mutated', function() { 86 | const oldSpec = 'it(\'makes a basic failing assertion\', function() { expect(true).toBe(false); });'; 87 | const newSpec = 'it(\'makes a basic passing assertion\', function() { expect(true).toBe(true); });'; 88 | let pathToMutableSpec; 89 | 90 | beforeEach(function() { 91 | pathToMutableSpec = path.resolve(__dirname, 'fixtures', 'mutable_spec.js'); 92 | }); 93 | 94 | afterEach(function(done) { 95 | fs.writeFile(pathToMutableSpec, oldSpec, done); 96 | }); 97 | 98 | it.async('supports webpack with watch: true', async function() { 99 | const {process: gulpProcess} = gulp('webpack-server'); 100 | page = (await visit('http://localhost:8888')).page; 101 | await page.waitForExist('.jasmine-bar.jasmine-failed'); 102 | let text = await page.getText('.jasmine-bar.jasmine-failed'); 103 | expect(text).toMatch('1 spec, 1 failure'); 104 | function waitForWebpack() { 105 | return new Promise(resolve => { 106 | gulpProcess.stdout.on('data', chunk => chunk.match(/webpack is watching for changes/i) && resolve()); 107 | }); 108 | } 109 | fs.writeFileSync(pathToMutableSpec, newSpec); 110 | 111 | await waitForWebpack(); 112 | text = await page.refresh() 113 | .waitForExist('.jasmine-bar.jasmine-passed') 114 | .getText('.jasmine-bar.jasmine-passed'); 115 | expect(text).toMatch('1 spec, 0 failures'); 116 | }); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /spec/lib/server_spec.js: -------------------------------------------------------------------------------- 1 | import {Deferred, withUnhandledRejection} from '../spec_helper'; 2 | import request from 'supertest'; 3 | import {createServer} from '../../dist/lib/server'; 4 | 5 | describe('Server', function() { 6 | let app, files; 7 | beforeEach(function() { 8 | files = { 9 | 'specRunner.html': 'The Spec Runner' 10 | }; 11 | }); 12 | 13 | describe('when the server is not passed options', function() { 14 | beforeEach(function() { 15 | app = createServer(files); 16 | }); 17 | 18 | describe('GET /', function() { 19 | it.async('renders the spec runner', async function() { 20 | const res = await request(app).get('/').expect(200); 21 | expect(res.text).toContain('The Spec Runner'); 22 | }); 23 | }); 24 | 25 | describe('GET *', function() { 26 | describe('with a file that exists', function() { 27 | beforeEach(function() { 28 | files['foo.js'] = 'Foo Content'; 29 | }); 30 | 31 | it.async('renders the file', async function() { 32 | const res = await request(app).get('/foo.js').expect(200); 33 | expect(res.text).toContain('Foo Content'); 34 | }); 35 | }); 36 | 37 | describe('with a file that does not exist', function() { 38 | it.async('returns 404', async function() { 39 | const res = await request(app).get('/bar.js'); 40 | expect(res.statusCode).toBe(404); 41 | }); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('when the server is passed whenReady', function() { 47 | let whenReady; 48 | beforeEach(function() { 49 | whenReady = new Deferred(); 50 | app = createServer(files, {whenReady: () => whenReady}); 51 | }); 52 | 53 | describe('GET /', function() { 54 | describe('whenReady is resolved', function() { 55 | it.async('renders the valid version of spec runner', async function() { 56 | setTimeout(function() { 57 | files['specRunner.html'] = 'The New Version'; 58 | whenReady.resolve(); 59 | }, 100); 60 | 61 | const res = await request(app).get('/').expect(200); 62 | expect(res.text).toContain('The New Version'); 63 | }); 64 | 65 | describe('when there is an error', () => { 66 | withUnhandledRejection(); 67 | 68 | it.async('does not render intermediate invalid states', async function() { 69 | setTimeout(function() { 70 | files['specRunner.html'] = 'The Bad Version'; 71 | whenReady.reject(new Error('some error')); 72 | whenReady = new Deferred(); 73 | }, 100); 74 | 75 | setTimeout(function() { 76 | files['specRunner.html'] = 'The Good Version'; 77 | whenReady.resolve(); 78 | }, 200); 79 | 80 | const res = await request(app).get('/').expect(200); 81 | expect(res.text).toContain('The Good Version'); 82 | }); 83 | }); 84 | 85 | }); 86 | }); 87 | }); 88 | }); -------------------------------------------------------------------------------- /spec/lib/spec_runner_spec.js: -------------------------------------------------------------------------------- 1 | import '../spec_helper'; 2 | import jasmineCore from 'jasmine-core'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import $ from 'cheerio'; 6 | import SpecRunner from '../../dist/lib/spec_runner'; 7 | 8 | describe('SpecRunner', () => { 9 | let cssFiles, jsFiles, subject; 10 | 11 | function loadJasmineFiles(...types) { 12 | return types.map(type => { 13 | return jasmineCore.files[type].map(fileName => fs.readFileSync(path.resolve(jasmineCore.files.path, fileName), 'utf8')); 14 | }); 15 | } 16 | 17 | beforeEach(() => { 18 | const result = loadJasmineFiles('jsFiles', 'cssFiles'); 19 | jsFiles = result[0]; 20 | cssFiles = result[1]; 21 | }); 22 | 23 | describe('when the console true option is true', () => { 24 | let jsonStreamReporter, bootJs; 25 | 26 | beforeEach(() => { 27 | jsonStreamReporter = fs.readFileSync(require.resolve('jasmine-json-stream-reporter/browser.js'), 'utf8'); 28 | bootJs = fs.readFileSync(path.resolve(__dirname, '..', '..', 'dist', 'lib', 'boot.js'), 'utf8'); 29 | subject = new SpecRunner({console: true}); 30 | }); 31 | 32 | it('includes all of the Jasmine library files', () => { 33 | const html = subject.contents.toString(); 34 | const $tags = $.load(html)('script,style,link'); 35 | 36 | expect($tags.length).toBe(6); 37 | 38 | expect($tags.eq(0).is('style')).toBe(true); 39 | expect($tags.eq(0).html()).toBe(cssFiles[0]); 40 | 41 | expect($tags.eq(1).is('script')).toBe(true); 42 | expect($tags.eq(1).html()).toBe(jsFiles[0]); 43 | 44 | expect($tags.eq(2).is('script')).toBe(true); 45 | expect($tags.eq(2).html()).toBe(jsFiles[1]); 46 | 47 | expect($tags.eq(3).is('script')).toBe(true); 48 | expect($tags.eq(3).html()).toBe(jsFiles[2]); 49 | 50 | expect($tags.eq(4).is('script')).toBe(true); 51 | expect($tags.eq(4).html()).toBe(jsonStreamReporter); 52 | 53 | expect($tags.eq(5).is('script')).toBe(true); 54 | expect($tags.eq(5).html()).toBe(bootJs); 55 | }); 56 | 57 | it('allows adding additional css and js files', () => { 58 | subject.addFile('foo.js'); 59 | subject.addFile('bar.css'); 60 | subject.addFile('foo.js'); 61 | subject.addFile('bar.css'); 62 | subject.addFile('bar.css'); 63 | 64 | const html = subject.contents; 65 | const $tags = $.load(html)('script,style,link'); 66 | 67 | expect($tags.length).toBe(8); 68 | 69 | expect($tags.eq(6).attr('src')).toBe('/foo.js'); 70 | expect($tags.eq(6).is('script')).toBe(true); 71 | 72 | expect($tags.eq(7).attr('href')).toBe('/bar.css'); 73 | expect($tags.eq(7).attr('type')).toBe('text/css'); 74 | expect($tags.eq(7).attr('rel')).toBe('stylesheet'); 75 | expect($tags.eq(7).is('link')).toBe(true); 76 | }); 77 | 78 | describe('when profile is true', () => { 79 | let profileReporterJs; 80 | 81 | beforeEach(() => { 82 | profileReporterJs = fs.readFileSync(require.resolve('jasmine-profile-reporter/browser.js'), 'utf8'); 83 | subject = new SpecRunner({profile: true}); 84 | }); 85 | 86 | it('includes all of the Jasmine library files', () => { 87 | const html = subject.contents.toString(); 88 | const $tags = $.load(html)('script,style,link'); 89 | 90 | expect($tags.length).toBe(7); 91 | 92 | expect($tags.eq(4).is('script')).toBe(true); 93 | expect($tags.eq(4).html()).toBe(profileReporterJs); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('when the console true option is not true', () => { 99 | let bootFiles; 100 | 101 | beforeEach(() => { 102 | bootFiles = loadJasmineFiles('bootFiles')[0]; 103 | }); 104 | 105 | describe('when the sourcemapped stacktrace is not true', () => { 106 | beforeEach(() => { 107 | subject = new SpecRunner(); 108 | }); 109 | 110 | it('includes all of the Jasmine library files', () => { 111 | const html = subject.contents.toString(); 112 | const $tags = $.load(html)('script,style,link'); 113 | 114 | expect($tags.length).toBe(5); 115 | 116 | expect($tags.eq(0).is('style')).toBe(true); 117 | expect($tags.eq(0).html()).toBe(cssFiles[0]); 118 | 119 | expect($tags.eq(1).is('script')).toBe(true); 120 | expect($tags.eq(1).html()).toBe(jsFiles[0]); 121 | 122 | expect($tags.eq(2).is('script')).toBe(true); 123 | expect($tags.eq(2).html()).toBe(jsFiles[1]); 124 | 125 | expect($tags.eq(3).is('script')).toBe(true); 126 | expect($tags.eq(3).html()).toBe(jsFiles[2]); 127 | 128 | expect($tags.eq(4).is('script')).toBe(true); 129 | expect($tags.eq(4).html()).toBe(bootFiles[0]); 130 | }); 131 | }); 132 | 133 | describe('when the sourcemapped stacktrace is true', () => { 134 | let sourcemappedStacktraceJs, sourceMappedStacktraceReporterCss, sourceMappedStacktraceReporterJs; 135 | 136 | beforeEach(() => { 137 | sourcemappedStacktraceJs = fs.readFileSync(require.resolve('sourcemapped-stacktrace/dist/sourcemapped-stacktrace.js'), 'utf8'); 138 | sourceMappedStacktraceReporterCss = fs.readFileSync(path.resolve(__dirname, '..', '..', 'dist', 'lib', 'stylesheets', 'sourcemapped_stacktrace_reporter.css'), 'utf8'); 139 | sourceMappedStacktraceReporterJs = fs.readFileSync(path.resolve(__dirname, '..', '..', 'dist', 'lib', 'reporters', 'add_sourcemapped_stacktrace_reporter.js'), 'utf8'); 140 | subject = new SpecRunner({sourcemappedStacktrace: true}); 141 | }); 142 | 143 | it('includes all of the Jasmine library files', () => { 144 | const html = subject.contents.toString(); 145 | const $tags = $.load(html)('script,style,link'); 146 | 147 | expect($tags.length).toBe(8); 148 | 149 | expect($tags.eq(0).is('style')).toBe(true); 150 | expect($tags.eq(0).html()).toBe(cssFiles[0]); 151 | 152 | expect($tags.eq(1).is('style')).toBe(true); 153 | expect($tags.eq(1).html()).toBe(sourceMappedStacktraceReporterCss); 154 | 155 | expect($tags.eq(2).is('script')).toBe(true); 156 | expect($tags.eq(2).html()).toBe(jsFiles[0]); 157 | 158 | expect($tags.eq(3).is('script')).toBe(true); 159 | expect($tags.eq(3).html()).toBe(jsFiles[1]); 160 | 161 | expect($tags.eq(4).is('script')).toBe(true); 162 | expect($tags.eq(4).html()).toBe(jsFiles[2]); 163 | 164 | expect($tags.eq(5).is('script')).toBe(true); 165 | expect($tags.eq(5).html()).toBe(bootFiles[0]); 166 | 167 | expect($tags.eq(6).is('script')).toBe(true); 168 | expect($tags.eq(6).html()).toBe(sourcemappedStacktraceJs); 169 | 170 | expect($tags.eq(7).is('script')).toBe(true); 171 | expect($tags.eq(7).html()).toBe(sourceMappedStacktraceReporterJs); 172 | }); 173 | }); 174 | 175 | describe('when profile is true', () => { 176 | let addProfileReporterJs, profileReporterJs; 177 | 178 | beforeEach(() => { 179 | addProfileReporterJs = fs.readFileSync(path.resolve(__dirname, '..', '..', 'dist', 'lib', 'reporters', 'add_profile_reporter.js'), 'utf8'); 180 | profileReporterJs = fs.readFileSync(require.resolve('jasmine-profile-reporter/browser.js'), 'utf8'); 181 | subject = new SpecRunner({profile: true}); 182 | }); 183 | 184 | it('includes all of the Jasmine library files', () => { 185 | const html = subject.contents.toString(); 186 | const $tags = $.load(html)('script,style,link'); 187 | 188 | expect($tags.length).toBe(7); 189 | 190 | expect($tags.eq(0).is('style')).toBe(true); 191 | expect($tags.eq(0).html()).toBe(cssFiles[0]); 192 | 193 | expect($tags.eq(1).is('script')).toBe(true); 194 | expect($tags.eq(1).html()).toBe(jsFiles[0]); 195 | 196 | expect($tags.eq(2).is('script')).toBe(true); 197 | expect($tags.eq(2).html()).toBe(jsFiles[1]); 198 | 199 | expect($tags.eq(3).is('script')).toBe(true); 200 | expect($tags.eq(3).html()).toBe(jsFiles[2]); 201 | 202 | expect($tags.eq(4).is('script')).toBe(true); 203 | expect($tags.eq(4).html()).toBe(profileReporterJs); 204 | 205 | expect($tags.eq(5).is('script')).toBe(true); 206 | expect($tags.eq(5).html()).toBe(bootFiles[0]); 207 | 208 | expect($tags.eq(6).is('script')).toBe(true); 209 | expect($tags.eq(6).html()).toBe(addProfileReporterJs); 210 | }); 211 | }); 212 | }); 213 | }); -------------------------------------------------------------------------------- /spec/spec_helper.js: -------------------------------------------------------------------------------- 1 | import Deferred from './support/deferred'; 2 | import {visit, describeWithWebdriver} from './support/webdriver_helper'; 3 | import JasmineAsync from 'jasmine-async-suite'; 4 | import {withUnhandledRejection} from './support/unhandled_rejection_helper'; 5 | 6 | const {DEFAULT_TIMEOUT_INTERVAL} = jasmine; 7 | 8 | JasmineAsync.install(); 9 | 10 | function describeWithoutTravisCI(text, callback) { 11 | if (process.env.TRAVIS !== 'true') callback(); 12 | } 13 | 14 | function timeout(duration = 0) { 15 | return new Promise(resolve => setTimeout(resolve, duration)); 16 | } 17 | 18 | beforeEach(() => { 19 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 20 | }); 21 | 22 | afterAll(() => { 23 | JasmineAsync.uninstall(); 24 | jasmine.DEFAULT_TIMEOUT_INTERVAL = DEFAULT_TIMEOUT_INTERVAL; 25 | delete require.cache[require.resolve(__filename)]; 26 | }); 27 | 28 | export {describeWithoutTravisCI, describeWithWebdriver, Deferred, timeout, visit, withUnhandledRejection}; -------------------------------------------------------------------------------- /spec/support/deferred.js: -------------------------------------------------------------------------------- 1 | export default function Deferred() { 2 | let resolver, rejector; 3 | const promise = new Promise(function(res, rej) { 4 | resolver = res; 5 | rejector = rej; 6 | }); 7 | 8 | const wrapper = Object.assign(promise, { 9 | resolve(...args) { 10 | resolver(...args); 11 | return wrapper; 12 | }, 13 | reject(...args) { 14 | rejector(...args); 15 | return wrapper; 16 | }, 17 | promise() { 18 | return promise; 19 | } 20 | }); 21 | return wrapper; 22 | } -------------------------------------------------------------------------------- /spec/support/jasmine_webdriver.js: -------------------------------------------------------------------------------- 1 | import * as selenium from './selenium'; 2 | import thenify from 'thenify'; 3 | import * as webdriverio from 'webdriverio'; 4 | import waitOnCallback from 'wait-on'; 5 | 6 | const waitOn = thenify(waitOnCallback); 7 | const privates = new WeakMap(); 8 | 9 | export default class JasmineWebdriver { 10 | constructor({browser = 'firefox', timeout = 500} = {}) { 11 | privates.set(this, {processes: [], desiredCapabilities: {browserName: browser}, timeout}); 12 | } 13 | 14 | driver() { 15 | const {desiredCapabilities, processes, timeout} = privates.get(this); 16 | return new Promise(async (resolve, reject) => { 17 | const port = 4444; 18 | await selenium.install(); 19 | const process = await selenium.start({spawnOptions: {stdio: ['ignore', 'ignore', 'ignore']}}); 20 | processes.push({process, closed: new Promise(res => process.once('close', res))}); 21 | await waitOn({resources: [`tcp:${port}`], timeout: 30000}).catch(() => reject(`error in waiting for selenium server on port ${port}`)); 22 | const driver = webdriverio.remote({desiredCapabilities, waitforTimeout: timeout}).init(); 23 | await driver; 24 | processes.push({driver}); 25 | resolve({driver}); 26 | }); 27 | } 28 | 29 | async end() { 30 | const {processes} = privates.get(this); 31 | const webdriverProcesses = processes.filter(p => p.driver).map(p => p.driver.end()); 32 | await Promise.all(webdriverProcesses); 33 | const otherProcesses = processes.filter(p => p.process).map(p => (p.process.kill(), p.closed)); 34 | await Promise.all(otherProcesses); 35 | return Promise.all([webdriverProcesses, ...otherProcesses]); 36 | } 37 | } -------------------------------------------------------------------------------- /spec/support/selenium.js: -------------------------------------------------------------------------------- 1 | import seleniumStandalone from 'selenium-standalone'; 2 | import thenify from 'thenify'; 3 | 4 | export const install = thenify(seleniumStandalone.install); 5 | export const start = thenify(seleniumStandalone.start); 6 | -------------------------------------------------------------------------------- /spec/support/unhandled_rejection_helper.js: -------------------------------------------------------------------------------- 1 | export function withUnhandledRejection() { 2 | let unhandledRejection; 3 | beforeEach(() => { 4 | unhandledRejection = jasmine.createSpy('unhandledRejection'); 5 | if (!process.listeners('unhandledRejection').length) process.on('unhandledRejection', unhandledRejection); 6 | }); 7 | 8 | afterEach(() => { 9 | process.removeListener(unhandledRejection, unhandledRejection); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /spec/support/webdriver_helper.js: -------------------------------------------------------------------------------- 1 | import JasmineWebdriver from './jasmine_webdriver'; 2 | 3 | let webdriver; 4 | 5 | export function visit(url) { 6 | return webdriver.driver().then(({driver}) => { 7 | return driver.url(url).then(() => ({page: driver})); 8 | }); 9 | } 10 | 11 | export function describeWithWebdriver(name, callback, options = {}) { 12 | describe(name, function() { 13 | beforeEach(() => { 14 | webdriver = webdriver || new JasmineWebdriver({timeout: 5000, ...options}); 15 | }); 16 | 17 | afterEach.async(async function() { 18 | await webdriver.end(); 19 | }); 20 | 21 | callback(); 22 | }); 23 | } -------------------------------------------------------------------------------- /spec/webpack/jasmine-plugin_spec.js: -------------------------------------------------------------------------------- 1 | import '../spec_helper'; 2 | import JasminePlugin from '../../dist/webpack/jasmine-plugin'; 3 | import {EventEmitter} from 'events'; 4 | 5 | describe('JasminePlugin', function() { 6 | let subject, compiler; 7 | beforeEach(function() { 8 | subject = new JasminePlugin(); 9 | compiler = new EventEmitter(); 10 | compiler.plugin = compiler.on; 11 | subject.apply(compiler); 12 | }); 13 | 14 | describe('#whenReady', function() { 15 | let doneSpy, failSpy; 16 | beforeEach(function() { 17 | doneSpy = jasmine.createSpy('done'); 18 | failSpy = jasmine.createSpy('fail'); 19 | }); 20 | 21 | it('does not resolve or reject the promise', function(done) { 22 | subject.whenReady().then(doneSpy, failSpy); 23 | expect(doneSpy).not.toHaveBeenCalled(); 24 | expect(failSpy).not.toHaveBeenCalled(); 25 | done(); 26 | }); 27 | 28 | describe('when the done event is emitted', function() { 29 | beforeEach(function() { 30 | compiler.emit('done'); 31 | }); 32 | 33 | it.async('resolves the promise', async function() { 34 | await subject.whenReady().then(doneSpy, failSpy); 35 | expect(doneSpy).toHaveBeenCalled(); 36 | }); 37 | 38 | describe('and then the invalid event is emitted', function() { 39 | it('resets the promise', function(done) { 40 | compiler.emit('invalid'); 41 | subject.whenReady().then(doneSpy, failSpy); 42 | setTimeout(function() { 43 | expect(doneSpy).not.toHaveBeenCalled(); 44 | expect(failSpy).not.toHaveBeenCalled(); 45 | done(); 46 | }, 1); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('when the invalid event is emitted', function() { 52 | it('rejects the promise', async function(done) { 53 | try { 54 | const whenReady = subject.whenReady(); 55 | whenReady.then(doneSpy, failSpy); 56 | compiler.emit('invalid'); 57 | await whenReady; 58 | } finally { 59 | expect(failSpy).toHaveBeenCalled(); 60 | done(); 61 | } 62 | }); 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {obj as through} from 'through2'; 2 | import {headless, server, slimerjs, phantomjs, chrome} from './lib/headless'; 3 | import SpecRunner from './lib/spec_runner'; 4 | 5 | function specRunner({profile, sourcemappedStacktrace} = {}) { 6 | const specRunner = new SpecRunner({profile, sourcemappedStacktrace, path: '/specRunner.html'}); 7 | const consoleRunner = new SpecRunner({console: true, path: '/consoleRunner.html'}); 8 | return through(function(file, encoding, next) { 9 | this.push(file); 10 | this.push(specRunner.addFile(file.relative)); 11 | this.push(consoleRunner.addFile(file.relative)); 12 | next(); 13 | }); 14 | } 15 | 16 | export {headless, server, slimerjs, phantomjs, chrome, specRunner}; -------------------------------------------------------------------------------- /src/lib/boot.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function injectQueryParams(env, QueryString, SpecFilter) { 3 | //TODO: test query params 4 | 5 | var queryString = new QueryString({ 6 | getWindowLocation: function() { return window.location; } 7 | }); 8 | 9 | var stoppingOnSpecFailure = queryString.getParam('failFast'); 10 | env.stopOnSpecFailure(typeof stoppingOnSpecFailure === 'undefined' ? true : stoppingOnSpecFailure); 11 | 12 | var throwingExpectationFailures = queryString.getParam('throwFailures'); 13 | env.throwOnExpectationFailure(throwingExpectationFailures); 14 | 15 | var random = queryString.getParam('random'); 16 | env.randomizeTests(random); 17 | 18 | var seed = queryString.getParam('seed'); 19 | if (seed) { 20 | env.seed(seed); 21 | } 22 | 23 | var specFilter = new SpecFilter({ 24 | filterString: function() { return queryString.getParam('spec'); } 25 | }); 26 | 27 | env.specFilter = function(spec) { 28 | return specFilter.matches(spec.getFullName()); 29 | }; 30 | } 31 | 32 | function extend(destination, source) { 33 | for (var property in source) destination[property] = source[property]; 34 | return destination; 35 | } 36 | 37 | window.jasmine = window.jasmine || jasmineRequire.core(jasmineRequire); 38 | 39 | var env = jasmine.getEnv(); 40 | injectQueryParams(env, jasmineRequire.QueryString(), jasmineRequire.HtmlSpecFilter()); 41 | 42 | var jasmineInterface = jasmineRequire.interface(jasmine, env); 43 | 44 | extend(window, jasmineInterface); 45 | 46 | if (window.JasmineJsonStreamReporter) { 47 | var jsonStreamReporter = new JasmineJsonStreamReporter({ 48 | print: function(message) { 49 | callPhantom({message: message}); 50 | }, 51 | onComplete: function() { 52 | if (window.__coverage__) jsonStreamReporter.coverage(__coverage__); 53 | callPhantom({exit: true}); 54 | } 55 | }); 56 | jasmine.snapshot = function(snapshot) { 57 | jsonStreamReporter.snapshot(snapshot); 58 | }; 59 | env.addReporter(jsonStreamReporter); 60 | } 61 | 62 | var currentWindowOnload = window.onload; 63 | window.onload = function() { 64 | if (currentWindowOnload) { 65 | currentWindowOnload(); 66 | } 67 | if (window.callPhantom) env.execute(); 68 | }; 69 | })(); 70 | -------------------------------------------------------------------------------- /src/lib/drivers/chrome.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | export default function chrome() { 3 | return { 4 | get command() { 5 | return path.resolve(__dirname, '../runners/chrome_runner'); 6 | }, 7 | output: 'stderr' 8 | }; 9 | } -------------------------------------------------------------------------------- /src/lib/drivers/phantomjs.js: -------------------------------------------------------------------------------- 1 | export default function phantomJs() { 2 | return { 3 | get command() { 4 | try { 5 | return require('phantomjs-prebuilt').path; 6 | } catch(e) { 7 | try { 8 | return require('phantomjs').path; 9 | } catch(e) { 10 | return 'phantomjs'; 11 | } 12 | } 13 | }, 14 | runner: 'phantom_runner.js', 15 | output: 'stderr' 16 | }; 17 | } -------------------------------------------------------------------------------- /src/lib/drivers/phantomjs1.js: -------------------------------------------------------------------------------- 1 | export default function phantomJs1() { 2 | return { 3 | get command() { 4 | try { 5 | return require('phantomjs').path; 6 | } catch(e) { 7 | return 'phantomjs'; 8 | } 9 | }, 10 | runner: 'phantom_runner.js', 11 | output: 'stderr' 12 | }; 13 | } -------------------------------------------------------------------------------- /src/lib/drivers/slimerjs.js: -------------------------------------------------------------------------------- 1 | export default function slimerJs() { 2 | return { 3 | get command() { 4 | try { 5 | return require('slimerjs').path; 6 | } catch(e) { 7 | return 'slimerjs'; 8 | } 9 | }, 10 | runner: 'slimer_runner.js', 11 | output: 'stdout' 12 | }; 13 | } -------------------------------------------------------------------------------- /src/lib/headless.js: -------------------------------------------------------------------------------- 1 | import {listen} from './server'; 2 | import {resolve} from 'path'; 3 | import {stringify} from 'qs'; 4 | import {spawn} from 'child_process'; 5 | import {obj as through} from 'through2'; 6 | import {obj as reduce} from 'through2-reduce'; 7 | import ProfileReporter from 'jasmine-profile-reporter'; 8 | import TerminalReporter from 'jasmine-terminal-reporter'; 9 | import toReporter from 'jasmine-json-stream-reporter/to-reporter'; 10 | import split from 'split2'; 11 | import flatMap from 'flat-map'; 12 | import once from 'lodash.once'; 13 | import ChromeDriver from './drivers/chrome'; 14 | import PhantomJsDriver from './drivers/phantomjs'; 15 | import PhantomJs1Driver from './drivers/phantomjs1'; 16 | import SlimerJsDriver from './drivers/slimerjs'; 17 | import portastic from 'portastic'; 18 | import {compact, parse} from './helper'; 19 | import pipe from 'multipipe'; 20 | 21 | const DEFAULT_JASMINE_PORT = 8888; 22 | 23 | const drivers = { 24 | chrome: ChromeDriver, 25 | phantomjs: PhantomJsDriver, 26 | phantomjs1: PhantomJs1Driver, 27 | slimerjs: SlimerJsDriver, 28 | _default: PhantomJsDriver 29 | }; 30 | 31 | function onError(message) { 32 | try { 33 | const {PluginError} = require('plugin-error'); 34 | return new PluginError('gulp-jasmine-browser', {message, showProperties: false}); 35 | } catch(e) { 36 | return new Error(message); 37 | } 38 | } 39 | 40 | function findPort() { 41 | return portastic.find({min: 8000, max: DEFAULT_JASMINE_PORT, retrieve: 1}).then(([port]) => port); 42 | } 43 | 44 | function startServer(files, options) { 45 | const {port} = options; 46 | if (!port) return findPort().then(port => listen(port, files, options)); 47 | return listen(port, files, options); 48 | } 49 | 50 | function defaultReporters(options, profile) { 51 | return compact([new TerminalReporter(options), profile && new ProfileReporter(options)]); 52 | } 53 | 54 | function findOrStartServer(options) { 55 | function helper(port, streamPort, files) { 56 | let serverPromise, streamPortPromise; 57 | if (!port) serverPromise = startServer(files, options); 58 | else serverPromise = portastic.test(port).then(isOpen => { 59 | if (!isOpen) return {server: {close: () => {}}, port}; 60 | return startServer(files, options); 61 | }); 62 | if (!streamPort) streamPortPromise = findPort(); 63 | else streamPortPromise = Promise.resolve(streamPort); 64 | return Promise.all([serverPromise, streamPortPromise]); 65 | } 66 | 67 | return through((files, enc, next) => helper(options.port, options.streamPort, files) 68 | .then(i => next(null, i)).catch(next)); 69 | } 70 | 71 | function createServer(options) { 72 | const {driver = 'chrome', file, random, throwFailures, spec, seed, reporter, profile, onCoverage, onSnapshot, 73 | onConsoleMessage = (...args) => console.log(...args), withSandbox, ...opts} = options; 74 | const query = stringify({catch: options.catch, file, random, throwFailures, spec, seed}); 75 | const {command, runner, output} = drivers[driver in drivers ? driver : '_default'](); 76 | 77 | return pipe( 78 | reduce((memo, file) => (memo[file.relative] = file.contents, memo), {}), 79 | findOrStartServer(options), 80 | flatMap(([{server, port}, streamPort], next) => { 81 | const stdio = ['pipe', output === 'stdout' ? 'pipe' : 1, output === 'stderr' ? 'pipe' : 2]; 82 | const env = {...process.env}; 83 | env.STREAM_PORT = streamPort; 84 | withSandbox && (env.WITH_SANDBOX = true); 85 | const phantomProcess = spawn(command, compact([runner, port, query]), {cwd: resolve(__dirname, './runners'), env, stdio}); 86 | phantomProcess.on('close', () => server.close()); 87 | ['SIGINT', 'SIGTERM'].forEach(e => process.once(e, () => { 88 | phantomProcess && phantomProcess.kill(); 89 | process.exit(); 90 | })); 91 | next(null, phantomProcess[output].pipe(split(parse))); 92 | }), 93 | toReporter(reporter || defaultReporters(opts, profile), {onError, onConsoleMessage, onCoverage, onSnapshot}), 94 | ); 95 | } 96 | 97 | function createServerWatch(options) { 98 | const files = {}; 99 | const createServerOnce = once(() => startServer(files, options)); 100 | return through((file, enc, next) => { 101 | files[file.relative] = file.contents; 102 | createServerOnce(); 103 | next(null, file); 104 | }); 105 | } 106 | 107 | function headless(options = {}) { 108 | return createServer(options); 109 | } 110 | 111 | function server(options = {}) { 112 | return createServerWatch({port: DEFAULT_JASMINE_PORT, ...options}); 113 | } 114 | 115 | const [slimerjs, phantomjs, chrome] = ['slimerjs', 'phantomjs', 'chrome'].map(driver => { 116 | return function(options = {}) { 117 | return headless({...options, driver}); 118 | }; 119 | }); 120 | 121 | export {headless, server, slimerjs, phantomjs, chrome}; 122 | -------------------------------------------------------------------------------- /src/lib/helper.js: -------------------------------------------------------------------------------- 1 | function compact(array) { 2 | return array.filter(Boolean); 3 | } 4 | 5 | function parse(obj) { 6 | try { 7 | return JSON.parse(obj); 8 | } catch(e) { 9 | } 10 | } 11 | 12 | export {compact, parse}; -------------------------------------------------------------------------------- /src/lib/reporters/add_profile_reporter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (window.JasmineProfileReporter) { 3 | var profileReporter = new JasmineProfileReporter({ 4 | print: function(message) { console.log(message); } 5 | }); 6 | jasmine.getEnv().addReporter(profileReporter); 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /src/lib/reporters/add_sourcemapped_stacktrace_reporter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | jasmine.getEnv().addReporter({ 3 | jasmineDone: function() { 4 | Array.prototype.slice.call(document.querySelectorAll('.jasmine-stack-trace'), 0).forEach(function(node) { 5 | sourceMappedStackTrace.mapStackTrace(node.textContent, function(stack) { 6 | stack = stack.filter(function(line) { return line.match(/\.js:\d+:\d+\)$/); }); 7 | node.textContent = node.previousSibling.textContent + '\n' + stack.join('\n'); 8 | node.style.display = 'block'; 9 | }); 10 | }); 11 | } 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/lib/runners/chrome_evaluate.js: -------------------------------------------------------------------------------- 1 | module.exports = streamPort => { 2 | let resolve; 3 | const promise = new Promise(res => resolve = res); 4 | const socket = new WebSocket(`ws://localhost:${streamPort}`); 5 | window.callPhantom = result => { 6 | if (result.message) socket.send(result.message); 7 | if (result.exit) (socket.close(), resolve()) 8 | }; 9 | socket.onopen = () => jasmine.getEnv().execute(); 10 | return promise; 11 | }; -------------------------------------------------------------------------------- /src/lib/runners/chrome_runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | require('babel-polyfill'); 4 | 5 | (async function run(...args) { 6 | const {Server} = require('ws'); 7 | const puppeteer = require('puppeteer'); 8 | const run = require('./chrome_evaluate'); 9 | 10 | const [,,port = 8888, query] = args; 11 | const {STREAM_PORT, WITH_SANDBOX = 'false'} = process.env; 12 | let url = `http://localhost:${port}/consoleRunner`; 13 | if (query) url += `/?${query}`; 14 | 15 | let server; 16 | 17 | try { 18 | server = new Server({port: STREAM_PORT}); 19 | server.on('connection', socket => socket.on('message', console.error)); 20 | 21 | let browser; 22 | try { 23 | const args = []; 24 | if (!JSON.parse(WITH_SANDBOX)) args.push('--no-sandbox'); 25 | browser = await puppeteer.launch({args}); 26 | const page = await browser.newPage(); 27 | await page.on('pageerror', ({message}) => { 28 | console.error(JSON.stringify({id: ':consoleMessage', message})); 29 | }); 30 | await page.on('error', ({message}) => { 31 | console.error(JSON.stringify({id: ':consoleMessage', message})); 32 | }); 33 | await page.on('console', msg => { 34 | const type = typeof msg.type ==='string' ? msg.type : msg.type(); 35 | const text = typeof msg.text ==='string' ? msg.text : msg.text(); 36 | console.error(JSON.stringify({id: ':consoleMessage', message: `${type}: ${text}`})); 37 | }); 38 | 39 | await page.goto(url); 40 | await page.evaluate(run, STREAM_PORT); 41 | } finally { 42 | browser && browser.close(); 43 | } 44 | } finally { 45 | server.close(); 46 | } 47 | })(...process.argv); 48 | -------------------------------------------------------------------------------- /src/lib/runners/phantom_runner.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | var webPage = require('webpage'); 3 | var args = system.args; 4 | 5 | var port = args[1] || 8888; 6 | var query = args[2]; 7 | 8 | var page = webPage.create(); 9 | page.onCallback = function(result) { 10 | if (result.message) system.stderr.writeLine(result.message); 11 | if (result.exit) phantom.exit(); 12 | }; 13 | page.onConsoleMessage = function() { 14 | page.onCallback({message: JSON.stringify({id: ':consoleMessage', message: Array.prototype.slice.call(arguments, 0).join('')})}); 15 | }; 16 | 17 | var url = 'http://localhost:' + port + '/consoleRunner'; 18 | if (query) url += '/?' + query; 19 | page.open(url); -------------------------------------------------------------------------------- /src/lib/runners/slimer_runner.js: -------------------------------------------------------------------------------- 1 | var system = require('system'); 2 | var webPage = require('webpage'); 3 | var args = system.args; 4 | 5 | var port = args[1] || 8888; 6 | var query = args[2]; 7 | 8 | var page = webPage.create(); 9 | page.onCallback = function(result) { 10 | if (result.message) system.stdout.writeLine(result.message); 11 | if (result.exit) slimer.exit(0); 12 | }; 13 | page.onConsoleMessage = function() { 14 | page.onCallback({message: JSON.stringify({id: ':consoleMessage', message: Array.prototype.slice.call(arguments, 0).join('')})}); 15 | }; 16 | 17 | var url = 'http://localhost:' + port + '/consoleRunner'; 18 | if (query) url += '/?' + query; 19 | page.open(url); -------------------------------------------------------------------------------- /src/lib/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import mime from 'mime'; 3 | import path from 'path'; 4 | import favicon from 'serve-favicon'; 5 | import {PassThrough} from 'stream'; 6 | 7 | function log(message) { 8 | try { 9 | const log = require('fancy-log'); 10 | log(message); 11 | } catch(e) { 12 | console.log(message); 13 | } 14 | } 15 | 16 | function renderFile(res, files, pathname, whenReady) { 17 | whenReady() 18 | .then(function() { 19 | pathname = decodeURIComponent(pathname); 20 | if (pathname && files[pathname]) { 21 | res.status(200).type(mime.lookup(pathname)); 22 | const stream = new PassThrough(); 23 | stream.end(files[pathname]); 24 | stream.pipe(res); 25 | return; 26 | } 27 | res.status(404).send('File not Found'); 28 | }, function() { 29 | renderFile(res, files, pathname, whenReady); 30 | }); 31 | } 32 | 33 | function createServer(files, options = {}) { 34 | const app = express(); 35 | 36 | app.use(favicon(path.resolve(__dirname, '..', 'public', 'jasmine_favicon.png'))); 37 | 38 | app.get('/', function(req, res) { 39 | const {whenReady = () => Promise.resolve()} = options; 40 | renderFile(res, files, 'specRunner.html', whenReady); 41 | }); 42 | 43 | app.get('/consoleRunner', function(req, res) { 44 | const {whenReady = () => Promise.resolve()} = options; 45 | renderFile(res, files, 'consoleRunner.html', whenReady); 46 | }); 47 | 48 | app.get('*', function(req, res) { 49 | const {whenReady = () => Promise.resolve()} = options; 50 | const filePath = req.path.replace(/^\//, ''); 51 | const pathname = path.normalize(filePath); 52 | renderFile(res, files, pathname, whenReady); 53 | }); 54 | 55 | return app; 56 | } 57 | 58 | function listen(port, files, options = {}) { 59 | return new Promise(resolve => { 60 | const server = createServer(files, options).listen(port, function() { 61 | log(`Jasmine server listening on port ${port}`); 62 | resolve({server, port}); 63 | }); 64 | }); 65 | } 66 | 67 | export {createServer, listen}; -------------------------------------------------------------------------------- /src/lib/spec_runner.js: -------------------------------------------------------------------------------- 1 | import File from 'vinyl'; 2 | import {files as jasmineCoreFiles} from 'jasmine-core'; 3 | import {readFileSync} from 'fs'; 4 | import {resolve, extname} from 'path'; 5 | 6 | function resolveJasmineFiles(directoryProp, fileNamesProp) { 7 | const directory = jasmineCoreFiles[directoryProp]; 8 | const fileNames = jasmineCoreFiles[fileNamesProp]; 9 | return fileNames.map(fileName => resolve(directory, fileName)); 10 | } 11 | 12 | const inlineTagExtensions = {'.css': 'style', '.js': 'script'}; 13 | 14 | const htmlForExtension = { 15 | '.js': filePath => ``, 16 | '.css': filePath => ``, 17 | '_default': () => '' 18 | }; 19 | 20 | const privates = new WeakMap(); 21 | 22 | export default class SpecRunner extends File { 23 | constructor(options = {}) { 24 | const {path, profile, console, sourcemappedStacktrace} = options; 25 | super({path, base: '/'}); 26 | 27 | this.contents = new Buffer(''); 28 | const useSourcemappedStacktrace = !console && sourcemappedStacktrace; 29 | privates.set(this, {files: new Set()}); 30 | [ 31 | ...resolveJasmineFiles('path', 'cssFiles'), 32 | useSourcemappedStacktrace && 'stylesheets/sourcemapped_stacktrace_reporter.css', 33 | ...resolveJasmineFiles('path', 'jsFiles'), 34 | profile && require.resolve('jasmine-profile-reporter/browser.js'), 35 | ...(console ? [require.resolve('jasmine-json-stream-reporter/browser.js'), 'boot.js'] : resolveJasmineFiles('bootDir', 'bootFiles')), 36 | profile && !console && 'reporters/add_profile_reporter.js', 37 | useSourcemappedStacktrace && require.resolve('sourcemapped-stacktrace/dist/sourcemapped-stacktrace.js'), 38 | useSourcemappedStacktrace && 'reporters/add_sourcemapped_stacktrace_reporter.js' 39 | ].filter(Boolean).forEach(fileName => this.inlineFile(fileName)); 40 | } 41 | 42 | inlineFile(filePath) { 43 | const fileContents = readFileSync(resolve(__dirname, filePath), {encoding: 'utf8'}); 44 | const fileExtension = inlineTagExtensions[extname(filePath)]; 45 | this.contents = Buffer.concat([ 46 | this.contents, 47 | new Buffer(`<${fileExtension}>${fileContents}`) 48 | ]); 49 | return this; 50 | } 51 | 52 | addFile(filePath) { 53 | const {files} = privates.get(this); 54 | if (files.has(filePath)) return this; 55 | files.add(filePath); 56 | const fileExtension = extname(filePath); 57 | this.contents = Buffer.concat([ 58 | this.contents, 59 | new Buffer(htmlForExtension[fileExtension in htmlForExtension ? fileExtension : '_default'](filePath)) 60 | ]); 61 | return this; 62 | } 63 | } -------------------------------------------------------------------------------- /src/lib/stylesheets/sourcemapped_stacktrace_reporter.css: -------------------------------------------------------------------------------- 1 | .jasmine-stack-trace { 2 | display: none; 3 | } -------------------------------------------------------------------------------- /src/webpack/jasmine-plugin.js: -------------------------------------------------------------------------------- 1 | const privates = new WeakMap(); 2 | 3 | export default class JasminePlugin { 4 | constructor() { 5 | let resolve = function() {}; 6 | let reject = function() {}; 7 | const promise = new Promise(function(res, rej) { 8 | resolve = res; 9 | reject = rej; 10 | }); 11 | 12 | privates.set(this, {promise, resolve, reject}); 13 | 14 | this.whenReady = () => privates.get(this).promise; 15 | } 16 | apply(compiler) { 17 | compiler.plugin('invalid', () => { 18 | let {resolve, reject, promise} = privates.get(this); 19 | reject(); 20 | promise = new Promise(function(res, rej) { 21 | resolve = res; 22 | reject = rej; 23 | }); 24 | privates.set(this, {promise, resolve, reject}); 25 | return promise; 26 | }); 27 | compiler.plugin('done', () => privates.get(this).resolve()); 28 | } 29 | } -------------------------------------------------------------------------------- /tasks/build.js: -------------------------------------------------------------------------------- 1 | import babel from 'gulp-babel'; 2 | import del from 'del'; 3 | import gulp from 'gulp'; 4 | import mergeStream from 'merge-stream'; 5 | 6 | const TRANSPILE_SRC = ['src/lib/drivers/**/*.js', 'src/webpack/**/*.js', 'src/lib/headless.js', 'src/lib/server.js', 'src/lib/spec_runner.js', 'src/lib/helper.js', 'src/lib/runners/chrome_runner', 'src/index.js']; 7 | const RAW_SRC = ['src/lib/boot.js', 'src/lib/runners/phantom_runner.js', 'src/lib/runners/slimer_runner.js', 'src/lib/runners/chrome_evaluate.js', 'src/lib/reporters/**/*.js']; 8 | const CSS_SRC = ['src/lib/stylesheets/**/*.css']; 9 | const NON_JS_SRC = ['LICENSE.md', 'README.md', 'package.json', 'public/jasmine_favicon.png']; 10 | 11 | gulp.task('clean', done => del('dist', done)); 12 | 13 | gulp.task('babel', () => { 14 | return mergeStream( 15 | gulp.src(TRANSPILE_SRC, {base: 'src'}).pipe(babel()), 16 | gulp.src(RAW_SRC, {base: 'src'}), 17 | gulp.src(CSS_SRC, {base: 'src'}), 18 | gulp.src(NON_JS_SRC, {base: '.'}) 19 | ).pipe(gulp.dest('dist')); 20 | }); 21 | 22 | gulp.task('build', gulp.series('clean', 'babel')); 23 | 24 | gulp.task('build-watch', () => { 25 | return gulp.watch([...TRANSPILE_SRC, ...RAW_SRC], ['babel']); 26 | }); 27 | 28 | gulp.task('watch', gulp.series('build', 'build-watch')); 29 | -------------------------------------------------------------------------------- /tasks/default.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | gulp.task('default', gulp.series('lint', 'spec')); 4 | -------------------------------------------------------------------------------- /tasks/lint.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import eslint from 'gulp-eslint'; 3 | import gulpIf from 'gulp-if'; 4 | 5 | gulp.task('lint', () => { 6 | const {FIX: fix = true} = process.env; 7 | return gulp.src(['gulpfile.js', 'tasks/**/*.js', 'src/**/*.js', 'spec/**/*.js'], {base: '.'}) 8 | .pipe(eslint({fix})) 9 | .pipe(eslint.format('stylish')) 10 | .pipe(gulpIf(file => file.eslint && typeof file.eslint.output === 'string', gulp.dest('.'))) 11 | .pipe(eslint.failOnError()); 12 | }); 13 | -------------------------------------------------------------------------------- /tasks/publish.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import npm from 'npm'; 3 | 4 | gulp.task('publish', gulp.series('build', done => { 5 | npm.load({}, error => { 6 | if (error) { 7 | console.error(error); 8 | done(); 9 | } 10 | npm.commands.publish(['dist'], error => { 11 | if (error) console.error(error); 12 | done(); 13 | }); 14 | }); 15 | })); -------------------------------------------------------------------------------- /tasks/spec.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import jasmine from 'gulp-jasmine' 3 | import plumber from 'gulp-plumber'; 4 | 5 | gulp.task('spec-run', () => { 6 | return gulp.src(['spec/**/*_spec.js', '!spec/fixtures/**/*.js']) 7 | .pipe(plumber()) 8 | .pipe(jasmine({includeStackTrace: true})); 9 | }); 10 | 11 | gulp.task('spec', gulp.series('build', 'spec-run')); 12 | --------------------------------------------------------------------------------