UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

463 lines (405 loc) 17.6 kB
Object.defineProperty(exports, '__esModule', { value: true }); /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _Ansi2; function _Ansi() { return _Ansi2 = _interopRequireDefault(require('./Ansi')); } var _atom2; function _atom() { return _atom2 = require('atom'); } var _reactForAtom2; function _reactForAtom() { return _reactForAtom2 = require('react-for-atom'); } var _TestRunModel2; function _TestRunModel() { return _TestRunModel2 = _interopRequireDefault(require('./TestRunModel')); } var _uiTestRunnerPanel2; function _uiTestRunnerPanel() { return _uiTestRunnerPanel2 = _interopRequireDefault(require('./ui/TestRunnerPanel')); } var _TestSuiteModel2; function _TestSuiteModel() { return _TestSuiteModel2 = _interopRequireDefault(require('./TestSuiteModel')); } var _os2; function _os() { return _os2 = _interopRequireDefault(require('os')); } var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _commonsAtomConsumeFirstProvider2; function _commonsAtomConsumeFirstProvider() { return _commonsAtomConsumeFirstProvider2 = _interopRequireDefault(require('../../commons-atom/consumeFirstProvider')); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)(); var TestRunnerController = (function () { function TestRunnerController(state_, testRunners) { _classCallCheck(this, TestRunnerController); var state = state_; if (state == null) { state = {}; } this._state = { panelVisible: state.panelVisible }; // Bind Functions for use as callbacks; // TODO: Replace with property initializers when supported by Flow; this.clearOutput = this.clearOutput.bind(this); this.hidePanel = this.hidePanel.bind(this); this.stopTests = this.stopTests.bind(this); this._handleClickRun = this._handleClickRun.bind(this); this._onDebuggerCheckboxChanged = this._onDebuggerCheckboxChanged.bind(this); // TODO: Use the ReadOnlyTextBuffer class from nuclide-atom-text-editor when it is exported. this._buffer = new (_atom2 || _atom()).TextBuffer(); // Make `delete` a no-op to effectively create a read-only buffer. this._buffer.delete = function () {}; this._executionState = (_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default.ExecutionState.STOPPED; this._testRunners = testRunners; this._attachDebuggerBeforeRunning = false; this._runningTest = false; this._renderPanel(); } _createClass(TestRunnerController, [{ key: 'clearOutput', value: function clearOutput() { this._buffer.setText(''); this._path = undefined; this._run = undefined; this._stopListening(); this._testSuiteModel = undefined; this._renderPanel(); } }, { key: 'destroy', value: function destroy() { this._stopListening(); if (this._root) { (_reactForAtom2 || _reactForAtom()).ReactDOM.unmountComponentAtNode(this._root); this._root = null; } if (this._panel) { this._panel.destroy(); this._panel = null; } } }, { key: 'didUpdateTestRunners', value: function didUpdateTestRunners() { this._renderPanel(); } }, { key: 'hidePanel', value: function hidePanel() { this.stopTests(); this._state.panelVisible = false; if (this._panel) { this._panel.hide(); } } /** * @return A Promise that resolves when testing has succesfully started. */ }, { key: 'runTests', value: _asyncToGenerator(function* (path) { var _this = this; // If the test runner panel is not rendered yet, ensure it is rendered before continuing. if (this._testRunnerPanel == null || !this._state.panelVisible) { yield new Promise(function (resolve, reject) { _this.showPanel(resolve); }); } if (this._testRunnerPanel == null) { logger.error('Test runner panel did not render as expected. Aborting testing.'); return; } // Get selected test runner when Flow knows `this._testRunnerPanel` is defined. var selectedTestRunner = this._testRunnerPanel.getSelectedTestRunner(); if (!selectedTestRunner) { logger.warn('No test runner selected. Active test runners: ' + this._testRunners.size); return; } // 1. Use the `path` argument to this function // 2. Use `this._path` on the instance // 3. Let `testPath` be `undefined` so the path will be taken from the active `TextEditor` var testPath = path === undefined ? this._path : path; // If there's no path yet, get the path from the active `TextEditor`. if (testPath === undefined) { var activeTextEditor = atom.workspace.getActiveTextEditor(); if (!activeTextEditor) { logger.debug('Attempted to run tests with no active text editor.'); return; } // If the active text editor has no path, bail because there's nowhere to run tests. testPath = activeTextEditor.getPath(); } if (!testPath) { logger.warn('Attempted to run tests on an editor with no path.'); return; } // If the test runner is debuggable, and the user has checked the box, then we will launch // the debugger before running the tests. We do not handle killing the debugger. if (this._isSelectedTestRunnerDebuggable() && this._attachDebuggerBeforeRunning) { var isAttached = yield this._isDebuggerAttached(selectedTestRunner.debuggerProviderName); if (!isAttached) { yield selectedTestRunner.attachDebugger(testPath); } } // If the user has cancelled the test run while control was yielded, we should not run the test. if (!this._runningTest) { return; } this.clearOutput(); this._runTestRunnerServiceForPath(selectedTestRunner.runTest(testPath), testPath, selectedTestRunner.label); (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('testrunner-run-tests', { path: testPath, testRunner: selectedTestRunner.label }); // Set state as "Running" to give immediate feedback in the UI. this._setExecutionState((_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default.ExecutionState.RUNNING); this._path = testPath; this._renderPanel(); }) }, { key: '_isSelectedTestRunnerDebuggable', value: function _isSelectedTestRunnerDebuggable() { if (this._testRunnerPanel == null) { return false; } var selectedTestRunner = this._testRunnerPanel.getSelectedTestRunner(); return selectedTestRunner != null && selectedTestRunner.attachDebugger != null; } }, { key: '_isDebuggerAttached', value: _asyncToGenerator(function* (debuggerProviderName) { var debuggerService = yield (0, (_commonsAtomConsumeFirstProvider2 || _commonsAtomConsumeFirstProvider()).default)('nuclide-debugger.remote'); return debuggerService.isInDebuggingMode(debuggerProviderName); }) }, { key: 'stopTests', value: function stopTests() { // Resume the debugger if needed. atom.commands.dispatch(atom.views.getView(atom.workspace), 'nuclide-debugger:continue-debugging'); if (this._runTestsSubscription != null) { this._runTestsSubscription.dispose(); } this._stopListening(); // Respond in the UI immediately and assume the process is properly killed. this._setExecutionState((_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default.ExecutionState.STOPPED); } }, { key: 'serialize', value: function serialize() { return this._state; } }, { key: 'showPanel', value: function showPanel(didRender) { (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('testrunner-show-panel'); this._state.panelVisible = true; this._renderPanel(didRender); if (this._panel) { this._panel.show(); } } }, { key: 'togglePanel', value: function togglePanel() { (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('testrunner-hide-panel'); if (this._state.panelVisible) { this.hidePanel(); } else { this.showPanel(); } } }, { key: 'isVisible', value: function isVisible() { return this._state.panelVisible; } /** * Adds an end-of-line character to `text` and appends the resulting string to this controller's * text buffer. */ }, { key: '_appendToBuffer', value: function _appendToBuffer(text) { // `undo: 'skip'` disables the TextEditor's "undo system". Since the buffer is managed by this // class, an undo will never happen. Disable it when appending to prevent doing unneeded // bookkeeping. // // @see {@link https://atom.io/docs/api/v1.0.4/TextBuffer#instance-append|TextBuffer::append} this._buffer.append('' + text + (_os2 || _os()).default.EOL, { undo: 'skip' }); } }, { key: '_onDebuggerCheckboxChanged', value: function _onDebuggerCheckboxChanged(isChecked) { this._attachDebuggerBeforeRunning = isChecked; this._renderPanel(); } }, { key: '_handleClickRun', value: function _handleClickRun(event) { this._runningTest = true; // Don't pass a reference to `runTests` directly because the callback receives a mouse event as // its argument. `runTests` needs to be called with no arguments. this.runTests(); } }, { key: '_runTestRunnerServiceForPath', value: function _runTestRunnerServiceForPath(testRun, path, label) { var _this2 = this; var subscription = testRun.do(function (message) { switch (message.kind) { case 'summary': _this2._testSuiteModel = new (_TestSuiteModel2 || _TestSuiteModel()).default(message.summaryInfo); _this2._renderPanel(); break; case 'run-test': var testInfo = message.testInfo; if (_this2._testSuiteModel) { _this2._testSuiteModel.addTestRun(testInfo); } // If a test run throws an exception, the stack trace is returned in 'details'. // Append its entirety to the console. if (testInfo.hasOwnProperty('details') && testInfo.details !== '') { // $FlowFixMe(peterhal) _this2._appendToBuffer(testInfo.details); } // Append a PASS/FAIL message depending on whether the class has test failures. _this2._appendToBuffer((_TestRunModel2 || _TestRunModel()).default.formatStatusMessage(testInfo.name, testInfo.durationSecs, testInfo.status)); _this2._renderPanel(); break; case 'start': if (_this2._run) { _this2._run.start(); } break; case 'error': var error = message.error; if (_this2._run) { _this2._run.stop(); } if (error.code === 'ENOENT') { _this2._appendToBuffer((_Ansi2 || _Ansi()).default.YELLOW + 'Command \'' + error.path + '\' does not exist' + (_Ansi2 || _Ansi()).default.RESET); _this2._appendToBuffer((_Ansi2 || _Ansi()).default.YELLOW + 'Are you trying to run remotely?' + (_Ansi2 || _Ansi()).default.RESET); _this2._appendToBuffer((_Ansi2 || _Ansi()).default.YELLOW + 'Path: ' + path + (_Ansi2 || _Ansi()).default.RESET); } _this2._appendToBuffer((_Ansi2 || _Ansi()).default.RED + 'Original Error: ' + error.message + (_Ansi2 || _Ansi()).default.RESET); _this2._setExecutionState((_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default.ExecutionState.STOPPED); logger.error('Error running tests: "' + error.message + '"'); break; case 'stderr': // Color stderr output red in the console to distinguish it as error. _this2._appendToBuffer('' + (_Ansi2 || _Ansi()).default.RED + message.data + (_Ansi2 || _Ansi()).default.RESET); break; } }).finally(function () { _this2._stopListening(); _this2._setExecutionState((_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default.ExecutionState.STOPPED); }).subscribe(); this._run = new (_TestRunModel2 || _TestRunModel()).default(label, subscription.unsubscribe.bind(subscription)); } }, { key: '_setExecutionState', value: function _setExecutionState(executionState) { this._executionState = executionState; this._renderPanel(); } }, { key: '_renderPanel', value: function _renderPanel(didRender) { // Initialize and render the contents of the panel only if the hosting container is visible by // the user's choice. if (!this._state.panelVisible) { return; } var root = this._root; if (!root) { root = document.createElement('div'); this._root = root; } var progressValue = undefined; if (this._testSuiteModel && this._executionState === (_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default.ExecutionState.RUNNING) { progressValue = this._testSuiteModel.progressPercent(); } else { // If there is no running test suite, fill the progress bar because there is no progress to // track. progressValue = 100; } var component = (_reactForAtom2 || _reactForAtom()).ReactDOM.render((_reactForAtom2 || _reactForAtom()).React.createElement((_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default, { attachDebuggerBeforeRunning: this._attachDebuggerBeforeRunning, buffer: this._buffer, executionState: this._executionState, onClickClear: this.clearOutput, onClickClose: this.hidePanel, onClickRun: this._handleClickRun, onClickStop: this.stopTests, onDebuggerCheckboxChanged: this._onDebuggerCheckboxChanged, path: this._path, progressValue: progressValue, runDuration: this._run && this._run.getDuration(), // `TestRunnerPanel` expects an Array so it can render the test runners in a dropdown and // maintain a selected index. `Set` maintains items in insertion order, so the ordering is // determinate on each render. testRunners: Array.from(this._testRunners), testSuiteModel: this._testSuiteModel }), root, didRender); (0, (_assert2 || _assert()).default)(component instanceof (_uiTestRunnerPanel2 || _uiTestRunnerPanel()).default); this._testRunnerPanel = component; if (!this._panel) { this._panel = atom.workspace.addBottomPanel({ item: root, visible: this._state.panelVisible }); } } }, { key: '_stopListening', value: function _stopListening() { this._runningTest = false; if (this._run && this._run.dispose != null) { try { var dispose = this._run.dispose; this._run.dispose = null; this._run.stop(); (0, (_assert2 || _assert()).default)(this._run); // Calling `stop()` should never null the `_run` property. (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('testrunner-stop-tests', { testRunner: this._run.label }); dispose(); } catch (e) { (0, (_assert2 || _assert()).default)(this._run); // Nothing in the try block should ever null the `_run` property. // If the remote connection goes away, it won't be possible to stop tests. Log an error and // proceed as usual. logger.error('Error when stopping test run #\'' + this._run.label + ': ' + e); } } } }]); return TestRunnerController; })(); exports.default = TestRunnerController; module.exports = exports.default;