Skip to content
This repository was archived by the owner on Aug 8, 2020. It is now read-only.

Commit 8e79809

Browse files
jacobmendozasindresorhus
authored andcommitted
Run and show test results in the editor (#8)
1 parent 152214a commit 8e79809

16 files changed

+753
-0
lines changed

keymaps/ava.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"atom-workspace": {
3+
"ctrl-alt-r": "ava:run",
4+
"ctrl-alt-a": "ava:toggle"
5+
}
6+
}

lib/html-renderer-helper.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** @babel */
2+
3+
class HtmlRendererHelper {
4+
createContainer(cssClass = null, textContent = null) {
5+
const element = document.createElement('div');
6+
if (cssClass) {
7+
element.classList.add(cssClass);
8+
}
9+
if (textContent) {
10+
element.textContent = textContent;
11+
}
12+
return element;
13+
}
14+
15+
createImage(src, cssClass = null) {
16+
const img = document.createElement('img');
17+
img.src = src;
18+
img.classList.add(cssClass);
19+
return img;
20+
}
21+
}
22+
23+
module.exports = HtmlRendererHelper;

lib/main.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/** @babel */
2+
3+
import path from 'path';
4+
import {CompositeDisposable} from 'atom';
5+
import Panel from './panel.js';
6+
import TestRunnerProcess from './test-runner-process';
7+
8+
module.exports = {
9+
activate(state) {
10+
this.initRunnerProcess();
11+
this.initUI(state);
12+
13+
this.subscriptions = new CompositeDisposable();
14+
15+
this.subscriptions.add(
16+
atom.commands.add('atom-workspace', 'ava:toggle', () => this.toggle()));
17+
18+
this.subscriptions.add(
19+
atom.commands.add('atom-workspace', 'ava:run', () => this.run()));
20+
},
21+
initRunnerProcess() {
22+
this.testRunnerProcess = new TestRunnerProcess();
23+
this.testRunnerProcess.on('assert', result => this.panel.renderAssert(result));
24+
this.testRunnerProcess.on('complete', results => this.panel.renderFinalReport(results));
25+
},
26+
initUI() {
27+
this.panel = new Panel(this);
28+
this.panel.renderBase();
29+
this.atomPanel = atom.workspace.addRightPanel({item: this.panel, visible: false});
30+
},
31+
canRun() {
32+
return (atom.workspace.getActiveTextEditor() && this.testRunnerProcess.canRun());
33+
},
34+
run() {
35+
if (!this.atomPanel.isVisible()) {
36+
this.toggle();
37+
}
38+
if (!this.canRun()) {
39+
return;
40+
}
41+
42+
const editor = atom.workspace.getActiveTextEditor();
43+
const currentFileName = editor.buffer.file.path;
44+
const folder = path.dirname(currentFileName);
45+
const file = path.basename(currentFileName);
46+
47+
this.panel.renderStartProcess(file);
48+
this.testRunnerProcess.run(folder, file);
49+
},
50+
toggle() {
51+
if (this.atomPanel.isVisible()) {
52+
this.atomPanel.hide();
53+
} else {
54+
this.atomPanel.show();
55+
this.run();
56+
}
57+
},
58+
closePanel() {
59+
this.atomPanel.hide();
60+
},
61+
deactivate() {
62+
this.subscriptions.dispose();
63+
this.panel.destroy();
64+
},
65+
serialize() {
66+
this.atomAva = this.panel.serialize();
67+
}
68+
};

lib/panel.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/** @babel */
2+
/* global __dirname */
3+
4+
import path from 'path';
5+
import fs from 'fs';
6+
import HtmlRendererHelper from './html-renderer-helper';
7+
8+
class Panel {
9+
constructor(pluginInstance, htmlRendererHelper = new HtmlRendererHelper()) {
10+
this.htmlRendererHelper = htmlRendererHelper;
11+
this.pluginInstance = pluginInstance;
12+
this.loadingSelector = 'sk-three-bounce';
13+
}
14+
15+
renderBase() {
16+
this.element = document.createElement('div');
17+
this.element.classList.add('ava');
18+
19+
const resolvedPath = path.resolve(__dirname, '../views/panel.html');
20+
this.element.innerHTML = fs.readFileSync(resolvedPath);
21+
22+
this.testsContainer = this.element.getElementsByClassName('tests-container')[0];
23+
const closeIcon = this.element.getElementsByClassName('close-icon')[0];
24+
closeIcon.addEventListener('click', e => this.pluginInstance.closePanel(e), false);
25+
}
26+
27+
renderAssert(assertResult) {
28+
const assert = assertResult.assert;
29+
30+
const newTest = this.htmlRendererHelper.createContainer('test');
31+
newTest.classList.add(this._getCssClassForAssert(assert));
32+
newTest.textContent = `${assert.name}`;
33+
this.testsContainer.appendChild(newTest);
34+
35+
this._updateTestStatisticSection(assertResult);
36+
}
37+
38+
_getCssClassForAssert(assert) {
39+
if (assert.ok) {
40+
return (assert.skip) ? 'skipped' : 'ok';
41+
}
42+
return (assert.todo) ? 'todo' : 'ko';
43+
}
44+
45+
renderFinalReport(results) {
46+
this.hideExecutingIndicator();
47+
48+
const summary = this.htmlRendererHelper.createContainer('summary');
49+
const passed = results.pass - (results.skip ? results.skip : 0);
50+
const percentage = Math.round((passed / results.count) * 100);
51+
summary.textContent = `${results.count} total - ${percentage}% passed`;
52+
53+
this.testsContainer.appendChild(summary);
54+
}
55+
56+
cleanTestsContainer() {
57+
this.testsContainer.innerHTML = '';
58+
}
59+
60+
renderStartProcess(fileName) {
61+
const fileHeader = document.getElementById('file-header');
62+
fileHeader.textContent = fileName;
63+
64+
this.displayExecutingIndicator();
65+
this.cleanTestsContainer();
66+
}
67+
68+
displayExecutingIndicator() {
69+
const executing = document.getElementById(this.loadingSelector);
70+
if (executing) {
71+
executing.style.display = 'block';
72+
}
73+
}
74+
75+
hideExecutingIndicator() {
76+
const executing = document.getElementById(this.loadingSelector);
77+
if (executing) {
78+
executing.style.display = 'none';
79+
}
80+
}
81+
82+
_updateTestStatisticSection(assertResult) {
83+
const passedContainer = document.getElementById('passed');
84+
const failedContainer = document.getElementById('failed');
85+
passedContainer.textContent = assertResult.currentExecution.passed;
86+
failedContainer.textContent = assertResult.currentExecution.failed;
87+
}
88+
89+
serialize() { }
90+
91+
destroy() {
92+
this.element.remove();
93+
}
94+
}
95+
96+
module.exports = Panel;

lib/parser-factory.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @babel */
2+
3+
import parser from 'tap-parser';
4+
5+
class ParserFactory {
6+
getParser() {
7+
return parser();
8+
}
9+
}
10+
11+
module.exports = ParserFactory;

lib/terminal-command-executor.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/** @babel */
2+
3+
import EventEmitter from 'events';
4+
import ChildProcess from 'child_process';
5+
6+
class TerminalCommandExecutor extends EventEmitter {
7+
constructor() {
8+
super();
9+
EventEmitter.call(this);
10+
11+
this.dataReceivedEventName = 'dataReceived';
12+
this.dataFinishedEventName = 'dataFinished';
13+
}
14+
15+
run(command, destinyFolder = null) {
16+
this.command = command;
17+
this.destinyFolder = destinyFolder;
18+
19+
const spawn = ChildProcess.spawn;
20+
21+
this.terminal = spawn('bash', ['-l']);
22+
this.terminal.on('close', statusCode => this._streamClosed(statusCode));
23+
this.terminal.stdout.on('data', data => this._stdOutDataReceived(data));
24+
this.terminal.stderr.on('data', data => this._stdErrDataReceived(data));
25+
26+
const terminalCommand = this.destinyFolder ?
27+
`cd \"${this.destinyFolder}\" && ${this.command}\n` :
28+
`${this.command}\n`;
29+
30+
this.terminal.stdin.write(terminalCommand);
31+
this.terminal.stdin.write('exit\n');
32+
}
33+
34+
_stdOutDataReceived(newData) {
35+
this.emit(this.dataReceivedEventName, newData.toString());
36+
}
37+
38+
_stdErrDataReceived(newData) {
39+
this.emit(this.dataReceivedEventName, newData.toString());
40+
}
41+
42+
_streamClosed(code) {
43+
this.emit(this.dataFinishedEventName, code);
44+
}
45+
}
46+
47+
module.exports = TerminalCommandExecutor;

lib/test-runner-process.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/** @babel */
2+
3+
import EventEmitter from 'events';
4+
import TerminalCommandExecutor from './terminal-command-executor';
5+
import ParserFactory from './parser-factory';
6+
7+
class TestRunnerProcess extends EventEmitter {
8+
constructor(
9+
executor = new TerminalCommandExecutor(),
10+
parserFactory = new ParserFactory()) {
11+
super();
12+
13+
this.eventHandlers = {};
14+
this.terminalCommandExecutor = executor;
15+
this.parserFactory = parserFactory;
16+
17+
this.terminalCommandExecutor.on('dataReceived', data => this._addAvaOutput(data));
18+
this.terminalCommandExecutor.on('dataFinished', () => this._endAvaOutput());
19+
20+
EventEmitter.call(this);
21+
}
22+
23+
canRun() {
24+
return !this.isRunning;
25+
}
26+
27+
run(folder, file) {
28+
if (!this.canRun()) {
29+
return;
30+
}
31+
32+
this.isRunning = true;
33+
this.currentExecution = {passed: 0, failed: 0};
34+
35+
this.parser = this.parserFactory.getParser();
36+
this._setHandlersOnParser(this.parser);
37+
38+
const command = `ava ${file} --tap`;
39+
40+
this.terminalCommandExecutor.run(command, folder);
41+
}
42+
43+
_setHandlersOnParser(parser) {
44+
const instance = this;
45+
parser.on('assert', assert => {
46+
instance._updateCurrentExecution(assert);
47+
const result = {
48+
currentExecution: this.currentExecution, assert
49+
};
50+
instance.emit('assert', result);
51+
});
52+
53+
parser.on('complete', results => this.emit('complete', results));
54+
}
55+
56+
_updateCurrentExecution(assert) {
57+
if (assert.ok) {
58+
if (!assert.skip) {
59+
this.currentExecution.passed++;
60+
}
61+
} else if (!assert.todo) {
62+
this.currentExecution.failed++;
63+
}
64+
}
65+
66+
_addAvaOutput(data) {
67+
this.parser.write(data);
68+
}
69+
70+
_endAvaOutput() {
71+
this.parser.end();
72+
this.isRunning = false;
73+
}
74+
75+
destroy() {
76+
this.isRunning = false;
77+
this.terminalCommandExecutor.destroy();
78+
}
79+
}
80+
81+
module.exports = TestRunnerProcess;

media/logo.png

6.77 KB
Loading

package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "ava",
33
"version": "0.3.2",
44
"description": "Snippets for AVA",
5+
"main": "./lib/main",
56
"license": "MIT",
67
"repository": "sindresorhus/atom-ava",
78
"private": true,
@@ -28,17 +29,33 @@
2829
"scripts": {
2930
"test": "xo"
3031
},
32+
"activationCommands": {
33+
"atom-workspace": [
34+
"ava:toggle",
35+
"ava:run"
36+
]
37+
},
3138
"keywords": [
3239
"snippets",
3340
"test",
3441
"runner",
3542
"ava",
3643
"mocha"
3744
],
45+
"dependencies": {
46+
"tap-parser": "^1.2.2",
47+
"ava": "^0.13.0"
48+
},
3849
"devDependencies": {
3950
"xo": "*"
4051
},
4152
"xo": {
53+
"envs": [
54+
"browser",
55+
"node",
56+
"jasmine",
57+
"atomtest"
58+
],
4259
"esnext": true,
4360
"globals": [
4461
"atom"

0 commit comments

Comments
 (0)