├── .codeclimate.yml ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .mailmap ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── karma ├── client ├── .eslintrc ├── constants.js ├── karma.js ├── main.js └── updater.js ├── commitlint.config.js ├── common ├── stringify.js └── util.js ├── config.tpl.coffee ├── config.tpl.js ├── config.tpl.ls ├── config.tpl.ts ├── context ├── karma.js └── main.js ├── cucumber.js ├── docs ├── about │ ├── 01-versioning.md │ └── 03-migration.md ├── config │ ├── 01-configuration-file.md │ ├── 02-files.md │ ├── 03-browsers.md │ ├── 04-preprocessors.md │ └── 05-plugins.md ├── dev │ ├── 01-contributing.md │ ├── 02-making-changes.md │ ├── 03-maintaining.md │ ├── 04-public-api.md │ ├── 05-plugins.md │ └── 06-git-commit-msg.md ├── index.md ├── intro │ ├── 01-installation.md │ ├── 02-configuration.md │ ├── 03-how-it-works.md │ ├── 04-faq.md │ └── 05-troubleshooting.md └── plus │ ├── 01-requirejs.md │ ├── 02-travis.md │ ├── 03-jenkins.md │ ├── 04-semaphore.md │ ├── 05-cloud9.md │ ├── 06-angularjs.md │ ├── 07-yeoman.md │ ├── 08-emberjs.md │ ├── 09-codio.md │ └── 10-teamcity.md ├── lib ├── browser.js ├── browser_collection.js ├── browser_result.js ├── cli.js ├── completion.js ├── config.js ├── constants.js ├── detached.js ├── emitter_wrapper.js ├── events.js ├── executor.js ├── file-list.js ├── file.js ├── helper.js ├── index.js ├── init.js ├── init │ ├── color_schemes.js │ ├── formatters.js │ ├── log-queue.js │ └── state_machine.js ├── launcher.js ├── launchers │ ├── base.js │ ├── capture_timeout.js │ ├── process.js │ └── retry.js ├── logger.js ├── middleware │ ├── common.js │ ├── karma.js │ ├── proxy.js │ ├── runner.js │ ├── source_files.js │ ├── stopper.js │ └── strip_host.js ├── plugin.js ├── preprocessor.js ├── reporter.js ├── reporters │ ├── base.js │ ├── base_color.js │ ├── dots.js │ ├── dots_color.js │ ├── multi.js │ ├── progress.js │ └── progress_color.js ├── runner.js ├── server.js ├── stopper.js ├── temp_dir.js ├── url.js ├── utils │ ├── crypto-utils.js │ ├── dns-utils.js │ ├── file-utils.js │ ├── net-utils.js │ ├── path-utils.js │ └── pattern-utils.js ├── watcher.js └── web-server.js ├── logo ├── banner.png ├── favicon.ico ├── logo.ai ├── logo.eps ├── logo.png └── logo.svg ├── package-lock.json ├── package.json ├── release.config.js ├── requirejs.config.tpl.coffee ├── requirejs.config.tpl.js ├── scripts ├── client.js ├── integration-tests.sh └── karma-completion.sh ├── static ├── client.html ├── client_with_context.html ├── context.html ├── context.js ├── debug.html ├── debug.js ├── favicon.ico └── karma.js ├── test ├── .eslintrc ├── client │ ├── .eslintrc │ ├── browser-exceptions.log │ ├── karma.conf.js │ ├── karma.spec.js │ ├── mocks.js │ ├── stringify.spec.js │ └── util.spec.js ├── e2e │ ├── .eslintrc │ ├── basic.feature │ ├── browser_console.feature │ ├── cli.feature │ ├── custom-context.feature │ ├── displayname.feature │ ├── error.feature │ ├── files.feature │ ├── headers.feature │ ├── helpful-logs.feature │ ├── launcher-error.feature │ ├── load.feature │ ├── middleware.feature │ ├── mocharepoter.feature │ ├── module-types.feature │ ├── pass-opts.feature │ ├── proxy.feature │ ├── reconnecting.feature │ ├── reporting.feature │ ├── restart-on-change.feature │ ├── runInParent.feature │ ├── step_definitions │ │ ├── core_steps.js │ │ ├── hooks.js │ │ └── utils.js │ ├── stop.feature │ ├── support │ │ ├── basic │ │ │ ├── plus.js │ │ │ └── test.js │ │ ├── behind-proxy │ │ │ ├── plus.js │ │ │ └── test.js │ │ ├── browser-console │ │ │ ├── log.js │ │ │ └── test.js │ │ ├── context │ │ │ ├── context2.html │ │ │ └── test.js │ │ ├── env.js │ │ ├── error │ │ │ ├── import-something-from-somewhere.js │ │ │ ├── test.js │ │ │ └── under-test.js │ │ ├── files │ │ │ ├── log_foo.js │ │ │ └── test.js │ │ ├── headers │ │ │ ├── foo.js │ │ │ └── test.js │ │ ├── launcher-error │ │ │ ├── fake-browser.sh │ │ │ └── specs.js │ │ ├── middleware │ │ │ ├── middleware.js │ │ │ └── test.js │ │ ├── mocha │ │ │ ├── plus.js │ │ │ └── test.js │ │ ├── modules │ │ │ ├── __tests__ │ │ │ │ ├── minus.test.mjs │ │ │ │ └── plus.test.js │ │ │ ├── minus.mjs │ │ │ └── plus.js │ │ ├── pass-opts │ │ │ └── test.js │ │ ├── proxy.js │ │ ├── proxy │ │ │ ├── .tern-port │ │ │ ├── foo.js │ │ │ ├── plugin.js │ │ │ └── test.js │ │ ├── reconnecting │ │ │ ├── plus.js │ │ │ └── test.js │ │ ├── reporting │ │ │ └── test.js │ │ ├── tag │ │ │ ├── tag.js │ │ │ ├── test-with-version.js │ │ │ └── test-without-version.js │ │ ├── timeout │ │ │ ├── fake-browser.sh │ │ │ └── specs.js │ │ └── world.js │ ├── tag.feature │ ├── timeout.feature │ └── upstream-proxy.feature ├── mocha.opts └── unit │ ├── browser.spec.js │ ├── browser_collection.spec.js │ ├── browser_result.spec.js │ ├── certificates │ ├── server.crt │ └── server.key │ ├── cli.spec.js │ ├── completion.spec.js │ ├── config.spec.js │ ├── emitter_wrapper.spec.js │ ├── events.spec.js │ ├── executor.spec.js │ ├── file-list.spec.js │ ├── file.spec.js │ ├── fixtures │ ├── format-error-property.js │ └── format-error-root.js │ ├── helper.spec.js │ ├── index.spec.js │ ├── init.spec.js │ ├── init │ ├── formatters.spec.js │ └── state_machine.spec.js │ ├── launcher.spec.js │ ├── launchers │ ├── base.spec.js │ ├── capture_timeout.spec.js │ ├── process.spec.js │ └── retry.spec.js │ ├── logger.spec.js │ ├── middleware │ ├── karma.spec.js │ ├── proxy.spec.js │ ├── runner.spec.js │ ├── source_files.spec.js │ └── strip_host.spec.js │ ├── mocha-globals.js │ ├── mocks │ └── timer.js │ ├── plugin.spec.js │ ├── preprocessor.spec.js │ ├── reporter.spec.js │ ├── reporters │ ├── base.spec.js │ └── progress.spec.js │ ├── runner.spec.js │ ├── server.spec.js │ ├── url.spec.js │ ├── utils │ ├── crypto-utils.spec.js │ ├── net-utils.spec.js │ ├── path-utils.spec.js │ └── pattern-utils.spec.js │ ├── watcher.spec.js │ └── web-server.spec.js ├── thesis.pdf ├── tools ├── update-contributors.js └── update-docs.js └── wallaby.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | rubocop: 4 | enabled: false 5 | coffeelint: 6 | enabled: true 7 | eslint: 8 | enabled: true 9 | csslint: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - "**.coffee" 14 | - "**.js" 15 | - "**.css" 16 | exclude_paths: 17 | - node_modules/**/* 18 | - test/**/* 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/e2e/support/sandbox 2 | test/e2e/support/error/under-test.js 3 | test/unit/fixtures/bundled.js 4 | static/karma.js 5 | static/context.js 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "arrow-parens": [2, "always"], 5 | "space-before-function-paren": ["error", "always"] 6 | } 7 | } 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create an actionable bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please read https://karma-runner.github.io/4.0/intro/troubleshooting.html first 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | linux: 10 | name: Linux - Lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: 16 19 | cache: npm 20 | - run: npm ci 21 | - run: | 22 | npm run commitlint -- \ 23 | --verbose \ 24 | --from `git merge-base origin/master $GITHUB_SHA` 25 | - run: npm run lint 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | main: 10 | name: Test, Tag Commit and Release to NPM 11 | runs-on: ubuntu-latest 12 | env: 13 | BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} 14 | BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 15 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 16 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 17 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 18 | GITHUB_TOKEN: ${{ secrets.KARMARUNNERBOT_GITHUB_TOKEN }} 19 | KARMA_TEST_NO_FALLBACK: 1 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | token: ${{ env.GITHUB_TOKEN }} 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: 16 27 | cache: npm 28 | - run: npm ci 29 | - run: npm run lint 30 | - run: npm run build:check 31 | - run: npm run test:unit 32 | - run: npm run test:e2e 33 | - run: npm run test:client 34 | - run: npm run test:integration 35 | - run: npm run semantic-release 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | main: 10 | name: Unit (Client and Server), E2E and Integration Test 11 | runs-on: ubuntu-latest 12 | env: 13 | BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} 14 | BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 15 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 16 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: 16 24 | cache: npm 25 | - run: npm ci 26 | - run: npm run build:check 27 | - run: npm run test:unit 28 | - run: npm run test:e2e 29 | - run: npm run test:client 30 | - run: npm run test:integration 31 | linux: 32 | name: "Node ${{ matrix.node }} on Linux: Server Unit and E2E Test" 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | node: [10, 12, 14, 18] 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions/setup-node@v2 40 | with: 41 | node-version: ${{ matrix.node }} 42 | cache: npm 43 | - run: npm ci 44 | - run: npm run test:unit 45 | - run: npm run test:e2e 46 | windows: 47 | name: "Node ${{ matrix.node }} on Windows: Server Unit and Client Unit Test" 48 | runs-on: windows-latest 49 | strategy: 50 | matrix: 51 | node: [10, 12, 14, 16, 18] 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions/setup-node@v2 55 | with: 56 | node-version: ${{ matrix.node }} 57 | cache: npm 58 | - run: npm ci 59 | - run: npm run test:unit 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea/* 4 | *.iml 5 | docs/_build 6 | *.swp 7 | *.swo 8 | test/e2e/support/sandbox 9 | test/e2e/coverage/coverage 10 | test/e2e/coverageQunit/coverage 11 | test/e2e/coverageRequirejs/coverage 12 | test/e2e/coffee-coverage/coverage 13 | test-results.xml 14 | test/unit/test.log 15 | test/unit/fixtures/bundled.js 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | <bitwiseman@gmail.com> <lnewman@book.com> 2 | <vojta.jina@gmail.com> <vojta@google.com> 3 | <friedel.ziegelmayer@gmail.com> <dignifiedquire@gmail.com> 4 | Michał Gołębiowski-Owczarek <m.goleb@gmail.com> 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.tgz 3 | 4 | tmp 5 | test 6 | tasks 7 | /tools/ 8 | docs 9 | client 10 | logo 11 | integration-tests 12 | 13 | TODO.md 14 | CONTRIBUTING.md 15 | Gruntfile.coffee 16 | credentials 17 | Karma.sublime-* 18 | 19 | static/karma.src.js 20 | static/karma.wrapper 21 | test-results.xml 22 | thesis.pdf 23 | mocha-watch.sh 24 | mocha-watch-debug.sh 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.0.0, available at <https://www.contributor-covenant.org/version/1/0/0/code-of-conduct/> 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Karma 2 | If you are thinking about making Karma better, or you just want to hack on it, that’s great! 3 | 4 | > Check out the docs on how to [get started][docs_contributing] and please follow 5 | > the [code of conduct](CODE_OF_CONDUCT.md). 6 | 7 | 8 | ## Got a Question or Problem? 9 | 10 | If you have questions about how to use Karma, please direct these to the [Gitter][gitter] 11 | discussion list or [Stack Overflow][stackoverflow]. 12 | 13 | ## Found an Issue? 14 | If you find a bug in the source code or a mistake in the documentation, you can help us by 15 | submitting an issue to our [GitHub Repository][github_newissue]. Even better you can submit a Pull Request 16 | with a fix. 17 | 18 | **Working on your first Pull Request?** You can learn how from this *free* series 19 | [How to Contribute to an Open Source Project on GitHub][egghead_series] 20 | 21 | [docs_contributing]: https://karma-runner.github.io/latest/dev/contributing.html 22 | [gitter]: https://gitter.im/karma-runner/karma 23 | [stackoverflow]: https://stackoverflow.com/questions/tagged/karma-runner 24 | [github_newissue]: https://github.com/karma-runner/karma/issues/new 25 | [egghead_series]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 26 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behaviour 2 | 3 | ### Actual behaviour 4 | 5 | ### Environment Details 6 | 7 | - Karma version (output of `karma --version`): 8 | - Relevant part of your `karma.config.js` file 9 | 10 | ### Steps to reproduce the behaviour 11 | 12 | 1. 13 | 2. 14 | 3. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2011-2021 Google, Inc. 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version of the project are currently being supported with security updates. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a security issue, please email karma-runner-eng+security@google.com 10 | with a description of the issue, the steps you took to create the issue, 11 | affected versions, and if known, mitigations for the issue. 12 | -------------------------------------------------------------------------------- /bin/karma: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli').run(); 4 | -------------------------------------------------------------------------------- /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | VERSION: '%KARMA_VERSION%', 3 | KARMA_URL_ROOT: '%KARMA_URL_ROOT%', 4 | KARMA_PROXY_PATH: '%KARMA_PROXY_PATH%', 5 | BROWSER_SOCKET_TIMEOUT: '%BROWSER_SOCKET_TIMEOUT%', 6 | CONTEXT_URL: 'context.html' 7 | } 8 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | /* global io */ 2 | /* eslint-disable no-new */ 3 | 4 | var Karma = require('./karma') 5 | var StatusUpdater = require('./updater') 6 | var util = require('../common/util') 7 | var constants = require('./constants') 8 | 9 | var KARMA_URL_ROOT = constants.KARMA_URL_ROOT 10 | var KARMA_PROXY_PATH = constants.KARMA_PROXY_PATH 11 | var BROWSER_SOCKET_TIMEOUT = constants.BROWSER_SOCKET_TIMEOUT 12 | 13 | // Connect to the server using socket.io https://socket.io/ 14 | var socket = io(location.host, { 15 | reconnectionDelay: 500, 16 | reconnectionDelayMax: Infinity, 17 | timeout: BROWSER_SOCKET_TIMEOUT, 18 | path: KARMA_PROXY_PATH + KARMA_URL_ROOT.slice(1) + 'socket.io', 19 | 'sync disconnect on unload': true, 20 | useNativeTimers: true 21 | }) 22 | 23 | // instantiate the updater of the view 24 | var updater = new StatusUpdater(socket, util.elm('title'), util.elm('banner'), util.elm('browsers')) 25 | window.karma = new Karma(updater, socket, util.elm('context'), window.open, 26 | window.navigator, window.location, window.document) 27 | -------------------------------------------------------------------------------- /client/updater.js: -------------------------------------------------------------------------------- 1 | var VERSION = require('./constants').VERSION 2 | 3 | function StatusUpdater (socket, titleElement, bannerElement, browsersElement) { 4 | function updateBrowsersInfo (browsers) { 5 | if (!browsersElement) { 6 | return 7 | } 8 | var status 9 | 10 | // clear browsersElement 11 | while (browsersElement.firstChild) { 12 | browsersElement.removeChild(browsersElement.firstChild) 13 | } 14 | 15 | for (var i = 0; i < browsers.length; i++) { 16 | status = browsers[i].isConnected ? 'idle' : 'executing' 17 | var li = document.createElement('li') 18 | li.setAttribute('class', status) 19 | li.textContent = browsers[i].name + ' is ' + status 20 | browsersElement.appendChild(li) 21 | } 22 | } 23 | 24 | var connectionText = 'never-connected' 25 | var testText = 'loading' 26 | var pingText = '' 27 | 28 | function updateBanner () { 29 | if (!titleElement || !bannerElement) { 30 | return 31 | } 32 | titleElement.textContent = 'Karma v ' + VERSION + ' - ' + connectionText + '; test: ' + testText + '; ' + pingText 33 | bannerElement.className = connectionText === 'connected' ? 'online' : 'offline' 34 | } 35 | 36 | function updateConnectionStatus (connectionStatus) { 37 | connectionText = connectionStatus || connectionText 38 | updateBanner() 39 | } 40 | function updateTestStatus (testStatus) { 41 | testText = testStatus || testText 42 | updateBanner() 43 | } 44 | function updatePingStatus (pingStatus) { 45 | pingText = pingStatus || pingText 46 | updateBanner() 47 | } 48 | 49 | socket.on('connect', function () { 50 | updateConnectionStatus('connected') 51 | }) 52 | socket.on('disconnect', function () { 53 | updateConnectionStatus('disconnected') 54 | }) 55 | socket.on('reconnecting', function (sec) { 56 | updateConnectionStatus('reconnecting in ' + sec + ' seconds') 57 | }) 58 | socket.on('reconnect', function () { 59 | updateConnectionStatus('reconnected') 60 | }) 61 | socket.on('reconnect_failed', function () { 62 | updateConnectionStatus('reconnect_failed') 63 | }) 64 | 65 | socket.on('info', updateBrowsersInfo) 66 | socket.on('disconnect', function () { 67 | updateBrowsersInfo([]) 68 | }) 69 | 70 | socket.on('ping', function () { 71 | updatePingStatus('ping...') 72 | }) 73 | socket.on('pong', function (latency) { 74 | updatePingStatus('ping ' + latency + 'ms') 75 | }) 76 | 77 | return { updateTestStatus: updateTestStatus } 78 | } 79 | 80 | module.exports = StatusUpdater 81 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-angular'] } 2 | -------------------------------------------------------------------------------- /common/stringify.js: -------------------------------------------------------------------------------- 1 | var serialize = null 2 | try { 3 | serialize = require('dom-serialize') 4 | } catch (e) { 5 | // Ignore failure on IE8 6 | } 7 | 8 | var instanceOf = require('./util').instanceOf 9 | 10 | function isNode (obj) { 11 | return (obj.tagName || obj.nodeName) && obj.nodeType 12 | } 13 | 14 | function stringify (obj, depth) { 15 | if (depth === 0) { 16 | return '...' 17 | } 18 | 19 | if (obj === null) { 20 | return 'null' 21 | } 22 | 23 | switch (typeof obj) { 24 | case 'symbol': 25 | return obj.toString() 26 | case 'string': 27 | return "'" + obj + "'" 28 | case 'undefined': 29 | return 'undefined' 30 | case 'function': 31 | try { 32 | // function abc(a, b, c) { /* code goes here */ } 33 | // -> function abc(a, b, c) { ... } 34 | return obj.toString().replace(/\{[\s\S]*\}/, '{ ... }') 35 | } catch (err) { 36 | if (err instanceof TypeError) { 37 | // Support older browsers 38 | return 'function ' + (obj.name || '') + '() { ... }' 39 | } else { 40 | throw err 41 | } 42 | } 43 | case 'boolean': 44 | return obj ? 'true' : 'false' 45 | case 'object': 46 | var strs = [] 47 | if (instanceOf(obj, 'Array')) { 48 | strs.push('[') 49 | for (var i = 0, ii = obj.length; i < ii; i++) { 50 | if (i) { 51 | strs.push(', ') 52 | } 53 | strs.push(stringify(obj[i], depth - 1)) 54 | } 55 | strs.push(']') 56 | } else if (instanceOf(obj, 'Date')) { 57 | return obj.toString() 58 | } else if (instanceOf(obj, 'Text')) { 59 | return obj.nodeValue 60 | } else if (instanceOf(obj, 'Comment')) { 61 | return '<!--' + obj.nodeValue + '-->' 62 | } else if (obj.outerHTML) { 63 | return obj.outerHTML 64 | } else if (isNode(obj)) { 65 | if (serialize) { 66 | return serialize(obj) 67 | } else { 68 | return 'Skipping stringify, no support for dom-serialize' 69 | } 70 | } else if (instanceOf(obj, 'Error')) { 71 | return obj.toString() + '\n' + obj.stack 72 | } else { 73 | var constructor = 'Object' 74 | if (obj.constructor && typeof obj.constructor === 'function') { 75 | constructor = obj.constructor.name 76 | } 77 | 78 | strs.push(constructor) 79 | strs.push('{') 80 | var first = true 81 | for (var key in obj) { 82 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 83 | if (first) { 84 | first = false 85 | } else { 86 | strs.push(', ') 87 | } 88 | 89 | strs.push(key + ': ' + stringify(obj[key], depth - 1)) 90 | } 91 | } 92 | strs.push('}') 93 | } 94 | return strs.join('') 95 | default: 96 | return obj 97 | } 98 | } 99 | 100 | module.exports = stringify 101 | -------------------------------------------------------------------------------- /common/util.js: -------------------------------------------------------------------------------- 1 | exports.instanceOf = function (value, constructorName) { 2 | return Object.prototype.toString.apply(value) === '[object ' + constructorName + ']' 3 | } 4 | 5 | exports.elm = function (id) { 6 | return document.getElementById(id) 7 | } 8 | 9 | exports.generateId = function (prefix) { 10 | return prefix + Math.floor(Math.random() * 10000) 11 | } 12 | 13 | exports.isUndefined = function (value) { 14 | return typeof value === 'undefined' 15 | } 16 | 17 | exports.isDefined = function (value) { 18 | return !exports.isUndefined(value) 19 | } 20 | 21 | exports.parseQueryParams = function (locationSearch) { 22 | var params = {} 23 | var pairs = locationSearch.slice(1).split('&') 24 | var keyValue 25 | 26 | for (var i = 0; i < pairs.length; i++) { 27 | keyValue = pairs[i].split('=') 28 | params[decodeURIComponent(keyValue[0])] = decodeURIComponent(keyValue[1]) 29 | } 30 | 31 | return params 32 | } 33 | -------------------------------------------------------------------------------- /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 | 11 | # frameworks to use 12 | # available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 13 | frameworks: [%FRAMEWORKS%] 14 | 15 | 16 | # list of files / patterns to load in the browser 17 | files: [%FILES% 18 | ] 19 | 20 | 21 | # list of files / patterns to exclude 22 | exclude: [%EXCLUDE% 23 | ] 24 | 25 | 26 | # preprocess matching files before serving them to the browser 27 | # available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 28 | preprocessors: {%PREPROCESSORS% 29 | } 30 | 31 | 32 | # test results reporter to use 33 | # possible values: 'dots', 'progress' 34 | # available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 35 | reporters: ['progress'] 36 | 37 | 38 | # web server port 39 | port: 9876 40 | 41 | 42 | # enable / disable colors in the output (reporters and logs) 43 | colors: true 44 | 45 | 46 | # level of logging 47 | # possible values: 48 | # - config.LOG_DISABLE 49 | # - config.LOG_ERROR 50 | # - config.LOG_WARN 51 | # - config.LOG_INFO 52 | # - config.LOG_DEBUG 53 | logLevel: config.LOG_INFO 54 | 55 | 56 | # enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: %AUTO_WATCH% 58 | 59 | 60 | # start these browsers 61 | # available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 62 | browsers: [%BROWSERS%] 63 | 64 | 65 | # Continuous Integration mode 66 | # if true, Karma captures browsers, runs the tests and exits 67 | singleRun: false 68 | 69 | # Concurrency level 70 | # how many browser instances should be started simultaneously 71 | concurrency: Infinity 72 | -------------------------------------------------------------------------------- /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 all patterns (eg. files, exclude) 8 | basePath: '%BASE_PATH%', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 13 | frameworks: [%FRAMEWORKS%], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [%FILES% 18 | ], 19 | 20 | 21 | // list of files / patterns to exclude 22 | exclude: [%EXCLUDE% 23 | ], 24 | 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 28 | preprocessors: {%PREPROCESSORS% 29 | }, 30 | 31 | 32 | // test results reporter to use 33 | // possible values: 'dots', 'progress' 34 | // available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 35 | reporters: ['progress'], 36 | 37 | 38 | // web server port 39 | port: 9876, 40 | 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | 51 | // enable / disable watching file and executing tests whenever any file changes 52 | autoWatch: %AUTO_WATCH%, 53 | 54 | 55 | // start these browsers 56 | // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 57 | browsers: [%BROWSERS%], 58 | 59 | 60 | // Continuous Integration mode 61 | // if true, Karma captures browsers, runs the tests and exits 62 | singleRun: false, 63 | 64 | // Concurrency level 65 | // how many browser instances should be started simultaneously 66 | concurrency: Infinity 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /config.tpl.ls: -------------------------------------------------------------------------------- 1 | # Karma configuration 2 | # Generated on %DATE% 3 | 4 | module.exports = (config) -> 5 | config.set do 6 | 7 | # base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '%BASE_PATH%' 9 | 10 | 11 | # frameworks to use 12 | # available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 13 | frameworks: [%FRAMEWORKS%] 14 | 15 | 16 | # list of files / patterns to load in the browser 17 | files: [%FILES% 18 | ] 19 | 20 | 21 | # list of files / patterns to exclude 22 | exclude: [%EXCLUDE% 23 | ] 24 | 25 | 26 | # preprocess matching files before serving them to the browser 27 | # available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 28 | preprocessors: {%PREPROCESSORS% 29 | } 30 | 31 | # test results reporter to use 32 | # possible values: 'dots', 'progress' 33 | # available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 34 | reporters: ['progress'] 35 | 36 | 37 | # web server port 38 | port: 9876 39 | 40 | 41 | # enable / disable colors in the output (reporters and logs) 42 | colors: true 43 | 44 | 45 | # level of logging 46 | # possible values: 47 | # - config.LOG_DISABLE 48 | # - config.LOG_ERROR 49 | # - config.LOG_WARN 50 | # - config.LOG_INFO 51 | # - config.LOG_DEBUG 52 | logLevel: config.LOG_INFO 53 | 54 | 55 | # enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: %AUTO_WATCH% 57 | 58 | 59 | # satart these browsers 60 | # available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 61 | browsers: [%BROWSERS%] 62 | 63 | 64 | # Continuous Integration mode 65 | # if true, Karma captures browsers, runs the tests and exits 66 | singleRun: false 67 | 68 | # Concurrency level 69 | # how many browser instances should be started simultaneously 70 | concurrency: Infinity 71 | -------------------------------------------------------------------------------- /config.tpl.ts: -------------------------------------------------------------------------------- 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 | 11 | // frameworks to use 12 | // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter 13 | frameworks: [%FRAMEWORKS%], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [%FILES% 18 | ], 19 | 20 | 21 | // list of files / patterns to exclude 22 | exclude: [%EXCLUDE% 23 | ], 24 | 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor 28 | preprocessors: {%PREPROCESSORS% 29 | }, 30 | 31 | 32 | // test results reporter to use 33 | // possible values: 'dots', 'progress' 34 | // available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter 35 | reporters: ['progress'], 36 | 37 | 38 | // web server port 39 | port: 9876, 40 | 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | 51 | // enable / disable watching file and executing tests whenever any file changes 52 | autoWatch: %AUTO_WATCH%, 53 | 54 | 55 | // start these browsers 56 | // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher 57 | browsers: [%BROWSERS%], 58 | 59 | 60 | // Continuous Integration mode 61 | // if true, Karma captures browsers, runs the tests and exits 62 | singleRun: false, 63 | 64 | // Concurrency level 65 | // how many browser instances should be started simultaneously 66 | concurrency: Infinity 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /context/main.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var ContextKarma = require('./karma') 3 | 4 | // Resolve our parent window 5 | var parentWindow = window.opener || window.parent 6 | 7 | // Define a remote call method for Karma 8 | var callParentKarmaMethod = ContextKarma.getDirectCallParentKarmaMethod(parentWindow) 9 | 10 | // If we don't have access to the window, then use `postMessage` 11 | // DEV: In Electron, we don't have access to the parent window due to it being in a separate process 12 | // DEV: We avoid using this in Internet Explorer as they only support strings 13 | // https://caniuse.com/?search=postmessage 14 | var haveParentAccess = false 15 | try { haveParentAccess = !!parentWindow.window } catch (err) { /* Ignore errors (likely permission errors) */ } 16 | if (!haveParentAccess) { 17 | callParentKarmaMethod = ContextKarma.getPostMessageCallParentKarmaMethod(parentWindow) 18 | } 19 | 20 | // Define a window-scoped Karma 21 | window.__karma__ = new ContextKarma(callParentKarmaMethod) 22 | -------------------------------------------------------------------------------- /cucumber.js: -------------------------------------------------------------------------------- 1 | // Shared configuration for Cucumber.js tests. 2 | // See https://github.com/cucumber/cucumber-js/blob/master/docs/cli.md#profiles 3 | const options = [ 4 | '--format progress', 5 | '--require test/e2e/support/env.js', 6 | '--require test/e2e/support/world.js', 7 | '--require test/e2e/step_definitions/core_steps.js', 8 | '--require test/e2e/step_definitions/hooks.js' 9 | ] 10 | 11 | module.exports = { 12 | default: options.join(' ') 13 | } 14 | -------------------------------------------------------------------------------- /docs/about/01-versioning.md: -------------------------------------------------------------------------------- 1 | Karma uses [Semantic Versioning]. 2 | 3 | It is recommended that you add Karma by running: 4 | 5 | ```bash 6 | $ yarn add --dev karma 7 | ``` 8 | 9 | or: 10 | 11 | ```bash 12 | $ npm --save-dev install karma 13 | ``` 14 | 15 | [Semantic Versioning]: https://semver.org/ 16 | -------------------------------------------------------------------------------- /docs/about/03-migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Migration from v0.10 3 | --- 4 | 5 | The good thing is that you don't have to migrate everything at once. 6 | You can leave all the existing projects using an older version of Karma and only use the latest 7 | version for the new projects. Alternatively, you can migrate the existing projects one at a time... 8 | 9 | 10 | Anyway, this migration should be easy ;-) so let's get started... 11 | 12 | ```bash 13 | cd <path-to-your-project> 14 | npm install karma --save-dev 15 | ``` 16 | This will install the latest version of Karma and also update `package.json` of your project. 17 | 18 | 19 | ## Install missing plugins 20 | Karma does not ship with any "default" plugins anymore. 21 | For existing projects, this should not cause any problems as npm (when updating Karma to 0.10 using 22 | `npm install karma --save-dev`) added these "default" plugins into `package.json` as regular dependencies. 23 | For new projects, just remember you have to install all the plugins you need. These are the "default" plugins that were removed: 24 | - karma-jasmine 25 | - karma-requirejs 26 | - karma-coffee-preprocessor 27 | - karma-html2js-preprocessor 28 | - karma-chrome-launcher 29 | - karma-firefox-launcher 30 | - karma-phantomjs-launcher 31 | - karma-script-launcher 32 | 33 | 34 | ## Install CLI interface 35 | Karma does not put the `karma` command in your system PATH anymore. 36 | If you want to use the `karma` command, please install the command line interface (`karma-cli`). 37 | 38 | You probably have the `karma` package installed globally, in which case you should remove it first: 39 | ```bash 40 | npm remove -g karma 41 | ``` 42 | 43 | And then install the command line interface: 44 | ```bash 45 | npm install -g karma-cli 46 | ``` 47 | 48 | 49 | ## Default configuration 50 | `autoWatch` is true by default, so if you don't wanna use it make sure you set it to `false`. 51 | But hey, give it a shot first, it's really awesome to run your tests on every save! 52 | 53 | 54 | ## npm complaining 55 | In some cases, npm can run into dependency tree issues during the migration process. If you are faced with an "unsatisfied peer dependency" error, removing all of the packages (`rm -rf ./node_modules`) and installing them again should clear up the issue. 56 | 57 | If you have any other issues, please ask on the [mailing list]. 58 | 59 | 60 | [mailing list]: https://groups.google.com/forum/?fromgroups#!forum/karma-users 61 | -------------------------------------------------------------------------------- /docs/config/05-plugins.md: -------------------------------------------------------------------------------- 1 | Karma can be easily extended through plugins. In fact, all the existing preprocessors, reporters, browser launchers and frameworks are plugins. 2 | 3 | You can install [existing plugins] from npm or you can write [your own plugins][developing plugins] for Karma. 4 | 5 | ## Installing Plugins 6 | 7 | The recommended way to install plugins is to add them as project dependencies in your `package.json`: 8 | 9 | ```json 10 | { 11 | "devDependencies": { 12 | "karma": "~0.10", 13 | "karma-mocha": "~0.0.1", 14 | "karma-growl-reporter": "~0.0.1", 15 | "karma-firefox-launcher": "~0.0.1" 16 | } 17 | } 18 | ``` 19 | 20 | Therefore, a simple way to install a plugin is: 21 | 22 | ```bash 23 | npm install karma-<plugin name> --save-dev 24 | ``` 25 | 26 | ## Loading Plugins 27 | 28 | By default, Karma loads plugins from all sibling npm packages which have a name starting with `karma-*`. 29 | 30 | You can also override this behavior and explicitly list plugins you want to load via the `plugins` configuration setting: 31 | 32 | ```javascript 33 | config.set({ 34 | plugins: [ 35 | // Load a plugin you installed from npm. 36 | require('karma-jasmine'), 37 | 38 | // Load a plugin from the file in your project. 39 | require('./my-custom-plugin'), 40 | 41 | // Define a plugin inline. 42 | { 'framework:xyz': ['factory', factoryFn] }, 43 | 44 | // Specify a module name or path which Karma will require() and load its 45 | // default export as a plugin. 46 | 'karma-chrome-launcher', 47 | './my-fancy-plugin' 48 | ] 49 | }) 50 | ``` 51 | 52 | ## Activating Plugins 53 | 54 | Adding a plugin to the `plugins` array only makes Karma aware of the plugin, but it does not activate it. Depending on the plugin type you'll need to add a plugin name into `frameworks`, `reporters`, `preprocessors`, `middleware` or `browsers` configuration key to activate it. For the detailed information refer to the corresponding plugin documentation or check out [Developing plugins][developing plugins] guide for more in-depth explanation of how plugins work. 55 | 56 | [existing plugins]: https://www.npmjs.com/search?q=keywords:karma-plugin 57 | [developing plugins]: ../dev/plugins.html 58 | -------------------------------------------------------------------------------- /docs/dev/01-contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Contributing to Karma 3 | --- 4 | 5 | **Working on your first Pull Request?** You can learn how from this *free* series 6 | [How to Contribute to an Open Source Project on GitHub] 7 | 8 | You want to contribute to Karma? That is truly great! 9 | Here are some tips to get you started... 10 | 11 | ### Help others 12 | The best way to start contributing to any open source project is to help other people. 13 | You can answer questions on the [Gitter] or [Stack Overflow]. 14 | Either find something you already know the answer for, or something you feel interested in and 15 | dig into it a little bit to find the answer. 16 | 17 | Soon, you will realize you know Karma pretty well... 18 | 19 | 20 | ### Improve the documentation 21 | You don’t feel like hacking on the code, but still want to help? 22 | Improving the documentation is very valuable as it will help many others. 23 | 24 | All the source code is in [`docs/`]. 25 | 26 | 27 | ### Fix something that bothers you 28 | Did you find a bug that really bothers you? It’s more likely it bothers other users too, and maybe 29 | it’s not that hard to fix it! Try to find an existing issue. If it does not exist yet, create one. 30 | Look into the code and let others know what solution you are thinking about. 31 | Then, send a pull request and let other contributors review. 32 | 33 | [Here](./making-changes.html) is some more info on how to set up your workspace and send a pull 34 | request. 35 | 36 | 37 | ### Fix something else 38 | You want to contribute some code but not sure where to start? That's cool. Fortunately, 39 | there are many issues labeled as "PR please". These are typically fairly simple issues with 40 | a well-known, waiting just for you... 41 | 42 | [Here](https://github.com/karma-runner/karma/issues?labels=PR+please&page=1&state=open) is a list 43 | of all the issues for the core repo. In the same way, each plugin has "PR please" label as well... 44 | 45 | 46 | ### Review others work 47 | Check out the list of outstanding pull requests if there is something you might be interested in. 48 | Maybe somebody is trying to fix that stupid bug that bothers you. Review the PR. 49 | Do you have any better ideas how to fix this problem? Let us know... 50 | 51 | ### I want to help more 52 | Check out [Maintaining Karma]. Becoming a Karma maintainer is simple. 53 | You just do it. There is no test to pass ;-) 54 | 55 | [gitter]: https://gitter.im/karma-runner/karma 56 | [Stack Overflow]: https://stackoverflow.com/questions/tagged/karma-runner 57 | [`docs/`]: https://github.com/karma-runner/karma/tree/master/docs 58 | [Maintaining Karma]: ./maintaining.html 59 | [How to Contribute to an Open Source Project on GitHub]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 60 | -------------------------------------------------------------------------------- /docs/dev/02-making-changes.md: -------------------------------------------------------------------------------- 1 | <!--- 2 | TODO: 3 | - add more info about updating PR 4 | - rebasing/squashing changes 5 | - making sure CI is green 6 | - how to run tests on sauce labs 7 | - how to set up plugins 8 | --> 9 | 10 | If you are thinking about making Karma better, or you just want to hack on it, that’s great! 11 | Here are some tips on how to set up a Karma workspace and how to send a good pull request. 12 | 13 | ## Setting up the Workspace 14 | 15 | * Make sure you have a [GitHub account](https://github.com/signup/free). 16 | * [Fork the repository] on GitHub. 17 | * Clone your fork 18 | ```bash 19 | $ git clone https://github.com/<your-username>/karma.git 20 | $ cd karma 21 | ``` 22 | * Install for development 23 | ```bash 24 | $ npm install 25 | ``` 26 | 27 | ## Testing and Building 28 | - Run the tests via: 29 | ```bash 30 | $ npm test 31 | # or you can run test suits individually 32 | $ npm run test:unit 33 | $ npm run test:e2e 34 | $ npm run test:client 35 | ``` 36 | 37 | - Lint the code via: 38 | ```bash 39 | $ npm run lint 40 | # or you can also apply auto-fixes where possible 41 | $ npm run lint:fix 42 | ``` 43 | 44 | - Build the client code via: 45 | ```bash 46 | $ npm run build 47 | # or use the watch mode 48 | $ npm run build:watch 49 | ``` 50 | 51 | ## Changing the Code 52 | Checkout a new branch and name it accordingly to what you intend to do: 53 | - Features get the prefix `feature-`. 54 | - Bug fixes get the prefix `fix-`. 55 | - Improvements to the documentation get the prefix `docs-`. 56 | ```bash 57 | $ git checkout -b <branch_name> 58 | ``` 59 | 60 | Open your favorite editor, make some changes, run the tests, change the code, run the tests, 61 | change the code, run the tests, etc. 62 | 63 | - Please follow http://nodeguide.com/style.html (with exception of 100 characters per line). 64 | 65 | 66 | ## Sending a Pull Request 67 | 68 | - Commit your changes (please follow [commit message conventions]): 69 | ```bash 70 | $ git commit -m "..." 71 | ``` 72 | - Verify that the last commit follows the conventions: 73 | ```bash 74 | $ npm run commit:check 75 | ``` 76 | - Push to your GitHub repo: 77 | ```bash 78 | $ git push origin <branch_name> 79 | ``` 80 | - Go to the GitHub page and click "Open a Pull request". 81 | - Write a good description of the change. 82 | 83 | After sending a pull request, other developers will review and discuss your change. 84 | Please address all the comments. Once everything is all right, one of the maintainers will merge 85 | your changes in. 86 | 87 | 88 | ## Contributor License Agreement 89 | Please sign our Contributor License Agreement (CLA) before sending pull requests. 90 | For any code changes to be accepted, the CLA must be signed. It's a quick process, we promise! 91 | - For individuals, we have a [simple click-through form]. 92 | - For corporations we'll need you to print, sign and one of scan+email, fax or mail [the form]. 93 | 94 | ## Additional Resources 95 | 96 | - [Mailing List](https://groups.google.com/forum/#!forum/karma-users) 97 | - [Issue tracker](https://github.com/karma-runner/karma/issues) 98 | - [General GitHub documentation](https://docs.github.com/) 99 | - [GitHub pull request documentation](https://docs.github.com/github/collaborating-with-issues-and-pull-requests/about-pull-requests#about-pull-requests) 100 | 101 | [commit message conventions]: git-commit-msg.html 102 | [simple click-through form]: https://code.google.com/legal/individual-cla-v1.0.html 103 | [the form]: https://code.google.com/legal/corporate-cla-v1.0.html 104 | [Fork the repository]: https://github.com/karma-runner/karma/fork 105 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: homepage 3 | pageTitle: Spectacular Test Runner for Javascript 4 | --- 5 | -------------------------------------------------------------------------------- /docs/intro/01-installation.md: -------------------------------------------------------------------------------- 1 | Karma runs on [Node.js] and is available as an [npm] package. 2 | 3 | ## Installing Node.js 4 | 5 | On Mac or Linux we recommend using [NVM](https://github.com/creationix/nvm). On Windows, download Node.js 6 | from [the official site](https://nodejs.org/) or use the [NVM PowerShell Module](https://www.powershellgallery.com/packages/nvm). 7 | 8 | Karma works on all [LTS releases](https://nodejs.org/en/about/releases/) of Node.js. 9 | 10 | ## Installing Karma and plugins 11 | 12 | The recommended approach is to install Karma (and all the plugins your project needs) locally in 13 | the project's directory. 14 | 15 | ```bash 16 | # Install Karma: 17 | $ npm install karma --save-dev 18 | 19 | # Install plugins that your project needs: 20 | $ npm install karma-jasmine karma-chrome-launcher jasmine-core --save-dev 21 | 22 | ``` 23 | 24 | This will install `karma`, `karma-jasmine`, `karma-chrome-launcher` and `jasmine-core` packages into `node_modules` in your current 25 | working directory and also save these as `devDependencies` in `package.json`, so that any 26 | other developer working on the project will only have to do `npm install` in order to get all these 27 | dependencies installed. 28 | 29 | ```bash 30 | # Run Karma: 31 | $ ./node_modules/karma/bin/karma start 32 | ``` 33 | 34 | ## Commandline Interface 35 | Typing `./node_modules/karma/bin/karma start` sucks and so you might find it useful to install `karma-cli` globally. You will need to do this if you want to run Karma on Windows from the command line. 36 | 37 | ```bash 38 | $ npm install -g karma-cli 39 | ``` 40 | 41 | Then, you can run Karma simply by `karma` from anywhere and it will always run the local version. 42 | 43 | 44 | [Node.js]: https://nodejs.org/ 45 | [npm]: https://www.npmjs.com/package/karma 46 | [NVM]: https://github.com/creationix/nvm 47 | [FAQ]: ./faq.html 48 | -------------------------------------------------------------------------------- /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 all 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 | Press Enter 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 | Press Enter 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 init` with a `*.coffee` extension such as `karma init karma.conf.coffee`, 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` or `karma.conf.coffee` 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 my.conf.js --log-level debug --single-run 69 | ``` 70 | 71 | Try `karma start --help` if you want to see all available options. 72 | 73 | 74 | ## Integrating with Grunt/Gulp 75 | - [grunt-karma] 76 | - [gulp-karma] 77 | 78 | 79 | [configuration file docs]: ../config/configuration-file.html 80 | [Grunt]: https://gruntjs.com/ 81 | [grunt-karma]: https://github.com/karma-runner/grunt-karma 82 | [Gulp]: https://gulpjs.com 83 | [gulp-karma]: https://github.com/karma-runner/gulp-karma 84 | -------------------------------------------------------------------------------- /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 of each test against each browser are examined and displayed via the command line to the developer 3 | such that they can 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 to 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 | ## Outline of workflow. 18 | 19 | Here is roughly how Karma works: 20 | 21 | After starting up, Karma loads plugins and the configuration file, then starts its local web server which listens for connections. 22 | Any browser already waiting on websockets from the server will reconnect immediately. As part of loading the plugins, test reporters 23 | register for 'browser' events so they are ready for test results. 24 | 25 | Then karma launches zero, one, or more browsers, setting their start page the Karma server URL. 26 | 27 | When the browsers connect, Karma serves a 'client.html' page; when this page runs in the browser it connects back to the server via websockets. 28 | 29 | Once the server sees the websocket connection, it instructs the client -- over the websocket -- to execute tests. The client page opens an iframe with a 'context.html' page from the server. The server generates this context.html page using the configuration. This page includes the test framework adapter, the code to be tested, and the test code. 30 | 31 | When the browser loads this context page, the onload event handler connects the context page to the client page via postMessage. The framework adapter is in charge at this point: it runs the test, reporting errors or success by messaging through the client page. 32 | 33 | Messages sent to the client page are forwarded through the websocket to the Karma server. The server re-dispatches these messages as 'browser' events. The reporters listening to 'browser' events get the data; they may print it, save it to files, or forward the data to another service. 34 | Since the data is sent by the test framework adapter to the reporter, adapters and reporters almost always come in pairs, like karma-jasmine and karma-jasmine-reporter. The detailed content of test-result data is of no concern to other parts of karma: only the reporter needs to know its format. 35 | 36 | Karma has many variations and options that may cause different workflow with different configurations. 37 | 38 | If you are interested in learning more about the design, Karma itself originates from a university thesis, which goes into detail about the design 39 | and implementation, and it is available to read [right here]. 40 | 41 | [right here]: https://github.com/karma-runner/karma/raw/master/thesis.pdf 42 | [browsers]: ../config/browsers.html 43 | -------------------------------------------------------------------------------- /docs/intro/04-faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Frequently Asked Questions 3 | menuTitle: FAQ 4 | --- 5 | 6 | The list below is a collection of common questions regarding Karma and its use. 7 | If you have any other questions in mind, please visit the [mailing list] to let the community know. 8 | 9 | 10 | ### Can I use Karma with testing framework X? 11 | Yes. There are plugins for most of the common testing frameworks (such as Jasmine, Mocha, QUnit). 12 | If there is no plugin for the testing framework you like, go ahead and write one. It is simple - 13 | you can start by looking into the source code of the existing ones. 14 | 15 | 16 | ### Can I use Karma to do end to end testing? 17 | Karma has primarily been designed for low level (unit) testing. If it's an AngularJS app, you can 18 | use Karma with the [karma-ng-scenario] plugin. However, we recommend [Protractor] for high-level testing. 19 | 20 | 21 | ### Can I use Karma on Continuous Integration server ? 22 | Of course! Check out the docs for [Jenkins], [Semaphore], [TeamCity] or [Travis]. 23 | 24 | 25 | ### Which version of Karma should I use? 26 | The latest stable version from npm (`npm install karma`). See [versioning] for more detailed information about Karma's release channels. 27 | 28 | 29 | ### Which version of Node.js does Karma run with? 30 | Karma works on all LTS versions of Node.js as specified by the [Node.js Release Working Group](https://github.com/nodejs/Release/blob/master/README.md). The Node.js version numbers are set in the package.json. Older versions of karma work with older versions of Node.js, but are not maintained or updated. 31 | 32 | [mailing list]: https://groups.google.com/d/forum/karma-users 33 | [karma-ng-scenario]: https://github.com/karma-runner/karma-ng-scenario 34 | [Protractor]: https://github.com/angular/protractor 35 | [Jenkins]: ../plus/jenkins.html 36 | [Semaphore]: ../plus/semaphore.html 37 | [TeamCity]: ../plus/teamcity.html 38 | [Travis]: ../plus/travis.html 39 | [versioning]: ../about/versioning.html 40 | [browsers]: ../config/browsers.html 41 | -------------------------------------------------------------------------------- /docs/plus/02-travis.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Travis CI 3 | menuTitle: Travis CI 4 | --- 5 | 6 | [Travis CI] is a popular continuous integration service that 7 | integrates with your [Github] repository to automatically run your 8 | tests when the code is pushed. Integration is done by adding a simple 9 | [YAML] file to your project root; Travis and Github take care of the 10 | rest. Whenever tested, the Travis results will appear in your Github pull requests and your 11 | history will be available within their control panel. This article assumes you 12 | already have Travis account. 13 | 14 | ## Configure Travis 15 | Create a file in your project root called `.travis.yml` with the 16 | following YAML content: 17 | 18 | ```ruby 19 | language: node_js 20 | node_js: 21 | - "4" 22 | ``` 23 | 24 | ## Set up a Test Command 25 | If you do not already have a `package.json` in your project root, create one now. Travis runs `npm test` to trigger your tests, so this 26 | is where you tell Travis how to run your tests. 27 | 28 | ```json 29 | // ...snip... 30 | "devDependencies": { 31 | "karma": "~0.12" 32 | }, 33 | // ...snip... 34 | "scripts": { 35 | "test": "karma start --single-run --browsers PhantomJS" 36 | } 37 | // ...snip... 38 | ``` 39 | 40 | Travis will run `npm install` before every suite, so this is your 41 | chance to specify any modules your app needs that Travis does not know 42 | about like Karma. 43 | 44 | ## Configure Travis with Firefox 45 | Travis supports running a real browser (Firefox) with a virtual 46 | screen. Just update your `.travis.yml` to set up the virtual screen 47 | like this (if you're using Xenial): 48 | ```ruby 49 | language: node_js 50 | node_js: 51 | - "4" 52 | dist: xenial 53 | services: 54 | - xvfb 55 | ``` 56 | 57 | Or this, for Trusty and below: 58 | ```ruby 59 | language: node_js 60 | node_js: 61 | - "4" 62 | before_script: 63 | - export DISPLAY=:99.0 64 | - sh -e /etc/init.d/xvfb start 65 | ``` 66 | 67 | And now, you can run your tests on Firefox, just change the `npm test` 68 | command to 69 | ```bash 70 | karma start --browsers Firefox --single-run 71 | ``` 72 | 73 | ## Notes 74 | 75 | * Travis' Node environment has very little available. If the startup 76 | process in Travis fails to check for missing module information and be sure to add them to your `package.json` dependencies. 77 | * Travis does not run on your local network so any code that attempts 78 | to connect to resources should be stubbed out using [Nock]. 79 | * There are more options available to your `.travis.yml`, such as 80 | running scripts before the install or test run. There are hints in 81 | the Travis docs for [GUI apps] configuration. 82 | 83 | 84 | [Travis CI]: https://travis-ci.org/ 85 | [Github]: https://github.com/ 86 | [YAML]: https://yaml.org/ 87 | [PhantomJS]: https://phantomjs.org/ 88 | [GUI apps]: https://docs.travis-ci.com/user/gui-and-headless-browsers/ 89 | [Nock]: https://github.com/nock/nock 90 | -------------------------------------------------------------------------------- /docs/plus/03-jenkins.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Jenkins CI 3 | menuTitle: Jenkins CI 4 | --- 5 | 6 | [Jenkins CI] is one of the most popular continuous integration servers 7 | in the market today. At some point while developing your [AngularJS] 8 | project (hopefully early on), you might want to have automated tests run 9 | off your code versioning system. Jenkins will help you with this task. 10 | This tutorial assumes you have Jenkins already setup and running 11 | on your CI environment. 12 | 13 | ## Install Prerequisites 14 | You need the following tools installed on your Jenkins CI server: 15 | 16 | * Node 17 | * Karma 18 | 19 | The following Jenkins plugin is optional, but the next guidelines are based on it: 20 | * [EnvInject] - it makes things easier under certain linux distributions and user permissions. 21 | 22 | ## Configure Karma 23 | Make the following additions and changes to your `karma.conf.js` 24 | file as needed: 25 | 26 | ```javascript 27 | singleRun: true, 28 | reporters: ['dots', 'junit'], 29 | junitReporter: { 30 | outputFile: 'test-results.xml' 31 | }, 32 | ``` 33 | 34 | Please note the `test-results.xml` files will be written to subdirectories 35 | named after the browsers the tests were run in inside the present working 36 | directory (and you will need to tell Jenkins where to find them). 37 | 38 | ## Create a new Jenkins Job 39 | In Jenkins, start a new job for Angular/Karma with the basic 40 | settings (Name, description, parameters, source code repo to pull 41 | from, etc.) 42 | 43 | ## Configure the Build Environment 44 | First go to the job page and click on configure. Then in the Build 45 | Environment sub-section, check the “Inject environment 46 | variables to the build process' checkbox. A few textboxes will 47 | appear and in the “Properties Content” box set the following: 48 | 49 | ```bash 50 | $ PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin 51 | $ PHANTOMJS_BIN=/usr/local/bin/phantomjs #or wherever PhantomJS happens to be installed 52 | ``` 53 | 54 | Further down the page, in the Post-build Actions sub-section add a 55 | `Publish JUnit test result report` from the Post-build action drop 56 | down menu. When the textbox labeled Test report XMLs appears, enter 57 | the path to where the `test-results.xml` files are relative to the root of your 58 | Jenkins job workspace (you can use wildcards for this, so `**/test-results.xml` 59 | will find the file even if it was stored inside a browser-specific 60 | subdirectory). 61 | 62 | 63 | 64 | [Jenkins CI]: https://www.jenkins.io/ 65 | [AngularJS]: https://angularjs.org 66 | [EnvInject]: https://plugins.jenkins.io/envinject/ 67 | -------------------------------------------------------------------------------- /docs/plus/04-semaphore.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Semaphore CI 3 | menuTitle: Semaphore CI 4 | --- 5 | 6 | [Semaphore] is a popular continuous integration service that 7 | supports a [wide range of programming languages]. Up-to-date 8 | versions of [Firefox], [PhantomJS] and [Node.js] make it a good 9 | testing ground for JavaScript applications. This article assumes 10 | you already have a Semaphore account. 11 | 12 | ## Configure Your Project 13 | 14 | If you do not already have a `package.json` in your project root, 15 | create one now. This will both document your configuration and 16 | make it easy to run your tests. Here's an example: 17 | 18 | ```json 19 | // ...snip... 20 | "devDependencies": { 21 | "karma": "~0.10" 22 | }, 23 | // ...snip... 24 | "scripts": { 25 | "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS" 26 | } 27 | // ...snip... 28 | ``` 29 | 30 | Another option is to use Firefox as your test browser. To do this, change 31 | the last part to: 32 | 33 | ```json 34 | "scripts": { 35 | "test": "./node_modules/.bin/karma start --single-run --browsers Firefox" 36 | } 37 | ``` 38 | 39 | Now running `npm test` within your project will run your tests with Karma. 40 | 41 | ## Add Your Project to Semaphore 42 | 43 | Follow the process as shown in the [screencast] on the Semaphore docs. 44 | 45 | After the analysis is finished, ignore the Ruby version Semaphore has set 46 | for you, choose to customize your build commands and use these: 47 | 48 | ```bash 49 | npm install 50 | npm test 51 | ``` 52 | 53 | That's it - proceed to your first build. In case you're using Firefox as 54 | your test browser, Semaphore will automatically run it on a virtual screen 55 | during your builds. 56 | 57 | Also, if necessary, build commands can be further [customized] at any time. 58 | 59 | 60 | [screencast]: https://semaphoreci.com/docs/adding-github-bitbucket-project-to-semaphore.html 61 | [Semaphore]: https://semaphoreci.com 62 | [wide range of programming languages]: https://semaphoreci.com/docs/supported-stack.html 63 | [Firefox]: https://semaphoreci.com/docs/firefox.html 64 | [PhantomJS]: https://semaphoreci.com/docs/phantomjs.html 65 | [Node.js]: https://semaphoreci.com/docs/languages/javascript/javascript-support-on-semaphore.html 66 | [platform]: https://semaphoreci.com/docs/supported-stack.html 67 | [customized]: https://semaphoreci.com/docs/customizing-build-commands.html 68 | -------------------------------------------------------------------------------- /docs/plus/05-cloud9.md: -------------------------------------------------------------------------------- 1 | [Cloud9 IDE] is an open source web-based cloud integrated development environment that supports 2 | several programming languages, with a focus on the web stack (specifically JavaScript and NodeJS). 3 | It is written almost entirely in JavaScript and uses NodeJS on the back-end. 4 | 5 | ## Configuration 6 | 7 | First, make sure the `karma.conf.js` includes the following entries: 8 | 9 | ```javascript 10 | hostname: process.env.IP, 11 | port: process.env.PORT 12 | ``` 13 | 14 | ## Capture the browser manually on the local machine 15 | 16 | You can use any of your local browsers. 17 | 18 | ```bash 19 | # Start Karma without browsers: 20 | $ karma start --no-browsers 21 | ``` 22 | 23 | Now, open `http://<projectName>.<cloud9User>.c9.io/` in your browser. 24 | 25 | ## Run Karma unit tests with PhantomJS 26 | 27 | It is also possible to run headless PhantomJS on the Cloud9 server. 28 | 29 | ```bash 30 | # Install the PhantomJS plugin: 31 | $ npm install karma-phantomjs-launcher 32 | 33 | # Start Karma: 34 | $ karma start --browsers PhantomJS 35 | ``` 36 | 37 | [Cloud9 IDE]: https://c9.io/ 38 | -------------------------------------------------------------------------------- /docs/plus/06-angularjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: AngularJS 3 | menuTitle: AngularJS 4 | --- 5 | 6 | If you're using [AngularJS](https://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. 7 | -------------------------------------------------------------------------------- /docs/plus/07-yeoman.md: -------------------------------------------------------------------------------- 1 | [Yeoman](https://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 | -------------------------------------------------------------------------------- /docs/plus/08-emberjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: Ember.js 3 | menuTitle: Ember.js 4 | --- 5 | 6 | To execute javascript unit and integration tests with ember.js follow the steps below: 7 | 8 | 1. [install karma] 9 | 10 | 2. install the qunit plugin 11 | 12 | ```bash 13 | npm install karma-qunit --save-dev 14 | ``` 15 | 16 | 3. install the ember preprocessor plugin 17 | 18 | ```bash 19 | npm install karma-ember-preprocessor --save-dev 20 | ``` 21 | 22 | 4. generate a configuration file for karma 23 | ```bash 24 | karma init 25 | ``` 26 | note -the above will walk you through the basic setup. An example configuration file that works with ember.js/qunit and phantomjs is below 27 | 28 | ```javascript 29 | module.exports = function(config) { 30 | config.set({ 31 | basePath: 'js', 32 | 33 | files: [ 34 | 'vendor/jquery/jquery.min.js', 35 | 'vendor/handlebars/handlebars.js', 36 | 'vendor/ember/ember.js', 37 | 'app.js', 38 | 'tests/*.js', 39 | 'templates/*.handlebars' 40 | ], 41 | 42 | browsers: ['PhantomJS'], 43 | singleRun: true, 44 | autoWatch: false, 45 | 46 | frameworks: ['qunit'], 47 | 48 | plugins: [ 49 | 'karma-qunit', 50 | 'karma-ember-preprocessor', 51 | 'karma-phantomjs-launcher' 52 | ], 53 | 54 | preprocessors: { 55 | '**/*.handlebars': 'ember' 56 | } 57 | }); 58 | }; 59 | ``` 60 | 61 | Note - the `files` section above should include all dependencies, ie- jQuery/handlebars/ember.js along with the js and handlebars files required to deploy and run your production ember.js application 62 | 63 | Note - when testing ember applications, it is important that karma does not try to run the tests until the ember application has finished initialization. You will need to include a small bootstrap file in the `files` section above to enforce this. Here's an example: 64 | ```javascript 65 | __karma__.loaded = function() {}; 66 | 67 | App.setupForTesting(); 68 | App.injectTestHelpers(); 69 | 70 | //this gate/check is required given that standard practice in Ember tests to is to call 71 | //Ember.reset() in the afterEach/tearDown for each test. Doing so, causes the application 72 | //to 're-initialize', resulting in repeated calls to the initialize function below 73 | var karma_started = false; 74 | App.initializer({ 75 | name: "run tests", 76 | initialize: function(container, application) { 77 | if (!karma_started) { 78 | karma_started = true; 79 | __karma__.start(); 80 | } 81 | } 82 | }); 83 | ``` 84 | 85 | 5. add a simple Qunit test 86 | 87 | ```javascript 88 | test('one should equal one', function() { 89 | equal(1, 1, 'error: one did not equal one'); 90 | }); 91 | ``` 92 | 93 | 6. run the tests with karma from the command line 94 | ```bash 95 | karma start 96 | ``` 97 | 98 | A simple unit / integration tested example app showing karma / qunit / ember in action can be found [here] 99 | 100 | [install karma]: ../intro/installation.html 101 | [here]: https://github.com/toranb/ember-testing-example 102 | -------------------------------------------------------------------------------- /docs/plus/09-codio.md: -------------------------------------------------------------------------------- 1 | [Codio] is a web-based cloud integrated development environment that supports almost any programming language. Every project gets its individual Box: an instantly available server-side development environment with full terminal access. Unlimited panels and tabs, and a plethora of productivity features. 2 | 3 | ## Customize your Codio Project 4 | 5 | Next to the help menu you will see the "Configure" option, if you don't see it click the little arrow near the end and then select "Configure". 6 | 7 | 8 | This opens a .codio file which you can customize and use rather than entering commands in Terminal. Replace the existing text with the text below: 9 | 10 | 11 | { 12 | // Configure your Run and Preview buttons here. 13 | 14 | // Run button configuration 15 | "commands": { 16 | "Karma Start": "karma start --no-browsers" 17 | "Karam Run": "karma run" 18 | }, 19 | 20 | // Preview button configuration 21 | "preview": { 22 | "Karma Preview": "http://{{domain}}:8080" 23 | } 24 | } 25 | 26 | 27 | *If you wish, you can change the port for the `Karma Preview` entry, but make a note of the change as you will need to include that port in the `karma.config js` file later.* 28 | 29 | 30 | ## Configuration 31 | 32 | - Edit the `karma.conf.js` and add the following: 33 | 34 | 35 | // hostname for the server 36 | hostname: require('os').hostname() + '.codio.io', 37 | 38 | 39 | - Review webserver port entry to ensure same port defined as entered in .codio file 40 | 41 | 42 | // web server port 43 | port: 8080, 44 | 45 | 46 | ## Capture the browser manually on the local machine 47 | 48 | You can use your local browser. 49 | 50 | - Either: 51 | 52 | - Open a Terminal window and enter 53 | 54 | $ karma start --no-browsers 55 | or 56 | - Select `Karma Start` from the Run menu (the 2nd from the right button). 57 | 58 | 59 | - Select `Karma Preview` from the Preview menu (the right-hand button). 60 | 61 | - Switch back into your Codio project and either: 62 | 63 | - Open a new Terminal window and enter 64 | 65 | $ karma run 66 | or 67 | - Select `Karma Run` from the Run menu (the 2nd from the right button). 68 | 69 | and your test will execute 70 | 71 | 72 | [Codio]: https://codio.com 73 | -------------------------------------------------------------------------------- /docs/plus/10-teamcity.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageTitle: TeamCity 3 | menuTitle: TeamCity 4 | --- 5 | 6 | Running Karma in your [TeamCity] build is as simple as adding command line build 7 | step to perform the task. That is basically it. 8 | 9 | ## Install Prerequisites 10 | The only prerequisite is `Node` (with `npm`) installed on the agent(s) you are going to use to 11 | run build on. 12 | You may decide to install Karma and Karma-related packages on the agent globally to reuse the same 13 | Karma installation by different builds. 14 | 15 | ## Configure project 16 | Add `karma-teamcity-reporter` as a dependency to your project: 17 | 18 | npm i --save-dev karma-teamcity-reporter 19 | 20 | It is also a good idea to check that you have all karma npm dependencies listed in your 21 | `package.json` file (e.g. `karma-jasmine`, `karma-phantomjs-launcher` and so on) to have them 22 | being installed during the build. 23 | 24 | ## Create a new TeamCity build step 25 | Add new build step to the build configuration: use Command Line runner and fill in `Custom 26 | script` text area. If you had decided not to install *all* your npm dependencies globally 27 | add `npm install` at the beginning of the script. Then add command to run Karma, e.g.: 28 | 29 | karma start --reporters teamcity --single-run --browsers PhantomJS --colors false 30 | 31 | Running Karma with all these options provided via command line allows to run Karma in 32 | TeamCity build and locally in development environment (with options from configuration 33 | file). 34 | 35 | [TeamCity]: https://www.jetbrains.com/teamcity/ 36 | -------------------------------------------------------------------------------- /lib/browser_collection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BrowserResult = require('./browser_result') 4 | const helper = require('./helper') 5 | 6 | class BrowserCollection { 7 | constructor (emitter, browsers = []) { 8 | this.browsers = browsers 9 | this.emitter = emitter 10 | } 11 | 12 | add (browser) { 13 | this.browsers.push(browser) 14 | this.emitter.emit('browsers_change', this) 15 | } 16 | 17 | remove (browser) { 18 | if (helper.arrayRemove(this.browsers, browser)) { 19 | this.emitter.emit('browsers_change', this) 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | getById (browserId) { 26 | return this.browsers.find((browser) => browser.id === browserId) || null 27 | } 28 | 29 | getNonReady () { 30 | return this.browsers.filter((browser) => !browser.isConnected()) 31 | } 32 | 33 | areAllReady () { 34 | return this.browsers.every((browser) => browser.isConnected()) 35 | } 36 | 37 | serialize () { 38 | return this.browsers.map((browser) => browser.serialize()) 39 | } 40 | 41 | calculateExitCode (results, singleRunBrowserNotCaptured, config) { 42 | config = config || {} 43 | if (results.disconnected || singleRunBrowserNotCaptured) { 44 | return 1 45 | } 46 | if (results.skipped && config.failOnSkippedTests) { 47 | return 1 48 | } 49 | if (results.success + results.failed === 0 && !!config.failOnEmptyTestSuite) { 50 | return 1 51 | } 52 | if (results.error) { 53 | return 1 54 | } 55 | if (config.failOnFailingTestSuite === false) { 56 | return 0 // Tests executed without infrastructure error, exit with 0 independent of test status. 57 | } 58 | return results.failed ? 1 : 0 59 | } 60 | 61 | getResults (singleRunBrowserNotCaptured, config) { 62 | const results = { success: 0, failed: 0, skipped: 0, error: false, disconnected: false, exitCode: 0 } 63 | this.browsers.forEach((browser) => { 64 | results.success += browser.lastResult.success 65 | results.failed += browser.lastResult.failed 66 | results.skipped += browser.lastResult.skipped 67 | results.error = results.error || browser.lastResult.error 68 | results.disconnected = results.disconnected || browser.lastResult.disconnected 69 | }) 70 | 71 | results.exitCode = this.calculateExitCode(results, singleRunBrowserNotCaptured, config) 72 | return results 73 | } 74 | 75 | clearResults () { 76 | this.browsers.forEach((browser) => { 77 | browser.lastResult = new BrowserResult() 78 | }) 79 | } 80 | 81 | clone () { 82 | return new BrowserCollection(this.emitter, this.browsers.slice()) 83 | } 84 | 85 | // Array APIs 86 | map (callback, context) { 87 | return this.browsers.map(callback, context) 88 | } 89 | 90 | forEach (callback, context) { 91 | return this.browsers.forEach(callback, context) 92 | } 93 | 94 | get length () { 95 | return this.browsers.length 96 | } 97 | } 98 | 99 | BrowserCollection.factory = function (emitter) { 100 | return new BrowserCollection(emitter) 101 | } 102 | 103 | module.exports = BrowserCollection 104 | -------------------------------------------------------------------------------- /lib/browser_result.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class BrowserResult { 4 | constructor (total = 0) { 5 | this.startTime = Date.now() 6 | 7 | this.total = total 8 | this.skipped = this.failed = this.success = 0 9 | this.netTime = this.totalTime = 0 10 | this.disconnected = this.error = false 11 | } 12 | 13 | totalTimeEnd () { 14 | this.totalTime = Date.now() - this.startTime 15 | } 16 | 17 | add (result) { 18 | if (result.skipped) { 19 | this.skipped++ 20 | } else if (result.success) { 21 | this.success++ 22 | } else { 23 | this.failed++ 24 | } 25 | 26 | this.netTime += result.time 27 | } 28 | } 29 | 30 | module.exports = BrowserResult 31 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('graceful-fs') 4 | const path = require('path') 5 | 6 | const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '/../package.json')).toString()) 7 | 8 | exports.VERSION = pkg.version 9 | 10 | exports.DEFAULT_PORT = process.env.PORT || 9876 11 | exports.DEFAULT_HOSTNAME = process.env.IP || 'localhost' 12 | exports.DEFAULT_LISTEN_ADDR = process.env.LISTEN_ADDR || '0.0.0.0' 13 | 14 | // log levels 15 | exports.LOG_DISABLE = 'OFF' 16 | exports.LOG_ERROR = 'ERROR' 17 | exports.LOG_WARN = 'WARN' 18 | exports.LOG_INFO = 'INFO' 19 | exports.LOG_DEBUG = 'DEBUG' 20 | exports.LOG_LOG = 'LOG' 21 | exports.LOG_PRIORITIES = [ 22 | exports.LOG_DISABLE, 23 | exports.LOG_ERROR, 24 | exports.LOG_WARN, 25 | exports.LOG_LOG, 26 | exports.LOG_INFO, 27 | exports.LOG_DEBUG 28 | ] 29 | 30 | // Default patterns for the pattern layout. 31 | exports.COLOR_PATTERN = '%[%d{DATETIME}:%p [%c]: %]%m' 32 | exports.NO_COLOR_PATTERN = '%d{DATETIME}:%p [%c]: %m' 33 | 34 | // Default console appender 35 | exports.CONSOLE_APPENDER = { 36 | type: 'console', 37 | layout: { 38 | type: 'pattern', 39 | pattern: exports.COLOR_PATTERN 40 | } 41 | } 42 | 43 | exports.EXIT_CODE = '\x1FEXIT' 44 | -------------------------------------------------------------------------------- /lib/detached.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | 5 | const Server = require('./server') 6 | const configurationFile = process.argv[2] 7 | const fileContents = fs.readFileSync(configurationFile, 'utf-8') 8 | fs.unlink(configurationFile, function () {}) 9 | const data = JSON.parse(fileContents) 10 | const server = new Server(data) 11 | server.start(data) 12 | -------------------------------------------------------------------------------- /lib/emitter_wrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class EmitterWrapper { 4 | constructor (emitter) { 5 | this.listeners = {} 6 | this.emitter = emitter 7 | } 8 | 9 | addListener (event, listener) { 10 | this.emitter.addListener(event, listener) 11 | this.listeners[event] = this.listeners[event] || [] 12 | this.listeners[event].push(listener) 13 | return this 14 | } 15 | 16 | on (event, listener) { 17 | return this.addListener(event, listener) 18 | } 19 | 20 | removeAllListeners (event) { 21 | const events = event ? [event] : Object.keys(this.listeners) 22 | events.forEach((event) => { 23 | this.listeners[event].forEach((listener) => { 24 | this.emitter.removeListener(event, listener) 25 | }) 26 | delete this.listeners[event] 27 | }) 28 | 29 | return this 30 | } 31 | } 32 | 33 | module.exports = EmitterWrapper 34 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | const helper = require('./helper') 5 | 6 | function bufferEvents (emitter, eventsToBuffer) { 7 | const listeners = [] 8 | const eventsToReply = [] 9 | 10 | function genericListener () { 11 | eventsToReply.push(Array.from(arguments)) 12 | } 13 | 14 | eventsToBuffer.forEach((eventName) => { 15 | const listener = genericListener.bind(null, eventName) 16 | listeners.push(listener) 17 | emitter.on(eventName, listener) 18 | }) 19 | 20 | return function () { 21 | listeners.forEach((listener, i) => { 22 | emitter.removeListener(eventsToBuffer[i], listener) 23 | }) 24 | 25 | eventsToReply.forEach((args) => { 26 | EventEmitter.prototype.emit.apply(emitter, args) 27 | }) 28 | 29 | listeners.length = 0 30 | eventsToReply.length = 0 31 | } 32 | } 33 | 34 | class KarmaEventEmitter extends EventEmitter { 35 | bind (object) { 36 | for (const method in object) { 37 | if (method.startsWith('on') && helper.isFunction(object[method])) { 38 | this.on(helper.camelToSnake(method.slice(2)), function () { 39 | // We do not use an arrow function here, to supply the caller as this. 40 | object[method].apply(object, Array.from(arguments).concat(this)) 41 | }) 42 | } 43 | } 44 | } 45 | 46 | emitAsync (name) { 47 | // TODO(vojta): allow passing args 48 | // TODO(vojta): ignore/throw if listener call done() multiple times 49 | let pending = this.listeners(name).length 50 | const deferred = helper.defer() 51 | 52 | this.emit(name, () => { 53 | if (!--pending) { 54 | deferred.resolve() 55 | } 56 | }) 57 | 58 | if (!pending) { 59 | deferred.resolve() 60 | } 61 | 62 | return deferred.promise 63 | } 64 | } 65 | 66 | exports.EventEmitter = KarmaEventEmitter 67 | exports.bufferEvents = bufferEvents 68 | -------------------------------------------------------------------------------- /lib/executor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const log = require('./logger').create() 4 | 5 | class Executor { 6 | constructor (capturedBrowsers, config, emitter) { 7 | this.capturedBrowsers = capturedBrowsers 8 | this.config = config 9 | this.emitter = emitter 10 | 11 | this.executionScheduled = false 12 | this.errorsScheduled = [] 13 | this.pendingCount = 0 14 | this.runningBrowsers = null 15 | 16 | this.emitter.on('run_complete', () => this.onRunComplete()) 17 | this.emitter.on('browser_complete', () => this.onBrowserComplete()) 18 | } 19 | 20 | schedule () { 21 | if (this.capturedBrowsers.length === 0) { 22 | log.warn(`No captured browser, open ${this.config.protocol}//${this.config.hostname}:${this.config.port}${this.config.urlRoot}`) 23 | return false 24 | } else if (this.capturedBrowsers.areAllReady()) { 25 | log.debug('All browsers are ready, executing') 26 | log.debug(`Captured ${this.capturedBrowsers.length} browsers`) 27 | this.executionScheduled = false 28 | this.capturedBrowsers.clearResults() 29 | this.pendingCount = this.capturedBrowsers.length 30 | this.runningBrowsers = this.capturedBrowsers.clone() 31 | this.emitter.emit('run_start', this.runningBrowsers) 32 | this.socketIoSockets.emit('execute', this.config.client) 33 | return true 34 | } else { 35 | log.info('Delaying execution, these browsers are not ready: ' + this.capturedBrowsers.getNonReady().join(', ')) 36 | this.executionScheduled = true 37 | return false 38 | } 39 | } 40 | 41 | /** 42 | * Schedule an error to be reported 43 | * @param {string} errorMessage 44 | * @returns {boolean} a boolean indicating whether or not the error was handled synchronously 45 | */ 46 | scheduleError (errorMessage) { 47 | // We don't want to interfere with any running test. 48 | // Verify that no test is running before reporting the error. 49 | if (this.capturedBrowsers.areAllReady()) { 50 | log.warn(errorMessage) 51 | const errorResult = { 52 | success: 0, 53 | failed: 0, 54 | skipped: 0, 55 | error: errorMessage, 56 | exitCode: 1 57 | } 58 | const noBrowsersStartedTests = [] 59 | this.emitter.emit('run_start', noBrowsersStartedTests) // A run cannot complete without being started 60 | this.emitter.emit('run_complete', noBrowsersStartedTests, errorResult) 61 | return true 62 | } else { 63 | this.errorsScheduled.push(errorMessage) 64 | return false 65 | } 66 | } 67 | 68 | onRunComplete () { 69 | if (this.executionScheduled) { 70 | this.schedule() 71 | } 72 | if (this.errorsScheduled.length) { 73 | const errorsToReport = this.errorsScheduled 74 | this.errorsScheduled = [] 75 | errorsToReport.forEach((error) => this.scheduleError(error)) 76 | } 77 | } 78 | 79 | onBrowserComplete () { 80 | this.pendingCount-- 81 | 82 | if (!this.pendingCount) { 83 | // Ensure run_complete is emitted in the next tick 84 | // so it is never emitted before browser_complete 85 | setTimeout(() => { 86 | this.emitter.emit('run_complete', this.runningBrowsers, this.runningBrowsers.getResults()) 87 | }) 88 | } 89 | } 90 | } 91 | 92 | Executor.factory = function (capturedBrowsers, config, emitter) { 93 | return new Executor(capturedBrowsers, config, emitter) 94 | } 95 | 96 | module.exports = Executor 97 | -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | /** 6 | * File object used for tracking files in `file-list.js`. 7 | */ 8 | class File { 9 | constructor (path, mtime, doNotCache, type, isBinary, integrity) { 10 | // used for serving (processed path, eg some/file.coffee -> some/file.coffee.js) 11 | this.path = path 12 | 13 | // original absolute path, id of the file 14 | this.originalPath = path 15 | 16 | // where the content is stored (processed) 17 | this.contentPath = path 18 | 19 | // encodings format {[encodingType]: encodedContent} 20 | // example: {gzip: <Buffer 1f 8b 08...>} 21 | this.encodings = Object.create(null) 22 | 23 | this.mtime = mtime 24 | this.isUrl = false 25 | 26 | this.doNotCache = doNotCache === undefined ? false : doNotCache 27 | 28 | this.type = type 29 | 30 | // Tri state: null means probe file for binary. 31 | this.isBinary = isBinary === undefined ? null : isBinary 32 | 33 | this.integrity = integrity 34 | } 35 | 36 | /** 37 | * Detect type from the file extension. 38 | * @returns {string} detected file type or empty string 39 | */ 40 | detectType () { 41 | return path.extname(this.path).slice(1) 42 | } 43 | 44 | toString () { 45 | return this.path 46 | } 47 | } 48 | 49 | module.exports = File 50 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const constants = require('./constants') 4 | const Server = require('./server') 5 | const runner = require('./runner') 6 | const stopper = require('./stopper') 7 | const launcher = require('./launcher') 8 | const cfg = require('./config') 9 | 10 | module.exports = { 11 | constants: constants, 12 | VERSION: constants.VERSION, 13 | Server: Server, 14 | runner: runner, 15 | stopper: stopper, 16 | launcher: launcher, 17 | config: { parseConfig: cfg.parseConfig } // lets start with only opening up the `parseConfig` api 18 | } 19 | -------------------------------------------------------------------------------- /lib/init/color_schemes.js: -------------------------------------------------------------------------------- 1 | const COLORS_ON = { 2 | RESET: '\x1B[39m', 3 | ANSWER: '\x1B[36m', // NYAN 4 | SUCCESS: '\x1B[32m', // GREEN 5 | QUESTION: '\x1B[1m', // BOLD 6 | question: function (str) { 7 | return this.QUESTION + str + '\x1B[22m' 8 | }, 9 | success: function (str) { 10 | return this.SUCCESS + str + this.RESET 11 | } 12 | } 13 | 14 | const COLORS_OFF = { 15 | RESET: '', 16 | ANSWER: '', 17 | SUCCESS: '', 18 | QUESTION: '', 19 | question: function (str) { 20 | return str 21 | }, 22 | success: function (str) { 23 | return str 24 | } 25 | } 26 | 27 | exports.ON = COLORS_ON 28 | exports.OFF = COLORS_OFF 29 | -------------------------------------------------------------------------------- /lib/init/formatters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const FileUtils = require('../utils/file-utils') 5 | 6 | function quote (value) { 7 | return `'${value}'` 8 | } 9 | 10 | function formatLine (items) { 11 | return items.map(quote).join(', ') 12 | } 13 | 14 | function formatMultiLines (items) { 15 | return items 16 | .map((file) => '\n ' + file) 17 | .join(',') 18 | } 19 | 20 | function formatFiles (includedFiles, onlyServedFiles) { 21 | const lines = [] 22 | .concat(includedFiles.map(quote)) 23 | .concat(onlyServedFiles.map((file) => `{ pattern: ${quote(file)}, included: false }`)) 24 | 25 | return formatMultiLines(lines) 26 | } 27 | 28 | function formatPreprocessors (preprocessors) { 29 | const lines = Object.keys(preprocessors) 30 | .map((pattern) => `${quote(pattern)}: [${formatLine(preprocessors[pattern])}]`) 31 | 32 | return formatMultiLines(lines) 33 | } 34 | 35 | function getConfigPath (file) { 36 | return path.join(__dirname, `/../../${file}`) 37 | } 38 | 39 | class JavaScriptFormatter { 40 | constructor () { 41 | this.TEMPLATE_FILE_PATH = getConfigPath('config.tpl.js') 42 | this.REQUIREJS_TEMPLATE_FILE = getConfigPath('requirejs.config.tpl.js') 43 | } 44 | 45 | generateConfigFile (answers) { 46 | const replacements = this.formatAnswers(answers) 47 | 48 | return FileUtils 49 | .readFile(this.TEMPLATE_FILE_PATH) 50 | .replace(/%(.*)%/g, (a, key) => replacements[key]) 51 | } 52 | 53 | writeConfigFile (path, answers) { 54 | FileUtils.saveFile(path, this.generateConfigFile(answers)) 55 | } 56 | 57 | writeRequirejsConfigFile (path) { 58 | FileUtils.copyFile(this.REQUIREJS_TEMPLATE_FILE, path) 59 | } 60 | 61 | formatAnswers (answers) { 62 | return { 63 | DATE: new Date(), 64 | BASE_PATH: answers.basePath, 65 | FRAMEWORKS: formatLine(answers.frameworks), 66 | FILES: formatFiles(answers.files, answers.onlyServedFiles), 67 | EXCLUDE: formatFiles(answers.exclude, []), 68 | AUTO_WATCH: answers.autoWatch ? 'true' : 'false', 69 | BROWSERS: formatLine(answers.browsers), 70 | PREPROCESSORS: formatPreprocessors(answers.preprocessors) 71 | } 72 | } 73 | } 74 | 75 | class CoffeeFormatter extends JavaScriptFormatter { 76 | constructor () { 77 | super() 78 | this.TEMPLATE_FILE_PATH = getConfigPath('config.tpl.coffee') 79 | this.REQUIREJS_TEMPLATE_FILE = getConfigPath('requirejs.config.tpl.coffee') 80 | } 81 | } 82 | 83 | class LiveFormatter extends JavaScriptFormatter { 84 | constructor () { 85 | super() 86 | this.TEMPLATE_FILE_PATH = getConfigPath('config.tpl.ls') 87 | } 88 | } 89 | 90 | class TypeFormatter extends JavaScriptFormatter { 91 | constructor () { 92 | super() 93 | this.TEMPLATE_FILE_PATH = getConfigPath('config.tpl.ts') 94 | } 95 | } 96 | 97 | exports.JavaScript = JavaScriptFormatter 98 | exports.Coffee = CoffeeFormatter 99 | exports.Live = LiveFormatter 100 | exports.Type = TypeFormatter 101 | 102 | exports.createForPath = function (path) { 103 | if (/\.coffee$/.test(path)) { 104 | return new CoffeeFormatter() 105 | } 106 | 107 | if (/\.ls$/.test(path)) { 108 | return new LiveFormatter() 109 | } 110 | 111 | if (/\.ts$/.test(path)) { 112 | return new TypeFormatter() 113 | } 114 | 115 | return new JavaScriptFormatter() 116 | } 117 | -------------------------------------------------------------------------------- /lib/init/log-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logQueue = [] 4 | function printLogQueue () { 5 | logQueue.forEach((log) => log()) 6 | logQueue.length = 0 7 | } 8 | 9 | function push (log) { 10 | logQueue.push(log) 11 | } 12 | 13 | module.exports = { 14 | printLogQueue, push 15 | } 16 | -------------------------------------------------------------------------------- /lib/launchers/capture_timeout.js: -------------------------------------------------------------------------------- 1 | const log = require('../logger').create('launcher') 2 | 3 | /** 4 | * Kill browser if it does not capture in given `captureTimeout`. 5 | */ 6 | function CaptureTimeoutLauncher (timer, captureTimeout) { 7 | if (!captureTimeout) { 8 | return 9 | } 10 | 11 | let pendingTimeoutId = null 12 | 13 | this.on('start', () => { 14 | pendingTimeoutId = timer.setTimeout(() => { 15 | pendingTimeoutId = null 16 | if (this.state !== this.STATE_BEING_CAPTURED) { 17 | return 18 | } 19 | 20 | log.warn(`${this.name} has not captured in ${captureTimeout} ms, killing.`) 21 | this.error = 'timeout' 22 | this.kill() 23 | }, captureTimeout) 24 | }) 25 | 26 | this.on('done', () => { 27 | if (pendingTimeoutId) { 28 | timer.clearTimeout(pendingTimeoutId) 29 | pendingTimeoutId = null 30 | } 31 | }) 32 | } 33 | 34 | CaptureTimeoutLauncher.decoratorFactory = function (timer, 35 | /* config.captureTimeout */ captureTimeout) { 36 | return function (launcher) { 37 | CaptureTimeoutLauncher.call(launcher, timer, captureTimeout) 38 | } 39 | } 40 | 41 | CaptureTimeoutLauncher.decoratorFactory.$inject = ['timer', 'config.captureTimeout'] 42 | 43 | module.exports = CaptureTimeoutLauncher 44 | -------------------------------------------------------------------------------- /lib/launchers/retry.js: -------------------------------------------------------------------------------- 1 | const log = require('../logger').create('launcher') 2 | 3 | function RetryLauncher (retryLimit) { 4 | this._retryLimit = retryLimit 5 | 6 | this.on('done', () => { 7 | if (!this.error) { 8 | return 9 | } 10 | 11 | if (this._retryLimit > 0) { 12 | log.info(`Trying to start ${this.name} again (${retryLimit - this._retryLimit + 1}/${retryLimit}).`) 13 | this.restart() 14 | this._retryLimit-- 15 | } else if (this._retryLimit === 0) { 16 | log.error(`${this.name} failed ${retryLimit} times (${this.error}). Giving up.`) 17 | } else { 18 | log.debug(`${this.name} failed (${this.error}). Not restarting.`) 19 | } 20 | }) 21 | } 22 | 23 | RetryLauncher.decoratorFactory = function (retryLimit) { 24 | return function (launcher) { 25 | RetryLauncher.call(launcher, retryLimit) 26 | } 27 | } 28 | 29 | RetryLauncher.decoratorFactory.$inject = ['config.retryLimit'] 30 | 31 | module.exports = RetryLauncher 32 | -------------------------------------------------------------------------------- /lib/middleware/source_files.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const querystring = require('querystring') 4 | const common = require('./common') 5 | 6 | const log = require('../logger').create('middleware:source-files') 7 | 8 | function findByPath (files, path) { 9 | return Array.from(files).find((file) => file.path === path) 10 | } 11 | 12 | function composeUrl (url, basePath, urlRoot) { 13 | return url 14 | .replace(urlRoot, '/') 15 | .replace(/\?.*$/, '') 16 | .replace(/^\/absolute/, '') 17 | .replace(/^\/base/, basePath) 18 | } 19 | 20 | // Source Files middleware is responsible for serving all the source files under the test. 21 | function createSourceFilesMiddleware (filesPromise, serveFile, basePath, urlRoot) { 22 | return function (request, response, next) { 23 | const requestedFilePath = composeUrl(request.url, basePath, urlRoot) 24 | // When a path contains HTML-encoded characters (e.g %2F used by Jenkins for branches with /) 25 | const requestedFilePathUnescaped = composeUrl(querystring.unescape(request.url), basePath, urlRoot) 26 | 27 | request.pause() 28 | 29 | log.debug(`Requesting ${request.url}`) 30 | log.debug(`Fetching ${requestedFilePath}`) 31 | 32 | return filesPromise.then(function (files) { 33 | // TODO(vojta): change served to be a map rather then an array 34 | const file = findByPath(files.served, requestedFilePath) || findByPath(files.served, requestedFilePathUnescaped) 35 | const rangeHeader = request.headers.range 36 | 37 | if (file) { 38 | const acceptEncodingHeader = request.headers['accept-encoding'] 39 | const matchedEncoding = Object.keys(file.encodings).find( 40 | (encoding) => new RegExp(`(^|.*, ?)${encoding}(,|$)`).test(acceptEncodingHeader) 41 | ) 42 | const content = file.encodings[matchedEncoding] || file.content 43 | 44 | serveFile(file.contentPath || file.path, rangeHeader, response, function () { 45 | if (/\?\w+/.test(request.url)) { 46 | common.setHeavyCacheHeaders(response) // files with timestamps - cache one year, rely on timestamps 47 | } else { 48 | common.setNoCacheHeaders(response) // without timestamps - no cache (debug) 49 | } 50 | if (matchedEncoding) { 51 | response.setHeader('Content-Encoding', matchedEncoding) 52 | } 53 | }, content, file.doNotCache) 54 | } else { 55 | next() 56 | } 57 | 58 | request.resume() 59 | }) 60 | } 61 | } 62 | 63 | createSourceFilesMiddleware.$inject = [ 64 | 'filesPromise', 'serveFile', 'config.basePath', 'config.urlRoot' 65 | ] 66 | 67 | exports.create = createSourceFilesMiddleware 68 | -------------------------------------------------------------------------------- /lib/middleware/stopper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stopper middleware is responsible for communicating with `karma stop`. 3 | */ 4 | 5 | const log = require('../logger').create('middleware:stopper') 6 | 7 | function createStopperMiddleware (urlRoot) { 8 | return function (request, response, next) { 9 | if (request.url !== urlRoot + 'stop') return next() 10 | response.writeHead(200) 11 | log.info('Stopping server') 12 | response.end('OK') 13 | process.kill(process.pid, 'SIGINT') 14 | } 15 | } 16 | 17 | createStopperMiddleware.$inject = ['config.urlRoot'] 18 | exports.create = createStopperMiddleware 19 | -------------------------------------------------------------------------------- /lib/middleware/strip_host.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Strip hostname from request path 3 | * This to handle requests that uses (normally over proxies) an absoluteURI as request path 4 | */ 5 | 6 | function stripHostFromUrl (url) { 7 | return url.replace(/^https?:\/\/[a-z.:\d-]+\//, '/') 8 | } 9 | 10 | // PUBLIC API 11 | exports.stripHost = stripHostFromUrl 12 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('graceful-fs') 4 | const path = require('path') 5 | const helper = require('./helper') 6 | 7 | const log = require('./logger').create('plugin') 8 | 9 | const IGNORED_PACKAGES = ['karma-cli', 'karma-runner.github.com'] 10 | 11 | function resolve (plugins, emitter) { 12 | const modules = [] 13 | 14 | function requirePlugin (name) { 15 | log.debug(`Loading plugin ${name}.`) 16 | try { 17 | modules.push(require(name)) 18 | } catch (e) { 19 | if (e.code === 'MODULE_NOT_FOUND' && e.message.includes(name)) { 20 | log.error(`Cannot find plugin "${name}".\n Did you forget to install it?\n npm install ${name} --save-dev`) 21 | } else { 22 | log.error(`Error during loading "${name}" plugin:\n ${e.message}`) 23 | } 24 | emitter.emit('load_error', 'plug_in', name) 25 | } 26 | } 27 | 28 | plugins.forEach(function (plugin) { 29 | if (helper.isString(plugin)) { 30 | if (!plugin.includes('*')) { 31 | requirePlugin(plugin) 32 | return 33 | } 34 | const pluginDirectory = path.normalize(path.join(__dirname, '/../..')) 35 | const regexp = new RegExp(`^${plugin.replace(/\*/g, '.*').replace(/\//g, '[/\\\\]')}`) 36 | 37 | log.debug(`Loading ${plugin} from ${pluginDirectory}`) 38 | fs.readdirSync(pluginDirectory) 39 | .map((e) => { 40 | const modulePath = path.join(pluginDirectory, e) 41 | if (e[0] === '@') { 42 | return fs.readdirSync(modulePath).map((e) => path.join(modulePath, e)) 43 | } 44 | return modulePath 45 | }) 46 | .reduce((a, x) => a.concat(x), []) 47 | .map((modulePath) => path.relative(pluginDirectory, modulePath)) 48 | .filter((moduleName) => !IGNORED_PACKAGES.includes(moduleName) && regexp.test(moduleName)) 49 | .forEach((pluginName) => requirePlugin(path.join(pluginDirectory, pluginName))) 50 | } else if (helper.isObject(plugin)) { 51 | log.debug(`Loading inline plugin defining ${Object.keys(plugin).join(', ')}.`) 52 | modules.push(plugin) 53 | } else { 54 | log.error(`Invalid plugin ${plugin}`) 55 | emitter.emit('load_error', 'plug_in', plugin) 56 | } 57 | }) 58 | 59 | return modules 60 | } 61 | 62 | /** 63 | Create a function to handle errors in plugin loading. 64 | @param {Object} injector, the dict of dependency injection objects. 65 | @return function closed over injector, which reports errors. 66 | */ 67 | function createInstantiatePlugin (injector) { 68 | const emitter = injector.get('emitter') 69 | // Cache to avoid report errors multiple times per plugin. 70 | const pluginInstances = new Map() 71 | return function instantiatePlugin (kind, name) { 72 | if (pluginInstances.has(name)) { 73 | return pluginInstances.get(name) 74 | } 75 | 76 | let p 77 | try { 78 | p = injector.get(`${kind}:${name}`) 79 | if (!p) { 80 | log.error(`Failed to instantiate ${kind} ${name}`) 81 | emitter.emit('load_error', kind, name) 82 | } 83 | } catch (e) { 84 | if (e.message.includes(`No provider for "${kind}:${name}"`)) { 85 | log.error(`Cannot load "${name}", it is not registered!\n Perhaps you are missing some plugin?`) 86 | } else { 87 | log.error(`Cannot load "${name}"!\n ` + e.stack) 88 | } 89 | emitter.emit('load_error', kind, name) 90 | } 91 | pluginInstances.set(name, p, `${kind}:${name}`) 92 | return p 93 | } 94 | } 95 | 96 | createInstantiatePlugin.$inject = ['injector'] 97 | 98 | module.exports = { resolve, createInstantiatePlugin } 99 | -------------------------------------------------------------------------------- /lib/reporters/base_color.js: -------------------------------------------------------------------------------- 1 | const { red, yellow, green, cyan } = require('@colors/colors/safe') 2 | 3 | function BaseColorReporter () { 4 | this.USE_COLORS = true 5 | 6 | this.LOG_SINGLE_BROWSER = '%s: ' + cyan('%s') + '\n' 7 | this.LOG_MULTI_BROWSER = '%s %s: ' + cyan('%s') + '\n' 8 | 9 | this.SPEC_FAILURE = red('%s %s FAILED') + '\n' 10 | this.SPEC_SLOW = yellow('%s SLOW %s: %s') + '\n' 11 | this.ERROR = red('%s ERROR') + '\n' 12 | 13 | this.FINISHED_ERROR = red(' ERROR') 14 | this.FINISHED_SUCCESS = green(' SUCCESS') 15 | this.FINISHED_DISCONNECTED = red(' DISCONNECTED') 16 | 17 | this.X_FAILED = red(' (%d FAILED)') 18 | 19 | this.TOTAL_SUCCESS = green('TOTAL: %d SUCCESS') + '\n' 20 | this.TOTAL_FAILED = red('TOTAL: %d FAILED, %d SUCCESS') + '\n' 21 | } 22 | 23 | // PUBLISH 24 | module.exports = BaseColorReporter 25 | -------------------------------------------------------------------------------- /lib/reporters/dots.js: -------------------------------------------------------------------------------- 1 | const BaseReporter = require('./base') 2 | 3 | function DotsReporter (formatError, reportSlow, useColors, browserConsoleLogOptions) { 4 | BaseReporter.call(this, formatError, reportSlow, useColors, browserConsoleLogOptions) 5 | 6 | const DOTS_WRAP = 80 7 | this.EXCLUSIVELY_USE_COLORS = false 8 | this.onRunStart = function () { 9 | this._browsers = [] 10 | this._dotsCount = 0 11 | } 12 | 13 | this.onBrowserStart = function (browser) { 14 | this._browsers.push(browser) 15 | } 16 | 17 | this.writeCommonMsg = function (msg) { 18 | if (this._dotsCount) { 19 | this._dotsCount = 0 20 | msg = '\n' + msg 21 | } 22 | 23 | this.write(msg) 24 | } 25 | 26 | this.specSuccess = function () { 27 | this._dotsCount = (this._dotsCount + 1) % DOTS_WRAP 28 | this.write(this._dotsCount ? '.' : '.\n') 29 | } 30 | 31 | this.onBrowserComplete = function (browser) { 32 | this.writeCommonMsg(this.renderBrowser(browser) + '\n') 33 | } 34 | 35 | this.onRunComplete = function (browsers, results) { 36 | if (browsers.length > 1 && !results.disconnected && !results.error) { 37 | if (!results.failed) { 38 | this.write(this.TOTAL_SUCCESS, results.success) 39 | } else { 40 | this.write(this.TOTAL_FAILED, results.failed, results.success) 41 | } 42 | } 43 | } 44 | } 45 | 46 | // PUBLISH 47 | module.exports = DotsReporter 48 | -------------------------------------------------------------------------------- /lib/reporters/dots_color.js: -------------------------------------------------------------------------------- 1 | const DotsReporter = require('./dots') 2 | const BaseColorReporter = require('./base_color') 3 | 4 | function DotsColorReporter (formatError, reportSlow, useColors, browserConsoleLogOptions) { 5 | DotsReporter.call(this, formatError, reportSlow, useColors, browserConsoleLogOptions) 6 | BaseColorReporter.call(this) 7 | this.EXCLUSIVELY_USE_COLORS = true 8 | } 9 | 10 | // PUBLISH 11 | module.exports = DotsColorReporter 12 | -------------------------------------------------------------------------------- /lib/reporters/multi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const helper = require('../helper') 4 | 5 | class MultiReporter { 6 | constructor (reporters) { 7 | this._reporters = reporters 8 | } 9 | 10 | addAdapter (adapter) { 11 | this._reporters.forEach((reporter) => reporter.adapters.push(adapter)) 12 | } 13 | 14 | removeAdapter (adapter) { 15 | this._reporters.forEach((reporter) => helper.arrayRemove(reporter.adapters, adapter)) 16 | } 17 | } 18 | 19 | module.exports = MultiReporter 20 | -------------------------------------------------------------------------------- /lib/reporters/progress.js: -------------------------------------------------------------------------------- 1 | const BaseReporter = require('./base') 2 | 3 | function ProgressReporter (formatError, reportSlow, useColors, browserConsoleLogOptions) { 4 | BaseReporter.call(this, formatError, reportSlow, useColors, browserConsoleLogOptions) 5 | 6 | this.EXCLUSIVELY_USE_COLORS = false 7 | this._browsers = [] 8 | 9 | this.writeCommonMsg = function (msg) { 10 | this.write(this._remove() + msg + this._render()) 11 | } 12 | 13 | this.specSuccess = function () { 14 | this.write(this._refresh()) 15 | } 16 | 17 | this.onBrowserComplete = function () { 18 | this.write(this._refresh()) 19 | } 20 | 21 | this.onRunStart = function () { 22 | this._browsers = [] 23 | this._isRendered = false 24 | } 25 | 26 | this.onBrowserStart = function (browser) { 27 | this._browsers.push(browser) 28 | 29 | if (this._isRendered) { 30 | this.write('\n') 31 | } 32 | 33 | this.write(this._refresh()) 34 | } 35 | 36 | this._remove = function () { 37 | if (!this._isRendered) { 38 | return '' 39 | } 40 | 41 | let cmd = '' 42 | this._browsers.forEach(function () { 43 | cmd += '\x1B[1A' + '\x1B[2K' 44 | }) 45 | 46 | this._isRendered = false 47 | 48 | return cmd 49 | } 50 | 51 | this._render = function () { 52 | this._isRendered = true 53 | 54 | return this._browsers.map(this.renderBrowser).join('\n') + '\n' 55 | } 56 | 57 | this._refresh = function () { 58 | return this._remove() + this._render() 59 | } 60 | } 61 | 62 | // PUBLISH 63 | module.exports = ProgressReporter 64 | -------------------------------------------------------------------------------- /lib/reporters/progress_color.js: -------------------------------------------------------------------------------- 1 | const ProgressReporter = require('./progress') 2 | const BaseColorReporter = require('./base_color') 3 | 4 | function ProgressColorReporter (formatError, reportSlow, useColors, browserConsoleLogOptions) { 5 | ProgressReporter.call(this, formatError, reportSlow, useColors, browserConsoleLogOptions) 6 | BaseColorReporter.call(this) 7 | this.EXCLUSIVELY_USE_COLORS = true 8 | } 9 | 10 | // PUBLISH 11 | module.exports = ProgressColorReporter 12 | -------------------------------------------------------------------------------- /lib/stopper.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const cfg = require('./config') 3 | const logger = require('./logger') 4 | const helper = require('./helper') 5 | const { lookup } = require('./utils/dns-utils') 6 | 7 | exports.stop = function (cliOptionsOrConfig, done) { 8 | cliOptionsOrConfig = cliOptionsOrConfig || {} 9 | const log = logger.create('stopper') 10 | done = helper.isFunction(done) ? done : process.exit 11 | 12 | let config 13 | if (cliOptionsOrConfig instanceof cfg.Config) { 14 | config = cliOptionsOrConfig 15 | } else { 16 | logger.setupFromConfig({ 17 | colors: cliOptionsOrConfig.colors, 18 | logLevel: cliOptionsOrConfig.logLevel 19 | }) 20 | const deprecatedCliOptionsMessage = 21 | 'Passing raw CLI options to `stopper(config, done)` is deprecated. Use ' + 22 | '`parseConfig(configFilePath, cliOptions, {promiseConfig: true, throwErrors: true})` ' + 23 | 'to prepare a processed `Config` instance and pass that as the ' + 24 | '`config` argument instead.' 25 | log.warn(deprecatedCliOptionsMessage) 26 | try { 27 | config = cfg.parseConfig( 28 | cliOptionsOrConfig.configFile, 29 | cliOptionsOrConfig, 30 | { 31 | promiseConfig: false, 32 | throwErrors: true 33 | } 34 | ) 35 | } catch (parseConfigError) { 36 | // TODO: change how `done` falls back to exit in next major version 37 | // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378 38 | done(1) 39 | } 40 | } 41 | 42 | const request = http.request({ 43 | hostname: config.hostname, 44 | path: config.urlRoot + 'stop', 45 | port: config.port, 46 | method: 'GET', 47 | lookup 48 | }) 49 | 50 | request.on('response', function (response) { 51 | if (response.statusCode === 200) { 52 | log.info('Server stopped.') 53 | done(0) 54 | } else { 55 | log.error(`Server returned status code: ${response.statusCode}`) 56 | done(1) 57 | } 58 | }) 59 | 60 | request.on('error', function (e) { 61 | if (e.code === 'ECONNREFUSED') { 62 | log.error(`There is no server listening on port ${config.port}`) 63 | done(1, e.code) 64 | } else { 65 | throw e 66 | } 67 | }) 68 | request.end() 69 | } 70 | -------------------------------------------------------------------------------- /lib/temp_dir.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('graceful-fs') 5 | const rimraf = require('rimraf') 6 | const log = require('./logger').create('temp-dir') 7 | 8 | const TEMP_DIR = require('os').tmpdir() 9 | 10 | module.exports = { 11 | getPath (suffix) { 12 | return path.normalize(TEMP_DIR + suffix) 13 | }, 14 | 15 | create (path) { 16 | log.debug(`Creating temp dir at ${path}`) 17 | 18 | try { 19 | fs.mkdirSync(path) 20 | } catch (e) { 21 | log.warn(`Failed to create a temp dir at ${path}`) 22 | } 23 | 24 | return path 25 | }, 26 | 27 | remove (path, done) { 28 | log.debug(`Cleaning temp dir ${path}`) 29 | rimraf(path, done) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { URL } = require('url') 5 | 6 | /** 7 | * Url object used for tracking files in `file-list.js`. 8 | */ 9 | class Url { 10 | constructor (path, type, integrity) { 11 | this.path = path 12 | this.originalPath = path 13 | this.type = type 14 | this.integrity = integrity 15 | this.isUrl = true 16 | } 17 | 18 | /** 19 | * Detect type from the file extension in the path part of the URL. 20 | * @returns {string} detected file type or empty string 21 | */ 22 | detectType () { 23 | return path.extname(new URL(this.path).pathname).slice(1) 24 | } 25 | 26 | toString () { 27 | return this.path 28 | } 29 | } 30 | 31 | module.exports = Url 32 | -------------------------------------------------------------------------------- /lib/utils/crypto-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | 5 | const CryptoUtils = { 6 | sha1 (data) { 7 | return crypto 8 | .createHash('sha1') 9 | .update(data) 10 | .digest('hex') 11 | } 12 | } 13 | 14 | module.exports = CryptoUtils 15 | -------------------------------------------------------------------------------- /lib/utils/dns-utils.js: -------------------------------------------------------------------------------- 1 | const dns = require('dns') 2 | 3 | // Node >=17 has different DNS resolution (see 4 | // https://github.com/nodejs/node/issues/40702), it resolves domains 5 | // according to the OS settings instead of IPv4-address first. The Karma server 6 | // only listens on IPv4 address (127.0.0.1) by default, but the requests are 7 | // sent to `localhost` in several places and `localhost` is resolved into IPv6 8 | // address (`::`). So the run/stop/proxy request is unable to reach the Karma 9 | // server and produces an error. To mitigate this issue karma force the 10 | // IPv4-address first approach in Node >=17 as well. 11 | module.exports.lookup = (hostname, options, callback) => dns.lookup(hostname, { ...options, verbatim: false }, callback) 12 | -------------------------------------------------------------------------------- /lib/utils/file-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('graceful-fs') 4 | 5 | const FileUtils = { 6 | readFile (path) { 7 | return fs.readFileSync(path).toString() 8 | }, 9 | 10 | saveFile (path, content) { 11 | fs.writeFileSync(path, content) 12 | }, 13 | 14 | copyFile (src, dest) { 15 | FileUtils.saveFile(dest, FileUtils.readFile(src)) 16 | }, 17 | 18 | removeFileIfExists (src) { 19 | if (fs.existsSync(src)) { 20 | fs.unlinkSync(src) 21 | } 22 | } 23 | } 24 | 25 | module.exports = FileUtils 26 | -------------------------------------------------------------------------------- /lib/utils/net-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const net = require('net') 4 | 5 | const NetUtils = { 6 | bindAvailablePort (port, listenAddress) { 7 | return new Promise((resolve, reject) => { 8 | const server = net.createServer() 9 | 10 | server 11 | .on('error', (err) => { 12 | server.close() 13 | if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { 14 | server.listen(++port, listenAddress) 15 | } else { 16 | reject(new Error(`Failed to bind ${port}: ` + (err.stack || err))) 17 | } 18 | }) 19 | .on('listening', () => { 20 | resolve(server) 21 | }) 22 | .listen(port, listenAddress) 23 | }) 24 | } 25 | } 26 | 27 | module.exports = NetUtils 28 | -------------------------------------------------------------------------------- /lib/utils/path-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const PathUtils = { 6 | formatPathMapping (path, line, column) { 7 | return path + (line ? `:${line}` : '') + (column ? `:${column}` : '') 8 | }, 9 | 10 | calculateAbsolutePath (karmaRelativePath) { 11 | return path.join(__dirname, '..', '..', karmaRelativePath) 12 | } 13 | 14 | } 15 | 16 | module.exports = PathUtils 17 | -------------------------------------------------------------------------------- /lib/utils/pattern-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | const PatternUtils = { 6 | getBaseDir (pattern) { 7 | return pattern 8 | .replace(/[/\\][^/\\]*\*.*$/, '') // remove parts with * 9 | .replace(/[/\\][^/\\]*[!+]\(.*$/, '') // remove parts with !(...) and +(...) 10 | .replace(/[/\\][^/\\]*\)\?.*$/, '') || path.sep // remove parts with (...)? 11 | } 12 | } 13 | 14 | module.exports = PatternUtils 15 | -------------------------------------------------------------------------------- /lib/watcher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mm = require('minimatch') 4 | const braces = require('braces') 5 | const PatternUtils = require('./utils/pattern-utils') 6 | 7 | const helper = require('./helper') 8 | const log = require('./logger').create('watcher') 9 | 10 | const DIR_SEP = require('path').sep 11 | 12 | function watchPatterns (patterns, watcher) { 13 | let expandedPatterns = [] 14 | patterns.map((pattern) => { 15 | // expand ['a/{b,c}'] to ['a/b', 'a/c'] 16 | expandedPatterns = expandedPatterns.concat(braces.expand(pattern, { keepEscaping: true })) 17 | }) 18 | expandedPatterns 19 | .map(PatternUtils.getBaseDir) 20 | .filter((path, index, paths) => paths.indexOf(path) === index) // filter unique values 21 | .forEach((path, index, paths) => { 22 | if (!paths.some((p) => path.startsWith(p + DIR_SEP))) { 23 | watcher.add(path) 24 | log.debug(`Watching "${path}"`) 25 | } 26 | }) 27 | } 28 | 29 | function checkAnyPathMatch (patterns, path) { 30 | return patterns.some((pattern) => mm(path, pattern, { dot: true })) 31 | } 32 | 33 | function createIgnore (patterns, excludes) { 34 | return function (path, stat) { 35 | if (stat && !stat.isDirectory()) { 36 | return !checkAnyPathMatch(patterns, path) || checkAnyPathMatch(excludes, path) 37 | } else { 38 | return false 39 | } 40 | } 41 | } 42 | 43 | function getWatchedPatterns (patterns) { 44 | return patterns 45 | .filter((pattern) => pattern.watched) 46 | .map((pattern) => pattern.pattern) 47 | } 48 | 49 | function watch (patterns, excludes, fileList, usePolling, emitter) { 50 | const watchedPatterns = getWatchedPatterns(patterns) 51 | // Lazy-load 'chokidar' to make the dependency optional. This is desired when 52 | // third-party watchers are in use. 53 | const chokidar = require('chokidar') 54 | const watcher = new chokidar.FSWatcher({ 55 | usePolling: usePolling, 56 | ignorePermissionErrors: true, 57 | ignoreInitial: true, 58 | ignored: createIgnore(watchedPatterns, excludes) 59 | }) 60 | 61 | watchPatterns(watchedPatterns, watcher) 62 | 63 | watcher 64 | .on('add', (path) => fileList.addFile(helper.normalizeWinPath(path))) 65 | .on('change', (path) => fileList.changeFile(helper.normalizeWinPath(path))) 66 | .on('unlink', (path) => fileList.removeFile(helper.normalizeWinPath(path))) 67 | .on('error', log.debug.bind(log)) 68 | 69 | emitter.on('exit', (done) => { 70 | watcher.close() 71 | done() 72 | }) 73 | 74 | return watcher 75 | } 76 | 77 | watch.$inject = [ 78 | 'config.files', 79 | 'config.exclude', 80 | 'fileList', 81 | 'config.usePolling', 82 | 'emitter' 83 | ] 84 | 85 | module.exports = watch 86 | -------------------------------------------------------------------------------- /logo/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/logo/banner.png -------------------------------------------------------------------------------- /logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/logo/favicon.ico -------------------------------------------------------------------------------- /logo/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/logo/logo.ai -------------------------------------------------------------------------------- /logo/logo.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/logo/logo.eps -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/logo/logo.png -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Add logging for releases until we are fully confident of the release solution. 3 | debug: true, 4 | branches: 'master', 5 | verifyConditions: [ 6 | '@semantic-release/changelog', 7 | '@semantic-release/npm', 8 | '@semantic-release/github' 9 | ], 10 | prepare: [ 11 | './tools/update-contributors', 12 | '@semantic-release/changelog', 13 | '@semantic-release/npm', 14 | '@semantic-release/git' 15 | ], 16 | publish: [ 17 | '@semantic-release/npm', 18 | '@semantic-release/github' 19 | ], 20 | success: [ 21 | '@semantic-release/github', 22 | './tools/update-docs' 23 | ], 24 | 25 | // The release rules determine what kind of release should be triggered 26 | // based on the information included in the commit message. The default 27 | // rules used by semantic-release are the same, but they are set explicitly 28 | // for better visibility. 29 | // See https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js 30 | releaseRules: [ 31 | { breaking: true, release: 'major' }, 32 | { revert: true, release: 'patch' }, 33 | { type: 'feat', release: 'minor' }, 34 | { type: 'fix', release: 'patch' }, 35 | { type: 'perf', release: 'patch' }, 36 | { type: 'build', release: 'patch' } 37 | ], 38 | 39 | // The preset determines which commits are included in the changelog and how 40 | // the changelog is formatted. The default value used by semantic-release is 41 | // the same, but it is set explicitly for visibility. 42 | // See https://semantic-release.gitbook.io/semantic-release/#commit-message-format 43 | // See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular 44 | preset: 'angular' 45 | } 46 | -------------------------------------------------------------------------------- /requirejs.config.tpl.coffee: -------------------------------------------------------------------------------- 1 | allTestFiles = [] 2 | TEST_REGEXP = /(spec|test)(\.coffee)?(\.js)?$/i 3 | 4 | # Get a list of all the test files to include 5 | Object.keys(window.__karma__.files).forEach (file) -> 6 | 7 | if TEST_REGEXP.test(file) 8 | # Normalize paths to RequireJS module names. 9 | # If you require sub-dependencies of test files to be loaded as-is (requiring file extension) 10 | # then do not normalize the paths 11 | allTestFiles.push file.replace(/^\/base\/|\.js$/g, '') 12 | return 13 | 14 | require.config 15 | # Karma serves files under /base, which is the basePath from your config file 16 | baseUrl: "/base" 17 | 18 | # dynamically load all test files 19 | deps: allTestFiles 20 | 21 | # we have to kickoff jasmine, as it is asynchronous 22 | callback: window.__karma__.start 23 | -------------------------------------------------------------------------------- /requirejs.config.tpl.js: -------------------------------------------------------------------------------- 1 | var allTestFiles = [] 2 | var TEST_REGEXP = /(spec|test)\.js$/i 3 | 4 | // Get a list of all the test files to include 5 | Object.keys(window.__karma__.files).forEach(function (file) { 6 | if (TEST_REGEXP.test(file)) { 7 | // Normalize paths to RequireJS module names. 8 | // If you require sub-dependencies of test files to be loaded as-is (requiring file extension) 9 | // then do not normalize the paths 10 | var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '') 11 | allTestFiles.push(normalizedTestModule) 12 | } 13 | }) 14 | 15 | require.config({ 16 | // Karma serves files under /base, which is the basePath from your config file 17 | baseUrl: '/base', 18 | 19 | // dynamically load all test files 20 | deps: allTestFiles, 21 | 22 | // we have to kickoff jasmine, as it is asynchronous 23 | callback: window.__karma__.start 24 | }) 25 | -------------------------------------------------------------------------------- /scripts/client.js: -------------------------------------------------------------------------------- 1 | const browserify = require('browserify') 2 | const watchify = require('watchify') 3 | const { createWriteStream } = require('fs') 4 | const { readFile } = require('fs').promises 5 | 6 | const bundleResourceToFile = (inPath, outPath) => { 7 | return new Promise((resolve, reject) => { 8 | browserify(inPath).bundle() 9 | .once('error', (e) => reject(e)) 10 | .pipe(createWriteStream(outPath)) 11 | .once('finish', () => resolve()) 12 | }) 13 | } 14 | 15 | const bundleResource = (inPath) => { 16 | return new Promise((resolve, reject) => { 17 | browserify(inPath).bundle((err, buffer) => { 18 | if (err != null) { 19 | reject(err) 20 | return 21 | } 22 | 23 | resolve(buffer) 24 | }) 25 | }) 26 | } 27 | 28 | const watchResourceToFile = (inPath, outPath) => { 29 | const b = browserify({ 30 | entries: [inPath], 31 | cache: {}, 32 | packageCache: {}, 33 | plugin: [watchify] 34 | }) 35 | 36 | const bundle = () => { 37 | b.bundle() 38 | .once('error', (e) => { 39 | console.error(`Failed to bundle ${inPath} into ${outPath}.`) 40 | console.error(e) 41 | }) 42 | .pipe(createWriteStream(outPath)) 43 | .once('finish', () => console.log(`Bundled ${inPath} into ${outPath}.`)) 44 | } 45 | 46 | b.on('update', bundle) 47 | bundle() 48 | } 49 | 50 | const main = async () => { 51 | if (process.argv[2] === 'build') { 52 | await bundleResourceToFile('client/main.js', 'static/karma.js') 53 | await bundleResourceToFile('context/main.js', 'static/context.js') 54 | } else if (process.argv[2] === 'check') { 55 | const expectedClient = await bundleResource('client/main.js') 56 | const expectedContext = await bundleResource('context/main.js') 57 | 58 | const actualClient = await readFile('static/karma.js') 59 | const actualContext = await readFile('static/context.js') 60 | 61 | if (Buffer.compare(expectedClient, actualClient) !== 0 || Buffer.compare(expectedContext, actualContext) !== 0) { 62 | // eslint-disable-next-line no-throw-literal 63 | throw 'Bundled client assets are outdated. Forgot to run "npm run build"?' 64 | } 65 | } else if (process.argv[2] === 'watch') { 66 | watchResourceToFile('client/main.js', 'static/karma.js') 67 | watchResourceToFile('context/main.js', 'static/context.js') 68 | } else { 69 | // eslint-disable-next-line no-throw-literal 70 | throw `Unknown command: ${process.argv[2]}` 71 | } 72 | } 73 | 74 | main().catch((err) => { 75 | console.error(err) 76 | process.exit(1) 77 | }) 78 | -------------------------------------------------------------------------------- /scripts/integration-tests.sh: -------------------------------------------------------------------------------- 1 | PKG_FILE="$PWD/$(npm pack)" 2 | git clone https://github.com/karma-runner/integration-tests.git --depth 1 3 | cd integration-tests 4 | ./run.sh $PKG_FILE 5 | -------------------------------------------------------------------------------- /scripts/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/karma 8 | # 9 | 10 | if type complete &>/dev/null; then 11 | __karma_completion () { 12 | local si="$IFS" 13 | IFS=#39;\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=#39;\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 | -------------------------------------------------------------------------------- /static/client.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <!-- 3 | The entry point for client. This file is loaded just once when the client is captured. 4 | It contains socket.io and all the communication logic. 5 | --> 6 | <html> 7 | <head> 8 | %X_UA_COMPATIBLE% 9 | <title>Karma</title> 10 | <link href="favicon.ico" rel="icon" type="image/x-icon"> 11 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 12 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> 13 | <style type="text/css"> 14 | iframe { 15 | height: 100%; 16 | width: 100%; 17 | border: 0; 18 | } 19 | 20 | html, body { 21 | height: 100%; 22 | padding: 0; 23 | margin: 0; 24 | 25 | font-family: sans-serif; 26 | } 27 | 28 | .offline { 29 | background: #DDD; 30 | } 31 | 32 | .online { 33 | background: #6C4; 34 | } 35 | 36 | .idle { 37 | } 38 | 39 | .executing { 40 | background: #F99; 41 | } 42 | 43 | #banner { 44 | padding: 5px 10px; 45 | } 46 | 47 | h1 { 48 | font-size: 1.8em; 49 | margin: 0; 50 | padding: 0; 51 | } 52 | 53 | ul { 54 | margin: 0; 55 | padding: 0; 56 | 57 | list-style: none; 58 | } 59 | 60 | li { 61 | padding: 5px 12px; 62 | } 63 | 64 | .btn-debug { 65 | float: right; 66 | } 67 | 68 | .offline .btn-debug { 69 | display: none; 70 | } 71 | 72 | .btn-debug { 73 | -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; 74 | -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; 75 | box-shadow:inset 0px 1px 0px 0px #ffffff; 76 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ffffff), color-stop(1, #f6f6f6) ); 77 | background:-moz-linear-gradient( center top, #ffffff 5%, #f6f6f6 100% ); 78 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f6f6f6'); 79 | background-color:#ffffff; 80 | -moz-border-radius:6px; 81 | -webkit-border-radius:6px; 82 | border-radius:6px; 83 | border:1px solid #dcdcdc; 84 | display:inline-block; 85 | color:#666666; 86 | font-family:arial; 87 | font-size:15px; 88 | font-weight:bold; 89 | padding:6px 24px; 90 | text-decoration:none; 91 | text-shadow:1px 1px 0px #ffffff; 92 | } 93 | 94 | .btn-debug:hover { 95 | background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f6f6f6), color-stop(1, #ffffff) ); 96 | background:-moz-linear-gradient( center top, #f6f6f6 5%, #ffffff 100% ); 97 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f6f6f6', endColorstr='#ffffff'); 98 | background-color:#f6f6f6; 99 | } 100 | </style> 101 | </head> 102 | <body> 103 | <div id="banner" class="offline"> 104 | <a href="#" onclick="window.open('debug.html%X_UA_COMPATIBLE_URL%')" class="btn-debug">DEBUG</a> 105 | <h1 id="title">Karma - starting</h1> 106 | </div> 107 | 108 | <ul id="browsers"></ul> 109 | 110 | <iframe id="context" src="about:blank" width="100%" height="100%"></iframe> 111 | 112 | <script src="socket.io/socket.io.min.js"></script> 113 | <script src="karma.js"></script> 114 | </body> 115 | </html> 116 | -------------------------------------------------------------------------------- /static/context.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <!-- 3 | This is the execution context. 4 | Loaded within the iframe. 5 | Reloaded before every execution run. 6 | --> 7 | <html> 8 | <head> 9 | <title></title> 10 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 11 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> 12 | </head> 13 | <body> 14 | <!-- The scripts need to be in the body DOM element, as some test running frameworks need the body 15 | to have already been created so they can insert their magic into it. For example, if loaded 16 | before body, Angular Scenario test framework fails to find the body and crashes and burns in 17 | an epic manner. --> 18 | <script src="context.js"></script> 19 | <script type="text/javascript"> 20 | // Configure our Karma and set up bindings 21 | %CLIENT_CONFIG% 22 | window.__karma__.setupContext(window); 23 | 24 | // All served files with the latest timestamps 25 | %MAPPINGS% 26 | </script> 27 | <!-- Dynamically replaced with <script> tags --> 28 | %SCRIPTS% 29 | <!-- Since %SCRIPTS% might include modules, the `loaded()` call needs to be in a module too. 30 | This ensures all the tests will have been declared before karma tries to run them. --> 31 | <script type="module"> 32 | window.__karma__.loaded(); 33 | </script> 34 | <script nomodule> 35 | window.__karma__.loaded(); 36 | </script> 37 | </body> 38 | </html> 39 | -------------------------------------------------------------------------------- /static/debug.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <!-- 3 | This file is almost the same as context.html - loads all source files, 4 | but its purpose is to be loaded in the main frame (not within an iframe), 5 | just for immediate execution, without reporting to Karma server. 6 | --> 7 | <html> 8 | <head> 9 | %X_UA_COMPATIBLE% 10 | <title>Karma DEBUG RUNNER</title> 11 | <link href="favicon.ico" rel="icon" type="image/x-icon" /> 12 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 13 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> 14 | </head> 15 | <body> 16 | <!-- The scripts need to be at the end of body, so that some test running frameworks 17 | (Angular Scenario, for example) need the body to be loaded so that it can insert its magic 18 | into it. If it is before body, then it fails to find the body and crashes and burns in an epic 19 | manner. --> 20 | <script src="context.js"></script> 21 | <script src="debug.js"></script> 22 | <script type="text/javascript"> 23 | // Configure our Karma 24 | %CLIENT_CONFIG% 25 | 26 | // All served files with the latest timestamps 27 | %MAPPINGS% 28 | </script> 29 | <!-- Dynamically replaced with <script> tags --> 30 | %SCRIPTS% 31 | <!-- Since %SCRIPTS% might include modules, the `loaded()` call needs to be in a module too. 32 | This ensures all the tests will have been declared before karma tries to run them. --> 33 | <script type="module"> 34 | window.__karma__.loaded(); 35 | </script> 36 | <script nomodule> 37 | window.__karma__.loaded(); 38 | </script> 39 | </body> 40 | </html> 41 | -------------------------------------------------------------------------------- /static/debug.js: -------------------------------------------------------------------------------- 1 | // Override the Karma setup for local debugging 2 | window.__karma__.info = function (info) { 3 | if (info.dump && window.console) window.console.log(info.dump) 4 | } 5 | window.__karma__.complete = function () { 6 | if (window.console) window.console.log('Skipped ' + this.skipped + ' tests') 7 | } 8 | window.__karma__.skipped = 0 9 | window.__karma__.result = window.console ? function (result) { 10 | if (result.skipped) { 11 | this.skipped++ 12 | return 13 | } 14 | var msg = result.success ? 'SUCCESS ' : 'FAILED ' 15 | window.console.log(msg + result.suite.join(' ') + ' ' + result.description) 16 | 17 | for (var i = 0; i < result.log.length; i++) { 18 | // Printing error without losing stack trace 19 | (function (err) { 20 | setTimeout(function () { 21 | window.console.error(err) 22 | }) 23 | })(result.log[i]) 24 | } 25 | } : function () {} 26 | window.__karma__.loaded = function () { 27 | this.start() 28 | } 29 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/static/favicon.ico -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | }, 9 | "rules": { 10 | "no-unused-expressions": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true, 4 | "browser": true 5 | } 6 | } -------------------------------------------------------------------------------- /test/client/mocks.js: -------------------------------------------------------------------------------- 1 | function Emitter () { 2 | var listeners = {} 3 | 4 | this.on = function (event, fn) { 5 | if (!listeners[event]) { 6 | listeners[event] = [] 7 | } 8 | 9 | listeners[event].push(fn) 10 | } 11 | 12 | this.emit = function (event) { 13 | var eventListeners = listeners[event] 14 | 15 | if (!eventListeners) return 16 | 17 | var i = 0 18 | while (i < eventListeners.length) { 19 | eventListeners[i].apply(null, Array.prototype.slice.call(arguments, 1)) 20 | i++ 21 | } 22 | } 23 | } 24 | 25 | function MockSocket () { 26 | Emitter.call(this) 27 | 28 | this.socket = { transport: { name: 'websocket' } } 29 | 30 | var transportName = 'websocket' 31 | 32 | this.io = { 33 | engine: { 34 | on: function (event, cb) { 35 | if (event === 'upgrade' && transportName === 'websocket') { 36 | cb() 37 | } 38 | } 39 | } 40 | } 41 | 42 | this.disconnect = function () { 43 | this.emit('disconnect') 44 | } 45 | 46 | // MOCK API 47 | this._setTransportNameTo = function (name) { 48 | transportName = name 49 | } 50 | } 51 | 52 | exports.Socket = MockSocket 53 | -------------------------------------------------------------------------------- /test/client/util.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | 3 | var util = require('../../common/util') 4 | 5 | describe('util', function () { 6 | it('parseQueryParams', function () { 7 | var params = util.parseQueryParams('?id=123&return_url=http://whatever.com') 8 | 9 | assert.deepStrictEqual(params, { id: '123', return_url: 'http://whatever.com' }) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/basic.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic Testrunner 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to be able to run tests from the command line. 5 | 6 | Scenario: Execute a test in ChromeHeadless 7 | Given a configuration with: 8 | """ 9 | files = ['basic/plus.js', 'basic/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | """ 16 | When I start Karma 17 | Then it passes with: 18 | """ 19 | .. 20 | Chrome Headless 21 | """ 22 | 23 | Scenario: Execute a test in Firefox 24 | Given a configuration with: 25 | """ 26 | files = ['basic/plus.js', 'basic/test.js'] 27 | browsers = ['FirefoxHeadless'] 28 | plugins = [ 29 | 'karma-jasmine', 30 | 'karma-firefox-launcher' 31 | ] 32 | """ 33 | When I start Karma 34 | Then it passes with: 35 | """ 36 | .. 37 | Firefox 38 | """ 39 | -------------------------------------------------------------------------------- /test/e2e/custom-context.feature: -------------------------------------------------------------------------------- 1 | Feature: Custom Context File 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to use a custom context file 5 | 6 | Scenario: Custom context.html file 7 | Given a configuration with: 8 | """ 9 | files = ['context/*.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | customContextFile = 'context/context2.html' 16 | """ 17 | When I start Karma 18 | Then it passes with: 19 | """ 20 | . 21 | Chrome Headless 22 | """ 23 | -------------------------------------------------------------------------------- /test/e2e/displayname.feature: -------------------------------------------------------------------------------- 1 | Feature: Custom Display-name 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to Karma to send custom display-name. 5 | 6 | Scenario: Execute a test in ChromeHeadless 7 | Given a configuration with: 8 | """ 9 | files = ['basic/plus.js', 'basic/test.js']; 10 | browsers = ['customChrome']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | customLaunchers = { 16 | customChrome: { 17 | base: 'ChromeHeadlessNoSandbox', 18 | displayName: '42' 19 | } 20 | }; 21 | """ 22 | When I start Karma 23 | Then it passes with: 24 | """ 25 | .. 26 | 42 27 | """ 28 | -------------------------------------------------------------------------------- /test/e2e/error.feature: -------------------------------------------------------------------------------- 1 | Feature: Error Display 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to log errors 5 | 6 | Scenario: Syntax Error in a test file 7 | Given a configuration with: 8 | """ 9 | files = ['error/test.js', 'error/under-test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | """ 16 | When I start Karma 17 | Then it fails with: 18 | """ 19 | SyntaxError: Unexpected token '}' 20 | """ 21 | 22 | Scenario: Not single-run Syntax Error in a test file 23 | Given a configuration with: 24 | """ 25 | files = ['error/test.js', 'error/under-test.js']; 26 | browsers = ['ChromeHeadlessNoSandbox']; 27 | plugins = [ 28 | 'karma-jasmine', 29 | 'karma-chrome-launcher' 30 | ]; 31 | singleRun = false; 32 | """ 33 | When I start a server in background 34 | And I wait until server output contains: 35 | """ 36 | Executed 2 of 2 (1 FAILED) 37 | """ 38 | And I run Karma 39 | Then it fails with like: 40 | """ 41 | SyntaxError: Unexpected token '}' 42 | """ 43 | 44 | Scenario: Missing module Error in a test file 45 | Given a configuration with: 46 | """ 47 | files = [{pattern: 'error/import-something-from-somewhere.js', type: 'module'}]; 48 | browsers = ['ChromeHeadlessNoSandbox']; 49 | plugins = [ 50 | 'karma-jasmine', 51 | 'karma-chrome-launcher' 52 | ]; 53 | """ 54 | When I start Karma 55 | Then it fails with: 56 | """ 57 | Uncaught Error loading error/import-something-from-somewhere.js 58 | """ 59 | -------------------------------------------------------------------------------- /test/e2e/headers.feature: -------------------------------------------------------------------------------- 1 | Feature: Custom Headers 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to Karma to send custom headers on files sent. 5 | 6 | Scenario: Simple file with headers 7 | Given a configuration with: 8 | """ 9 | files = ['headers/*.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | customHeaders = [{ 16 | match: 'foo.js', 17 | name: 'Custom-Header-Awesomeness', 18 | value: 'there.is.no.dana.only.zuul' 19 | }]; 20 | """ 21 | When I start Karma 22 | Then it passes with: 23 | """ 24 | . 25 | Chrome Headless 26 | """ 27 | -------------------------------------------------------------------------------- /test/e2e/helpful-logs.feature: -------------------------------------------------------------------------------- 1 | Feature: Helpful warning and errors 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to get messages which help me to fix problems 5 | 6 | Scenario: Karma fails to determine a file type from the file extension 7 | Given a configuration with: 8 | """ 9 | files = [ 'modules/**/*.mjs' ]; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | frameworks = ['mocha', 'chai']; 12 | plugins = [ 13 | 'karma-mocha', 14 | 'karma-chai', 15 | 'karma-chrome-launcher' 16 | ]; 17 | """ 18 | When I start Karma 19 | Then the stdout matches RegExp: 20 | """ 21 | WARN \[middleware:karma\]: Unable to determine file type from the file extension, defaulting to js. 22 | To silence the warning specify a valid type for .+modules/minus.mjs in the configuration file. 23 | See https://karma-runner.github.io/latest/config/files.html 24 | """ 25 | -------------------------------------------------------------------------------- /test/e2e/launcher-error.feature: -------------------------------------------------------------------------------- 1 | Feature: Launcher error 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to output stderr if a browser fails to connect. 5 | 6 | Scenario: Broken Browser 7 | Given a configuration with: 8 | """ 9 | files = ['launcher-error/specs.js']; 10 | browsers = [_resolve('launcher-error/fake-browser.sh')]; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-script-launcher' 14 | ]; 15 | """ 16 | When I start Karma 17 | Then it fails with like: 18 | """ 19 | Missing fake dependency 20 | """ 21 | -------------------------------------------------------------------------------- /test/e2e/load.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic Testrunner 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to terminate upon misconfiguration 5 | 6 | Scenario: Execute with missing browser 7 | Given a configuration with: 8 | """ 9 | files = ['basic/plus.js', 'basic/test.js']; 10 | browsers = ['NonExistingBrowser', 'ChromeHeadless']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | singleRun = false 16 | """ 17 | When I start Karma 18 | Then it fails with like: 19 | """ 20 | Cannot load browser "NonExistingBrowser": it is not registered! Perhaps you are missing some plugin\? 21 | """ 22 | And it fails with like: 23 | """ 24 | Found 1 load error 25 | """ 26 | 27 | Scenario: Execute with missing plugin 28 | Given a configuration with: 29 | """ 30 | files = ['basic/plus.js', 'basic/test.js']; 31 | browsers = ['ChromeHeadlessNoSandbox']; 32 | plugins = [ 33 | 'karma-totally-non-existing-plugin', 34 | 'karma-jasmine', 35 | 'karma-chrome-launcher' 36 | ]; 37 | singleRun = false 38 | """ 39 | When I start Karma 40 | Then it fails with like: 41 | """ 42 | Cannot find plugin "karma-totally-non-existing-plugin". 43 | [\s]+Did you forget to install it\? 44 | [\s]+npm install karma-totally-non-existing-plugin --save-dev 45 | """ 46 | And it fails with like: 47 | """ 48 | Found 1 load error 49 | """ 50 | 51 | Scenario: Execute with missing reporter 52 | Given a configuration with: 53 | """ 54 | files = ['basic/plus.js', 'basic/test.js']; 55 | browsers = ['ChromeHeadlessNoSandbox']; 56 | reporters = ['unreal-reporter'] 57 | plugins = [ 58 | 'karma-jasmine', 59 | 'karma-chrome-launcher' 60 | ]; 61 | singleRun = false 62 | """ 63 | When I start Karma 64 | Then it fails with like: 65 | """ 66 | Can not load reporter "unreal-reporter", it is not registered! 67 | [\s]+Perhaps you are missing some plugin\? 68 | """ 69 | And it fails with like: 70 | """ 71 | Found 1 load error 72 | """ 73 | 74 | Scenario: Execute with missing reporter, plugin and browser 75 | Given a configuration with: 76 | """ 77 | files = ['basic/plus.js', 'basic/test.js']; 78 | browsers = ['NonExistingBrowser', 'ChromeHeadless']; 79 | reporters = ['unreal-reporter'] 80 | plugins = [ 81 | 'karma-totally-non-existing-plugin', 82 | 'karma-jasmine', 83 | 'karma-chrome-launcher' 84 | ]; 85 | singleRun = false 86 | """ 87 | When I start Karma 88 | Then it fails with like: 89 | """ 90 | Can not load reporter "unreal-reporter", it is not registered! 91 | [\s]+Perhaps you are missing some plugin\? 92 | """ 93 | And it fails with like: 94 | """ 95 | Cannot find plugin "karma-totally-non-existing-plugin". 96 | [\s]+Did you forget to install it\? 97 | [\s]+npm install karma-totally-non-existing-plugin --save-dev 98 | """ 99 | And it fails with like: 100 | """ 101 | Found 2 load errors 102 | """ 103 | -------------------------------------------------------------------------------- /test/e2e/middleware.feature: -------------------------------------------------------------------------------- 1 | Feature: Middleware 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to use custom middleware with Karma. 5 | 6 | Scenario: Simple middleware 7 | Given a configuration with: 8 | """ 9 | files = ['middleware/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher', 14 | _resolve('middleware/middleware') 15 | ]; 16 | middleware = [ 17 | 'foo' 18 | ] 19 | """ 20 | When I start Karma 21 | Then it passes with: 22 | """ 23 | . 24 | Chrome Headless 25 | """ 26 | 27 | Scenario: Frameworks can add middleware 28 | Given a configuration with: 29 | """ 30 | files = ['middleware/test.js']; 31 | browsers = ['ChromeHeadlessNoSandbox']; 32 | plugins = [ 33 | 'karma-jasmine', 34 | 'karma-chrome-launcher', 35 | _resolve('middleware/middleware') 36 | ]; 37 | frameworks = ['jasmine', 'foo'] 38 | """ 39 | When I start Karma 40 | Then it passes with: 41 | """ 42 | . 43 | Chrome Headless 44 | """ 45 | -------------------------------------------------------------------------------- /test/e2e/mocharepoter.feature: -------------------------------------------------------------------------------- 1 | Feature: Mocha reporter 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to be able to use the mocha reporter. 5 | 6 | Scenario: Execute a test in ChromeHeadless with colors 7 | Given a configuration with: 8 | """ 9 | files = ['mocha/plus.js', 'mocha/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | frameworks = ['mocha', 'chai'] 12 | colors = true 13 | plugins = [ 14 | 'karma-jasmine', 15 | 'karma-chrome-launcher', 16 | 'karma-mocha-reporter', 17 | 'karma-mocha', 18 | 'karma-chai' 19 | ]; 20 | reporters = ['mocha']; 21 | """ 22 | When I start Karma 23 | Then it passes with like: 24 | """ 25 | 2 tests completed 26 | """ 27 | 28 | Scenario: Execute a test in ChromeHeadless with no-colors 29 | Given a configuration with: 30 | """ 31 | files = ['mocha/plus.js', 'mocha/test.js']; 32 | browsers = ['ChromeHeadlessNoSandbox']; 33 | frameworks = ['mocha', 'chai'] 34 | colors = false 35 | plugins = [ 36 | 'karma-jasmine', 37 | 'karma-chrome-launcher', 38 | 'karma-mocha-reporter', 39 | 'karma-mocha', 40 | 'karma-chai' 41 | ]; 42 | reporters = ['mocha']; 43 | """ 44 | When I start Karma 45 | Then it passes with like: 46 | """ 47 | ✔ 2 tests completed 48 | """ 49 | -------------------------------------------------------------------------------- /test/e2e/module-types.feature: -------------------------------------------------------------------------------- 1 | Feature: ES Modules 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to use different script types with Karma. 5 | 6 | Scenario: Globbing modules, with both .js and .mjs extensions 7 | Given a configuration with: 8 | """ 9 | files = [ 10 | { pattern: 'modules/**/*.js', type: 'module' }, 11 | { pattern: 'modules/**/*.mjs', type: 'module' }, 12 | ]; 13 | // Chrome fails on Travis, so we must use Firefox (which means we must 14 | // manually enable modules). 15 | customLaunchers = { 16 | FirefoxWithModules: { 17 | base: 'FirefoxHeadless', 18 | prefs: { 19 | 'dom.moduleScripts.enabled': true 20 | } 21 | } 22 | }; 23 | browsers = ['FirefoxWithModules']; 24 | frameworks = ['mocha', 'chai']; 25 | plugins = [ 26 | 'karma-mocha', 27 | 'karma-chai', 28 | 'karma-firefox-launcher' 29 | ]; 30 | """ 31 | When I start Karma 32 | Then it passes with like: 33 | """ 34 | Executed 4 of 4 SUCCESS 35 | """ 36 | -------------------------------------------------------------------------------- /test/e2e/pass-opts.feature: -------------------------------------------------------------------------------- 1 | Feature: Passing Options 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to be able to pass arguments from the config file to the browser. 5 | 6 | Scenario: Passing Options to run on the Command Line 7 | Given a configuration with: 8 | """ 9 | files = ['pass-opts/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | singleRun = false; 16 | """ 17 | When I start a server in background 18 | And I wait until server output contains: 19 | """ 20 | Executed 1 of 1 (1 FAILED) 21 | """ 22 | And I run Karma with additional arguments: "-- arg1 arg2" 23 | Then it passes with: 24 | """ 25 | . 26 | """ 27 | -------------------------------------------------------------------------------- /test/e2e/proxy.feature: -------------------------------------------------------------------------------- 1 | Feature: Proxying 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to Karma to proxy requests. 5 | 6 | Scenario: Simple file proxy 7 | Given a configuration with: 8 | """ 9 | files = ['proxy/test.js', 'proxy/foo.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | proxies = { 16 | '/foo.js': '/base/proxy/foo.js' 17 | } 18 | """ 19 | When I start Karma 20 | Then it passes with: 21 | """ 22 | . 23 | Chrome Headless 24 | """ 25 | 26 | Scenario: Added by a framework 27 | Given a configuration with: 28 | """ 29 | files = ['proxy/test.js', 'proxy/foo.js']; 30 | browsers = ['ChromeHeadlessNoSandbox']; 31 | plugins = [ 32 | 'karma-jasmine', 33 | 'karma-chrome-launcher', 34 | _resolve('proxy/plugin') 35 | ]; 36 | frameworks = ['jasmine', 'foo'] 37 | """ 38 | When I start Karma 39 | Then it passes with: 40 | """ 41 | . 42 | Chrome Headless 43 | """ 44 | 45 | Scenario: URLRoot 46 | Given a configuration with: 47 | """ 48 | files = ['proxy/test.js', 'proxy/foo.js']; 49 | browsers = ['ChromeHeadlessNoSandbox']; 50 | plugins = [ 51 | 'karma-jasmine', 52 | 'karma-chrome-launcher' 53 | ]; 54 | urlRoot = '/__karma__/'; 55 | proxies = { 56 | '/foo.js': '/base/proxy/foo.js' 57 | } 58 | """ 59 | When I start Karma 60 | Then it passes with: 61 | """ 62 | . 63 | Chrome Headless 64 | """ 65 | -------------------------------------------------------------------------------- /test/e2e/reconnecting.feature: -------------------------------------------------------------------------------- 1 | Feature: Passing Options 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to the browser to reconnect to Karma when it gets disconnected. 5 | 6 | Scenario: Manual disconnect from the browser 7 | Given a configuration with: 8 | """ 9 | files = ['reconnecting/test.js', 'reconnecting/plus.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | client = { 16 | jasmine: { 17 | random: false 18 | } 19 | }; 20 | """ 21 | When I start Karma 22 | Then it passes with: 23 | """ 24 | LOG: '============== START TEST ==============' 25 | ..... 26 | Chrome Headless 27 | """ 28 | -------------------------------------------------------------------------------- /test/e2e/reporting.feature: -------------------------------------------------------------------------------- 1 | Feature: Results reporting 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to Karma to report test results in the same order as they are executed. 5 | 6 | Scenario: Results appear as tests are executed 7 | Given a configuration with: 8 | """ 9 | files = ['reporting/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-mocha', 13 | 'karma-mocha-reporter', 14 | 'karma-chrome-launcher' 15 | ]; 16 | frameworks = ['mocha'] 17 | reporters = ['mocha'] 18 | """ 19 | When I start Karma 20 | Then it passes with like: 21 | """ 22 | START: 23 | Reporting order 24 | ✔ sync test 25 | ✔ async test 26 | """ 27 | -------------------------------------------------------------------------------- /test/e2e/restart-on-change.feature: -------------------------------------------------------------------------------- 1 | Feature: Restart on file change 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to re-run tests whenever file changes. 5 | 6 | Scenario: Re-run tests when file changes 7 | Given a configuration with: 8 | """ 9 | files = ['basic/plus.js', 'basic/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | restartOnFileChange = true; 16 | singleRun = false; 17 | """ 18 | When I start a server in background 19 | And I wait until server output contains: 20 | """ 21 | .. 22 | Chrome Headless 23 | """ 24 | When I touch file: "basic/test.js" 25 | Then the background stdout matches RegExp: 26 | """ 27 | Executed 2 of 2 SUCCESS[\s\S]+Executed 2 of 2 SUCCESS 28 | """ 29 | -------------------------------------------------------------------------------- /test/e2e/runInParent.feature: -------------------------------------------------------------------------------- 1 | Feature: runInParent option 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to run without iframe or opening new window 5 | 6 | Scenario: Execute a test in ChromeHeadless 7 | Given a configuration with: 8 | """ 9 | files = ['basic/plus.js', 'basic/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | client = { 16 | useIframe: false, 17 | runInParent: true 18 | }; 19 | """ 20 | When I start Karma 21 | Then it passes with: 22 | """ 23 | .. 24 | Chrome Headless 25 | """ 26 | 27 | Scenario: Execute a test in Firefox 28 | Given a configuration with: 29 | """ 30 | files = ['basic/plus.js', 'basic/test.js'] 31 | browsers = ['FirefoxHeadless'] 32 | plugins = [ 33 | 'karma-jasmine', 34 | 'karma-firefox-launcher' 35 | ] 36 | client = { 37 | useIframe: false, 38 | runInParent: true 39 | } 40 | """ 41 | When I start Karma 42 | Then it passes with: 43 | """ 44 | .. 45 | Firefox 46 | """ 47 | -------------------------------------------------------------------------------- /test/e2e/step_definitions/hooks.js: -------------------------------------------------------------------------------- 1 | const { After, Before } = require('cucumber') 2 | 3 | Before(function () { 4 | this.ensureSandbox() 5 | }) 6 | 7 | After(async function () { 8 | await this.proxy.stopIfRunning() 9 | await this.stopBackgroundProcessIfRunning() 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/step_definitions/utils.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util') 2 | 3 | const sleep = promisify(setTimeout) 4 | 5 | module.exports.waitForCondition = async (evaluateCondition, timeout = 1000, customError = null) => { 6 | let remainingTime = timeout 7 | while (!evaluateCondition()) { 8 | if (remainingTime > 0) { 9 | await sleep(50) 10 | remainingTime -= 50 11 | } else { 12 | if (customError != null) { 13 | throw customError() 14 | } else { 15 | throw new Error(`Condition not fulfilled, waited ${timeout}ms`) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/e2e/stop.feature: -------------------------------------------------------------------------------- 1 | Feature: Stop karma 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to be able to stop Karma. 5 | 6 | Scenario: A server can't be stopped if it isn't running 7 | Given a default configuration 8 | When I stop Karma 9 | Then it fails with like: 10 | """ 11 | ERROR \[stopper\]: There is no server listening on port [0-9]+ 12 | """ 13 | 14 | Scenario: A server can be stopped 15 | Given a configuration with: 16 | """ 17 | files = ['basic/plus.js', 'basic/test.js']; 18 | browsers = ['ChromeHeadlessNoSandbox']; 19 | plugins = [ 20 | 'karma-jasmine', 21 | 'karma-chrome-launcher' 22 | ]; 23 | singleRun = false; 24 | """ 25 | When I start a server in background 26 | And I stop Karma 27 | Then The server is dead with exit code 0 28 | 29 | Scenario: A server can be stopped and give informative output 30 | Given a configuration with: 31 | """ 32 | files = ['basic/plus.js', 'basic/test.js']; 33 | browsers = ['ChromeHeadlessNoSandbox']; 34 | plugins = [ 35 | 'karma-jasmine', 36 | 'karma-chrome-launcher' 37 | ]; 38 | singleRun = false; 39 | """ 40 | When I start a server in background 41 | And I stop Karma with additional arguments: "--log-level info" 42 | Then it passes with like: 43 | """ 44 | Server stopped. 45 | """ 46 | 47 | 48 | Scenario: A server can be stopped programmatically 49 | Given a configuration with: 50 | """ 51 | files = ['basic/plus.js', 'basic/test.js']; 52 | browsers = ['ChromeHeadlessNoSandbox']; 53 | plugins = [ 54 | 'karma-jasmine', 55 | 'karma-chrome-launcher' 56 | ]; 57 | singleRun = false; 58 | logLevel = 'error'; 59 | """ 60 | When I start a server in background 61 | And I stop a server programmatically 62 | Then The server is dead with exit code 0 63 | And The stopper is dead with exit code 0 64 | -------------------------------------------------------------------------------- /test/e2e/support/basic/plus.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Some code under test 3 | function plus (a, b) { 4 | return a + b 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/support/basic/test.js: -------------------------------------------------------------------------------- 1 | /* globals plus */ 2 | describe('plus', function () { 3 | it('should pass', function () { 4 | expect(true).toBe(true) 5 | }) 6 | 7 | it('should work', function () { 8 | expect(plus(1, 2)).toBe(3) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/support/behind-proxy/plus.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Some code under test 3 | function plus (a, b) { 4 | return a + b 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/support/behind-proxy/test.js: -------------------------------------------------------------------------------- 1 | /* globals plus */ 2 | describe('plus', function () { 3 | it('should pass', function () { 4 | expect(true).toBe(true) 5 | }) 6 | 7 | it('should work', function () { 8 | expect(plus(1, 2)).toBe(3) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/support/browser-console/log.js: -------------------------------------------------------------------------------- 1 | 2 | console.log('foo') 3 | console.debug('bar') 4 | console.info('baz') 5 | console.warn('foobar') 6 | console.error('barbaz') 7 | -------------------------------------------------------------------------------- /test/e2e/support/browser-console/test.js: -------------------------------------------------------------------------------- 1 | describe('Truism', function () { 2 | it('should pass', function () { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /test/e2e/support/context/context2.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <!-- 3 | This is the execution context. 4 | Loaded within the iframe. 5 | Reloaded before every execution run. 6 | --> 7 | <html> 8 | <head> 9 | <title></title> 10 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 11 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> 12 | </head> 13 | <body> 14 | <!-- The scripts need to be in the body DOM element, as some test running frameworks need the body 15 | to have already been created so they can insert their magic into it. For example, if loaded 16 | before body, Angular Scenario test framework fails to find the body and crashes and burns in 17 | an epic manner. --> 18 | <div id="custom-context"></div> 19 | <script src="context.js"></script> 20 | <script type="text/javascript"> 21 | // Configure our Karma and set up bindings 22 | %CLIENT_CONFIG% 23 | window.__karma__.setupContext(window); 24 | 25 | // All served files with the latest timestamps 26 | %MAPPINGS% 27 | </script> 28 | <!-- Dynamically replaced with <script> tags --> 29 | %SCRIPTS% 30 | <script type="text/javascript"> 31 | window.__karma__.loaded(); 32 | </script> 33 | </body> 34 | </html> 35 | -------------------------------------------------------------------------------- /test/e2e/support/context/test.js: -------------------------------------------------------------------------------- 1 | describe('custom context file', function () { 2 | it('should be able to find custom DOM elements', function () { 3 | expect(document.querySelector('#custom-context') == null).toBe(false) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /test/e2e/support/env.js: -------------------------------------------------------------------------------- 1 | const { setDefaultTimeout } = require('cucumber') 2 | 3 | setDefaultTimeout(60 * 1000) 4 | -------------------------------------------------------------------------------- /test/e2e/support/error/import-something-from-somewhere.js: -------------------------------------------------------------------------------- 1 | import { something } from './somewhere.js' 2 | console.log(something) 3 | -------------------------------------------------------------------------------- /test/e2e/support/error/test.js: -------------------------------------------------------------------------------- 1 | /* global plus */ 2 | describe('plus', function () { 3 | it('should pass', function () { 4 | expect(true).toBe(true) 5 | }) 6 | 7 | it('should work', function () { 8 | expect(plus(1, 2)).toBe(3) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/support/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 | -------------------------------------------------------------------------------- /test/e2e/support/files/log_foo.js: -------------------------------------------------------------------------------- 1 | 2 | console.log('foo') 3 | -------------------------------------------------------------------------------- /test/e2e/support/files/test.js: -------------------------------------------------------------------------------- 1 | describe('plus', function () { 2 | it('should pass', function () { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /test/e2e/support/headers/foo.js: -------------------------------------------------------------------------------- 1 | '/base/headers/foo.js source' 2 | -------------------------------------------------------------------------------- /test/e2e/support/headers/test.js: -------------------------------------------------------------------------------- 1 | function httpGet (url) { 2 | const xmlHttp = new XMLHttpRequest() 3 | 4 | xmlHttp.open('GET', url, false) 5 | xmlHttp.send(null) 6 | 7 | return xmlHttp 8 | } 9 | 10 | describe('setting custom headers', function () { 11 | it('should get custom headers', function () { 12 | expect(httpGet('/base/headers/foo.js').getResponseHeader('Custom-Header-Awesomeness')).toBe('there.is.no.dana.only.zuul') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/support/launcher-error/fake-browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Missing fake dependency" 1>&2 4 | exit 1 5 | -------------------------------------------------------------------------------- /test/e2e/support/launcher-error/specs.js: -------------------------------------------------------------------------------- 1 | describe('something', function () { 2 | it('should never happen anyway', function () { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /test/e2e/support/middleware/middleware.js: -------------------------------------------------------------------------------- 1 | function middleware (request, response, next) { 2 | if (/\/foo\.js/.test(request.normalizedUrl)) { 3 | response.setHeader('Content-Type', 'text/plain') 4 | response.writeHead(200) 5 | response.end('this is the middleware response') 6 | return 7 | } 8 | next() 9 | } 10 | 11 | function framework (config) { 12 | config.middleware = config.middleware || [] 13 | config.middleware.push('foo') 14 | } 15 | 16 | framework.$inject = ['config'] 17 | 18 | module.exports = { 19 | 'framework:foo': ['factory', framework], 20 | 'middleware:foo': ['value', middleware] 21 | } 22 | -------------------------------------------------------------------------------- /test/e2e/support/middleware/test.js: -------------------------------------------------------------------------------- 1 | function httpGet (url) { 2 | const xmlHttp = new XMLHttpRequest() 3 | 4 | xmlHttp.open('GET', url, false) 5 | xmlHttp.send(null) 6 | 7 | return xmlHttp.responseText 8 | } 9 | 10 | describe('foo', function () { 11 | it('should should serve /foo.js', function () { 12 | expect(httpGet('/foo.js')).toBe('this is the middleware response') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/support/mocha/plus.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Some code under test 3 | function plus (a, b) { 4 | return a + b 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/support/mocha/test.js: -------------------------------------------------------------------------------- 1 | /* globals plus */ 2 | describe('plus', function () { 3 | it('should pass', function () { 4 | expect(true).to.be.true 5 | }) 6 | 7 | it('should work', function () { 8 | expect(plus(1, 2)).to.equal(3) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/e2e/support/modules/__tests__/minus.test.mjs: -------------------------------------------------------------------------------- 1 | import { minus } from '../minus.mjs' 2 | 3 | describe('minus', function () { 4 | it('should pass', function () { 5 | expect(true).to.be.true 6 | }) 7 | 8 | it('should work', function () { 9 | expect(minus(3, 2)).to.equal(1) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/support/modules/__tests__/plus.test.js: -------------------------------------------------------------------------------- 1 | import { plus } from '../plus.js' 2 | 3 | describe('plus', function () { 4 | it('should pass', function () { 5 | expect(true).to.be.true 6 | }) 7 | 8 | it('should work', function () { 9 | expect(plus(1, 2)).to.equal(3) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/e2e/support/modules/minus.mjs: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | export function minus (a, b) { 3 | return a - b 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/support/modules/plus.js: -------------------------------------------------------------------------------- 1 | // Some code under test 2 | export function plus (a, b) { 3 | return a + b 4 | } 5 | -------------------------------------------------------------------------------- /test/e2e/support/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']) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /test/e2e/support/proxy.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const httpProxy = require('http-proxy') 3 | const { promisify } = require('util') 4 | 5 | module.exports = class Proxy { 6 | constructor () { 7 | this.running = false 8 | this.proxyPathRegExp = null 9 | 10 | this.proxy = httpProxy.createProxyServer({ 11 | target: 'http://127.0.0.1:9876' 12 | }) 13 | 14 | this.proxy.on('error', (err) => { 15 | console.log('support/proxy onerror', err) 16 | }) 17 | 18 | this.server = http.createServer((req, res) => { 19 | const url = req.url 20 | const match = url.match(this.proxyPathRegExp) 21 | if (match) { 22 | req.url = '/' + match[1] 23 | this.proxy.web(req, res) 24 | } else { 25 | res.statusCode = 404 26 | res.statusMessage = 'Not found' 27 | res.end() 28 | } 29 | }) 30 | 31 | this.server.on('clientError', (err) => { 32 | console.log('support/proxy clientError', err) 33 | }) 34 | } 35 | 36 | async start (port, proxyPath) { 37 | this.proxyPathRegExp = new RegExp('^' + proxyPath + '(.*)') 38 | await promisify(this.server.listen.bind(this.server))(port) 39 | this.running = true 40 | } 41 | 42 | async stopIfRunning () { 43 | if (this.running) { 44 | this.running = false 45 | await promisify(this.server.close.bind(this.server))() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/e2e/support/proxy/.tern-port: -------------------------------------------------------------------------------- 1 | 63683 -------------------------------------------------------------------------------- /test/e2e/support/proxy/foo.js: -------------------------------------------------------------------------------- 1 | '/base/proxy/foo.js source' 2 | -------------------------------------------------------------------------------- /test/e2e/support/proxy/plugin.js: -------------------------------------------------------------------------------- 1 | function framework (config) { 2 | config.proxies = { 3 | '/foo.js': '/base/proxy/foo.js' 4 | } 5 | } 6 | 7 | framework.$inject = ['config'] 8 | 9 | module.exports = { 10 | 'framework:foo': ['factory', framework] 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/support/proxy/test.js: -------------------------------------------------------------------------------- 1 | function httpGet (url) { 2 | const xmlHttp = new XMLHttpRequest() 3 | 4 | xmlHttp.open('GET', url, false) 5 | xmlHttp.send(null) 6 | 7 | return xmlHttp.responseText 8 | } 9 | 10 | describe('foo', function () { 11 | it('should should serve /foo.js', function () { 12 | expect(httpGet('/foo.js')).toBe("'/base/proxy/foo.js source'\n") 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/e2e/support/reconnecting/plus.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Some code under test 3 | function plus (a, b) { 4 | return a + b 5 | } 6 | -------------------------------------------------------------------------------- /test/e2e/support/reconnecting/test.js: -------------------------------------------------------------------------------- 1 | /* globals plus */ 2 | describe('plus', function () { 3 | // super hacky way to get the actual socket to manipulate it... 4 | function socket () { 5 | return window.parent.karma.socket 6 | } 7 | 8 | it('should pass', function () { 9 | // In flaky fails we probably get two starts. 10 | console.log('============== START TEST ==============') 11 | expect(1).toBe(1) 12 | }) 13 | 14 | it('should disconnect', function (done) { 15 | expect(2).toBe(2) 16 | setTimeout(() => { 17 | socket().disconnect() 18 | done() 19 | }, 500) 20 | }) 21 | 22 | it('should work', function () { 23 | expect(plus(1, 2)).toBe(3) 24 | }) 25 | 26 | it('should re-connect', function () { 27 | expect(4).toBe(4) 28 | socket().connect() 29 | }) 30 | 31 | it('should work', function () { 32 | expect(plus(3, 2)).toBe(5) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/e2e/support/reporting/test.js: -------------------------------------------------------------------------------- 1 | describe('Reporting order', () => { 2 | it('sync test', () => { 3 | // pass 4 | }) 5 | 6 | it('async test', (done) => { 7 | setTimeout(done, 200) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/e2e/support/tag/tag.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | function isFirefoxBefore59 () { 3 | return typeof InstallTrigger !== 'undefined' && parseFloat(navigator.userAgent.match(/\d+\.\d+$/)) < 59 4 | } 5 | 6 | function containsJsTag () { 7 | const scripts = document.getElementsByTagName('script') 8 | for (let i = 0; i < scripts.length; i++) { 9 | if (scripts[i].type.indexOf(';version=') > -1) { 10 | return true 11 | } 12 | } 13 | return false 14 | } 15 | -------------------------------------------------------------------------------- /test/e2e/support/tag/test-with-version.js: -------------------------------------------------------------------------------- 1 | /* globals containsJsTag, isFirefoxBefore59 */ 2 | describe('JavaScript version tag', function () { 3 | it('should add the version tag, if Firefox is used', function () { 4 | expect(containsJsTag()).toBe(isFirefoxBefore59()) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /test/e2e/support/tag/test-without-version.js: -------------------------------------------------------------------------------- 1 | /* globals containsJsTag */ 2 | describe('JavaScript version tag', function () { 3 | it('should not add the version tag for every browser', function () { 4 | expect(containsJsTag()).toBe(false) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /test/e2e/support/timeout/fake-browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read 4 | -------------------------------------------------------------------------------- /test/e2e/support/timeout/specs.js: -------------------------------------------------------------------------------- 1 | describe('something', function () { 2 | it('should never happen anyway', function () { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /test/e2e/tag.feature: -------------------------------------------------------------------------------- 1 | Feature: JavaScript Tag 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want to add a JavaScript version tag in Firefox only. 5 | 6 | Scenario: Execute a test in Firefox with version, with JavaScript tag 7 | Given a configuration with: 8 | """ 9 | files = ['tag/tag.js', 'tag/test-with-version.js']; 10 | browsers = ['FirefoxHeadless'] 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-firefox-launcher' 14 | ] 15 | """ 16 | When I start Karma 17 | Then it passes with like: 18 | """ 19 | . 20 | Firefox 21 | """ 22 | 23 | Scenario: Execute a test in ChromeHeadless with version, without JavaScript tag 24 | Given a configuration with: 25 | """ 26 | files = ['tag/tag.js', 'tag/test-with-version.js']; 27 | browsers = ['ChromeHeadlessNoSandbox']; 28 | plugins = [ 29 | 'karma-jasmine', 30 | 'karma-chrome-launcher' 31 | ]; 32 | """ 33 | When I start Karma 34 | Then it passes with: 35 | """ 36 | . 37 | Chrome 38 | """ 39 | 40 | Scenario: Execute a test in Firefox without version, without JavaScript tag 41 | Given a configuration with: 42 | """ 43 | files = ['tag/tag.js', 'tag/test-without-version.js']; 44 | browsers = ['FirefoxHeadless'] 45 | plugins = [ 46 | 'karma-jasmine', 47 | 'karma-firefox-launcher' 48 | ] 49 | """ 50 | When I start Karma 51 | Then it passes with: 52 | """ 53 | . 54 | Firefox 55 | """ 56 | 57 | Scenario: Execute a test in ChromeHeadless without version, without JavaScript tag 58 | Given a configuration with: 59 | """ 60 | files = ['tag/tag.js', 'tag/test-without-version.js']; 61 | browsers = ['ChromeHeadlessNoSandbox']; 62 | plugins = [ 63 | 'karma-jasmine', 64 | 'karma-chrome-launcher' 65 | ]; 66 | """ 67 | When I start Karma 68 | Then it passes with: 69 | """ 70 | . 71 | Chrome 72 | """ 73 | -------------------------------------------------------------------------------- /test/e2e/timeout.feature: -------------------------------------------------------------------------------- 1 | Feature: Timeout 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to timeout if a browser fails to connect. 5 | 6 | Scenario: Broken Browser 7 | Given a configuration with: 8 | """ 9 | files = ['timeout/specs.js']; 10 | browsers = [_resolve('timeout/fake-browser.sh')]; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-script-launcher' 14 | ]; 15 | captureTimeout = 100 16 | """ 17 | When I start Karma 18 | Then it fails with like: 19 | """ 20 | has not captured in 100 ms 21 | """ 22 | -------------------------------------------------------------------------------- /test/e2e/upstream-proxy.feature: -------------------------------------------------------------------------------- 1 | Feature: UpstreamProxy 2 | In order to use Karma 3 | As a person who wants to write great tests 4 | I want Karma to work when it is behind a proxy that prepends to the base path. 5 | 6 | Scenario: UpstreamProxy 7 | Given a configuration with: 8 | """ 9 | files = ['behind-proxy/plus.js', 'behind-proxy/test.js']; 10 | browsers = ['ChromeHeadlessNoSandbox']; 11 | plugins = [ 12 | 'karma-jasmine', 13 | 'karma-chrome-launcher' 14 | ]; 15 | urlRoot = '/__karma__/'; 16 | upstreamProxy = { 17 | path: '/__proxy__/' 18 | }; 19 | """ 20 | And a proxy on port 9875 that prepends '/__proxy__/' to the base path 21 | When I start Karma with additional arguments: "--log-level debug" 22 | Then it passes with regexp: 23 | """ 24 | Chrome Headless.*Executed.*SUCCESS 25 | """ 26 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter dot 2 | --ui bdd 3 | test/unit/mocha-globals.js 4 | -------------------------------------------------------------------------------- /test/unit/browser_result.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe('BrowserResult', () => { 4 | const BrowserResult = require('../../lib/browser_result') 5 | let result = null 6 | 7 | const successResultFromBrowser = { 8 | success: true, 9 | skipped: false, 10 | time: 100 11 | } 12 | 13 | const failedResultFromBrowser = { 14 | success: false, 15 | skipped: false, 16 | time: 200 17 | } 18 | 19 | const skippedResultFromBrowser = { 20 | success: false, 21 | skipped: true, 22 | time: 0 23 | } 24 | 25 | beforeEach(() => { 26 | sinon.stub(Date, 'now') 27 | Date.now.returns(123) 28 | result = new BrowserResult() 29 | }) 30 | 31 | afterEach(() => { 32 | Date.now.restore() 33 | }) 34 | 35 | it('should compute totalTime', () => { 36 | Date.now.returns(223) 37 | result.totalTimeEnd() 38 | expect(result.totalTime).to.equal(223 - 123) 39 | }) 40 | 41 | it('should sum success/failed/skipped', () => { 42 | result.add(successResultFromBrowser) 43 | expect(result.success).to.equal(1) 44 | expect(result.failed).to.equal(0) 45 | expect(result.skipped).to.equal(0) 46 | 47 | result.add(failedResultFromBrowser) 48 | expect(result.success).to.equal(1) 49 | expect(result.failed).to.equal(1) 50 | expect(result.skipped).to.equal(0) 51 | 52 | result.add(successResultFromBrowser) 53 | expect(result.success).to.equal(2) 54 | expect(result.failed).to.equal(1) 55 | expect(result.skipped).to.equal(0) 56 | 57 | result.add(skippedResultFromBrowser) 58 | expect(result.success).to.equal(2) 59 | expect(result.failed).to.equal(1) 60 | expect(result.skipped).to.equal(1) 61 | }) 62 | 63 | it('should sum net time of all results', () => { 64 | result.add(successResultFromBrowser) 65 | result.add(failedResultFromBrowser) 66 | expect(result.netTime).to.equal(300) 67 | 68 | result.add(successResultFromBrowser) 69 | expect(result.netTime).to.equal(400) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/unit/certificates/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICAzCCAWwCCQDlm49KXF45gzANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJB 3 | VTETMBEGA1UECBMKU29tZS1TdGF0ZTEQMA4GA1UEChMHR3J1bnRKUzEQMA4GA1UE 4 | AxMHMC4wLjAuMDAeFw0xNDAyMTkyMzE1NDRaFw0xNTAyMTkyMzE1NDRaMEYxCzAJ 5 | BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRAwDgYDVQQKEwdHcnVudEpT 6 | MRAwDgYDVQQDEwcwLjAuMC4wMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCm 7 | ipCqKyQ6aJJiVMvXZVoTw9sEC5dKFA35n15r9fG565/Zj8LVg/kgt79am1bnF+/H 8 | F880f8kfDsgEaAC1qzo8XU8yqt+UoFOB2Ncw76g6B6ZiuC2R1uHyD/46sYtMejy3 9 | n8EcTk9jNmNlglF6Ig6/hWcz+0XH6QjJT0lAM06tswIDAQABMA0GCSqGSIb3DQEB 10 | BQUAA4GBADnTBlN7+Aa8zj2zsUBSUv9w7iYut3ZDvrEY+IJt8EurwA6+Q7rQqVsY 11 | an5ztiEESriWvqNIfvWb+Yekhv9sISJFMfApVbimmT6QseQcFEIlRNW5cfukHQVH 12 | 9dBI7upQO2vN7N9ABo4a3aBANMBxIvCnE+adiqNOTJF/8qkiAFY9 13 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/unit/certificates/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQCmipCqKyQ6aJJiVMvXZVoTw9sEC5dKFA35n15r9fG565/Zj8LV 3 | g/kgt79am1bnF+/HF880f8kfDsgEaAC1qzo8XU8yqt+UoFOB2Ncw76g6B6ZiuC2R 4 | 1uHyD/46sYtMejy3n8EcTk9jNmNlglF6Ig6/hWcz+0XH6QjJT0lAM06tswIDAQAB 5 | AoGATqG34hCSf11mWDUPNXjuCcz8eLF8Ugab/pMngrPR2OWOSKue4y73jmITYBVd 6 | 96iOlqMAOxpmfFp/R81PIHdi++Bax1NfSBT8tK0U7HHzkbHEXyvHiBSug78Y14h8 7 | Y/NMZXEvVapY7lapr5ZgOSf2rcKOlceMRsoohl6bGc+55BECQQDPZTw5WxDDe7/W 8 | oYzHy7abLw+A92cP8A6qlwXBik9ko6jtYXvoI454OIr6RsHoFPU9bUkx5G1fvOUZ 9 | J3sxfxMZAkEAzZJEwcvmxHizX/2NZZ8LvVyWGpao07bBcAEvDXDZFOZqKUujukOe 10 | iilQD6JZDJTmW9RJmOgdQKeL9ZaTlX3MqwJASMJrbnPUXcB8fQAQM8f0OF06QzSI 11 | o77EZnS1QEEVuWjxStZ4ceiHgwXTPBq2zIUNxI8irq5E8OGEPl7riWHbgQJARzqL 12 | QGsaRrFb1cLRH4kAVFikWgoh7VnBpMGEQC/9x9QerLhcvsl3QYAXEZO7LzTYrLDd 13 | 33Ft0V08jZfjA0VXiQJAOwX6glfDKf79AK1sifFQc/v0Yu87LIOAwp0zLlsnO0Q9 14 | xQV3TdjlNQebfTG+Uw1tmbcCb2wcGFfD199IHpAzIA== 15 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /test/unit/completion.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const c = require('../../lib/completion') 4 | 5 | describe('completion', () => { 6 | let completion 7 | 8 | function mockEnv (line) { 9 | const words = line.split(' ') 10 | 11 | return { 12 | words: words, 13 | count: words.length, 14 | last: words[words.length - 1], 15 | prev: words[words.length - 2] 16 | } 17 | } 18 | 19 | beforeEach(() => { 20 | sinon.stub(console, 'log').callsFake((msg) => completion.push(msg)) 21 | completion = [] 22 | }) 23 | 24 | describe('opositeWord', () => { 25 | it('should handle --no-x args', () => { 26 | expect(c.opositeWord('--no-single-run')).to.equal('--single-run') 27 | }) 28 | 29 | it('should handle --x args', () => { 30 | expect(c.opositeWord('--browsers')).to.equal('--no-browsers') 31 | }) 32 | 33 | it('should ignore args without --', () => { 34 | expect(c.opositeWord('start')).to.equal(null) 35 | }) 36 | }) 37 | 38 | describe('sendCompletion', () => { 39 | it('should filter only words matching last typed partial', () => { 40 | c.sendCompletion(['start', 'init', 'run'], mockEnv('in')) 41 | expect(completion).to.deep.equal(['init']) 42 | }) 43 | 44 | it('should filter out already used words/args', () => { 45 | c.sendCompletion(['--single-run', '--port', '--xxx'], mockEnv('start --single-run ')) 46 | expect(completion).to.deep.equal(['--port', '--xxx']) 47 | }) 48 | 49 | it('should filter out already used oposite words', () => { 50 | c.sendCompletion(['--auto-watch', '--port'], mockEnv('start --no-auto-watch ')) 51 | expect(completion).to.deep.equal(['--port']) 52 | }) 53 | }) 54 | 55 | describe('complete', () => { 56 | it('should complete the basic commands', () => { 57 | c.complete(mockEnv('')) 58 | expect(completion).to.deep.equal(['start', 'init', 'run']) 59 | 60 | completion.length = 0 // reset 61 | c.complete(mockEnv('s')) 62 | expect(completion).to.deep.equal(['start']) 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/unit/emitter_wrapper.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | const EmitterWrapper = require('../../lib/emitter_wrapper') 5 | 6 | describe('emitter_wrapper', () => { 7 | let emitter 8 | let wrapped 9 | 10 | beforeEach(() => { 11 | emitter = new EventEmitter() 12 | emitter.aMethod = (e) => true 13 | emitter.on('anEvent', emitter.aMethod) 14 | wrapped = new EmitterWrapper(emitter) 15 | }) 16 | 17 | describe('addListener', () => { 18 | const aListener = (e) => true 19 | 20 | it('should add a listener to the wrapped emitter', () => { 21 | wrapped.addListener('anEvent', aListener) 22 | expect(emitter.listeners('anEvent')).to.contain(aListener) 23 | }) 24 | 25 | it('returns the wrapped emitter', () => { 26 | expect(wrapped.addListener('anEvent', aListener)).to.equal(wrapped) 27 | }) 28 | }) 29 | 30 | describe('removeAllListeners', () => { 31 | const aListener = (e) => true 32 | 33 | beforeEach(() => { 34 | wrapped.addListener('anEvent', aListener) 35 | }) 36 | 37 | it('should remove listeners that were attached via the wrapper', () => { 38 | wrapped.removeAllListeners() 39 | expect(emitter.listeners('anEvent')).not.to.contain(aListener) 40 | }) 41 | 42 | it('should not remove listeners that were attached to the original emitter', () => { 43 | wrapped.removeAllListeners() 44 | expect(emitter.listeners('anEvent')).to.contain(emitter.aMethod) 45 | }) 46 | 47 | it('should remove only matching listeners when called with an event name', () => { 48 | const anotherListener = (e) => true 49 | wrapped.addListener('anotherEvent', anotherListener) 50 | wrapped.removeAllListeners('anEvent') 51 | expect(emitter.listeners('anEvent')).not.to.contain(aListener) 52 | expect(emitter.listeners('anotherEvent')).to.contain(anotherListener) 53 | }) 54 | 55 | it('returns the wrapped emitter', () => { 56 | expect(wrapped.addListener('anEvent', aListener)).to.equal(wrapped) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/unit/executor.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Browser = require('../../lib/browser') 4 | const BrowserCollection = require('../../lib/browser_collection') 5 | const EventEmitter = require('../../lib/events').EventEmitter 6 | const Executor = require('../../lib/executor') 7 | 8 | const log = require('../../lib/logger').create() 9 | 10 | describe('executor', () => { 11 | let emitter 12 | let capturedBrowsers 13 | let config 14 | let spy 15 | let executor 16 | 17 | beforeEach(() => { 18 | config = { client: {} } 19 | emitter = new EventEmitter() 20 | capturedBrowsers = new BrowserCollection(emitter) 21 | capturedBrowsers.add(new Browser()) 22 | executor = new Executor(capturedBrowsers, config, emitter) 23 | executor.socketIoSockets = new EventEmitter() 24 | 25 | spy = { 26 | onRunStart: sinon.stub(), 27 | onSocketsExecute: sinon.stub(), 28 | onRunComplete: sinon.stub() 29 | } 30 | sinon.stub(log, 'warn') 31 | 32 | emitter.on('run_start', spy.onRunStart) 33 | emitter.on('run_complete', spy.onRunComplete) 34 | executor.socketIoSockets.on('execute', spy.onSocketsExecute) 35 | }) 36 | 37 | describe('schedule', () => { 38 | it('should start the run and pass client config', () => { 39 | capturedBrowsers.areAllReady = () => true 40 | 41 | executor.schedule() 42 | expect(spy.onRunStart).to.have.been.called 43 | expect(spy.onSocketsExecute).to.have.been.calledWith(config.client) 44 | }) 45 | 46 | it('should wait for all browsers to finish', () => { 47 | capturedBrowsers.areAllReady = () => false 48 | 49 | // they are not ready yet 50 | executor.schedule() 51 | expect(spy.onRunStart).not.to.have.been.called 52 | expect(spy.onSocketsExecute).not.to.have.been.called 53 | 54 | capturedBrowsers.areAllReady = () => true 55 | emitter.emit('run_complete') 56 | expect(spy.onRunStart).to.have.been.called 57 | expect(spy.onSocketsExecute).to.have.been.called 58 | }) 59 | }) 60 | 61 | describe('scheduleError', () => { 62 | it('should return `true` if scheduled synchronously', () => { 63 | const result = executor.scheduleError('expected error') 64 | expect(result).to.be.true 65 | }) 66 | 67 | it('should emit both "run_start" and "run_complete"', () => { 68 | executor.scheduleError('expected error') 69 | expect(spy.onRunStart).to.have.been.called 70 | expect(spy.onRunComplete).to.have.been.called 71 | expect(spy.onRunStart).to.have.been.calledBefore(spy.onRunComplete) 72 | }) 73 | 74 | it('should report the error', () => { 75 | const expectedError = 'expected error' 76 | executor.scheduleError(expectedError) 77 | expect(spy.onRunComplete).to.have.been.calledWith([], { 78 | success: 0, 79 | failed: 0, 80 | skipped: 0, 81 | error: expectedError, 82 | exitCode: 1 83 | }) 84 | }) 85 | 86 | it('should wait for scheduled runs to end before reporting the error', () => { 87 | // Arrange 88 | let browsersAreReady = true 89 | const expectedError = 'expected error' 90 | capturedBrowsers.areAllReady = () => browsersAreReady 91 | executor.schedule() 92 | browsersAreReady = false 93 | 94 | // Act 95 | const result = executor.scheduleError(expectedError) 96 | browsersAreReady = true 97 | 98 | // Assert 99 | expect(result).to.be.false 100 | expect(spy.onRunComplete).to.not.have.been.called 101 | emitter.emit('run_complete') 102 | expect(spy.onRunComplete).to.have.been.calledWith([], sinon.match({ 103 | error: expectedError 104 | })) 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /test/unit/file.spec.js: -------------------------------------------------------------------------------- 1 | const File = require('../../lib/file') 2 | 3 | describe('File', () => { 4 | describe('detectType', () => { 5 | it('should detect type from the file extension', () => { 6 | const file = new File('/path/to/file.js') 7 | expect(file.detectType()).to.equal('js') 8 | }) 9 | 10 | it('should return empty string if file does not have an extension', () => { 11 | const file = new File('/path/to/file-without-extension') 12 | expect(file.detectType()).to.equal('') 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/unit/fixtures/format-error-property.js: -------------------------------------------------------------------------------- 1 | exports.formatError = function formatErrorProperty (msg) { 2 | return msg 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/format-error-root.js: -------------------------------------------------------------------------------- 1 | // a valid --format-error file 2 | module.exports = function formatErrorRoot (msg) { 3 | return msg 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cfg = require('../../lib/config') 4 | 5 | describe('index', () => { 6 | const index = require('../../lib/index') 7 | 8 | it('should expose the `config` object', () => { 9 | expect(index.config.parseConfig).to.be.eq(cfg.parseConfig) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/unit/init/formatters.spec.js: -------------------------------------------------------------------------------- 1 | const formatters = require('../../../lib/init/formatters') 2 | 3 | describe('init/formatters', () => { 4 | let formatter 5 | 6 | describe('JavaScript', () => { 7 | beforeEach(() => { 8 | formatter = new formatters.JavaScript() 9 | }) 10 | 11 | describe('formatAnswers', () => { 12 | function createAnswers (ans) { 13 | ans = ans || {} 14 | ans.frameworks = ans.frameworks || [] 15 | ans.files = ans.files || [] 16 | ans.onlyServedFiles = ans.onlyServedFiles || [] 17 | ans.exclude = ans.exclude || [] 18 | ans.browsers = ans.browsers || [] 19 | ans.preprocessors = ans.preprocessors || {} 20 | return ans 21 | } 22 | 23 | it('should format FRAMEWORKS', () => { 24 | const replacements = formatter.formatAnswers(createAnswers({ frameworks: ['jasmine', 'requirejs'] })) 25 | expect(replacements.FRAMEWORKS).to.equal("'jasmine', 'requirejs'") 26 | }) 27 | 28 | it('should format FILES', () => { 29 | let replacements = formatter.formatAnswers(createAnswers()) 30 | expect(replacements.FILES).to.equal('') 31 | 32 | replacements = formatter.formatAnswers(createAnswers({ files: ['*.js', 'other/file.js'] })) 33 | expect(replacements.FILES).to.equal( 34 | "\n '*.js',\n 'other/file.js'" 35 | ) 36 | }) 37 | 38 | it('should format BROWSERS', () => { 39 | const replacements = formatter.formatAnswers(createAnswers({ browsers: ['Chrome', 'Firefox'] })) 40 | expect(replacements.BROWSERS).to.equal("'Chrome', 'Firefox'") 41 | }) 42 | 43 | it('should format AUTO_WATCH', () => { 44 | let replacements = formatter.formatAnswers(createAnswers({ autoWatch: true })) 45 | expect(replacements.AUTO_WATCH).to.equal('true') 46 | 47 | replacements = formatter.formatAnswers(createAnswers({ autoWatch: false })) 48 | expect(replacements.AUTO_WATCH).to.equal('false') 49 | }) 50 | 51 | it('should format onlyServedFiles', () => { 52 | const replacements = formatter.formatAnswers(createAnswers({ 53 | files: ['test-main.js'], 54 | onlyServedFiles: ['src/*.js'] 55 | })) 56 | 57 | expect(replacements.FILES).to.equal( 58 | "\n 'test-main.js',\n { pattern: 'src/*.js', included: false }" 59 | ) 60 | }) 61 | 62 | it('should format PREPROCESSORS', () => { 63 | const replacements = formatter.formatAnswers(createAnswers({ preprocessors: { '*.coffee': ['coffee'] } })) 64 | 65 | expect(replacements.PREPROCESSORS).to.equal( 66 | "\n '*.coffee': ['coffee']" 67 | ) 68 | }) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/unit/launchers/capture_timeout.spec.js: -------------------------------------------------------------------------------- 1 | const BaseLauncher = require('../../../lib/launchers/base') 2 | const CaptureTimeoutLauncher = require('../../../lib/launchers/capture_timeout') 3 | const createMockTimer = require('../mocks/timer') 4 | 5 | describe('launchers/capture_timeout.js', () => { 6 | let timer 7 | let launcher 8 | 9 | beforeEach(() => { 10 | timer = createMockTimer() 11 | launcher = new BaseLauncher('fake-id') 12 | 13 | sinon.spy(launcher, 'kill') 14 | }) 15 | 16 | it('should kill if not captured in captureTimeout', () => { 17 | CaptureTimeoutLauncher.call(launcher, timer, 10) 18 | 19 | launcher.start() 20 | timer.wind(20) 21 | expect(launcher.kill).to.have.been.called 22 | }) 23 | 24 | it('should not kill if browser got captured', () => { 25 | CaptureTimeoutLauncher.call(launcher, timer, 10) 26 | 27 | launcher.start() 28 | launcher.markCaptured() 29 | timer.wind(20) 30 | expect(launcher.kill).not.to.have.been.called 31 | }) 32 | 33 | it('should not do anything if captureTimeout = 0', () => { 34 | CaptureTimeoutLauncher.call(launcher, timer, 0) 35 | 36 | launcher.start() 37 | timer.wind(20) 38 | expect(launcher.kill).not.to.have.been.called 39 | }) 40 | 41 | it('should clear timeout between restarts', async () => { 42 | CaptureTimeoutLauncher.call(launcher, timer, 10) 43 | 44 | // simulate process finished 45 | launcher.on('kill', (onKillDone) => { 46 | launcher._done() 47 | onKillDone() 48 | }) 49 | 50 | launcher.start() 51 | timer.wind(8) 52 | await launcher.kill() 53 | launcher.kill.resetHistory() 54 | launcher.start() 55 | timer.wind(8) 56 | expect(launcher.kill).not.to.have.been.called 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/unit/launchers/retry.spec.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | const BaseLauncher = require('../../../lib/launchers/base') 4 | const RetryLauncher = require('../../../lib/launchers/retry') 5 | const EventEmitter = require('../../../lib/events').EventEmitter 6 | 7 | describe('launchers/retry.js', () => { 8 | let emitter 9 | let launcher 10 | 11 | beforeEach(() => { 12 | emitter = new EventEmitter() 13 | launcher = new BaseLauncher('fake-id', emitter) 14 | }) 15 | 16 | it('should restart if browser crashed', (done) => { 17 | RetryLauncher.call(launcher, 2) 18 | 19 | launcher.start('http://localhost:9876') 20 | 21 | sinon.spy(launcher, 'start') 22 | const spyOnBrowserProcessFailure = sinon.spy() 23 | emitter.on('browser_process_failure', spyOnBrowserProcessFailure) 24 | 25 | // simulate crash 26 | launcher._done('crash') 27 | 28 | _.defer(() => { 29 | expect(launcher.start).to.have.been.called 30 | expect(spyOnBrowserProcessFailure).not.to.have.been.called 31 | done() 32 | }) 33 | }) 34 | 35 | it('should eventually fail with "browser_process_failure"', (done) => { 36 | RetryLauncher.call(launcher, 2) 37 | 38 | launcher.start('http://localhost:9876') 39 | 40 | sinon.spy(launcher, 'start') 41 | const spyOnBrowserProcessFailure = sinon.spy() 42 | emitter.on('browser_process_failure', spyOnBrowserProcessFailure) 43 | 44 | // simulate first crash 45 | launcher._done('crash') 46 | 47 | _.defer(() => { 48 | expect(launcher.start).to.have.been.called 49 | expect(spyOnBrowserProcessFailure).not.to.have.been.called 50 | launcher.start.resetHistory() 51 | 52 | // simulate second crash 53 | launcher._done('crash') 54 | 55 | _.defer(() => { 56 | expect(launcher.start).to.have.been.called 57 | expect(spyOnBrowserProcessFailure).not.to.have.been.called 58 | launcher.start.resetHistory() 59 | 60 | // simulate third crash 61 | launcher._done('crash') 62 | 63 | _.defer(() => { 64 | expect(launcher.start).not.to.have.been.called 65 | expect(spyOnBrowserProcessFailure).to.have.been.called 66 | done() 67 | }) 68 | }) 69 | }) 70 | }) 71 | 72 | it('should not restart if killed normally', (done) => { 73 | RetryLauncher.call(launcher, 2) 74 | 75 | launcher.start('http://localhost:9876') 76 | 77 | sinon.spy(launcher, 'start') 78 | const spyOnBrowserProcessFailure = sinon.spy() 79 | emitter.on('browser_process_failure', spyOnBrowserProcessFailure) 80 | 81 | // process just exited normally 82 | launcher._done() 83 | 84 | _.defer(() => { 85 | expect(launcher.start).not.to.have.been.called 86 | expect(spyOnBrowserProcessFailure).not.to.have.been.called 87 | expect(launcher.state).to.equal(launcher.STATE_FINISHED) 88 | done() 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /test/unit/logger.spec.js: -------------------------------------------------------------------------------- 1 | const loadFile = require('mocks').loadFile 2 | const path = require('path') 3 | 4 | describe('logger', () => { 5 | let m 6 | let configuration 7 | 8 | beforeEach(() => { 9 | const mockLog4Js = { 10 | configure: function (config) { 11 | configuration = config 12 | } 13 | } 14 | m = loadFile(path.join(__dirname, '/../../lib/logger.js'), { log4js: mockLog4Js }) 15 | }) 16 | 17 | describe('setup', () => { 18 | it('should allow for configuration via setup() using an array for back-compat', () => { 19 | m.setup('INFO', true, [{ 20 | type: 'file', 21 | filename: 'test/unit/test.log' 22 | }]) 23 | expect(configuration).to.have.keys(['appenders', 'categories']) 24 | expect(configuration.appenders).to.have.keys(['0']) 25 | expect(configuration.appenders['0'].type).to.equal('file') 26 | expect(configuration.categories).to.have.keys(['default']) 27 | expect(configuration.categories.default.appenders).to.have.keys(['0']) 28 | expect(configuration.categories.default.level).to.equal('INFO') 29 | }) 30 | it('should allow setup() using log4js v2 object', () => { 31 | m.setup('WARN', true, { 32 | fileAppender: { 33 | type: 'file', 34 | filename: 'test/unit/test.log' 35 | } 36 | }) 37 | expect(configuration).to.have.keys(['appenders', 'categories']) 38 | expect(configuration.appenders).to.have.keys(['fileAppender']) 39 | expect(configuration.appenders.fileAppender.type).to.equal('file') 40 | expect(configuration.categories).to.have.keys(['default']) 41 | expect(configuration.categories.default.appenders[0]).to.equal('fileAppender') 42 | expect(configuration.categories.default.level).to.equal('WARN') 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/unit/middleware/strip_host.spec.js: -------------------------------------------------------------------------------- 1 | describe('middleware.strip_host', function () { 2 | const stripHost = require('../../../lib/middleware/strip_host').stripHost 3 | 4 | it('should strip request with IP number', function () { 5 | const normalizedUrl = stripHost('http://192.12.31.100/base/a.js?123345') 6 | expect(normalizedUrl).to.equal('/base/a.js?123345') 7 | }) 8 | 9 | it('should strip request with absoluteURI', function () { 10 | const normalizedUrl = stripHost('http://localhost/base/a.js?123345') 11 | expect(normalizedUrl).to.equal('/base/a.js?123345') 12 | }) 13 | 14 | it('should strip request with absoluteURI and port', function () { 15 | const normalizedUrl = stripHost('http://localhost:9876/base/a.js?123345') 16 | expect(normalizedUrl).to.equal('/base/a.js?123345') 17 | }) 18 | 19 | it('should strip request with absoluteURI over HTTPS', function () { 20 | const normalizedUrl = stripHost('https://karma-runner.github.io/base/a.js?123345') 21 | expect(normalizedUrl).to.equal('/base/a.js?123345') 22 | }) 23 | 24 | it('should return same url as passed one', function () { 25 | const normalizedUrl = stripHost('/base/b.js?123345') 26 | expect(normalizedUrl).to.equal('/base/b.js?123345') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/unit/mocha-globals.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const chai = require('chai') 3 | const logger = require('../../lib/logger') 4 | const recording = require('log4js/lib/appenders/recording') 5 | 6 | // publish globals that all specs can use 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 | chai.use(require('chai-subset')) 15 | 16 | beforeEach(() => { 17 | global.sinon = sinon.createSandbox() 18 | 19 | // Use https://log4js-node.github.io/log4js-node/recording.html to verify logs 20 | const vcr = { vcr: { type: 'recording' } } 21 | logger.setup('INFO', false, vcr) 22 | }) 23 | 24 | afterEach(() => { 25 | global.sinon.restore() 26 | recording.erase() 27 | }) 28 | 29 | // TODO(vojta): move to helpers or something 30 | chai.use((chai, utils) => { 31 | chai.Assertion.addMethod('beServedAs', function (expectedStatus, expectedBody) { 32 | const response = utils.flag(this, 'object') 33 | 34 | this.assert(response._status === expectedStatus, 35 | `expected response status '${response._status}' to be '${expectedStatus}'`) 36 | this.assert(response._body === expectedBody, 37 | `expected response body '${response._body}' to be '${expectedBody}'`) 38 | }) 39 | 40 | chai.Assertion.addMethod('beNotServed', function () { 41 | const response = utils.flag(this, 'object') 42 | 43 | this.assert(response._status === null, 44 | `expected response status to not be set, it was '${response._status}'`) 45 | this.assert(response._body === null, 46 | `expected response body to not be set, it was '${response._body}'`) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/unit/mocks/timer.js: -------------------------------------------------------------------------------- 1 | const Timer = require('timer-shim').Timer 2 | 3 | module.exports = function () { 4 | const timer = new Timer() 5 | timer.pause() 6 | 7 | return { 8 | setTimeout: timer.setTimeout, 9 | clearTimeout: timer.clearTimeout, 10 | setInterval: timer.setInterval, 11 | clearInterval: timer.clearInterval, 12 | wind: timer.wind 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/reporters/progress.spec.js: -------------------------------------------------------------------------------- 1 | describe('reporter', function () { 2 | const ProgressReporter = require('../../../lib/reporters/progress') 3 | 4 | describe('Progress', function () { 5 | let reporter 6 | let formatError 7 | 8 | beforeEach(function () { 9 | formatError = sinon.spy() 10 | reporter = new ProgressReporter(formatError, null, false, { terminal: true }) 11 | }) 12 | 13 | it('should turn off colors', function () { 14 | expect(reporter.EXCLUSIVELY_USE_COLORS).to.equal(false) 15 | }) 16 | 17 | it('should prepare state on run tests', function () { 18 | sinon.stub(reporter, 'write') 19 | sinon.stub(reporter, 'renderBrowser') 20 | 21 | reporter.onRunStart() 22 | reporter.onBrowserStart(createBrowserMock()) 23 | 24 | reporter.onRunStart() 25 | 26 | expect(reporter._browsers.length).to.equal(0) 27 | expect(reporter._isRendered).to.equal(false) 28 | }) 29 | 30 | it('should not throw exception if browser exit with error without run tests', function () { 31 | sinon.stub(reporter, 'write') 32 | sinon.stub(reporter, 'renderBrowser') 33 | 34 | expect(function () { 35 | reporter.onBrowserError(createBrowserMock()) 36 | }).to.not.throw() 37 | }) 38 | 39 | function createBrowserMock () { 40 | return {} 41 | } 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/unit/runner.spec.js: -------------------------------------------------------------------------------- 1 | const loadFile = require('mocks').loadFile 2 | const path = require('path') 3 | 4 | const constant = require('../../lib/constants') 5 | 6 | describe('runner', () => { 7 | let m 8 | 9 | beforeEach(() => { 10 | m = loadFile(path.join(__dirname, '/../../lib/runner.js')) 11 | }) 12 | 13 | describe('parseExitCode', () => { 14 | const EXIT = constant.EXIT_CODE 15 | 16 | it('should return 0 exit code if present in the buffer', () => { 17 | const result = m.parseExitCode(Buffer.from(`something\nfake${EXIT}10`)) 18 | expect(result.exitCode).to.equal(0) 19 | }) 20 | 21 | it('should remove the exit code part of the returned buffer', () => { 22 | const buffer = Buffer.from(`some${EXIT}01`) 23 | const result = m.parseExitCode(buffer) 24 | 25 | expect(buffer.toString()).to.equal(`some${EXIT}01`) 26 | expect(result.buffer.toString()).to.equal('some') 27 | }) 28 | 29 | it('should not touch buffer without exit code and return default', () => { 30 | const msg = 'some nice \n messgae {}' 31 | const buffer = Buffer.from(msg) 32 | const result = m.parseExitCode(buffer, 10) 33 | 34 | expect(result.buffer.toString()).to.equal(msg) 35 | expect(result.buffer).to.equal(buffer) 36 | expect(result.exitCode).to.equal(10) 37 | }) 38 | 39 | it('should not slice buffer if smaller than exit code msg', () => { 40 | // regression 41 | const fakeBuffer = { length: 1, slice: () => null } 42 | sinon.stub(fakeBuffer, 'slice') 43 | 44 | m.parseExitCode(fakeBuffer, 10) 45 | expect(fakeBuffer.slice).not.to.have.been.called 46 | }) 47 | 48 | it('should return same buffer if smaller than exit code msg', () => { 49 | // regression 50 | const fakeBuffer = { length: 1, slice: () => null } 51 | const result = m.parseExitCode(fakeBuffer, 10) 52 | expect(fakeBuffer).to.equal(result.buffer) 53 | }) 54 | 55 | it('should parse any single digit exit code', () => { 56 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}01`)).exitCode).to.equal(1) 57 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}17`)).exitCode).to.equal(7) 58 | }) 59 | 60 | it('should return exit code 0 if failOnEmptyTestSuite is false and and non-empty int is 0', () => { 61 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}01`), undefined, false).exitCode).to.equal(0) 62 | }) 63 | 64 | it('should return exit code if failOnEmptyTestSuite is true', () => { 65 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}00`), undefined, true).exitCode).to.equal(0) 66 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}01`), undefined, true).exitCode).to.equal(1) 67 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}07`), undefined, true).exitCode).to.equal(7) 68 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}10`), undefined, true).exitCode).to.equal(0) 69 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}11`), undefined, true).exitCode).to.equal(1) 70 | expect(m.parseExitCode(Buffer.from(`something\nfake${EXIT}17`), undefined, true).exitCode).to.equal(7) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /test/unit/url.spec.js: -------------------------------------------------------------------------------- 1 | const Url = require('../../lib/url') 2 | 3 | describe('Url', () => { 4 | describe('detectType', () => { 5 | it('should detect type from the file extension in the path of the URL', () => { 6 | const file = new Url('https://example.com/path/to/file.js') 7 | expect(file.detectType()).to.equal('js') 8 | }) 9 | 10 | it('should detect type for URL with query params', () => { 11 | const file = new Url('https://example.com/path/to/file.js?query=simple') 12 | expect(file.detectType()).to.equal('js') 13 | }) 14 | 15 | it('should detect type for URL with a fragment', () => { 16 | const file = new Url('https://example.com/path/to/file.js#fragment') 17 | expect(file.detectType()).to.equal('js') 18 | }) 19 | 20 | it('should return empty string if URL does not have path', () => { 21 | const file = new Url('https://example.com') 22 | expect(file.detectType()).to.equal('') 23 | }) 24 | 25 | it('should return empty string if path in the URL does not have an extension', () => { 26 | const file = new Url('https://example.com/path/to/file-without-extension') 27 | expect(file.detectType()).to.equal('') 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/unit/utils/crypto-utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CryptoUtils = require('../../../lib/utils/crypto-utils') 4 | 5 | describe('CryptoUtils.sha1', () => { 6 | it('create sha1 digest from string', () => { 7 | expect(CryptoUtils.sha1('Example text')).to.equal('cf3df620b86f0c9f586359136950217cb3b8c035') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/unit/utils/net-utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const NetUtils = require('../../../lib/utils/net-utils') 4 | const connect = require('connect') 5 | const net = require('net') 6 | 7 | describe('NetUtils.bindAvailablePort', () => { 8 | it('resolves server with bound port when it is available', (done) => { 9 | NetUtils.bindAvailablePort(9876, '127.0.0.1').then((boundServer) => { 10 | const port = boundServer.address().port 11 | expect(port).to.be.equal(9876) 12 | expect(boundServer).not.to.be.null 13 | const server = net.createServer(connect()).listen(boundServer, () => { 14 | server.close(done) 15 | }) 16 | }) 17 | }) 18 | 19 | it('resolves with next available port', (done) => { 20 | const server = net.createServer(connect()).listen(9876, '127.0.0.1', () => { 21 | NetUtils.bindAvailablePort(9876, '127.0.0.1').then((boundServer) => { 22 | const port = boundServer.address().port 23 | expect(port).to.be.equal(9877) 24 | expect(boundServer).not.to.be.null 25 | boundServer.close() 26 | server.close(done) 27 | }) 28 | }) 29 | }) 30 | 31 | it('rejects if a critical error occurs', (done) => { 32 | const incorrectAddress = '123.321' 33 | NetUtils.bindAvailablePort(9876, incorrectAddress).catch((err) => { 34 | expect(err).not.to.be.null 35 | done() 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/unit/utils/path-utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const PathUtils = require('../../../lib/utils/path-utils') 3 | const fs = require('fs') 4 | 5 | describe('PathUtils.calculateAbsolutePath', () => { 6 | it('returns absolute path from karma project relative path', () => { 7 | expect(fs.existsSync(PathUtils.calculateAbsolutePath('logo/banner.png'))).to.be.true 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/unit/utils/pattern-utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const PatternUtils = require('../../../lib/utils/pattern-utils') 3 | 4 | describe('PatternUtils.getBaseDir', () => { 5 | it('return parent directory without start', () => { 6 | expect(PatternUtils.getBaseDir('/some/path/**/more.js')).to.equal('/some/path') 7 | expect(PatternUtils.getBaseDir('/some/p*/file.js')).to.equal('/some') 8 | }) 9 | 10 | it('remove part with !(x)', () => { 11 | expect(PatternUtils.getBaseDir('/some/p/!(a|b).js')).to.equal('/some/p') 12 | expect(PatternUtils.getBaseDir('/some/p!(c|b)*.js')).to.equal('/some') 13 | }) 14 | 15 | it('remove part with +(x)', () => { 16 | expect(PatternUtils.getBaseDir('/some/p/+(a|b).js')).to.equal('/some/p') 17 | expect(PatternUtils.getBaseDir('/some/p+(c|bb).js')).to.equal('/some') 18 | }) 19 | 20 | it('remove part with (x)?', () => { 21 | expect(PatternUtils.getBaseDir('/some/p/(a|b)?.js')).to.equal('/some/p') 22 | expect(PatternUtils.getBaseDir('/some/p(c|b)?.js')).to.equal('/some') 23 | }) 24 | 25 | it('allow paths with parentheses', () => { 26 | expect(PatternUtils.getBaseDir('/some/x (a|b)/a.js')).to.equal('/some/x (a|b)/a.js') 27 | expect(PatternUtils.getBaseDir('/some/p(c|b)/*.js')).to.equal('/some/p(c|b)') 28 | }) 29 | 30 | it('ignore exact files', () => { 31 | expect(PatternUtils.getBaseDir('/usr/local/bin.js')).to.equal('/usr/local/bin.js') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /thesis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karma-runner/karma/84f85e7016efc2266fa6b3465f494a3fa151c85c/thesis.pdf -------------------------------------------------------------------------------- /tools/update-contributors.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const { readFileSync, writeFileSync } = require('fs') 3 | const { resolve } = require('path') 4 | 5 | const prepare = async (pluginConfig, { logger }) => { 6 | // Example output: 7 | // 1042 Vojta Jina <vojta.jina@gmail.com> 8 | // 412 Friedel Ziegelmayer <friedel.ziegelmayer@gmail.com> 9 | // 206 dignifiedquire <friedel.ziegelmayer@gmail.com> 10 | // 139 johnjbarton <johnjbarton@johnjbarton.com> 11 | const stdout = execSync('git log --pretty=short | git shortlog -nse', { encoding: 'utf8' }) 12 | 13 | const pkgPath = resolve(__dirname, '..', 'package.json') 14 | const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) 15 | 16 | // First line is already included as author field. Last line is dropped as it is an empty line. 17 | pkg.contributors = stdout.split('\n').slice(1, -1).map((line) => line.replace(/^[\W\d]+/, '')) 18 | writeFileSync(pkgPath, JSON.stringify(pkg, undefined, ' ') + '\n', 'utf8') 19 | 20 | logger.info('Updated contributors list.') 21 | } 22 | 23 | module.exports = { prepare } 24 | -------------------------------------------------------------------------------- /tools/update-docs.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const { dirSync } = require('tmp') 3 | 4 | const success = async (pluginConfig, { nextRelease, logger }) => { 5 | const [major, minor] = nextRelease.version.split('.') 6 | const docsVersion = `${major}.${minor}` 7 | 8 | const { name: docsPath } = dirSync() 9 | 10 | // This is a regular repository remote one would get if they click a Clone 11 | // button on GitHub. The only added part is GITHUB_TOKEN value in the 12 | // `userinfo` part of the URL (https://en.wikipedia.org/wiki/URL), which 13 | // allows GitHub to authenticate a user and authorise the push to the 14 | // repository. 15 | const repoOrigin = `https://${process.env.GITHUB_TOKEN}@github.com/karma-runner/karma-runner.github.com.git` 16 | 17 | const options = { encoding: 'utf8', cwd: docsPath } 18 | 19 | logger.log(execSync(`git clone ${repoOrigin} .`, options)) 20 | logger.log(execSync('npm ci', options)) 21 | logger.log(execSync(`./sync-docs.sh "${nextRelease.gitTag}" "${docsVersion}"`, options)) 22 | logger.log(execSync('git push origin master', options)) 23 | } 24 | 25 | module.exports = { success } 26 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (wallaby) { 3 | return { 4 | files: [ 5 | { 6 | pattern: 'package.json', 7 | instrument: false 8 | }, 9 | { 10 | pattern: 'config.tpl.js', 11 | instrument: false 12 | }, 13 | { 14 | pattern: 'test/unit/certificates/server.key', 15 | instrument: false 16 | }, 17 | { 18 | pattern: 'test/unit/certificates/server.crt', 19 | instrument: false 20 | }, 21 | { 22 | pattern: 'test/unit/**/*.spec.js', 23 | ignore: true 24 | }, 25 | 'lib/**/*.js', 26 | 'test/unit/**/*.js', 27 | 'test/unit/mocha-globals.js' 28 | ], 29 | 30 | tests: [ 31 | 'test/unit/**/*.spec.js' 32 | ], 33 | 34 | bootstrap: function (w) { 35 | var path = require('path') 36 | var mocha = w.testFramework 37 | 38 | mocha.suite.on('pre-require', function () { 39 | // always passing wallaby.js globals to mocks.loadFile 40 | var mocks = require('mocks') 41 | var loadFile = mocks.loadFile 42 | mocks.loadFile = function (filePath, mocks, globals, mockNested) { 43 | mocks = mocks || {} 44 | globals = globals || {} 45 | globals.$_$wp = global.$_$wp || {} 46 | globals.$_$wpe = global.$_$wpe || {} 47 | globals.$_$w = global.$_$w || {} 48 | globals.$_$wf = global.$_$wf || {} 49 | globals.$_$tracer = global.$_$tracer || {} 50 | return loadFile(filePath, mocks, globals, mockNested) 51 | } 52 | 53 | // loading mocha-globals for each run 54 | require(path.join(process.cwd(), 'test/unit/mocha-globals')) 55 | }) 56 | }, 57 | 58 | env: { 59 | type: 'node', 60 | params: { 61 | runner: '--harmony --harmony_arrow_functions' 62 | } 63 | } 64 | } 65 | } 66 | --------------------------------------------------------------------------------