├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── bin └── testcafe-live.js ├── lib ├── client │ ├── .eslintrc.json │ └── index.js ├── controller.js ├── empty-test.js ├── file-watcher │ ├── index.js │ └── modules-graph.js ├── index.js ├── log-update-async-hook │ ├── cli-cursor.js │ ├── index.js │ └── restore-cursor.js ├── logger │ ├── base.js │ ├── index.js │ ├── plain.js │ └── updating.js ├── test-run-controller.js └── test-runner.js ├── media └── testcafe-live-twitter.gif └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/log-update-async-hook/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | 4 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ], 24 | "no-console": 0 25 | } 26 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | *.js text eol=lf 4 | *.ts text eol=lf 5 | *.css text eol=lf 6 | *.html text eol=lf 7 | *.md text eol=lf 8 | 9 | *.png binary 10 | *.ico binary 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Developer Express Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TestCafe Live 2 | 3 | > This module is now **deprecated**. 4 | > 5 | > In TestCafe v1.0.0, we have integrated features from the `testcafe-live` module into the main TestCafe code. We have introduced the live mode, which provides experience similar to `testcafe-live`. See [Live Mode](https://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/live-mode.html) for more information. 6 | 7 | See instant feedback when working on tests. 8 | 9 | ## What is it? 10 | 11 | TestCafe Live provides a service that keeps the TestCafe process and browsers opened the whole time you are 12 | working on tests. Changes you make in code immediately restart the tests. That is, TestCafe Live allows you to see test results instantly. 13 | 14 | ![TestCafe Live Demo](https://raw.githubusercontent.com/DevExpress/testcafe-live/master/media/testcafe-live-twitter.gif) 15 | 16 | Watch [the full review on YouTube](https://www.youtube.com/watch?v=RWQtB6Xv01Q). 17 | 18 | ## Install 19 | 20 | TestCafe Live is a CLI tool. To start using it, you need to install both `testcafe` and `testcafe-live`: 21 | 22 | ```sh 23 | npm install testcafe testcafe-live -g 24 | ``` 25 | 26 | If you have already installed the `testcafe` module (version `0.18.0` or higher) you can install only `testcafe-live`: 27 | 28 | ```sh 29 | npm install testcafe-live -g 30 | ``` 31 | 32 | This installs modules on your machine [globally](https://docs.npmjs.com/getting-started/installing-npm-packages-globally). 33 | 34 | If necessary, you can add these modules locally to your project: 35 | 36 | ```sh 37 | cd 38 | npm install testcafe testcafe-live --save-dev 39 | ``` 40 | 41 | If you have installed `testcafe-live` locally to your project, add an npm script to `package.json` to run tests: 42 | 43 | ```json 44 | "scripts": { 45 | "testcafe-live": "testcafe-live chrome tests/" 46 | }, 47 | ``` 48 | 49 | ```sh 50 | npm run testcafe-live 51 | ``` 52 | 53 | ## How to use 54 | 55 | Run tests with `testcafe-live` in the same way as you do with `testcafe`: 56 | 57 | ```sh 58 | testcafe-live chrome tests/ 59 | ``` 60 | 61 | Use [standard `testcafe` arguments](https://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html) to run tests with `testcafe-live`. It opens the required browsers, run tests 62 | there, shows the reports and waits for your further actions. 63 | 64 | TestCafe Live watches files that you pass as the `src` argument and files that are required in them. Once you make changes in files and save them, TestCafe Live immediately reruns your tests. 65 | 66 | When the tests are done, browsers stay on the last opened page so you can work with it and explore it by using the browser's developer tools. 67 | 68 | ### Commands 69 | 70 | - ctrl+s - stop current test run; 71 | - ctrl+r - restart current test run; 72 | - ctrl+w - turn off/on files watching; 73 | - ctrl+c - close opened browsers and terminate the process. 74 | 75 | ## Features 76 | 77 | - TestCafe Live watches files with tests and helper modules rerunning the tests once changes are saved; 78 | - You can explore the tested page in the same browser when tests are finished; 79 | - If tests authenticate into your web application using [User Roles](https://devexpress.github.io/testcafe/documentation/test-api/authentication/user-roles.html), you do not need to execute login actions every test run, it saves your working time; 80 | - Use the same API as TestCafe; 81 | - CLI interface allows you to stop test runs, restart them and pause file watching; 82 | - You can use TestCafe Live with any browsers (local, remote, mobile or headless). 83 | 84 | ## Why TestCafe Live is a separate repository 85 | 86 | TestCafe Live is developed by the TestCafe Team as an addition to the main TestCafe module. Keeping it a separate project provides many benefits: 87 | 88 | - We can deliver new functionality once it's ready regardless of the TestCafe release cycle; 89 | - We can get feedback early and make new releases as fast as necessary to provide the best experience for developers; 90 | - We can try experimental features that may be added to TestCafe later and get early feedback about them. 91 | 92 | ### Will this functionality be released in the main TestCafe tool 93 | 94 | We will decide when we have more feedback and when we consider TestCafe Live finished and stable. Since TestCafe is a test runner it is possible 95 | that `live` mode will exist as an additional tool. 96 | 97 | ## Tips and Tricks 98 | 99 | ### Which path should I pass as the `src` argument 100 | 101 | You can pass either a path to a file with tests or a path to a directory. 102 | 103 | If you specify a single file, `testcafe-live` will watch changes in it. Additionally, it will watch changes in files that are required from this file. Once you save changes in this file or in one of the required files, tests are rerun. 104 | 105 | ```sh 106 | testcafe-live chrome tests/test.js 107 | ``` 108 | 109 | You can also pass a path to a directory where your files with tests are stored. 110 | 111 | ```sh 112 | testcafe-live chrome tests/ 113 | ``` 114 | 115 | TestCafe will watch all files in this directory and all files that are required from there and restart all tests once one of them is changed. 116 | 117 | ### I have lots of tests but would like to restart only one 118 | 119 | When you work on a particular test, just add the `.only` call for it: 120 | 121 | ``` 122 | test.only('Current test', async t => {}); 123 | ``` 124 | 125 | Once you are done with it and ready to run the whole suite, just remove the `.only` directive and save the file. 126 | 127 | ### Should I use TestCafe Live or TestCafe for CI 128 | 129 | TestCafe Live is designed to work with tests locally. So use the main `testcafe` module for CI. 130 | 131 | ### How avoid performing authentication actions every run 132 | 133 | If you test a page with a login form, you need to enter credentials at the beginning of each test. TestCafe provides [User Roles](https://devexpress.github.io/testcafe/documentation/test-api/authentication/user-roles.html) to impove your experience here. 134 | 135 | At first, create a user role with authentication steps in a separate file and export it: 136 | 137 | ```js 138 | // roles.js 139 | 140 | import { Role, t } from 'testcafe'; 141 | 142 | export loggedUser = Role('http://mysite/login-page', async t => { 143 | await t 144 | .typeText('#login-input', 'my-login') 145 | .typeText('#password-input', 'my-input') 146 | .click('#submit-btn'); 147 | }, { preserveUrl: true }); 148 | ``` 149 | 150 | Import the role into a file with tests and use it in the `beforeEach` function: 151 | 152 | ```js 153 | // test.js 154 | import { loggedUser } from './roles.js'; 155 | 156 | fixture `Check logged user` 157 | .page `http://mysite/profile` 158 | .beforeEach(async t => { 159 | await t.useRole(loggedUser); 160 | }); 161 | 162 | test('My test with logged user', async t => { 163 | // perform any action here as a logged user 164 | }); 165 | ``` 166 | 167 | When you run tests with TestCafe Live for the first time, role initialization steps will be executed and your tests will run with an authorized user profile. If you change the test file, the next run will skip role initialization and just load the page with saved 168 | credentials. If you change code in a role, it will be 'refreshed' and role initialization steps will be executed at the next run. 169 | 170 | ### I'd like to make changes in several files before running tests 171 | 172 | Just focus your terminal and press `ctrl+p`. TestCafe Live will not run tests until your press `ctrl+r`. 173 | 174 | ## Feedback 175 | 176 | Report issues and leave proposals regarding this 'live' mode in this repository. Please address all issues about TestCafe to the main [TestCafe repository](https://github.com/DevExpress/testcafe/issues). 177 | If you like this mode please let us know. We will be glad to hear your proposals on how to make it more convinient. 178 | Feel free to share your experience with other developers. 179 | 180 | ## Author 181 | 182 | Developer Express Inc. ([https://devexpress.com](https://devexpress.com)) 183 | -------------------------------------------------------------------------------- /bin/testcafe-live.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../lib'); 6 | -------------------------------------------------------------------------------- /lib/client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | } 5 | } -------------------------------------------------------------------------------- /lib/client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function () { 4 | function getDriver (callback) { 5 | var interval = window.setInterval(function () { 6 | var testCafeDriver = window['%testCafeDriverInstance%']; 7 | 8 | if (testCafeDriver) { 9 | window.clearInterval(interval); 10 | callback(testCafeDriver); 11 | } 12 | }, 50); 13 | } 14 | 15 | // NOTE: enable interaction with a page when the last test is completed 16 | var UNLOCK_PAGE_FLAG = 'testcafe-live|driver|unlock-page-flag'; 17 | 18 | // TestCafe > 0.18.5 required 19 | getDriver(function (testCafeDriver) { 20 | var testCafeCore = window['%testCafeCore%']; 21 | var hammerhead = window['%hammerhead%']; 22 | 23 | testCafeDriver.setCustomCommandHandlers('unlock-page', function () { 24 | testCafeCore.disableRealEventsPreventing(); 25 | 26 | testCafeDriver.contextStorage.setItem(UNLOCK_PAGE_FLAG, true); 27 | 28 | return hammerhead.Promise.resolve(); 29 | }); 30 | 31 | var chain = testCafeDriver.contextStorage ? hammerhead.Promise.resolve() : testCafeDriver.readyPromise; 32 | 33 | chain.then(function () { 34 | if (testCafeDriver.contextStorage.getItem(UNLOCK_PAGE_FLAG)) 35 | testCafeCore.disableRealEventsPreventing(); 36 | }); 37 | }); 38 | })(); -------------------------------------------------------------------------------- /lib/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const TestRunner = require('./test-runner'); 5 | const FileWatcher = require('./file-watcher'); 6 | const logger = require('./logger'); 7 | 8 | class Controller extends EventEmitter { 9 | constructor () { 10 | super(); 11 | 12 | this.REQUIRED_MODULE_FOUND_EVENT = 'require-module-found'; 13 | 14 | this.testRunner = null; 15 | this.src = null; 16 | 17 | this.running = false; 18 | this.restarting = false; 19 | this.watchingPaused = false; 20 | this.stopping = false; 21 | } 22 | 23 | init (tcArguments) { 24 | this._initFileWatching(tcArguments.resolvedFiles); 25 | 26 | this.testRunner = new TestRunner(tcArguments, logger); 27 | 28 | this.testRunner.on(this.testRunner.TEST_RUN_STARTED, () => logger.testsStarted()); 29 | 30 | this.testRunner.on(this.testRunner.TEST_RUN_DONE_EVENT, e => { 31 | this.running = false; 32 | if (!this.restarting) { 33 | logger.testsFinished(); 34 | } 35 | if (e.err) { 36 | console.log(`ERROR: ${e.err}`); 37 | } 38 | }); 39 | 40 | this.testRunner.on(this.testRunner.REQUIRED_MODULE_FOUND_EVENT, e => { 41 | this.emit(this.REQUIRED_MODULE_FOUND_EVENT, e); 42 | }); 43 | 44 | return this.testRunner.init() 45 | .then(() => logger.intro(tcArguments)) 46 | .then(() => this._runTests()); 47 | } 48 | 49 | _initFileWatching (src) { 50 | const fileWatcher = new FileWatcher(src); 51 | 52 | this.on(this.REQUIRED_MODULE_FOUND_EVENT, e => fileWatcher.addFile(e.filename)); 53 | 54 | fileWatcher.on(fileWatcher.FILE_CHANGED_EVENT, () => this._runTests(true)); 55 | } 56 | 57 | _runTests (sourceChanged) { 58 | if (this.watchingPaused || this.running) { 59 | return; 60 | } 61 | this.running = true; 62 | this.restarting = false; 63 | logger.runTests(sourceChanged); 64 | return this.testRunner.run(); 65 | } 66 | 67 | toggleWatching () { 68 | this.watchingPaused = !this.watchingPaused; 69 | 70 | logger.toggleWatching(!this.watchingPaused); 71 | } 72 | 73 | stop () { 74 | if (!this.testRunner || !this.running) { 75 | logger.nothingToStop(); 76 | 77 | return Promise.resolve(); 78 | } 79 | 80 | logger.stopRunning(); 81 | 82 | return this.testRunner.stop() 83 | .then(() => { 84 | this.restarting = false; 85 | this.running = false; 86 | }); 87 | } 88 | 89 | restart () { 90 | if (this.restarting) { 91 | return Promise.resolve(); 92 | } 93 | this.restarting = true; 94 | if (this.running) { 95 | return this.stop() 96 | .then(() => logger.testsFinished()) 97 | .then(() => this._runTests()); 98 | } 99 | 100 | return this._runTests(); 101 | 102 | } 103 | 104 | exit () { 105 | if (this.stopping) 106 | return Promise.resolve(); 107 | 108 | logger.exit(); 109 | 110 | this.stopping = true; 111 | 112 | return this.testRunner ? this.testRunner.exit() : Promise.resolve(); 113 | } 114 | } 115 | 116 | module.exports = new Controller(); 117 | -------------------------------------------------------------------------------- /lib/empty-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | fixture `empty`; 3 | 4 | test('empty', function () { 5 | return Promise.resolve(); 6 | }); 7 | /* eslint-enable */ 8 | -------------------------------------------------------------------------------- /lib/file-watcher/index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const fs = require('fs'); 3 | const ModulesGraph = require('./modules-graph'); 4 | 5 | const LOCK_CACHE_TIMEOUT = 200; 6 | 7 | module.exports = class FileWatcher extends EventEmitter { 8 | constructor (files) { 9 | super(); 10 | 11 | this.FILE_CHANGED_EVENT = 'file-changed'; 12 | 13 | this.watchers = {}; 14 | this.lockedFiles = {}; 15 | this.modulesGraph = null; 16 | this.lastChangedFiles = []; 17 | 18 | files.forEach(f => this.addFile(f)); 19 | } 20 | 21 | _onChanged (file) { 22 | if (this.lockedFiles[file]) 23 | return; 24 | 25 | this.lockedFiles[file] = true; 26 | 27 | const cache = require.cache; 28 | 29 | if (!this.modulesGraph) { 30 | this.modulesGraph = new ModulesGraph(); 31 | this.modulesGraph.build(cache, Object.keys(this.watchers)); 32 | } 33 | else { 34 | this.lastChangedFiles.forEach(changedFile => this.modulesGraph.rebuildNode(cache, changedFile)); 35 | this.lastChangedFiles = []; 36 | } 37 | 38 | this.lastChangedFiles.push(file); 39 | this.modulesGraph.clearParentsCache(cache, file); 40 | 41 | setTimeout(() => this.lockedFiles[file] = void 0, LOCK_CACHE_TIMEOUT); 42 | 43 | this.emit(this.FILE_CHANGED_EVENT, { file }); 44 | } 45 | 46 | addFile (file) { 47 | if (!this.watchers[file] && file.indexOf('node_modules') < 0) { 48 | if (this.modulesGraph) { 49 | this.lastChangedFiles.push(file); 50 | this.modulesGraph.addNode(file, require.cache); 51 | } 52 | 53 | this.watchers[file] = fs.watch(file, () => { 54 | this._onChanged(file); 55 | }); 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /lib/file-watcher/modules-graph.js: -------------------------------------------------------------------------------- 1 | const Graph = require('graphlib').Graph; 2 | 3 | module.exports = class ModulesGraph { 4 | constructor () { 5 | this.graph = new Graph(); 6 | } 7 | 8 | _updateChildren (node, cache) { 9 | const cached = cache[node]; 10 | 11 | if (!cached) 12 | return; 13 | 14 | const outEdges = this.graph.outEdges(node) || []; 15 | 16 | outEdges.forEach(edge => this.graph.removeEdge(edge.v, edge.w)); 17 | 18 | const children = cached && cached.children.map(child => child.id); 19 | 20 | if (children) { 21 | children.forEach(child => { 22 | if (child.indexOf('node_modules') > -1) 23 | return; 24 | 25 | this.addNode(child, cache); 26 | this.graph.setEdge(node, child); 27 | }); 28 | } 29 | } 30 | 31 | addNode (node, cache) { 32 | if (this.graph.hasNode(node)) 33 | return; 34 | 35 | const cached = cache[node]; 36 | 37 | if (cached) 38 | this.graph.setNode(node); 39 | 40 | const parent = cached && cached.parent; 41 | 42 | if (parent && parent.id.indexOf('node_modules') < 0) { 43 | this.addNode(parent.id, cache); 44 | this.graph.setEdge(parent.id, node); 45 | } 46 | 47 | this._updateChildren(node, cache); 48 | } 49 | 50 | build (cache, nodes) { 51 | nodes.forEach(node => this.addNode(node, cache, true)); 52 | } 53 | 54 | rebuildNode (cache, node) { 55 | this._updateChildren(node, cache); 56 | } 57 | 58 | clearParentsCache (cache, node) { 59 | if (!cache[node]) 60 | return; 61 | 62 | cache[node] = null; 63 | 64 | let parentEdges = this.graph.inEdges(node); 65 | 66 | if (!parentEdges || !parentEdges.length) 67 | return; 68 | 69 | parentEdges.map(edge => edge.v).forEach(parent => this.clearParentsCache(cache, parent)); 70 | } 71 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CLIArgumentParser = require('testcafe/lib/cli/argument-parser'); 4 | const exitHook = require('async-exit-hook'); 5 | const keypress = require('keypress'); 6 | const controller = require('./controller'); 7 | const globby = require('globby'); 8 | 9 | const tcArguments = new CLIArgumentParser(); 10 | 11 | exitHook(cb => { 12 | controller.exit() 13 | .then(cb); 14 | }); 15 | 16 | tcArguments.parse(process.argv) 17 | .then(() => globby(tcArguments.src)) 18 | .then((resolvedFiles) => tcArguments.resolvedFiles = resolvedFiles) 19 | .then(() => controller.init(tcArguments)); 20 | 21 | 22 | // Listen commands 23 | keypress(process.stdin); 24 | 25 | process.stdin.on('keypress', function (ch, key) { 26 | if (key && key.ctrl) { 27 | if (key.name === 's') 28 | return controller.stop(); 29 | 30 | else if (key.name === 'r') 31 | return controller.restart(); 32 | 33 | else if (key.name === 'c') 34 | return controller.exit().then(() => process.exit(0)); 35 | 36 | else if (key.name === 'w') 37 | return controller.toggleWatching(); 38 | } 39 | }); 40 | 41 | if (process.stdout.isTTY) { 42 | process.stdin.setRawMode(true); 43 | } 44 | -------------------------------------------------------------------------------- /lib/log-update-async-hook/cli-cursor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var restoreCursor = require('./restore-cursor'); 3 | 4 | var hidden = false; 5 | 6 | exports.show = function (stream) { 7 | var s = stream || process.stderr; 8 | 9 | if (!s.isTTY) { 10 | return; 11 | } 12 | 13 | hidden = false; 14 | s.write('\u001b[?25h'); 15 | }; 16 | 17 | exports.hide = function (stream) { 18 | var s = stream || process.stderr; 19 | 20 | if (!s.isTTY) { 21 | return; 22 | } 23 | 24 | restoreCursor(); 25 | hidden = true; 26 | s.write('\u001b[?25l'); 27 | }; 28 | 29 | exports.toggle = function (force, stream) { 30 | if (force !== undefined) { 31 | hidden = force; 32 | } 33 | 34 | if (hidden) { 35 | exports.show(stream); 36 | } 37 | else { 38 | exports.hide(stream); 39 | } 40 | }; -------------------------------------------------------------------------------- /lib/log-update-async-hook/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var ansiEscapes = require('ansi-escapes'); 3 | var wrapAnsi = require('wrap-ansi'); 4 | var cliCursor = require('./cli-cursor'); 5 | 6 | 7 | function main (stream) { 8 | var prevLineCount = 0; 9 | 10 | var render = function () { 11 | cliCursor.hide(); 12 | 13 | var out = [].join.call(arguments, ' ') + '\n'; 14 | 15 | out = wrapAnsi(out, process.stdout.columns || 80, { wordWrap: false }); 16 | stream.write(ansiEscapes.eraseLines(prevLineCount) + out); 17 | prevLineCount = out.split('\n').length; 18 | }; 19 | 20 | render.clear = function () { 21 | stream.write(ansiEscapes.eraseLines(prevLineCount)); 22 | prevLineCount = 0; 23 | }; 24 | 25 | render.done = function () { 26 | prevLineCount = 0; 27 | cliCursor.show(); 28 | }; 29 | 30 | return render; 31 | } 32 | 33 | module.exports = main(process.stdout); 34 | module.exports.stderr = main(process.stderr); 35 | module.exports.create = main; -------------------------------------------------------------------------------- /lib/log-update-async-hook/restore-cursor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var onetime = require('onetime'); 3 | var exitHook = require('async-exit-hook'); 4 | 5 | module.exports = onetime(function () { 6 | exitHook(function () { 7 | process.stderr.write('\u001b[?25h'); 8 | }); 9 | }); -------------------------------------------------------------------------------- /lib/logger/base.js: -------------------------------------------------------------------------------- 1 | const origStrOutWrite = process.stdout.write; 2 | 3 | module.exports = class BaseLogger { 4 | constructor () { 5 | this.testingStarted = false; 6 | this.aborted = false; 7 | this.running = false; 8 | this.watching = true; 9 | 10 | process.stdout.write = (...args) => this._onStdoutWrite(...args); 11 | 12 | this.MESSAGES = { 13 | intro: ` 14 | TestCafe Live watches the files and reruns 15 | the tests once you've saved your changes. 16 | 17 | You can use the following keys in the terminal: 18 | 'ctrl+s' - stop current test run; 19 | 'ctrl+r' - restart current test run; 20 | 'ctrl+w' - turn off/on watching; 21 | 'ctrl+c' - close browsers and terminate the process. 22 | 23 | `, 24 | 25 | sourceChanged: 'Sources have been changed. Test run is starting...', 26 | testRunStarting: 'Test run is starting...', 27 | testRunStarted: 'Test run in progress...', 28 | testRunStopping: 'Current test run is stopping...', 29 | testRunFinishedWatching: 'Make changes in the source files or press ctrl+r to restart test run.', 30 | testRunFinishedNotWatching: 'Press ctrl+r to restart test run.', 31 | fileWatchingEnabled: 'File watching enabled. Save changes in your files to run tests.', 32 | fileWatchingDisabled: 'File watching disabled.', 33 | nothingToStop: 'There are no tests running at the moment.', 34 | testCafeStopping: 'Stopping TestCafe Live...' 35 | }; 36 | } 37 | 38 | _write (msg) { 39 | origStrOutWrite.call(process.stdout, msg); 40 | } 41 | 42 | _onStdoutWrite () { 43 | throw new Error('Not implemented'); 44 | } 45 | 46 | _report () { 47 | throw new Error('Not implemented'); 48 | } 49 | 50 | _status () { 51 | throw new Error('Not implemented'); 52 | } 53 | 54 | intro (tcArguments) { 55 | this._write(this.MESSAGES.intro); 56 | if (tcArguments && Array.isArray(tcArguments.resolvedFiles)) { 57 | this._status('Watching files:'); 58 | tcArguments.resolvedFiles.forEach((file) => this._write(' ' + file + '\n')); 59 | this._write('\n'); 60 | } 61 | } 62 | 63 | runTests (sourcesChanged) { 64 | this.testingStarted = true; 65 | this.aborted = false; 66 | 67 | if (sourcesChanged) 68 | this._status(this.MESSAGES.sourceChanged); 69 | else 70 | this._status(this.MESSAGES.testRunStarting); 71 | } 72 | 73 | testsStarted () { 74 | this.running = true; 75 | 76 | this._status(this.MESSAGES.testRunStarted); 77 | } 78 | 79 | testsFinished () { 80 | this.running = false; 81 | 82 | this._status(this.watching ? this.MESSAGES.testRunFinishedWatching : this.MESSAGES.testRunFinishedNotWatching); 83 | } 84 | 85 | stopRunning () { 86 | this._status(this.MESSAGES.testRunStopping); 87 | } 88 | 89 | nothingToStop () { 90 | this._status(this.MESSAGES.nothingToStop); 91 | } 92 | 93 | toggleWatching (enable) { 94 | this.watching = enable; 95 | 96 | if (enable) 97 | this._status(this.MESSAGES.fileWatchingEnabled); 98 | else 99 | this._status(this.MESSAGES.fileWatchingDisabled); 100 | } 101 | 102 | exit () { 103 | this._status(this.MESSAGES.testCafeStopping); 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /lib/logger/index.js: -------------------------------------------------------------------------------- 1 | const PlainLogger = require('./plain'); 2 | const UpdatingLogger = require('./updating'); 3 | 4 | module.exports = process.env.EXPERIMENTAL_TESTCAFE_LOG ? new UpdatingLogger() : new PlainLogger(); 5 | -------------------------------------------------------------------------------- /lib/logger/plain.js: -------------------------------------------------------------------------------- 1 | const BaseLogger = require('./base'); 2 | 3 | module.exports = class PlainLogger extends BaseLogger { 4 | constructor () { 5 | super(); 6 | } 7 | 8 | _onStdoutWrite (msg) { 9 | if (msg.indexOf('Error: Test run aborted') > -1) { 10 | this.aborted = true; 11 | this._write('Test run aborted'); 12 | } 13 | else 14 | this._write(msg); 15 | } 16 | 17 | _status (msg) { 18 | if (msg === this.MESSAGES.testRunStarted) 19 | return; 20 | 21 | this._write('\n' + msg + '\n'); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/logger/updating.js: -------------------------------------------------------------------------------- 1 | const querystring = require('querystring'); 2 | const BaseLogger = require('./base'); 3 | 4 | const logUpdate = require('log-update-async-hook'); 5 | 6 | // NOTE: clear the cache to don't use the same scope with TestCafe. 7 | // Otherwise it can lead to log updating mutual influence. 8 | delete require.cache[require.resolve('log-update-async-hook')]; 9 | 10 | 11 | const ansiEscapesCodeRe = /%1B%5B(\d*[A-KSTsun]|\d*%3B\d*H)/g; 12 | 13 | module.exports = class UpdatingLogger extends BaseLogger { 14 | constructor() { 15 | super(); 16 | 17 | this.appUpdatedLog = ''; 18 | this.currentReport = ''; 19 | this.statusMsg = ''; 20 | this.writing = false; 21 | this.aborted = false; 22 | this.running = false; 23 | } 24 | 25 | _onStdoutWrite(msg) { 26 | if (this.writing || !this.testingStarted) 27 | this._write(msg); 28 | else { 29 | const escapedMsg = querystring.escape(msg); 30 | 31 | if (msg.indexOf('Error: Test run aborted') > -1) { 32 | this.aborted = true; 33 | this.currentReport = ''; 34 | } 35 | else if (ansiEscapesCodeRe.test(escapedMsg) || /DEBUGGER PAUSE:|DEBUGGER PAUSE ON FAILED TEST:/.test(msg)) 36 | this.appUpdatedLog = querystring.unescape(escapedMsg.replace(ansiEscapesCodeRe, '')); 37 | else 38 | this.currentReport += msg.toString(); 39 | 40 | this._log(); 41 | } 42 | } 43 | 44 | _status(msg) { 45 | this.statusMsg = msg; 46 | this._log(); 47 | } 48 | 49 | _log() { 50 | this.writing = true; 51 | 52 | logUpdate(this._generateMsg()); 53 | 54 | this.writing = false; 55 | } 56 | 57 | _generateMsg() { 58 | let separator = '\n-----------------------------\n\n'; 59 | 60 | let appUpdatedLog = this.appUpdatedLog && this.running ? ` 61 | /* TESTCAFE LOG AREA */ 62 | ${this.appUpdatedLog} 63 | ` : ''; 64 | 65 | let report = ''; 66 | 67 | if (this.aborted) { 68 | report = ` 69 | /* TEST RUN REPORT AREA */ 70 | Test run aborted. 71 | `; 72 | } 73 | else { 74 | report = this.currentReport ? ` 75 | /* TEST RUN REPORT AREA */ 76 | ${this.currentReport} 77 | 78 | ` : ''; 79 | } 80 | 81 | const status = ` 82 | /* CURRENT STATUS */ 83 | ${this.statusMsg} 84 | `; 85 | 86 | return separator + report + appUpdatedLog + status; 87 | } 88 | 89 | runTests(sourcesChanged) { 90 | this.currentReport = ''; 91 | 92 | super.runTests(sourcesChanged); 93 | } 94 | 95 | testsFinished() { 96 | this.appUpdatedLog = null; 97 | 98 | super.testsFinished(); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /lib/test-run-controller.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | const testcafe = require('testcafe'); 4 | 5 | const TestRun = testcafe.embeddingUtils.TestRun; 6 | 7 | const liveTestRunStorage = Symbol('live-test-run-storage'); 8 | 9 | const testRunCtorFactory = function (callbacks, command) { 10 | const { created, started, done } = callbacks; 11 | const { registerStopHandler } = command; 12 | 13 | return class DebugRun extends TestRun { 14 | constructor (test, browserConnection, screenshotCapturer, warningLog, opts) { 15 | super(test, browserConnection, screenshotCapturer, warningLog, opts); 16 | 17 | this[liveTestRunStorage] = { test, stopping: false, stop: false, isInRoleInitializing: false }; 18 | 19 | created(this, test); 20 | 21 | this.injectable.scripts.push('/testcafe-live.js'); 22 | 23 | registerStopHandler(this, () => { 24 | this[liveTestRunStorage].stop = true; 25 | }); 26 | } 27 | 28 | start () { 29 | started(this); 30 | super.start.apply(this, arguments); 31 | } 32 | 33 | _useRole (...args) { 34 | this[liveTestRunStorage].isInRoleInitializing = true; 35 | 36 | return super._useRole.apply(this, args) 37 | .then(res => { 38 | this[liveTestRunStorage].isInRoleInitializing = false; 39 | 40 | return res; 41 | }) 42 | .catch(err => { 43 | this[liveTestRunStorage].isInRoleInitializing = false; 44 | 45 | throw err; 46 | }); 47 | } 48 | 49 | executeCommand (command, callsite, forced) { 50 | // NOTE: don't close the page and the session when the last test in the queue is done 51 | if (command.type === 'test-done' && !forced) { 52 | done(this, this[liveTestRunStorage].stop).then(() => { 53 | this.executeCommand(command, callsite, true); 54 | }); 55 | 56 | this.executeCommand({ type: 'unlock-page' }, null); 57 | 58 | return Promise.resolve(); 59 | } 60 | 61 | if (this[liveTestRunStorage].stop && !this[liveTestRunStorage].stopping && 62 | !this[liveTestRunStorage].isInRoleInitializing) { 63 | this[liveTestRunStorage].stopping = true; 64 | 65 | return Promise.reject(new Error('Test run aborted')); 66 | } 67 | 68 | return super.executeCommand(command, callsite); 69 | } 70 | }; 71 | }; 72 | 73 | const TEST_STATE = { 74 | created: 'created', 75 | running: 'running', 76 | done: 'done' 77 | }; 78 | 79 | const TEST_RUN_STATE = { 80 | created: 'created', 81 | running: 'running', 82 | waitingForDone: 'waiting-for-done', 83 | done: 'done' 84 | }; 85 | 86 | module.exports = class TestRunController extends EventEmitter { 87 | constructor () { 88 | super(); 89 | 90 | this.RUN_FINISHED_EVENT = 'run-finished-event'; 91 | this.RUN_STOPPED_EVENT = 'run-stopped-event'; 92 | this.RUN_STARTED_EVENT = 'run-started-event'; 93 | 94 | this.testWrappers = []; 95 | this.testRunWrappers = []; 96 | this.expectedTestCount = 0; 97 | this._testRunCtor = null; 98 | } 99 | 100 | get TestRunCtor () { 101 | if (!this._testRunCtor) 102 | this._testRunCtor = testRunCtorFactory({ 103 | created: testRun => this._onTestRunCreated(testRun), 104 | started: testRun => this._onTestRunStarted(testRun), 105 | done: (testRun, forced) => this._onTestRunDone(testRun, forced) 106 | }, { 107 | registerStopHandler: (testRun, handler) => { 108 | this._getWrappers(testRun).testRunWrapper.stop = () => handler(); 109 | } 110 | }); 111 | 112 | return this._testRunCtor; 113 | } 114 | 115 | _getTestWrapper (test) { 116 | return this.testWrappers.filter(w => w.test === test)[0]; 117 | } 118 | 119 | _getWrappers (testRun) { 120 | const test = testRun[liveTestRunStorage].test; 121 | const testWrapper = this._getTestWrapper(test); 122 | const testRunWrappers = testWrapper.testRunWrappers; 123 | const testRunWrapper = testRunWrappers.filter(w => w.testRun === testRun)[0]; 124 | 125 | return { testRunWrapper, testWrapper }; 126 | } 127 | 128 | _onTestRunCreated (testRun) { 129 | this.testWrappers = []; 130 | const test = testRun[liveTestRunStorage].test; 131 | 132 | let testWrapper = this._getTestWrapper(test); 133 | 134 | if (!testWrapper) { 135 | testWrapper = { 136 | test, 137 | state: TEST_STATE.created, 138 | testRunWrappers: [] 139 | }; 140 | 141 | this.testWrappers.push(testWrapper); 142 | } 143 | 144 | testWrapper.testRunWrappers.push({ testRun, state: TEST_RUN_STATE.created, finish: null, stop: null }); 145 | } 146 | 147 | _onTestRunStarted (testRun) { 148 | if (!this.testWrappers.filter(w => w.state !== TEST_RUN_STATE.created).length) 149 | this.emit(this.RUN_STARTED_EVENT, {}); 150 | 151 | const { testRunWrapper, testWrapper } = this._getWrappers(testRun); 152 | 153 | testRunWrapper.state = TEST_RUN_STATE.running; 154 | testWrapper.state = TEST_STATE.running; 155 | } 156 | 157 | _onTestRunDone (testRun, forced) { 158 | const { testRunWrapper, testWrapper } = this._getWrappers(testRun); 159 | 160 | testRunWrapper.state = TEST_RUN_STATE.waitingForDone; 161 | 162 | const waitingTestRunCount = testWrapper.testRunWrappers.filter(w => w.state === TEST_RUN_STATE.created).length; 163 | const runningTestRunCount = testWrapper.testRunWrappers.filter(w => w.state === TEST_RUN_STATE.running).length; 164 | 165 | const waitForOtherTestRuns = runningTestRunCount || waitingTestRunCount && !forced; 166 | 167 | if (!waitForOtherTestRuns) { 168 | testWrapper.state = TEST_STATE.done; 169 | 170 | //check other active tests 171 | setTimeout(() => { 172 | const hasTestsToRun = this.testWrappers.length < this.expectedTestCount || 173 | !!this.testWrappers.filter(w => w.state === TEST_STATE.created).length; 174 | 175 | if (!forced && hasTestsToRun) 176 | testWrapper.testRunWrappers.forEach(w => w.finish()); 177 | else 178 | this.emit(forced ? this.RUN_STOPPED_EVENT : this.RUN_FINISHED_EVENT); 179 | }, 0); 180 | } 181 | 182 | return new Promise(resolve => { 183 | testRunWrapper.finish = () => { 184 | testRunWrapper.finish = null; 185 | testRunWrapper.state = TEST_RUN_STATE.done; 186 | resolve(); 187 | }; 188 | }); 189 | } 190 | 191 | run (testCount) { 192 | const pendingRunsResolvers = []; 193 | 194 | this.expectedTestCount = testCount; 195 | 196 | this.testWrappers.forEach(testWrapper => { 197 | testWrapper.testRunWrappers.forEach(testRunWrapper => { 198 | if (testRunWrapper.finish) 199 | pendingRunsResolvers.push(testRunWrapper.finish); 200 | }); 201 | }); 202 | 203 | pendingRunsResolvers.forEach(r => r()); 204 | } 205 | 206 | stop () { 207 | const runningTestWrappers = this.testWrappers.filter(w => w.state === TEST_RUN_STATE.running); 208 | 209 | runningTestWrappers.forEach(testWrapper => { 210 | testWrapper.testRunWrappers.forEach(testRunWrapper => testRunWrapper.stop()); 211 | }); 212 | } 213 | }; 214 | -------------------------------------------------------------------------------- /lib/test-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const EventEmitter = require('events'); 6 | const Module = require('module'); 7 | const createTestCafe = require('testcafe'); 8 | const remotesWizard = require('testcafe/lib/cli/remotes-wizard'); 9 | const TestRunController = require('./test-run-controller'); 10 | 11 | const CLIENT_JS = fs.readFileSync(path.join(__dirname, './client/index.js')); 12 | 13 | const originalRequire = Module.prototype.require; 14 | 15 | module.exports = class TestRunner extends EventEmitter { 16 | constructor (tcArguments) { 17 | super(); 18 | 19 | /* EVENTS */ 20 | this.TEST_RUN_STARTED = 'test-run-started'; 21 | this.TEST_RUN_DONE_EVENT = 'test-run-done'; 22 | this.TEST_RUN_STOPPED = 'test-run-stopped'; 23 | this.REQUIRED_MODULE_FOUND_EVENT = 'require-module-found'; 24 | 25 | this.opts = tcArguments.opts; 26 | this.port1 = this.opts.ports && this.opts.ports[0]; 27 | this.port2 = this.opts.ports && this.opts.ports[1]; 28 | this.externalProxyHost = this.opts.proxy; 29 | 30 | this.remoteCount = tcArguments.remoteCount; 31 | this.concurrency = tcArguments.concurrency || 1; 32 | this.browsers = tcArguments.browsers; 33 | this.src = tcArguments.resolvedFiles; 34 | this.filter = tcArguments.filter; 35 | 36 | this.reporters = this.opts.reporters.map(r => { 37 | return { 38 | name: r.name, 39 | outStream: r.outFile ? fs.createWriteStream(r.outFile) : void 0 40 | }; 41 | }); 42 | 43 | this.testCafe = null; 44 | this.closeTestCafe = null; 45 | this.tcRunner = null; 46 | this.runnableConf = null; 47 | 48 | this.activeTestCount = 0; 49 | this.testRunDonePromiseResolvers = []; 50 | this.stopping = false; 51 | this.tcRunnerTaskPromise = null; 52 | 53 | this.testRunController = new TestRunController(); 54 | 55 | this.testRunController.on(this.testRunController.RUN_STARTED_EVENT, () => this.emit(this.TEST_RUN_STARTED, {})); 56 | } 57 | 58 | _mockRequire () { 59 | const runner = this; 60 | 61 | Module.prototype.require = function (filePath) { 62 | const filename = Module._resolveFilename(filePath, this, false); 63 | 64 | if (path.isAbsolute(filename) || /^\.\.?[/\\]/.test(filename)) 65 | runner.emit(runner.REQUIRED_MODULE_FOUND_EVENT, { filename }); 66 | 67 | return originalRequire.apply(this, arguments); 68 | }; 69 | } 70 | 71 | _restoreRequire () { 72 | Module.prototype.require = function () { 73 | return originalRequire.apply(this, arguments); 74 | }; 75 | } 76 | 77 | _onTaskStarted (testCount) { 78 | this.activeTestCount = testCount; 79 | this.emit(this.TEST_RUN_STARTED, {}); 80 | } 81 | 82 | _onTestFinished () { 83 | if (--this.activeTestCount) { 84 | return this._resolveAllTestRunPromises(); 85 | } 86 | } 87 | 88 | _handleTestRunCommand () { 89 | return !this.stopping; 90 | } 91 | 92 | _handleTestRunDone () { 93 | if (this.stopping) 94 | this.emit(this.TEST_RUN_STOPPED); 95 | 96 | return new Promise(resolve => { 97 | this.testRunDonePromiseResolvers.push(resolve); 98 | }); 99 | } 100 | 101 | _resolveAllTestRunPromises () { 102 | this.testRunDonePromiseResolvers.forEach(r => r()); 103 | } 104 | 105 | init () { 106 | return createTestCafe(this.opts.hostname, this.port1, this.port2) 107 | .then(tc => { 108 | this.testCafe = tc; 109 | 110 | const origTestCafeClose = this.testCafe.close; 111 | 112 | this.closeTestCafe = () => origTestCafeClose.call(this.testCafe); 113 | this.testCafe.close = () => new Promise(() => { 114 | }); 115 | 116 | return remotesWizard(this.testCafe, this.remoteCount, this.opts.qrCode); 117 | }) 118 | .then(remoteBrowsers => { 119 | this.browsers = this.browsers.concat(remoteBrowsers); 120 | }); 121 | } 122 | 123 | _createTCRunner () { 124 | const runner = this.testCafe.createRunner() 125 | .embeddingOptions({ 126 | TestRunCtor: this.testRunController.TestRunCtor, 127 | assets: [ 128 | { 129 | path: '/testcafe-live.js', 130 | info: { content: CLIENT_JS, contentType: 'application/x-javascript' } 131 | } 132 | ] 133 | }); 134 | 135 | runner.proxy.closeSession = () => { 136 | }; 137 | 138 | runner 139 | .useProxy(this.externalProxyHost) 140 | .src(this.src) 141 | .browsers(this.browsers) 142 | .concurrency(this.concurrency) 143 | .filter(this.filter) 144 | .screenshots(this.opts.screenshots, this.opts.screenshotsOnFails) 145 | .startApp(this.opts.app, this.opts.appInitDelay); 146 | 147 | if (this.reporters.length) 148 | this.reporters.forEach(r => runner.reporter(r.name, r.outStream)); 149 | else 150 | runner.reporter('spec'); 151 | 152 | // HACK: TestCafe doesn't call `cleanUp` for compilers if test compiling is failed. 153 | // So, we force it here. 154 | // TODO: fix it in TestCafe 155 | const origBootstrapperGetTests = runner.bootstrapper._getTests; 156 | 157 | runner.bootstrapper._getTests = () => { 158 | let bsError = null; 159 | const sources = runner.bootstrapper.sources; 160 | 161 | this._mockRequire(); 162 | 163 | return origBootstrapperGetTests.apply(runner.bootstrapper) 164 | .then(res => { 165 | this._restoreRequire(); 166 | 167 | return res; 168 | }) 169 | .catch(err => { 170 | this._restoreRequire(); 171 | 172 | bsError = err; 173 | 174 | runner.bootstrapper.sources = [path.join(__dirname, './empty-test.js')]; 175 | 176 | return origBootstrapperGetTests.apply(runner.bootstrapper) 177 | .then(() => { 178 | runner.bootstrapper.sources = sources; 179 | 180 | throw bsError; 181 | }); 182 | }); 183 | }; 184 | 185 | 186 | return runner.bootstrapper 187 | .createRunnableConfiguration() 188 | .then(runnableConf => { 189 | const browserSet = runnableConf.browserSet; 190 | 191 | browserSet.origDispose = browserSet.dispose; 192 | 193 | browserSet.dispose = () => Promise.resolve(); 194 | 195 | runner.bootstrapper.createRunnableConfiguration = () => Promise.resolve(runnableConf); 196 | 197 | return { runner, runnableConf }; 198 | }); 199 | } 200 | 201 | _runTests (tcRunner, runnableConf) { 202 | return tcRunner.bootstrapper 203 | ._getTests() 204 | .then(tests => { 205 | runnableConf.tests = tests; 206 | 207 | this.testRunController.run(tests.filter(t => !t.skip).length); 208 | this.tcRunnerTaskPromise = tcRunner.run(this.opts); 209 | 210 | return this.tcRunnerTaskPromise; 211 | }); 212 | } 213 | 214 | run () { 215 | let runError = null; 216 | 217 | let testRunPromise = null; 218 | 219 | if (!this.tcRunner) { 220 | testRunPromise = this 221 | ._createTCRunner() 222 | .then(res => { 223 | this.tcRunner = res.runner; 224 | this.runnableConf = res.runnableConf; 225 | 226 | return this._runTests(res.runner, res.runnableConf); 227 | }) 228 | .catch(err => { 229 | this.tcRunner = null; 230 | this.runnableConf = null; 231 | 232 | runError = err; 233 | }); 234 | } 235 | else { 236 | testRunPromise = this 237 | ._runTests(this.tcRunner, this.runnableConf) 238 | .catch(err => { 239 | runError = err; 240 | }); 241 | } 242 | 243 | return testRunPromise 244 | .then(() => { 245 | this.tcRunnerTaskPromise = null; 246 | 247 | this.emit(this.TEST_RUN_DONE_EVENT, { err: runError }); 248 | }); 249 | } 250 | 251 | stop () { 252 | if (!this.tcRunnerTaskPromise) 253 | return Promise.resolve(); 254 | 255 | return new Promise(resolve => { 256 | this.testRunController.once(this.testRunController.RUN_STOPPED_EVENT, () => { 257 | this.stopping = false; 258 | resolve(); 259 | 260 | this.emit(this.TEST_RUN_DONE_EVENT, {}); 261 | }); 262 | 263 | this.stopping = true; 264 | this.testRunController.stop(); 265 | this.tcRunnerTaskPromise.cancel(); 266 | }); 267 | } 268 | 269 | exit () { 270 | if (this.tcRunnerTaskPromise) 271 | this.tcRunnerTaskPromise.cancel(); 272 | 273 | let chain = Promise.resolve(); 274 | 275 | if (this.runnableConf) 276 | chain = chain.then(() => this.runnableConf.browserSet.origDispose()); 277 | 278 | return chain 279 | .then(() => this.closeTestCafe()); 280 | } 281 | }; 282 | -------------------------------------------------------------------------------- /media/testcafe-live-twitter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevExpress/testcafe-live/ecd82fe34fd28c5527732a3de7f0fd015248e830/media/testcafe-live-twitter.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testcafe-live", 3 | "version": "0.1.4", 4 | "description": "A watcher utility for TestCafe. Watches for changes in test files and automatically runs tests when these changes occur.", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "testcafe-live": "./bin/testcafe-live.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/DevExpress/testcafe-live.git" 12 | }, 13 | "engines": { 14 | "node": ">=4.0.0" 15 | }, 16 | "author": { 17 | "name": "Developer Express Inc.", 18 | "url": "https://www.devexpress.com/" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/DevExpress/testcafe-live/issues" 23 | }, 24 | "homepage": "https://github.com/DevExpress/testcafe-live#readme", 25 | "dependencies": { 26 | "ansi-escapes": "^3.0.0", 27 | "async-exit-hook": "^2.0.1", 28 | "globby": "^8.0.1", 29 | "graphlib": "^2.1.5", 30 | "keypress": "^0.2.1", 31 | "log-update-async-hook": "^2.0.2", 32 | "wrap-ansi": "^3.0.1" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^4.11.0", 36 | "testcafe": "*" 37 | }, 38 | "peerDependencies": { 39 | "testcafe": ">=0.18.0" 40 | }, 41 | "scripts": { 42 | "test": "eslint ./**/*.js" 43 | } 44 | } 45 | --------------------------------------------------------------------------------