Skip to content

Commit ecf05f9

Browse files
gautamaroraCompuIves
authored andcommitted
Native Integration for Running Unit Tests in CodeSandbox (codesandbox#445)
* Adding native integration for unit tests * Apply jest globals to evaluation * Creating new Test Manager * Hooking Test Runner up with Test Manager * Reset results in test manager on re-evaluation * Fixing calls to manager from test manager * Fixing dispatch for test results to Console * Renaming for clarity (jest-lite, test-runner) * Fixing jest mock export * Update Console logging for test message + Find tests in __tests__ - Added support for nested describes - Added summary messages for test runs - Log failing tests with file path - Added support for finding tests in __tests__ - * Fix for failed message count * Moving globals to Test Runner class * Adding typescript support * Fix manager * Add test time to summary * Remove odd file * Adding myself to contributors
1 parent 3847663 commit ecf05f9

File tree

7 files changed

+282
-8
lines changed

7 files changed

+282
-8
lines changed

.all-contributorsrc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,16 @@
274274
"contributions": [
275275
"code"
276276
]
277+
},
278+
{
279+
"login": "gautamarora",
280+
"name": "Gautam Arora",
281+
"avatar_url": "https://avatars3.githubusercontent.com/u/1215971?v=4",
282+
"profile": "https://twitter.com/gautam",
283+
"contributions": [
284+
"code",
285+
"ideas"
286+
]
277287
}
278288
]
279289
}

packages/app/src/app/components/sandbox/Preview/DevTools/Console/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ class Console extends React.Component<Props, State> {
9090
}
9191
break;
9292
}
93+
case 'test-result': {
94+
const { result, error } = data;
95+
96+
const aggregatedResults = result ? CircularJSON.parse(result) : result;
97+
const { summaryMessage, failedMessages } = aggregatedResults;
98+
99+
if (!error) {
100+
if (aggregatedResults) {
101+
this.addMessage('log', [summaryMessage]);
102+
failedMessages.forEach(t => {
103+
this.addMessage('warn', [t]);
104+
});
105+
} else {
106+
this.addMessage('warn', [undefined], 'return');
107+
}
108+
} else {
109+
this.addMessage('error', [aggregatedResults]);
110+
}
111+
break;
112+
}
93113
default: {
94114
break;
95115
}

packages/app/src/sandbox/compile.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import _debug from 'app/utils/debug';
55
import initializeErrorTransformers from './errors/transformers';
66
import getPreset from './eval';
77
import Manager from './eval/manager';
8+
import TestRunner from './eval/tests/jest-lite';
9+
import transformJSON from './console/transform-json';
810

911
import { resetScreen } from './status-screen';
1012

@@ -192,6 +194,21 @@ async function compile({
192194
initializeResizeListener();
193195
}
194196

197+
//Testing
198+
const ttt = Date.now();
199+
let testRunner = manager.testRunner;
200+
testRunner.initialize();
201+
testRunner.findTests(modules);
202+
await testRunner.runTests();
203+
let aggregatedResults = testRunner.reportResults();
204+
debug(`Test Evaluation time: ${Date.now() - ttt}ms`);
205+
206+
dispatch({
207+
type: 'test-result',
208+
result: transformJSON(aggregatedResults),
209+
});
210+
//End - Testing
211+
195212
debug(`Total time: ${Date.now() - startTime}ms`);
196213

197214
dispatch({

packages/app/src/sandbox/eval/loaders/eval.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,33 @@ export default function(
66
code: string,
77
require: Function,
88
module: Object,
9-
env: Object = {}
9+
env: Object = {},
10+
globals: Object = {}
1011
) {
1112
const exports = module.exports;
1213

1314
const global = window;
1415
const process = buildProcess(env);
1516
window.global = global;
1617

18+
const globalsCode = ', ' + Object.keys(globals).join(', ');
19+
const globalsValues = Object.keys(globals).map(k => globals[k]);
20+
1721
try {
18-
const newCode = `(function evaluate(require, module, exports, process, setImmediate, Buffer, global) {${
19-
code
20-
}\n})`;
22+
const newCode = `(function evaluate(require, module, exports, process, setImmediate, Buffer, global${
23+
globalsCode
24+
}) {${code}\n})`;
2125
// eslint-disable-next-line no-eval
22-
(0, eval)(newCode)(
26+
(0, eval)(newCode).apply(this, [
2327
require,
2428
module,
2529
exports,
2630
process,
2731
setImmediate,
2832
Buffer,
29-
global
30-
);
33+
global,
34+
...globalsValues,
35+
]);
3136

3237
return module.exports;
3338
} catch (e) {

packages/app/src/sandbox/eval/manager.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import coreLibraries from './npm/get-core-libraries';
1616
import getDependencyName from './utils/get-dependency-name';
1717
import DependencyNotFoundError from '../errors/dependency-not-found-error';
1818
import ModuleNotFoundError from '../errors/module-not-found-error';
19+
import TestRunner from './tests/jest-lite';
1920

2021
type Externals = {
2122
[name: string]: string,
@@ -77,6 +78,7 @@ export default class Manager {
7778
webpackHMR: boolean = false;
7879
hardReload: boolean = false;
7980
hmrStatus: 'idle' | 'check' | 'apply' | 'fail' = 'idle';
81+
testRunner: TestRunner;
8082

8183
// List of modules that are being transpiled, to prevent duplicate jobs.
8284
transpileJobs: { [transpiledModuleId: string]: true };
@@ -90,6 +92,7 @@ export default class Manager {
9092
this.cachedPaths = {};
9193
this.transpileJobs = {};
9294
modules.forEach(m => this.addModule(m));
95+
this.testRunner = new TestRunner(this);
9396

9497
if (process.env.NODE_ENV === 'development') {
9598
window.manager = this;
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// @flow
2+
import expect from 'jest-matchers';
3+
import jestMock from 'jest-mock';
4+
import { getCurrentManager } from '../../compile';
5+
6+
const describe = (name, fn) => {
7+
const testRunner = getCurrentManager().testRunner;
8+
testRunner.setCurrentDescribe(name);
9+
fn();
10+
testRunner.resetCurrentDescribe(name);
11+
};
12+
13+
const test = (name, fn) => {
14+
const testRunner = getCurrentManager().testRunner;
15+
let error = false;
16+
try {
17+
fn();
18+
} catch (Error) {
19+
error = true;
20+
testRunner.addResult({ status: 'fail', name });
21+
} finally {
22+
if (!error) {
23+
testRunner.addResult({ status: 'pass', name });
24+
}
25+
}
26+
};
27+
28+
export default class TestRunner {
29+
tests: Array;
30+
aggregatedResults: Object;
31+
currentDescribe: String;
32+
manager: Manager;
33+
startTime: Date;
34+
endTime: Date;
35+
36+
constructor(manager) {
37+
this.tests = [];
38+
this.aggregatedResults = this._makeEmptyAggregatedResults();
39+
this.currentDescribe = '';
40+
this.currentPath = '';
41+
this.manager = manager;
42+
this.startTime = Date.now();
43+
this.endTime = Date.now();
44+
}
45+
46+
_makeEmptyAggregatedResults() {
47+
return {
48+
failedTestSuites: 0,
49+
failedTests: 0,
50+
passedTestSuites: 0,
51+
passedTests: 0,
52+
totalTestSuites: 0,
53+
totalTests: 0,
54+
summaryMessage: '',
55+
failedMessages: [],
56+
results: [],
57+
};
58+
}
59+
60+
initialize() {
61+
this.resetResults();
62+
this.startTime = Date.now();
63+
this.endTime = Date.now();
64+
}
65+
66+
static testGlobals() {
67+
return {
68+
describe,
69+
test,
70+
it: test,
71+
expect,
72+
jest: jestMock,
73+
};
74+
}
75+
76+
findTests(modules) {
77+
this.tests = modules.filter(m => {
78+
let matched = false;
79+
if (
80+
m.path.includes('__tests__') &&
81+
(m.path.endsWith('.js') || m.path.endsWith('.ts'))
82+
) {
83+
matched = true;
84+
}
85+
if (m.path.endsWith('.test.js') || m.path.endsWith('.test.ts')) {
86+
matched = true;
87+
}
88+
if (m.path.endsWith('.spec.js') || m.path.endsWith('.spec.ts')) {
89+
matched = true;
90+
}
91+
return matched;
92+
});
93+
}
94+
95+
async transpileTests() {
96+
for (let t of this.tests) {
97+
await this.manager.transpileModules(t);
98+
}
99+
}
100+
101+
async runTests() {
102+
await this.transpileTests();
103+
this.tests.forEach(t => {
104+
this.setCurrentPath(t.path);
105+
this.manager.evaluateModule(t);
106+
this.resetCurrentPath();
107+
});
108+
}
109+
110+
addResult({ status, name }) {
111+
let describe = this.currentDescribe;
112+
let path = this.currentPath;
113+
114+
this.aggregatedResults.results.push({ status, name, describe, path });
115+
116+
this.aggregatedResults.totalTests++;
117+
if (status === 'pass') {
118+
this.aggregatedResults.passedTests++;
119+
} else {
120+
this.aggregatedResults.failedTests++;
121+
}
122+
let totalTestSuites = new Set();
123+
let failedTestSuites = new Set();
124+
this.aggregatedResults.results.forEach(({ status, path }) => {
125+
totalTestSuites.add(path);
126+
if (status === 'fail') {
127+
failedTestSuites.add(path);
128+
}
129+
});
130+
this.aggregatedResults.totalTestSuites = totalTestSuites.size;
131+
this.aggregatedResults.failedTestSuites = failedTestSuites.size;
132+
this.aggregatedResults.passedTestSuites =
133+
totalTestSuites.size - failedTestSuites.size;
134+
}
135+
136+
reportResults() {
137+
let aggregatedResults = this.aggregatedResults;
138+
let results = this.aggregatedResults.results;
139+
let summaryMessage = '';
140+
let failedMessages = [];
141+
let summaryEmoji = '';
142+
143+
if (aggregatedResults.totalTestSuites === 0) {
144+
return null;
145+
}
146+
147+
results.forEach(({ status, name, describe, path }) => {
148+
if (status === 'fail') {
149+
let message = `FAIL (${path}) `;
150+
if (describe) {
151+
message += `${describe} > `;
152+
}
153+
message += `${name}`;
154+
failedMessages.push(message);
155+
}
156+
});
157+
158+
summaryEmoji =
159+
aggregatedResults.totalTestSuites === aggregatedResults.passedTestSuites
160+
? '😎'
161+
: '👻';
162+
summaryMessage = `Test Summary: ${summaryEmoji}\n\n`;
163+
summaryMessage += 'Test Suites: ';
164+
if (aggregatedResults.failedTestSuites !== null) {
165+
summaryMessage += `${aggregatedResults.failedTestSuites} failed, `;
166+
}
167+
if (aggregatedResults.passedTestSuites !== null) {
168+
summaryMessage += `${aggregatedResults.passedTestSuites} passed, `;
169+
}
170+
summaryMessage += `${aggregatedResults.totalTestSuites} total`;
171+
summaryMessage += '\n';
172+
173+
summaryMessage += 'Tests: ';
174+
if (aggregatedResults.failedTests !== null) {
175+
summaryMessage += `${aggregatedResults.failedTests} failed, `;
176+
}
177+
if (aggregatedResults.passedTests !== null) {
178+
summaryMessage += `${aggregatedResults.passedTests} passed, `;
179+
}
180+
summaryMessage += `${aggregatedResults.totalTests} total`;
181+
summaryMessage += '\n';
182+
183+
this.endTime = Date.now();
184+
this.duration = this.endTime - this.startTime;
185+
summaryMessage += `Time: ${this.duration}ms`;
186+
summaryMessage += '\n';
187+
188+
aggregatedResults.summaryMessage = summaryMessage;
189+
aggregatedResults.failedMessages = failedMessages;
190+
return aggregatedResults;
191+
}
192+
193+
resetResults() {
194+
this.aggregatedResults = this._makeEmptyAggregatedResults();
195+
}
196+
197+
setCurrentDescribe(name) {
198+
if (this.currentDescribe) {
199+
this.currentDescribe += ` > ${name}`;
200+
} else {
201+
this.currentDescribe += `${name}`;
202+
}
203+
}
204+
205+
resetCurrentDescribe() {
206+
this.currentDescribe = '';
207+
}
208+
209+
setCurrentPath(name) {
210+
this.currentPath = name;
211+
}
212+
213+
resetCurrentPath() {
214+
this.currentPath = '';
215+
}
216+
}

packages/app/src/sandbox/eval/transpiled-module.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import type { WarningStructure } from './transpilers/utils/worker-warning-handle
1818
import resolveDependency from './loaders/dependency-resolver';
1919
import evaluate from './loaders/eval';
2020

21+
import TestRunner from './tests/jest-lite';
22+
2123
import Manager from './manager';
2224
import HMR from './hmr';
2325

@@ -661,7 +663,8 @@ export default class TranspiledModule {
661663
this.source.compiledCode,
662664
require,
663665
this.compilation,
664-
manager.envVariables
666+
manager.envVariables,
667+
TestRunner.testGlobals()
665668
);
666669

667670
const hmrConfig = this.hmrConfig;

0 commit comments

Comments
 (0)