vitest 0.0.28 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.gh.md +51 -15
- package/bin/vitest.mjs +14 -2
- package/dist/integrations/chai/snapshot/manager.d.ts +2 -1
- package/dist/integrations/chai/snapshot/manager.js +4 -2
- package/dist/node/cli.js +36 -10
- package/dist/node/entry.js +5 -0
- package/dist/node/node.d.ts +3 -2
- package/dist/node/node.js +40 -10
- package/dist/reporters/default.d.ts +5 -2
- package/dist/reporters/default.js +33 -19
- package/dist/reporters/error.d.ts +9 -0
- package/dist/reporters/error.js +182 -0
- package/dist/run/index.js +26 -17
- package/dist/types.d.ts +12 -3
- package/package.json +20 -8
package/README.gh.md
CHANGED
|
@@ -9,6 +9,8 @@ A blazing fast unit test framework powered by Vite.
|
|
|
9
9
|
|
|
10
10
|
> ⚠️ **DISCLAIMER**: Vitest is still in development and not stable yet. It's not recommended to use it in production.
|
|
11
11
|
|
|
12
|
+
> Vitest requires Vite v2.7.0 or above
|
|
13
|
+
|
|
12
14
|
## Features
|
|
13
15
|
|
|
14
16
|
- [Vite](https://vitejs.dev/)'s config, transformers, resolvers, and plugins.
|
|
@@ -20,6 +22,7 @@ A blazing fast unit test framework powered by Vite.
|
|
|
20
22
|
- ESM friendly
|
|
21
23
|
- Out-of-box TypeScript support
|
|
22
24
|
- Suite and Test filtering (skip, only, todo)
|
|
25
|
+
- [Test coverage](#coverage)
|
|
23
26
|
|
|
24
27
|
```ts
|
|
25
28
|
import { it, describe, expect, assert } from 'vitest'
|
|
@@ -44,6 +47,12 @@ describe('suite name', () => {
|
|
|
44
47
|
$ npx vitest
|
|
45
48
|
```
|
|
46
49
|
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
- [Unit Testing](./test/core)
|
|
53
|
+
- [Vue Component Testing](./test/vue)
|
|
54
|
+
- [React Component Testing](./test/react)
|
|
55
|
+
|
|
47
56
|
## Configuration
|
|
48
57
|
|
|
49
58
|
`vitest` will read your root `vite.config.ts` when it present to match with the plugins and setup as your Vite app. If you want to it to have a different configuration for testing, you could either:
|
|
@@ -82,7 +91,7 @@ export default defineConfig({
|
|
|
82
91
|
|
|
83
92
|
To get TypeScript working with the global APIs, add `vitest/global` to the `types` filed in your `tsconfig.json`
|
|
84
93
|
|
|
85
|
-
```
|
|
94
|
+
```jsonc
|
|
86
95
|
// tsconfig.json
|
|
87
96
|
{
|
|
88
97
|
"compilerOptions": {
|
|
@@ -114,10 +123,50 @@ export default defineConfig({
|
|
|
114
123
|
$ vitest -w
|
|
115
124
|
```
|
|
116
125
|
|
|
117
|
-
Vitest
|
|
126
|
+
Vitest smartly searches the module graph and only rerun the related tests (just like how HMR works in Vite!).
|
|
127
|
+
|
|
128
|
+
## Coverage
|
|
129
|
+
|
|
130
|
+
Vitest works perfectly with [c8](https://github.com/bcoe/c8)
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
$ c8 vitest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"scripts": {
|
|
139
|
+
"test": "vitest",
|
|
140
|
+
"coverage": "c8 vitest"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For convenience, we also provide a shorthand for passing `--coverage` option to CLI, which will wrap the process with `c8` for you. Note when using the shorthand, you will lose the ability to pass additional options to `c8`.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
$ vitest --coverage
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For more configuration avaliable, please refer to [c8](https://github.com/bcoe/c8)'s documentation.
|
|
118
152
|
|
|
119
153
|
## Filtering
|
|
120
154
|
|
|
155
|
+
### CLI
|
|
156
|
+
|
|
157
|
+
You can use CLI to filter test files my name:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
$ vitest basic
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Will only execute test files that contain `basic`, e.g.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
basic.test.ts
|
|
167
|
+
basic-foo.test.ts
|
|
168
|
+
```
|
|
169
|
+
|
|
121
170
|
### Skipping suites and tasks
|
|
122
171
|
|
|
123
172
|
Use `.skip` to avoid running certain suites or tests
|
|
@@ -177,19 +226,6 @@ describe('suite', () => {
|
|
|
177
226
|
})
|
|
178
227
|
```
|
|
179
228
|
|
|
180
|
-
## TODO
|
|
181
|
-
|
|
182
|
-
- [x] Reporter & Better output
|
|
183
|
-
- [x] Task filter
|
|
184
|
-
- [x] Mock
|
|
185
|
-
- [x] Global Mode & Types
|
|
186
|
-
- [ ] Parallel Executing
|
|
187
|
-
- [x] CLI Help
|
|
188
|
-
- [x] JSDom
|
|
189
|
-
- [x] Watch
|
|
190
|
-
- [ ] Source Map
|
|
191
|
-
- [ ] Coverage
|
|
192
|
-
|
|
193
229
|
## Sponsors
|
|
194
230
|
|
|
195
231
|
<p align="center">
|
package/bin/vitest.mjs
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
'use strict'
|
|
3
2
|
|
|
4
|
-
import '
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { resolve } from 'path'
|
|
5
|
+
|
|
6
|
+
const argv = process.argv.slice(2)
|
|
7
|
+
const filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const entry = resolve(filename, '../../dist/node/cli.js')
|
|
9
|
+
|
|
10
|
+
if (argv.includes('--coverage')) {
|
|
11
|
+
process.argv.splice(2, 0, process.argv[0], entry)
|
|
12
|
+
await import('c8/bin/c8.js')
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
await import('../dist/node/cli.js')
|
|
16
|
+
}
|
|
@@ -58,6 +58,9 @@ export class SnapshotManager {
|
|
|
58
58
|
expect(actual.trim()).equals(expected ? expected.trim() : '', message || `Snapshot name: \`${key}\``);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
clear() {
|
|
62
|
+
this.snapshotSummary = makeEmptySnapshotSummary(this.snapshotOptions);
|
|
63
|
+
}
|
|
61
64
|
saveSnap() {
|
|
62
65
|
if (!this.testFile || !this.snapshotState)
|
|
63
66
|
return;
|
|
@@ -69,7 +72,6 @@ export class SnapshotManager {
|
|
|
69
72
|
report() {
|
|
70
73
|
const outputs = getSnapshotSummaryOutput(this.rootDir, this.snapshotSummary);
|
|
71
74
|
if (outputs.length > 1)
|
|
72
|
-
|
|
73
|
-
console.log(`\n${outputs.join('\n')}`);
|
|
75
|
+
return outputs;
|
|
74
76
|
}
|
|
75
77
|
}
|
package/dist/node/cli.js
CHANGED
|
@@ -19,13 +19,19 @@ sade('vitest [filter]', true)
|
|
|
19
19
|
.option('--global', 'inject apis globally', false)
|
|
20
20
|
.option('--dev', 'dev mode', false)
|
|
21
21
|
.option('--jsdom', 'mock browser api using JSDOM', false)
|
|
22
|
-
.action(async (filters,
|
|
22
|
+
.action(async (filters, argv) => {
|
|
23
23
|
process.env.VITEST = 'true';
|
|
24
|
+
const defaultInline = [
|
|
25
|
+
'vue',
|
|
26
|
+
'@vue',
|
|
27
|
+
'diff',
|
|
28
|
+
];
|
|
24
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const root = resolve(
|
|
26
|
-
const configPath =
|
|
27
|
-
? resolve(root,
|
|
30
|
+
const root = resolve(argv.root || process.cwd());
|
|
31
|
+
const configPath = argv.config
|
|
32
|
+
? resolve(root, argv.config)
|
|
28
33
|
: await findUp(['vitest.config.ts', 'vitest.config.js', 'vitest.config.mjs', 'vite.config.ts', 'vite.config.js', 'vite.config.mjs'], { cwd: root });
|
|
34
|
+
const options = argv;
|
|
29
35
|
options.config = configPath;
|
|
30
36
|
options.root = root;
|
|
31
37
|
options.filters = filters
|
|
@@ -39,7 +45,7 @@ sade('vitest [filter]', true)
|
|
|
39
45
|
await startViteNode({
|
|
40
46
|
root,
|
|
41
47
|
files: [
|
|
42
|
-
resolve(__dirname,
|
|
48
|
+
resolve(__dirname, argv.dev ? '../../src/node/entry.ts' : './entry.js'),
|
|
43
49
|
],
|
|
44
50
|
config: configPath,
|
|
45
51
|
defaultConfig: {
|
|
@@ -49,11 +55,31 @@ sade('vitest [filter]', true)
|
|
|
49
55
|
],
|
|
50
56
|
},
|
|
51
57
|
},
|
|
52
|
-
shouldExternalize(id) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
shouldExternalize(id, server) {
|
|
59
|
+
var _a, _b, _c, _d;
|
|
60
|
+
const inline = ['vitest', ...defaultInline, ...((_b = (_a = server.config.test) === null || _a === void 0 ? void 0 : _a.deps) === null || _b === void 0 ? void 0 : _b.inline) || []];
|
|
61
|
+
const external = ((_d = (_c = server.config.test) === null || _c === void 0 ? void 0 : _c.deps) === null || _d === void 0 ? void 0 : _d.external) || [];
|
|
62
|
+
for (const ex of inline) {
|
|
63
|
+
if (typeof ex === 'string') {
|
|
64
|
+
if (id.includes(`/node_modules/${ex}/`))
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (ex.test(id))
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const ex of external) {
|
|
73
|
+
if (typeof ex === 'string') {
|
|
74
|
+
if (id.includes(`/node_modules/${ex}/`))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (ex.test(id))
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return id.includes('/node_modules/');
|
|
57
83
|
},
|
|
58
84
|
});
|
|
59
85
|
})
|
package/dist/node/entry.js
CHANGED
|
@@ -5,3 +5,8 @@ const inlineOptions = process.__vite_node__.server.config.test || {};
|
|
|
5
5
|
const cliOptions = process.__vitest__.options || {};
|
|
6
6
|
const options = Object.assign(Object.assign({}, cliOptions), inlineOptions);
|
|
7
7
|
await run(options);
|
|
8
|
+
const timer = setTimeout(() => {
|
|
9
|
+
// TODO: warn user and maybe error out
|
|
10
|
+
process.exit();
|
|
11
|
+
}, 500);
|
|
12
|
+
timer.unref();
|
package/dist/node/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InlineConfig, ViteDevServer } from 'vite';
|
|
1
|
+
import { InlineConfig, ViteDevServer, TransformResult } from 'vite';
|
|
2
2
|
declare global {
|
|
3
3
|
namespace NodeJS {
|
|
4
4
|
interface Process {
|
|
@@ -6,6 +6,7 @@ declare global {
|
|
|
6
6
|
server: ViteDevServer;
|
|
7
7
|
watch?: boolean;
|
|
8
8
|
moduleCache: Map<string, Promise<any>>;
|
|
9
|
+
modulesTransformResult: Map<string, TransformResult>;
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
}
|
|
@@ -15,7 +16,7 @@ export interface ViteNodeOptions {
|
|
|
15
16
|
root: string;
|
|
16
17
|
files: string[];
|
|
17
18
|
_?: string[];
|
|
18
|
-
shouldExternalize?: (file: string) => boolean;
|
|
19
|
+
shouldExternalize?: (file: string, server: ViteDevServer) => boolean;
|
|
19
20
|
config?: string;
|
|
20
21
|
defaultConfig?: InlineConfig;
|
|
21
22
|
}
|
package/dist/node/node.js
CHANGED
|
@@ -5,7 +5,8 @@ import vm from 'vm';
|
|
|
5
5
|
import { createServer, mergeConfig } from 'vite';
|
|
6
6
|
import c from 'picocolors';
|
|
7
7
|
const { red, dim, yellow } = c;
|
|
8
|
-
const
|
|
8
|
+
const moduleCache = new Map();
|
|
9
|
+
const modulesTransformResult = new Map();
|
|
9
10
|
export async function run(argv) {
|
|
10
11
|
process.exitCode = 0;
|
|
11
12
|
const root = argv.root || process.cwd();
|
|
@@ -22,7 +23,8 @@ export async function run(argv) {
|
|
|
22
23
|
await server.pluginContainer.buildStart({});
|
|
23
24
|
process.__vite_node__ = {
|
|
24
25
|
server,
|
|
25
|
-
moduleCache
|
|
26
|
+
moduleCache,
|
|
27
|
+
modulesTransformResult,
|
|
26
28
|
};
|
|
27
29
|
try {
|
|
28
30
|
await execute(files, server, argv);
|
|
@@ -42,6 +44,8 @@ function normalizeId(id) {
|
|
|
42
44
|
id = `\0${id.slice('/@id/__x00__'.length)}`;
|
|
43
45
|
if (id && id.startsWith('/@id/'))
|
|
44
46
|
id = id.slice('/@id/'.length);
|
|
47
|
+
if (id.startsWith('__vite-browser-external:'))
|
|
48
|
+
id = id.slice('__vite-browser-external:'.length);
|
|
45
49
|
return id;
|
|
46
50
|
}
|
|
47
51
|
function toFilePath(id, server) {
|
|
@@ -58,6 +62,29 @@ function toFilePath(id, server) {
|
|
|
58
62
|
absolute = `/${absolute}`;
|
|
59
63
|
return absolute;
|
|
60
64
|
}
|
|
65
|
+
const stubRequests = {
|
|
66
|
+
'/@vite/client': {
|
|
67
|
+
injectQuery: (id) => id,
|
|
68
|
+
createHotContext() {
|
|
69
|
+
return {
|
|
70
|
+
accept: () => { },
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
async function transform(server, id) {
|
|
76
|
+
if (id.match(/\.(?:[cm]?[jt]sx?|json)$/)) {
|
|
77
|
+
return await server.transformRequest(id, { ssr: true });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// for components like Vue, we want to use the client side
|
|
81
|
+
// plugins but then covert the code to be consumed by the server
|
|
82
|
+
const result = await server.transformRequest(id);
|
|
83
|
+
if (!result)
|
|
84
|
+
return undefined;
|
|
85
|
+
return await server.ssrTransform(result.code, result.map, id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
61
88
|
async function execute(files, server, options) {
|
|
62
89
|
const result = [];
|
|
63
90
|
for (const file of files)
|
|
@@ -74,9 +101,12 @@ async function execute(files, server, options) {
|
|
|
74
101
|
}
|
|
75
102
|
return cachedRequest(dep, callstack);
|
|
76
103
|
};
|
|
77
|
-
|
|
104
|
+
if (id in stubRequests)
|
|
105
|
+
return stubRequests[id];
|
|
106
|
+
const result = await transform(server, id);
|
|
78
107
|
if (!result)
|
|
79
108
|
throw new Error(`failed to load ${id}`);
|
|
109
|
+
modulesTransformResult.set(id, result);
|
|
80
110
|
const url = pathToFileURL(fsPath);
|
|
81
111
|
const exports = {};
|
|
82
112
|
const context = {
|
|
@@ -97,16 +127,16 @@ async function execute(files, server, options) {
|
|
|
97
127
|
return exports;
|
|
98
128
|
}
|
|
99
129
|
async function cachedRequest(rawId, callstack) {
|
|
100
|
-
if (builtinModules.includes(rawId))
|
|
101
|
-
return import(rawId);
|
|
102
130
|
const id = normalizeId(rawId);
|
|
131
|
+
if (builtinModules.includes(id))
|
|
132
|
+
return import(id);
|
|
103
133
|
const fsPath = toFilePath(id, server);
|
|
104
|
-
if (options.shouldExternalize(fsPath))
|
|
134
|
+
if (options.shouldExternalize(fsPath, server))
|
|
105
135
|
return import(fsPath);
|
|
106
|
-
if (
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
return await
|
|
136
|
+
if (moduleCache.has(fsPath))
|
|
137
|
+
return moduleCache.get(fsPath);
|
|
138
|
+
moduleCache.set(fsPath, directRequest(id, fsPath, callstack));
|
|
139
|
+
return await moduleCache.get(fsPath);
|
|
110
140
|
}
|
|
111
141
|
function exportAll(exports, sourceModule) {
|
|
112
142
|
// eslint-disable-next-line no-restricted-syntax
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Listr from 'listr';
|
|
2
|
-
import { File, Reporter, RunnerContext, Task } from '../types';
|
|
2
|
+
import { File, Reporter, RunnerContext, Task, ResolvedConfig } from '../types';
|
|
3
3
|
interface TaskPromise {
|
|
4
4
|
promise: Promise<void>;
|
|
5
5
|
resolve: () => void;
|
|
@@ -11,9 +11,12 @@ export declare class DefaultReporter implements Reporter {
|
|
|
11
11
|
listr: Listr | null;
|
|
12
12
|
listrPromise: Promise<void> | null;
|
|
13
13
|
taskMap: Map<Task, TaskPromise>;
|
|
14
|
+
cwd: string;
|
|
15
|
+
relative(path: string): string;
|
|
16
|
+
onStart(config: ResolvedConfig): void;
|
|
14
17
|
onCollected(files: File[]): void;
|
|
15
18
|
onTaskEnd(task: Task): void;
|
|
16
|
-
onFinished(ctx: RunnerContext): Promise<void>;
|
|
19
|
+
onFinished(ctx: RunnerContext, files?: File[]): Promise<void>;
|
|
17
20
|
onWatcherStart(ctx: RunnerContext): Promise<void>;
|
|
18
21
|
onWatcherRerun(files: string[], trigger: string): Promise<void>;
|
|
19
22
|
onSnapshotUpdate(): void;
|
|
@@ -3,6 +3,7 @@ import { performance } from 'perf_hooks';
|
|
|
3
3
|
import { relative } from 'path';
|
|
4
4
|
import c from 'picocolors';
|
|
5
5
|
import Listr from 'listr';
|
|
6
|
+
import { printError } from './error';
|
|
6
7
|
const CROSS = '✖ ';
|
|
7
8
|
export class DefaultReporter {
|
|
8
9
|
constructor() {
|
|
@@ -11,6 +12,14 @@ export class DefaultReporter {
|
|
|
11
12
|
this.listr = null;
|
|
12
13
|
this.listrPromise = null;
|
|
13
14
|
this.taskMap = new Map();
|
|
15
|
+
this.cwd = process.cwd();
|
|
16
|
+
}
|
|
17
|
+
relative(path) {
|
|
18
|
+
return relative(this.cwd, path);
|
|
19
|
+
}
|
|
20
|
+
onStart(config) {
|
|
21
|
+
this.cwd = config.root;
|
|
22
|
+
console.log(c.green(`Running tests under ${c.gray(this.cwd)}\n`));
|
|
14
23
|
}
|
|
15
24
|
onCollected(files) {
|
|
16
25
|
this.start = performance.now();
|
|
@@ -41,7 +50,7 @@ export class DefaultReporter {
|
|
|
41
50
|
};
|
|
42
51
|
this.listr = new Listr(files.map((file) => {
|
|
43
52
|
return {
|
|
44
|
-
title: relative(
|
|
53
|
+
title: this.relative(file.filepath),
|
|
45
54
|
task: () => {
|
|
46
55
|
if (file.error)
|
|
47
56
|
throw file.error;
|
|
@@ -69,11 +78,16 @@ export class DefaultReporter {
|
|
|
69
78
|
else
|
|
70
79
|
(_b = this.taskMap.get(task)) === null || _b === void 0 ? void 0 : _b.resolve();
|
|
71
80
|
}
|
|
72
|
-
async onFinished(ctx) {
|
|
81
|
+
async onFinished(ctx, files = ctx.files) {
|
|
82
|
+
var _a;
|
|
73
83
|
await this.listrPromise;
|
|
74
84
|
this.end = performance.now();
|
|
75
85
|
console.log();
|
|
76
|
-
const
|
|
86
|
+
const snapshot = ctx.snapshotManager.report();
|
|
87
|
+
if (snapshot)
|
|
88
|
+
console.log(snapshot.join('\n'));
|
|
89
|
+
const suites = files.flatMap(i => i.suites);
|
|
90
|
+
const tasks = suites.flatMap(i => i.tasks);
|
|
77
91
|
const failedFiles = files.filter(i => i.error);
|
|
78
92
|
const failedSuites = suites.filter(i => i.error);
|
|
79
93
|
const runnable = tasks.filter(i => i.state === 'pass' || i.state === 'fail');
|
|
@@ -82,30 +96,30 @@ export class DefaultReporter {
|
|
|
82
96
|
const skipped = tasks.filter(i => i.state === 'skip');
|
|
83
97
|
const todo = tasks.filter(i => i.state === 'todo');
|
|
84
98
|
if (failedFiles.length) {
|
|
85
|
-
console.error(c.bold(`\nFailed to parse ${failedFiles.length} files:`));
|
|
86
|
-
|
|
87
|
-
console.error(c.red(
|
|
88
|
-
|
|
99
|
+
console.error(c.red(c.bold(`\nFailed to parse ${failedFiles.length} files:`)));
|
|
100
|
+
for (const file of failedFiles)
|
|
101
|
+
console.error(c.red(`- ${file.filepath}`));
|
|
102
|
+
console.log();
|
|
103
|
+
for (const file of failedFiles) {
|
|
104
|
+
await printError(file.error);
|
|
89
105
|
console.log();
|
|
90
|
-
}
|
|
106
|
+
}
|
|
91
107
|
}
|
|
92
108
|
if (failedSuites.length) {
|
|
93
109
|
console.error(c.bold(c.red(`\nFailed to run ${failedSuites.length} suites:`)));
|
|
94
|
-
failedSuites
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.error(i.error || 'Unknown error');
|
|
110
|
+
for (const suite of failedSuites) {
|
|
111
|
+
console.error(c.red(`\n- ${(_a = suite.file) === null || _a === void 0 ? void 0 : _a.filepath} > ${suite.name}`));
|
|
112
|
+
await printError(suite.error);
|
|
98
113
|
console.log();
|
|
99
|
-
}
|
|
114
|
+
}
|
|
100
115
|
}
|
|
101
116
|
if (failed.length) {
|
|
102
117
|
console.error(c.bold(c.red(`\nFailed Tests (${failed.length})`)));
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.error(task.error || 'Unknown error');
|
|
118
|
+
for (const task of failed) {
|
|
119
|
+
console.error(`${c.red(`\n${CROSS + c.inverse(' FAIL ')}`)} ${[task.suite.name, task.name].filter(Boolean).join(' > ')}`);
|
|
120
|
+
await printError(task.error);
|
|
107
121
|
console.log();
|
|
108
|
-
}
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
124
|
console.log(c.bold(c.green(`Passed ${passed.length} / ${runnable.length}`)));
|
|
111
125
|
if (failed.length)
|
|
@@ -127,7 +141,7 @@ export class DefaultReporter {
|
|
|
127
141
|
async onWatcherRerun(files, trigger) {
|
|
128
142
|
await this.listrPromise;
|
|
129
143
|
console.clear();
|
|
130
|
-
console.log(c.blue('Re-running tests...') + c.dim(` [ ${relative(
|
|
144
|
+
console.log(c.blue('Re-running tests...') + c.dim(` [ ${this.relative(trigger)} ]\n`));
|
|
131
145
|
}
|
|
132
146
|
// TODO:
|
|
133
147
|
onSnapshotUpdate() {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function printError(error: unknown): Promise<void>;
|
|
2
|
+
interface Poisition {
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function posToNumber(source: string, pos: number | Poisition): number;
|
|
7
|
+
export declare function numberToPos(source: string, offset: number | Poisition): Poisition;
|
|
8
|
+
export declare function generateCodeFrame(source: string, start?: number | Poisition, end?: number, range?: number): string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { promises as fs, existsSync } from 'fs';
|
|
3
|
+
import c from 'picocolors';
|
|
4
|
+
import * as diff from 'diff';
|
|
5
|
+
import { notNullish } from '@antfu/utils';
|
|
6
|
+
import { SourceMapConsumer } from 'source-map';
|
|
7
|
+
export async function printError(error) {
|
|
8
|
+
if (!(error instanceof Error)) {
|
|
9
|
+
console.error(error);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const { modulesTransformResult } = process.__vite_node__;
|
|
13
|
+
const e = error;
|
|
14
|
+
let codeFramePrinted = false;
|
|
15
|
+
const stacks = parseStack(e.stack || '');
|
|
16
|
+
const nearest = stacks.find(stack => modulesTransformResult.has(stack.file));
|
|
17
|
+
if (nearest) {
|
|
18
|
+
const transformResult = modulesTransformResult.get(nearest.file);
|
|
19
|
+
const pos = await getOriginalPos(transformResult === null || transformResult === void 0 ? void 0 : transformResult.map, nearest);
|
|
20
|
+
if (pos && existsSync(nearest.file)) {
|
|
21
|
+
const sourceCode = await fs.readFile(nearest.file, 'utf-8');
|
|
22
|
+
console.error(`${c.red(`${c.bold(e.name)}: ${e.message}`)}`);
|
|
23
|
+
console.log(c.gray(`${nearest.file}:${pos.line}:${pos.column}`));
|
|
24
|
+
console.log(c.yellow(generateCodeFrame(sourceCode, pos)));
|
|
25
|
+
codeFramePrinted = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!codeFramePrinted)
|
|
29
|
+
console.error(e);
|
|
30
|
+
if (e.showDiff)
|
|
31
|
+
console.error(c.gray(generateDiff(stringify(e.actual), stringify(e.expected))));
|
|
32
|
+
}
|
|
33
|
+
function getOriginalPos(map, { line, column }) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
if (!map)
|
|
36
|
+
return resolve(null);
|
|
37
|
+
SourceMapConsumer.with(map, null, (consumer) => {
|
|
38
|
+
const pos = consumer.originalPositionFor({ line, column });
|
|
39
|
+
if (pos.line != null && pos.column != null)
|
|
40
|
+
resolve(pos);
|
|
41
|
+
else
|
|
42
|
+
resolve(null);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const splitRE = /\r?\n/;
|
|
47
|
+
export function posToNumber(source, pos) {
|
|
48
|
+
if (typeof pos === 'number')
|
|
49
|
+
return pos;
|
|
50
|
+
const lines = source.split(splitRE);
|
|
51
|
+
const { line, column } = pos;
|
|
52
|
+
let start = 0;
|
|
53
|
+
for (let i = 0; i < line - 1; i++)
|
|
54
|
+
start += lines[i].length + 1;
|
|
55
|
+
return start + column;
|
|
56
|
+
}
|
|
57
|
+
export function numberToPos(source, offset) {
|
|
58
|
+
if (typeof offset !== 'number')
|
|
59
|
+
return offset;
|
|
60
|
+
if (offset > source.length) {
|
|
61
|
+
throw new Error(`offset is longer than source length! offset ${offset} > length ${source.length}`);
|
|
62
|
+
}
|
|
63
|
+
const lines = source.split(splitRE);
|
|
64
|
+
let counted = 0;
|
|
65
|
+
let line = 0;
|
|
66
|
+
let column = 0;
|
|
67
|
+
for (; line < lines.length; line++) {
|
|
68
|
+
const lineLength = lines[line].length + 1;
|
|
69
|
+
if (counted + lineLength >= offset) {
|
|
70
|
+
column = offset - counted + 1;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
counted += lineLength;
|
|
74
|
+
}
|
|
75
|
+
return { line: line + 1, column };
|
|
76
|
+
}
|
|
77
|
+
export function generateCodeFrame(source, start = 0, end, range = 2) {
|
|
78
|
+
start = posToNumber(source, start);
|
|
79
|
+
end = end || start;
|
|
80
|
+
const lines = source.split(splitRE);
|
|
81
|
+
let count = 0;
|
|
82
|
+
const res = [];
|
|
83
|
+
for (let i = 0; i < lines.length; i++) {
|
|
84
|
+
count += lines[i].length + 1;
|
|
85
|
+
if (count >= start) {
|
|
86
|
+
for (let j = i - range; j <= i + range || end > count; j++) {
|
|
87
|
+
if (j < 0 || j >= lines.length)
|
|
88
|
+
continue;
|
|
89
|
+
const line = j + 1;
|
|
90
|
+
res.push(`${c.gray(`${line}${' '.repeat(Math.max(3 - String(line).length, 0))}|`)} ${lines[j]}`);
|
|
91
|
+
const lineLength = lines[j].length;
|
|
92
|
+
if (j === i) {
|
|
93
|
+
// push underline
|
|
94
|
+
const pad = start - (count - lineLength) + 1;
|
|
95
|
+
const length = Math.max(1, end > count ? lineLength - pad : end - start);
|
|
96
|
+
res.push(`${c.gray(' |')} ${' '.repeat(pad)}${'^'.repeat(length)}`);
|
|
97
|
+
}
|
|
98
|
+
else if (j > i) {
|
|
99
|
+
if (end > count) {
|
|
100
|
+
const length = Math.max(Math.min(end - count, lineLength), 1);
|
|
101
|
+
res.push(`${c.gray(' |')} ${'^'.repeat(length)}`);
|
|
102
|
+
}
|
|
103
|
+
count += lineLength + 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return res.join('\n');
|
|
110
|
+
}
|
|
111
|
+
function stringify(obj) {
|
|
112
|
+
// TODO: handle more types
|
|
113
|
+
return String(obj);
|
|
114
|
+
}
|
|
115
|
+
const stackFnCallRE = /at (.*) \((.+):(\d+):(\d+)\)$/;
|
|
116
|
+
const stackBarePathRE = /at ()(.+):(\d+):(\d+)$/;
|
|
117
|
+
function parseStack(stack) {
|
|
118
|
+
const lines = stack.split('\n');
|
|
119
|
+
const stackFrames = lines.map((raw) => {
|
|
120
|
+
const line = raw.trim();
|
|
121
|
+
const match = line.match(stackFnCallRE) || line.match(stackBarePathRE);
|
|
122
|
+
if (!match)
|
|
123
|
+
return null;
|
|
124
|
+
let file = match[2];
|
|
125
|
+
if (file.startsWith('file://'))
|
|
126
|
+
file = file.slice(7);
|
|
127
|
+
return {
|
|
128
|
+
method: match[1],
|
|
129
|
+
file: match[2],
|
|
130
|
+
line: parseInt(match[3]),
|
|
131
|
+
column: parseInt(match[4]),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
return stackFrames.filter(notNullish);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns a diff between 2 strings with coloured ANSI output.
|
|
138
|
+
*
|
|
139
|
+
* @description
|
|
140
|
+
* The diff will be either inline or unified dependent on the value
|
|
141
|
+
* of `Base.inlineDiff`.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} actual
|
|
144
|
+
* @param {string} expected
|
|
145
|
+
* @return {string} Diff
|
|
146
|
+
*/
|
|
147
|
+
function generateDiff(actual, expected) {
|
|
148
|
+
const diffSize = 2048;
|
|
149
|
+
if (actual.length > diffSize)
|
|
150
|
+
actual = `${actual.substring(0, diffSize)} ... Lines skipped`;
|
|
151
|
+
if (expected.length > diffSize)
|
|
152
|
+
expected = `${expected.substring(0, diffSize)} ... Lines skipped`;
|
|
153
|
+
return unifiedDiff(actual, expected);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Returns unified diff between two strings with coloured ANSI output.
|
|
157
|
+
*
|
|
158
|
+
* @private
|
|
159
|
+
* @param {String} actual
|
|
160
|
+
* @param {String} expected
|
|
161
|
+
* @return {string} The diff.
|
|
162
|
+
*/
|
|
163
|
+
function unifiedDiff(actual, expected) {
|
|
164
|
+
const indent = ' ';
|
|
165
|
+
function cleanUp(line) {
|
|
166
|
+
if (line[0] === '+')
|
|
167
|
+
return indent + c.green(`${line[0]} ${line.slice(1)}`);
|
|
168
|
+
if (line[0] === '-')
|
|
169
|
+
return indent + c.red(`${line[0]} ${line.slice(1)}`);
|
|
170
|
+
if (line.match(/@@/))
|
|
171
|
+
return '--';
|
|
172
|
+
if (line.match(/\\ No newline/))
|
|
173
|
+
return null;
|
|
174
|
+
return indent + line;
|
|
175
|
+
}
|
|
176
|
+
const msg = diff.createPatch('string', actual, expected);
|
|
177
|
+
const lines = msg.split('\n').splice(5);
|
|
178
|
+
return (`\n${indent}${c.red('- actual')}\n${indent}${c.green('+ expected')}\n\n${lines.map(cleanUp).filter(notBlank).join('\n')}`);
|
|
179
|
+
}
|
|
180
|
+
function notBlank(line) {
|
|
181
|
+
return typeof line !== 'undefined' && line !== null;
|
|
182
|
+
}
|
package/dist/run/index.js
CHANGED
|
@@ -163,6 +163,7 @@ export async function run(config) {
|
|
|
163
163
|
(await import('../integrations/jsdom')).setupJSDOM(globalThis);
|
|
164
164
|
await ((_b = reporter.onStart) === null || _b === void 0 ? void 0 : _b.call(reporter, config));
|
|
165
165
|
const filesMap = await collectFiles(testFilepaths);
|
|
166
|
+
const snapshotManager = getSnapshotManager();
|
|
166
167
|
const ctx = {
|
|
167
168
|
filesMap,
|
|
168
169
|
get files() {
|
|
@@ -177,24 +178,25 @@ export async function run(config) {
|
|
|
177
178
|
.reduce((tasks, suite) => tasks.concat(suite.tasks), []);
|
|
178
179
|
},
|
|
179
180
|
config,
|
|
180
|
-
reporter
|
|
181
|
+
reporter,
|
|
182
|
+
snapshotManager,
|
|
181
183
|
};
|
|
182
184
|
await runFiles(filesMap, ctx);
|
|
183
|
-
|
|
184
|
-
snapshot === null || snapshot === void 0 ? void 0 : snapshot.saveSnap();
|
|
185
|
-
snapshot === null || snapshot === void 0 ? void 0 : snapshot.report();
|
|
185
|
+
snapshotManager.saveSnap();
|
|
186
186
|
await ((_c = reporter.onFinished) === null || _c === void 0 ? void 0 : _c.call(reporter, ctx));
|
|
187
187
|
if (config.watch)
|
|
188
|
-
startWatcher(ctx);
|
|
188
|
+
await startWatcher(ctx);
|
|
189
189
|
}
|
|
190
190
|
export async function startWatcher(ctx) {
|
|
191
|
-
var _a
|
|
192
|
-
|
|
191
|
+
var _a;
|
|
192
|
+
const { reporter, snapshotManager, filesMap } = ctx;
|
|
193
|
+
await ((_a = reporter.onWatcherStart) === null || _a === void 0 ? void 0 : _a.call(reporter, ctx));
|
|
193
194
|
let timer;
|
|
194
195
|
const changedTests = new Set();
|
|
195
196
|
const seen = new Set();
|
|
196
197
|
const { server, moduleCache } = process.__vite_node__;
|
|
197
198
|
server.watcher.on('change', async (id) => {
|
|
199
|
+
id = normalizePath(id);
|
|
198
200
|
getDependencyTests(id, ctx, changedTests, seen);
|
|
199
201
|
seen.forEach(i => moduleCache.delete(i));
|
|
200
202
|
seen.clear();
|
|
@@ -202,23 +204,30 @@ export async function startWatcher(ctx) {
|
|
|
202
204
|
return;
|
|
203
205
|
clearTimeout(timer);
|
|
204
206
|
timer = setTimeout(async () => {
|
|
205
|
-
var _a, _b, _c
|
|
207
|
+
var _a, _b, _c;
|
|
206
208
|
if (changedTests.size === 0)
|
|
207
209
|
return;
|
|
208
|
-
|
|
210
|
+
snapshotManager.clear();
|
|
209
211
|
const paths = Array.from(changedTests);
|
|
210
212
|
changedTests.clear();
|
|
211
|
-
await ((
|
|
213
|
+
await ((_a = reporter.onWatcherRerun) === null || _a === void 0 ? void 0 : _a.call(reporter, paths, id, ctx));
|
|
212
214
|
paths.forEach(i => moduleCache.delete(i));
|
|
213
|
-
const
|
|
214
|
-
Object.assign(
|
|
215
|
-
await runFiles(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await ((_d = (_c = ctx.reporter).onWatcherStart) === null || _d === void 0 ? void 0 : _d.call(_c, ctx));
|
|
215
|
+
const newFilesMap = await collectFiles(paths);
|
|
216
|
+
Object.assign(filesMap, newFilesMap);
|
|
217
|
+
await runFiles(newFilesMap, ctx);
|
|
218
|
+
snapshotManager.saveSnap();
|
|
219
|
+
await ((_b = reporter.onFinished) === null || _b === void 0 ? void 0 : _b.call(reporter, ctx, Object.values(newFilesMap)));
|
|
220
|
+
await ((_c = reporter.onWatcherStart) === null || _c === void 0 ? void 0 : _c.call(reporter, ctx));
|
|
220
221
|
}, 100);
|
|
221
222
|
});
|
|
223
|
+
// add an empty promise so it never resolves
|
|
224
|
+
await new Promise(() => { });
|
|
225
|
+
}
|
|
226
|
+
function normalizePath(path) {
|
|
227
|
+
const normalized = path.replace(/\\/g, '/');
|
|
228
|
+
if (normalized.startsWith('/'))
|
|
229
|
+
return normalized;
|
|
230
|
+
return `/${normalized}`;
|
|
222
231
|
}
|
|
223
232
|
function getDependencyTests(id, ctx, set = new Set(), seen = new Set()) {
|
|
224
233
|
if (seen.has(id) || set.has(id))
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SnapshotManager } from './integrations/chai/snapshot/manager';
|
|
1
2
|
export declare type Awaitable<T> = Promise<T> | T;
|
|
2
3
|
export interface UserOptions {
|
|
3
4
|
/**
|
|
@@ -11,6 +12,13 @@ export interface UserOptions {
|
|
|
11
12
|
* @default ['**\/node_modules\/**']
|
|
12
13
|
*/
|
|
13
14
|
excludes?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Handling for dependencies inlining or externalizing
|
|
17
|
+
*/
|
|
18
|
+
deps?: {
|
|
19
|
+
external?: (string | RegExp)[];
|
|
20
|
+
inline?: (string | RegExp)[];
|
|
21
|
+
};
|
|
14
22
|
/**
|
|
15
23
|
* Register apis globally
|
|
16
24
|
*
|
|
@@ -52,6 +60,7 @@ export interface UserOptions {
|
|
|
52
60
|
}
|
|
53
61
|
export interface ResolvedConfig extends Required<UserOptions> {
|
|
54
62
|
filters?: string[];
|
|
63
|
+
config?: string;
|
|
55
64
|
}
|
|
56
65
|
export declare type RunMode = 'run' | 'skip' | 'only' | 'todo';
|
|
57
66
|
export declare type TaskState = RunMode | 'pass' | 'fail';
|
|
@@ -108,15 +117,16 @@ export interface RunnerContext {
|
|
|
108
117
|
tasks: Task[];
|
|
109
118
|
config: ResolvedConfig;
|
|
110
119
|
reporter: Reporter;
|
|
120
|
+
snapshotManager: SnapshotManager;
|
|
111
121
|
}
|
|
112
122
|
export interface GlobalContext {
|
|
113
123
|
suites: SuiteCollector[];
|
|
114
124
|
currentSuite: SuiteCollector | null;
|
|
115
125
|
}
|
|
116
126
|
export interface Reporter {
|
|
117
|
-
onStart?: (
|
|
127
|
+
onStart?: (config: ResolvedConfig) => Awaitable<void>;
|
|
118
128
|
onCollected?: (files: File[], ctx: RunnerContext) => Awaitable<void>;
|
|
119
|
-
onFinished?: (ctx: RunnerContext) => Awaitable<void>;
|
|
129
|
+
onFinished?: (ctx: RunnerContext, files?: File[]) => Awaitable<void>;
|
|
120
130
|
onSuiteBegin?: (suite: Suite, ctx: RunnerContext) => Awaitable<void>;
|
|
121
131
|
onSuiteEnd?: (suite: Suite, ctx: RunnerContext) => Awaitable<void>;
|
|
122
132
|
onFileBegin?: (file: File, ctx: RunnerContext) => Awaitable<void>;
|
|
@@ -125,5 +135,4 @@ export interface Reporter {
|
|
|
125
135
|
onTaskEnd?: (task: Task, ctx: RunnerContext) => Awaitable<void>;
|
|
126
136
|
onWatcherStart?: (ctx: RunnerContext) => Awaitable<void>;
|
|
127
137
|
onWatcherRerun?: (files: string[], trigger: string, ctx: RunnerContext) => Awaitable<void>;
|
|
128
|
-
onSnapshotUpdate?: () => Awaitable<void>;
|
|
129
138
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
4
4
|
"description": "A blazing fast unit test framework powered by Vite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vite",
|
|
@@ -44,16 +44,22 @@
|
|
|
44
44
|
"lint": "eslint \"{src,test}/**/*.ts\"",
|
|
45
45
|
"prepublishOnly": "nr build",
|
|
46
46
|
"release": "bumpp --commit --push --tag && esmo scripts/publish.ts",
|
|
47
|
-
"test": "
|
|
48
|
-
"test:
|
|
47
|
+
"test": "run-s test:*",
|
|
48
|
+
"test:core": "node bin/vitest.mjs --dev -r test/core",
|
|
49
|
+
"test:vue": "node bin/vitest.mjs --dev -r test/vue",
|
|
50
|
+
"test:react": "node bin/vitest.mjs --dev -r test/react",
|
|
51
|
+
"coverage": "node bin/vitest.mjs --dev -r test/core --coverage",
|
|
49
52
|
"watch": "tsc -p src/tsconfig.json --watch"
|
|
50
53
|
},
|
|
51
54
|
"dependencies": {
|
|
55
|
+
"@antfu/utils": "^0.3.0",
|
|
52
56
|
"@jest/test-result": "^27.4.2",
|
|
53
57
|
"@types/chai": "^4.2.22",
|
|
54
58
|
"@types/sinon-chai": "^3.2.6",
|
|
59
|
+
"c8": "^7.10.0",
|
|
55
60
|
"chai": "^4.3.4",
|
|
56
61
|
"chai-subset": "^1.6.0",
|
|
62
|
+
"diff": "^5.0.0",
|
|
57
63
|
"fast-glob": "^3.2.7",
|
|
58
64
|
"find-up": "^6.2.0",
|
|
59
65
|
"jest-snapshot": "^27.4.2",
|
|
@@ -63,22 +69,28 @@
|
|
|
63
69
|
"picocolors": "^1.0.0",
|
|
64
70
|
"sade": "^1.7.4",
|
|
65
71
|
"sinon": "^12.0.1",
|
|
66
|
-
"sinon-chai": "^3.7.0"
|
|
72
|
+
"sinon-chai": "^3.7.0",
|
|
73
|
+
"source-map": "^0.7.3"
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"vite": "^2.7.0"
|
|
67
77
|
},
|
|
68
78
|
"devDependencies": {
|
|
69
|
-
"@antfu/eslint-config": "^0.12.
|
|
79
|
+
"@antfu/eslint-config": "^0.12.2",
|
|
70
80
|
"@antfu/ni": "^0.11.0",
|
|
71
81
|
"@types/chai-subset": "^1.3.3",
|
|
82
|
+
"@types/diff": "^5.0.1",
|
|
72
83
|
"@types/jsdom": "^16.2.13",
|
|
73
84
|
"@types/listr": "^0.14.4",
|
|
74
|
-
"@types/node": "^16.11.
|
|
85
|
+
"@types/node": "^16.11.12",
|
|
75
86
|
"@types/sade": "^1.7.3",
|
|
76
87
|
"@types/sinon": "^10.0.6",
|
|
77
88
|
"bumpp": "^7.1.1",
|
|
78
|
-
"eslint": "^8.4.
|
|
89
|
+
"eslint": "^8.4.1",
|
|
79
90
|
"esno": "^0.12.1",
|
|
91
|
+
"npm-run-all": "^4.1.5",
|
|
80
92
|
"rimraf": "^3.0.2",
|
|
81
93
|
"typescript": "^4.5.2",
|
|
82
|
-
"vite": "^2.
|
|
94
|
+
"vite": "^2.7.0"
|
|
83
95
|
}
|
|
84
96
|
}
|