Skip to content

Commit

Permalink
Integrate testcafe live (closes DevExpress#3215) (DevExpress#3222)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKamaev authored and kirovboris committed Dec 18, 2019
1 parent d9607ab commit fa5dee2
Show file tree
Hide file tree
Showing 28 changed files with 1,336 additions and 38 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@
"error-stack-parser": "^1.3.6",
"globby": "^3.0.1",
"graceful-fs": "^4.1.11",
"graphlib": "^2.1.5",
"gulp-data": "^1.3.1",
"import-lazy": "^3.1.0",
"indent-string": "^1.2.2",
"is-ci": "^1.0.10",
"is-glob": "^2.0.1",
"is-stream": "^1.1.0",
"json5": "^2.1.0",
"keypress": "^0.2.1",
"lodash": "^4.17.10",
"log-update-async-hook": "^2.0.2",
"make-dir": "^1.3.0",
Expand Down
1 change: 1 addition & 0 deletions src/cli/argument-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default class CLIArgumentParser {
.option('-F, --fixture-grep <pattern>', 'run only fixtures matching the specified pattern')
.option('-a, --app <command>', 'launch the tested app using the specified command before running tests')
.option('-c, --concurrency <number>', 'run tests concurrently')
.option('-L, --live', 'enable the live mode')
.option('--test-meta <key=value[,key2=value2,...]>', 'run only tests with matching metadata')
.option('--fixture-meta <key=value[,key2=value2,...]>', 'run only fixtures with matching metadata')
.option('--debug-on-fail', 'pause the test if it fails')
Expand Down
6 changes: 4 additions & 2 deletions src/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ async function runTests (argParser) {
const testCafe = await createTestCafe(opts.hostname, port1, port2, opts.ssl, opts.dev);
const remoteBrowsers = await remotesWizard(testCafe, argParser.remoteCount, opts.qrCode);
const browsers = argParser.browsers.concat(remoteBrowsers);
const runner = testCafe.createRunner();
let failed = 0;
const runner = opts.live ? testCafe.createLiveModeRunner() : testCafe.createRunner();

let failed = 0;


runner.isCli = true;

Expand Down
8 changes: 8 additions & 0 deletions src/client/driver/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export default class Driver {
hammerhead.on(hammerhead.EVENTS.uncaughtJsError, err => this._onJsError(err));
hammerhead.on(hammerhead.EVENTS.unhandledRejection, err => this._onJsError(err));
hammerhead.on(hammerhead.EVENTS.consoleMethCalled, e => this._onConsoleMessage(e));

this.setCustomCommandHandlers(COMMAND_TYPE.unlockPage, () => this._unlockPageAfterTestIsDone());
}

set speed (val) {
Expand Down Expand Up @@ -165,6 +167,12 @@ export default class Driver {
return null;
}

_unlockPageAfterTestIsDone () {
disableRealEventsPreventing();

return Promise.resolve();
}

_failIfClientCodeExecutionIsInterrupted () {
// NOTE: ClientFunction should be used primarily for observation. We raise
// an error if the page was reloaded during ClientFunction execution.
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ export default class Compiler {
tests = tests.concat(await Promise.all(compileUnits));
}

testFileCompilers.forEach(c => c.cleanUp());
Compiler.cleanUp();

tests = flatten(tests).filter(test => !!test);

return tests;
}

static cleanUp () {
testFileCompilers.forEach(c => c.cleanUp());
}
}
2 changes: 2 additions & 0 deletions src/errors/runtime/message.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions src/live/bootstrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import path from 'path';
import Module from 'module';
import Bootstrapper from '../runner/bootstrapper';
import Compiler from '../compiler';

const originalRequire = Module.prototype.require;

class LiveModeBootstrapper extends Bootstrapper {
constructor (runner, browserConnectionGateway) {
super(browserConnectionGateway);

this.runner = runner;
}

_getTests () {
this._mockRequire();

return super._getTests()
.then(result => {
this._restoreRequire();

return result;
})
.catch(err => {
this._restoreRequire();

Compiler.cleanUp();

this.runner.setBootstrappingError(err);
});
}

_mockRequire () {
const runner = this.runner;

// NODE: we replace the `require` method to add required files to watcher
Module.prototype.require = function (filePath) {
const filename = Module._resolveFilename(filePath, this, false);

if (path.isAbsolute(filename) || /^\.\.?[/\\]/.test(filename))
runner.emit(runner.REQUIRED_MODULE_FOUND_EVENT, { filename });


return originalRequire.apply(this, arguments);
};
}

_restoreRequire () {
Module.prototype.require = originalRequire;
}
}

export default LiveModeBootstrapper;
165 changes: 165 additions & 0 deletions src/live/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import EventEmitter from 'events';
import FileWatcher from './file-watcher';
import Logger from './logger';
import process from 'process';
import keypress from 'keypress';
import Promise from 'pinkie';

const REQUIRED_MODULE_FOUND_EVENT = 'require-module-found';
const LOCK_KEY_PRESS_TIMEOUT = 1000;

class LiveModeController extends EventEmitter {
constructor (runner) {
super();

this.src = null;
this.running = false;
this.restarting = false;
this.watchingPaused = false;
this.stopping = false;
this.logger = new Logger();
this.runner = runner;
this.lockKeyPress = false;
}

init (files) {
this._prepareProcessStdin();
this._listenKeyPress();
this._initFileWatching(files);
this._listenTestRunnerEvents();
this._setRunning();

return Promise.resolve()
.then(() => this.logger.writeIntroMessage(files));
}

_toggleWatching () {
this.watchingPaused = !this.watchingPaused;

this.logger.writeToggleWatchingMessage(!this.watchingPaused);
}

_stop () {
if (!this.runner || !this.running) {
this.logger.writeNothingToStopMessage();

return Promise.resolve();
}

this.logger.writeStopRunningMessage();

return this.runner.stop()
.then(() => {
this.restarting = false;
this.running = false;
});
}

_restart () {
if (this.restarting || this.watchingPaused)
return Promise.resolve();

this.restarting = true;

if (this.running) {
return this._stop()
.then(() => this.logger.writeTestsFinishedMessage())
.then(() => this._runTests());
}

return this._runTests();
}

_exit () {
if (this.stopping)
return Promise.resolve();

this.logger.writeExitMessage();

this.stopping = true;

return this.runner ? this.runner.exit() : Promise.resolve();
}

_createFileWatcher (src) {
return new FileWatcher(src);
}

_prepareProcessStdin () {
if (process.stdout.isTTY)
process.stdin.setRawMode(true);
}

_listenKeyPress () {
// Listen commands
keypress(process.stdin);

process.stdin.on('keypress', (ch, key) => {
if (this.lockKeyPress)
return null;

this.lockKeyPress = true;

setTimeout(() => {
this.lockKeyPress = false;
}, LOCK_KEY_PRESS_TIMEOUT);

if (key && key.ctrl) {
switch (key.name) {
case 's':
return this._stop();
case 'r':
return this._restart();
case 'c':
return this._exit();
case 'w':
return this._toggleWatching();
}
}

return null;
});
}

_listenTestRunnerEvents () {
this.runner.on(this.runner.TEST_RUN_DONE_EVENT, e => {
this.running = false;

if (!this.restarting)
this.logger.writeTestsFinishedMessage();

if (e.err)
this.logger.err(`ERROR: ${e.err}`);
});

this.runner.on(this.runner.REQUIRED_MODULE_FOUND_EVENT, e => {
this.emit(REQUIRED_MODULE_FOUND_EVENT, e);
});
}

_initFileWatching (src) {
const fileWatcher = this._createFileWatcher(src);

this.on(REQUIRED_MODULE_FOUND_EVENT, e => fileWatcher.addFile(e.filename));

fileWatcher.on(fileWatcher.FILE_CHANGED_EVENT, () => this._runTests(true));
}

_setRunning () {
this.running = true;
this.restarting = false;
}

_runTests (sourceChanged) {
if (this.watchingPaused || this.running)
return Promise.resolve();

this._setRunning();

this.logger.writeRunTestsMessage(sourceChanged);

return this.runner.runTests();
}
}

export default LiveModeController;
60 changes: 60 additions & 0 deletions src/live/file-watcher/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import EventEmitter from 'events';
import fs from 'fs';
import ModulesGraph from './modules-graph';

const WATCH_LOCKED_TIMEOUT = 700;

export default class FileWatcher extends EventEmitter {
constructor (files) {
super();

this.FILE_CHANGED_EVENT = 'file-changed';

this.watchers = {};
this.lockedFiles = {};
this.modulesGraph = null;
this.lastChangedFiles = [];

files.forEach(f => this.addFile(f));
}

_onChanged (file) {
const cache = require.cache;

if (!this.modulesGraph) {
this.modulesGraph = new ModulesGraph();
this.modulesGraph.build(cache, Object.keys(this.watchers));
}
else {
this.lastChangedFiles.forEach(changedFile => this.modulesGraph.rebuildNode(cache, changedFile));
this.lastChangedFiles = [];
}

this.lastChangedFiles.push(file);
this.modulesGraph.clearParentsCache(cache, file);

this.emit(this.FILE_CHANGED_EVENT, { file });
}

_watch (file) {
if (this.lockedFiles[file])
return;

this.lockedFiles[file] = setTimeout(() => {
this._onChanged(file);

delete this.lockedFiles[file];
}, WATCH_LOCKED_TIMEOUT);
}

addFile (file) {
if (!this.watchers[file] && file.indexOf('node_modules') < 0) {
if (this.modulesGraph) {
this.lastChangedFiles.push(file);
this.modulesGraph.addNode(file, require.cache);
}

this.watchers[file] = fs.watch(file, () => this._watch(file));
}
}
}
Loading

0 comments on commit fa5dee2

Please sign in to comment.