├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .travis.yml ├── .yarnrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── appveyor.yml ├── decls ├── atom.js.flow └── jasmine.js.flow ├── keymaps └── tester.json ├── lib ├── commands.js ├── decorate-manager.js ├── helpers.js ├── main.js ├── redux │ ├── actions.js │ ├── epics │ │ ├── clear.js │ │ ├── error.js │ │ ├── goToNextTest.js │ │ ├── goToPreviousTest.js │ │ ├── index.js │ │ ├── setCurrentMessage.js │ │ ├── setEditor.js │ │ ├── setFilter.js │ │ ├── setSortBy.js │ │ ├── startTest.js │ │ ├── stopTest.js │ │ ├── test.js │ │ ├── testLast.js │ │ ├── testProject.js │ │ ├── transformMessages.js │ │ ├── updateEditor.js │ │ ├── updateMessages.js │ │ └── updateOutput.js │ └── reducers.js ├── types.js └── views │ ├── ConsoleOutputView.js │ ├── ResultView.js │ └── StatusBarTile.js ├── menus └── tester.json ├── package.json ├── resources ├── console-output.png ├── gutter-markers.png ├── inline-error.png ├── preview.gif ├── result-view.png └── status-bar.png ├── spec ├── commands-spec.js ├── common.js ├── decorate-manager-spec.js ├── fixtures │ └── test.txt ├── helpers-spec.js ├── main-spec.js ├── redux │ ├── actions-spec.js │ ├── epics │ │ ├── clear-spec.js │ │ ├── error-spec.js │ │ ├── goToNextTest-spec.js │ │ ├── goToPreviousTest-spec.js │ │ ├── setCurrentMessage-spec.js │ │ ├── setEditor-spec.js │ │ ├── setFilter-spec.js │ │ ├── setSortBy-spec.js │ │ ├── startTest-spec.js │ │ ├── stopTest-spec.js │ │ ├── test-spec.js │ │ ├── testLast-spec.js │ │ ├── testProject-spec.js │ │ ├── transformMessages-spec.js │ │ ├── updateEditor-spec.js │ │ ├── updateMessages-spec.js │ │ └── updateOutput-spec.js │ └── reducers-spec.js └── views │ ├── ConsoleOutputView-spec.js │ ├── ResultView-spec.js │ └── StatusBar-spec.js ├── styles └── tester.less ├── test ├── all-in-one.js └── sample-mocha.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | decls 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "react", 6 | "jsx-a11y", 7 | "import", 8 | "flowtype" 9 | ], 10 | "env": { 11 | "atomtest": true, 12 | "es6": true, 13 | "node": true, 14 | "jasmine": true 15 | }, 16 | "globals": { 17 | "atom": true, 18 | "document": true, 19 | "window": true 20 | }, 21 | "rules": { 22 | "import/extensions": 0, 23 | "import/no-unresolved": 0, 24 | "import/no-extraneous-dependencies": 0, 25 | "no-restricted-syntax": 0, 26 | "strict": 0, 27 | "class-methods-use-this": 0, 28 | "no-param-reassign": 0, 29 | "no-useless-escape": 0, 30 | "prefer-rest-params": 0, 31 | "react/jsx-filename-extension": 0, 32 | "react/sort-comp": 0, 33 | "no-return-assign": 0, 34 | "no-unused-vars": ["error", { "varsIgnorePattern": "^atom$" }], 35 | "no-underscore-dangle": 0, 36 | "no-plusplus": 0, 37 | "consistent-return": 0, 38 | "global-require": 0, 39 | "max-len": [2, 150], 40 | "no-nested-ternary": 0, 41 | "no-template-curly-in-string": 0, 42 | "no-console": [1, { "allow": ["warn", "error"] }] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | decls 7 | 8 | [options] 9 | module.system=node 10 | unsafe.enable_getters_and_setters=true 11 | suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Yarn 40 | yarn-error.log 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | 10 | branches: 11 | only: 12 | - master 13 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.0 2 | * :gift: add setting for state column style (text or icon) #21 3 | * :gift: re-sizable columns in results view 4 | * :gift: serialize columns width 5 | * :bug: fix sort if wrong message format 6 | 7 | ## 1.3.2 8 | * :bug: fix #16 9 | * :bug: fix update editor epic 10 | 11 | ## 1.3.1 12 | * :bug: fix error format if message is empty (mocha >3.4) 13 | * :bug: fix test with windows path 14 | * :bug: fix padding for console view 15 | 16 | ## 1.3.0 17 | * :gift: save tester state for each project even after restart (serialize panels, results, output etc.) 18 | * :gift: merge test results per file that not remove another file results 19 | * :gift: add 'current file only' filter 20 | * :gift: add 'Clear' button 21 | * :gift: add settings for status bat position and priority 22 | * :gift: add total tests and total time indicators to results view 23 | * :gift: improve soft wrap for errors in results view 24 | * :gift: add copy context menu to console output view 25 | * :gift: additional args are also used for file test now 26 | * :art: rewrite package with redux and redux-observable to use state 27 | * :racehorse: improve performance and code quality 28 | * :arrow_up: upgrade dependencies 29 | 30 | ## 1.2.1 31 | * :bug: clear gutter after each test run #17 32 | 33 | ## 1.2.0 34 | * :gift: add serialization 35 | 36 | ## 1.1.1 37 | * :bug: fix #11 38 | 39 | ## 1.1.0 40 | * :gift: add `test-last` command 41 | 42 | ## 1.0.1 43 | * :bug: fix #10 44 | 45 | ## 1.0.0 46 | * add "Run all project tests" feature :tada: 47 | * add "Results View" :tada: 48 | * rewrite all view with etch (remove jquery) :racehorse: 49 | * upgrade dependencies :arrow_up: 50 | 51 | ## 0.4.6 52 | * add unknown state with inline messages 53 | 54 | ## 0.4.5 55 | * add 'read more' function on click for inline messages 56 | 57 | ## 0.4.4 58 | * set default inline message position to tail 59 | * fix tail styles 60 | * fix inline error message format 61 | 62 | ## 0.4.3 63 | * add tail option for inline messages 64 | 65 | ## 0.4.2 66 | * fix scroll after test 67 | * add scroll console output to bottom setting 68 | * add scroll to bottom and top button to view 69 | 70 | ## 0.4.1 71 | * fix output view resize limitations 72 | 73 | ## 0.4.0 74 | * remove 'testOnChagne' feature 75 | * make output view resizable 76 | * fix errors if test finished and editor was closed 77 | 78 | ## 0.3.4 79 | * add loading spinner to output view 80 | 81 | ## 0.3.2 82 | * fix #6 83 | 84 | ## 0.3.1 85 | * add experimental feature - test all opened after any save 86 | * fix status tiny 87 | * improve pop-up notifications 88 | 89 | ## 0.3.0 90 | * add inline error messages 91 | 92 | ## 0.2.14 93 | * fix statusBar position 94 | 95 | ## 0.2.13 96 | * better tiny behavior 97 | 98 | ## 0.2.12 99 | * show info with test results if test editor is not active 100 | 101 | ## 0.2.11 102 | * add setting "test on save" 103 | * remember last test results and show it on tab switch 104 | 105 | ## 0.2.10 106 | * escape html in console output 107 | * set right colors for console output 108 | 109 | ## 0.2.9 110 | * add stop functionality 111 | 112 | ## v0.2.8 113 | * fix scopes path for Windows 114 | * add windows build checks 115 | 116 | ## v0.2.7 117 | * add config to convert console output from ansi to html 118 | 119 | ## v0.2.6 120 | * Merge pull request #1 from hotchpotch/changetoggle_command 121 | 122 | ## v0.2.5 123 | * fix test on change 124 | 125 | ## v0.2.4 126 | * disable test on change by default 127 | 128 | ## v0.2.3 129 | * fix destroy functionality 130 | 131 | ## v0.2.2 132 | * fix regexp handling 133 | * alway show status bar 134 | 135 | ## v0.2.1 136 | * fix scopes pattern 137 | * fix locking 138 | 139 | ## v0.2.0 - First Functional release 140 | * Added Interactive Tester 141 | * Added First integration with [tester-mocha](https://github.com/yacut/tester-mocha) provider 142 | * Useful IDE based Feedback 143 | * Session based test watching 144 | 145 | ## v0.1.0 - First Release 146 | * Just block the name in the atom registry 147 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Basic Steps 4 | 5 | If you would like to contribute enhancements or fixes, please do the following: 6 | 7 | 1. Fork the package repository 8 | 2. Run `npm install` to setup all dependencies 9 | 3. Hack on a separate topic branch created from the latest `master`. Changes to 10 | the package code should be made to the files in the `lib` directory. 11 | 4. Check for lint errors with `npm test`. 12 | 5. Commit the changes under `lib` and push the topic branch 13 | 6. Make a pull request 14 | 15 | ## Styleguides 16 | 17 | ### Git Commit Messages 18 | 19 | * Use the present tense ("Add feature" not "Added feature") 20 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 21 | * Limit the first line to 72 characters or less 22 | * Reference issues and pull requests liberally 23 | * When only changing documentation, include `[ci skip]` in the commit description 24 | * Consider starting the commit message with an applicable emoji: 25 | * :gift: `:gift:` when adding a new feature 26 | * :art: `:art:` when improving the format/structure of the code 27 | * :racehorse: `:racehorse:` when improving performance 28 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks 29 | * :memo: `:memo:` when writing docs 30 | * :penguin: `:penguin:` when fixing something on Linux 31 | * :apple: `:apple:` when fixing something on macOS 32 | * :checkered_flag: `:checkered_flag:` when fixing something on Windows 33 | * :bug: `:bug:` when fixing a bug 34 | * :fire: `:fire:` when removing code or files 35 | * :green_heart: `:green_heart:` when fixing the CI build 36 | * :white_check_mark: `:white_check_mark:` when adding tests 37 | * :lock: `:lock:` when dealing with security 38 | * :arrow_up: `:arrow_up:` when upgrading dependencies 39 | * :arrow_down: `:arrow_down:` when downgrading dependencies 40 | * :shirt: `:shirt:` when removing linter warnings 41 | 42 | ### JavaScript Styleguide 43 | 44 | Please note that modifications should follow these coding guidelines: 45 | 46 | * Indent is 2 spaces 47 | * Code should pass the `eslint` linter 48 | * Vertical whitespace helps readability, don’t be afraid to use it 49 | 50 | ## Releasing 51 | 52 | Project members with push access to the repository also have the permissions 53 | needed to release a new version. If there have been changes to the project and 54 | the team decides it is time for a new release, the process is to: 55 | 56 | 1. Update `CHANGELOG.md` with the planned version number and a short bulleted 57 | list of major changes. Include pull request numbers if applicable. 58 | 2. Commit the changelog and any `lib/` changes to master. 59 | 3. Publish a new version with `apm publish {major|minor|patch}`, using semver to 60 | decide what type of version should be released. 61 | 4. `apm` will then automatically: 62 | * Update `package.json` with the new version number 63 | * Commit the changed `package.json` to master 64 | * Create a git tag for the new version and push it to GitHub 65 | * Publish the package to the Atom package manager 66 | 67 | Thank you for helping out! 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 yacut 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tester 2 | 3 | [![Donate Bitcoin](https://img.shields.io/badge/donate-bitcoin-orange.svg)](https://blockchain.info/payment_request?address=1Ndg2GN1r4UfyqBtAUgLmVVjv8a9xYokU5&message=I+like+your+GitHub+Project!) 4 | [![Build Status](https://travis-ci.org/yacut/tester.svg)](https://travis-ci.org/yacut/tester) 5 | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/yacut/tester?svg=true)](https://ci.appveyor.com/api/projects/status/github/yacut/tester) 6 | [![APM Version](https://img.shields.io/apm/v/tester.svg)](https://atom.io/packages/tester) 7 | [![APM Downloads](https://img.shields.io/apm/dm/tester.svg)](https://atom.io/packages/tester) 8 | [![GitHub stars](https://img.shields.io/github/stars/yacut/tester.svg)](https://github.com/yacut/tester/stargazers) 9 | [![GitHub issues](https://img.shields.io/github/issues/yacut/tester.svg)](https://github.com/yacut/tester/issues) 10 | [![Dependency Status](https://david-dm.org/yacut/tester.svg)](https://david-dm.org/yacut/tester) 11 | 12 | Tester is a test runner for the hackable [Atom Editor](http://atom.io). Additionally, you need to install a specific tester provider for your test framework. You will find a full list below in the [Known provider](#known-providers) section. 13 | 14 | ![Preview](https://raw.githubusercontent.com/yacut/tester/master/resources/preview.gif) 15 | 16 | ### Base Features 17 | - IDE based Feedback 18 | 19 | - Gutter test result markers 20 | 21 | ![gutter-markers](https://raw.githubusercontent.com/yacut/tester/master/resources/gutter-markers.png) 22 | 23 | - In-line error messages 24 | 25 | ![inline-error](https://raw.githubusercontent.com/yacut/tester/master/resources/inline-error.png) 26 | 27 | - Console test output 28 | 29 | ![console-output](https://raw.githubusercontent.com/yacut/tester/master/resources/console-output.png) 30 | 31 | - Test result view 32 | 33 | ![result-view](https://raw.githubusercontent.com/yacut/tester/master/resources/result-view.png) 34 | 35 | - Session based test watching 36 | - Test file on open 37 | - Test file after save 38 | - Test project 39 | 40 | - Supported test frameworks (for now): 41 | * [Mocha](https://mochajs.org/) 42 | * [Jest](https://github.com/facebook/jest) 43 | * [PHPUnit](https://phpunit.de/) 44 | 45 | #### How to / Installation 46 | 47 | You can install through the CLI by doing: 48 | 49 | ``` 50 | $ apm install tester 51 | ``` 52 | 53 | Or you can install from Settings view by searching for `Tester`. 54 | 55 | ### Known providers 56 | 57 | * [Mocha](https://atom.io/packages/tester-mocha) test runner. 58 | * [Jest](https://atom.io/packages/tester-jest) test runner. 59 | * [PHPUnit](https://atom.io/packages/tester-phpunit) test runner. 60 | 61 | ### Tester API 62 | 63 | #### Example 64 | 65 | Declare the provider callback in the `package.json`. 66 | 67 | ```js 68 | "providedServices": { 69 | "tester": { 70 | "versions": { 71 | "1.0.0": "provideTester" 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | Define the provider callback in `lib/main.js`. 78 | 79 | ```js 80 | export function provideTester() { 81 | return { 82 | name: 'tester-name', 83 | options: {}, 84 | scopes: ['**/test/*.js', '**/*spec.js'], 85 | test(textEditor/* or null to run project tests*/, additionalArgs/* from results views*/) { 86 | // Note, a Promise may be returned as well! 87 | return { 88 | messages: [ 89 | { 90 | duration: 1, // duration in ms 91 | error: { 92 | name: 'optional error object', 93 | message: 'something went wrong', 94 | actual: 'optional actual result', // can be an object 95 | expected: 'optional expected result', // can be an object 96 | operator: 'optional operator', 97 | }, 98 | filePath: 'file path to highlight', 99 | lineNumber: 1, // line number to highlight 100 | state: 'failed', // 'passed' | 'failed' | 'skipped', 101 | title: 'some test title', 102 | } 103 | ], 104 | output: 'tester console output' 105 | }; 106 | }, 107 | stop() { 108 | // stop tester if needed 109 | } 110 | }; 111 | } 112 | ``` 113 | 114 | ### Inspiration 115 | 116 | I'd like to give a shout out to [Wallaby.js](https://wallabyjs.com/), which is a significantly more comprehensive and covers a lot more editors, if this extension interests you - check out that too. 117 | 118 | ### Contribute 119 | 120 | Stick to imposed code style: 121 | 122 | * `$ npm install` 123 | * `$ npm test` 124 | 125 | ### Roadmap 126 | 127 | - [x] add unknown status for test which not ran 128 | - [x] replace all views with react components (etch) 129 | - [x] add table view with results similar to nuclide diagnostics 130 | - [x] sort data by column head click 131 | - [x] quick set additional args for test runner 132 | - [x] merge results from each test runner 133 | - [x] re-sizable columns 134 | - [ ] side by side diff view for expectations 135 | - [x] go to next/previous test commands 136 | - [x] add run all project tests command 137 | - [x] implement [Redux](https://github.com/reactjs/redux) and [redux-observable](https://github.com/redux-observable/redux-observable) for result view 138 | - [x] [serialization](http://flight-manual.atom.io/hacking-atom/sections/package-active-editor-info/#serialization) 139 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | platform: x64 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | skip_tags: true 10 | 11 | environment: 12 | APM_TEST_PACKAGES: 13 | 14 | matrix: 15 | - ATOM_CHANNEL: stable 16 | 17 | install: 18 | - ps: Install-Product node 5 19 | 20 | build_script: 21 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) 22 | 23 | test: off 24 | deploy: off 25 | -------------------------------------------------------------------------------- /decls/atom.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // Global Atom Object 4 | declare var atom: Object; 5 | 6 | declare module 'atom' { 7 | declare var Point: any; 8 | declare var Range: any; 9 | declare var Panel: any; 10 | declare var TextEditor: any; 11 | declare var TextBuffer: any; 12 | declare var BufferMarker: any; 13 | declare var TextEditorGutter: any; 14 | declare var TextEditorMarker: any; 15 | declare var Emitter: any; 16 | declare var Disposable: any; 17 | declare var CompositeDisposable: any; 18 | } 19 | -------------------------------------------------------------------------------- /decls/jasmine.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | declare var jasmine: Object; 3 | declare function it(name: string, callback: Function): ?Promise; 4 | declare function fit(name: string, callback: Function): ?Promise; 5 | declare function spyOn(object: Object, property: string): Object; 6 | declare function expect(value: any): Object; 7 | declare function describe(name: string, callback: Function): ?Promise; 8 | declare function xdescribe(name: string, callback: Function): ?Promise; 9 | declare function fdescribe(name: string, callback: Function): ?Promise; 10 | declare function beforeEach(callback: Function): ?Promise; 11 | declare function afterEach(callback: Function): ?Promise; 12 | declare function waitsFor(callback: Function): ?Promise; 13 | -------------------------------------------------------------------------------- /keymaps/tester.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-workspace": { 3 | "ctrl-alt-t": "tester:toggle-tester-result", 4 | "ctrl-alt-shift-p": "tester:test-project" 5 | }, 6 | "atom-pane .tester-view atom-text-editor": { 7 | "enter": "tester:test-project" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/commands.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | // @flow 4 | 5 | import { CompositeDisposable, Emitter } from 'atom'; 6 | import type { Disposable } from 'atom'; 7 | 8 | export default class Commands { 9 | emitter: Emitter; 10 | subscriptions: CompositeDisposable; 11 | 12 | constructor() { 13 | this.emitter = new Emitter(); 14 | this.subscriptions = new CompositeDisposable(); 15 | 16 | this.subscriptions.add(this.emitter); 17 | this.subscriptions.add(atom.commands.add('atom-text-editor:not([mini])', { 18 | 'tester:test': () => this.test(), 19 | 'tester:toggle-full-size-inline-errors': () => this.toggleFullSizeInlineErrors(), 20 | })); 21 | this.subscriptions.add(atom.commands.add('atom-workspace', { 22 | 'tester:test-project': () => this.testProject(), 23 | 'tester:test-last': () => this.testLast(), 24 | 'tester:stop': () => this.stop(), 25 | 'tester:clear': () => this.clear(), 26 | 'tester:toggle-tester-output': () => this.toggleTesterOutput(), 27 | 'tester:toggle-tester-result': () => this.toggleTesterResultView(), 28 | 'tester:go-to-next-test': () => this.goToNextTest(), 29 | 'tester:go-to-previous-test': () => this.goToPreviousTest(), 30 | })); 31 | } 32 | toggleTesterOutput() { 33 | this.emitter.emit('should-toggle-tester-output'); 34 | } 35 | toggleTesterResultView() { 36 | this.emitter.emit('should-toggle-tester-result-view'); 37 | } 38 | test() { 39 | this.emitter.emit('should-test'); 40 | } 41 | testLast() { 42 | this.emitter.emit('should-test-last'); 43 | } 44 | testProject() { 45 | this.emitter.emit('should-test-project'); 46 | } 47 | stop() { 48 | this.emitter.emit('should-stop'); 49 | } 50 | clear() { 51 | this.emitter.emit('should-clear'); 52 | } 53 | goToNextTest() { 54 | this.emitter.emit('should-go-to-next-test'); 55 | } 56 | goToPreviousTest() { 57 | this.emitter.emit('should-go-to-previous-test'); 58 | } 59 | onShouldToggleTesterOutput(callback : Function) : Disposable { 60 | return this.emitter.on('should-toggle-tester-output', callback); 61 | } 62 | onShouldToggleTesterResultView(callback : Function) : Disposable { 63 | return this.emitter.on('should-toggle-tester-result-view', callback); 64 | } 65 | onShouldTest(callback : Function) : Disposable { 66 | return this.emitter.on('should-test', callback); 67 | } 68 | onShouldTestLast(callback : Function) : Disposable { 69 | return this.emitter.on('should-test-last', callback); 70 | } 71 | onShouldTestProject(callback : Function) : Disposable { 72 | return this.emitter.on('should-test-project', callback); 73 | } 74 | onShouldStop(callback : Function) : Disposable { 75 | return this.emitter.on('should-stop', callback); 76 | } 77 | onShouldClear(callback : Function) : Disposable { 78 | return this.emitter.on('should-clear', callback); 79 | } 80 | onShouldGoToNextTest(callback: Function): Disposable { 81 | return this.emitter.on('should-go-to-next-test', callback); 82 | } 83 | onShouldGoToPreviousTest(callback: Function): Disposable { 84 | return this.emitter.on('should-go-to-previous-test', callback); 85 | } 86 | toggleFullSizeInlineErrors() { 87 | Array.from(document.getElementsByClassName('tester-inline-message')).forEach(item => item.click()); 88 | } 89 | dispose() { 90 | this.subscriptions.dispose(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/decorate-manager.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | // @flow 4 | import type { TextEditor } from 'atom'; 5 | import { Observable } from 'rxjs'; 6 | import { normalizeString } from './helpers'; 7 | import type { Message } from './types'; 8 | 9 | export function handleGutter(textEditor: TextEditor) { 10 | return new Promise((resolve) => { 11 | if (!textEditor || !atom.workspace.isTextEditor(textEditor)) { 12 | return resolve(); 13 | } 14 | 15 | let gutter = textEditor.gutterWithName('tester'); 16 | if (gutter && !atom.config.get('tester.gutterEnabled')) { 17 | try { 18 | gutter.destroy(); 19 | gutter = null; 20 | } catch (error) { 21 | console.error('Tester: ', error); 22 | } 23 | } 24 | 25 | if (!gutter && atom.config.get('tester.gutterEnabled')) { 26 | const position = atom.config.get('tester.gutterPosition'); 27 | gutter = textEditor.addGutter({ 28 | name: 'tester', 29 | priority: position === 'Left' ? -100 : 100, 30 | }); 31 | } 32 | resolve(); 33 | }); 34 | } 35 | 36 | export function setInlineMessages(editor: TextEditor, messages: Array): Promise { 37 | if (!atom.config.get('tester.showInlineError')) { 38 | return Promise.resolve(); 39 | } 40 | if (!editor || !messages) { 41 | return Promise.resolve(); 42 | } 43 | const currentFilePath = editor.getPath(); 44 | return Observable.from(messages) 45 | .filter((message: Message) => currentFilePath === message.filePath) 46 | .do((message) => { 47 | const content = normalizeString(message); 48 | if (!content) { 49 | return; 50 | } 51 | const rowRange = editor.getBuffer().rangeForRow(message.lineNumber); 52 | const marker = editor.markBufferRange( 53 | rowRange, 54 | { invalidate: 'never' }, 55 | ); 56 | 57 | const inlineMessage = document.createElement('div'); 58 | inlineMessage.classList.add('inline-block', 'tester-inline-message'); 59 | inlineMessage.innerHTML = content; 60 | inlineMessage.onclick = () => inlineMessage.classList.toggle('full-size'); 61 | 62 | const inlineErrorPosition = atom.config.get('tester.inlineErrorPosition'); 63 | if (inlineErrorPosition === 'tail') { 64 | editor.decorateMarker(marker, { 65 | type: 'overlay', 66 | class: 'tester-inline-message-tail', 67 | item: inlineMessage, 68 | }); 69 | } else { 70 | editor.decorateMarker(marker, { 71 | type: 'block', 72 | position: inlineErrorPosition, 73 | item: inlineMessage, 74 | }); 75 | } 76 | 77 | if (!editor.testerMarkers) { 78 | editor.testerMarkers = []; 79 | } 80 | editor.testerMarkers.push(marker); 81 | }) 82 | .toPromise(); 83 | } 84 | 85 | export function clearInlineMessages(editor: TextEditor): Promise { 86 | if (!editor || !editor.testerMarkers) { 87 | return Promise.resolve(); 88 | } 89 | return Observable.from(editor.testerMarkers) 90 | .filter(marker => !marker.isDestroyed()) 91 | .do((marker) => { 92 | marker.destroy(); 93 | }) 94 | .finally(() => { 95 | editor.testerMarkers = []; 96 | }) 97 | .toPromise(); 98 | } 99 | 100 | export function clearDecoratedGutter(editor: ?TextEditor): Promise { 101 | if (!editor) { 102 | return Promise.resolve(); 103 | } 104 | 105 | return Observable.from(editor.getDecorations({ gutterName: 'tester', type: 'gutter' })) 106 | .filter(decoration => !decoration.isDestroyed()) 107 | .do((decoration) => { 108 | decoration.getMarker().destroy(); 109 | decoration.destroy(); 110 | }) 111 | .toPromise(); 112 | } 113 | 114 | export function decorateGutter(editor: ?TextEditor, messages: Array): Promise { 115 | if (!atom.config.get('tester.gutterEnabled')) { 116 | return Promise.resolve(); 117 | } 118 | if (!editor || !messages) { 119 | return Promise.resolve(); 120 | } 121 | const gutter = editor.gutterWithName('tester'); 122 | if (!gutter) { 123 | return Promise.resolve(); 124 | } 125 | const currentFilePath = editor.getPath(); 126 | return Observable.from(messages) 127 | .filter((message: Message) => currentFilePath === message.filePath) 128 | .do((message: Message) => { 129 | const tooltipDuration = document.createElement('span'); 130 | tooltipDuration.classList.add('highlight-info'); 131 | if (message.duration) { 132 | tooltipDuration.textContent = `${message.duration}ms`; 133 | } 134 | const tooltipTesterName = document.createElement('span'); 135 | tooltipTesterName.textContent = 'Tester'; 136 | tooltipTesterName.classList.add('highlight'); 137 | 138 | const tooltipTesterState = document.createElement('span'); 139 | if (message.state === 'passed') { 140 | tooltipTesterState.classList.add('highlight-success'); 141 | } else if (message.state === 'failed') { 142 | tooltipTesterState.classList.add('highlight-error'); 143 | } else if (message.state === 'skipped') { 144 | tooltipTesterState.classList.add('highlight-warning'); 145 | } else { 146 | tooltipTesterState.classList.add('highlight-info'); 147 | } 148 | const tooltip = document.createElement('span'); 149 | tooltip.appendChild(tooltipTesterName); 150 | tooltip.appendChild(tooltipTesterState); 151 | tooltip.appendChild(tooltipDuration); 152 | tooltip.classList.add('inline-block', 'tester-tooltip-title'); 153 | const item = document.createElement('span'); 154 | item.classList.add('block', 'tester-gutter', 'tester-highlight', `${message.state}`); 155 | atom.tooltips.add(item, { 156 | title: tooltip.innerHTML, 157 | placement: 'right', 158 | delay: { show: 100, hide: 100 }, 159 | }); 160 | if (editor && gutter) { 161 | const marker = editor.getBuffer() 162 | .markRange([[message.lineNumber, 0], [message.lineNumber, 0]], { invalidate: 'inside' }); 163 | gutter.decorateMarker(marker, { 164 | class: 'tester-row', 165 | item, 166 | }); 167 | } 168 | }) 169 | .toPromise(); 170 | } 171 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | // @flow 4 | 5 | import { Disposable } from 'atom'; 6 | import AnsiToHtml from 'ansi-to-html'; 7 | import { Observable } from 'rxjs'; 8 | import type { Message, SubscribeFunction } from './types'; 9 | 10 | const entityMap = { 11 | '&': '&', 12 | '<': '<', 13 | '>': '>', 14 | '"': '"', 15 | "'": ''', 16 | '/': '/', 17 | '`': '`', 18 | '=': '=', 19 | }; 20 | 21 | let ansiToHtml; 22 | export const $activated = '__$sb_tester_activated'; 23 | export const $requestLatest = '__$sb_tester_request_latest'; 24 | export const $requestLastReceived = '__$sb_tester_request_last_received'; 25 | 26 | export function subscriptiveObserve( 27 | object : Object, 28 | eventName : string, 29 | callback : Function) : Disposable { 30 | let subscription = null; 31 | const eventSubscription = object.observe(eventName, function observeProps(props) { 32 | if (subscription) { 33 | subscription.dispose(); 34 | } 35 | subscription = callback.call(this, props); 36 | }); 37 | 38 | return new Disposable(() => { 39 | eventSubscription.dispose(); 40 | if (subscription) { 41 | subscription.dispose(); 42 | } 43 | }); 44 | } 45 | 46 | export function convertWindowsPathToUnixPath(path :string) :string { 47 | if (process.platform.match(/^win/)) { 48 | path = path.replace(/[\\]+/g, '/'); 49 | } 50 | return path; 51 | } 52 | 53 | export function convertAnsiStringToHtml(string :string) { 54 | // dark background colors 55 | let ansiToHtmlOptions = { 56 | fg: '#FFF', 57 | bg: '#000', 58 | }; 59 | // light background colors 60 | if (atom.themes.getActiveThemeNames().some(themeName => themeName.includes('light'))) { 61 | ansiToHtmlOptions = { 62 | fg: '#000', 63 | bg: '#FFF', 64 | }; 65 | } 66 | ansiToHtml = new AnsiToHtml(ansiToHtmlOptions); 67 | return ansiToHtml.toHtml(string); 68 | } 69 | 70 | export function escapeHtml(string :string) { 71 | return String(string).replace(/[&<>"'`=\/]/g, s => entityMap[s]); 72 | } 73 | 74 | export function normalizeString(message: Message) { 75 | if (!message || !message.error) { 76 | return ''; 77 | } 78 | 79 | const error = message.error; 80 | let content = ''; 81 | if (error.message && error.message !== '') { 82 | content = error.message; 83 | } else if (error.name && error.operator && error.actual && error.expected) { 84 | content = `${error.name}: ${JSON.stringify(error.actual)} ${error.operator || ''} ${JSON.stringify(error.expected)}`; 85 | } 86 | content = escapeHtml(content); 87 | if (atom.config.get('tester.ansiToHtml')) { 88 | content = convertAnsiStringToHtml(content); 89 | } 90 | 91 | return content; 92 | } 93 | 94 | export function observableFromSubscribeFunction( 95 | fn: SubscribeFunction, 96 | ): Observable { 97 | return Observable.create((observer) => { 98 | const disposable = fn(observer.next.bind(observer)); 99 | return () => { 100 | disposable.dispose(); 101 | }; 102 | }); 103 | } 104 | 105 | export function sort(messages: Array, key: string, desc: ?boolean): Array { 106 | if (!messages || messages.constructor !== Array || messages.length === 0) { 107 | return []; 108 | } 109 | if (key === '') { 110 | return messages; 111 | } 112 | return messages.sort((current, next) => { 113 | const currentValue = (key === 'error') ? (current.error ? current.error.message : '') : current[key]; 114 | const nextValue = (key === 'error') ? (next.error ? next.error.message : '') : next[key]; 115 | 116 | if (currentValue < nextValue) { 117 | return desc ? 1 : -1; 118 | } 119 | if (currentValue > nextValue) { 120 | return desc ? -1 : 1; 121 | } 122 | return 0; 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow */ 4 | 5 | import { Disposable, CompositeDisposable } from 'atom'; 6 | 7 | import 'rxjs'; 8 | import { createStore, applyMiddleware } from 'redux'; 9 | import { createEpicMiddleware } from 'redux-observable'; 10 | import reducers from './redux/reducers'; 11 | import epics from './redux/epics'; 12 | import { 13 | addTesterAction, 14 | clearAction, 15 | goToNextTestAction, 16 | goToPreviousTestAction, 17 | removeTesterAction, 18 | setAdditionalArgsAction, 19 | setEditorAction, 20 | setFilterAction, 21 | setResultViewColumns, 22 | setSortByAction, 23 | stopTestAction, 24 | testAction, 25 | testLastAction, 26 | testProjectAction, 27 | } from './redux/actions'; 28 | 29 | import Commands from './commands'; 30 | import ConsoleOutputView from './views/ConsoleOutputView'; 31 | import ResultView from './views/ResultView'; 32 | import StatusBarTile from './views/StatusBarTile'; 33 | import type { Store, TesterSorter, Column, WidthMap } from './types'; 34 | 35 | class Tester { 36 | commands: Commands; 37 | subscriptions: CompositeDisposable; 38 | statusBarTile: Object; 39 | consoleView: ?ConsoleOutputView; 40 | resultView: ?ResultView; 41 | deserializeResultView: Function; 42 | deserializeConsoleOutput: Function; 43 | store: Store; 44 | getStore: Function; 45 | 46 | constructor(initialState) { 47 | this.commands = new Commands(); 48 | this.subscriptions = new CompositeDisposable(); 49 | this.subscriptions.add(this.commands); 50 | const self = this; 51 | 52 | this.getStore = () => { 53 | if (self.store == null) { 54 | initialState = Object.assign({ 55 | additionalArgs: '', 56 | currentFileOnly: false, 57 | currentMessage: null, 58 | editor: null, 59 | isProjectTest: false, 60 | messages: [], 61 | output: '', 62 | rawMessages: [], 63 | sorter: { key: '', desc: false }, 64 | testers: [], 65 | testRunning: false, 66 | }, initialState); 67 | 68 | self.store = createStore( 69 | reducers, 70 | initialState, 71 | applyMiddleware(createEpicMiddleware(epics)), 72 | ); 73 | 74 | self.store.subscribe(async () => { 75 | const currentState = self.getStore().getState(); 76 | if (self.statusBarTile) { 77 | await self.statusBarTile.update(currentState); 78 | } 79 | if (this.resultView) { 80 | await this.resultView.update(currentState); 81 | } 82 | if (this.consoleView) { 83 | await this.consoleView.update(currentState); 84 | } 85 | }); 86 | } 87 | return self.store; 88 | }; 89 | 90 | this.subscriptions.add(atom.workspace.onDidChangeActivePaneItem((paneItem: any) => { 91 | if (atom.workspace.isTextEditor(paneItem)) { 92 | self.getStore().dispatch(setEditorAction(paneItem)); 93 | self.getStore().dispatch(setFilterAction(null)); 94 | } 95 | })); 96 | 97 | this.subscriptions.add(new Disposable(() => { 98 | atom.workspace.getPaneItems().forEach((item) => { 99 | if (item instanceof ConsoleOutputView) { 100 | item.destroy(); 101 | } 102 | }); 103 | })); 104 | 105 | this.deserializeConsoleOutput = function deserializeConsoleOutput() { 106 | self.consoleView = new ConsoleOutputView({ 107 | state: self.getStore().getState(), 108 | }); 109 | return self.consoleView; 110 | }; 111 | 112 | this.subscriptions.add(atom.workspace.addOpener((uri) => { 113 | if (uri === 'atom://tester-console-output') { 114 | return self.deserializeConsoleOutput(); 115 | } 116 | })); 117 | 118 | this.subscriptions.add(new Disposable(() => { 119 | atom.workspace.getPaneItems().forEach((item) => { 120 | if (item instanceof ResultView) { 121 | item.destroy(); 122 | } 123 | }); 124 | })); 125 | 126 | this.deserializeResultView = function deserializeResultView() { 127 | const resultView = new ResultView({ 128 | state: self.getStore().getState(), 129 | softWrap: atom.config.get('tester.softWrapDefault'), 130 | }); 131 | resultView.onTestButtonClick(() => { 132 | self.getStore().dispatch(testProjectAction()); 133 | }); 134 | resultView.onSortByClick((sorter: TesterSorter) => { 135 | if (sorter && sorter.key) { 136 | self.getStore().dispatch(setSortByAction(sorter)); 137 | } 138 | }); 139 | resultView.onCurrentFileOnlyClick((currentFileOnly: boolean) => { 140 | self.getStore().dispatch(setFilterAction(currentFileOnly)); 141 | }); 142 | resultView.onClearButtonClick(() => { 143 | self.getStore().dispatch(clearAction()); 144 | }); 145 | resultView.onSetAdditionalArgs((additionalArgs: ?string) => { 146 | self.getStore().dispatch(setAdditionalArgsAction(additionalArgs)); 147 | }); 148 | resultView.onSetResultViewColumns((resultViewColumns: { 149 | columns: Array; 150 | columnWidthRatios: WidthMap; 151 | }) => { 152 | self.getStore().dispatch(setResultViewColumns(resultViewColumns)); 153 | }); 154 | self.resultView = resultView; 155 | return self.resultView; 156 | }; 157 | 158 | this.subscriptions.add(atom.workspace.addOpener((uri) => { 159 | if (uri === 'atom://tester-result-view') { 160 | return self.deserializeResultView(); 161 | } 162 | })); 163 | 164 | this.commands.onShouldTestLast(() => { 165 | self.getStore().dispatch(testLastAction()); 166 | }); 167 | this.commands.onShouldTestProject(() => { 168 | self.getStore().dispatch(testProjectAction()); 169 | }); 170 | this.commands.onShouldTest(() => { 171 | self.getStore().dispatch(testAction()); 172 | }); 173 | this.commands.onShouldStop(() => { 174 | self.getStore().dispatch(stopTestAction()); 175 | }); 176 | this.commands.onShouldClear(() => { 177 | self.getStore().dispatch(clearAction()); 178 | }); 179 | this.commands.onShouldToggleTesterOutput(() => { 180 | this.toggleConsoleView(); 181 | }); 182 | this.commands.onShouldToggleTesterResultView(() => { 183 | this.toggleResultView(); 184 | }); 185 | this.commands.onShouldGoToNextTest(() => { 186 | self.getStore().dispatch(goToNextTestAction()); 187 | }); 188 | this.commands.onShouldGoToPreviousTest(() => { 189 | self.getStore().dispatch(goToPreviousTestAction()); 190 | }); 191 | } 192 | 193 | createStatusBar(statusBar : Object) { 194 | if (!atom.config.get('tester.showStatusBar')) { 195 | return; 196 | } 197 | const onclick = () => { 198 | const statusBarOnClick = atom.config.get('tester.statusBarOnClick'); 199 | if (statusBarOnClick === 'console' || statusBarOnClick === 'both') { 200 | this.toggleConsoleView(); 201 | } 202 | if (statusBarOnClick === 'results' || statusBarOnClick === 'both') { 203 | this.toggleResultView(); 204 | } 205 | }; 206 | this.statusBarTile = new StatusBarTile({ state: this.getStore().getState(), onclick }); 207 | 208 | if (atom.config.get('tester.statusBarPosition') === 'Right') { 209 | statusBar.addRightTile({ 210 | item: this.statusBarTile.element, 211 | priority: atom.config.get('tester.statusBarPriority'), 212 | }); 213 | } else { 214 | statusBar.addLeftTile({ 215 | item: this.statusBarTile.element, 216 | priority: atom.config.get('tester.statusBarPriority'), 217 | }); 218 | } 219 | } 220 | 221 | toggleConsoleView() { 222 | if (atom.getVersion() < '1.17') { 223 | return atom.notifications.addWarning('Please update atom to version >=1.17 to use tester dock views.'); 224 | } 225 | atom.workspace.toggle('atom://tester-console-output'); 226 | } 227 | 228 | toggleResultView() { 229 | if (atom.getVersion() < '1.17') { 230 | return atom.notifications.addWarning('Please update atom to version >=1.17 to use tester dock views.'); 231 | } 232 | atom.workspace.toggle('atom://tester-result-view'); 233 | } 234 | 235 | async dispose() { 236 | if (this.statusBarTile) { 237 | await this.statusBarTile.destroy(); 238 | } 239 | if (this.consoleView) { 240 | await this.consoleView.destroy(); 241 | } 242 | if (this.resultView) { 243 | await this.resultView.destroy(); 244 | } 245 | this.subscriptions.dispose(); 246 | } 247 | } 248 | 249 | export default { 250 | instance: null, 251 | consumeStatusBar(statusBar: any) { 252 | this.instance.createStatusBar(statusBar); 253 | }, 254 | 255 | initialize(initialState: any) { 256 | this.instance = new Tester(initialState); 257 | this.deserializeResultView = this.instance.deserializeResultView; 258 | this.deserializeConsoleOutput = this.instance.deserializeConsoleOutput; 259 | }, 260 | 261 | getInstance(): ?Tester { 262 | return this.instance; 263 | }, 264 | 265 | consumeTester(tester: Tester) : Disposable { 266 | if (!tester || !tester.name || !tester.scopes || !tester.test || !tester.stop) { 267 | atom.notifications.addError('Tester: Could not registry a test provider.'); 268 | return; 269 | } 270 | // $FlowIgnore 271 | this.instance.getStore().dispatch(addTesterAction(tester)); 272 | return new Disposable(() => { 273 | // $FlowIgnore 274 | this.instance.getStore().dispatch(removeTesterAction(tester)); 275 | }); 276 | }, 277 | 278 | serialize() { 279 | if (!this.instance) { 280 | return {}; 281 | } 282 | const state = Object.assign({}, this.instance.getStore().getState()); 283 | state.testRunning = false; 284 | state.testers = []; 285 | delete state.editor; 286 | return state; 287 | }, 288 | 289 | deactivate() { 290 | this.instance.dispose(); 291 | }, 292 | }; 293 | -------------------------------------------------------------------------------- /lib/redux/actions.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import type { TextEditor } from 'atom'; 5 | import type { 6 | Message, 7 | Tester, 8 | TesterAction, 9 | TesterSorter, 10 | Column, 11 | WidthMap, 12 | } from '../types'; 13 | 14 | /* 15 | * action types 16 | */ 17 | 18 | export const ADD_TESTER = 'tester/addTester'; 19 | export const CLEAR = 'tester/clear'; 20 | export const ERROR = 'tester/error'; 21 | export const GO_TO_NEXT_TEST = 'tester/goToNextTest'; 22 | export const GO_TO_PREVIOUS_TEST = 'tester/goToPreviousTest'; 23 | export const SET_CURRENT_MESSAGE = 'tester/setCurrentMessage'; 24 | export const FINISH_TEST = 'tester/finishTest'; 25 | export const REMOVE_TESTER = 'tester/removeTester'; 26 | export const SET_ADDITIONAL_ARGS = 'tester/setAdditionalArgs'; 27 | export const SET_EDITOR = 'tester/setEditor'; 28 | export const SET_FILTER = 'tester/setFilter'; 29 | export const SET_RESULT_VIEW_COLUMNS = 'tester/setResultViewColumns'; 30 | export const SET_SORTBY = 'tester/setSortBy'; 31 | export const START_TEST = 'tester/startTest'; 32 | export const STOP_TEST = 'tester/stopTest'; 33 | export const TEST = 'tester/test'; 34 | export const TEST_LAST = 'tester/testLast'; 35 | export const TEST_PROJECT = 'tester/testProject'; 36 | export const TRANSFORM_MESSAGES = 'tester/transformMessages'; 37 | export const UPDATE_EDITOR = 'tester/updateEditor'; 38 | export const UPDATE_MESSAGES = 'tester/updateMessages'; 39 | export const UPDATE_OUTPUT = 'tester/updateOutput'; 40 | 41 | /* 42 | * action creators 43 | */ 44 | 45 | export function addTesterAction(tester: Tester): TesterAction { 46 | return { 47 | type: ADD_TESTER, 48 | payload: { tester }, 49 | }; 50 | } 51 | 52 | export function clearAction(): TesterAction { 53 | return { 54 | type: CLEAR, 55 | }; 56 | } 57 | 58 | export function errorAction(message: any): TesterAction { 59 | return { 60 | type: ERROR, 61 | error: message, 62 | }; 63 | } 64 | 65 | export function goToNextTestAction(): TesterAction { 66 | return { 67 | type: GO_TO_NEXT_TEST, 68 | }; 69 | } 70 | 71 | export function goToPreviousTestAction(): TesterAction { 72 | return { 73 | type: GO_TO_PREVIOUS_TEST, 74 | }; 75 | } 76 | 77 | export function setAdditionalArgsAction(additionalArgs: ?string): TesterAction { 78 | return { 79 | type: SET_ADDITIONAL_ARGS, 80 | payload: { additionalArgs }, 81 | }; 82 | } 83 | 84 | export function setCurrentMessageAction(currentMessage: Message): TesterAction { 85 | return { 86 | type: SET_CURRENT_MESSAGE, 87 | payload: { currentMessage }, 88 | }; 89 | } 90 | 91 | export function setEditorAction(editor: TextEditor): TesterAction { 92 | return { 93 | type: SET_EDITOR, 94 | payload: { editor }, 95 | }; 96 | } 97 | 98 | export function setFilterAction(currentFileOnly: ?boolean): TesterAction { 99 | return { 100 | type: SET_FILTER, 101 | payload: { currentFileOnly }, 102 | }; 103 | } 104 | 105 | export function finishTestAction(): TesterAction { 106 | return { 107 | type: FINISH_TEST, 108 | }; 109 | } 110 | 111 | export function removeTesterAction(tester: Tester): TesterAction { 112 | return { 113 | type: REMOVE_TESTER, 114 | payload: { tester }, 115 | }; 116 | } 117 | 118 | export function setSortByAction(sorter: TesterSorter): TesterAction { 119 | return { 120 | type: SET_SORTBY, 121 | payload: { sorter }, 122 | }; 123 | } 124 | 125 | export function setResultViewColumns( 126 | resultViewColumns :{ 127 | columns: Array; 128 | columnWidthRatios: WidthMap; 129 | }): TesterAction { 130 | return { 131 | type: SET_RESULT_VIEW_COLUMNS, 132 | payload: { resultViewColumns }, 133 | }; 134 | } 135 | 136 | export function startTestAction(isProjectTest: ?boolean): TesterAction { 137 | return { 138 | type: START_TEST, 139 | payload: { isProjectTest }, 140 | }; 141 | } 142 | 143 | export function stopTestAction(): TesterAction { 144 | return { 145 | type: STOP_TEST, 146 | }; 147 | } 148 | 149 | export function testAction(): TesterAction { 150 | return { 151 | type: TEST, 152 | }; 153 | } 154 | 155 | export function testLastAction(): TesterAction { 156 | return { 157 | type: TEST_LAST, 158 | }; 159 | } 160 | 161 | export function testProjectAction(): TesterAction { 162 | return { 163 | type: TEST_PROJECT, 164 | }; 165 | } 166 | 167 | export function transformMessagesAction(rawMessages: ?Array): TesterAction { 168 | return { 169 | type: TRANSFORM_MESSAGES, 170 | payload: { rawMessages }, 171 | }; 172 | } 173 | 174 | export function updateEditorAction(editor: TextEditor): TesterAction { 175 | return { 176 | type: UPDATE_EDITOR, 177 | payload: { editor }, 178 | }; 179 | } 180 | 181 | export function updateMessagesAction(messages: Array, rawMessages: ?Array): TesterAction { 182 | return { 183 | type: UPDATE_MESSAGES, 184 | payload: { messages, rawMessages }, 185 | }; 186 | } 187 | 188 | export function updateOutputAction(output: string): TesterAction { 189 | return { 190 | type: UPDATE_OUTPUT, 191 | payload: { output }, 192 | }; 193 | } 194 | -------------------------------------------------------------------------------- /lib/redux/epics/clear.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { CLEAR, errorAction, updateMessagesAction, updateOutputAction } from '../actions'; 7 | import type { TesterAction } from '../../types'; 8 | 9 | export default function clear(action$: Observable): Observable { 10 | return action$.ofType(CLEAR) 11 | .switchMap(() => Observable.of(updateMessagesAction([], []), updateOutputAction(''))) 12 | .catch(err => Observable.of(errorAction(err))); 13 | } 14 | -------------------------------------------------------------------------------- /lib/redux/epics/error.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { ERROR } from '../actions'; 7 | import type { TesterAction } from '../../types'; 8 | 9 | export default function error(action$: Observable): Observable { 10 | return action$.ofType(ERROR) 11 | .map((action: TesterAction) => action.error) 12 | .switchMap((err: Error) => { 13 | console.error('Tester:', err); 14 | atom.notifications.addError(`Tester: ${err.message}`); 15 | return Observable.empty(); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /lib/redux/epics/goToNextTest.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { GO_TO_NEXT_TEST, errorAction, setCurrentMessageAction } from '../actions'; 7 | import type { TesterAction, Store } from '../../types'; 8 | 9 | export default function goToNextTest(action$: Observable, store: Store): Observable { 10 | return action$.ofType(GO_TO_NEXT_TEST) 11 | .switchMap(() => { 12 | const currentState = store.getState(); 13 | 14 | if (!currentState.messages || currentState.messages.length === 0) { 15 | return Observable.empty(); 16 | } 17 | 18 | if (!currentState.currentMessage) { 19 | return Observable.of(setCurrentMessageAction(currentState.messages[0])); 20 | } 21 | 22 | const currentMessage = currentState.currentMessage; 23 | const index = currentState.messages.findIndex(message => 24 | message.filePath === currentMessage.filePath && 25 | message.lineNumber === currentMessage.lineNumber); 26 | 27 | if (index >= 0 && index < currentState.messages.length - 1) { 28 | return Observable.of(setCurrentMessageAction(currentState.messages[index + 1])); 29 | } 30 | 31 | return Observable.of(setCurrentMessageAction(currentState.messages[0])); 32 | }) 33 | .catch(err => Observable.of(errorAction(err))); 34 | } 35 | -------------------------------------------------------------------------------- /lib/redux/epics/goToPreviousTest.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { GO_TO_PREVIOUS_TEST, errorAction, setCurrentMessageAction } from '../actions'; 7 | import type { TesterAction, Store } from '../../types'; 8 | 9 | export default function goToPreviousTest(action$: Observable, store: Store): Observable { 10 | return action$.ofType(GO_TO_PREVIOUS_TEST) 11 | .switchMap(() => { 12 | const currentState = store.getState(); 13 | 14 | if (!currentState.messages || currentState.messages.length === 0) { 15 | return Observable.empty(); 16 | } 17 | 18 | if (!currentState.currentMessage) { 19 | return Observable.of(setCurrentMessageAction(currentState.messages[currentState.messages.length - 1])); 20 | } 21 | 22 | const currentMessage = currentState.currentMessage; 23 | const index = currentState.messages.findIndex(message => 24 | message.filePath === currentMessage.filePath && 25 | message.lineNumber === currentMessage.lineNumber); 26 | 27 | if (index > 0 && index <= currentState.messages.length - 1) { 28 | return Observable.of(setCurrentMessageAction(currentState.messages[index - 1])); 29 | } 30 | 31 | return Observable.of(setCurrentMessageAction(currentState.messages[currentState.messages.length - 1])); 32 | }) 33 | .catch(err => Observable.of(errorAction(err))); 34 | } 35 | -------------------------------------------------------------------------------- /lib/redux/epics/index.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { combineEpics } from 'redux-observable'; 6 | import clearEpic from './clear'; 7 | import errorEpic from './error'; 8 | import goToNextTestEpic from './goToNextTest'; 9 | import goToPreviousTestEpic from './goToPreviousTest'; 10 | import setCurrentMessageEpic from './setCurrentMessage'; 11 | import setEditorEpic from './setEditor'; 12 | import setFilterEpic from './setFilter'; 13 | import setSortByEpic from './setSortBy'; 14 | import startTestEpic from './startTest'; 15 | import stopTestEpic from './stopTest'; 16 | import testEpic from './test'; 17 | import testLastEpic from './testLast'; 18 | import testProjectEpic from './testProject'; 19 | import transformMessagesEpic from './transformMessages'; 20 | import updateEditorEpic from './updateEditor'; 21 | import updateMessagesEpic from './updateMessages'; 22 | import updateOutputEpic from './updateOutput'; 23 | 24 | export default (...args: any) => combineEpics( 25 | clearEpic, 26 | errorEpic, 27 | goToNextTestEpic, 28 | goToPreviousTestEpic, 29 | setCurrentMessageEpic, 30 | setEditorEpic, 31 | setFilterEpic, 32 | setSortByEpic, 33 | startTestEpic, 34 | stopTestEpic, 35 | testEpic, 36 | testLastEpic, 37 | testProjectEpic, 38 | transformMessagesEpic, 39 | updateEditorEpic, 40 | updateMessagesEpic, 41 | updateOutputEpic, 42 | )(...args); 43 | -------------------------------------------------------------------------------- /lib/redux/epics/setCurrentMessage.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { SET_CURRENT_MESSAGE, errorAction } from '../actions'; 7 | import type { Message, TesterAction } from '../../types'; 8 | 9 | export default function setCurrentMessage(action$: Observable): Observable { 10 | return action$.ofType(SET_CURRENT_MESSAGE) 11 | .filter((action: TesterAction) => action.payload && action.payload.currentMessage && action.payload.currentMessage.filePath) 12 | .map(action => action.payload.currentMessage) 13 | .switchMap((message: Message) => 14 | Observable.fromPromise(atom.workspace.open(message.filePath, { initialLine: message.lineNumber })) 15 | .switchMap(() => Observable.empty())) 16 | .catch(err => Observable.of(errorAction(err))); 17 | } 18 | -------------------------------------------------------------------------------- /lib/redux/epics/setEditor.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import type { TextEditor } from 'atom'; 7 | import globToRegex from 'glob-to-regexp'; 8 | import { SET_EDITOR, errorAction, updateEditorAction } from '../actions'; 9 | import { convertWindowsPathToUnixPath } from '../../helpers'; 10 | import type { Store, TesterAction, Tester, TesterState } from '../../types'; 11 | 12 | export default function setEditor(action$: Observable, store: Store): Observable { 13 | return action$.ofType(SET_EDITOR) 14 | .switchMap((action: TesterAction) => { 15 | const currentEditor: ?TextEditor = action.payload && action.payload.editor ? action.payload.editor : null; 16 | const filePath = currentEditor ? currentEditor.getPath() : ''; 17 | if (!filePath) { 18 | return Observable.of(updateEditorAction(null)); 19 | } 20 | 21 | const currentState: TesterState = store.getState(); 22 | return Observable.from(currentState.testers) 23 | .filter((tester: Tester) => 24 | tester.scopes.some((scope: string) => 25 | globToRegex(scope).test(convertWindowsPathToUnixPath(filePath)))) 26 | .isEmpty() 27 | .switchMap((isEmpty: boolean) => { 28 | if (isEmpty) { 29 | return Observable.of(updateEditorAction(null)); 30 | } 31 | return Observable.of(updateEditorAction(currentEditor)); 32 | }); 33 | }) 34 | .catch(err => Observable.of(errorAction(err))); 35 | } 36 | -------------------------------------------------------------------------------- /lib/redux/epics/setFilter.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { SET_FILTER, errorAction, transformMessagesAction } from '../actions'; 7 | import type { TesterAction } from '../../types'; 8 | 9 | export default function setFilter(action$: Observable): Observable { 10 | return action$.ofType(SET_FILTER) 11 | .map(() => transformMessagesAction()) 12 | .catch(err => Observable.of(errorAction(err))); 13 | } 14 | -------------------------------------------------------------------------------- /lib/redux/epics/setSortBy.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { SET_SORTBY, errorAction, transformMessagesAction } from '../actions'; 7 | import type { TesterAction } from '../../types'; 8 | 9 | export default function setSortBy(action$: Observable): Observable { 10 | return action$.ofType(SET_SORTBY) 11 | .map(() => transformMessagesAction()) 12 | .catch(err => Observable.of(errorAction(err))); 13 | } 14 | -------------------------------------------------------------------------------- /lib/redux/epics/startTest.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import { Observable } from 'rxjs'; 5 | import globToRegex from 'glob-to-regexp'; 6 | import { 7 | START_TEST, 8 | errorAction, 9 | transformMessagesAction, 10 | updateOutputAction, 11 | finishTestAction, 12 | stopTestAction, 13 | setFilterAction, 14 | } from '../actions'; 15 | import { convertWindowsPathToUnixPath } from '../../helpers'; 16 | import type { Store, Tester, TesterAction, TesterResults, TesterState } from '../../types'; 17 | 18 | export default function startTest(action$: Observable, store: Store): Observable { 19 | return action$.ofType(START_TEST) 20 | .switchMap((action: TesterAction) => { 21 | const currentState: TesterState = store.getState(); 22 | const isProjectTest: ?boolean = action.payload ? action.payload.isProjectTest : false; 23 | if (!isProjectTest && (!currentState.editor || !currentState.editor.getPath() || currentState.editor.isModified())) { 24 | return Observable.of(finishTestAction()); 25 | } 26 | 27 | const filePath = currentState.editor ? currentState.editor.getPath() : ''; 28 | return Observable.from(currentState.testers) 29 | .filter((tester: Tester) => isProjectTest || tester.scopes.some(scope => 30 | globToRegex(scope).test(convertWindowsPathToUnixPath(filePath)))) 31 | .flatMap((tester: Tester) => tester.test(isProjectTest ? null : currentState.editor, currentState.additionalArgs)) 32 | .reduce((results: TesterResults, result: ?TesterResults) => { 33 | if (result && result.messages && result.messages.constructor === Array) { 34 | result.messages.forEach((message) => { 35 | results.messages = results.messages.filter(m => m.filePath !== message.filePath); 36 | }); 37 | results.messages = results.messages.concat(result.messages); 38 | results.output += result.output; 39 | } 40 | return results; 41 | }, 42 | { 43 | messages: currentState.rawMessages.filter(m => m.filePath) || [], 44 | output: '', 45 | }) 46 | .switchMap((results: TesterResults) => { 47 | let stream = Observable.of(finishTestAction()); 48 | if (results.output) { 49 | stream = Observable.concat(Observable.of(updateOutputAction(results.output)), stream); 50 | } 51 | if (results.messages && results.messages.length > 0) { 52 | stream = Observable.concat(Observable.of(transformMessagesAction(results.messages)), stream); 53 | } 54 | if (isProjectTest && currentState.currentFileOnly && atom.config.get('tester.removeCurrentFileFilterIfProjectTest')) { 55 | stream = Observable.concat(Observable.of(setFilterAction(false)), stream); 56 | } 57 | return stream; 58 | }); 59 | }) 60 | .catch(err => Observable.of(errorAction(err), stopTestAction())); 61 | } 62 | -------------------------------------------------------------------------------- /lib/redux/epics/stopTest.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { STOP_TEST, errorAction } from '../actions'; 7 | import type { Store, Tester, TesterAction } from '../../types'; 8 | 9 | export default function stopTest(action$: Observable, store: Store): Observable { 10 | return action$.ofType(STOP_TEST) 11 | .switchMap(() => Observable.from(store.getState().testers) 12 | .do((tester: Tester) => tester.stop()) 13 | .switchMap(() => Observable.empty())) 14 | .catch(err => Observable.of(errorAction(err))); 15 | } 16 | -------------------------------------------------------------------------------- /lib/redux/epics/test.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import globToRegex from 'glob-to-regexp'; 7 | import { TEST, errorAction, startTestAction } from '../actions'; 8 | import { convertWindowsPathToUnixPath } from '../../helpers'; 9 | import type { Store, Tester, TesterAction, TesterState } from '../../types'; 10 | 11 | export default function test(action$: Observable, store: Store): Observable { 12 | return action$.ofType(TEST) 13 | .debounceTime(500) 14 | .filter(() => !store.getState().testRunning) 15 | .switchMap(() => { 16 | const currentState: TesterState = store.getState(); 17 | const filePath = currentState.editor ? currentState.editor.getPath() : ''; 18 | if (!filePath) { 19 | return Observable.empty(); 20 | } 21 | return Observable.from(currentState.testers) 22 | .filter((tester: Tester) => 23 | tester.scopes.some(scope => globToRegex(scope).test(convertWindowsPathToUnixPath(filePath)))) 24 | .isEmpty() 25 | .switchMap((isEmpty: boolean) => { 26 | if (isEmpty) { 27 | return Observable.empty(); 28 | } 29 | return Observable.of(startTestAction(false)); 30 | }); 31 | }) 32 | .catch(err => Observable.of(errorAction(err))); 33 | } 34 | -------------------------------------------------------------------------------- /lib/redux/epics/testLast.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { TEST_LAST, errorAction, startTestAction } from '../actions'; 7 | import type { Store, TesterAction } from '../../types'; 8 | 9 | export default function testLast(action$: Observable, store: Store): Observable { 10 | return action$.ofType(TEST_LAST) 11 | .filter(() => !store.getState().testRunning) 12 | .map(() => startTestAction(store.getState().isProjectTest)) 13 | .catch(err => Observable.of(errorAction(err))); 14 | } 15 | -------------------------------------------------------------------------------- /lib/redux/epics/testProject.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { TEST_PROJECT, errorAction, startTestAction } from '../actions'; 7 | import type { Store, TesterAction } from '../../types'; 8 | 9 | export default function testProject(action$: Observable, store: Store): Observable { 10 | return action$.ofType(TEST_PROJECT) 11 | .filter(() => !store.getState().testRunning) 12 | .map(() => startTestAction(true)) 13 | .catch(err => Observable.of(errorAction(err))); 14 | } 15 | -------------------------------------------------------------------------------- /lib/redux/epics/transformMessages.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { TRANSFORM_MESSAGES, errorAction, updateMessagesAction } from '../actions'; 7 | import type { Message, Store, TesterAction, TesterState } from '../../types'; 8 | 9 | 10 | export default function transformMessages(action$: Observable, store: Store): Observable { 11 | return action$.ofType(TRANSFORM_MESSAGES) 12 | .switchMap((action) => { 13 | const currentState: TesterState = store.getState(); 14 | const filePath = currentState.editor ? currentState.editor.getPath() : ''; 15 | const sortKey = currentState.sorter.key; 16 | const desc = currentState.sorter.desc; 17 | const rawMessages: Array = action.payload && action.payload.rawMessages ? action.payload.rawMessages : currentState.rawMessages; 18 | return Observable.from(rawMessages) 19 | .filter((message: Message) => { 20 | if (!currentState.currentFileOnly) { 21 | return true; 22 | } 23 | return message.filePath === filePath; 24 | }) 25 | .toArray() 26 | .map((messages: Array) => { 27 | if (!sortKey) { 28 | return messages; 29 | } 30 | return messages.sort((current, next) => { 31 | const currentValue = (sortKey === 'error') ? (current.error ? current.error.message : '') : current[sortKey]; 32 | const nextValue = (sortKey === 'error') ? (next.error ? next.error.message : '') : next[sortKey]; 33 | 34 | if (currentValue && nextValue) { 35 | if (currentValue.toString() < nextValue.toString()) { 36 | return desc ? 1 : -1; 37 | } 38 | if (currentValue.toString() > nextValue.toString()) { 39 | return desc ? -1 : 1; 40 | } 41 | } 42 | return 0; 43 | }); 44 | }) 45 | .switchMap((transformedMessages: Array) => { 46 | if (currentState.messages.length === transformedMessages.length && 47 | currentState.messages.every((v, i) => v === transformedMessages[i])) { 48 | return Observable.empty(); 49 | } 50 | return Observable.of(updateMessagesAction(transformedMessages, rawMessages)); 51 | }); 52 | }) 53 | .catch(err => Observable.of(errorAction(err))); 54 | } 55 | -------------------------------------------------------------------------------- /lib/redux/epics/updateEditor.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import type { TextEditor } from 'atom'; 5 | import { Observable } from 'rxjs'; 6 | import { UPDATE_EDITOR, errorAction, testAction } from '../actions'; 7 | import { observableFromSubscribeFunction } from './../../helpers'; 8 | import { handleGutter } from '../../decorate-manager'; 9 | import type { TesterAction } from '../../types'; 10 | 11 | export default function updateEditor(action$: Observable): Observable { 12 | return action$.ofType(UPDATE_EDITOR) 13 | .switchMap((action: TesterAction) => { 14 | if (action.payload && action.payload.editor) { 15 | const textEditor: TextEditor = action.payload.editor; 16 | let subscription = Observable.fromPromise(handleGutter(textEditor)).switchMap(() => Observable.empty()); 17 | if (atom.config.get('tester.testOnSave') && textEditor) { 18 | subscription = observableFromSubscribeFunction(callback => textEditor.onDidSave(callback)).mapTo(testAction()); 19 | } 20 | if (atom.config.get('tester.testOnOpen')) { 21 | subscription = Observable.concat( 22 | Observable.of(testAction()), 23 | subscription, 24 | ); 25 | } 26 | return subscription; 27 | } 28 | return Observable.empty(); 29 | }) 30 | .catch(err => Observable.of(errorAction(err))); 31 | } 32 | -------------------------------------------------------------------------------- /lib/redux/epics/updateMessages.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { UPDATE_MESSAGES, errorAction } from '../actions'; 7 | import { clearDecoratedGutter, clearInlineMessages, decorateGutter, setInlineMessages } from '../../decorate-manager'; 8 | import type { TesterAction, Store } from '../../types'; 9 | 10 | export default function updateMessages(action$: Observable, store: Store): Observable { 11 | return action$.ofType(UPDATE_MESSAGES) 12 | .filter(() => store.getState().editor) 13 | .do(async () => { 14 | const currentState = store.getState(); 15 | 16 | await clearDecoratedGutter(currentState.editor); 17 | await decorateGutter(currentState.editor, currentState.messages); 18 | 19 | await clearInlineMessages(currentState.editor); 20 | await setInlineMessages(currentState.editor, currentState.messages); 21 | }) 22 | .switchMap(() => Observable.empty()) 23 | .catch(err => Observable.of(errorAction(err))); 24 | } 25 | -------------------------------------------------------------------------------- /lib/redux/epics/updateOutput.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import { Observable } from 'rxjs'; 6 | import { UPDATE_OUTPUT, errorAction } from '../actions'; 7 | import type { TesterAction } from '../../types'; 8 | 9 | export default function updateOutput(action$: Observable): Observable { 10 | return action$.ofType(UPDATE_OUTPUT) 11 | .switchMap(() => Observable.empty()) 12 | .catch(err => Observable.of(errorAction(err))); 13 | } 14 | -------------------------------------------------------------------------------- /lib/redux/reducers.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | // import { combineReducers } from 'redux'; 6 | import type { 7 | TesterAction, 8 | TesterState, 9 | } from '../types'; 10 | 11 | import { 12 | ADD_TESTER, 13 | ERROR, 14 | FINISH_TEST, 15 | REMOVE_TESTER, 16 | SET_ADDITIONAL_ARGS, 17 | SET_FILTER, 18 | SET_RESULT_VIEW_COLUMNS, 19 | SET_SORTBY, 20 | SET_CURRENT_MESSAGE, 21 | START_TEST, 22 | STOP_TEST, 23 | UPDATE_EDITOR, 24 | UPDATE_MESSAGES, 25 | UPDATE_OUTPUT, 26 | } from './actions'; 27 | 28 | export default function ( 29 | state: TesterState = { 30 | rawMessages: [], 31 | currentFileOnly: false, 32 | currentMessage: null, 33 | messages: [], 34 | output: '', 35 | testRunning: false, 36 | editor: null, 37 | isProjectTest: false, 38 | sorter: { key: '', desc: false }, 39 | testers: [], 40 | additionalArgs: '', 41 | }, 42 | action: TesterAction): ?TesterState { 43 | switch (action.type) { 44 | 45 | case ADD_TESTER: 46 | if (action.payload && action.payload.tester) { 47 | if (!state.testers) { 48 | state.testers = []; 49 | } 50 | state.testers.push(action.payload.tester); 51 | } 52 | return state; 53 | case REMOVE_TESTER: 54 | if (action.payload && action.payload.tester) { 55 | const index = state.testers.indexOf(action.payload.tester); 56 | state.testers.splice(index, 1); 57 | } 58 | return state; 59 | 60 | case START_TEST: 61 | return Object.assign({}, state, { 62 | isProjectTest: action.payload && action.payload.isProjectTest ? action.payload.isProjectTest : false, 63 | testRunning: true, 64 | }); 65 | case STOP_TEST: 66 | return Object.assign({}, state, { 67 | testRunning: false, 68 | }); 69 | case FINISH_TEST: 70 | return Object.assign({}, state, { 71 | testRunning: false, 72 | }); 73 | 74 | case SET_FILTER: 75 | return Object.assign({}, state, { 76 | currentFileOnly: action.payload && action.payload.currentFileOnly !== null ? action.payload.currentFileOnly : state.currentFileOnly, 77 | }); 78 | case SET_SORTBY: 79 | return Object.assign({}, state, { 80 | sorter: action.payload && action.payload.sorter ? action.payload.sorter : { key: '', desc: false }, 81 | }); 82 | case SET_ADDITIONAL_ARGS: 83 | return Object.assign({}, state, { 84 | additionalArgs: action.payload && action.payload.additionalArgs ? action.payload.additionalArgs : '', 85 | }); 86 | case SET_CURRENT_MESSAGE: 87 | return Object.assign({}, state, { 88 | currentMessage: action.payload && action.payload.currentMessage ? action.payload.currentMessage : null, 89 | }); 90 | 91 | case UPDATE_EDITOR: 92 | return Object.assign({}, state, { 93 | editor: (action.payload && action.payload.editor ? action.payload.editor : null), 94 | }); 95 | case UPDATE_MESSAGES: 96 | return Object.assign({}, state, { 97 | messages: (action.payload && action.payload.messages ? action.payload.messages : state.messages), 98 | rawMessages: (action.payload && action.payload.rawMessages ? action.payload.rawMessages : state.rawMessages), 99 | }); 100 | case UPDATE_OUTPUT: 101 | return Object.assign({}, state, { 102 | output: (action.payload && action.payload.output ? action.payload.output : ''), 103 | }); 104 | case SET_RESULT_VIEW_COLUMNS: 105 | if (action.payload && 106 | action.payload.resultViewColumns && 107 | action.payload.resultViewColumns.columns && 108 | action.payload.resultViewColumns.columnWidthRatios) { 109 | return Object.assign({}, state, { 110 | resultViewColumns: action.payload.resultViewColumns, 111 | }); 112 | } 113 | return state; 114 | 115 | case ERROR: 116 | return Object.assign({}, state, { 117 | output: action.error ? action.error.message : '', 118 | }); 119 | default: return state; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | // @flow 4 | import type { TextEditor, Disposable } from 'atom'; 5 | import type { Observable } from 'rxjs'; 6 | 7 | export type MessageError = { 8 | name: string, 9 | message: string, 10 | actual?: any, 11 | expected?: any, 12 | operator?: string, 13 | } 14 | 15 | export type Message = { 16 | duration: number, 17 | error: ?MessageError, 18 | filePath: string, 19 | lineNumber: number, 20 | state: 'passed' | 'failed' | 'skipped', 21 | title: string, 22 | } 23 | 24 | export type TesterResults = { 25 | messages: Array, 26 | output: string 27 | } 28 | 29 | export type Tester = { 30 | // From providers 31 | name: string, 32 | options?: Object, 33 | scopes: Array, 34 | test(textEditor: TextEditor, additionalArgs: ?string): ?TesterResults | 35 | Promise, 36 | stop(textEditor: ?TextEditor) :void|Promise 37 | } 38 | 39 | export type Gutter = { 40 | decorateMarker: (Object, Object) => void; 41 | } 42 | 43 | export type TesterFilter = { 44 | key: ''|'state'|'duration'|'title'|'error'|'filePath', 45 | value: ?string, 46 | }; 47 | 48 | export type TesterSorter = { 49 | key: ''|'state'|'duration'|'title'|'error'|'filePath', 50 | desc: ?boolean, 51 | }; 52 | 53 | export type ColumnKey = string; 54 | 55 | export type Column = { 56 | key: ColumnKey, 57 | width?: number, 58 | } 59 | 60 | export type WidthMap = { 61 | [key: ColumnKey]: number, 62 | }; 63 | 64 | export type TesterAction = { 65 | type: string, 66 | payload?: ?{ 67 | additionalArgs?: ?string, 68 | currentFileOnly?: ?boolean, 69 | currentMessage?: ?Message, 70 | isProjectTest?: ?boolean, 71 | messages?: ?Array, 72 | output?: ?string, 73 | rawMessages?: ?Array, 74 | sorter?: ?TesterSorter, 75 | tester?: ?Tester, 76 | resultViewColumns?: { 77 | columns: Array; 78 | columnWidthRatios: WidthMap; 79 | } 80 | }, 81 | error?: ?Error, 82 | }; 83 | 84 | export type TesterState = { 85 | additionalArgs: ?string, 86 | currentFileOnly: ?boolean, 87 | currentMessage: ?Message, 88 | editor: ?TextEditor, 89 | isProjectTest: ? boolean, 90 | messages: Array, 91 | output: ?string, 92 | rawMessages: Array, 93 | sorter: TesterSorter, 94 | testers: Array, 95 | testRunning: boolean, 96 | resultViewColumns?: { 97 | columns: Array; 98 | columnWidthRatios: WidthMap; 99 | } 100 | }; 101 | 102 | export type Store = { 103 | getState(): TesterState, 104 | dispatch(action: TesterAction): void, 105 | }; 106 | 107 | export type TesterEpic = (Observable, Store) => Observable; 108 | 109 | export type SubscribeCallback = (item: T) => any; 110 | export type SubscribeFunction = (callback: SubscribeCallback) => Disposable; 111 | -------------------------------------------------------------------------------- /lib/views/ConsoleOutputView.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /** @jsx etch.dom */ 4 | /* @flow*/ 5 | import etch from 'etch'; 6 | import { Emitter, CompositeDisposable } from 'atom'; 7 | import { convertAnsiStringToHtml, escapeHtml } from '../helpers'; 8 | import type { TesterState } from '../types'; 9 | 10 | export const defaultContent = 'No console output'; 11 | 12 | export default class ConsoleOutputView { 13 | properties: { 14 | state: TesterState; 15 | } 16 | refs: any; 17 | element: any; 18 | panel: any; 19 | emitter: Emitter; 20 | disposables: CompositeDisposable; 21 | 22 | constructor(properties: {state: TesterState}) { 23 | this.properties = properties; 24 | this.emitter = new Emitter(); 25 | this.disposables = new CompositeDisposable(); 26 | etch.initialize(this); 27 | 28 | let content = properties.state.output; 29 | if (!content) { 30 | content = defaultContent; 31 | } else { 32 | content = escapeHtml(content); 33 | if (atom.config.get('tester.ansiToHtml')) { 34 | content = convertAnsiStringToHtml(content); 35 | } 36 | } 37 | this.refs.output.innerHTML = content; 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 |

 44 |       
45 | ); 46 | } 47 | 48 | update(newState: TesterState) { 49 | if (this.properties.state !== newState) { 50 | this.properties.state = newState; 51 | let content = this.properties.state.output; 52 | if (!content) { 53 | content = defaultContent; 54 | } else { 55 | content = escapeHtml(content); 56 | if (atom.config.get('tester.ansiToHtml')) { 57 | content = convertAnsiStringToHtml(content); 58 | } 59 | } 60 | this.refs.output.innerHTML = content; 61 | 62 | if (this.properties.scrollToBottom) { 63 | this.refs.output.scrollTop = this.refs.output.scrollHeight; 64 | } 65 | 66 | return etch.update(this); 67 | } 68 | return Promise.resolve(); 69 | } 70 | 71 | async destroy() { 72 | await etch.destroy(this); 73 | this.disposables.dispose(); 74 | } 75 | 76 | getTitle() { 77 | return 'Tester Console'; 78 | } 79 | 80 | getIconName() { 81 | return 'terminal'; 82 | } 83 | 84 | getDefaultLocation() { 85 | return 'bottom'; 86 | } 87 | 88 | getAllowedLocations() { 89 | return ['left', 'right', 'bottom']; 90 | } 91 | 92 | getURI() { 93 | return 'atom://tester-console-output'; 94 | } 95 | 96 | getElement() { 97 | return this.element; 98 | } 99 | 100 | serialize() { 101 | return { 102 | deserializer: 'tester-console-output', 103 | }; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/views/ResultView.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /** @jsx etch.dom */ 4 | /* @flow*/ 5 | import etch from 'etch'; 6 | import { Emitter, CompositeDisposable, Disposable, TextEditor } from 'atom'; 7 | import { normalizeString } from '../helpers'; 8 | import type { TesterState, Column, ColumnKey, WidthMap } from '../types'; 9 | 10 | export default class ResultView { 11 | properties: { 12 | state: TesterState; 13 | softWrap: ?boolean; 14 | currentFileOnly: ?boolean; 15 | } 16 | refs: any; 17 | element: any; 18 | panel: any; 19 | emitter: Emitter; 20 | disposables: CompositeDisposable; 21 | globalEventsDisposable: ?Disposable; 22 | resizeStartX: ?number; 23 | tableWidth: ?number; 24 | columnBeingResized: ?ColumnKey; 25 | 26 | constructor(properties: { 27 | state: TesterState, 28 | softWrap?: ?boolean, 29 | currentFileOnly?: ?boolean, 30 | }) { 31 | this.properties = properties; 32 | this.emitter = new Emitter(); 33 | this.disposables = new CompositeDisposable(); 34 | this.globalEventsDisposable = null; 35 | this.resizeStartX = null; 36 | this.tableWidth = null; 37 | this.columnBeingResized = null; 38 | (this: any).handleResizerGlobalMouseUp = this.handleResizerGlobalMouseUp.bind(this); 39 | (this: any).handleResizerGlobalMouseMove = this.handleResizerGlobalMouseMove.bind(this); 40 | if (!this.properties.state.resultViewColumns || 41 | !this.properties.state.resultViewColumns.columns || 42 | !this.properties.state.resultViewColumns.columnWidthRatios) { 43 | const columns = [ 44 | { key: 'state', width: 0.03 }, 45 | { key: 'duration', width: 0.05 }, 46 | { key: 'title', width: 0.3 }, 47 | { key: 'error', width: 0.5 }, 48 | { key: 'location', width: 0.12 }, 49 | ]; 50 | const columnWidthRatios = this.getInitialWidthsForColumns(columns); 51 | this.properties.state.resultViewColumns = { 52 | columns, 53 | columnWidthRatios, 54 | }; 55 | } 56 | 57 | etch.initialize(this); 58 | 59 | const additionalArgs = this.properties.state && this.properties.state.additionalArgs ? this.properties.state.additionalArgs : ''; 60 | this.refs.additionalArgs.getBuffer().setText(additionalArgs); 61 | 62 | const additionalArgsChangeHandler = () => { 63 | this.emitter.emit('set-additional-args', this.refs.additionalArgs.getText()); 64 | }; 65 | this.refs.additionalArgs.getBuffer().onDidChange(additionalArgsChangeHandler); 66 | this.disposables.add(new Disposable(() => { this.refs.additionalArgs.element.removeEventListener('onChange', additionalArgsChangeHandler()); })); 67 | 68 | const softWrapHandler = async () => { 69 | this.properties.softWrap = this.refs.softWrap.checked; 70 | await this.update(this.properties.state, true); 71 | }; 72 | this.refs.softWrap.addEventListener('click', softWrapHandler); 73 | this.disposables.add(new Disposable(() => { this.refs.softWrap.removeEventListener('click', softWrapHandler()); })); 74 | } 75 | 76 | render() { 77 | let messages = this.properties.state.messages; 78 | if (!messages || messages.constructor !== Array) { 79 | messages = []; 80 | } 81 | let totalMessages = this.properties.state.rawMessages; 82 | if (!totalMessages || totalMessages.constructor !== Array) { 83 | totalMessages = []; 84 | } 85 | const totalTime = (totalMessages.reduce((acc, message) => acc + (message.duration || 0), 0) / 1000).toFixed(2); 86 | const sortKey = this.properties.state.sorter.key; 87 | const desc = this.properties.state.sorter.desc; 88 | 89 | const failedTests = messages.filter(result => result.state === 'failed').length; 90 | const skippedTests = messages.filter(result => result.state === 'skipped').length; 91 | const passedTests = messages.filter(result => result.state === 'passed').length; 92 | const stateStyle = atom.config.get('tester.testStateStyle'); 93 | const { columnWidthRatios } = this.properties.state.resultViewColumns || {}; 94 | 95 | return ( 96 |
97 | 98 |
99 | 0 ? 'inline-block text-error' : 'inline-block'}>Failed: {failedTests} 100 | 0 ? 'inline-block text-warning' : 'inline-block'}>Skipped: {skippedTests} 101 | 0 ? 'inline-block text-success' : 'inline-block'}>Passed: {passedTests} 102 | Total: {totalMessages.length} 103 | {!isNaN(totalTime) ? totalTime : 0}s 104 |
105 | 110 | 112 |
113 | {etch.dom(TextEditor, { 114 | ref: 'additionalArgs', 115 | mini: true, 116 | placeholderText: 'Additional command line args', 117 | })} 118 |
119 | 126 | 130 |
131 |
132 | 133 |
134 |
135 |
142 | {stateStyle === 'Icon' ? : 'State'} 143 | 145 | 146 |
147 |
Duration 150 | 152 | 153 |
154 |
Title 157 | 159 | 160 |
161 |
Error 164 | 166 | 167 |
168 |
Location 171 | 173 |
174 |
175 | 176 |
177 |
0 ? 'display: none;' : ''}>No tester messages
179 | 180 | {messages.map((message, index) => 181 |
184 |
190 | {stateStyle === 'Icon' ? 191 | 196 | : 197 | {message.state || 'unknown'} 202 | 203 | } 204 |
205 |
{message.duration || 0}ms
207 |
{message.title || ''}
209 |
211 | {this.properties.softWrap ? 212 |
 :
213 |                         }
214 |                     
215 |
217 | {atom.project.relativizePath(message.filePath)[1] || message.filePath || ''} 218 | {message.lineNumber ? `:${message.lineNumber + 1}` : ''} 219 |
220 |
)} 221 |
222 |
223 |
224 | ); 225 | } 226 | 227 | update(newState :TesterState, shouldUpdate :?boolean) { 228 | if (newState && this.properties.state !== newState) { 229 | this.properties.state = newState; 230 | return etch.update(this); 231 | } 232 | if (shouldUpdate) { 233 | return etch.update(this); 234 | } 235 | return Promise.resolve(); 236 | } 237 | 238 | async destroy() { 239 | await etch.destroy(this); 240 | this.disposables.dispose(); 241 | } 242 | 243 | getTitle() { 244 | return 'Tester Results'; 245 | } 246 | 247 | getIconName() { 248 | return 'beaker'; 249 | } 250 | 251 | getDefaultLocation() { 252 | return 'bottom'; 253 | } 254 | 255 | getAllowedLocations() { 256 | return ['left', 'right', 'bottom']; 257 | } 258 | 259 | getURI() { 260 | return 'atom://tester-result-view'; 261 | } 262 | 263 | getElement() { 264 | return this.element; 265 | } 266 | 267 | serialize() { 268 | return { 269 | deserializer: 'tester-result-view', 270 | }; 271 | } 272 | 273 | onTestButtonClick(callback: Function) : Disposable { 274 | return this.emitter.on('test-project-button-click', callback); 275 | } 276 | 277 | onSortByClick(callback: Function) : Disposable { 278 | return this.emitter.on('sort-by-click', callback); 279 | } 280 | 281 | onSetAdditionalArgs(callback: Function) : Disposable { 282 | return this.emitter.on('set-additional-args', callback); 283 | } 284 | 285 | onSetResultViewColumns(callback: Function) : Disposable { 286 | return this.emitter.on('set-result-view-columns', callback); 287 | } 288 | 289 | onCurrentFileOnlyClick(callback: Function) : Disposable { 290 | return this.emitter.on('current-file-only-click', callback); 291 | } 292 | 293 | onClearButtonClick(callback: Function) : Disposable { 294 | return this.emitter.on('clear-click', callback); 295 | } 296 | 297 | handleTestButtonClick(): void { 298 | this.emitter.emit('test-project-button-click'); 299 | } 300 | 301 | handleRowClick(selectedIndex: number): void { 302 | if (!this.properties.state.messages || this.properties.state.messages.constructor !== Array) { 303 | return; 304 | } 305 | const message = this.properties.state.messages[selectedIndex]; 306 | if (!message || !message.filePath) { 307 | return; 308 | } 309 | atom.workspace.open(message.filePath, { initialLine: message.lineNumber }); 310 | } 311 | 312 | handleSortByClick(key: string): void { 313 | const ref = `header-${key}`; 314 | const headerElement = this.refs[ref]; 315 | const desc = headerElement.className.includes('asc'); 316 | this.emitter.emit('sort-by-click', { key, desc }); 317 | } 318 | 319 | handleCurrentFileOnlyClick(currentFileOnly: boolean): void { 320 | this.emitter.emit('current-file-only-click', currentFileOnly); 321 | } 322 | 323 | handleClearButtonClick(): void { 324 | this.emitter.emit('clear-click'); 325 | } 326 | 327 | getInitialWidthsForColumns(columns: Array): WidthMap { 328 | const columnWidthRatios = {}; 329 | let assignedWidth = 0; 330 | const unresolvedColumns = []; 331 | columns.forEach((column) => { 332 | const { key, width } = column; 333 | if (width != null) { 334 | columnWidthRatios[key] = width; 335 | assignedWidth += width; 336 | } else { 337 | unresolvedColumns.push(column); 338 | } 339 | }); 340 | const residualColumnWidth = (1 - assignedWidth) / unresolvedColumns.length; 341 | unresolvedColumns.forEach(column => columnWidthRatios[column.key] = residualColumnWidth); 342 | return columnWidthRatios; 343 | } 344 | 345 | handleResizerMouseDown(key: ColumnKey, event: any): void { 346 | if (this.globalEventsDisposable != null) { 347 | this.unsubscribeFromGlobalEvents(); 348 | } 349 | const selection = document.getSelection(); 350 | if (selection != null) { 351 | selection.removeAllRanges(); 352 | } 353 | document.addEventListener('mousemove', this.handleResizerGlobalMouseMove); 354 | document.addEventListener('mouseup', this.handleResizerGlobalMouseUp); 355 | this.resizeStartX = event.pageX; 356 | this.tableWidth = this.refs.messages.getBoundingClientRect().width; 357 | this.columnBeingResized = key; 358 | this.globalEventsDisposable = new Disposable(() => { 359 | document.removeEventListener('mousemove', this.handleResizerGlobalMouseMove); 360 | document.removeEventListener('mouseup', this.handleResizerGlobalMouseUp); 361 | this.resizeStartX = null; 362 | this.tableWidth = null; 363 | this.columnBeingResized = null; 364 | }); 365 | } 366 | 367 | unsubscribeFromGlobalEvents(): void { 368 | if (this.globalEventsDisposable == null) { 369 | return; 370 | } 371 | this.globalEventsDisposable.dispose(); 372 | this.globalEventsDisposable = null; 373 | } 374 | 375 | handleResizerGlobalMouseUp(): void { 376 | this.unsubscribeFromGlobalEvents(); 377 | } 378 | 379 | handleResizerGlobalMouseMove(event: any): void { 380 | if (this.resizeStartX == null || 381 | this.tableWidth == null || 382 | this.columnBeingResized == null || 383 | !this.properties.state.resultViewColumns) { 384 | return; 385 | } 386 | const { pageX } = (event: any); 387 | const deltaX = pageX - this.resizeStartX; 388 | const { columnWidthRatios, columns } = this.properties.state.resultViewColumns; 389 | const currentColumnSize = columnWidthRatios[this.columnBeingResized]; 390 | const updatedColumnWidths = this.updateWidths(this.columnBeingResized, ((this.tableWidth * currentColumnSize) + deltaX) / this.tableWidth); 391 | if (updatedColumnWidths) { 392 | this.resizeStartX = pageX; 393 | this.emitter.emit('set-result-view-columns', { columns, columnWidthRatios: updatedColumnWidths }); 394 | } 395 | } 396 | 397 | updateWidths(resizedColumn: string, newColumnSize: number): ?WidthMap { 398 | if (!this.properties.state.resultViewColumns) { 399 | return null; 400 | } 401 | const { columnWidthRatios, columns } = this.properties.state.resultViewColumns; 402 | const originalColumnSize = columnWidthRatios[resizedColumn]; 403 | const columnAfterResizedColumn = columns[columns.findIndex(column => column.key === resizedColumn) + 1].key; 404 | const followingColumnSize = columnWidthRatios[columnAfterResizedColumn]; 405 | const constrainedNewColumnSize = Math.max(0, Math.min(newColumnSize, followingColumnSize + originalColumnSize)); 406 | if (Math.abs(newColumnSize - constrainedNewColumnSize) > Number.EPSILON) { 407 | return null; 408 | } 409 | const updatedColumnWidths = {}; 410 | columns.forEach((column) => { 411 | const { key } = column; 412 | let width; 413 | if (column.key === resizedColumn) { 414 | width = constrainedNewColumnSize; 415 | } else if (column.key === columnAfterResizedColumn) { 416 | width = (columnWidthRatios[resizedColumn] - constrainedNewColumnSize) + columnWidthRatios[key]; 417 | } else { 418 | width = columnWidthRatios[key]; 419 | } 420 | updatedColumnWidths[key] = width; 421 | }); 422 | 423 | return updatedColumnWidths; 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /lib/views/StatusBarTile.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /** @jsx etch.dom */ 4 | /* @flow*/ 5 | import etch from 'etch'; 6 | import { Emitter, CompositeDisposable } from 'atom'; 7 | import type { TesterState } from '../types'; 8 | 9 | export default class ConsoleOutputView { 10 | properties: { 11 | state: TesterState; 12 | onclick: Function; 13 | } 14 | refs: any; 15 | element: any; 16 | panel: any; 17 | emitter: Emitter; 18 | disposables: CompositeDisposable; 19 | 20 | constructor(properties :{ state: TesterState, onclick: Function }) { 21 | this.properties = properties; 22 | this.emitter = new Emitter(); 23 | this.disposables = new CompositeDisposable(); 24 | 25 | etch.initialize(this); 26 | 27 | this.disposables.add(atom.tooltips.add(this.refs.failed, { title: 'Failed Tests' })); 28 | this.disposables.add(atom.tooltips.add(this.refs.skipped, { title: 'Skipped Tests' })); 29 | this.disposables.add(atom.tooltips.add(this.refs.passed, { title: 'Passed Tests' })); 30 | this.disposables.add(atom.tooltips.add(this.refs.beaker, { title: 'Click to toggle Tester' })); 31 | } 32 | 33 | render() { 34 | let messages = this.properties.state.messages; 35 | if (!messages || messages.constructor !== Array) { 36 | messages = []; 37 | } 38 | const failedTests = messages.filter(result => result.state === 'failed').length; 39 | const skippedTests = messages.filter(result => result.state === 'skipped').length; 40 | const passedTests = messages.filter(result => result.state === 'passed').length; 41 | return ( 42 | 63 | ); 64 | } 65 | 66 | update(newState :TesterState) { 67 | if (this.properties.state !== newState) { 68 | this.properties.state = newState; 69 | return etch.update(this); 70 | } 71 | return Promise.resolve(); 72 | } 73 | 74 | async destroy() { 75 | await etch.destroy(this); 76 | this.disposables.dispose(); 77 | } 78 | 79 | getElement() { 80 | return this.element; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /menus/tester.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": { 3 | "atom-text-editor": [ 4 | { 5 | "label": "Tester", 6 | "submenu": [ 7 | { 8 | "label": "Toggle console output", 9 | "command": "tester:toggle-tester-output" 10 | }, 11 | { 12 | "label": "Toggle result view", 13 | "command": "tester:toggle-tester-result" 14 | }, 15 | { 16 | "label": "Toggle full size inline errors", 17 | "command": "tester:toggle-full-size-inline-errors" 18 | }, 19 | { 20 | "label": "Test", 21 | "command": "tester:test" 22 | }, 23 | { 24 | "label": "Stop", 25 | "command": "tester:stop" 26 | } 27 | ] 28 | } 29 | ], 30 | ".tester-view .output": [ 31 | { 32 | "label": "Copy", 33 | "command": "core:copy" 34 | } 35 | ] 36 | }, 37 | "menu": [ 38 | { 39 | "label": "Packages", 40 | "submenu": [ 41 | { 42 | "label": "Tester", 43 | "submenu": [ 44 | { 45 | "label": "Toggle tester output", 46 | "command": "tester:toggle-tester-output" 47 | }, 48 | { 49 | "label": "Toggle tester result view", 50 | "command": "tester:toggle-tester-result" 51 | }, 52 | { 53 | "label": "Toggle full size inline errors", 54 | "command": "tester:toggle-full-size-inline-errors" 55 | }, 56 | { 57 | "label": "Test", 58 | "command": "tester:test" 59 | }, 60 | { 61 | "label": "Test Project", 62 | "command": "tester:test-project" 63 | }, 64 | { 65 | "label": "Stop", 66 | "command": "tester:stop" 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tester", 3 | "main": "./lib/main", 4 | "author": "yacut", 5 | "version": "1.4.0", 6 | "description": "A interactive test runner with IDE based Feedback", 7 | "keywords": [ 8 | "test", 9 | "tester", 10 | "testing", 11 | "interactive", 12 | "mocha", 13 | "jest", 14 | "phpunit", 15 | "wallaby.js" 16 | ], 17 | "repository": "https://github.com/yacut/tester", 18 | "bugs": { 19 | "url": "https://github.com/yacut/tester/issues" 20 | }, 21 | "license": "MIT", 22 | "engines": { 23 | "atom": ">=1.7.0 <2.0.0" 24 | }, 25 | "scripts": { 26 | "test": "(apm test) && (flow check) && (eslint . )" 27 | }, 28 | "consumedServices": { 29 | "status-bar": { 30 | "versions": { 31 | "^1.0.0": "consumeStatusBar" 32 | } 33 | }, 34 | "tester": { 35 | "versions": { 36 | "^1.0.0": "consumeTester" 37 | } 38 | } 39 | }, 40 | "deserializers": { 41 | "tester-result-view": "deserializeResultView", 42 | "tester-console-output": "deserializeConsoleOutput" 43 | }, 44 | "dependencies": { 45 | "ansi-to-html": "0.6.2", 46 | "atom-package-deps": "4.6.0", 47 | "etch": "0.12.4", 48 | "glob-to-regexp": "0.3.0", 49 | "redux": "3.6.0", 50 | "redux-observable": "0.14.1", 51 | "rxjs": "5.4.0" 52 | }, 53 | "devDependencies": { 54 | "babel-cli": "6.24.1", 55 | "babel-eslint": "7.2.3", 56 | "babel-preset-es2015": "6.24.1", 57 | "eslint": "3.19.0", 58 | "eslint-config-airbnb-base": "11.2.0", 59 | "eslint-plugin-flowtype": "2.34.0", 60 | "eslint-plugin-import": "2.3.0", 61 | "eslint-plugin-jsx-a11y": "4.0.0", 62 | "eslint-plugin-react": "7.0.1", 63 | "flow-bin": "0.47.0" 64 | }, 65 | "configSchema": { 66 | "testOnOpen": { 67 | "title": "Test on Open", 68 | "description": "Should test files on open?", 69 | "type": "boolean", 70 | "default": true 71 | }, 72 | "testOnSave": { 73 | "title": "Test on Save", 74 | "description": "Should test files on save?", 75 | "type": "boolean", 76 | "default": true 77 | }, 78 | "gutterEnabled": { 79 | "title": "Gutter Enabled", 80 | "description": "Should show test results in gutter highlights?", 81 | "type": "boolean", 82 | "default": true 83 | }, 84 | "gutterPosition": { 85 | "title": "Gutter Position", 86 | "description": "Where should be the position of the gutter highlights?", 87 | "enum": [ 88 | "Left", 89 | "Right" 90 | ], 91 | "type": "string", 92 | "default": "Right" 93 | }, 94 | "ansiToHtml": { 95 | "title": "Convert ansi console output to html", 96 | "description": "If console output has ansi colors should it convert to html colors?", 97 | "type": "boolean", 98 | "default": true 99 | }, 100 | "showInlineError": { 101 | "title": "Show errors inline after test run", 102 | "description": "Should show errors in text editor after test run?", 103 | "type": "boolean", 104 | "default": false 105 | }, 106 | "inlineErrorPosition": { 107 | "title": "Inline error position", 108 | "description": "Should show errors in text editor after or before test case?", 109 | "type": "string", 110 | "enum": [ 111 | "after", 112 | "before", 113 | "tail" 114 | ], 115 | "default": "tail" 116 | }, 117 | "showNotifications": { 118 | "title": "Show notifications after test run", 119 | "description": "Should show notifications after test run if editor not active?", 120 | "type": "boolean", 121 | "default": true 122 | }, 123 | "scrollToBottom": { 124 | "title": "Scroll console output to bottom after test run", 125 | "description": "Should scroll console output to bottom after test run?", 126 | "type": "boolean", 127 | "default": true 128 | }, 129 | "showStatusBar": { 130 | "title": "Show status bar", 131 | "description": "Should show status bar?", 132 | "type": "boolean", 133 | "default": true 134 | }, 135 | "statusBarOnClick": { 136 | "title": "Status Bar on click", 137 | "description": "What should opens on status bar click?", 138 | "type": "string", 139 | "enum": [ 140 | "console", 141 | "results", 142 | "both" 143 | ], 144 | "default": "both" 145 | }, 146 | "statusBarPosition": { 147 | "title": "Status bar position", 148 | "description": "Which position should be status bar: left or right?", 149 | "type": "string", 150 | "enum": [ 151 | "Left", 152 | "Right" 153 | ], 154 | "default": "Left" 155 | }, 156 | "statusBarPriority": { 157 | "title": "Status bar priority", 158 | "description": "Lower priority tiles are placed further to the position side.", 159 | "type": "integer", 160 | "default": 0, 161 | "minimum": -1000, 162 | "maximum": 1000 163 | }, 164 | "softWrapDefault": { 165 | "title": "Soft Wrap default", 166 | "description": "Should wrap the text in results view?", 167 | "type": "boolean", 168 | "default": true 169 | }, 170 | "removeCurrentFileFilterIfProjectTest": { 171 | "title": "Remove current file only filter if project test run", 172 | "description": "Should remove current file filter if project test run?", 173 | "type": "boolean", 174 | "default": true 175 | }, 176 | "testStateStyle": { 177 | "title": "Test state style", 178 | "description": "Show results state as text or icon?", 179 | "type": "string", 180 | "enum": [ 181 | "Text", 182 | "Icon" 183 | ], 184 | "default": "Text" 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /resources/console-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacut/tester/85fc0d338c0db2ceb1f20cfe3d7754894c92b8bc/resources/console-output.png -------------------------------------------------------------------------------- /resources/gutter-markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacut/tester/85fc0d338c0db2ceb1f20cfe3d7754894c92b8bc/resources/gutter-markers.png -------------------------------------------------------------------------------- /resources/inline-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacut/tester/85fc0d338c0db2ceb1f20cfe3d7754894c92b8bc/resources/inline-error.png -------------------------------------------------------------------------------- /resources/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacut/tester/85fc0d338c0db2ceb1f20cfe3d7754894c92b8bc/resources/preview.gif -------------------------------------------------------------------------------- /resources/result-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacut/tester/85fc0d338c0db2ceb1f20cfe3d7754894c92b8bc/resources/result-view.png -------------------------------------------------------------------------------- /resources/status-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacut/tester/85fc0d338c0db2ceb1f20cfe3d7754894c92b8bc/resources/status-bar.png -------------------------------------------------------------------------------- /spec/commands-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow */ 4 | 5 | import Path from 'path'; 6 | import Commands from '../lib/commands'; 7 | import { asyncTest } from './common'; 8 | 9 | let commands; 10 | 11 | describe('Tester Commands', () => { 12 | beforeEach(() => { 13 | if (commands) { 14 | commands.dispose(); 15 | } 16 | commands = new Commands(); 17 | }); 18 | 19 | it('should properly notifies its listeners of command execution', asyncTest(async (done) => { 20 | let clearCalled = 0; 21 | let goToNextTestCalled = 0; 22 | let goToPreviousTestCalled = 0; 23 | let stopCalled = 0; 24 | let testCalled = 0; 25 | let testProjectCalled = 0; 26 | let toggleOutputCalled = 0; 27 | let toggleResultsCalled = 0; 28 | 29 | commands.onShouldClear(() => (clearCalled += 1)); 30 | commands.onShouldGoToNextTest(() => (goToNextTestCalled += 1)); 31 | commands.onShouldGoToPreviousTest(() => (goToPreviousTestCalled += 1)); 32 | commands.onShouldStop(() => (stopCalled += 1)); 33 | commands.onShouldTest(() => (testCalled += 1)); 34 | commands.onShouldTestProject(() => (testProjectCalled += 1)); 35 | commands.onShouldToggleTesterOutput(() => (toggleOutputCalled += 1)); 36 | commands.onShouldToggleTesterResultView(() => (toggleResultsCalled += 1)); 37 | 38 | await atom.workspace.open(Path.join(__dirname, 'fixtures', 'test.txt')); 39 | const textEditor = atom.views.getView(atom.workspace.getActiveTextEditor()); 40 | const workspaceElement = atom.views.getView(atom.workspace); 41 | 42 | expect(clearCalled).toBe(0); 43 | expect(goToNextTestCalled).toBe(0); 44 | expect(goToPreviousTestCalled).toBe(0); 45 | expect(stopCalled).toBe(0); 46 | expect(testCalled).toBe(0); 47 | expect(testProjectCalled).toBe(0); 48 | expect(toggleOutputCalled).toBe(0); 49 | expect(toggleResultsCalled).toBe(0); 50 | 51 | atom.commands.dispatch(workspaceElement, 'tester:clear'); 52 | expect(clearCalled).toBe(1); 53 | expect(goToNextTestCalled).toBe(0); 54 | expect(goToPreviousTestCalled).toBe(0); 55 | expect(stopCalled).toBe(0); 56 | expect(testCalled).toBe(0); 57 | expect(testProjectCalled).toBe(0); 58 | expect(toggleOutputCalled).toBe(0); 59 | expect(toggleResultsCalled).toBe(0); 60 | 61 | atom.commands.dispatch(workspaceElement, 'tester:go-to-next-test'); 62 | expect(clearCalled).toBe(1); 63 | expect(goToNextTestCalled).toBe(1); 64 | expect(goToPreviousTestCalled).toBe(0); 65 | expect(stopCalled).toBe(0); 66 | expect(testCalled).toBe(0); 67 | expect(testProjectCalled).toBe(0); 68 | expect(toggleOutputCalled).toBe(0); 69 | expect(toggleResultsCalled).toBe(0); 70 | 71 | atom.commands.dispatch(workspaceElement, 'tester:go-to-previous-test'); 72 | expect(clearCalled).toBe(1); 73 | expect(goToNextTestCalled).toBe(1); 74 | expect(goToPreviousTestCalled).toBe(1); 75 | expect(stopCalled).toBe(0); 76 | expect(testCalled).toBe(0); 77 | expect(testProjectCalled).toBe(0); 78 | expect(toggleOutputCalled).toBe(0); 79 | expect(toggleResultsCalled).toBe(0); 80 | 81 | atom.commands.dispatch(workspaceElement, 'tester:stop'); 82 | expect(clearCalled).toBe(1); 83 | expect(goToNextTestCalled).toBe(1); 84 | expect(goToPreviousTestCalled).toBe(1); 85 | expect(stopCalled).toBe(1); 86 | expect(testCalled).toBe(0); 87 | expect(testProjectCalled).toBe(0); 88 | expect(toggleOutputCalled).toBe(0); 89 | expect(toggleResultsCalled).toBe(0); 90 | 91 | atom.commands.dispatch(textEditor, 'tester:test'); 92 | expect(clearCalled).toBe(1); 93 | expect(goToNextTestCalled).toBe(1); 94 | expect(goToPreviousTestCalled).toBe(1); 95 | expect(stopCalled).toBe(1); 96 | expect(testCalled).toBe(1); 97 | expect(testProjectCalled).toBe(0); 98 | expect(toggleOutputCalled).toBe(0); 99 | expect(toggleResultsCalled).toBe(0); 100 | 101 | atom.commands.dispatch(workspaceElement, 'tester:test-project'); 102 | expect(clearCalled).toBe(1); 103 | expect(goToNextTestCalled).toBe(1); 104 | expect(goToPreviousTestCalled).toBe(1); 105 | expect(stopCalled).toBe(1); 106 | expect(testCalled).toBe(1); 107 | expect(testProjectCalled).toBe(1); 108 | expect(toggleOutputCalled).toBe(0); 109 | expect(toggleResultsCalled).toBe(0); 110 | 111 | atom.commands.dispatch(workspaceElement, 'tester:toggle-tester-output'); 112 | expect(clearCalled).toBe(1); 113 | expect(goToNextTestCalled).toBe(1); 114 | expect(goToPreviousTestCalled).toBe(1); 115 | expect(stopCalled).toBe(1); 116 | expect(testCalled).toBe(1); 117 | expect(testProjectCalled).toBe(1); 118 | expect(toggleOutputCalled).toBe(1); 119 | expect(toggleResultsCalled).toBe(0); 120 | 121 | atom.commands.dispatch(workspaceElement, 'tester:toggle-tester-result'); 122 | expect(clearCalled).toBe(1); 123 | expect(goToNextTestCalled).toBe(1); 124 | expect(goToPreviousTestCalled).toBe(1); 125 | expect(stopCalled).toBe(1); 126 | expect(testCalled).toBe(1); 127 | expect(testProjectCalled).toBe(1); 128 | expect(toggleOutputCalled).toBe(1); 129 | expect(toggleResultsCalled).toBe(1); 130 | done(); 131 | })); 132 | }); 133 | -------------------------------------------------------------------------------- /spec/common.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | 5 | import Path from 'path'; 6 | import { tmpdir } from 'os'; 7 | import { TextBuffer, TextEditor } from 'atom'; 8 | import { Subject, Observable } from 'rxjs'; 9 | import { ActionsObservable } from 'redux-observable'; 10 | 11 | import type { TesterAction, TesterEpic, TesterState } from '../lib/types'; 12 | 13 | export const passedTest = { 14 | duration: 1, 15 | error: null, 16 | filePath: '/path/to/spec.js', 17 | lineNumber: 1, 18 | state: 'passed', 19 | title: 'passed test', 20 | }; 21 | 22 | export const skippedTest = { 23 | duration: 0, 24 | error: null, 25 | filePath: '/path/to/some-spec.js', 26 | lineNumber: 2, 27 | state: 'skipped', 28 | title: 'skipped test', 29 | }; 30 | 31 | export const failedTest = { 32 | duration: 1, 33 | error: { 34 | name: 'optional error object', 35 | message: 'something went wrong', 36 | actual: 'optional actual result', 37 | expected: 'optional expected result', 38 | operator: 'optional operator', 39 | }, 40 | filePath: 'file path to highlight', 41 | lineNumber: 1, 42 | state: 'failed', 43 | title: 'some test title', 44 | }; 45 | 46 | export const messages = [passedTest, failedTest]; 47 | 48 | export const state = { 49 | additionalArgs: '', 50 | currentFileOnly: false, 51 | currentMessage: null, 52 | editor: null, 53 | isProjectTest: false, 54 | messages: [], 55 | output: '', 56 | rawMessages: [], 57 | sorter: { key: '', desc: false }, 58 | testers: [], 59 | testRunning: false, 60 | }; 61 | 62 | export const sampleTester = { 63 | name: 'tester-name', 64 | options: {}, 65 | scopes: ['*test.js', '**spec.js'], 66 | test() { 67 | // Note, a Promise may be returned as well! 68 | return { 69 | messages, 70 | output: 'tester console output', 71 | }; 72 | }, 73 | stop() {}, 74 | }; 75 | 76 | export function getFixturesPath() : string { 77 | return Path.join(__dirname, 'fixtures', 'test.txt'); 78 | } 79 | 80 | export function getTextEditor(text: ?string, path: ?string) : TextEditor { 81 | const buffer = new TextBuffer({ text: text || 'some text' }); 82 | if (path && path !== '') { 83 | path = Path.join(tmpdir(), path); 84 | } 85 | buffer.setPath(path); 86 | const textEditor = new TextEditor({ buffer, largeFileMode: true }); 87 | 88 | return textEditor; 89 | } 90 | 91 | export function sleep(milliSeconds: number): Promise { 92 | return new Promise((resolve) => { setTimeout(resolve, milliSeconds); }); 93 | } 94 | 95 | // https://jasmine.github.io/1.3/introduction?#section-Asynchronous_Support 96 | export function asyncTest(run: Function) { 97 | return () => { 98 | let done = false; 99 | waitsFor(() => done); 100 | run(() => { done = true; }); 101 | }; 102 | } 103 | export function getEpicActions(epic: TesterEpic, action: TesterAction, currentState: TesterState = state) { 104 | const actions = new Subject(); 105 | const actions$ = new ActionsObservable(actions); 106 | const store = { 107 | getState: () => currentState, 108 | dispatch: a => ActionsObservable.concat(actions$, Observable.of(a)), 109 | }; 110 | const promiseEpic = epic(actions$, store) 111 | .toArray() 112 | .toPromise(); 113 | 114 | actions.next(action); 115 | actions.complete(); 116 | 117 | return promiseEpic; 118 | } 119 | -------------------------------------------------------------------------------- /spec/decorate-manager-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import { TextBuffer, TextEditor } from 'atom'; 5 | import { asyncTest, failedTest } from './common'; 6 | import { clearInlineMessages, clearDecoratedGutter, decorateGutter, setInlineMessages } from '../lib/decorate-manager'; 7 | 8 | describe('Decorate Manager', () => { 9 | let textEditor; 10 | let messages; 11 | beforeEach(async () => { 12 | messages = [Object.assign(failedTest)]; 13 | const buffer = new TextBuffer({ text: 'some text' }); 14 | buffer.setPath(messages[0].filePath); 15 | textEditor = new TextEditor({ buffer, largeFileMode: true }); 16 | textEditor.addGutter({ name: 'tester' }); 17 | }); 18 | 19 | describe('setInlineMessages', () => { 20 | it('should not throw if calls with empty editor', () => { 21 | expect(() => setInlineMessages(null, [])).not.toThrow(); 22 | }); 23 | it('should not throw if calls with empty messages', () => { 24 | expect(() => setInlineMessages(textEditor, [])).not.toThrow(); 25 | }); 26 | it('should add the inline message if messages are not empty', () => { 27 | atom.config.set('tester.showInlineError', true); 28 | setInlineMessages(textEditor, messages); 29 | expect(textEditor.testerMarkers).toBeTruthy(); 30 | expect(textEditor.testerMarkers.length).toBe(1); 31 | }); 32 | it('should not set a inline message if setting is disabled', () => { 33 | atom.config.set('tester.showInlineError', false); 34 | setInlineMessages(textEditor, messages); 35 | expect(textEditor.testerMarkers).not.toBeTruthy(); 36 | }); 37 | }); 38 | 39 | describe('clearInlineMessages', () => { 40 | it('should not throw if calls with empty editor', () => { 41 | expect(async () => { await clearInlineMessages(null); }).not.toThrow(); 42 | }); 43 | it('should not throw if calls with empty messages', () => { 44 | expect(async () => { await clearInlineMessages(textEditor); }).not.toThrow(); 45 | }); 46 | it('should clear the inline mesages', asyncTest(async (done) => { 47 | atom.config.set('tester.showInlineError', true); 48 | await setInlineMessages(textEditor, messages); 49 | await clearInlineMessages(textEditor); 50 | expect(textEditor.testerMarkers.length).toBe(0); 51 | done(); 52 | })); 53 | }); 54 | 55 | describe('decorateGutter', () => { 56 | it('should not throw if calls with empty editor', () => { 57 | expect(() => decorateGutter(null, [])).not.toThrow(); 58 | }); 59 | it('should not throw if calls with empty gutter', () => { 60 | expect(() => decorateGutter(textEditor, [])).not.toThrow(); 61 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0); 62 | }); 63 | it('should not throw if calls with empty gutter', () => { 64 | expect(textEditor.gutterWithName('tester')).toBeTruthy(); 65 | expect(() => decorateGutter(textEditor, [])).not.toThrow(); 66 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0); 67 | expect(() => clearDecoratedGutter(textEditor)).not.toThrow(); 68 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0); 69 | }); 70 | it('should clear the inline mesages', asyncTest(async (done) => { 71 | atom.config.set('tester.gutterEnabled', true); 72 | expect(textEditor.gutterWithName('tester')).toBeTruthy(); 73 | await decorateGutter(textEditor, messages); 74 | expect(textEditor.getBuffer().getMarkerCount()).toBe(1); 75 | 76 | await clearDecoratedGutter(textEditor); 77 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0); 78 | done(); 79 | })); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /spec/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | some test file -------------------------------------------------------------------------------- /spec/helpers-spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { Disposable } from 'atom'; 4 | import * as Helpers from '../lib/helpers'; 5 | 6 | describe('Helpers', () => { 7 | describe('subscriptiveObserve', () => { 8 | it('should activate synchronously', () => { 9 | let activated = false; 10 | Helpers.subscriptiveObserve({ 11 | observe(eventName, callback) { 12 | activated = true; 13 | expect(eventName).toBe('someEvent'); 14 | expect(typeof callback).toBe('function'); 15 | }, 16 | }, 'someEvent', () => { }); 17 | expect(activated).toBe(true); 18 | }); 19 | 20 | it('should clear last subscription when value changes', () => { 21 | let disposed = 0; 22 | let activated = false; 23 | Helpers.subscriptiveObserve({ 24 | observe(eventName, callback) { 25 | activated = true; 26 | expect(disposed).toBe(0); 27 | callback(); 28 | expect(disposed).toBe(0); 29 | callback(); 30 | expect(disposed).toBe(1); 31 | callback(); 32 | expect(disposed).toBe(2); 33 | }, 34 | }, 'someEvent', () => new Disposable(() => { 35 | disposed += 1; 36 | })); 37 | expect(activated).toBe(true); 38 | }); 39 | 40 | it('should clear both subscriptions at the end', () => { 41 | let disposed = 0; 42 | let observeDisposed = 0; 43 | let activated = false; 44 | const subscription = Helpers.subscriptiveObserve({ 45 | observe(eventName, callback) { 46 | activated = true; 47 | expect(disposed).toBe(0); 48 | callback(); 49 | expect(disposed).toBe(0); 50 | return new Disposable(() => { 51 | observeDisposed += 1; 52 | }); 53 | }, 54 | }, 'someEvent', () => new Disposable(() => { 55 | disposed += 1; 56 | })); 57 | expect(activated).toBe(true); 58 | subscription.dispose(); 59 | expect(disposed).toBe(1); 60 | expect(observeDisposed).toBe(1); 61 | }); 62 | }); 63 | 64 | describe('convertWindowsPathToUnixPath', () => { 65 | const originalPlatform = process.platform; 66 | afterEach(() => { 67 | Object.defineProperty(process, 'platform', { value: originalPlatform }); 68 | }); 69 | 70 | it('should convert windows path', () => { 71 | Object.defineProperty(process, 'platform', { value: 'win32' }); 72 | expect(Helpers.convertWindowsPathToUnixPath('C:\\path\\to\\file.txt')).toBe('C:/path/to/file.txt'); 73 | }); 74 | 75 | it('should not convert unix path', () => { 76 | Object.defineProperty(process, 'platform', { value: 'linux' }); 77 | expect(Helpers.convertWindowsPathToUnixPath('/path/to/file.txt')).toBe('/path/to/file.txt'); 78 | }); 79 | }); 80 | 81 | describe('convertAnsiStringToHtml', () => { 82 | it('should convert ansi string to html for light theme', () => { 83 | atom.config.set('tester.ansiToHtml', true); 84 | spyOn(atom.themes, 'getActiveThemeNames').andCallFake(() => ['light theme']); 85 | expect(Helpers.convertAnsiStringToHtml('\x1B[49m some dark text with light background \x1B[0m')) 86 | .toBe(' some dark text with light background '); 87 | }); 88 | 89 | it('should convert ansi string to html for dark theme', () => { 90 | atom.config.set('tester.ansiToHtml', true); 91 | spyOn(atom.themes, 'getActiveThemeNames').andCallFake(() => ['dark theme']); 92 | expect(Helpers.convertAnsiStringToHtml('\x1B[49m some light text with dark background \x1B[0m')) 93 | .toBe(' some light text with dark background '); 94 | }); 95 | }); 96 | 97 | describe('escapeHtml', () => { 98 | it('should escape some html', () => { 99 | expect(Helpers.escapeHtml('
some html
')) 100 | .toBe('<div>some html</div>'); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /spec/main-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import main from '../lib/main'; 4 | import { sampleTester } from './common'; 5 | import { addTesterAction } from '../lib/redux/actions'; 6 | 7 | describe('Main', () => { 8 | describe('When creating a new Tester', () => { 9 | it('should not throw', () => { 10 | expect(() => main.initialize()).not.toThrow(); 11 | }); 12 | it('should have all properties and methods wenn initialize', () => { 13 | expect(main.consumeStatusBar).toBeTruthy(); 14 | expect(main.initialize).toBeTruthy(); 15 | expect(main.getInstance).toBeTruthy(); 16 | expect(main.consumeTester).toBeTruthy(); 17 | expect(main.serialize).toBeTruthy(); 18 | expect(main.deactivate).toBeTruthy(); 19 | 20 | main.initialize(); 21 | const tester = main.getInstance() || {}; 22 | expect(tester).toBeTruthy(); 23 | expect(tester.commands).toBeTruthy(); 24 | expect(tester.subscriptions).toBeTruthy(); 25 | expect(tester.createStatusBar).toBeTruthy(); 26 | expect(tester.toggleConsoleView).toBeTruthy(); 27 | expect(tester.toggleResultView).toBeTruthy(); 28 | expect(tester.getStore).toBeTruthy(); 29 | expect(tester.deserializeConsoleOutput).toBeTruthy(); 30 | expect(tester.deserializeResultView).toBeTruthy(); 31 | expect(tester.dispose).toBeTruthy(); 32 | }); 33 | }); 34 | 35 | describe('When using methods', () => { 36 | it('should call createStatusBar', () => { 37 | main.initialize(); 38 | const instance = main.getInstance(); 39 | spyOn(instance, 'createStatusBar'); 40 | const statusBar = 'statusBarObject'; 41 | main.consumeStatusBar(statusBar); 42 | expect(instance.createStatusBar).toHaveBeenCalledWith(statusBar); 43 | }); 44 | 45 | it('should dispatch action wenn add tester', () => { 46 | main.initialize(); 47 | const instance = main.getInstance(); 48 | const store = { dispatch: () => {} }; 49 | spyOn(store, 'dispatch'); 50 | spyOn(instance, 'getStore').andCallFake(() => store); 51 | main.consumeTester(sampleTester); 52 | expect(store.dispatch).toHaveBeenCalledWith(addTesterAction(sampleTester)); 53 | }); 54 | 55 | it('should not dispatch an action wenn tester is bad', () => { 56 | main.initialize(); 57 | const instance = main.getInstance(); 58 | const store = { dispatch: () => {} }; 59 | spyOn(store, 'dispatch'); 60 | spyOn(instance, 'getStore').andCallFake(() => store); 61 | spyOn(atom.notifications, 'addError'); 62 | main.consumeTester({}); 63 | expect(atom.notifications.addError).toHaveBeenCalled(); 64 | expect(store.dispatch).not.toHaveBeenCalled(); 65 | }); 66 | 67 | it('should return state wenn serialize', () => { 68 | main.initialize(); 69 | const instance = main.getInstance(); 70 | const state = { 71 | editor: 'should be removed', 72 | testers: ['should clear array'], 73 | testRunning: true, 74 | rest: 'rest', 75 | }; 76 | const store = { getState: () => state }; 77 | spyOn(store, 'getState').andCallThrough(); 78 | spyOn(instance, 'getStore').andCallFake(() => store); 79 | const actualState = main.serialize(); 80 | expect(instance.getStore).toHaveBeenCalled(); 81 | expect(store.getState).toHaveBeenCalled(); 82 | expect(actualState).toEqual({ 83 | testers: [], 84 | testRunning: false, 85 | rest: 'rest', 86 | }); 87 | }); 88 | 89 | it('should call dispose wenn deactivate', () => { 90 | main.initialize(); 91 | const instance = main.getInstance(); 92 | spyOn(instance, 'dispose'); 93 | main.deactivate(); 94 | expect(instance.dispose).toHaveBeenCalled(); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /spec/redux/actions-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import * as actions from '../../lib/redux/actions'; 4 | 5 | describe('Actions', () => { 6 | describe('creating an action', () => { 7 | it('should set right action type', () => { 8 | expect(actions.addTesterAction().type).toBe(actions.ADD_TESTER); 9 | expect(actions.clearAction().type).toBe(actions.CLEAR); 10 | expect(actions.errorAction().type).toBe(actions.ERROR); 11 | expect(actions.goToNextTestAction().type).toBe(actions.GO_TO_NEXT_TEST); 12 | expect(actions.goToPreviousTestAction().type).toBe(actions.GO_TO_PREVIOUS_TEST); 13 | expect(actions.setCurrentMessageAction().type).toBe(actions.SET_CURRENT_MESSAGE); 14 | expect(actions.finishTestAction().type).toBe(actions.FINISH_TEST); 15 | expect(actions.removeTesterAction().type).toBe(actions.REMOVE_TESTER); 16 | expect(actions.setAdditionalArgsAction().type).toBe(actions.SET_ADDITIONAL_ARGS); 17 | expect(actions.setEditorAction().type).toBe(actions.SET_EDITOR); 18 | expect(actions.setFilterAction().type).toBe(actions.SET_FILTER); 19 | expect(actions.setSortByAction().type).toBe(actions.SET_SORTBY); 20 | expect(actions.startTestAction().type).toBe(actions.START_TEST); 21 | expect(actions.stopTestAction().type).toBe(actions.STOP_TEST); 22 | expect(actions.testAction().type).toBe(actions.TEST); 23 | expect(actions.testLastAction().type).toBe(actions.TEST_LAST); 24 | expect(actions.testProjectAction().type).toBe(actions.TEST_PROJECT); 25 | expect(actions.transformMessagesAction().type).toBe(actions.TRANSFORM_MESSAGES); 26 | expect(actions.updateEditorAction().type).toBe(actions.UPDATE_EDITOR); 27 | expect(actions.updateMessagesAction().type).toBe(actions.UPDATE_MESSAGES); 28 | expect(actions.updateOutputAction().type).toBe(actions.UPDATE_OUTPUT); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/redux/epics/clear-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions } from '../../common'; 4 | import clearEpic from '../../../lib/redux/epics/clear'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('clearEpic', () => { 8 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 9 | const expectedOutputActions = [actions.updateMessagesAction([], []), actions.updateOutputAction('')]; 10 | const actualOutputActions = await getEpicActions(clearEpic, actions.clearAction()); 11 | expect(actualOutputActions).toEqual(expectedOutputActions); 12 | done(); 13 | })); 14 | 15 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 16 | const errorMessage = 'some error'; 17 | spyOn(actions, 'updateOutputAction').andCallFake(() => { throw errorMessage; }); 18 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 19 | const actualOutputActions = await getEpicActions(clearEpic, actions.clearAction()); 20 | expect(actualOutputActions).toEqual(expectedOutputActions); 21 | done(); 22 | })); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/redux/epics/error-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions } from '../../common'; 4 | import errorEpic from '../../../lib/redux/epics/error'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('errorEpic', () => { 8 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 9 | spyOn(atom.notifications, 'addError'); 10 | spyOn(console, 'error'); 11 | const expectedOutputActions = []; 12 | const actualOutputActions = await getEpicActions(errorEpic, actions.errorAction(new Error('some error'))); 13 | expect(actualOutputActions).toEqual(expectedOutputActions); 14 | expect(atom.notifications.addError).toHaveBeenCalled(); 15 | expect(console.error).toHaveBeenCalled(); 16 | done(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /spec/redux/epics/goToNextTest-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, state, messages } from '../../common'; 4 | import goToNextTestEpic from '../../../lib/redux/epics/goToNextTest'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('goToNextTestEpic', () => { 8 | it('dispatches nothing when no messages', asyncTest(async (done) => { 9 | const expectedOutputActions = []; 10 | const actualOutputActions = await getEpicActions(goToNextTestEpic, actions.goToNextTestAction()); 11 | expect(actualOutputActions).toEqual(expectedOutputActions); 12 | done(); 13 | })); 14 | 15 | it('dispatches the correct actions with first message when the current message not set', asyncTest(async (done) => { 16 | const currentState = Object.assign({}, state); 17 | currentState.messages = messages; 18 | const expectedOutputActions = [actions.setCurrentMessageAction(messages[0])]; 19 | const actualOutputActions = await getEpicActions(goToNextTestEpic, actions.goToNextTestAction(), currentState); 20 | expect(actualOutputActions).toEqual(expectedOutputActions); 21 | done(); 22 | })); 23 | 24 | it('dispatches the correct actions with second message when the current message is first', asyncTest(async (done) => { 25 | const currentState = Object.assign({}, state); 26 | const firstMessage = Object.assign({}, messages[0]); 27 | firstMessage.lineNumber = '1'; 28 | const secondMessage = Object.assign({}, messages[0]); 29 | secondMessage.lineNumber = '2'; 30 | 31 | currentState.messages = [firstMessage, secondMessage]; 32 | currentState.currentMessage = firstMessage; 33 | 34 | const expectedOutputActions = [actions.setCurrentMessageAction(secondMessage)]; 35 | const actualOutputActions = await getEpicActions(goToNextTestEpic, actions.goToNextTestAction(), currentState); 36 | expect(actualOutputActions).toEqual(expectedOutputActions); 37 | done(); 38 | })); 39 | 40 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 41 | const currentState = Object.assign({}, state); 42 | currentState.messages = messages; 43 | const errorMessage = 'some error'; 44 | spyOn(actions, 'setCurrentMessageAction').andCallFake(() => { throw errorMessage; }); 45 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 46 | const actualOutputActions = await getEpicActions(goToNextTestEpic, actions.goToNextTestAction(), currentState); 47 | expect(actualOutputActions).toEqual(expectedOutputActions); 48 | done(); 49 | })); 50 | }); 51 | -------------------------------------------------------------------------------- /spec/redux/epics/goToPreviousTest-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, state, failedTest } from '../../common'; 4 | import goToPreviousTestEpic from '../../../lib/redux/epics/goToPreviousTest'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('goToPreviousTestEpic', () => { 8 | it('dispatches nothing when no messages', asyncTest(async (done) => { 9 | const expectedOutputActions = []; 10 | const actualOutputActions = await getEpicActions(goToPreviousTestEpic, actions.goToPreviousTestAction()); 11 | expect(actualOutputActions).toEqual(expectedOutputActions); 12 | done(); 13 | })); 14 | 15 | it('dispatches the correct actions with last message when the current message not set', asyncTest(async (done) => { 16 | const currentState = Object.assign({}, state); 17 | currentState.messages = [failedTest]; 18 | const expectedOutputActions = [actions.setCurrentMessageAction(failedTest)]; 19 | const actualOutputActions = await getEpicActions(goToPreviousTestEpic, actions.goToPreviousTestAction(), currentState); 20 | expect(actualOutputActions).toEqual(expectedOutputActions); 21 | done(); 22 | })); 23 | 24 | it('dispatches the correct actions with fist message when the current message is second', asyncTest(async (done) => { 25 | const currentState = Object.assign({}, state); 26 | const firstMessage = Object.assign({}, failedTest); 27 | firstMessage.lineNumber = '1'; 28 | const secondMessage = Object.assign({}, failedTest); 29 | secondMessage.lineNumber = '2'; 30 | 31 | currentState.messages = [firstMessage, secondMessage]; 32 | currentState.currentMessage = secondMessage; 33 | 34 | const expectedOutputActions = [actions.setCurrentMessageAction(firstMessage)]; 35 | const actualOutputActions = await getEpicActions(goToPreviousTestEpic, actions.goToPreviousTestAction(), currentState); 36 | expect(actualOutputActions).toEqual(expectedOutputActions); 37 | done(); 38 | })); 39 | 40 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 41 | const currentState = Object.assign({}, state); 42 | currentState.messages = [failedTest]; 43 | const errorMessage = 'some error'; 44 | spyOn(actions, 'setCurrentMessageAction').andCallFake(() => { throw errorMessage; }); 45 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 46 | const actualOutputActions = await getEpicActions(goToPreviousTestEpic, actions.goToPreviousTestAction(), currentState); 47 | expect(actualOutputActions).toEqual(expectedOutputActions); 48 | done(); 49 | })); 50 | }); 51 | -------------------------------------------------------------------------------- /spec/redux/epics/setCurrentMessage-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, passedTest } from '../../common'; 4 | import setCurrentMessageEpic from '../../../lib/redux/epics/setCurrentMessage'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('setCurrentMessageEpic', () => { 8 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 9 | spyOn(atom.workspace, 'open').andCallFake(() => Promise.resolve()); 10 | const expectedOutputActions = []; 11 | const actualOutputActions = await getEpicActions(setCurrentMessageEpic, actions.setCurrentMessageAction(passedTest)); 12 | expect(actualOutputActions).toEqual(expectedOutputActions); 13 | expect(atom.workspace.open).toHaveBeenCalledWith(passedTest.filePath, { initialLine: passedTest.lineNumber }); 14 | done(); 15 | })); 16 | 17 | it('dispatches nothing when no current message', asyncTest(async (done) => { 18 | spyOn(atom.workspace, 'open').andCallFake(() => Promise.resolve()); 19 | const expectedOutputActions = []; 20 | const actualOutputActions = await getEpicActions(setCurrentMessageEpic, actions.setCurrentMessageAction()); 21 | expect(actualOutputActions).toEqual(expectedOutputActions); 22 | expect(atom.workspace.open).not.toHaveBeenCalled(); 23 | done(); 24 | })); 25 | 26 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 27 | const errorMessage = 'some error'; 28 | spyOn(atom.workspace, 'open').andCallFake(() => { throw errorMessage; }); 29 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 30 | const actualOutputActions = await getEpicActions(setCurrentMessageEpic, actions.setCurrentMessageAction(passedTest)); 31 | expect(actualOutputActions).toEqual(expectedOutputActions); 32 | done(); 33 | })); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/redux/epics/setEditor-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { TextBuffer, TextEditor } from 'atom'; 4 | import { asyncTest, getEpicActions, passedTest, state } from '../../common'; 5 | import setEditorEpic from '../../../lib/redux/epics/setEditor'; 6 | import * as actions from '../../../lib/redux/actions'; 7 | 8 | describe('setEditorEpic', () => { 9 | let textEditor; 10 | let currentState; 11 | beforeEach(async () => { 12 | const buffer = new TextBuffer({ text: 'some text' }); 13 | buffer.setPath(passedTest.filePath); 14 | textEditor = new TextEditor({ buffer, largeFileMode: true }); 15 | currentState = Object.assign({}, state); 16 | currentState.messages = [Object.assign({}, passedTest)]; 17 | currentState.testers = [{ scopes: ['*'] }]; 18 | }); 19 | 20 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 21 | const expectedOutputActions = [actions.updateEditorAction(textEditor)]; 22 | const actualOutputActions = await getEpicActions(setEditorEpic, actions.setEditorAction(textEditor), currentState); 23 | expect(actualOutputActions).toEqual(expectedOutputActions); 24 | done(); 25 | })); 26 | 27 | it('dispatches nothing when not in scope', asyncTest(async (done) => { 28 | currentState.testers = [{ scopes: [] }]; 29 | const expectedOutputActions = [actions.updateEditorAction(null)]; 30 | const actualOutputActions = await getEpicActions(setEditorEpic, actions.setEditorAction(textEditor), currentState); 31 | expect(actualOutputActions).toEqual(expectedOutputActions); 32 | done(); 33 | })); 34 | 35 | it('dispatches nothing when no editor in action', asyncTest(async (done) => { 36 | const expectedOutputActions = [actions.updateEditorAction(null)]; 37 | const actualOutputActions = await getEpicActions(setEditorEpic, actions.setEditorAction(null), currentState); 38 | expect(actualOutputActions).toEqual(expectedOutputActions); 39 | done(); 40 | })); 41 | 42 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 43 | const errorMessage = new Error('some error'); 44 | spyOn(actions, 'updateEditorAction').andCallFake(() => { throw errorMessage; }); 45 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 46 | const actualOutputActions = await getEpicActions(setEditorEpic, actions.setEditorAction(textEditor), currentState); 47 | expect(actualOutputActions).toEqual(expectedOutputActions); 48 | done(); 49 | })); 50 | }); 51 | -------------------------------------------------------------------------------- /spec/redux/epics/setFilter-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions } from '../../common'; 4 | import setFilterEpic from '../../../lib/redux/epics/setFilter'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('setFilterEpic', () => { 8 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 9 | const expectedOutputActions = [actions.transformMessagesAction()]; 10 | const actualOutputActions = await getEpicActions(setFilterEpic, actions.setFilterAction()); 11 | expect(actualOutputActions).toEqual(expectedOutputActions); 12 | done(); 13 | })); 14 | 15 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 16 | const errorMessage = 'some error'; 17 | spyOn(actions, 'transformMessagesAction').andCallFake(() => { throw errorMessage; }); 18 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 19 | const actualOutputActions = await getEpicActions(setFilterEpic, actions.setFilterAction()); 20 | expect(actualOutputActions).toEqual(expectedOutputActions); 21 | done(); 22 | })); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/redux/epics/setSortBy-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions } from '../../common'; 4 | import setSortByEpic from '../../../lib/redux/epics/setSortBy'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('setSortByEpic', () => { 8 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 9 | const expectedOutputActions = [actions.transformMessagesAction()]; 10 | const actualOutputActions = await getEpicActions(setSortByEpic, actions.setSortByAction()); 11 | expect(actualOutputActions).toEqual(expectedOutputActions); 12 | done(); 13 | })); 14 | 15 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 16 | const errorMessage = 'some error'; 17 | spyOn(actions, 'transformMessagesAction').andCallFake(() => { throw errorMessage; }); 18 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 19 | const actualOutputActions = await getEpicActions(setSortByEpic, actions.setSortByAction()); 20 | expect(actualOutputActions).toEqual(expectedOutputActions); 21 | done(); 22 | })); 23 | }); 24 | -------------------------------------------------------------------------------- /spec/redux/epics/startTest-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, state, sampleTester, passedTest, failedTest, getTextEditor } from '../../common'; 4 | import startTestEpic from '../../../lib/redux/epics/startTest'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('startTestEpic', () => { 8 | let currentState; 9 | const output = 'some console output'; 10 | 11 | beforeEach(async () => { 12 | currentState = Object.assign({}, state); 13 | currentState.testers = [sampleTester]; 14 | }); 15 | 16 | it('dispatches the correct actions when not project test and no editor set', asyncTest(async (done) => { 17 | currentState.editor = null; 18 | spyOn(sampleTester, 'test'); 19 | const expectedOutputActions = [actions.finishTestAction()]; 20 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(), currentState); 21 | expect(actualOutputActions).toEqual(expectedOutputActions); 22 | expect(sampleTester.test).not.toHaveBeenCalled(); 23 | done(); 24 | })); 25 | 26 | it('dispatches the correct actions when not project test and no file set', asyncTest(async (done) => { 27 | currentState.editor = getTextEditor(null, ''); 28 | spyOn(sampleTester, 'test'); 29 | const expectedOutputActions = [actions.finishTestAction()]; 30 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(), currentState); 31 | expect(actualOutputActions).toEqual(expectedOutputActions); 32 | expect(sampleTester.test).not.toHaveBeenCalled(); 33 | done(); 34 | })); 35 | 36 | it('dispatches the correct actions when not project test and file set but modified', asyncTest(async (done) => { 37 | currentState.editor = getTextEditor(null, 'tester.txt'); 38 | spyOn(sampleTester, 'test'); 39 | spyOn(currentState.editor, 'isModified').andCallFake(() => true); 40 | const expectedOutputActions = [actions.finishTestAction()]; 41 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(), currentState); 42 | expect(actualOutputActions).toEqual(expectedOutputActions); 43 | expect(sampleTester.test).not.toHaveBeenCalled(); 44 | done(); 45 | })); 46 | 47 | it('dispatches the correct actions when project test', asyncTest(async (done) => { 48 | const messages = [Object.assign({}, passedTest)]; 49 | spyOn(sampleTester, 'test').andCallFake(() => Promise.resolve({ messages, output })); 50 | const expectedOutputActions = [ 51 | actions.transformMessagesAction(messages), 52 | actions.updateOutputAction(output), 53 | actions.finishTestAction()]; 54 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 55 | expect(actualOutputActions).toEqual(expectedOutputActions); 56 | expect(sampleTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 57 | done(); 58 | })); 59 | 60 | it('dispatches the correct actions when project test and remove filter setting', asyncTest(async (done) => { 61 | atom.config.set('tester.removeCurrentFileFilterIfProjectTest', true); 62 | currentState.currentFileOnly = true; 63 | const messages = [Object.assign({}, passedTest)]; 64 | spyOn(sampleTester, 'test').andCallFake(() => Promise.resolve({ messages, output })); 65 | const expectedOutputActions = [ 66 | actions.setFilterAction(false), 67 | actions.transformMessagesAction(messages), 68 | actions.updateOutputAction(output), 69 | actions.finishTestAction(), 70 | ]; 71 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 72 | expect(actualOutputActions).toEqual(expectedOutputActions); 73 | expect(sampleTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 74 | done(); 75 | })); 76 | 77 | it('dispatches the correct actions when project test and no console output', asyncTest(async (done) => { 78 | const messages = [Object.assign({}, passedTest)]; 79 | spyOn(sampleTester, 'test').andCallFake(() => Promise.resolve({ messages, output: '' })); 80 | currentState.additionalArgs = '--some-arg'; 81 | const expectedOutputActions = [ 82 | actions.transformMessagesAction(messages), 83 | actions.finishTestAction(), 84 | ]; 85 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 86 | expect(actualOutputActions).toEqual(expectedOutputActions); 87 | expect(sampleTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 88 | done(); 89 | })); 90 | 91 | it('dispatches the correct actions when project test and no messages', asyncTest(async (done) => { 92 | spyOn(sampleTester, 'test').andCallFake(() => Promise.resolve({ messages: [], output })); 93 | const expectedOutputActions = [ 94 | actions.updateOutputAction(output), 95 | actions.finishTestAction(), 96 | ]; 97 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 98 | expect(actualOutputActions).toEqual(expectedOutputActions); 99 | expect(sampleTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 100 | done(); 101 | })); 102 | 103 | it('dispatches the correct actions when project test, state messages and tester messages', asyncTest(async (done) => { 104 | const stateMessages = [Object.assign({}, passedTest)]; 105 | const testerMessages = [Object.assign({}, failedTest)]; 106 | currentState.rawMessages = stateMessages; 107 | spyOn(sampleTester, 'test').andCallFake(() => Promise.resolve({ messages: testerMessages, output })); 108 | const expectedOutputActions = [ 109 | actions.transformMessagesAction([passedTest, failedTest]), 110 | actions.updateOutputAction(output), 111 | actions.finishTestAction(), 112 | ]; 113 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 114 | expect(actualOutputActions).toEqual(expectedOutputActions); 115 | expect(sampleTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 116 | done(); 117 | })); 118 | 119 | it('dispatches the correct actions when file test and in scope', asyncTest(async (done) => { 120 | currentState.editor = getTextEditor(null, 'tester.txt'); 121 | const filePath = currentState.editor.getPath(); 122 | const messages = [Object.assign({}, passedTest, { filePath })]; 123 | spyOn(sampleTester, 'test').andCallFake(() => Promise.resolve({ messages, output })); 124 | sampleTester.scopes = ['**']; 125 | const expectedOutputActions = [ 126 | actions.transformMessagesAction(messages), 127 | actions.updateOutputAction(output), 128 | actions.finishTestAction(), 129 | ]; 130 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(false), currentState); 131 | expect(actualOutputActions).toEqual(expectedOutputActions); 132 | expect(sampleTester.test).toHaveBeenCalledWith(currentState.editor, currentState.additionalArgs); 133 | done(); 134 | })); 135 | 136 | it('dispatches the correct actions when file test and not in scope', asyncTest(async (done) => { 137 | currentState.editor = getTextEditor(null, 'tester.txt'); 138 | spyOn(sampleTester, 'test'); 139 | sampleTester.scopes = ['some non scope regex']; 140 | const expectedOutputActions = [ 141 | actions.finishTestAction(), 142 | ]; 143 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(false), currentState); 144 | expect(actualOutputActions).toEqual(expectedOutputActions); 145 | expect(sampleTester.test).not.toHaveBeenCalled(); 146 | done(); 147 | })); 148 | 149 | it('dispatches the correct actions when project test and two testers', asyncTest(async (done) => { 150 | const firstTester = Object.assign({}, sampleTester); 151 | const secondTester = Object.assign({}, sampleTester); 152 | currentState.testers = [firstTester, secondTester]; 153 | const firstTesterMessages = [Object.assign({}, passedTest)]; 154 | const secondTesterMessages = [Object.assign({}, failedTest)]; 155 | 156 | spyOn(firstTester, 'test').andCallFake(() => Promise.resolve({ messages: firstTesterMessages, output: 'first' })); 157 | spyOn(secondTester, 'test').andCallFake(() => Promise.resolve({ messages: secondTesterMessages, output: 'second' })); 158 | const expectedOutputActions = [ 159 | actions.transformMessagesAction([passedTest, failedTest]), 160 | actions.updateOutputAction('firstsecond'), 161 | actions.finishTestAction(), 162 | ]; 163 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 164 | expect(actualOutputActions).toEqual(expectedOutputActions); 165 | expect(firstTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 166 | expect(secondTester.test).toHaveBeenCalledWith(null, currentState.additionalArgs); 167 | done(); 168 | })); 169 | 170 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 171 | const errorMessage = 'some error'; 172 | currentState.editor = getTextEditor(); 173 | spyOn(currentState.editor, 'getPath').andCallFake(() => ''); 174 | spyOn(sampleTester, 'test').andCallFake(() => { throw errorMessage; }); 175 | const expectedOutputActions = [actions.errorAction(errorMessage), actions.stopTestAction()]; 176 | const actualOutputActions = await getEpicActions(startTestEpic, actions.startTestAction(true), currentState); 177 | expect(actualOutputActions).toEqual(expectedOutputActions); 178 | expect(sampleTester.test).toHaveBeenCalled(); 179 | done(); 180 | })); 181 | }); 182 | -------------------------------------------------------------------------------- /spec/redux/epics/stopTest-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, state, sampleTester } from '../../common'; 4 | import stopTestEpic from '../../../lib/redux/epics/stopTest'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('stopTestEpic', () => { 8 | let currentState; 9 | 10 | beforeEach(async () => { 11 | currentState = Object.assign({}, state); 12 | }); 13 | 14 | it('dispatches the correct actions when file test', asyncTest(async (done) => { 15 | spyOn(sampleTester, 'stop').andCallFake(() => Promise.resolve()); 16 | currentState.testers = [sampleTester]; 17 | const expectedOutputActions = []; 18 | const actualOutputActions = await getEpicActions(stopTestEpic, actions.stopTestAction(), currentState); 19 | expect(actualOutputActions).toEqual(expectedOutputActions); 20 | expect(sampleTester.stop).toHaveBeenCalled(); 21 | done(); 22 | })); 23 | 24 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 25 | const errorMessage = 'some error'; 26 | spyOn(sampleTester, 'stop').andCallFake(() => { throw errorMessage; }); 27 | currentState.testers = [sampleTester]; 28 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 29 | const actualOutputActions = await getEpicActions(stopTestEpic, actions.stopTestAction(), currentState); 30 | expect(actualOutputActions).toEqual(expectedOutputActions); 31 | done(); 32 | })); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/redux/epics/test-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { TextBuffer, TextEditor } from 'atom'; 4 | import { asyncTest, getEpicActions, passedTest, state } from '../../common'; 5 | import testEpic from '../../../lib/redux/epics/test'; 6 | import * as actions from '../../../lib/redux/actions'; 7 | 8 | describe('testEpic', () => { 9 | let textEditor; 10 | let currentState; 11 | beforeEach(async () => { 12 | const messages = [Object.assign(passedTest)]; 13 | const buffer = new TextBuffer({ text: 'some text' }); 14 | buffer.setPath(messages[0].filePath); 15 | textEditor = new TextEditor({ buffer, largeFileMode: true }); 16 | currentState = Object.assign({}, state); 17 | currentState.messages = messages; 18 | currentState.editor = textEditor; 19 | currentState.testers = [{ scopes: ['*'] }]; 20 | currentState.testRunning = false; 21 | }); 22 | 23 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 24 | const expectedOutputActions = [actions.startTestAction(false)]; 25 | const actualOutputActions = await getEpicActions(testEpic, actions.testAction(), currentState); 26 | expect(actualOutputActions).toEqual(expectedOutputActions); 27 | done(); 28 | })); 29 | 30 | it('dispatches nothing when not in scope', asyncTest(async (done) => { 31 | currentState.testers = [{ scopes: [] }]; 32 | const expectedOutputActions = []; 33 | const actualOutputActions = await getEpicActions(testEpic, actions.testAction(), currentState); 34 | expect(actualOutputActions).toEqual(expectedOutputActions); 35 | done(); 36 | })); 37 | 38 | it('dispatches nothing when no editor in action', asyncTest(async (done) => { 39 | currentState.editor = null; 40 | const expectedOutputActions = []; 41 | const actualOutputActions = await getEpicActions(testEpic, actions.testAction(), currentState); 42 | expect(actualOutputActions).toEqual(expectedOutputActions); 43 | done(); 44 | })); 45 | 46 | it('dispatches nothing when test already running', asyncTest(async (done) => { 47 | currentState.testRunning = true; 48 | const expectedOutputActions = []; 49 | const actualOutputActions = await getEpicActions(testEpic, actions.testAction(), currentState); 50 | expect(actualOutputActions).toEqual(expectedOutputActions); 51 | done(); 52 | })); 53 | 54 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 55 | const errorMessage = 'some error'; 56 | spyOn(actions, 'startTestAction').andCallFake(() => { throw errorMessage; }); 57 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 58 | const actualOutputActions = await getEpicActions(testEpic, actions.testAction(), currentState); 59 | expect(actualOutputActions).toEqual(expectedOutputActions); 60 | done(); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /spec/redux/epics/testLast-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, state } from '../../common'; 4 | import testLastEpic from '../../../lib/redux/epics/testLast'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('testLastEpic', () => { 8 | let currentState; 9 | beforeEach(async () => { 10 | currentState = Object.assign({}, state); 11 | }); 12 | 13 | it('dispatches the correct actions when file test', asyncTest(async (done) => { 14 | currentState.testRunning = false; 15 | currentState.isProjectTest = false; 16 | const expectedOutputActions = [actions.startTestAction(false)]; 17 | const actualOutputActions = await getEpicActions(testLastEpic, actions.testLastAction(), currentState); 18 | expect(actualOutputActions).toEqual(expectedOutputActions); 19 | done(); 20 | })); 21 | 22 | it('dispatches the correct actions when project test', asyncTest(async (done) => { 23 | currentState.testRunning = false; 24 | currentState.isProjectTest = true; 25 | const expectedOutputActions = [actions.startTestAction(true)]; 26 | const actualOutputActions = await getEpicActions(testLastEpic, actions.testLastAction(), currentState); 27 | expect(actualOutputActions).toEqual(expectedOutputActions); 28 | done(); 29 | })); 30 | 31 | it('dispatches nothing when test already runnung', asyncTest(async (done) => { 32 | currentState.testRunning = true; 33 | const expectedOutputActions = []; 34 | const actualOutputActions = await getEpicActions(testLastEpic, actions.testLastAction(), currentState); 35 | expect(actualOutputActions).toEqual(expectedOutputActions); 36 | done(); 37 | })); 38 | 39 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 40 | const errorMessage = 'some error'; 41 | spyOn(actions, 'startTestAction').andCallFake(() => { throw errorMessage; }); 42 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 43 | const actualOutputActions = await getEpicActions(testLastEpic, actions.testLastAction(), currentState); 44 | expect(actualOutputActions).toEqual(expectedOutputActions); 45 | done(); 46 | })); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/redux/epics/testProject-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions, state } from '../../common'; 4 | import testProjectEpic from '../../../lib/redux/epics/testProject'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('testProjectEpic', () => { 8 | let currentState; 9 | beforeEach(async () => { 10 | currentState = Object.assign({}, state); 11 | }); 12 | 13 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 14 | currentState.testRunning = false; 15 | const expectedOutputActions = [actions.startTestAction(true)]; 16 | const actualOutputActions = await getEpicActions(testProjectEpic, actions.testProjectAction(), currentState); 17 | expect(actualOutputActions).toEqual(expectedOutputActions); 18 | done(); 19 | })); 20 | 21 | it('dispatches nothing when test already runnung', asyncTest(async (done) => { 22 | currentState.testRunning = true; 23 | const expectedOutputActions = []; 24 | const actualOutputActions = await getEpicActions(testProjectEpic, actions.testProjectAction(), currentState); 25 | expect(actualOutputActions).toEqual(expectedOutputActions); 26 | done(); 27 | })); 28 | 29 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 30 | const errorMessage = 'some error'; 31 | spyOn(actions, 'startTestAction').andCallFake(() => { throw errorMessage; }); 32 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 33 | const actualOutputActions = await getEpicActions(testProjectEpic, actions.testProjectAction(), currentState); 34 | expect(actualOutputActions).toEqual(expectedOutputActions); 35 | done(); 36 | })); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/redux/epics/transformMessages-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { TextBuffer, TextEditor } from 'atom'; 4 | import { asyncTest, getEpicActions, state, passedTest, failedTest, getTextEditor } from '../../common'; 5 | import transformMessagesEpic from '../../../lib/redux/epics/transformMessages'; 6 | import * as actions from '../../../lib/redux/actions'; 7 | 8 | describe('transformMessagesEpic', () => { 9 | let currentState; 10 | beforeEach(async () => { 11 | const buffer = new TextBuffer({ text: 'some text' }); 12 | // buffer.setPath(passedTest.filePath); 13 | currentState = Object.assign({}, state); 14 | currentState.editor = new TextEditor({ buffer, largeFileMode: true }); 15 | }); 16 | 17 | it('dispatches the correct actions when no messages', asyncTest(async (done) => { 18 | currentState.messages = [Object.assign({}, passedTest)]; 19 | currentState.rawMessages = [Object.assign({}, passedTest)]; 20 | const expectedOutputActions = [actions.updateMessagesAction([], [])]; 21 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction([]), currentState); 22 | expect(actualOutputActions).toEqual(expectedOutputActions); 23 | done(); 24 | })); 25 | 26 | it('dispatches the correct actions when state has same messages', asyncTest(async (done) => { 27 | currentState.messages = []; 28 | currentState.rawMessages = []; 29 | const expectedOutputActions = []; 30 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction([]), currentState); 31 | expect(actualOutputActions).toEqual(expectedOutputActions); 32 | done(); 33 | })); 34 | 35 | it('dispatches the correct actions when no messages in action but state and sort by state', asyncTest(async (done) => { 36 | const oldMessages = [Object.assign({}, passedTest), Object.assign({}, failedTest)]; 37 | currentState.rawMessages = oldMessages; 38 | currentState.sorter.key = 'state'; 39 | currentState.sorter.desc = false; 40 | currentState.currentFileOnly = false; 41 | const newMessages = [Object.assign({}, failedTest), Object.assign({}, passedTest)]; 42 | const expectedOutputActions = [actions.updateMessagesAction(newMessages, oldMessages)]; 43 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction(), currentState); 44 | expect(actualOutputActions).toEqual(expectedOutputActions); 45 | done(); 46 | })); 47 | 48 | it('dispatches the correct actions when no messages in action but state and sort by state desc', asyncTest(async (done) => { 49 | const oldMessages = [Object.assign({}, passedTest), Object.assign({}, failedTest)]; 50 | currentState.rawMessages = oldMessages; 51 | currentState.sorter.key = 'state'; 52 | currentState.sorter.desc = true; 53 | currentState.currentFileOnly = false; 54 | const newMessages = [Object.assign({}, passedTest), Object.assign({}, failedTest)]; 55 | const expectedOutputActions = [actions.updateMessagesAction(newMessages, oldMessages)]; 56 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction(), currentState); 57 | expect(actualOutputActions).toEqual(expectedOutputActions); 58 | done(); 59 | })); 60 | 61 | it('dispatches the correct actions when no messages in state and no sorter', asyncTest(async (done) => { 62 | currentState.rawMessages = []; 63 | currentState.sorter.key = ''; 64 | currentState.currentFileOnly = false; 65 | const newMessages = [Object.assign({}, passedTest), Object.assign({}, failedTest)]; 66 | const expectedOutputActions = [actions.updateMessagesAction(newMessages, newMessages)]; 67 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction(newMessages), currentState); 68 | expect(actualOutputActions).toEqual(expectedOutputActions); 69 | done(); 70 | })); 71 | 72 | it('dispatches the correct actions when no messages in state and currentFileOnly filter', asyncTest(async (done) => { 73 | currentState.editor = getTextEditor('some text', 'tester.txt'); 74 | const filePath = currentState.editor.getPath(); 75 | currentState.rawMessages = []; 76 | currentState.sorter.key = ''; 77 | currentState.currentFileOnly = true; 78 | 79 | const newMessages = [Object.assign({}, passedTest, { filePath }), Object.assign({}, failedTest)]; 80 | const expectedOutputActions = [actions.updateMessagesAction([Object.assign({}, passedTest, { filePath })], newMessages)]; 81 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction(newMessages), currentState); 82 | expect(actualOutputActions).toEqual(expectedOutputActions); 83 | done(); 84 | })); 85 | 86 | it('dispatches the correct actions when there is an error', asyncTest(async (done) => { 87 | currentState.messages = [Object.assign({}, passedTest)]; 88 | currentState.rawMessages = [Object.assign({}, passedTest)]; 89 | const errorMessage = 'some error'; 90 | spyOn(actions, 'updateMessagesAction').andCallFake(() => { throw errorMessage; }); 91 | const expectedOutputActions = [actions.errorAction(errorMessage)]; 92 | const actualOutputActions = await getEpicActions(transformMessagesEpic, actions.transformMessagesAction([]), currentState); 93 | expect(actualOutputActions).toEqual(expectedOutputActions); 94 | done(); 95 | })); 96 | }); 97 | -------------------------------------------------------------------------------- /spec/redux/epics/updateEditor-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { TextBuffer, TextEditor } from 'atom'; 4 | import { Observable } from 'rxjs'; 5 | import { ActionsObservable } from 'redux-observable'; 6 | import { asyncTest, getEpicActions, state, getFixturesPath } from '../../common'; 7 | import updateEditorEpic from '../../../lib/redux/epics/updateEditor'; 8 | import * as actions from '../../../lib/redux/actions'; 9 | import * as decorateManager from '../../../lib/decorate-manager'; 10 | 11 | describe('updateEditorEpic', () => { 12 | let currentState; 13 | let textEditor; 14 | beforeEach(async () => { 15 | const buffer = new TextBuffer({ text: 'some test file' }); 16 | currentState = Object.assign({}, state); 17 | textEditor = new TextEditor({ buffer, largeFileMode: true }); 18 | }); 19 | it('dispatches no actions when testOnOpen and testOnSave disabled', asyncTest(async (done) => { 20 | atom.config.set('tester.testOnOpen', false); 21 | atom.config.set('tester.testOnSave', false); 22 | spyOn(decorateManager, 'handleGutter').andCallFake(() => Promise.resolve()); 23 | const expectedOutputActions = []; 24 | const actualOutputActions = await getEpicActions(updateEditorEpic, actions.updateEditorAction(textEditor), currentState); 25 | 26 | expect(actualOutputActions).toEqual(expectedOutputActions); 27 | expect(await decorateManager.handleGutter).toHaveBeenCalled(); 28 | done(); 29 | })); 30 | it('dispatches the correct actions when testOnOpen enabled', asyncTest(async (done) => { 31 | atom.config.set('tester.testOnOpen', true); 32 | atom.config.set('tester.testOnSave', false); 33 | spyOn(decorateManager, 'handleGutter').andCallFake(() => Promise.resolve()); 34 | const expectedOutputActions = [actions.testAction()]; 35 | const actualOutputActions = await getEpicActions(updateEditorEpic, actions.updateEditorAction(textEditor), currentState); 36 | 37 | expect(actualOutputActions).toEqual(expectedOutputActions); 38 | expect(await decorateManager.handleGutter).toHaveBeenCalled(); 39 | done(); 40 | })); 41 | it('dispatches the correct actions when testOnOpen enabled and testOnSave disabled', asyncTest(async (done) => { 42 | atom.config.set('tester.testOnOpen', true); 43 | atom.config.set('tester.testOnSave', false); 44 | spyOn(decorateManager, 'handleGutter').andCallFake(() => Promise.resolve()); 45 | const expectedOutputActions = [actions.testAction()]; 46 | const action$ = new ActionsObservable(Observable.of(actions.updateEditorAction(textEditor))); 47 | const actualOutputActions = []; 48 | updateEditorEpic(action$).subscribe(action => actualOutputActions.push(action)); 49 | textEditor.saveAs(getFixturesPath()); 50 | 51 | expect(actualOutputActions).toEqual(expectedOutputActions); 52 | expect(await decorateManager.handleGutter).toHaveBeenCalled(); 53 | done(); 54 | })); 55 | it('dispatches the correct actions when testOnOpen and testOnSave enabled', asyncTest(async (done) => { 56 | atom.config.set('tester.testOnOpen', true); 57 | atom.config.set('tester.testOnSave', true); 58 | spyOn(decorateManager, 'handleGutter').andCallFake(() => Promise.resolve()); 59 | const expectedOutputActions = [actions.testAction(), actions.testAction()]; 60 | const action$ = new ActionsObservable(Observable.of(actions.updateEditorAction(textEditor))); 61 | const actualOutputActions = []; 62 | updateEditorEpic(action$).subscribe(action => actualOutputActions.push(action)); 63 | textEditor.saveAs(getFixturesPath()); 64 | 65 | expect(actualOutputActions).toEqual(expectedOutputActions); 66 | expect(await decorateManager.handleGutter).toHaveBeenCalled(); 67 | done(); 68 | })); 69 | }); 70 | -------------------------------------------------------------------------------- /spec/redux/epics/updateMessages-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { TextBuffer, TextEditor } from 'atom'; 4 | import { asyncTest, getEpicActions, state } from '../../common'; 5 | import updateMessagesEpic from '../../../lib/redux/epics/updateMessages'; 6 | import * as actions from '../../../lib/redux/actions'; 7 | import * as decorateManager from '../../../lib/decorate-manager'; 8 | 9 | describe('updateMessagesEpic', () => { 10 | let currentState; 11 | beforeEach(async () => { 12 | const buffer = new TextBuffer({ text: 'some text' }); 13 | currentState = Object.assign({}, state); 14 | currentState.editor = new TextEditor({ buffer, largeFileMode: true }); 15 | }); 16 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 17 | spyOn(decorateManager, 'clearDecoratedGutter').andCallFake(() => Promise.resolve()); 18 | spyOn(decorateManager, 'decorateGutter').andCallFake(() => Promise.resolve()); 19 | spyOn(decorateManager, 'clearInlineMessages').andCallFake(() => Promise.resolve()); 20 | spyOn(decorateManager, 'setInlineMessages').andCallFake(() => Promise.resolve()); 21 | const expectedOutputActions = []; 22 | const actualOutputActions = await getEpicActions(updateMessagesEpic, actions.updateMessagesAction(), currentState); 23 | 24 | expect(actualOutputActions).toEqual(expectedOutputActions); 25 | expect(await decorateManager.clearDecoratedGutter).toHaveBeenCalled(); 26 | expect(await decorateManager.decorateGutter).toHaveBeenCalled(); 27 | expect(await decorateManager.clearInlineMessages).toHaveBeenCalled(); 28 | expect(await decorateManager.setInlineMessages).toHaveBeenCalled(); 29 | done(); 30 | })); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/redux/epics/updateOutput-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import { asyncTest, getEpicActions } from '../../common'; 4 | import updateOutputEpic from '../../../lib/redux/epics/updateOutput'; 5 | import * as actions from '../../../lib/redux/actions'; 6 | 7 | describe('updateOutputEpic', () => { 8 | it('dispatches the correct actions when it is successful', asyncTest(async (done) => { 9 | const expectedOutputActions = []; 10 | const actualOutputActions = await getEpicActions(updateOutputEpic, actions.updateOutputAction()); 11 | expect(actualOutputActions).toEqual(expectedOutputActions); 12 | done(); 13 | })); 14 | }); 15 | -------------------------------------------------------------------------------- /spec/redux/reducers-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import reducer from '../../lib/redux/reducers'; 4 | 5 | describe('Reducers', () => { 6 | describe('creating reducer', () => { 7 | it('should set default state and return it', () => { 8 | const defaultState = { 9 | rawMessages: [], 10 | currentFileOnly: false, 11 | currentMessage: null, 12 | messages: [], 13 | output: '', 14 | testRunning: false, 15 | editor: null, 16 | isProjectTest: false, 17 | sorter: { key: '', desc: false }, 18 | testers: [], 19 | additionalArgs: '', 20 | }; 21 | expect(reducer(undefined, { type: 'unknown' })).toEqual(defaultState); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /spec/views/ConsoleOutputView-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import ConsoleOutputView, { defaultContent } from '../../lib/views/ConsoleOutputView'; 5 | import { asyncTest, state } from '../common'; 6 | 7 | describe('ConsoleOutputView', () => { 8 | it('should not throw new constructor', () => { 9 | expect(() => new ConsoleOutputView({ state })).not.toThrow(); 10 | }); 11 | 12 | it('should set output text', () => { 13 | const initState = Object.assign({}, state); 14 | initState.output = 'some text'; 15 | const view = new ConsoleOutputView({ state: initState }); 16 | expect(view.element.className).toBe('tester-view native-key-bindings'); 17 | expect(view.refs.output.textContent).toBe('some text'); 18 | }); 19 | 20 | it('should set default text', () => { 21 | const view = new ConsoleOutputView({ state }); 22 | expect(view.refs.output.innerHTML).toBe(defaultContent); 23 | }); 24 | 25 | it('should update output text', asyncTest(async (done) => { 26 | const view = new ConsoleOutputView({ state }); 27 | const newState = Object.assign({}, state); 28 | newState.output = 'another text'; 29 | await view.update(newState); 30 | expect(view.refs.output.textContent).toBe('another text'); 31 | done(); 32 | })); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/views/ResultView-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import { TextEditor } from 'atom'; 5 | import ResultView from '../../lib/views/ResultView'; 6 | import { asyncTest, state, failedTest, passedTest, skippedTest } from '../common'; 7 | 8 | // TODO add spec for 'testProject' button 9 | describe('ResultView', () => { 10 | const messages = [failedTest]; 11 | it('should not throw new constructor', () => { 12 | expect(() => new ResultView({ state })).not.toThrow(); 13 | }); 14 | 15 | it('should set counters to zero if no messages', () => { 16 | const view = new ResultView({ state }); 17 | expect(view.element.className).toBe('tester-view'); 18 | expect(view.refs.failed.textContent).toBe('Failed: 0'); 19 | expect(view.refs.skipped.textContent).toBe('Skipped: 0'); 20 | expect(view.refs.passed.textContent).toBe('Passed: 0'); 21 | expect(view.refs.testProject.className).not.toContain('tester-wait-button'); 22 | expect(view.refs.additionalArgs instanceof TextEditor).toBe(true); 23 | expect(view.refs.emptyContainer.style.display).not.toBe('none'); 24 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(0); 25 | }); 26 | 27 | it('should update elements', asyncTest(async (done) => { 28 | let newState; 29 | const view = new ResultView({ state }); 30 | expect(view.refs.failed.textContent).toBe('Failed: 0'); 31 | expect(view.refs.skipped.textContent).toBe('Skipped: 0'); 32 | expect(view.refs.passed.textContent).toBe('Passed: 0'); 33 | expect(view.refs.softWrap.checked).toBe(false); 34 | expect(view.refs.testProject.className).not.toContain('tester-wait-button'); 35 | expect(view.refs.emptyContainer.style.display).not.toBe('none'); 36 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(0); 37 | 38 | newState = Object.assign({}, state); 39 | await view.update(newState); 40 | expect(view.refs.failed.textContent).toBe('Failed: 0'); 41 | expect(view.refs.skipped.textContent).toBe('Skipped: 0'); 42 | expect(view.refs.passed.textContent).toBe('Passed: 0'); 43 | expect(view.refs.softWrap.checked).toBe(false); 44 | expect(view.refs.testProject.className).not.toContain('tester-wait-button'); 45 | expect(view.refs.emptyContainer.style.display).not.toBe('none'); 46 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(0); 47 | 48 | newState = Object.assign({}, state); 49 | newState.testRunning = true; 50 | newState.messages = messages; 51 | newState.rawMessages = messages; 52 | await view.update(newState); 53 | expect(view.refs.failed.textContent).toBe('Failed: 1'); 54 | expect(view.refs.skipped.textContent).toBe('Skipped: 0'); 55 | expect(view.refs.passed.textContent).toBe('Passed: 0'); 56 | expect(view.refs.softWrap.checked).toBe(false); 57 | expect(view.refs.testProject.className).toContain('tester-wait-button'); 58 | expect(view.refs.emptyContainer.style.display).toBe('none'); 59 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(1); 60 | 61 | newState = Object.assign({}, state); 62 | newState.testRunning = false; 63 | newState.messages = messages; 64 | newState.rawMessages = messages; 65 | await view.update(newState); 66 | expect(view.refs.failed.textContent).toBe('Failed: 1'); 67 | expect(view.refs.skipped.textContent).toBe('Skipped: 0'); 68 | expect(view.refs.passed.textContent).toBe('Passed: 0'); 69 | expect(view.refs.softWrap.checked).toBe(false); 70 | expect(view.refs.testProject.className).not.toContain('tester-wait-button'); 71 | expect(view.refs.emptyContainer.style.display).toBe('none'); 72 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(1); 73 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row')[0].style.whiteSpace).toBe('nowrap'); 74 | 75 | newState = Object.assign({}, state); 76 | newState.messages = [passedTest]; 77 | newState.rawMessages = [passedTest]; 78 | await view.update(newState); 79 | expect(view.refs.failed.textContent).toBe('Failed: 0'); 80 | expect(view.refs.skipped.textContent).toBe('Skipped: 0'); 81 | expect(view.refs.passed.textContent).toBe('Passed: 1'); 82 | expect(view.refs.emptyContainer.style.display).toBe('none'); 83 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(1); 84 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row .tester-message-state')[0].textContent).toBe('passed'); 85 | 86 | newState = Object.assign({}, state); 87 | newState.messages = [skippedTest]; 88 | newState.rawMessages = [skippedTest]; 89 | await view.update(newState); 90 | expect(view.refs.failed.textContent).toBe('Failed: 0'); 91 | expect(view.refs.skipped.textContent).toBe('Skipped: 1'); 92 | expect(view.refs.passed.textContent).toBe('Passed: 0'); 93 | expect(view.refs.emptyContainer.style.display).toBe('none'); 94 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(1); 95 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row .tester-message-state')[0].textContent).toBe('skipped'); 96 | 97 | newState = Object.assign({}, state); 98 | newState.messages = [passedTest, skippedTest, failedTest]; 99 | newState.rawMessages = [passedTest, skippedTest, failedTest]; 100 | await view.update(newState); 101 | expect(view.refs.failed.textContent).toBe('Failed: 1'); 102 | expect(view.refs.skipped.textContent).toBe('Skipped: 1'); 103 | expect(view.refs.passed.textContent).toBe('Passed: 1'); 104 | expect(view.refs.messagesContainer.querySelectorAll('.tester-message-row').length).toBe(3); 105 | done(); 106 | })); 107 | }); 108 | -------------------------------------------------------------------------------- /spec/views/StatusBar-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /* @flow*/ 4 | import StatusBarTile from '../../lib/views/StatusBarTile'; 5 | import { asyncTest, state, failedTest } from '../common'; 6 | 7 | describe('StatusBar', () => { 8 | it('should not throw new constructor', () => { 9 | expect(() => new StatusBarTile({ state, onclick: () => {} })).not.toThrow(); 10 | }); 11 | 12 | it('should set counters to zero if no messages', () => { 13 | const view = new StatusBarTile({ state, onclick: () => {} }); 14 | expect(view.element.className).toBe('status-bar-tester inline-block'); 15 | expect(view.refs.failed.textContent).toBe('0'); 16 | expect(view.refs.skipped.textContent).toBe('0'); 17 | expect(view.refs.passed.textContent).toBe('0'); 18 | expect(view.refs.beaker.className).not.toContain('tester-wait-beaker'); 19 | }); 20 | 21 | it('should update tiny if test running and counters if some message', asyncTest(async (done) => { 22 | let newState; 23 | const view = new StatusBarTile({ state, onclick: () => {} }); 24 | expect(view.element.className).toBe('status-bar-tester inline-block'); 25 | expect(view.refs.failed.textContent).toBe('0'); 26 | expect(view.refs.skipped.textContent).toBe('0'); 27 | expect(view.refs.passed.textContent).toBe('0'); 28 | expect(view.refs.beaker.className).not.toContain('tester-wait-bottom-status'); 29 | 30 | newState = Object.assign({}, state); 31 | newState.testRunning = true; 32 | await view.update(newState); 33 | expect(view.refs.failed.textContent).toBe('0'); 34 | expect(view.refs.skipped.textContent).toBe('0'); 35 | expect(view.refs.passed.textContent).toBe('0'); 36 | expect(view.refs.beaker.className).toContain('tester-wait-bottom-status'); 37 | 38 | newState = Object.assign({}, state); 39 | newState.testRunning = true; 40 | newState.messages = [failedTest]; 41 | newState.rawMessages = [failedTest]; 42 | await view.update(newState); 43 | expect(view.refs.failed.textContent).toBe('1'); 44 | expect(view.refs.skipped.textContent).toBe('0'); 45 | expect(view.refs.passed.textContent).toBe('0'); 46 | expect(view.refs.beaker.className).toContain('tester-wait-bottom-status'); 47 | 48 | newState = Object.assign({}, state); 49 | newState.testRunning = false; 50 | await view.update(newState); 51 | expect(view.refs.failed.textContent).toBe('0'); 52 | expect(view.refs.skipped.textContent).toBe('0'); 53 | expect(view.refs.passed.textContent).toBe('0'); 54 | expect(view.refs.beaker.className).not.toContain('tester-wait-bottom-status'); 55 | done(); 56 | })); 57 | }); 58 | -------------------------------------------------------------------------------- /styles/tester.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .gutter[gutter-name=tester] { 8 | min-width: 0.5em; 9 | } 10 | 11 | atom-text-editor.editor .tester-gutter { 12 | width: 100%; 13 | height: 90% !important; 14 | border-radius: 3px; 15 | } 16 | 17 | atom-text-editor.editor .tester-row { 18 | /* Take up the full allowed width */ 19 | left: 0; 20 | right: 0; 21 | /* Align the tester dot in the middle */ 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | atom-text-editor.editor .tester-highlight, .tester-highlight{ 28 | &.badge { 29 | border-radius: @component-border-radius; 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | &.failed { 34 | &:not(.line-number){ 35 | background-color: @background-color-error; 36 | color: contrast(@background-color-error); 37 | } 38 | .tester-gutter{ 39 | color: @background-color-error; 40 | } 41 | .region { 42 | border-bottom: 1px dashed @background-color-error; 43 | } 44 | } 45 | &.skipped { 46 | &:not(.line-number){ 47 | background-color: @background-color-warning; 48 | color: contrast(@background-color-warning); 49 | } 50 | .tester-gutter{ 51 | color: @background-color-warning; 52 | } 53 | .region { 54 | border-bottom: 1px dashed @background-color-warning; 55 | } 56 | } 57 | &.passed { 58 | &:not(.line-number){ 59 | background-color: @background-color-success; 60 | color: contrast(@background-color-success); 61 | } 62 | .tester-gutter{ 63 | color: @background-color-success; 64 | } 65 | .region { 66 | border-bottom: 1px dashed @background-color-success; 67 | } 68 | } 69 | &.unknown { 70 | &:not(.line-number){ 71 | background-color: #939395; 72 | color: contrast(#939395); 73 | } 74 | .tester-gutter{ 75 | color: #939395; 76 | } 77 | .region { 78 | border-bottom: 1px dashed #939395; 79 | } 80 | } 81 | 82 | // Used by bottom status icon 83 | &.status-failed { 84 | color: @background-color-error; 85 | } 86 | &.status-passed{ 87 | color: @background-color-success; 88 | } 89 | &.status-skip{ 90 | color: @background-color-warning; 91 | } 92 | } 93 | 94 | .tester-view { 95 | height: 100%; 96 | display: flex; 97 | overflow: hidden; 98 | flex-direction: column; 99 | 100 | .output { 101 | overflow: auto; 102 | background-color: inherit; 103 | height: 100%; 104 | padding: 0px !important; 105 | font-family: 'BlinkMacSystemFont', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif; 106 | font-size: 1em; 107 | cursor: text; 108 | -webkit-user-select: text; 109 | 110 | &.running { 111 | background-image: url('images/octocat-spinner-128.gif'); 112 | background-repeat: no-repeat; 113 | background-position: 50%; 114 | background-size: 64px; 115 | opacity: 0.5; 116 | } 117 | } 118 | } 119 | 120 | .tester-toolbar { 121 | width: 100%; 122 | padding: 0px 5px 0px 5px; 123 | line-height: 2.5em; 124 | } 125 | 126 | .tester-messages { 127 | height: 100%; 128 | width: 100%; 129 | display: flex; 130 | overflow: hidden; 131 | flex-direction: column; 132 | 133 | div { 134 | margin-right: 0px !important; 135 | } 136 | 137 | .tester-messages-container { 138 | height: 100%; 139 | width: 100%; 140 | overflow: overlay; 141 | } 142 | 143 | .tester-message-header { 144 | width: 100%; 145 | cursor: pointer; 146 | background-color: @panel-heading-background-color !important; 147 | font-weight: 700; 148 | word-break: break-word; 149 | text-overflow: ellipsis; 150 | white-space: nowrap; 151 | display: flex; 152 | min-height: 1.5em; 153 | .sort-indicator { 154 | position: absolute; 155 | transform: scale(0.6); 156 | } 157 | .tester-message-cell:hover { 158 | color: @text-color-selected 159 | } 160 | } 161 | .tester-header-resizer { 162 | cursor: ew-resize; 163 | float: right; 164 | width: 5px; 165 | height: 100%; 166 | margin-right: -3px; 167 | z-index: 1; 168 | } 169 | .tester-message-row { 170 | cursor: pointer; 171 | word-break: break-word; 172 | text-overflow: ellipsis; 173 | display: flex; 174 | -webkit-user-select: text; 175 | } 176 | .tester-message-row:nth-child(odd){ 177 | background-color: @background-color-highlight; 178 | } 179 | .tester-message-row:hover{ 180 | background-color: @panel-heading-border-color; 181 | color: @text-color-selected; 182 | } 183 | .tester-message-cell { 184 | border-left: 1px solid @panel-heading-border-color; 185 | overflow: hidden; 186 | padding: 2px 4px; 187 | word-break: break-word; 188 | text-overflow: inherit; 189 | } 190 | .tester-message-state { 191 | min-width: 4rem; 192 | } 193 | .tester-message-state-icon { 194 | min-width: 3rem; 195 | span { 196 | padding: 0px 0px 0px 4px; 197 | } 198 | .icon::before { 199 | transform: scale(0.8); 200 | bottom: -2px; 201 | } 202 | .sort-indicator{ 203 | margin-left: -16px; 204 | } 205 | } 206 | .tester-message-duration { 207 | min-width: 5rem; 208 | } 209 | .tester-message-title { 210 | min-width: 50px; 211 | } 212 | .tester-message-error { 213 | min-width: 50px; 214 | } 215 | .tester-message-location { 216 | min-width: 5rem; 217 | } 218 | .tester-empty-container { 219 | font-style: italic; 220 | padding: 1em; 221 | text-align: center; 222 | } 223 | .tester-header { 224 | border-top: 1px solid @panel-heading-border-color; 225 | border-bottom: 1px solid @panel-heading-border-color; 226 | padding-bottom: 4px; 227 | } 228 | } 229 | 230 | .tester-tooltip-title { 231 | float: left; 232 | } 233 | 234 | .tester-bottom-status { 235 | min-width: 1.5em; 236 | line-height: 1.2em; 237 | font-weight: 500; 238 | text-align: center; 239 | display: inline-block; 240 | &[hidden] { 241 | display: none; 242 | } 243 | &.tester-status-failed { 244 | border-bottom-right-radius: 0; 245 | border-top-right-radius: 0; 246 | } 247 | &.tester-status-passed { 248 | border-bottom-left-radius: 0; 249 | border-top-left-radius: 0; 250 | } 251 | &.tester-status-skipped { 252 | border-radius: 0; 253 | } 254 | } 255 | 256 | .tester-bottom-container { 257 | margin-right: .6em; 258 | [hidden] { 259 | display: none; 260 | } 261 | } 262 | 263 | .tester-inline-message { 264 | border: 1px solid @base-border-color; 265 | max-height: 1.7em; 266 | overflow: auto; 267 | background: @overlay-background-color; 268 | color: @text-color-highlight; 269 | border-radius: @component-border-radius; 270 | padding: 0px 3px 0px 3px; 271 | &.full-size { 272 | max-height: 100%; 273 | } 274 | } 275 | 276 | .tester-inline-message-tail { 277 | margin-top: -1.6em; 278 | margin-left: 1em; 279 | z-index: 0; 280 | } 281 | 282 | .tester-resize-handle { 283 | position: absolute; 284 | top: 0; 285 | left: 0; 286 | right: 0; 287 | height: 4px; 288 | cursor: row-resize; 289 | z-index: 3; 290 | } 291 | 292 | .tester-wait-button { 293 | opacity: 1; 294 | background-image: linear-gradient(to right, transparent 50%, rgba(0, 0, 0, 0.15) 50%) !important; 295 | background-size: 10px 100%; 296 | -webkit-animation: tester-waiting-animation 0.5s linear infinite; 297 | } 298 | @-webkit-keyframes tester-waiting-animation { 299 | 100% { background-position: -10px 0px; } 300 | } 301 | 302 | .tester-wait-bottom-status { 303 | &.icon-sync { 304 | cursor: wait; 305 | } 306 | &.icon-sync::before { 307 | @keyframes tester-wait-animation { 308 | 100% { transform: rotate(360deg); } 309 | } 310 | animation: tester-wait-animation 2s linear infinite; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /test/all-in-one.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Some mocha tests', () => { 4 | it('should compare strings', () => { 5 | assert.equal('stringA', 'stringB'); 6 | }); 7 | 8 | it.skip('should skip test', () => { 9 | // skiiped test 10 | }); 11 | 12 | it('should compare dates', () => { 13 | assert.equal(new Date(), new Date('01-01-2016')); 14 | }); 15 | 16 | it('should compare arrays', () => { 17 | assert.equal(['1', '2'], ['3', '2']); 18 | }); 19 | 20 | it('should compare two objects', () => { 21 | const foo = { foo: 1 }; 22 | const bar = { bar: 2 }; 23 | assert.equal(foo, bar); 24 | }); 25 | 26 | it('should compare booleans', () => { 27 | assert.equal(true, false); 28 | }); 29 | 30 | describe('Some second level describe', () => { 31 | it('should delay test and pass', (done) => { 32 | assert.ok(true, true); 33 | setTimeout(done, 300); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/sample-mocha.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Array', () => { 4 | it('should return -1 when the value is not present', () => { 5 | assert.equal(-1, [1, 2, 3].indexOf(4)); 6 | }); 7 | }); 8 | --------------------------------------------------------------------------------