├── test ├── e2e │ ├── timeout │ │ ├── fake-browser.sh │ │ ├── specs.js │ │ └── karma.conf.ignore.js │ ├── requirejs │ │ ├── shim.js │ │ ├── dependency.js │ │ ├── test.js │ │ ├── main.js │ │ └── karma.conf.js │ ├── html2js │ │ ├── template.html │ │ ├── test.js │ │ └── karma.conf.js │ ├── coffee │ │ ├── plus.coffee │ │ ├── test.coffee │ │ └── karma.conf.coffee │ ├── basic │ │ ├── plus.js │ │ ├── test.js │ │ └── karma.conf.js │ ├── junit │ │ ├── plus.js │ │ ├── test.js │ │ └── karma.conf.js │ ├── qunit │ │ ├── plus.js │ │ ├── test.js │ │ └── karma.conf.js │ ├── sauce │ │ ├── plus.js │ │ ├── test.js │ │ └── karma.conf.js │ ├── browserstack │ │ ├── plus.js │ │ ├── test.js │ │ └── karma.conf.js │ ├── coverage │ │ ├── lib │ │ │ ├── minus.js │ │ │ └── plus.js │ │ ├── test │ │ │ ├── minus.spec.js │ │ │ └── plus.spec.js │ │ └── karma.conf.js │ ├── coverageQunit │ │ ├── lib │ │ │ ├── minus.js │ │ │ └── plus.js │ │ ├── test │ │ │ ├── minus.spec.js │ │ │ └── plus.spec.js │ │ └── karma.conf.js │ ├── coverageRequirejs │ │ ├── dependency.js │ │ ├── main.js │ │ ├── test.js │ │ └── karma.conf.js │ ├── syntax-error │ │ ├── under-test.js │ │ ├── test.js │ │ └── karma.conf.ignore.js │ ├── dojo │ │ ├── dependency.js │ │ ├── test.js │ │ ├── main.js │ │ └── karma.conf.ignore.js │ ├── pass-opts │ │ ├── test.js │ │ └── karma.conf.js │ ├── angular-scenario │ │ ├── index.html │ │ ├── e2eSpec.js │ │ └── karma.conf.js │ └── mocha │ │ ├── karma.conf.js │ │ └── specs.js ├── unit │ ├── server.spec.coffee │ ├── logger.spec.coffee │ ├── reporters │ │ └── Base.spec.coffee │ ├── mocha-globals.coffee │ ├── executor.spec.coffee │ ├── runner.spec.coffee │ ├── completion.spec.coffee │ ├── reporter.spec.coffee │ ├── preprocessor.spec.coffee │ ├── web-server.spec.coffee │ ├── events.spec.coffee │ ├── watcher.spec.coffee │ ├── middleware │ │ ├── source-files.spec.coffee │ │ └── proxy.spec.coffee │ ├── launcher.spec.coffee │ └── cli.spec.coffee └── client │ ├── mocks.js │ ├── karma.conf.js │ └── karma.spec.js ├── thesis.pdf ├── scripts ├── init-dev-env.bat └── init-dev-env.js ├── docs ├── index.md ├── plus │ ├── 07-yeoman.md │ ├── 06-angularjs.md │ ├── 05-cloud9.md │ ├── 04-semaphore.md │ ├── 03-jenkins.md │ └── 02-travis.md ├── about │ ├── 01-versioning.md │ └── 03-migration.md ├── dev │ ├── 01-public-api.md │ ├── 03-contributing.md │ ├── 02-plugins.md │ └── 04-git-commit-msg.md ├── config │ ├── 05-plugins.md │ ├── 02-files.md │ ├── 04-preprocessors.md │ └── 03-browsers.md └── intro │ ├── 03-how-it-works.md │ ├── 01-installation.md │ ├── 04-faq.md │ └── 02-configuration.md ├── .mailmap ├── lib ├── index.js ├── reporters │ ├── DotsColor.js │ ├── ProgressColor.js │ ├── Multi.js │ ├── BaseColor.js │ ├── Dots.js │ ├── Progress.js │ └── Base.js ├── constants.js ├── events.js ├── executor.js ├── plugin.js ├── middleware │ ├── source-files.js │ ├── common.js │ ├── runner.js │ ├── proxy.js │ └── karma.js ├── runner.js ├── watcher.js ├── helper.js ├── preprocessor.js ├── web-server.js ├── reporter.js ├── launcher.js ├── logger.js ├── completion.js └── launchers │ └── Base.js ├── .npmignore ├── static ├── karma.wrapper ├── context.html ├── debug.html └── client.html ├── .gitignore ├── .gitattributes ├── .travis.yml ├── tasks ├── init-dev-env.js ├── build.js └── test.js ├── bin └── karma ├── LICENSE ├── config.tpl.coffee ├── config.tpl.js ├── karma-completion.sh ├── CONTRIBUTING.md └── Gruntfile.coffee /test/e2e/timeout/fake-browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read 4 | -------------------------------------------------------------------------------- /thesis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pl/karma/master/thesis.pdf -------------------------------------------------------------------------------- /test/e2e/requirejs/shim.js: -------------------------------------------------------------------------------- 1 | window.global = 'some exported value'; 2 | -------------------------------------------------------------------------------- /test/e2e/html2js/template.html: -------------------------------------------------------------------------------- 1 |
content of the template
2 | -------------------------------------------------------------------------------- /scripts/init-dev-env.bat: -------------------------------------------------------------------------------- 1 | set __dirname=%~dp0 2 | node %__dirname%init-dev-env.js -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | layout: homepage 2 | pageTitle: Spectacular Test Runner for Javascript 3 | -------------------------------------------------------------------------------- /test/e2e/coffee/plus.coffee: -------------------------------------------------------------------------------- 1 | # Some code under test 2 | plus = (a, b) -> 3 | a + b 4 | -------------------------------------------------------------------------------- /test/e2e/basic/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/junit/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/qunit/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/sauce/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/browserstack/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/coverage/lib/minus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function minus(a, b) { 3 | return a - b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/coverage/lib/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/coverageQunit/lib/minus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function minus(a, b) { 3 | return a - b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/coverageQunit/lib/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | function plus(a, b) { 3 | return a + b; 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/coverageQunit/test/minus.spec.js: -------------------------------------------------------------------------------- 1 | test('should work', function() { 2 | ok(minus(1, 2) === -1, "Passed"); 3 | }); 4 | -------------------------------------------------------------------------------- /test/e2e/coverageRequirejs/dependency.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | return function (a, b) { 3 | return a + b; 4 | }; 5 | } 6 | ); 7 | -------------------------------------------------------------------------------- /test/e2e/syntax-error/under-test.js: -------------------------------------------------------------------------------- 1 | // Some code under test, with syntax error 2 | }}}} 3 | 4 | function plus(a, b) { 5 | return a + b; 6 | } 7 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/e2e/coverage/test/minus.spec.js: -------------------------------------------------------------------------------- 1 | describe('minus', function() { 2 | it('should work', function() { 3 | expect(minus(1, 2)).toBe(-1); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/e2e/timeout/specs.js: -------------------------------------------------------------------------------- 1 | describe('something', function() { 2 | it('should never happen anyway', function() { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/e2e/dojo/dependency.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | // return the module value 3 | return function (a, b) { 4 | return a + b; 5 | }; 6 | } 7 | ); 8 | -------------------------------------------------------------------------------- /test/e2e/requirejs/dependency.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | // return the module value 3 | return function (a, b) { 4 | return a + b; 5 | }; 6 | } 7 | ); 8 | -------------------------------------------------------------------------------- /test/e2e/coffee/test.coffee: -------------------------------------------------------------------------------- 1 | describe 'plus', -> 2 | 3 | it 'should pass', -> 4 | expect(true).toBe true 5 | 6 | it 'should work', -> 7 | expect(plus 1, 2).toBe 3 8 | -------------------------------------------------------------------------------- /test/e2e/qunit/test.js: -------------------------------------------------------------------------------- 1 | test('should pass', function() { 2 | ok(true === true, "Passed"); 3 | }); 4 | 5 | test('should work', function() { 6 | ok(plus(1, 2) === 3, "Passed"); 7 | }); -------------------------------------------------------------------------------- /docs/plus/07-yeoman.md: -------------------------------------------------------------------------------- 1 | [Yeoman](http://yeoman.io/) is a set of tools to make building web apps easier. Yeoman has support for Karma via the [Karma Generator](https://github.com/yeoman/generator-karma). 2 | -------------------------------------------------------------------------------- /test/e2e/coverageQunit/test/plus.spec.js: -------------------------------------------------------------------------------- 1 | test('should pass', function() { 2 | ok(true === true, "Passed"); 3 | }); 4 | 5 | test('should work', function() { 6 | ok(plus(1, 2) === 3, "Passed"); 7 | }); 8 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // index module 2 | exports.VERSION = require('./constants').VERSION; 3 | exports.server = require('./server'); 4 | exports.runner = require('./runner'); 5 | exports.launcher = require('./launcher'); 6 | 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | 3 | tmp 4 | test 5 | tasks 6 | scripts 7 | docs 8 | 9 | TODO.md 10 | CONTRIBUTING.md 11 | Gruntfile.coffee 12 | 13 | Karma.sublime-* 14 | 15 | static/karma.src.js 16 | static/karma.wrapper 17 | -------------------------------------------------------------------------------- /static/karma.wrapper: -------------------------------------------------------------------------------- 1 | (function(window, document, io) { 2 | 3 | %CONTENT% 4 | 5 | 6 | window.karma = new Karma(socket, document.getElementById('context'), window.navigator, window.location); 7 | 8 | })(window, document, window.io); 9 | -------------------------------------------------------------------------------- /test/e2e/basic/test.js: -------------------------------------------------------------------------------- 1 | describe('plus', function() { 2 | it('should pass', function() { 3 | expect(true).toBe(true); 4 | }); 5 | 6 | it('should work', function() { 7 | expect(plus(1, 2)).toBe(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/e2e/sauce/test.js: -------------------------------------------------------------------------------- 1 | describe('plus', function() { 2 | it('should pass', function() { 3 | expect(true).toBe(true); 4 | }); 5 | 6 | it('should work', function() { 7 | expect(plus(1, 2)).toBe(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/e2e/browserstack/test.js: -------------------------------------------------------------------------------- 1 | describe('plus', function() { 2 | it('should pass', function() { 3 | expect(true).toBe(true); 4 | }); 5 | 6 | it('should work', function() { 7 | expect(plus(1, 2)).toBe(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/e2e/syntax-error/test.js: -------------------------------------------------------------------------------- 1 | describe('plus', function() { 2 | it('should pass', function() { 3 | expect(true).toBe(true); 4 | }); 5 | 6 | it('should work', function() { 7 | expect(plus(1, 2)).toBe(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/e2e/coverage/test/plus.spec.js: -------------------------------------------------------------------------------- 1 | describe('plus', function() { 2 | it('should pass', function() { 3 | expect(true).toBe(true); 4 | }); 5 | 6 | it('should work', function() { 7 | expect(plus(1, 2)).toBe(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/e2e/html2js/test.js: -------------------------------------------------------------------------------- 1 | describe('template', function() { 2 | it('should expose the templates to __html__', function() { 3 | document.body.innerHTML = __html__['template.html']; 4 | expect(document.getElementById('tpl')).toBeDefined(); 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /test/e2e/pass-opts/test.js: -------------------------------------------------------------------------------- 1 | describe('config', function() { 2 | it('should be passed through to the browser', function() { 3 | expect(window.__karma__.config).toBeDefined(); 4 | expect(window.__karma__.config.args).toEqual(['arg1','arg2','arg3']); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | static/karma.js 3 | .idea/* 4 | *.iml 5 | tmp/* 6 | docs/_build 7 | *.swp 8 | *.swo 9 | test/e2e/coverage/coverage 10 | test/e2e/coverageQunit/coverage 11 | test/e2e/coverageRequirejs/coverage 12 | test-results.xml 13 | test/unit/test.log 14 | -------------------------------------------------------------------------------- /test/unit/server.spec.coffee: -------------------------------------------------------------------------------- 1 | # TODO(vojta): 2 | 'should try next port if already in use' 3 | 'should launch browsers after web server started' 4 | 5 | # single run 6 | 'should run tests when all browsers captured' 7 | 'should run tests when first browser captured if no browser configured' 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/dealing-with-line-endings 2 | 3 | # By default, normalize all files to unix line endings when commiting. 4 | * text 5 | 6 | # Denote all files that are truly binary and should not be modified. 7 | *.png binary 8 | *.jpg binary 9 | *.pdf binary 10 | -------------------------------------------------------------------------------- /test/e2e/junit/test.js: -------------------------------------------------------------------------------- 1 | describe('plus', function() { 2 | it('should pass', function() { 3 | expect(true).toBe(true); 4 | }); 5 | 6 | it('should work', function() { 7 | console.log("First parameter: 1"); 8 | console.log("Second parameter: 2"); 9 | expect(plus(1, 2)).toBe(3); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/e2e/coverageRequirejs/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | // Karma serves files under /base, which is the basePath from your config file 3 | baseUrl: '/base', 4 | 5 | // load test.js 6 | deps: ['test'], 7 | 8 | // we have to kick of jasmine, as it is asynchronous 9 | callback: window.__karma__.start 10 | }); 11 | -------------------------------------------------------------------------------- /test/e2e/requirejs/test.js: -------------------------------------------------------------------------------- 1 | define(['dependency'], function(dep) { 2 | 3 | // jasmine 4 | describe('something', function() { 5 | it('should pass', function() { 6 | expect(true).toBe(true); 7 | }); 8 | 9 | it('should sum', function() { 10 | expect(dep(1, 2)).toBe(3); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/reporters/DotsColor.js: -------------------------------------------------------------------------------- 1 | var DotsReporter = require('./Dots'); 2 | var BaseColorReporter = require('./BaseColor'); 3 | 4 | 5 | var DotsColorReporter = function(formatError, reportSlow) { 6 | DotsReporter.call(this, formatError, reportSlow); 7 | BaseColorReporter.call(this); 8 | }; 9 | 10 | 11 | // PUBLISH 12 | module.exports = DotsColorReporter; 13 | -------------------------------------------------------------------------------- /lib/reporters/ProgressColor.js: -------------------------------------------------------------------------------- 1 | var ProgressReporter = require('./Progress'); 2 | var BaseColorReporter = require('./BaseColor'); 3 | 4 | 5 | var ProgressColorReporter = function(formatError, reportSlow) { 6 | ProgressReporter.call(this, formatError, reportSlow); 7 | BaseColorReporter.call(this); 8 | }; 9 | 10 | 11 | // PUBLISH 12 | module.exports = ProgressColorReporter; 13 | -------------------------------------------------------------------------------- /test/e2e/pass-opts/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | files: [ 6 | '*.js' 7 | ], 8 | 9 | browsers: [ process.env.TRAVIS ? 'Firefox' : 'Chrome' ], 10 | 11 | reporters: ['dots'], 12 | 13 | plugins: [ 14 | 'karma-jasmine', 15 | 'karma-chrome-launcher', 16 | 'karma-firefox-launcher' 17 | ], 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /test/e2e/angular-scenario/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample Angular App 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 |

Hello {{yourName}}!

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /test/e2e/coverageRequirejs/test.js: -------------------------------------------------------------------------------- 1 | define(['chai'], function(chai) { 2 | var expect = chai.expect; 3 | 4 | describe('something', function() { 5 | it('should pass', function() { 6 | expect(true).to.equal(true); 7 | }); 8 | 9 | it('should sum', function(done) { 10 | require(['dependency'], function(dep) { 11 | expect(dep(1, 2)).to.equal(3); 12 | done(); 13 | }); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/e2e/basic/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | files: [ 6 | '*.js' 7 | ], 8 | 9 | autoWatch: true, 10 | 11 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 12 | 13 | reporters: ['dots'], 14 | 15 | plugins: [ 16 | 'karma-jasmine', 17 | 'karma-chrome-launcher', 18 | 'karma-firefox-launcher' 19 | ], 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /test/e2e/qunit/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['qunit'], 4 | 5 | files: [ 6 | '*.js' 7 | ], 8 | 9 | autoWatch: true, 10 | 11 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 12 | 13 | reporters: ['dots'], 14 | 15 | plugins: [ 16 | 'karma-qunit', 17 | 'karma-chrome-launcher', 18 | 'karma-firefox-launcher' 19 | ], 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /test/e2e/angular-scenario/e2eSpec.js: -------------------------------------------------------------------------------- 1 | /** A Sample Angular E2E test */ 2 | 3 | describe('My Sample App', function() { 4 | 5 | it('should let Angular do its work', function() { 6 | browser().navigateTo('/index.html'); 7 | input('yourName').enter('A Pirate!'); 8 | expect(element('.ng-binding').text()).toEqual('Hello A Pirate!!'); 9 | }); 10 | 11 | xit('should skip this e2e test', function() { 12 | sleep(15); 13 | browser().navigateTo('/index.html'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/e2e/dojo/test.js: -------------------------------------------------------------------------------- 1 | define(['local/dependency','dojo/_base/lang'], function(dep,lang) { 2 | 3 | // jasmine 4 | describe('something', function() { 5 | it('should pass', function() { 6 | expect(true).toBe(true); 7 | }); 8 | 9 | it('should sum', function() { 10 | expect(dep(1, 2)).toBe(3); 11 | }); 12 | 13 | it('should trim using a dojo AMD module',function(){ 14 | expect(lang.trim(" len4 ").length).toEqual(4); 15 | }); 16 | 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/e2e/mocha/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['mocha'], 4 | 5 | files: [ 6 | '*.js' 7 | ], 8 | 9 | autoWatch: true, 10 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 11 | singleRun: false, 12 | 13 | browsers: ['Chrome'], 14 | 15 | reporters: ['dots'], 16 | 17 | plugins: [ 18 | 'karma-mocha', 19 | 'karma-chrome-launcher', 20 | 'karma-firefox-launcher' 21 | ], 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /test/e2e/html2js/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | files: [ 6 | '*.js', 7 | '*.html' 8 | ], 9 | 10 | autoWatch: true, 11 | 12 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 13 | 14 | reporters: ['dots'], 15 | 16 | plugins: [ 17 | 'karma-jasmine', 18 | 'karma-html2js-preprocessor', 19 | 'karma-chrome-launcher', 20 | 'karma-firefox-launcher' 21 | ], 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /test/e2e/syntax-error/karma.conf.ignore.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | // files to load 6 | files: [ 7 | '*.js' 8 | ], 9 | 10 | autoWatch: true, 11 | logLevel: config.LOG_INFO, 12 | logColors: true, 13 | 14 | browsers: ['Chrome'], 15 | 16 | reporters: ['dots'], 17 | 18 | plugins: [ 19 | 'karma-jasmine', 20 | 'karma-chrome-launcher', 21 | 'karma-firefox-launcher' 22 | ], 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/reporters/Multi.js: -------------------------------------------------------------------------------- 1 | var helper = require('../helper'); 2 | 3 | 4 | var MultiReporter = function(reporters) { 5 | 6 | this.addAdapter = function(adapter) { 7 | reporters.forEach(function(reporter) { 8 | reporter.adapters.push(adapter); 9 | }); 10 | }; 11 | 12 | this.removeAdapter = function(adapter) { 13 | reporters.forEach(function(reporter) { 14 | helper.arrayRemove(reporter.adapters, adapter); 15 | }); 16 | }; 17 | }; 18 | 19 | 20 | // PUBLISH 21 | module.exports = MultiReporter; 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.10 5 | 6 | env: 7 | global: 8 | - SAUCE_USERNAME: vojtajina 9 | - BROWSER_STACK_USERNAME: vojta.jina@gmail.com 10 | - secure: "k2x1puIrj42LqnWF8SoDod19xpeJhJN3RDMgRLK2LTYffcdJV8TXilPRUk4pXyN6u8B79fxAW8WucsrlxB6CaaB5cXsFTDG+jpO21AlFc3mFjPh6sTkp/1L/bxc3cJNc9w18Ol2+8MO5s37xuy/lJaQeBRFxQleZZWCLLjtXytg=" 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | - npm install -g grunt-cli 16 | - rm -rf node_modules/karma 17 | 18 | script: 19 | - grunt 20 | -------------------------------------------------------------------------------- /docs/plus/06-angularjs.md: -------------------------------------------------------------------------------- 1 | pageTitle: AngularJS 2 | menuTitle: AngularJS 3 | 4 | If you're using [AngularJS](http://angularjs.org), check out the [AngularJS Generator](https://github.com/yeoman/generator-angular), which makes use of the [Karma Generator](https://github.com/yeoman/generator-karma) to setup a fully featured, testing-ready project. 5 | 6 | Here is also a blog article explaining how to setup Grunt and Karma to test out an AngularJS application: [Full Spectrum Testing with AngularJS and Karma](http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-karma.html) 7 | -------------------------------------------------------------------------------- /tasks/init-dev-env.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | /** 4 | * Initialize development environment for Karma 5 | * 6 | * - register git hooks (commit-msg) 7 | */ 8 | grunt.registerTask('init-dev-env', 'Initialize dev environment.', function() { 9 | var fs = require('fs'); 10 | var done = this.async(); 11 | 12 | fs.symlink('../../tasks/lib/validate-commit-msg.js', '.git/hooks/commit-msg', function(e) { 13 | if (!e) { 14 | grunt.log.ok('Hook "validate-commit-msg" installed.'); 15 | } 16 | done(); 17 | }); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /test/e2e/angular-scenario/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['ng-scenario'], 4 | 5 | files: [ 6 | 'e2eSpec.js' 7 | ], 8 | 9 | urlRoot: '/__karma/', 10 | 11 | autoWatch: true, 12 | 13 | proxies: { 14 | '/': 'http://localhost:8000/test/e2e/angular-scenario/' 15 | }, 16 | 17 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 18 | 19 | reporters: ['dots'], 20 | 21 | plugins: [ 22 | 'karma-ng-scenario', 23 | 'karma-chrome-launcher', 24 | 'karma-firefox-launcher' 25 | ] 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /test/e2e/coffee/karma.conf.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (config) -> 2 | config.set 3 | frameworks: ['jasmine'] 4 | 5 | files: [ 6 | '*.coffee' 7 | ] 8 | 9 | autoWatch: true 10 | 11 | browsers: [if process.env.TRAVIS then 'Firefox' else 'Chrome'] 12 | 13 | coffeePreprocessor: 14 | options: 15 | sourceMap: true 16 | 17 | preprocessors: 18 | '**/*.coffee': 'coffee' 19 | 20 | reporters: ['dots'] 21 | 22 | plugins: [ 23 | 'karma-jasmine' 24 | 'karma-coffee-preprocessor' 25 | 'karma-chrome-launcher' 26 | 'karma-firefox-launcher' 27 | ] 28 | -------------------------------------------------------------------------------- /test/e2e/junit/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | files: [ 6 | '*.js' 7 | ], 8 | 9 | autoWatch: true, 10 | 11 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 12 | 13 | reporters: ['dots', 'junit'], 14 | 15 | logLevel: config.LOG_DEBUG, 16 | 17 | junitReporter: { 18 | outputFile: 'test-results.xml' 19 | }, 20 | 21 | plugins: [ 22 | 'karma-jasmine', 23 | 'karma-chrome-launcher', 24 | 'karma-firefox-launcher', 25 | 'karma-junit-reporter' 26 | ], 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /test/e2e/requirejs/main.js: -------------------------------------------------------------------------------- 1 | var allTestFiles = []; 2 | var TEST_REGEXP = /test\.js$/; 3 | 4 | Object.keys(window.__karma__.files).forEach(function(file) { 5 | if (TEST_REGEXP.test(file)) { 6 | allTestFiles.push(file); 7 | } 8 | }); 9 | 10 | require.config({ 11 | // Karma serves files under /base, which is the basePath from your config file 12 | baseUrl: '/base', 13 | 14 | // example of using shim, to load non AMD libraries (such as Backbone, jquery) 15 | shim: { 16 | '/base/shim.js': { 17 | exports: 'global' 18 | } 19 | }, 20 | 21 | // dynamically load all test files 22 | deps: allTestFiles, 23 | 24 | // we have to kick of jasmine, as it is asynchronous 25 | callback: window.__karma__.start 26 | }); 27 | -------------------------------------------------------------------------------- /test/e2e/mocha/specs.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect; 2 | 3 | describe('Array', function() { 4 | 5 | describe('.push()', function() { 6 | 7 | it('should append a value', function() { 8 | var arr = []; 9 | 10 | arr.push('foo'); 11 | arr.push('bar'); 12 | 13 | expect(arr[0]).to.equal('foo'); 14 | expect(arr[1]).to.equal('bar'); 15 | }); 16 | 17 | 18 | it('should return the length', function() { 19 | var i = 100; 20 | var some = 'x'; 21 | 22 | while (i--) { 23 | some = some + 'xxx'; 24 | } 25 | 26 | var arr = []; 27 | var n = arr.push('foo'); 28 | expect(n).to.equal(1); 29 | 30 | var n = arr.push('bar'); 31 | expect(n).to.equal(2); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/e2e/sauce/karma.conf.js: -------------------------------------------------------------------------------- 1 | var TRAVIS_WITHOUT_SAUCE = process.env.TRAVIS_SECURE_ENV_VARS === 'false'; 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | frameworks: ['jasmine'], 6 | 7 | files: [ 8 | '*.js' 9 | ], 10 | 11 | autoWatch: true, 12 | 13 | browsers: [TRAVIS_WITHOUT_SAUCE ? 'Firefox' : 'sl_chrome_linux'], 14 | 15 | reporters: ['dots'], 16 | 17 | logLevel: config.LOG_DEBUG, 18 | 19 | plugins: [ 20 | 'karma-jasmine', 21 | 'karma-sauce-launcher', 22 | 'karma-firefox-launcher' 23 | ], 24 | 25 | customLaunchers: { 26 | sl_chrome_linux: { 27 | base: 'SauceLabs', 28 | browserName: 'chrome', 29 | platform: 'linux' 30 | } 31 | } 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /docs/about/01-versioning.md: -------------------------------------------------------------------------------- 1 | Karma uses [Semantic Versioning] with a little exception: 2 | - even versions (eg. `0.6.x`, `0.8.x`) are considered stable - no breaking changes or new features, only bug fixes will pushed into this branch, 3 | - odd versions (eg. `0.7.x`, `0.9.x`) are unstable - anything can happen ;-) 4 | 5 | Therefore it is recommended to rely on the latest stable version, which gives you automatic bug fixes, but does not break you: 6 | ```javascript 7 | { 8 | "devDependencies": { 9 | "karma": "~0.10" 10 | } 11 | } 12 | ``` 13 | 14 | ## Stable channel (branch `stable`) 15 | ```bash 16 | $ npm install karma 17 | ``` 18 | 19 | ## Canary channel (branch `master`) 20 | ```bash 21 | $ npm install karma@canary 22 | ``` 23 | 24 | [Semantic Versioning]: http://semver.org/ 25 | -------------------------------------------------------------------------------- /test/e2e/coverageRequirejs/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['mocha', 'requirejs'], 4 | 5 | files: [ 6 | 'main.js', 7 | {pattern: '*.js', included: false}, 8 | ], 9 | 10 | autoWatch: true, 11 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 12 | singleRun: false, 13 | 14 | reporters: ['progress', 'coverage'], 15 | 16 | preprocessors: { 17 | 'dependency.js': 'coverage' 18 | }, 19 | 20 | coverageReporter: { 21 | type : 'html', 22 | dir : 'coverage/' 23 | }, 24 | 25 | plugins: [ 26 | 'karma-mocha', 27 | 'karma-requirejs', 28 | 'karma-coverage', 29 | 'karma-chrome-launcher', 30 | 'karma-firefox-launcher' 31 | ], 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/reporters/BaseColor.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | 3 | var BaseColorReporter = function() { 4 | this.USE_COLORS = true; 5 | 6 | this.LOG_SINGLE_BROWSER = '%s: ' + '%s'.cyan + '\n'; 7 | this.LOG_MULTI_BROWSER = '%s %s: ' + '%s'.cyan + '\n'; 8 | 9 | this.SPEC_FAILURE = '%s %s FAILED'.red + '\n'; 10 | this.SPEC_SLOW = '%s SLOW %s: %s'.yellow + '\n'; 11 | this.ERROR = '%s ERROR'.red + '\n'; 12 | 13 | this.FINISHED_ERROR = ' ERROR'.red; 14 | this.FINISHED_SUCCESS = ' SUCCESS'.green; 15 | this.FINISHED_DISCONNECTED = ' DISCONNECTED'.red; 16 | 17 | this.X_FAILED = ' (%d FAILED)'.red; 18 | 19 | this.TOTAL_SUCCESS = 'TOTAL: %d SUCCESS'.green + '\n'; 20 | this.TOTAL_FAILED = 'TOTAL: %d FAILED, %d SUCCESS'.red + '\n'; 21 | }; 22 | 23 | 24 | // PUBLISH 25 | module.exports = BaseColorReporter; 26 | -------------------------------------------------------------------------------- /test/client/mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | Mocks for testing static/karma.js 3 | Needs to be loaded before karma.js 4 | */ 5 | 6 | var Emitter = function() { 7 | var listeners = {}; 8 | 9 | this.on = function(event, fn) { 10 | if (!listeners[event]) { 11 | listeners[event] = []; 12 | } 13 | 14 | listeners[event].push(fn); 15 | }; 16 | 17 | this.emit = function(event) { 18 | var eventListeners = listeners[event]; 19 | 20 | if (!eventListeners) return; 21 | 22 | var i = 0; 23 | while (i < eventListeners.length) { 24 | eventListeners[i].apply(null, Array.prototype.slice.call(arguments, 1)); 25 | i++; 26 | } 27 | }; 28 | }; 29 | 30 | var MockSocket = Emitter; 31 | 32 | var io = { 33 | connect: function() { 34 | return new MockSocket(); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var pkg = JSON.parse(fs.readFileSync(__dirname + '/../package.json').toString()); 4 | 5 | exports.VERSION = pkg.version; 6 | 7 | exports.DEFAULT_PORT = 9876; 8 | exports.DEFAULT_HOSTNAME = 'localhost'; 9 | 10 | // log levels 11 | exports.LOG_DISABLE = 'OFF'; 12 | exports.LOG_ERROR = 'ERROR'; 13 | exports.LOG_WARN = 'WARN'; 14 | exports.LOG_INFO = 'INFO'; 15 | exports.LOG_DEBUG = 'DEBUG'; 16 | 17 | // Default patterns for the pattern layout. 18 | exports.COLOR_PATTERN = '%[%p [%c]: %]%m'; 19 | exports.NO_COLOR_PATTERN = '%p [%c]: %m'; 20 | 21 | // Default console appender 22 | exports.CONSOLE_APPENDER = { 23 | type: 'console', 24 | layout: { 25 | type: 'pattern', 26 | pattern: exports.COLOR_PATTERN 27 | } 28 | }; 29 | 30 | exports.EXIT_CODE = '\x1FEXIT'; 31 | -------------------------------------------------------------------------------- /tasks/build.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | /** 4 | * Build given file - wrap it with a function call 5 | * 6 | * grunt build 7 | * grunt build:client 8 | * grunt build:jasmine 9 | * grunt build:mocha 10 | * grunt build:ngScenario 11 | * grunt build:require 12 | */ 13 | grunt.registerMultiTask('build', 'Wrap given file into a function call.', function() { 14 | 15 | var src = grunt.file.expand(this.data).pop(); 16 | var dest = src.replace('src.js', 'js'); 17 | var wrapper = src.replace('src.js', 'wrapper'); 18 | 19 | grunt.file.copy(wrapper, dest, {process: function(content) { 20 | var wrappers = content.split(/%CONTENT%\r?\n/); 21 | return wrappers[0] + grunt.file.read(src) + wrappers[1]; 22 | }}); 23 | 24 | grunt.log.ok('Created ' + dest); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /bin/karma: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | // Try to find a local install 7 | var dir = path.resolve(process.cwd(), 'node_modules', 'karma', 'lib'); 8 | 9 | // Check if the local install exists else we use the install we are in 10 | if (!fs.existsSync(dir)) { 11 | dir = path.join('..', 'lib'); 12 | } 13 | 14 | var cli = require(path.join(dir, 'cli')); 15 | var config = cli.process(); 16 | 17 | switch (config.cmd) { 18 | case 'start': 19 | require(path.join(dir, 'server')).start(config); 20 | break; 21 | case 'run': 22 | require(path.join(dir, 'runner')).run(config); 23 | break; 24 | case 'init': 25 | require(path.join(dir, 'init')).init(config); 26 | break; 27 | case 'completion': 28 | require(path.join(dir, 'completion')).completion(config); 29 | break; 30 | } 31 | -------------------------------------------------------------------------------- /docs/dev/01-public-api.md: -------------------------------------------------------------------------------- 1 | Most of the time, you will be using Karma directly from the command line. 2 | You can, however, call Karma programmatically from your node module. Here is the public API. 3 | 4 | 5 | ## karma.server 6 | 7 | ### **server.start(options, [callback=process.exit])** 8 | 9 | Equivalent of `karma start`. 10 | 11 | ```javascript 12 | var server = require('karma').server; 13 | server.start({port: 9876}, function(exitCode) { 14 | console.log('Karma has exited with ' + exitCode); 15 | process.exit(exitCode); 16 | }); 17 | ``` 18 | 19 | ## karma.runner 20 | 21 | ### **runner.run(options, [callback=process.exit])** 22 | 23 | Equivalent of `karma run`. 24 | 25 | ```javascript 26 | var runner = require('karma').runner; 27 | runner.run({port: 9876}, function(exitCode) { 28 | console.log('Karma has exited with ' + exitCode); 29 | process.exit(exitCode); 30 | }); 31 | ``` 32 | -------------------------------------------------------------------------------- /test/e2e/dojo/main.js: -------------------------------------------------------------------------------- 1 | var allTestFiles = []; 2 | var TEST_REGEXP = /test.*\.js$/; 3 | 4 | Object.keys(window.__karma__.files).forEach(function(file) { 5 | if (TEST_REGEXP.test(file)) { 6 | allTestFiles.push(file); 7 | } 8 | }); 9 | 10 | var dojoConfig = { 11 | packages: [ 12 | { name:"local" ,location:"/base"}, 13 | { name: "dojo", location: "http://ajax.googleapis.com/ajax/libs/dojo/1.9.1/dojo" }, 14 | { name: "dojox", location: "http://ajax.googleapis.com/ajax/libs/dojo/1.9.1/dojox" } 15 | ], 16 | asynch: true 17 | }; 18 | 19 | 20 | /** 21 | * This function must be defined and is called back by the dojo adapter 22 | * @returns {string} a list of dojo spec/test modules to register with your testing framework 23 | */ 24 | window.__karma__.dojoStart = function(){ 25 | return allTestFiles; 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/unit/logger.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/logger.js module 3 | #============================================================================== 4 | 5 | describe 'logger', -> 6 | loadFile = require('mocks').loadFile 7 | logSpy = m = null 8 | beforeEach -> 9 | logSpy = sinon.spy() 10 | m = loadFile __dirname + '/../../lib/logger.js' 11 | 12 | #============================================================================ 13 | # setup() 14 | #============================================================================ 15 | describe 'setup', -> 16 | it 'should allow for configuration via setup() using an array', -> 17 | m.setup 'INFO', true, [ 18 | type: 'file' 19 | filename: 'test/unit/test.log' 20 | ] 21 | 22 | expect(m.log4js.appenders).to.have.keys ['console', 'file'] 23 | -------------------------------------------------------------------------------- /docs/plus/05-cloud9.md: -------------------------------------------------------------------------------- 1 | [Cloud9 IDE] is an open source web-based cloud integrated development environment that supports several programming languages, with a focus on the web stack (specifically JavaScript and [NodeJS]). It is written almost entirely in JavaScript, and uses [NodeJS] on the back-end. 2 | 3 | 4 | There are two possibilities in order to run unit tests with Karma in [Cloud9 IDE]: 5 | 6 | ## Capture the browser manually on the local machine 7 | 8 | Open `http://..c9.io/` in your browser. 9 | 10 | ## Run Karma unit tests with PhantomJS in cloud9 IDE 11 | 12 | ### Install PhantomJS 13 | PhantomJS must be installed with `npm install phantomjs`. 14 | 15 | ### Configure Karma 16 | The `karma.conf.js` file must include the following entries: 17 | 18 | ```javascript 19 | browsers: ['PhantomJS'], 20 | hostname: process.env.IP, 21 | port: process.env.PORT 22 | ``` 23 | 24 | [Cloud9 IDE]: https://c9.io/ 25 | [NodeJS]: http://nodejs.org/ 26 | -------------------------------------------------------------------------------- /test/e2e/coverage/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['jasmine'], 4 | 5 | files: [ 6 | 'lib/*.js', 7 | 'test/*.js' 8 | ], 9 | 10 | autoWatch: true, 11 | 12 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 13 | 14 | reporters: ['progress', 'coverage'], 15 | 16 | preprocessors: { 17 | 'lib/*.js': 'coverage' 18 | }, 19 | 20 | //Code Coverage options. report type available: 21 | //- html (default) 22 | //- lcov (lcov and html) 23 | //- lcovonly 24 | //- text (standard output) 25 | //- text-summary (standard output) 26 | //- cobertura (xml format supported by Jenkins) 27 | coverageReporter: { 28 | // cf. http://gotwarlost.github.io/istanbul/public/apidocs/ 29 | type : 'html', 30 | dir : 'coverage/' 31 | }, 32 | 33 | plugins: [ 34 | 'karma-jasmine', 35 | 'karma-coverage', 36 | 'karma-chrome-launcher', 37 | 'karma-firefox-launcher' 38 | ], 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /test/e2e/coverageQunit/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | frameworks: ['qunit'], 4 | 5 | files: [ 6 | 'lib/*.js', 7 | 'test/*.js' 8 | ], 9 | 10 | autoWatch: true, 11 | 12 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 13 | 14 | reporters: ['progress', 'coverage'], 15 | 16 | preprocessors: { 17 | 'lib/*.js': 'coverage' 18 | }, 19 | 20 | //Code Coverage options. report type available: 21 | //- html (default) 22 | //- lcov (lcov and html) 23 | //- lcovonly 24 | //- text (standard output) 25 | //- text-summary (standard output) 26 | //- cobertura (xml format supported by Jenkins) 27 | coverageReporter: { 28 | // cf. http://gotwarlost.github.io/istanbul/public/apidocs/ 29 | type : 'html', 30 | dir : 'coverage/' 31 | }, 32 | 33 | plugins: [ 34 | 'karma-qunit', 35 | 'karma-coverage', 36 | 'karma-chrome-launcher', 37 | 'karma-firefox-launcher' 38 | ], 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /test/e2e/browserstack/karma.conf.js: -------------------------------------------------------------------------------- 1 | var TRAVIS_WITHOUT_BS = process.env.TRAVIS_SECURE_ENV_VARS === 'false'; 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | frameworks: ['jasmine'], 6 | 7 | files: [ 8 | '*.js' 9 | ], 10 | 11 | autoWatch: true, 12 | 13 | browsers: TRAVIS_WITHOUT_BS ? ['Firefox'] : ['bs_ff_mac', 'bs_ch_mac'], 14 | 15 | reporters: ['dots'], 16 | 17 | browserStack: { 18 | username: 'vojta.jina@gmail.com', 19 | accessKey: process.env.BROWSER_STACK_ACCESS_KEY 20 | }, 21 | 22 | customLaunchers: { 23 | bs_ff_mac: { 24 | base: 'BrowserStack', 25 | browser: 'firefox', 26 | os: 'mac', 27 | version: '21.0' 28 | }, 29 | bs_ch_mac: { 30 | base: 'BrowserStack', 31 | browser: 'chrome', 32 | os: 'mac', 33 | version: 'latest' 34 | } 35 | }, 36 | 37 | plugins: [ 38 | 'karma-jasmine', 39 | 'karma-firefox-launcher', 40 | 'karma-browserstack-launcher' 41 | ] 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /static/context.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 24 | 25 | %SCRIPTS% 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/unit/reporters/Base.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/reporters/Base.js module 3 | #============================================================================== 4 | describe 'reporter', -> 5 | loadFile = require('mocks').loadFile 6 | m = null 7 | 8 | beforeEach -> 9 | m = loadFile __dirname + '/../../../lib/reporters/Base.js' 10 | 11 | describe 'Progress', -> 12 | adapter = reporter = null 13 | 14 | beforeEach -> 15 | 16 | adapter = sinon.spy() 17 | reporter = new m.BaseReporter null, null, adapter 18 | 19 | 20 | it 'should write to all registered adapters', -> 21 | anotherAdapter = sinon.spy() 22 | reporter.adapters.push anotherAdapter 23 | 24 | reporter.write 'some' 25 | expect(adapter).to.have.been.calledWith 'some' 26 | expect(anotherAdapter).to.have.been.calledWith 'some' 27 | 28 | 29 | it 'should format', -> 30 | reporter.write 'Success: %d Failure: %d', 10, 20 31 | 32 | expect(adapter).to.have.been.calledWith 'Success: 10 Failure: 20' 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2011-2013 Vojta Jína and contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | var Q = require('q'); 4 | 5 | var helper = require('./helper'); 6 | 7 | 8 | var bindAllEvents = function(object, context) { 9 | context = context || this; 10 | 11 | for (var method in object) { 12 | if (helper.isFunction(object[method]) && method.substr(0, 2) === 'on') { 13 | context.on(helper.camelToSnake(method.substr(2)), object[method].bind(object)); 14 | } 15 | } 16 | }; 17 | 18 | // TODO(vojta): log.debug all events 19 | var EventEmitter = function() { 20 | this.bind = bindAllEvents; 21 | 22 | this.emitAsync = function(name) { 23 | // TODO(vojta): allow passing args 24 | // TODO(vojta): ignore/throw if listener call done() multiple times 25 | var pending = this.listeners(name).length; 26 | var deferred = Q.defer(); 27 | var done = function() { 28 | if (!--pending) { 29 | deferred.resolve(); 30 | } 31 | }; 32 | 33 | this.emit(name, done); 34 | 35 | return deferred.promise; 36 | }; 37 | }; 38 | 39 | util.inherits(EventEmitter, events.EventEmitter); 40 | 41 | // PUBLISH 42 | exports.EventEmitter = EventEmitter; 43 | exports.bindAll = bindAllEvents; 44 | -------------------------------------------------------------------------------- /lib/reporters/Dots.js: -------------------------------------------------------------------------------- 1 | var BaseReporter = require('./Base'); 2 | 3 | 4 | var DotsReporter = function(formatError, reportSlow) { 5 | BaseReporter.call(this, formatError, reportSlow); 6 | 7 | var DOTS_WRAP = 80; 8 | 9 | this.onRunStart = function(browsers) { 10 | this._browsers = browsers; 11 | this._dotsCount = 0; 12 | }; 13 | 14 | this.writeCommonMsg = function(msg) { 15 | if (this._dotsCount) { 16 | this._dotsCount = 0; 17 | msg = '\n' + msg; 18 | } 19 | 20 | this.write(msg); 21 | 22 | }; 23 | 24 | 25 | this.specSuccess = function() { 26 | this._dotsCount = (this._dotsCount + 1) % DOTS_WRAP; 27 | this.write(this._dotsCount ? '.' : '.\n'); 28 | }; 29 | 30 | this.onRunComplete = function(browsers, results) { 31 | this.writeCommonMsg(browsers.map(this.renderBrowser).join('\n') + '\n'); 32 | 33 | if (browsers.length > 1 && !results.disconnected && !results.error) { 34 | if (!results.failed) { 35 | this.write(this.TOTAL_SUCCESS, results.success); 36 | } else { 37 | this.write(this.TOTAL_FAILED, results.failed, results.success); 38 | } 39 | } 40 | }; 41 | }; 42 | 43 | 44 | // PUBLISH 45 | module.exports = DotsReporter; 46 | -------------------------------------------------------------------------------- /lib/reporters/Progress.js: -------------------------------------------------------------------------------- 1 | var BaseReporter = require('./Base'); 2 | 3 | 4 | var ProgressReporter = function(formatError, reportSlow) { 5 | BaseReporter.call(this, formatError, reportSlow); 6 | 7 | 8 | this.writeCommonMsg = function(msg) { 9 | this.write(this._remove() + msg + this._render()); 10 | }; 11 | 12 | 13 | this.specSuccess = function() { 14 | this.write(this._refresh()); 15 | }; 16 | 17 | 18 | this.onBrowserComplete = function() { 19 | this.write(this._refresh()); 20 | }; 21 | 22 | 23 | this.onRunStart = function(browsers) { 24 | this._browsers = browsers; 25 | this._isRendered = false; 26 | }; 27 | 28 | 29 | this._remove = function() { 30 | if (!this._isRendered) { 31 | return ''; 32 | } 33 | 34 | var cmd = ''; 35 | this._browsers.forEach(function() { 36 | cmd += '\x1B[1A' + '\x1B[2K'; 37 | }); 38 | 39 | this._isRendered = false; 40 | 41 | return cmd; 42 | }; 43 | 44 | this._render = function() { 45 | this._isRendered = true; 46 | 47 | return this._browsers.map(this.renderBrowser).join('\n') + '\n'; 48 | }; 49 | 50 | this._refresh = function() { 51 | return this._remove() + this._render(); 52 | }; 53 | }; 54 | 55 | 56 | // PUBLISH 57 | module.exports = ProgressReporter; 58 | -------------------------------------------------------------------------------- /docs/config/05-plugins.md: -------------------------------------------------------------------------------- 1 | Karma can be easily extended through plugins. 2 | In fact, all the existing preprocessors, reporters, browser launchers and frameworks are also plugins. 3 | 4 | ## Installation 5 | 6 | Karma plugins are NPM modules, so the recommended way is to keep all the plugins your project requires in `package.json`: 7 | 8 | ```javascript 9 | { 10 | "devDependencies": { 11 | "karma": "~0.10", 12 | "karma-mocha": "~0.0.1", 13 | "karma-growl-reporter": "~0.0.1", 14 | "karma-firefox-launcher": "~0.0.1" 15 | } 16 | } 17 | ``` 18 | 19 | Therefore, a simple way how to install a plugin is 20 | ```bash 21 | npm install karma- --save-dev 22 | ``` 23 | 24 | 25 | ## Loading Plugins 26 | By default, Karma loads all NPM modules that are siblinks to it and their name matches `karma-*`. 27 | 28 | You can also explicitly list plugins you want to load via the `plugins` configuration setting. The configuration value can either be 29 | a string (module name), which will be required by Karma, or an object (inlined plugin). 30 | 31 | ```javascript 32 | plugins: [ 33 | // these plugins will be require() by Karma 34 | 'karma-jasmine', 35 | 'karma-chrome-launcher' 36 | 37 | // inelined plugins 38 | {'framework:xyz', ['factory', factoryFn]}, 39 | require('./plugin-required-from-config') 40 | ] 41 | ``` 42 | 43 | There are already many [existing plugins]. Of course, you write [your own plugins] too! 44 | 45 | [existing plugins]: https://npmjs.org/browse/keyword/karma-plugin 46 | [your own plugins]: ../dev/plugins.html 47 | -------------------------------------------------------------------------------- /docs/intro/03-how-it-works.md: -------------------------------------------------------------------------------- 1 | Karma is essentially a tool which spawns a web server that executes source code against test code for each of the browsers connected. 2 | The results for each test against each browser are examined and displayed via the command line to the developer 3 | such that they can to see which browsers and tests passed or failed. 4 | 5 | A browser can be captured either 6 | - manually, by visiting the URL where the Karma server is listening (typically `http://localhost:9876/`) 7 | - or automatically by letting Karma know which browsers to start when Karma is run (see browsers) 8 | 9 | Karma also watches all the files, specified within the configuration file, and whenever any file changes, it triggers the test run by 10 | sending a signal the testing server to inform all of the captured browsers to run the test code again. 11 | Each browser then loads the source files inside an IFrame, executes the tests and reports the results back to the server. 12 | 13 | The server collects the results from all of the captured browsers and presents them to the developer. 14 | 15 | This is only a very brief overview, as the internals of how Karma works aren't entirely necessary when using Karma. 16 | 17 | However, if you are interested in learning more, Karma itself originates from a university thesis, which goes into detail about the design 18 | and implementation, and it is available to read [right here]. 19 | 20 | [right here]: https://github.com/karma-runner/karma/raw/master/thesis.pdf 21 | [browsers]: ../config/browsers.html 22 | -------------------------------------------------------------------------------- /test/e2e/timeout/karma.conf.ignore.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Sep 30 2012 22:44:01 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | '*.js' 15 | ], 16 | 17 | 18 | // test results reporter to use 19 | // possible values: 'dots', 'progress', 'junit' 20 | reporters: ['progress'], 21 | 22 | 23 | // web server port 24 | port: 8080, 25 | 26 | 27 | // enable / disable colors in the output (reporters and logs) 28 | colors: true, 29 | 30 | 31 | // level of logging 32 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 33 | logLevel: config.LOG_INFO, 34 | 35 | 36 | // enable / disable watching file and executing tests whenever any file changes 37 | autoWatch: true, 38 | 39 | 40 | // Start these browsers, currently available: 41 | // - Chrome 42 | // - ChromeCanary 43 | // - Firefox 44 | // - Opera 45 | // - Safari (only Mac) 46 | // - PhantomJS 47 | // - IE (only Windows) 48 | browsers: [__dirname + '/fake-browser.sh'], 49 | 50 | 51 | // Continuous Integration mode 52 | // if true, it capture browsers, run tests and exit 53 | singleRun: false, 54 | 55 | plugins: [ 56 | 'karma-jasmine', 57 | 'karma-chrome-launcher', 58 | 'karma-firefox-launcher' 59 | ], 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /test/unit/mocha-globals.coffee: -------------------------------------------------------------------------------- 1 | sinon = require 'sinon' 2 | chai = require 'chai' 3 | logger = require '../../lib/logger' 4 | 5 | # publish globals that all specs can use 6 | global.timer = require 'timer-shim' 7 | global.expect = chai.expect 8 | global.should = chai.should() 9 | global.sinon = sinon 10 | 11 | # chai plugins 12 | chai.use(require 'chai-as-promised') 13 | chai.use(require 'sinon-chai') 14 | 15 | # TODO(vojta): remove this global stub 16 | sinon.stub(timer, 'setTimeout').callsArg 0 17 | 18 | beforeEach -> 19 | global.sinon = sinon.sandbox.create() 20 | 21 | # set logger to log INFO, but do not append to console 22 | # so that we can assert logs by logger.on('info', ...) 23 | logger.setup 'INFO', false, [] 24 | 25 | afterEach -> 26 | global.sinon.restore() 27 | 28 | 29 | 30 | # TODO(vojta): move to helpers or something 31 | chai.use (chai, utils) -> 32 | chai.Assertion.addMethod 'beServedAs', (expectedStatus, expectedBody) -> 33 | response = utils.flag @, 'object' 34 | 35 | @assert response._status is expectedStatus, 36 | "expected response status '#{response._status}' to be '#{expectedStatus}'" 37 | @assert response._body is expectedBody, 38 | "expected response body '#{response._body}' to be '#{expectedBody}'" 39 | 40 | chai.Assertion.addMethod 'beNotServed', -> 41 | response = utils.flag @, 'object' 42 | 43 | @assert response._status is null, 44 | "expected response status to not be set, it was '#{response._status}'" 45 | @assert response._body is null, 46 | "expected response body to not be set, it was '#{response._body}'" 47 | -------------------------------------------------------------------------------- /config.tpl.coffee: -------------------------------------------------------------------------------- 1 | # Karma configuration 2 | # Generated on %DATE% 3 | 4 | module.exports = (config) -> 5 | config.set 6 | 7 | # base path, that will be used to resolve all patterns, eg. files, exclude 8 | basePath: '%BASE_PATH%' 9 | 10 | # frameworks to use 11 | frameworks: [%FRAMEWORKS%] 12 | 13 | # list of files / patterns to load in the browser 14 | files: [ 15 | %FILES% 16 | ] 17 | 18 | # list of files to exclude 19 | exclude: [ 20 | %EXCLUDE% 21 | ] 22 | 23 | # test results reporter to use 24 | # possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 25 | reporters: ['progress'] 26 | 27 | # web server port 28 | port: 9876 29 | 30 | # enable / disable colors in the output (reporters and logs) 31 | colors: true 32 | 33 | # level of logging 34 | # possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 35 | logLevel: config.LOG_INFO 36 | 37 | # enable / disable watching file and executing tests whenever any file changes 38 | autoWatch: %AUTO_WATCH% 39 | 40 | # Start these browsers, currently available: 41 | # - Chrome 42 | # - ChromeCanary 43 | # - Firefox 44 | # - Opera 45 | # - Safari (only Mac) 46 | # - PhantomJS 47 | # - IE (only Windows) 48 | browsers: [%BROWSERS%] 49 | 50 | # If browser does not capture in given timeout [ms], kill it 51 | captureTimeout: 60000 52 | 53 | # Continuous Integration mode 54 | # if true, it capture browsers, run tests and exit 55 | singleRun: false 56 | -------------------------------------------------------------------------------- /test/e2e/dojo/karma.conf.ignore.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | // base path, that will be used to resolve files and exclude 4 | basePath: '', 5 | 6 | frameworks: ['jasmine', 'dojo'], 7 | 8 | // list of files / patterns to load in the browser 9 | files: [ 10 | 'main.js', 11 | 12 | // all the sources, tests 13 | {pattern: '*.js', included: false} 14 | ], 15 | 16 | 17 | // test results reporter to use 18 | // possible values: dots || progress 19 | reporters: ['dots'], 20 | 21 | 22 | // web server port 23 | port: 9876, 24 | 25 | 26 | // cli runner port 27 | runnerPort: 9100, 28 | 29 | 30 | // enable / disable colors in the output (reporters and logs) 31 | colors: true, 32 | 33 | 34 | // level of logging 35 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 36 | logLevel: config.LOG_INFO, 37 | 38 | 39 | // enable / disable watching file and executing tests whenever any file changes 40 | autoWatch: true, 41 | 42 | 43 | // Start these browsers, currently available: 44 | // - Chrome 45 | // - ChromeCanary 46 | // - Firefox 47 | // - Opera 48 | // - Safari 49 | // - PhantomJS 50 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 51 | 52 | 53 | // Continuous Integration mode 54 | // if true, it capture browsers, run tests and exit 55 | singleRun: false, 56 | 57 | plugins: [ 58 | 'karma-dojo', 59 | 'karma-jasmine', 60 | 'karma-chrome-launcher', 61 | 'karma-firefox-launcher' 62 | ] 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /lib/executor.js: -------------------------------------------------------------------------------- 1 | var log = require('./logger').create(); 2 | 3 | var Executor = function(capturedBrowsers, config, emitter) { 4 | var self = this; 5 | var executionScheduled = false; 6 | var pendingCount = 0; 7 | var runningBrowsers; 8 | 9 | var schedule = function() { 10 | var nonReady = []; 11 | 12 | if (!capturedBrowsers.length) { 13 | log.warn('No captured browser, open http://%s:%s%s', config.hostname, config.port, 14 | config.urlRoot); 15 | return false; 16 | } 17 | 18 | if (capturedBrowsers.areAllReady(nonReady)) { 19 | log.debug('All browsers are ready, executing'); 20 | executionScheduled = false; 21 | capturedBrowsers.setAllIsReadyTo(false); 22 | capturedBrowsers.clearResults(); 23 | pendingCount = capturedBrowsers.length; 24 | runningBrowsers = capturedBrowsers.clone(); 25 | emitter.emit('run_start', runningBrowsers); 26 | self.socketIoSockets.emit('execute', config.client); 27 | return true; 28 | } 29 | 30 | log.info('Delaying execution, these browsers are not ready: ' + nonReady.join(', ')); 31 | executionScheduled = true; 32 | return false; 33 | }; 34 | 35 | this.schedule = schedule; 36 | 37 | this.onRunComplete = function() { 38 | if (executionScheduled) { 39 | schedule(); 40 | } 41 | }; 42 | 43 | this.onBrowserComplete = function() { 44 | pendingCount--; 45 | 46 | if (!pendingCount) { 47 | emitter.emit('run_complete', runningBrowsers, runningBrowsers.getResults()); 48 | } 49 | }; 50 | 51 | // bind all the events 52 | emitter.bind(this); 53 | }; 54 | 55 | 56 | module.exports = Executor; 57 | -------------------------------------------------------------------------------- /test/e2e/requirejs/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Jul 26 2012 14:35:23 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | frameworks: ['jasmine', 'requirejs'], 10 | 11 | // list of files / patterns to load in the browser 12 | files: [ 13 | 'main.js', 14 | 15 | // all the sources, tests 16 | {pattern: '*.js', included: false} 17 | ], 18 | 19 | 20 | // test results reporter to use 21 | // possible values: dots || progress 22 | reporters: ['dots'], 23 | 24 | 25 | // web server port 26 | port: 9876, 27 | 28 | 29 | // enable / disable colors in the output (reporters and logs) 30 | colors: true, 31 | 32 | 33 | // level of logging 34 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 35 | logLevel: config.LOG_INFO, 36 | 37 | 38 | // enable / disable watching file and executing tests whenever any file changes 39 | autoWatch: true, 40 | 41 | 42 | // Start these browsers, currently available: 43 | // - Chrome 44 | // - ChromeCanary 45 | // - Firefox 46 | // - Opera 47 | // - Safari 48 | // - PhantomJS 49 | browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], 50 | 51 | 52 | // Continuous Integration mode 53 | // if true, it capture browsers, run tests and exit 54 | singleRun: false, 55 | 56 | plugins: [ 57 | 'karma-requirejs', 58 | 'karma-jasmine', 59 | 'karma-chrome-launcher', 60 | 'karma-firefox-launcher' 61 | ], 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /test/unit/executor.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'executor', -> 2 | Browser = require('../../lib/browser').Browser 3 | BrowserCollection = require('../../lib/browser').Collection 4 | EventEmitter = require('../../lib/events').EventEmitter 5 | Executor = require '../../lib/executor' 6 | 7 | executor = emitter = capturedBrowsers = config = spy = null 8 | 9 | beforeEach -> 10 | config = {client: {}} 11 | emitter = new EventEmitter 12 | capturedBrowsers = new BrowserCollection emitter 13 | capturedBrowsers.add new Browser 14 | executor = new Executor capturedBrowsers, config, emitter 15 | executor.socketIoSockets = new EventEmitter 16 | 17 | spy = 18 | onRunStart: -> null 19 | onSocketsExecute: -> null 20 | 21 | sinon.spy spy, 'onRunStart' 22 | sinon.spy spy, 'onSocketsExecute' 23 | 24 | emitter.on 'run_start', spy.onRunStart 25 | executor.socketIoSockets.on 'execute', spy.onSocketsExecute 26 | 27 | 28 | it 'should start the run and pass client config', -> 29 | capturedBrowsers.areAllReady = -> true 30 | 31 | executor.schedule() 32 | expect(spy.onRunStart).to.have.been.called 33 | expect(spy.onSocketsExecute).to.have.been.calledWith config.client 34 | 35 | 36 | it 'should wait for all browsers to finish', -> 37 | capturedBrowsers.areAllReady = -> false 38 | 39 | # they are not ready yet 40 | executor.schedule() 41 | expect(spy.onRunStart).not.to.have.been.called 42 | expect(spy.onSocketsExecute).not.to.have.been.called 43 | 44 | capturedBrowsers.areAllReady = -> true 45 | emitter.emit 'run_complete' 46 | expect(spy.onRunStart).to.have.been.called 47 | expect(spy.onSocketsExecute).to.have.been.called 48 | -------------------------------------------------------------------------------- /config.tpl.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on %DATE% 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '%BASE_PATH%', 9 | 10 | 11 | // frameworks to use 12 | frameworks: [%FRAMEWORKS%], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | %FILES% 18 | ], 19 | 20 | 21 | // list of files to exclude 22 | exclude: [ 23 | %EXCLUDE% 24 | ], 25 | 26 | 27 | // test results reporter to use 28 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 29 | reporters: ['progress'], 30 | 31 | 32 | // web server port 33 | port: 9876, 34 | 35 | 36 | // enable / disable colors in the output (reporters and logs) 37 | colors: true, 38 | 39 | 40 | // level of logging 41 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 42 | logLevel: config.LOG_INFO, 43 | 44 | 45 | // enable / disable watching file and executing tests whenever any file changes 46 | autoWatch: %AUTO_WATCH%, 47 | 48 | 49 | // Start these browsers, currently available: 50 | // - Chrome 51 | // - ChromeCanary 52 | // - Firefox 53 | // - Opera 54 | // - Safari (only Mac) 55 | // - PhantomJS 56 | // - IE (only Windows) 57 | browsers: [%BROWSERS%], 58 | 59 | 60 | // If browser does not capture in given timeout [ms], kill it 61 | captureTimeout: 60000, 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, it capture browsers, run tests and exit 66 | singleRun: false 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var helper = require('./helper'); 5 | var log = require('./logger').create('plugin'); 6 | 7 | 8 | exports.resolve = function(plugins) { 9 | var modules = []; 10 | 11 | var requirePlugin = function(name) { 12 | log.debug('Loading plugin %s.', name); 13 | try { 14 | modules.push(require(name)); 15 | } catch (e) { 16 | if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(name) !== -1) { 17 | log.warn('Cannot find plugin "%s".\n Did you forget to install it ?\n' + 18 | ' npm install %s --save-dev', name, name); 19 | } else { 20 | log.warn('Error during loading "%s" plugin:\n %s', name, e.message); 21 | } 22 | } 23 | }; 24 | 25 | plugins.forEach(function(plugin) { 26 | if (helper.isString(plugin)) { 27 | if (plugin.indexOf('*') !== -1) { 28 | var pluginDirectory = path.normalize(__dirname + '/../..'); 29 | var regexp = new RegExp('^' + plugin.replace('*', '.*')); 30 | 31 | log.debug('Loading %s from %s', plugin, pluginDirectory); 32 | fs.readdirSync(pluginDirectory).filter(function(pluginName) { 33 | return regexp.test(pluginName); 34 | }).forEach(function(pluginName) { 35 | requirePlugin(pluginDirectory + '/' + pluginName); 36 | }); 37 | } else { 38 | requirePlugin(plugin); 39 | } 40 | } else if (helper.isObject(plugin)) { 41 | log.debug('Loading inlined plugin (defining %s).', Object.keys(plugin).join(', ')); 42 | modules.push(plugin); 43 | } else { 44 | log.warn('Invalid plugin %s', plugin); 45 | } 46 | }); 47 | 48 | return modules; 49 | }; 50 | -------------------------------------------------------------------------------- /karma-completion.sh: -------------------------------------------------------------------------------- 1 | ###-begin-karma-completion-### 2 | # 3 | # karma command completion script 4 | # This is stolen from NPM. Thanks @isaac! 5 | # 6 | # Installation: karma completion >> ~/.bashrc (or ~/.zshrc) 7 | # Or, maybe: karma completion > /usr/local/etc/bash_completion.d/npm 8 | # 9 | 10 | if type complete &>/dev/null; then 11 | __karma_completion () { 12 | local si="$IFS" 13 | IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \ 14 | COMP_LINE="$COMP_LINE" \ 15 | COMP_POINT="$COMP_POINT" \ 16 | karma completion -- "${COMP_WORDS[@]}" \ 17 | 2>/dev/null)) || return $? 18 | IFS="$si" 19 | } 20 | complete -F __karma_completion karma 21 | elif type compdef &>/dev/null; then 22 | __karma_completion() { 23 | si=$IFS 24 | compadd -- $(COMP_CWORD=$((CURRENT-1)) \ 25 | COMP_LINE=$BUFFER \ 26 | COMP_POINT=0 \ 27 | karma completion -- "${words[@]}" \ 28 | 2>/dev/null) 29 | IFS=$si 30 | } 31 | compdef __karma_completion karma 32 | elif type compctl &>/dev/null; then 33 | __karma_completion () { 34 | local cword line point words si 35 | read -Ac words 36 | read -cn cword 37 | let cword-=1 38 | read -l line 39 | read -ln point 40 | si="$IFS" 41 | IFS=$'\n' reply=($(COMP_CWORD="$cword" \ 42 | COMP_LINE="$line" \ 43 | COMP_POINT="$point" \ 44 | karma completion -- "${words[@]}" \ 45 | 2>/dev/null)) || return $? 46 | IFS="$si" 47 | } 48 | compctl -K __karma_completion karma 49 | fi 50 | ###-end-karma-completion-### 51 | -------------------------------------------------------------------------------- /test/unit/runner.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/runner.js module 3 | #============================================================================== 4 | describe 'runner', -> 5 | loadFile = require('mocks').loadFile 6 | constant = require '../../lib/constants' 7 | m = null 8 | 9 | beforeEach -> 10 | m = loadFile __dirname + '/../../lib/runner.js' 11 | 12 | #============================================================================ 13 | # runner.parseExitCode 14 | #============================================================================ 15 | describe 'parseExitCode', -> 16 | EXIT = constant.EXIT_CODE 17 | 18 | it 'should return 0 exit code if present in the buffer', -> 19 | expect(m.parseExitCode new Buffer 'something\nfake' + EXIT + '0').to.equal 0 20 | 21 | 22 | it 'should null the exit code part of the buffer', -> 23 | buffer = new Buffer 'some' + EXIT + '1' 24 | m.parseExitCode buffer 25 | 26 | expect(buffer.toString()).to.equal 'some\0\0\0\0\0\0' 27 | 28 | 29 | it 'should not touch buffer without exit code and return default', -> 30 | msg = 'some nice \n messgae {}' 31 | buffer = new Buffer msg 32 | code = m.parseExitCode buffer, 10 33 | 34 | expect(buffer.toString()).to.equal msg 35 | expect(code).to.equal 10 36 | 37 | 38 | it 'should not slice buffer if smaller than exit code msg', -> 39 | # regression 40 | fakeBuffer = {length: 1, slice: -> null} 41 | sinon.stub fakeBuffer, 'slice' 42 | 43 | code = m.parseExitCode fakeBuffer, 10 44 | expect(fakeBuffer.slice).not.to.have.been.called 45 | 46 | 47 | it 'should parse any single digit exit code', -> 48 | expect(m.parseExitCode new Buffer 'something\nfake' + EXIT + '1').to.equal 1 49 | expect(m.parseExitCode new Buffer 'something\nfake' + EXIT + '7').to.equal 7 50 | -------------------------------------------------------------------------------- /lib/middleware/source-files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Source Files middleware is responsible for serving all the source files under the test. 3 | */ 4 | 5 | var querystring = require('querystring'); 6 | var common = require('./common'); 7 | var pause = require('connect').utils.pause; 8 | 9 | 10 | var findByPath = function(files, path) { 11 | for (var i = 0; i < files.length; i++) { 12 | if (files[i].path === path) { 13 | return files[i]; 14 | } 15 | } 16 | 17 | return null; 18 | }; 19 | 20 | 21 | var createSourceFilesMiddleware = function(filesPromise, serveFile, 22 | /* config.basePath */ basePath) { 23 | 24 | return function(request, response, next) { 25 | var requestedFilePath = querystring.unescape(request.url) 26 | .replace(/\?.*/, '') 27 | .replace(/^\/absolute/, '') 28 | .replace(/^\/base/, basePath); 29 | 30 | // Need to pause the request because of proxying, see: 31 | // https://groups.google.com/forum/#!topic/q-continuum/xr8znxc_K5E/discussion 32 | // TODO(vojta): remove once we don't care about Node 0.8 33 | var pausedRequest = pause(request); 34 | 35 | return filesPromise.then(function(files) { 36 | // TODO(vojta): change served to be a map rather then an array 37 | var file = findByPath(files.served, requestedFilePath); 38 | 39 | if (file) { 40 | serveFile(file.contentPath, response, function() { 41 | if (/\?\d+/.test(request.url)) { 42 | // files with timestamps - cache one year, rely on timestamps 43 | common.setHeavyCacheHeaders(response); 44 | } else { 45 | // without timestamps - no cache (debug) 46 | common.setNoCacheHeaders(response); 47 | } 48 | }); 49 | } else { 50 | next(); 51 | } 52 | 53 | pausedRequest.resume(); 54 | }); 55 | }; 56 | }; 57 | 58 | 59 | // PUBLIC API 60 | exports.create = createSourceFilesMiddleware; 61 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | var constant = require('./constants'); 4 | var helper = require('./helper'); 5 | var cfg = require('./config'); 6 | 7 | 8 | var parseExitCode = function(buffer, defaultCode) { 9 | var tailPos = buffer.length - Buffer.byteLength(constant.EXIT_CODE) - 1; 10 | 11 | if (tailPos < 0) { 12 | return defaultCode; 13 | } 14 | 15 | // tail buffer which might contain the message 16 | var tail = buffer.slice(tailPos); 17 | var tailStr = tail.toString(); 18 | if (tailStr.substr(0, tailStr.length - 1) === constant.EXIT_CODE) { 19 | tail.fill('\x00'); 20 | return parseInt(tailStr.substr(-1), 10); 21 | } 22 | 23 | return defaultCode; 24 | }; 25 | 26 | 27 | // TODO(vojta): read config file (port, host, urlRoot) 28 | exports.run = function(config, done) { 29 | done = helper.isFunction(done) ? done : process.exit; 30 | config = cfg.parseConfig(config.configFile, config); 31 | 32 | var exitCode = 1; 33 | var options = { 34 | hostname: config.hostname, 35 | path: config.urlRoot + 'run', 36 | port: config.port, 37 | method: 'POST', 38 | headers: { 39 | 'Content-Type': 'application/json' 40 | } 41 | }; 42 | 43 | var request = http.request(options, function(response) { 44 | response.on('data', function(buffer) { 45 | exitCode = parseExitCode(buffer, exitCode); 46 | process.stdout.write(buffer); 47 | }); 48 | 49 | response.on('end', function() { 50 | done(exitCode); 51 | }); 52 | }); 53 | 54 | request.on('error', function(e) { 55 | if (e.code === 'ECONNREFUSED') { 56 | console.error('There is no server listening on port %d', options.port); 57 | done(1); 58 | } else { 59 | throw e; 60 | } 61 | }); 62 | 63 | request.end(JSON.stringify({ 64 | args: config.clientArgs, 65 | removedFiles: config.removedFiles, 66 | changedFiles: config.changedFiles, 67 | addedFiles: config.addedFiles, 68 | refresh: config.refresh 69 | })); 70 | }; 71 | -------------------------------------------------------------------------------- /docs/intro/01-installation.md: -------------------------------------------------------------------------------- 1 | Karma runs on [Node.js] and is available via [NPM]. 2 | 3 | ## Requirements 4 | 5 |
    6 |
  1. 7 |

    Node.js

    8 |

    9 | There are installers for both Mac and Windows. On Linux, we recommend using 10 | NVM. 11 |

    12 |
  2. 13 |
  3. 14 |

    Node Package Manager (NPM)

    15 |

    16 | NPM is a package manager for Node.js which is used to install Karma. This should 17 | automatically be installed when Node.js is installed, but if not then please install 18 | it afterwards. 19 |

    20 |
  4. 21 |
22 | 23 | ## Global "System-Wide" Installation 24 | This is the recommended approach to installing and making use of Karma. It will install Karma into your global 25 | `node_modules` directory and create a symlink in your system path, so that you can run the `karma` 26 | command from any directory. This means that the `karma` command (which is the central command that Karma uses to run 27 | tests) can be executed anywhere via the command line. 28 | 29 | The following command will install Karma globally: 30 | 31 | ```bash 32 | $ npm install -g karma 33 | ``` 34 | 35 | Please note, that the `karma` command will always look for a locally installed instance of Karma first and 36 | before resorting to a global install and, if present, then the local version will be utilized. 37 | This allows you to use different version of Karma per project. 38 | 39 | ## Local Installation 40 | A local installation will install Karma into your current directory's `node_modules`. 41 | 42 | ```bash 43 | $ npm install karma --save-dev 44 | ``` 45 | 46 | The karma command can now be also executed directly from the node_modules directory: 47 | 48 | ```bash 49 | $ ./node_modules/.bin/karma 50 | ``` 51 | 52 | 53 | [Node.js]: http://nodejs.org/ 54 | [NPM]: npmjs.org/package/karma 55 | [NVM]: https://github.com/creationix/nvm 56 | -------------------------------------------------------------------------------- /lib/middleware/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module contains some common helpers shared between middlewares 3 | */ 4 | 5 | var mime = require('mime'); 6 | var log = require('../logger').create('web-server'); 7 | 8 | var PromiseContainer = function() { 9 | var promise; 10 | 11 | this.then = function(success, error) { 12 | return promise.then(success, error); 13 | }; 14 | 15 | this.set = function(newPromise) { 16 | promise = newPromise; 17 | }; 18 | }; 19 | 20 | 21 | var serve404 = function(response, path) { 22 | log.warn('404: ' + path); 23 | response.writeHead(404); 24 | return response.end('NOT FOUND'); 25 | }; 26 | 27 | 28 | var createServeFile = function(fs, directory) { 29 | return function(filepath, response, transform) { 30 | if (directory) { 31 | filepath = directory + filepath; 32 | } 33 | 34 | return fs.readFile(filepath, function(error, data) { 35 | if (error) { 36 | return serve404(response, filepath); 37 | } 38 | 39 | response.setHeader('Content-Type', mime.lookup(filepath, 'text/plain')); 40 | 41 | // call custom transform fn to transform the data 42 | var responseData = transform && transform(data.toString()) || data; 43 | 44 | response.writeHead(200); 45 | 46 | log.debug('serving: ' + filepath); 47 | return response.end(responseData); 48 | }); 49 | }; 50 | }; 51 | 52 | 53 | var setNoCacheHeaders = function(response) { 54 | response.setHeader('Cache-Control', 'no-cache'); 55 | response.setHeader('Pragma', 'no-cache'); 56 | response.setHeader('Expires', (new Date(0)).toString()); 57 | }; 58 | 59 | 60 | var setHeavyCacheHeaders = function(response) { 61 | response.setHeader('Cache-Control', ['public', 'max-age=31536000']); 62 | }; 63 | 64 | 65 | // PUBLIC API 66 | exports.PromiseContainer = PromiseContainer; 67 | exports.createServeFile = createServeFile; 68 | exports.setNoCacheHeaders = setNoCacheHeaders; 69 | exports.setHeavyCacheHeaders = setHeavyCacheHeaders; 70 | exports.serve404 = serve404; 71 | -------------------------------------------------------------------------------- /test/unit/completion.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/completion.js module 3 | #============================================================================== 4 | describe 'completion', -> 5 | c = require '../../lib/completion' 6 | completion = null 7 | 8 | mockEnv = (line) -> 9 | words = line.split ' ' 10 | 11 | words: words 12 | count: words.length 13 | last: words[words.length - 1] 14 | prev: words[words.length - 2] 15 | 16 | beforeEach -> 17 | sinon.stub console, 'log', (msg) -> completion.push msg 18 | completion = [] 19 | 20 | describe 'opositeWord', -> 21 | 22 | it 'should handle --no-x args', -> 23 | expect(c.opositeWord '--no-single-run').to.equal '--single-run' 24 | 25 | 26 | it 'should handle --x args', -> 27 | expect(c.opositeWord '--browsers').to.equal '--no-browsers' 28 | 29 | 30 | it 'should ignore args without --', -> 31 | expect(c.opositeWord 'start').to.equal null 32 | 33 | 34 | describe 'sendCompletion', -> 35 | 36 | it 'should filter only words matching last typed partial', -> 37 | c.sendCompletion ['start', 'init', 'run'], mockEnv 'in' 38 | expect(completion).to.deep.equal ['init'] 39 | 40 | 41 | it 'should filter out already used words/args', -> 42 | c.sendCompletion ['--single-run', '--port', '--xxx'], mockEnv 'start --single-run ' 43 | expect(completion).to.deep.equal ['--port', '--xxx'] 44 | 45 | 46 | it 'should filter out already used oposite words', -> 47 | c.sendCompletion ['--auto-watch', '--port'], mockEnv 'start --no-auto-watch ' 48 | expect(completion).to.deep.equal ['--port'] 49 | 50 | 51 | describe 'complete', -> 52 | 53 | it 'should complete the basic commands', -> 54 | c.complete mockEnv '' 55 | expect(completion).to.deep.equal ['start', 'init', 'run'] 56 | 57 | completion.length = 0 # reset 58 | c.complete mockEnv 's' 59 | expect(completion).to.deep.equal ['start'] 60 | -------------------------------------------------------------------------------- /static/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Karma DEBUG RUNNER 10 | 11 | 12 | 13 | 14 | 15 | 19 | 42 | 43 | %SCRIPTS% 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/plus/04-semaphore.md: -------------------------------------------------------------------------------- 1 | pageTitle: Semaphore CI 2 | menuTitle: Semaphore CI 3 | 4 | [Semaphore] is a popular continuous integration service for 5 | Ruby developers that [integrates] with [GitHub]. It also includes 6 | [Node.js], [PhantomJS] and headless Firefox in its [platform], 7 | making it fit for testing JavaScript applications as well. 8 | This article assumes you already have a Semaphore account. 9 | 10 | ## Configure Your Project 11 | 12 | If you do not already have a `package.json` in your project root, 13 | create one now. This will both document your configuration and 14 | make it easy to run your tests. Here's an example: 15 | 16 | ```javascript 17 | // ...snip... 18 | 'devDependencies': { 19 | 'karma': '~0.10' 20 | }, 21 | // ...snip... 22 | 'scripts': { 23 | 'test': './node_modules/.bin/karma start --single-run --browsers PhantomJS' 24 | } 25 | // ...snip... 26 | ``` 27 | 28 | Another option is to use Firefox as your test browser. To do this, change 29 | the last part to: 30 | 31 | ```javascript 32 | 'scripts': { 33 | 'test': './node_modules/.bin/karma start --single-run --browsers Firefox' 34 | } 35 | ``` 36 | 37 | Now running `npm test` within your project will run your tests with Karma. 38 | 39 | ## Add Your Project to Semaphore 40 | 41 | Follow the process as shown in the [screencast] on the Semaphore homepage. 42 | 43 | After the analysis is finished, ignore the Ruby version Semaphore has set 44 | for you, choose to customize your build commands and use these: 45 | 46 | ```bash 47 | npm install 48 | npm test 49 | ``` 50 | 51 | That's it - proceed to your first build. In case you're using Firefox as 52 | your test browser, Semaphore will automatically run it in a virtual screen 53 | during your builds. 54 | 55 | Also, if necessary, build commands can be further [customized] at any time. 56 | 57 | 58 | [screencast]: https://semaphoreapp.com/ 59 | [Semaphore]: https://semaphoreapp.com 60 | [integrates]: https://semaphoreapp.com/features 61 | [Github]: https://github.com/ 62 | [Node.js]: http://nodejs.org 63 | [PhantomJS]: http://phantomjs.org/ 64 | [platform]: http://docs.semaphoreapp.com/version-information 65 | [customized]: http://docs.semaphoreapp.com/custom-build-commands 66 | -------------------------------------------------------------------------------- /docs/dev/03-contributing.md: -------------------------------------------------------------------------------- 1 | If you are thinking about making Karma better, or you just want to hack on it, that’s great! 2 | Here are some tips to get you started. 3 | 4 | ## Getting Started 5 | 6 | * Make sure you have a [GitHub account](https://github.com/signup/free) 7 | * [Submit](https://github.com/karma-runner/karma/issues/new) a ticket for your issue, assuming one does not 8 | already exist. 9 | * Clearly describe the issue including steps to reproduce when it is a bug. 10 | * Make sure you fill in the earliest version that you know has the issue. 11 | * Fork the repository on GitHub 12 | 13 | ## Making Changes 14 | * Clone your fork 15 | ```bash 16 | $ git clone git@github.com:/karma.git 17 | ``` 18 | * Init your workspace 19 | 20 | ```bash 21 | $ ./scripts/init-dev-env.js 22 | ``` 23 | 24 | * Checkout a new branch (usually based on `master`) and name it accordingly to what 25 | you intend to do 26 | * Features get the prefix `feature-` 27 | * Bug fixes get the prefix `fix-` 28 | * Improvements to the documentation get the prefix `docs-` 29 | 30 | ## Testing and Building 31 | Run the tests via 32 | ```bash 33 | # All tests 34 | $ grunt test 35 | 36 | $ grunt test:unit 37 | $ grunt test:e2e 38 | $ grunt test:client 39 | ``` 40 | Lint the files via 41 | ```bash 42 | $ grunt lint 43 | ``` 44 | Build the project via 45 | ```bash 46 | $ grunt build 47 | ``` 48 | The default task, just calling `grunt` will run `build lint test`. 49 | 50 | If grunt fails, make sure grunt-0.4x is installed: https://github.com/gruntjs/grunt/wiki/Getting-started. 51 | 52 | ## Submitting Changes 53 | 54 | * One branch per feature/fix 55 | * Follow http://nodeguide.com/style.html (with exception of 100 characters per line) 56 | * Please follow [commit message conventions]. 57 | * Send a pull request to the `master` branch. 58 | 59 | 60 | ## Additional Resources 61 | 62 | * [Issue tracker](https://github.com/karma-runner/karma/issues) 63 | * [Mailing List](https://groups.google.com/forum/#!forum/karma-users) 64 | * [General GitHub documentation](http://help.github.com/) 65 | * [GitHub pull request documentation](http://help.github.com/send-pull-requests/) 66 | * [@JsKarma](http://twitter.com/JsKarma) 67 | 68 | [commit message conventions]: git-commit-msg.html 69 | -------------------------------------------------------------------------------- /docs/config/02-files.md: -------------------------------------------------------------------------------- 1 | **The `files` array determines which files are included in the browser and which files are watched and served by Karma.** 2 | 3 | 4 | ## Pattern matching and `basePath` 5 | - All of the relative patterns will get resolved to `basePath` first. 6 | - If the `basePath` is a relative path, it gets resolved to the 7 | directory where the configuration file is. 8 | - Eventually, all the patterns will get resolved into files using 9 | [glob], so you can use expressions like `test/unit/**/*.spec.js`. 10 | 11 | ## Ordering 12 | - The order of patterns determines the order of files in which they 13 | are included in the browser. 14 | - Multiple files matching a single pattern are sorted alphabetically. 15 | - Each file is included exactly once. If multiple patterns match the 16 | same file, it's included as if it only matched the first pattern. 17 | 18 | ## Included, served, watched 19 | Each pattern is either a simple string or an object with four properties: 20 | 21 | ### `pattern` 22 | * **Description.** The pattern to use for matching. This property is mandatory. 23 | 24 | ### `watched` 25 | * **Type.** Boolean 26 | * **Default.** `true` 27 | * **Description.** If `autoWatch` is `true` all files that have set `watched` to true will be 28 | watched for changes. 29 | 30 | ### `included` 31 | * **Type.** Boolean 32 | * **Default.** `true` 33 | * **Description.** Should the files be included in the browser using 34 | ` 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /docs/intro/02-configuration.md: -------------------------------------------------------------------------------- 1 | In order to serve you well, Karma needs to know about your project in order to test it 2 | and this is done via a configuration file. This page explains how to create such a configuration file. 3 | 4 | See [configuration file docs] for more information about the syntax and the available options. 5 | 6 | ## Generating the config file 7 | 8 | The configuration file can be generated using `karma init`: 9 | ```bash 10 | $ karma init my.conf.js 11 | 12 | Which testing framework do you want to use ? 13 | Press tab to list possible options. Enter to move to the next question. 14 | > jasmine 15 | 16 | Do you want to use Require.js ? 17 | This will add Require.js plugin. 18 | Press tab to list possible options. Enter to move to the next question. 19 | > no 20 | 21 | Do you want to capture a browser automatically ? 22 | Press tab to list possible options. Enter empty string to move to the next question. 23 | > Chrome 24 | > Firefox 25 | > 26 | 27 | What is the location of your source and test files ? 28 | You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js". 29 | Enter empty string to move to the next question. 30 | > *.js 31 | > test/**/*.js 32 | > 33 | 34 | Should any of the files included by the previous patterns be excluded ? 35 | You can use glob patterns, eg. "**/*.swp". 36 | Enter empty string to move to the next question. 37 | > 38 | 39 | Do you want Karma to watch all the files and run the tests on change ? 40 | Press tab to list possible options. 41 | > yes 42 | 43 | Config file generated at "/Users/vojta/Code/karma/my.conf.js". 44 | ``` 45 | 46 | The configuration file can be written in CoffeeScript as well. 47 | In fact, if you execute `karma run` with a `.coffee` filename extension, it will generate a CoffeeScript file. 48 | 49 | Of course, you can write the config file by hand or copy paste it from another project ;-) 50 | 51 | ## Starting Karma 52 | When starting Karma, the configuration file path can be passed in as the first argument. 53 | 54 | By default, Karma will look for `karma.conf.js` in the current directory. 55 | ```bash 56 | # Start Karma using your configuration 57 | $ karma start my.conf.js 58 | ``` 59 | 60 | For more detailed information about the Karma configuration file, such as available options and features, 61 | please read the [configuration file docs]. 62 | 63 | ## Command line arguments 64 | Some configurations, which are already present within the configuration file, can be overridden by specifying the configuration 65 | as a command line argument for when Karma is executed. 66 | 67 | ```bash 68 | karma start karma-conf.js --command-one --command-two 69 | ``` 70 | 71 | Try `karma start --help` if you want to see all available options. 72 | 73 | 74 | ## Using Grunt 75 | If you are using Grunt within your project, 76 | the [grunt-karma] plugin may be useful. 77 | The `grunt-karma` plugin allows you to place your Karma configurations directly within your `Gruntfile`. 78 | By doing so, the central `karma.conf.js` is no longer required. However, this also means that Karma must also be run as a Grunt task. 79 | Please visit the [Grunt Karma Github Page](https://github.com/karma-runner/grunt-karma#running-tests) to learn more about how it's used. 80 | 81 | 82 | [configuration file docs]: ../config/configuration-file.html 83 | [Grunt]: http://gruntjs.com/ 84 | [grunt-karma]: https://github.com/karma-runner/grunt-karma 85 | [grunt-karma-usage] https://github.com/karma-runner/grunt-karma#running-tests 86 | -------------------------------------------------------------------------------- /lib/middleware/proxy.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | var httpProxy = require('http-proxy'); 3 | 4 | var log = require('../logger').create('proxy'); 5 | 6 | 7 | var parseProxyConfig = function(proxies) { 8 | var proxyConfig = {}; 9 | var endsWithSlash = function(str) { 10 | return str.substr(-1) === '/'; 11 | }; 12 | 13 | if (!proxies) { 14 | return proxyConfig; 15 | } 16 | 17 | Object.keys(proxies).forEach(function(proxyPath) { 18 | var proxyUrl = proxies[proxyPath]; 19 | var proxyDetails = url.parse(proxyUrl); 20 | var pathname = proxyDetails.pathname; 21 | 22 | // normalize the proxies config 23 | // should we move this to lib/config.js ? 24 | if (endsWithSlash(proxyPath) && !endsWithSlash(proxyUrl)) { 25 | log.warn('proxy "%s" normalized to "%s"', proxyUrl, proxyUrl + '/'); 26 | proxyUrl += '/'; 27 | } 28 | 29 | if (!endsWithSlash(proxyPath) && endsWithSlash(proxyUrl)) { 30 | log.warn('proxy "%s" normalized to "%s"', proxyPath, proxyPath + '/'); 31 | proxyPath += '/'; 32 | } 33 | 34 | if (pathname === '/' && !endsWithSlash(proxyUrl)) { 35 | pathname = ''; 36 | } 37 | 38 | proxyConfig[proxyPath] = { 39 | host: proxyDetails.hostname, 40 | port: proxyDetails.port, 41 | baseProxyUrl: pathname, 42 | https: proxyDetails.protocol === 'https:' 43 | }; 44 | 45 | if (!proxyConfig[proxyPath].port) { 46 | proxyConfig[proxyPath].port = proxyConfig[proxyPath].https ? '443' : '80'; 47 | } 48 | }); 49 | 50 | return proxyConfig; 51 | }; 52 | 53 | 54 | /** 55 | * Returns a handler which understands the proxies and its redirects, along with the proxy to use 56 | * @param proxy A http-proxy.RoutingProxy object with the proxyRequest method 57 | * @param proxies a map of routes to proxy url 58 | * @return {Function} handler function 59 | */ 60 | var createProxyHandler = function(proxy, proxyConfig, proxyValidateSSL) { 61 | var proxies = parseProxyConfig(proxyConfig); 62 | var proxiesList = Object.keys(proxies).sort().reverse(); 63 | 64 | if (!proxiesList.length) { 65 | return function(request, response, next) { 66 | return next(); 67 | }; 68 | } 69 | proxy.on('proxyError', function(err, req) { 70 | if (err.code === 'ECONNRESET' && req.socket.destroyed) { 71 | log.debug('failed to proxy %s (browser hung up the socket)', req.url); 72 | } else { 73 | log.warn('failed to proxy %s (%s)', req.url, err); 74 | } 75 | }); 76 | 77 | return function(request, response, next) { 78 | for (var i = 0; i < proxiesList.length; i++) { 79 | if (request.url.indexOf(proxiesList[i]) === 0) { 80 | var proxiedUrl = proxies[proxiesList[i]]; 81 | 82 | log.debug('proxying request - %s to %s:%s', request.url, proxiedUrl.host, proxiedUrl.port); 83 | request.url = request.url.replace(proxiesList[i], proxiedUrl.baseProxyUrl); 84 | proxy.proxyRequest(request, response, { 85 | host: proxiedUrl.host, 86 | port: proxiedUrl.port, 87 | target: { https: proxiedUrl.https, rejectUnauthorized: proxyValidateSSL } 88 | }); 89 | return; 90 | } 91 | } 92 | 93 | return next(); 94 | }; 95 | }; 96 | 97 | 98 | exports.create = function(/* config.proxies */ proxies, /* config.proxyValidateSSL */ validateSSL) { 99 | return createProxyHandler(new httpProxy.RoutingProxy({changeOrigin: true}), proxies, validateSSL); 100 | }; 101 | -------------------------------------------------------------------------------- /test/unit/events.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/events.js module 3 | #============================================================================== 4 | describe 'events', -> 5 | e = require '../../lib/events' 6 | emitter = null 7 | 8 | beforeEach -> 9 | emitter = new e.EventEmitter 10 | 11 | #============================================================================ 12 | # events.EventEmitter 13 | #============================================================================ 14 | describe 'EventEmitter', -> 15 | 16 | it 'should emit events', -> 17 | spy = sinon.spy() 18 | 19 | emitter.on 'abc', spy 20 | emitter.emit 'abc' 21 | expect(spy).to.have.been.called 22 | 23 | 24 | #========================================================================== 25 | # events.EventEmitter.bind() 26 | #========================================================================== 27 | describe 'bind', -> 28 | object = null 29 | 30 | beforeEach -> 31 | object = sinon.stub 32 | onFoo: -> 33 | onFooBar: -> 34 | foo: -> 35 | bar: -> 36 | emitter.bind object 37 | 38 | 39 | it 'should register all "on" methods to events', -> 40 | emitter.emit 'foo' 41 | expect(object.onFoo).to.have.been.called 42 | 43 | emitter.emit 'foo_bar' 44 | expect(object.onFooBar).to.have.been.called 45 | 46 | expect(object.foo).not.to.have.been.called 47 | expect(object.bar).not.to.have.been.called 48 | 49 | 50 | it 'should bind methods to the owner object', -> 51 | emitter.emit 'foo' 52 | emitter.emit 'foo_bar' 53 | 54 | expect(object.onFoo).to.have.always.been.calledOn object 55 | expect(object.onFooBar).to.have.always.been.calledOn object 56 | expect(object.foo).not.to.have.been.called 57 | expect(object.bar).not.to.have.been.called 58 | 59 | 60 | #========================================================================== 61 | # events.EventEmitter.emitAsync() 62 | #========================================================================== 63 | describe 'emitAsync', -> 64 | object = null 65 | 66 | beforeEach -> 67 | object = sinon.stub 68 | onFoo: -> 69 | onFooBar: -> 70 | foo: -> 71 | bar: -> 72 | emitter.bind object 73 | 74 | 75 | it 'should resolve the promise once all listeners are done', (done) -> 76 | callbacks = [] 77 | eventDone = sinon.spy() 78 | 79 | emitter.on 'a', (d) -> d() 80 | emitter.on 'a', (d) -> callbacks.push d 81 | emitter.on 'a', (d) -> callbacks.push d 82 | 83 | promise = emitter.emitAsync('a') 84 | 85 | expect(eventDone).not.to.have.been.called 86 | callbacks.pop()() 87 | expect(eventDone).not.to.have.been.called 88 | callbacks.pop()() 89 | 90 | promise.then -> 91 | eventDone() 92 | expect(eventDone).to.have.been.called 93 | done() 94 | 95 | 96 | #============================================================================ 97 | # events.bindAll 98 | #============================================================================ 99 | describe 'bindAll', -> 100 | 101 | it 'should take emitter as second argument', -> 102 | object = sinon.stub onFoo: -> 103 | 104 | e.bindAll object, emitter 105 | emitter.emit 'foo' 106 | emitter.emit 'bar' 107 | 108 | expect(object.onFoo).to.have.been.called 109 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | If you are thinking about making Karma better, or you just want to hack on it, that’s great! Here 3 | are some tips to get you started. 4 | 5 | ## Getting Started 6 | 7 | * Make sure you have a [GitHub account](https://github.com/signup/free) 8 | * Consider [submiting a ticket](https://github.com/karma-runner/karma/issues/new) for your issue, 9 | assuming one does not already exist. 10 | * Clearly describe the issue including steps to reproduce when it is a bug. 11 | * Make sure you fill in the earliest version that you know has the issue. 12 | * For smaller tweaks and fixes that you plan to fix yourself and merge back soon, you may skip this step. 13 | When you submit your pull request, it will serve as your issue. 14 | * Fork the repository on GitHub 15 | 16 | ## Initial setup 17 | * Clone your fork. For these instructions, we assume you cloned into '~/github/karma'. 18 | * Install dependencies: 19 | 20 | ```bash 21 | $ cd ~/github/karma 22 | # install local dependencies 23 | $ npm install 24 | 25 | # install global dependencies 26 | $ npm install grunt-cli -g 27 | ``` 28 | 29 | * Ensure you have a stable working baseline for development. 30 | ```bash 31 | # This will run a full build and test pass. 32 | $ cd ~/github/karma 33 | $ grunt 34 | ``` 35 | On an unmodified 'master' branch, this command should always complete and report success. 36 | 37 | If `grunt fails: 38 | * Make sure grunt-0.4 is installed: http://gruntjs.com/getting-started 39 | * Review the open issues - perhaps this is a known problem that someone else has dealt with before. 40 | * File an issue (see above) 41 | 42 | * Run 'grunt init-dev-env'. This will install a git commit trigger that will ensure your commit messages 43 | follows the [Karma - Git Commit Msg Format Conventions] 44 | 45 | 46 | 47 | ## Making and Submitting Changes 48 | 49 | * Checkout a new branch (usually based on `master`) and name it accordingly to what 50 | you intend to do 51 | * Features get the prefix `feature-` 52 | * Bug fixes get the prefix `fix-` 53 | * Improvements to the documentation get the prefix `docs-` 54 | * Use one branch per feature/fix 55 | * Make your changes 56 | * Follow [node.js style guidelines](http://nodeguide.com/style.html) (with exception of 100 characters 57 | per line) 58 | * Add tests for your changes as (or before) you make them, if at all possible. 59 | We use coffee script for our tests. 60 | * Commit your changes 61 | * Follow the [Karma - Git Commit Msg Format Conventions] 62 | * Push your changes to your forked repository 63 | * Send a pull request to the `master` branch. 64 | * Before submitting, make sure the default 'grunt' command succeeds locally. 65 | * After submitting, TravisCI will pick up your changes and report results. 66 | * Make fixes and incorporate feedback, as needed. 67 | 68 | ## Build and Test Commands 69 | 70 | The default task, just calling `grunt`, will run `build jshint test`. You may also run tasks separately. 71 | 72 | Build the project: 73 | ```bash 74 | $ grunt build 75 | ``` 76 | Lint the files: 77 | ```bash 78 | $ grunt jshint 79 | ``` 80 | Run the tests: 81 | ```bash 82 | 83 | # All tests 84 | $ grunt test 85 | 86 | # Separate test suites 87 | $ grunt test:unit 88 | $ grunt test:e2e 89 | $ grunt test:client 90 | ``` 91 | 92 | # Additional Resources 93 | 94 | * [Issue tracker] 95 | * [Mailing List] 96 | * [General GitHub documentation] 97 | * [GitHub pull request documentation] 98 | * [@JsKarma] 99 | 100 | [Karma - Git Commit Msg Format Conventions]: http://karma-runner.github.io/0.8/dev/git-commit-msg.html 101 | [Issue tracker]: https://github.com/karma-runner/karma/issues 102 | [Mailing List]: https://groups.google.com/forum/#!forum/karma-users 103 | [General GitHub documentation]: http://help.github.com/ 104 | [GitHub pull request documentation]: http://help.github.com/send-pull-requests/ 105 | [@JsKarma]: http://twitter.com/JsKarma 106 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | # JS Hint options 2 | JSHINT_BROWSER = 3 | browser: true, 4 | es5: true, 5 | strict: false 6 | undef: false 7 | camelcase: false 8 | 9 | JSHINT_NODE = 10 | node: true, 11 | es5: true, 12 | strict: false 13 | 14 | module.exports = (grunt) -> 15 | 16 | # Project configuration. 17 | grunt.initConfig 18 | pkg: grunt.file.readJSON 'package.json' 19 | pkgFile: 'package.json' 20 | 21 | files: 22 | server: ['lib/**/*.js'] 23 | client: ['static/karma.src.js'] 24 | grunt: ['grunt.js', 'tasks/*.js'] 25 | scripts: ['scripts/*.js'] 26 | 27 | build: 28 | client: '<%= files.client %>' 29 | 30 | test: 31 | unit: 'simplemocha:unit' 32 | client: 'test/client/karma.conf.js' 33 | e2e: ['test/e2e/*/karma.conf.js', 'test/e2e/*/karma.conf.coffee'] 34 | 35 | watch: 36 | client: 37 | files: '<%= files.client %>' 38 | tasks: 'build:client' 39 | 40 | 41 | simplemocha: 42 | options: 43 | ui: 'bdd' 44 | reporter: 'dot' 45 | unit: 46 | src: [ 47 | 'test/unit/mocha-globals.coffee' 48 | 'test/unit/**/*.coffee' 49 | ] 50 | 51 | # JSHint options 52 | # http://www.jshint.com/options/ 53 | jshint: 54 | server: 55 | files: 56 | src: '<%= files.server %>' 57 | options: JSHINT_NODE 58 | grunt: 59 | files: 60 | src: '<%= files.grunt %>' 61 | options: JSHINT_NODE 62 | scripts: 63 | files: 64 | src: '<%= files.scripts %>' 65 | options: JSHINT_NODE 66 | client: 67 | files: 68 | src: '<%= files.client %>' 69 | options: JSHINT_BROWSER 70 | 71 | options: 72 | quotmark: 'single' 73 | bitwise: true 74 | indent: 2 75 | camelcase: true 76 | strict: true 77 | trailing: true 78 | curly: true 79 | eqeqeq: true 80 | immed: true 81 | latedef: true 82 | newcap: true 83 | noempty: true 84 | unused: true 85 | noarg: true 86 | sub: true 87 | undef: true 88 | maxdepth: 4 89 | maxlen: 100 90 | globals: {} 91 | 92 | # CoffeeLint options 93 | # http://www.coffeelint.org/#options 94 | coffeelint: 95 | unittests: files: src: ['test/unit/**/*.coffee'] 96 | taskstests: files: src: ['test/tasks/**/*.coffee'] 97 | options: 98 | max_line_length: 99 | value: 100 100 | 101 | 'npm-publish': 102 | options: 103 | requires: ['build'] 104 | abortIfDirty: true 105 | tag: -> 106 | minor = parseInt grunt.config('pkg.version').split('.')[1], 10 107 | if (minor % 2) then 'canary' else 'latest' 108 | 109 | 'npm-contributors': 110 | options: 111 | commitMessage: 'chore: update contributors' 112 | 113 | bump: 114 | options: 115 | updateConfigs: ['pkg'] 116 | commitFiles: ['package.json', 'CHANGELOG.md'] 117 | commitMessage: 'chore: release v%VERSION%' 118 | pushTo: 'upstream' 119 | 120 | 121 | grunt.loadTasks 'tasks' 122 | grunt.loadNpmTasks 'grunt-simple-mocha' 123 | grunt.loadNpmTasks 'grunt-contrib-jshint' 124 | grunt.loadNpmTasks 'grunt-contrib-watch' 125 | grunt.loadNpmTasks 'grunt-coffeelint' 126 | grunt.loadNpmTasks 'grunt-bump' 127 | grunt.loadNpmTasks 'grunt-npm' 128 | grunt.loadNpmTasks 'grunt-auto-release' 129 | grunt.loadNpmTasks 'grunt-conventional-changelog' 130 | 131 | grunt.registerTask 'default', ['build', 'test', 'lint'] 132 | grunt.registerTask 'lint', ['jshint', 'coffeelint'] 133 | grunt.registerTask 'release', 'Build, bump and publish to NPM.', (type) -> 134 | grunt.task.run [ 135 | 'npm-contributors' 136 | "bump:#{type||'patch'}:bump-only" 137 | 'build' 138 | 'changelog' 139 | 'bump-commit' 140 | 'npm-publish' 141 | ] 142 | -------------------------------------------------------------------------------- /test/unit/watcher.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/watcher.js module 3 | #============================================================================== 4 | describe 'watcher', -> 5 | mocks = require 'mocks' 6 | config = require '../../lib/config' 7 | m = null 8 | 9 | # create an array of pattern objects from given strings 10 | patterns = (strings...) -> 11 | new config.createPatternObject(str) for str in strings 12 | 13 | beforeEach -> 14 | mocks_ = chokidar: mocks.chokidar 15 | m = mocks.loadFile __dirname + '/../../lib/watcher.js', mocks_ 16 | 17 | #============================================================================ 18 | # baseDirFromPattern() [PRIVATE] 19 | #============================================================================ 20 | describe 'baseDirFromPattern', -> 21 | 22 | it 'should return parent directory without start', -> 23 | expect(m.baseDirFromPattern '/some/path/**/more.js').to.equal '/some/path' 24 | expect(m.baseDirFromPattern '/some/p*/file.js').to.equal '/some' 25 | 26 | 27 | it 'should remove part with parenthesis', -> 28 | expect(m.baseDirFromPattern '/some/p/(a|b).js').to.equal '/some/p' 29 | expect(m.baseDirFromPattern '/some/p(c|b)*.js').to.equal '/some' 30 | 31 | 32 | it 'should ignore exact files', -> 33 | expect(m.baseDirFromPattern '/usr/local/bin.js').to.equal '/usr/local/bin.js' 34 | 35 | 36 | #============================================================================== 37 | # watchPatterns() [PRIVATE] 38 | #============================================================================== 39 | describe 'watchPatterns', -> 40 | chokidarWatcher = null 41 | 42 | beforeEach -> 43 | chokidarWatcher = new mocks.chokidar.FSWatcher 44 | 45 | it 'should watch all the patterns', -> 46 | m.watchPatterns patterns('/some/*.js', '/a/*'), chokidarWatcher 47 | expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some', '/a'] 48 | 49 | 50 | it 'should not watch urls', -> 51 | m.watchPatterns patterns('http://some.com', '/a.*'), chokidarWatcher 52 | expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/'] 53 | 54 | 55 | it 'should not watch the same path twice', -> 56 | m.watchPatterns patterns('/some/a*.js', '/some/*.txt'), chokidarWatcher 57 | expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some'] 58 | 59 | 60 | it 'should not watch subpaths that are already watched', -> 61 | m.watchPatterns patterns('/some/sub/*.js', '/some/a*.*'), chokidarWatcher 62 | expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some'] 63 | 64 | 65 | it 'should watch a file matching subpath directory', -> 66 | # regression #521 67 | m.watchPatterns patterns('/some/test-file.js', '/some/test/**'), chokidarWatcher 68 | expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some/test-file.js', '/some/test'] 69 | 70 | 71 | it 'should not watch if watched false', -> 72 | m.watchPatterns [ 73 | new config.Pattern('/some/*.js', true, true, false) 74 | new config.Pattern('/some/sub/*.js') 75 | ], chokidarWatcher 76 | 77 | expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some/sub'] 78 | 79 | 80 | #============================================================================ 81 | # ignore() [PRIVATE] 82 | #============================================================================ 83 | describe 'ignore', -> 84 | 85 | it 'should ignore all files', -> 86 | ignore = m.createIgnore ['**/*'] 87 | expect(ignore '/some/files/deep/nested.js').to.equal true 88 | expect(ignore '/some/files').to.equal true 89 | 90 | 91 | it 'should ignore .# files', -> 92 | ignore = m.createIgnore ['**/.#*'] 93 | expect(ignore '/some/files/deep/nested.js').to.equal false 94 | expect(ignore '/some/files').to.equal false 95 | expect(ignore '/some/files/deep/.npm').to.equal false 96 | expect(ignore '.#files.js').to.equal true 97 | expect(ignore '/some/files/deeper/nested/.#files.js').to.equal true 98 | -------------------------------------------------------------------------------- /lib/middleware/karma.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Karma middleware is responsible for serving: 3 | * - client.html (the entrypoint for capturing a browser) 4 | * - debug.html 5 | * - context.html (the execution context, loaded within an iframe) 6 | * - karma.js 7 | * 8 | * The main part is generating context.html, as it contains: 9 | * - generating mappings 10 | * - including '; 21 | var LINK_TAG = ''; 22 | var SCRIPT_TYPE = { 23 | '.js': 'text/javascript', 24 | '.dart': 'application/dart' 25 | }; 26 | 27 | 28 | var filePathToUrlPath = function(filePath, basePath) { 29 | if (filePath.indexOf(basePath) === 0) { 30 | return '/base' + filePath.substr(basePath.length); 31 | } 32 | 33 | return '/absolute' + filePath; 34 | }; 35 | 36 | var createKarmaMiddleware = function(filesPromise, serveStaticFile, 37 | /* config.basePath */ basePath, /* config.urlRoot */ urlRoot) { 38 | 39 | return function(request, response, next) { 40 | var requestUrl = request.url.replace(/\?.*/, ''); 41 | 42 | // redirect /__karma__ to /__karma__ (trailing slash) 43 | if (requestUrl === urlRoot.substr(0, urlRoot.length - 1)) { 44 | response.setHeader('Location', urlRoot); 45 | response.writeHead(301); 46 | return response.end('MOVED PERMANENTLY'); 47 | } 48 | 49 | // ignore urls outside urlRoot 50 | if (requestUrl.indexOf(urlRoot) !== 0) { 51 | return next(); 52 | } 53 | 54 | // remove urlRoot prefix 55 | requestUrl = requestUrl.substr(urlRoot.length - 1); 56 | 57 | // serve client.html 58 | if (requestUrl === '/') { 59 | return serveStaticFile('/client.html', response); 60 | } 61 | 62 | // serve karma.js 63 | if (requestUrl === '/karma.js') { 64 | return serveStaticFile(requestUrl, response, function(data) { 65 | return data.replace('%KARMA_URL_ROOT%', urlRoot) 66 | .replace('%KARMA_VERSION%', VERSION); 67 | }); 68 | } 69 | 70 | // serve context.html - execution context within the iframe 71 | // or debug.html - execution context without channel to the server 72 | if (requestUrl === '/context.html' || requestUrl === '/debug.html') { 73 | return filesPromise.then(function(files) { 74 | serveStaticFile(requestUrl, response, function(data) { 75 | common.setNoCacheHeaders(response); 76 | 77 | var scriptTags = files.included.map(function(file) { 78 | var filePath = file.path; 79 | var fileExt = path.extname(filePath); 80 | 81 | if (!file.isUrl) { 82 | // TODO(vojta): serve these files from within urlRoot as well 83 | filePath = filePathToUrlPath(filePath, basePath); 84 | 85 | if (requestUrl === '/context.html') { 86 | filePath += '?' + file.mtime.getTime(); 87 | } 88 | } 89 | 90 | if (fileExt === '.css') { 91 | return util.format(LINK_TAG, filePath); 92 | } 93 | 94 | return util.format(SCRIPT_TAG, SCRIPT_TYPE[fileExt] || 'text/javascript', filePath); 95 | }); 96 | 97 | // TODO(vojta): don't compute if it's not in the template 98 | var mappings = files.served.map(function(file) { 99 | var filePath = filePathToUrlPath(file.path, basePath); 100 | 101 | return util.format(' \'%s\': \'%d\'', filePath, file.mtime.getTime()); 102 | }); 103 | 104 | mappings = 'window.__karma__.files = {\n' + mappings.join(',\n') + '\n};\n'; 105 | 106 | return data.replace('%SCRIPTS%', scriptTags.join('\n')).replace('%MAPPINGS%', mappings); 107 | }); 108 | }); 109 | } 110 | 111 | return next(); 112 | }; 113 | }; 114 | 115 | 116 | // PUBLIC API 117 | exports.create = createKarmaMiddleware; 118 | -------------------------------------------------------------------------------- /tasks/test.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | /** 4 | * Run tests 5 | * 6 | * grunt test 7 | * grunt test:unit 8 | * grunt test:client 9 | * grunt test:e2e 10 | */ 11 | grunt.registerMultiTask('test', 'Run tests.', function() { 12 | var specDone = this.async(); 13 | var node = require('which').sync('node'); 14 | var path = require('path'); 15 | var cmd = path.join(__dirname, '..', 'bin', 'karma'); 16 | 17 | var spawnKarma = function(args, callback) { 18 | grunt.log.writeln(['Running', cmd].concat(args).join(' ')); 19 | var child; 20 | if (process.platform === 'win32') { 21 | child = grunt.util.spawn({cmd: node, args: [cmd].concat(args)}, callback); 22 | } else { 23 | child = grunt.util.spawn({cmd: cmd, args: args}, callback); 24 | } 25 | child.stdout.pipe(process.stdout); 26 | child.stderr.pipe(process.stderr); 27 | }; 28 | 29 | var exec = function(args, failMsg) { 30 | spawnKarma(args, function(err, result, code) { 31 | if (code) { 32 | console.error(err); 33 | grunt.fail.fatal(failMsg, code); 34 | } else { 35 | specDone(); 36 | } 37 | }); 38 | }; 39 | 40 | 41 | // E2E tests 42 | if (this.target === 'e2e') { 43 | var tests = grunt.file.expand(this.data); 44 | var processToKill; 45 | var args = [ 46 | 'start', null, '--single-run', '--no-auto-watch' 47 | ]; 48 | 49 | 50 | var next = function(err, result, code) { 51 | var testArgs = []; 52 | if (processToKill) { 53 | processToKill.kill(); 54 | } 55 | 56 | if (err || code) { 57 | console.error(err); 58 | grunt.fail.fatal('E2E test "' + args[1] + '" failed.', code); 59 | } else { 60 | args[1] = tests.shift(); 61 | if (args[1]) { 62 | if (args[1] === 'test/e2e/angular-scenario/karma.conf.js') { 63 | processToKill = grunt.util.spawn({ 64 | cmd: node, 65 | args: ['test/e2e/angular-scenario/server.js'] 66 | }, function() {}); 67 | } 68 | 69 | if (args[1] === 'test/e2e/pass-opts/karma.conf.js') { 70 | var serverArgs = args.slice(); 71 | serverArgs.splice(args.indexOf('--single-run'), 1); 72 | var done = false; 73 | var cont = function() { 74 | if (!done) { 75 | done = true; 76 | next.apply(this, arguments); 77 | } 78 | }; 79 | 80 | processToKill = grunt.util.spawn({ 81 | cmd: node, 82 | args: [cmd].concat(serverArgs), 83 | opts: {stdio: [process.stdin, 'pipe', process.stderr]} 84 | }, cont); 85 | 86 | var onData = function(data) { 87 | data = data.toString(); 88 | // wait for the browser to connect 89 | if (/Connected on socket/.test(data)) { 90 | processToKill.stdout.removeListener('data', onData); 91 | spawnKarma(['run', '--','arg1','arg2','arg3'], cont); 92 | } else { 93 | console.log(data); 94 | } 95 | }; 96 | 97 | processToKill.stdout.on('data', onData); 98 | } else { 99 | spawnKarma(args.concat(testArgs), next); 100 | } 101 | } else { 102 | specDone(); 103 | } 104 | } 105 | }; 106 | 107 | 108 | // run only e2e tests specified by args 109 | if (arguments.length) { 110 | var slicedArgs = Array.prototype.slice.call(arguments); 111 | 112 | tests = tests.filter(function(configFile) { 113 | return slicedArgs.some(function(test) { 114 | return configFile.indexOf(test) !== -1; 115 | }); 116 | }); 117 | } 118 | 119 | return next(); 120 | } 121 | 122 | // CLIENT unit tests 123 | if (this.target === 'client') { 124 | return exec(['start', this.data, '--single-run', '--no-auto-watch', '--reporters=dots'], 125 | 'Client unit tests failed.'); 126 | } 127 | 128 | // UNIT tests or TASK tests 129 | grunt.task.run([this.data]); 130 | specDone(); 131 | }); 132 | }; 133 | -------------------------------------------------------------------------------- /test/unit/middleware/source-files.spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'middleware.source-files', -> 2 | q = require 'q' 3 | 4 | mocks = require 'mocks' 5 | HttpResponseMock = mocks.http.ServerResponse 6 | HttpRequestMock = mocks.http.ServerRequest 7 | 8 | File = require('../../../lib/file-list').File 9 | Url = require('../../../lib/file-list').Url 10 | 11 | fsMock = mocks.fs.create 12 | base: 13 | path: 14 | 'a.js': mocks.fs.file(0, 'js-src-a') 15 | src: 16 | 'some.js': mocks.fs.file(0, 'js-source') 17 | 'utf8ášč': 18 | 'some.js': mocks.fs.file(0, 'utf8-file') 19 | 20 | 21 | serveFile = require('../../../lib/middleware/common').createServeFile fsMock, null 22 | createSourceFilesMiddleware = require('../../../lib/middleware/source-files').create 23 | 24 | handler = filesDeferred = nextSpy = response = null 25 | 26 | beforeEach -> 27 | nextSpy = sinon.spy() 28 | response = new HttpResponseMock 29 | filesDeferred = q.defer() 30 | handler = createSourceFilesMiddleware filesDeferred.promise, serveFile, '/base/path' 31 | 32 | # helpers 33 | includedFiles = (files) -> 34 | filesDeferred.resolve {included: files, served: []} 35 | 36 | servedFiles = (files) -> 37 | filesDeferred.resolve {included: [], served: files} 38 | 39 | callHandlerWith = (urlPath, next) -> 40 | promise = handler new HttpRequestMock(urlPath), response, next or nextSpy 41 | if promise and promise.done then promise.done() 42 | 43 | 44 | it 'should serve absolute js source files ignoring timestamp', (done) -> 45 | servedFiles [ 46 | new File('/src/some.js') 47 | ] 48 | 49 | response.once 'end', -> 50 | expect(nextSpy).not.to.have.been.called 51 | expect(response).to.beServedAs 200, 'js-source' 52 | done() 53 | 54 | callHandlerWith '/absolute/src/some.js?123345' 55 | 56 | 57 | it 'should serve js source files from base folder ignoring timestamp', (done) -> 58 | servedFiles [ 59 | new File('/base/path/a.js') 60 | ] 61 | 62 | response.once 'end', -> 63 | expect(nextSpy).not.to.have.been.called 64 | expect(response).to.beServedAs 200, 'js-src-a' 65 | done() 66 | 67 | callHandlerWith '/base/a.js?123345' 68 | 69 | 70 | it 'should send strict caching headers for js source files with timestamps', (done) -> 71 | servedFiles [ 72 | new File('/src/some.js') 73 | ] 74 | 75 | response.once 'end', -> 76 | expect(nextSpy).not.to.have.been.called 77 | expect(response._headers['Cache-Control']).to.deep.equal ['public', 'max-age=31536000'] 78 | done() 79 | 80 | callHandlerWith '/absolute/src/some.js?12323' 81 | 82 | 83 | it 'should send no-caching headers for js source files without timestamps', (done) -> 84 | ZERO_DATE = (new Date 0).toString() 85 | 86 | servedFiles [ 87 | new File('/src/some.js') 88 | ] 89 | 90 | response.once 'end', -> 91 | expect(nextSpy).not.to.have.been.called 92 | expect(response._headers['Cache-Control']).to.equal 'no-cache' 93 | # idiotic IE8 needs more 94 | expect(response._headers['Pragma']).to.equal 'no-cache' 95 | expect(response._headers['Expires']).to.equal ZERO_DATE 96 | done() 97 | 98 | callHandlerWith '/absolute/src/some.js' 99 | 100 | 101 | it 'should not serve files that are not in served', (done) -> 102 | servedFiles [] 103 | 104 | callHandlerWith '/absolute/non-existing.html', -> 105 | expect(response).to.beNotServed() 106 | done() 107 | 108 | 109 | it 'should serve 404 if file is served but does not exist', (done) -> 110 | servedFiles [ 111 | new File('/non-existing.js') 112 | ] 113 | 114 | response.once 'end', -> 115 | expect(nextSpy).not.to.have.been.called 116 | expect(response).to.beServedAs 404, 'NOT FOUND' 117 | done() 118 | 119 | callHandlerWith '/absolute/non-existing.js' 120 | 121 | 122 | it 'should serve js source file from base path containing utf8 chars', (done) -> 123 | servedFiles [ 124 | new File('/utf8ášč/some.js') 125 | ] 126 | 127 | handler = createSourceFilesMiddleware filesDeferred.promise, serveFile, '/utf8ášč' 128 | 129 | response.once 'end', -> 130 | expect(nextSpy).not.to.have.been.called 131 | expect(response._body).to.equal 'utf8-file' 132 | expect(response._status).to.equal 200 133 | done() 134 | 135 | callHandlerWith '/base/some.js' 136 | -------------------------------------------------------------------------------- /docs/config/03-browsers.md: -------------------------------------------------------------------------------- 1 | Capturing browsers on your own is kinda tedious and time consuming, 2 | so Karma can do that for you. Just simply add into the configuration file: 3 | 4 | ```javascript 5 | browsers: ['Chrome'] 6 | ``` 7 | 8 | Then, Karma will take care of autocapturing these browsers, as well as killing them. 9 | 10 | Note: Most of the browser launchers needs to be loaded as [plugins]. 11 | 12 | ## Available browser launchers 13 | These launchers are shipped with Karma by default: 14 | - [Chrome and Chrome Canary] 15 | - [PhantomJS] 16 | 17 | Additional launchers can be loaded through [plugins], such as: 18 | - [Firefox] (install karma-firefox-launcher first) 19 | - [Safari] (install karma-safari-launcher first) 20 | - [Opera] (install karma-opera-launcher first) 21 | - [IE] (install karma-ie-launcher first) 22 | 23 | As mentioned above above, only Chrome and PhantomJS come bundled with Karma. Therefore, to use other browsers naturally, 24 | simply install the required launcher first using NPM and then assign the browser setting within the configuration file using the `browsers`. 25 | 26 | Here's an example of how to add Firefox to your testing suite: 27 | 28 | ```bash 29 | # Install it first with NPM 30 | $ npm install karma-firefox-launcher --save-dev 31 | ``` 32 | 33 | And then inside your configuration file... 34 | 35 | ```javascript 36 | module.exports = function(config) { 37 | config.set({ 38 | browsers : ['Chrome', 'Firefox'] 39 | }); 40 | }; 41 | ``` 42 | 43 | Also keep in mind that the `browsers` configuraiton setting is empty by default. 44 | 45 | Of course, you can write [custom plugins] too! 46 | 47 | ## Capturing any browser manually 48 | 49 | You can also capture browsers by simply opening `http://:/`, where `` is the IP address or hostname of the machine where Karma server is running and `` is the port where Karma server is listening (by default it's `9876`). With the default settings in place, just point your browser to `http://localhost:9876/`. 50 | 51 | This allows you to capture a browser on any device such as a tablet or a phone, that is on the same network as the machine running Karma (or using a local tunnel). 52 | 53 | 54 | ## Configured launchers 55 | Some of the launchers can be also configured: 56 | 57 | ```javascript 58 | sauceLabs: { 59 | username: 'michael_jackson' 60 | } 61 | ``` 62 | 63 | Or defined as a configured launcher: 64 | 65 | ```javascript 66 | customLaunchers: { 67 | chrome_without_security: { 68 | base: 'Chrome', 69 | flags: ['--disable-web-security'] 70 | }, 71 | sauce_chrome_win: { 72 | base: 'SauceLabs', 73 | browserName: 'chrome', 74 | platform: 'windows' 75 | } 76 | } 77 | ``` 78 | 79 | 80 | ## Correct path to browser binary 81 | Each plugin has some default paths where to find the browser binary on particular OS. 82 | You can override these settings by `_BIN` ENV variable, or alternatively by creating a `symlink`. 83 | 84 | ### POSIX shells 85 | ```bash 86 | # Changing the path to the Chrome binary 87 | $ export CHROME_BIN=/usr/local/bin/my-chrome-build 88 | 89 | # Changing the path to the Chrome Canary binary 90 | $ export CHROME_CANARY_BIN=/usr/local/bin/my-chrome-build 91 | 92 | # Changing the path to the PhantomJs binary 93 | $ export PHANTOMJS_BIN=$HOME/local/bin/phantomjs 94 | ``` 95 | 96 | ### Windows cmd.exe 97 | ```bash 98 | C:> SET IE_BIN=C:\Program Files\Internet Explorer\iexplore.exe 99 | ``` 100 | 101 | ### Windows Powershell 102 | ```bash 103 | $Env:FIREFOX_BIN = 'c:\Program Files (x86)\Mozilla Firefox 4.0 Beta 6\firefox.exe' 104 | ``` 105 | 106 | ## Custom browsers 107 | ```javascript 108 | // in the karma.conf.js 109 | browsers: ['/usr/local/bin/custom-browser.sh'], 110 | 111 | // from cli 112 | karma start --browsers /usr/local/bin/custom-browser.sh 113 | ``` 114 | The browser scripts need to take one argument, which is the URL with the ID 115 | parameter to be used to connect to the server. The supplied ID is used 116 | by the server to keep track of which specific browser is captured. 117 | 118 | 119 | 120 | [Chrome and Chrome Canary]: https://github.com/karma-runner/karma-chrome-launcher 121 | [PhantomJS]: https://github.com/karma-runner/karma-phantomjs-launcher 122 | [Firefox]: https://github.com/karma-runner/karma-firefox-launcher 123 | [Safari]: https://github.com/karma-runner/karma-safari-launcher 124 | [IE]: https://github.com/karma-runner/karma-ie-launcher 125 | [Opera]: https://github.com/karma-runner/karma-opera-launcher 126 | [SauceLabs]: https://github.com/karma-runner/karma-sauce-launcher 127 | [custom plugins]: ../dev/plugins.html 128 | [plugins]: plugins.html 129 | -------------------------------------------------------------------------------- /lib/reporters/Base.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | var helper = require('../helper'); 4 | 5 | 6 | var BaseReporter = function(formatError, reportSlow, adapter) { 7 | this.adapters = [adapter || process.stdout.write.bind(process.stdout)]; 8 | 9 | this.onRunStart = function(browsers) { 10 | this._browsers = browsers; 11 | }; 12 | 13 | 14 | this.renderBrowser = function(browser) { 15 | var results = browser.lastResult; 16 | var totalExecuted = results.success + results.failed; 17 | var msg = util.format('%s: Executed %d of %d', browser, totalExecuted, results.total); 18 | 19 | if (results.failed) { 20 | msg += util.format(this.X_FAILED, results.failed); 21 | } 22 | 23 | if (results.skipped) { 24 | msg += util.format(' (skipped %d)', results.skipped); 25 | } 26 | 27 | if (browser.isReady) { 28 | if (results.disconnected) { 29 | msg += this.FINISHED_DISCONNECTED; 30 | } else if (results.error) { 31 | msg += this.FINISHED_ERROR; 32 | } else if (!results.failed) { 33 | msg += this.FINISHED_SUCCESS; 34 | } 35 | 36 | msg += util.format(' (%s / %s)', helper.formatTimeInterval(results.totalTime), 37 | helper.formatTimeInterval(results.netTime)); 38 | } 39 | 40 | return msg; 41 | }; 42 | 43 | this.renderBrowser = this.renderBrowser.bind(this); 44 | 45 | 46 | this.write = function() { 47 | var msg = util.format.apply(null, Array.prototype.slice.call(arguments)); 48 | 49 | this.adapters.forEach(function(adapter) { 50 | adapter(msg); 51 | }); 52 | }; 53 | 54 | this.writeCommonMsg = this.write; 55 | 56 | 57 | this.onBrowserError = function(browser, error) { 58 | this.writeCommonMsg(util.format(this.ERROR, browser) + formatError(error, '\t')); 59 | }; 60 | 61 | 62 | this.onBrowserLog = function(browser, log, type) { 63 | if (!helper.isString(log)) { 64 | // TODO(vojta): change util to new syntax (config object) 65 | log = util.inspect(log, false, undefined, this.USE_COLORS); 66 | } 67 | 68 | if (this._browsers && this._browsers.length === 1) { 69 | this.writeCommonMsg(util.format(this.LOG_SINGLE_BROWSER, type.toUpperCase(), log)); 70 | } else { 71 | this.writeCommonMsg(util.format(this.LOG_MULTI_BROWSER, browser, type.toUpperCase(), log)); 72 | } 73 | }; 74 | 75 | 76 | this.onSpecComplete = function(browser, result) { 77 | if (result.skipped) { 78 | this.specSkipped(browser, result); 79 | } else if (result.success) { 80 | this.specSuccess(browser, result); 81 | } else { 82 | this.specFailure(browser, result); 83 | } 84 | 85 | if (reportSlow && result.time > reportSlow) { 86 | var specName = result.suite.join(' ') + ' ' + result.description; 87 | var time = helper.formatTimeInterval(result.time); 88 | 89 | this.writeCommonMsg(util.format(this.SPEC_SLOW, browser, time, specName)); 90 | } 91 | }; 92 | 93 | 94 | this.specSuccess = this.specSkipped = function() {}; 95 | 96 | 97 | this.specFailure = function(browser, result) { 98 | var specName = result.suite.join(' ') + ' ' + result.description; 99 | var msg = util.format(this.SPEC_FAILURE, browser, specName); 100 | 101 | result.log.forEach(function(log) { 102 | msg += formatError(log, '\t'); 103 | }); 104 | 105 | this.writeCommonMsg(msg); 106 | }; 107 | 108 | 109 | this.onRunComplete = function(browsers, results) { 110 | if (browsers.length > 1 && !results.error && !results.disconnected) { 111 | if (!results.failed) { 112 | this.write(this.TOTAL_SUCCESS, results.success); 113 | } else { 114 | this.write(this.TOTAL_FAILED, results.failed, results.success); 115 | } 116 | } 117 | }; 118 | 119 | this.USE_COLORS = false; 120 | 121 | this.LOG_SINGLE_BROWSER = 'LOG: %s\n'; 122 | this.LOG_MULTI_BROWSER = '%s LOG: %s\n'; 123 | 124 | this.SPEC_FAILURE = '%s %s FAILED' + '\n'; 125 | this.SPEC_SLOW = '%s SLOW %s: %s\n'; 126 | this.ERROR = '%s ERROR\n'; 127 | 128 | this.FINISHED_ERROR = ' ERROR'; 129 | this.FINISHED_SUCCESS = ' SUCCESS'; 130 | this.FINISHED_DISCONNECTED = ' DISCONNECTED'; 131 | 132 | this.X_FAILED = ' (%d FAILED)'; 133 | 134 | this.TOTAL_SUCCESS = 'TOTAL: %d SUCCESS\n'; 135 | this.TOTAL_FAILED = 'TOTAL: %d FAILED, %d SUCCESS\n'; 136 | }; 137 | 138 | BaseReporter.decoratorFactory = function(formatError, reportSlow) { 139 | return function(self) { 140 | BaseReporter.call(self, formatError, reportSlow); 141 | }; 142 | }; 143 | 144 | BaseReporter.decoratorFactory.$inject = ['formatError', 'config.reportSlowerThan']; 145 | 146 | 147 | // PUBLISH 148 | module.exports = BaseReporter; 149 | -------------------------------------------------------------------------------- /lib/completion.js: -------------------------------------------------------------------------------- 1 | var CUSTOM = ['']; 2 | var BOOLEAN = false; 3 | 4 | var options = { 5 | start: { 6 | '--port': CUSTOM, 7 | '--auto-watch': BOOLEAN, 8 | '--no-auto-watch': BOOLEAN, 9 | '--log-level': ['disable', 'debug', 'info', 'warn', 'error'], 10 | '--colors': BOOLEAN, 11 | '--no-colors': BOOLEAN, 12 | '--reporters': ['dots', 'progress'], 13 | '--no-reporters': BOOLEAN, 14 | '--browsers': ['Chrome', 'ChromeCanary', 'Firefox', 'PhantomJS', 'Safari', 'Opera'], 15 | '--no-browsers': BOOLEAN, 16 | '--single-run': BOOLEAN, 17 | '--no-single-run': BOOLEAN, 18 | '--help': BOOLEAN 19 | }, 20 | init: { 21 | '--colors': BOOLEAN, 22 | '--no-colors': BOOLEAN, 23 | '--help': BOOLEAN 24 | }, 25 | run: { 26 | '--no-refresh': BOOLEAN, 27 | '--port': CUSTOM, 28 | '--help': BOOLEAN 29 | } 30 | }; 31 | 32 | var parseEnv = function(argv, env) { 33 | var words = argv.slice(5); 34 | 35 | return { 36 | words: words, 37 | count: parseInt(env.COMP_CWORD, 10), 38 | last: words[words.length - 1], 39 | prev: words[words.length - 2] 40 | }; 41 | }; 42 | 43 | var opositeWord = function(word) { 44 | if (word.charAt(0) !== '-') { 45 | return null; 46 | } 47 | 48 | return word.substr(0, 5) === '--no-' ? '--' + word.substr(5) : '--no-' + word.substr(2); 49 | }; 50 | 51 | var sendCompletionNoOptions = function() {}; 52 | 53 | var sendCompletion = function(possibleWords, env) { 54 | var regexp = new RegExp('^' + env.last); 55 | var filteredWords = possibleWords.filter(function(word) { 56 | return regexp.test(word) && env.words.indexOf(word) === -1 && 57 | env.words.indexOf(opositeWord(word)) === -1; 58 | }); 59 | 60 | if (!filteredWords.length) { 61 | return sendCompletionNoOptions(env); 62 | } 63 | 64 | filteredWords.forEach(function(word) { 65 | console.log(word); 66 | }); 67 | }; 68 | 69 | 70 | var glob = require('glob'); 71 | var globOpts = { 72 | mark: true, 73 | nocase: true 74 | }; 75 | 76 | var sendCompletionFiles = function(env) { 77 | glob(env.last + '*', globOpts, function(err, files) { 78 | if (files.length === 1 && files[0].charAt(files[0].length - 1) === '/') { 79 | sendCompletionFiles({last: files[0]}); 80 | } else { 81 | console.log(files.join('\n')); 82 | } 83 | }); 84 | }; 85 | 86 | var sendCompletionConfirmLast = function(env) { 87 | console.log(env.last); 88 | }; 89 | 90 | var complete = function(env) { 91 | if (env.count === 1) { 92 | if (env.words[0].charAt(0) === '-') { 93 | return sendCompletion(['--help', '--version'], env); 94 | } 95 | 96 | return sendCompletion(Object.keys(options), env); 97 | } 98 | 99 | if (env.count === 2 && env.words[1].charAt(0) !== '-') { 100 | // complete files (probably karma.conf.js) 101 | return sendCompletionFiles(env); 102 | } 103 | 104 | var cmdOptions = options[env.words[0]]; 105 | var previousOption = cmdOptions[env.prev]; 106 | 107 | if (!cmdOptions) { 108 | // no completion, wrong command 109 | return sendCompletionNoOptions(); 110 | } 111 | 112 | if (previousOption === CUSTOM && env.last) { 113 | // custom value with already filled something 114 | return sendCompletionConfirmLast(env); 115 | } 116 | 117 | if (previousOption) { 118 | // custom options 119 | return sendCompletion(previousOption, env); 120 | } 121 | 122 | return sendCompletion(Object.keys(cmdOptions), env); 123 | }; 124 | 125 | 126 | var completion = function() { 127 | if (process.argv[3] === '--') { 128 | return complete(parseEnv(process.argv, process.env)); 129 | } 130 | 131 | // just print out the karma-completion.sh 132 | var fs = require('fs'); 133 | var path = require('path'); 134 | 135 | fs.readFile(path.resolve(__dirname, '../karma-completion.sh'), 'utf8', function (err, data) { 136 | process.stdout.write(data); 137 | process.stdout.on('error', function (error) { 138 | // Darwin is a real dick sometimes. 139 | // 140 | // This is necessary because the "source" or "." program in 141 | // bash on OS X closes its file argument before reading 142 | // from it, meaning that you get exactly 1 write, which will 143 | // work most of the time, and will always raise an EPIPE. 144 | // 145 | // Really, one should not be tossing away EPIPE errors, or any 146 | // errors, so casually. But, without this, `. <(karma completion)` 147 | // can never ever work on OS X. 148 | if (error.errno === 'EPIPE') { 149 | error = null; 150 | } 151 | }); 152 | }); 153 | }; 154 | 155 | 156 | // PUBLIC API 157 | exports.completion = completion; 158 | 159 | // for testing 160 | exports.opositeWord = opositeWord; 161 | exports.sendCompletion = sendCompletion; 162 | exports.complete = complete; 163 | -------------------------------------------------------------------------------- /test/unit/launcher.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/launcher.js module 3 | #============================================================================== 4 | describe 'launcher', -> 5 | di = require 'di' 6 | events = require '../../lib/events' 7 | logger = require '../../lib/logger' 8 | launcher = require '../../lib/launcher' 9 | 10 | # mock out id generator 11 | lastGeneratedId = null 12 | launcher.Launcher.generateId = -> 13 | ++lastGeneratedId 14 | 15 | class FakeBrowser 16 | constructor: (@id, @name, baseBrowserDecorator) -> 17 | baseBrowserDecorator @ 18 | FakeBrowser._instances.push @ 19 | sinon.stub @, 'start' 20 | sinon.stub @, 'kill' 21 | 22 | class ScriptBrowser 23 | constructor: (@id, @name, baseBrowserDecorator) -> 24 | baseBrowserDecorator @ 25 | ScriptBrowser._instances.push @ 26 | sinon.stub @, 'start' 27 | sinon.stub @, 'kill' 28 | 29 | 30 | beforeEach -> 31 | lastGeneratedId = 0 32 | FakeBrowser._instances = [] 33 | ScriptBrowser._instances = [] 34 | 35 | 36 | #============================================================================ 37 | # launcher.Launcher 38 | #============================================================================ 39 | describe 'Launcher', -> 40 | l = emitter = null 41 | 42 | beforeEach -> 43 | emitter = new events.EventEmitter() 44 | injector = new di.Injector [{ 45 | 'launcher:Fake': ['type', FakeBrowser] 46 | 'launcher:Script': ['type', ScriptBrowser] 47 | 'emitter': ['value', emitter] 48 | 'config': ['value', {captureTimeout: 0}] 49 | }] 50 | l = new launcher.Launcher emitter, injector 51 | 52 | describe 'launch', -> 53 | 54 | it 'should inject and start all browsers', -> 55 | l.launch ['Fake'], 'localhost', 1234, '/root/' 56 | 57 | browser = FakeBrowser._instances.pop() 58 | expect(browser.start).to.have.been.calledWith 'http://localhost:1234/root/' 59 | expect(browser.id).to.equal lastGeneratedId 60 | expect(browser.name).to.equal 'Fake' 61 | 62 | 63 | it 'should allow launching a script', -> 64 | l.launch ['/usr/local/bin/special-browser'], 'localhost', 1234, '/' 65 | 66 | script = ScriptBrowser._instances.pop() 67 | expect(script.start).to.have.been.calledWith 'http://localhost:1234/' 68 | expect(script.name).to.equal '/usr/local/bin/special-browser' 69 | 70 | 71 | it 'should use the non default host', -> 72 | l.launch ['Fake'], 'whatever', 1234, '/root/' 73 | 74 | browser = FakeBrowser._instances.pop() 75 | expect(browser.start).to.have.been.calledWith 'http://whatever:1234/root/' 76 | 77 | 78 | describe 'kill', -> 79 | exitSpy = null 80 | 81 | beforeEach -> 82 | exitSpy = sinon.spy() 83 | 84 | it 'should kill all running processe', -> 85 | l.launch ['Fake', 'Fake'], 'localhost', 1234 86 | l.kill() 87 | 88 | browser = FakeBrowser._instances.pop() 89 | expect(browser.kill).to.have.been.called 90 | 91 | browser = FakeBrowser._instances.pop() 92 | expect(browser.kill).to.have.been.called 93 | 94 | 95 | it 'should call callback when all processes killed', -> 96 | l.launch ['Fake', 'Fake'], 'localhost', 1234 97 | l.kill exitSpy 98 | 99 | expect(exitSpy).not.to.have.been.called 100 | 101 | # finish the first browser 102 | browser = FakeBrowser._instances.pop() 103 | browser.kill.invokeCallback() 104 | expect(exitSpy).not.to.have.been.called 105 | 106 | # finish the second browser 107 | browser = FakeBrowser._instances.pop() 108 | browser.kill.invokeCallback() 109 | expect(exitSpy).to.have.been.called 110 | # expect(browser.lastCall) 111 | 112 | 113 | it 'should call callback even if no browsers lanunched', (done) -> 114 | l.kill done 115 | 116 | 117 | describe 'areAllCaptured', -> 118 | 119 | it 'should return true if only if all browsers captured', -> 120 | l.launch ['Fake', 'Fake'], 'localhost', 1234 121 | 122 | expect(l.areAllCaptured()).to.equal false 123 | 124 | l.markCaptured 1 125 | expect(l.areAllCaptured()).to.equal false 126 | 127 | l.markCaptured 2 128 | expect(l.areAllCaptured()).to.equal true 129 | 130 | 131 | describe 'onExit', -> 132 | 133 | it 'should kill all browsers', (done) -> 134 | l.launch ['Fake', 'Fake'], 'localhost', 1234, '/', 0, 1 135 | 136 | emitter.emitAsync('exit').then done 137 | 138 | browser = FakeBrowser._instances.pop() 139 | browser.kill.invokeCallback() 140 | 141 | browser = FakeBrowser._instances.pop() 142 | browser.kill.invokeCallback() 143 | -------------------------------------------------------------------------------- /test/client/karma.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | Tests for static/karma.js 3 | These tests are executed in browser. 4 | */ 5 | 6 | describe('karma', function() { 7 | var socket, k, spyStart, windowNavigator, windowLocation; 8 | 9 | beforeEach(function() { 10 | socket = new MockSocket(); 11 | windowNavigator = {}; 12 | windowLocation = {}; 13 | k = new Karma(socket, {}, windowNavigator, windowLocation); 14 | spyStart = spyOn(k, 'start'); 15 | }); 16 | 17 | 18 | it('should start execution when all files loaded and pass config', function() { 19 | var config = {}; 20 | 21 | socket.emit('execute', config); 22 | expect(spyStart).not.toHaveBeenCalled(); 23 | 24 | k.loaded(); 25 | expect(spyStart).toHaveBeenCalledWith(config); 26 | }); 27 | 28 | 29 | it('should not start execution if any error during loading files', function() { 30 | k.error('syntax error', '/some/file.js', 11); 31 | k.loaded(); 32 | 33 | expect(spyStart).not.toHaveBeenCalled(); 34 | }); 35 | 36 | 37 | it('should remove reference to start even after syntax error', function() { 38 | k.error('syntax error', '/some/file.js', 11); 39 | k.loaded(); 40 | expect(k.start).toBeFalsy(); 41 | 42 | k.start = function() {}; 43 | k.loaded(); 44 | expect(k.start).toBeFalsy(); 45 | }); 46 | 47 | 48 | it('should not set up context if there was an error', function() { 49 | var mockWindow = {}; 50 | 51 | k.error('page reload'); 52 | k.setupContext(mockWindow); 53 | 54 | expect(mockWindow.__karma__).toBeUndefined(); 55 | expect(mockWindow.onbeforeunload).toBeUndefined(); 56 | expect(mockWindow.onerror).toBeUndefined(); 57 | }); 58 | 59 | 60 | it('should report navigator name', function() { 61 | var spyInfo = jasmine.createSpy('onInfo').andCallFake(function(info) { 62 | expect(info.name).toBe('Fake browser name'); 63 | }); 64 | 65 | windowNavigator.userAgent = 'Fake browser name'; 66 | windowLocation.search = ''; 67 | socket.on('register', spyInfo); 68 | socket.emit('connect'); 69 | 70 | expect(spyInfo).toHaveBeenCalled(); 71 | }); 72 | 73 | 74 | it('should report browser id', function() { 75 | var spyInfo = jasmine.createSpy('onInfo').andCallFake(function(info) { 76 | expect(info.id).toBe(567); 77 | }); 78 | 79 | windowLocation.search = '?id=567'; 80 | socket.on('register', spyInfo); 81 | socket.emit('connect'); 82 | 83 | expect(spyInfo).toHaveBeenCalled(); 84 | }); 85 | 86 | 87 | describe('setupContext', function() { 88 | it('should capture alert', function() { 89 | spyOn(k, 'log'); 90 | 91 | var mockWindow = { 92 | alert: function() { 93 | throw 'Alert was not patched!'; 94 | } 95 | }; 96 | 97 | k.setupContext(mockWindow); 98 | mockWindow.alert('What?'); 99 | expect(k.log).toHaveBeenCalledWith('alert', ['What?']); 100 | }) 101 | }); 102 | 103 | 104 | describe('store', function() { 105 | 106 | it('should be getter/setter', function() { 107 | k.store('a', 10); 108 | k.store('b', [1, 2, 3]); 109 | 110 | expect(k.store('a')).toBe(10); 111 | expect(k.store('b')).toEqual([1, 2, 3]); 112 | }); 113 | 114 | 115 | it('should clone arrays to avoid memory leaks', function() { 116 | var array = [1, 2, 3, 4, 5]; 117 | 118 | k.store('one.array', array); 119 | expect(k.store('one.array')).toEqual(array); 120 | expect(k.store('one.array')).not.toBe(array); 121 | }); 122 | }); 123 | 124 | 125 | describe('stringify', function() { 126 | it('should serialize string', function() { 127 | expect(k.stringify('aaa')).toBe("'aaa'"); 128 | }); 129 | 130 | 131 | it('should serialize booleans', function() { 132 | expect(k.stringify(true)).toBe('true'); 133 | expect(k.stringify(false)).toBe('false'); 134 | }); 135 | 136 | 137 | it('should serialize null and undefined', function() { 138 | expect(k.stringify(null)).toBe('null'); 139 | expect(k.stringify()).toBe('undefined'); 140 | }); 141 | 142 | 143 | it('should serialize functions', function() { 144 | function abc(a, b, c) { return 'whatever'; } 145 | var def = function(d, e, f) { return 'whatever'; }; 146 | 147 | expect(k.stringify(abc)).toBe('function abc(a, b, c) { ... }'); 148 | expect(k.stringify(def)).toBe('function (d, e, f) { ... }'); 149 | }); 150 | 151 | 152 | it('should serialize arrays', function() { 153 | expect(k.stringify(['a', 'b', null, true, false])).toBe("['a', 'b', null, true, false]"); 154 | }); 155 | 156 | 157 | it('should serialize html', function() { 158 | var div = document.createElement('div'); 159 | 160 | expect(k.stringify(div)).toBe('
'); 161 | 162 | div.innerHTML = 'some text'; 163 | expect(k.stringify(div)).toBe('
some text
'); 164 | }); 165 | 166 | 167 | it('should serialize across iframes', function() { 168 | var div = document.createElement('div'); 169 | expect(__karma__.stringify(div)).toBe('
'); 170 | 171 | expect(__karma__.stringify([1, 2])).toBe('[1, 2]'); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/unit/cli.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/cli.js module 3 | #============================================================================== 4 | describe 'cli', -> 5 | cli = require '../../lib/cli' 6 | optimist = require 'optimist' 7 | path = require 'path' 8 | constant = require '../../lib/constants' 9 | path = require 'path' 10 | mocks = require 'mocks' 11 | 12 | fsMock = mocks.fs.create 13 | cwd: 14 | 'karma.conf.js': true 15 | cwd2: 16 | 'karma.conf.coffee': true 17 | 18 | currentCwd = null 19 | 20 | pathMock = 21 | resolve: (p) -> path.resolve currentCwd, p 22 | 23 | setCWD = (cwd) -> 24 | currentCwd = cwd 25 | fsMock._setCWD cwd 26 | 27 | processArgs = (args, opts) -> 28 | argv = optimist.parse(args) 29 | cli.processArgs argv, opts || {}, fsMock, pathMock 30 | 31 | beforeEach -> setCWD '/' 32 | 33 | describe 'processArgs', -> 34 | 35 | it 'should return camelCased options', -> 36 | options = processArgs ['some.conf', '--port', '12', '--single-run'] 37 | 38 | expect(options.configFile).to.exist 39 | expect(options.port).to.equal 12 40 | expect(options.singleRun).to.equal true 41 | 42 | 43 | it 'should parse options without configFile and set default', -> 44 | setCWD '/cwd' 45 | options = processArgs ['--auto-watch', '--auto-watch-interval', '10'] 46 | 47 | expect(options.configFile).to.equal '/cwd/karma.conf.js' 48 | expect(options.autoWatch).to.equal true 49 | expect(options.autoWatchInterval).to.equal 10 50 | 51 | 52 | it 'should set default karma.conf.coffee config file if exists', -> 53 | setCWD '/cwd2' 54 | options = processArgs ['--port', '10'] 55 | 56 | expect(options.configFile).to.equal '/cwd2/karma.conf.coffee' 57 | 58 | 59 | it 'should not set default config if neither exists', -> 60 | setCWD '/' 61 | options = processArgs [] 62 | 63 | expect(options.configFile).to.equal null 64 | 65 | 66 | it 'should parse auto-watch, colors, singleRun to boolean', -> 67 | options = processArgs ['--auto-watch', 'false', '--colors', 'false', '--single-run', 'false'] 68 | 69 | expect(options.autoWatch).to.equal false 70 | expect(options.colors).to.equal false 71 | expect(options.singleRun).to.equal false 72 | 73 | options = processArgs ['--auto-watch', 'true', '--colors', 'true', '--single-run', 'true'] 74 | 75 | expect(options.autoWatch).to.equal true 76 | expect(options.colors).to.equal true 77 | expect(options.singleRun).to.equal true 78 | 79 | 80 | it 'should replace log-level constants', -> 81 | options = processArgs ['--log-level', 'debug'] 82 | expect(options.logLevel).to.equal constant.LOG_DEBUG 83 | 84 | options = processArgs ['--log-level', 'error'] 85 | expect(options.logLevel).to.equal constant.LOG_ERROR 86 | 87 | options = processArgs ['--log-level', 'warn'] 88 | expect(options.logLevel).to.equal constant.LOG_WARN 89 | 90 | 91 | it 'should parse browsers into an array', -> 92 | options = processArgs ['--browsers', 'Chrome,ChromeCanary,Firefox'] 93 | expect(options.browsers).to.deep.equal ['Chrome', 'ChromeCanary', 'Firefox'] 94 | 95 | 96 | it 'should resolve configFile to absolute path', -> 97 | setCWD '/cwd' 98 | options = processArgs ['some/config.js'] 99 | expect(options.configFile).to.equal '/cwd/some/config.js' 100 | 101 | 102 | it 'should parse report-slower-than to a number', -> 103 | options = processArgs ['--report-slower-than', '2000'] 104 | expect(options.reportSlowerThan).to.equal 2000 105 | 106 | options = processArgs ['--no-report-slower-than'] 107 | expect(options.reportSlowerThan).to.equal 0 108 | 109 | 110 | it 'should cast reporters to array', -> 111 | options = processArgs ['--reporters', 'dots,junit'] 112 | expect(options.reporters).to.deep.equal ['dots', 'junit'] 113 | 114 | options = processArgs ['--reporters', 'dots'] 115 | expect(options.reporters).to.deep.equal ['dots'] 116 | 117 | 118 | it 'should parse removed/added/changed files to array', -> 119 | options = processArgs [ 120 | '--removed-files', 'r1.js,r2.js', 121 | '--changed-files', 'ch1.js,ch2.js', 122 | '--added-files', 'a1.js,a2.js' 123 | ] 124 | 125 | expect(options.removedFiles).to.deep.equal ['r1.js', 'r2.js'] 126 | expect(options.addedFiles).to.deep.equal ['a1.js', 'a2.js'] 127 | expect(options.changedFiles).to.deep.equal ['ch1.js', 'ch2.js'] 128 | 129 | 130 | describe 'parseClientArgs', -> 131 | it 'should return arguments after --', -> 132 | args = cli.parseClientArgs ['node', 'karma.js', 'runArg', '--flag', '--', '--foo', '--bar', 133 | 'baz'] 134 | expect(args).to.deep.equal ['--foo', '--bar', 'baz'] 135 | 136 | it 'should return empty args if -- is not present', -> 137 | args = cli.parseClientArgs ['node', 'karma.js', 'runArg', '--flag', '--foo', '--bar', 'baz'] 138 | expect(args).to.deep.equal [] 139 | 140 | 141 | describe 'argsBeforeDoubleDash', -> 142 | it 'should return array of args that occur before --', -> 143 | args = cli.argsBeforeDoubleDash ['aa', '--bb', 'value', '--', 'some', '--no-more'] 144 | expect(args).to.deep.equal ['aa', '--bb', 'value'] 145 | -------------------------------------------------------------------------------- /lib/launchers/Base.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var rimraf = require('rimraf'); 5 | 6 | var log = require('../logger').create('launcher'); 7 | var env = process.env; 8 | 9 | var BEING_CAPTURED = 1; 10 | var CAPTURED = 2; 11 | var BEING_KILLED = 3; 12 | var FINISHED = 4; 13 | var BEING_TIMEOUTED = 5; 14 | 15 | 16 | var BaseBrowser = function(id, emitter, captureTimeout, retryLimit) { 17 | var self = this; 18 | var capturingUrl; 19 | var exitCallback = function() {}; 20 | 21 | this.id = id; 22 | this.state = null; 23 | this._tempDir = path.normalize((env.TMPDIR || env.TMP || env.TEMP || '/tmp') + '/karma-' + 24 | id.toString()); 25 | 26 | 27 | this.start = function(url) { 28 | capturingUrl = url; 29 | self.state = BEING_CAPTURED; 30 | 31 | try { 32 | log.debug('Creating temp dir at ' + self._tempDir); 33 | fs.mkdirSync(self._tempDir); 34 | } catch (e) {} 35 | 36 | self._start(capturingUrl + '?id=' + self.id); 37 | 38 | if (captureTimeout) { 39 | setTimeout(self._onTimeout, captureTimeout); 40 | } 41 | }; 42 | 43 | 44 | this._start = function(url) { 45 | self._execCommand(self._getCommand(), self._getOptions(url)); 46 | }; 47 | 48 | 49 | this.markCaptured = function() { 50 | self.state = CAPTURED; 51 | }; 52 | 53 | 54 | this.isCaptured = function() { 55 | return self.state === CAPTURED; 56 | }; 57 | 58 | 59 | this.kill = function(callback) { 60 | exitCallback = callback || function() {}; 61 | 62 | log.debug('Killing %s', self.name); 63 | 64 | if (self.state !== FINISHED) { 65 | self.state = BEING_KILLED; 66 | self._process.kill(); 67 | } else { 68 | process.nextTick(exitCallback); 69 | } 70 | }; 71 | 72 | 73 | this._onTimeout = function() { 74 | if (self.state !== BEING_CAPTURED) { 75 | return; 76 | } 77 | 78 | log.warn('%s have not captured in %d ms, killing.', self.name, captureTimeout); 79 | 80 | self.state = BEING_TIMEOUTED; 81 | self._process.kill(); 82 | }; 83 | 84 | 85 | this.toString = function() { 86 | return self.name; 87 | }; 88 | 89 | 90 | this._getCommand = function() { 91 | var cmd = path.normalize(env[self.ENV_CMD] || self.DEFAULT_CMD[process.platform]); 92 | 93 | if (!cmd) { 94 | log.error('No binary for %s browser on your platform.\n\t' + 95 | 'Please, set "%s" env variable.', self.name, self.ENV_CMD); 96 | } 97 | 98 | return cmd; 99 | }; 100 | 101 | 102 | this._execCommand = function(cmd, args) { 103 | // normalize the cmd, remove quotes (spawn does not like them) 104 | if (cmd.charAt(0) === cmd.charAt(cmd.length - 1) && '\'`"'.indexOf(cmd.charAt(0)) !== -1) { 105 | cmd = cmd.substring(1, cmd.length - 1); 106 | log.warn('The path should not be quoted.\n Normalized the path to %s', cmd); 107 | } 108 | 109 | log.debug(cmd + ' ' + args.join(' ')); 110 | self._process = spawn(cmd, args); 111 | 112 | var errorOutput = ''; 113 | 114 | self._process.on('close', function(code) { 115 | self._onProcessExit(code, errorOutput); 116 | }); 117 | 118 | self._process.on('error', function(err) { 119 | if (err.code === 'ENOENT') { 120 | retryLimit = 0; 121 | errorOutput = 'Can not find the binary ' + cmd + '\n\t' + 122 | 'Please set env variable ' + self.ENV_CMD; 123 | } else { 124 | errorOutput += err.toString(); 125 | } 126 | }); 127 | 128 | // Node 0.8 does not emit the error 129 | if (process.versions.node.indexOf('0.8') === 0) { 130 | self._process.stderr.on('data', function(data) { 131 | var msg = data.toString(); 132 | 133 | if (msg.indexOf('No such file or directory') !== -1) { 134 | retryLimit = 0; 135 | errorOutput = 'Can not find the binary ' + cmd + '\n\t' + 136 | 'Please set env variable ' + self.ENV_CMD; 137 | } else { 138 | errorOutput += msg; 139 | } 140 | }); 141 | } 142 | }; 143 | 144 | 145 | this._onProcessExit = function(code, errorOutput) { 146 | log.debug('Process %s exitted with code %d', self.name, code); 147 | 148 | if (self.state === BEING_CAPTURED) { 149 | log.error('Cannot start %s\n\t%s', self.name, errorOutput); 150 | } 151 | 152 | if (self.state === CAPTURED) { 153 | log.error('%s crashed.\n\t%s', self.name, errorOutput); 154 | } 155 | 156 | retryLimit--; 157 | 158 | if (self.state === BEING_CAPTURED || self.state === BEING_TIMEOUTED) { 159 | if (retryLimit > 0) { 160 | return self._cleanUpTmp(function() { 161 | log.info('Trying to start %s again.', self.name); 162 | self.start(capturingUrl); 163 | }); 164 | } else { 165 | emitter.emit('browser_process_failure', self); 166 | } 167 | } 168 | 169 | self.state = FINISHED; 170 | self._cleanUpTmp(exitCallback); 171 | }; 172 | 173 | 174 | this._cleanUpTmp = function(done) { 175 | log.debug('Cleaning temp dir %s', self._tempDir); 176 | rimraf(self._tempDir, done); 177 | }; 178 | 179 | 180 | this._getOptions = function(url) { 181 | return [url]; 182 | }; 183 | }; 184 | 185 | var baseBrowserDecoratorFactory = function(id, emitter, timeout) { 186 | return function(self) { 187 | BaseBrowser.call(self, id, emitter, timeout, 3); 188 | }; 189 | }; 190 | baseBrowserDecoratorFactory.$inject = ['id', 'emitter', 'config.captureTimeout']; 191 | 192 | 193 | // PUBLISH 194 | exports.BaseBrowser = BaseBrowser; 195 | exports.decoratorFactory = baseBrowserDecoratorFactory; 196 | -------------------------------------------------------------------------------- /test/unit/middleware/proxy.spec.coffee: -------------------------------------------------------------------------------- 1 | #============================================================================== 2 | # lib/proxy.js module 3 | #============================================================================== 4 | describe 'middleware.proxy', -> 5 | fsMock = require('mocks').fs 6 | httpMock = require('mocks').http 7 | loadFile = require('mocks').loadFile 8 | 9 | actualOptions = requestedUrl = response = nextSpy = null 10 | 11 | m = loadFile __dirname + '/../../../lib/middleware/proxy.js', {'http-proxy': {}} 12 | 13 | mockProxy = 14 | on: -> 15 | proxyRequest: (req, res, opt) -> 16 | actualOptions = opt 17 | requestedUrl = req.url 18 | res.writeHead 200 19 | res.end 'DONE' 20 | 21 | beforeEach -> 22 | actualOptions = {} 23 | requestedUrl = '' 24 | response = new httpMock.ServerResponse 25 | nextSpy = sinon.spy() 26 | 27 | 28 | it 'should proxy requests', (done) -> 29 | proxy = m.createProxyHandler mockProxy, {'/proxy': 'http://localhost:9000'}, true 30 | proxy new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy 31 | 32 | expect(nextSpy).not.to.have.been.called 33 | expect(requestedUrl).to.equal '/test.html' 34 | expect(actualOptions).to.deep.equal { 35 | host: 'localhost', 36 | port: '9000', 37 | target:{https:false, rejectUnauthorized:true} 38 | } 39 | done() 40 | 41 | it 'should enable https', (done) -> 42 | proxy = m.createProxyHandler mockProxy, {'/proxy': 'https://localhost:9000'}, true 43 | proxy new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy 44 | 45 | expect(nextSpy).not.to.have.been.called 46 | expect(requestedUrl).to.equal '/test.html' 47 | expect(actualOptions).to.deep.equal { 48 | host: 'localhost', 49 | port: '9000', 50 | target:{https:true, rejectUnauthorized:true} 51 | } 52 | done() 53 | 54 | it 'disable ssl validation', (done) -> 55 | proxy = m.createProxyHandler mockProxy, {'/proxy': 'https://localhost:9000'}, false 56 | proxy new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy 57 | 58 | expect(nextSpy).not.to.have.been.called 59 | expect(requestedUrl).to.equal '/test.html' 60 | expect(actualOptions).to.deep.equal { 61 | host: 'localhost', 62 | port: '9000', 63 | target:{https:true, rejectUnauthorized:false} 64 | } 65 | done() 66 | 67 | it 'should support multiple proxies', -> 68 | proxy = m.createProxyHandler mockProxy, { 69 | '/proxy': 'http://localhost:9000' 70 | '/static': 'http://gstatic.com' 71 | }, true 72 | proxy new httpMock.ServerRequest('/static/test.html'), response, nextSpy 73 | 74 | expect(nextSpy).not.to.have.been.called 75 | expect(requestedUrl).to.equal '/test.html' 76 | expect(actualOptions).to.deep.equal { 77 | host: 'gstatic.com', 78 | port: '80', 79 | target:{https:false, rejectUnauthorized:true} 80 | } 81 | 82 | it 'should handle nested proxies', -> 83 | proxy = m.createProxyHandler mockProxy, { 84 | '/sub': 'http://localhost:9000' 85 | '/sub/some': 'http://gstatic.com/something' 86 | }, true 87 | proxy new httpMock.ServerRequest('/sub/some/Test.html'), response, nextSpy 88 | 89 | expect(nextSpy).not.to.have.been.called 90 | expect(requestedUrl).to.equal '/something/Test.html' 91 | expect(actualOptions).to.deep.equal { 92 | host: 'gstatic.com', 93 | port: '80', 94 | target:{https:false, rejectUnauthorized:true} 95 | } 96 | 97 | 98 | it 'should call next handler if the path is not proxied', -> 99 | proxy = m.createProxyHandler mockProxy, {'/proxy': 'http://localhost:9000'} 100 | proxy new httpMock.ServerRequest('/non/proxy/test.html'), response, nextSpy 101 | 102 | expect(nextSpy).to.have.been.called 103 | 104 | 105 | it 'should call next handler if no proxy defined', -> 106 | proxy = m.createProxyHandler mockProxy, {} 107 | proxy new httpMock.ServerRequest('/non/proxy/test.html'), response, nextSpy 108 | 109 | expect(nextSpy).to.have.been.called 110 | 111 | 112 | it 'should parse a simple proxy config', -> 113 | proxy = {'/base/': 'http://localhost:8000/'} 114 | parsedProxyConfig = m.parseProxyConfig proxy 115 | expect(parsedProxyConfig).to.deep.equal { 116 | '/base/': {host: 'localhost', port: '8000', baseProxyUrl: '/', https:false} 117 | } 118 | 119 | it 'should set defualt http port', -> 120 | proxy = {'/base/': 'http://localhost/'} 121 | parsedProxyConfig = m.parseProxyConfig proxy 122 | expect(parsedProxyConfig).to.deep.equal { 123 | '/base/': {host: 'localhost', port: '80', baseProxyUrl: '/', https:false} 124 | } 125 | 126 | it 'should set defualt https port', -> 127 | proxy = {'/base/': 'https://localhost/'} 128 | parsedProxyConfig = m.parseProxyConfig proxy 129 | expect(parsedProxyConfig).to.deep.equal { 130 | '/base/': {host: 'localhost', port: '443', baseProxyUrl: '/', https:true} 131 | } 132 | 133 | 134 | it 'should handle proxy configs with paths', -> 135 | proxy = {'/base': 'http://localhost:8000/proxy'} 136 | parsedProxyConfig = m.parseProxyConfig proxy 137 | expect(parsedProxyConfig).to.deep.equal { 138 | '/base': {host: 'localhost', port: '8000', baseProxyUrl: '/proxy', https:false} 139 | } 140 | 141 | it 'should determine protocol', -> 142 | proxy = {'/base':'https://localhost:8000'} 143 | parsedProxyConfig = m.parseProxyConfig proxy 144 | expect(parsedProxyConfig).to.deep.equal { 145 | '/base': {host: 'localhost', port: '8000', baseProxyUrl: '', https: true} 146 | } 147 | 148 | it 'should handle empty proxy config', -> 149 | expect(m.parseProxyConfig {}).to.deep.equal({}) 150 | --------------------------------------------------------------------------------