Skip to content

Commit bdce0a2

Browse files
authoredMay 2, 2024··
feat: support standalone mode (#5565)
1 parent b9a411d commit bdce0a2

File tree

14 files changed

+187
-73
lines changed

14 files changed

+187
-73
lines changed
 

‎docs/guide/cli-table.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
| `--coverage.cleanOnRerun` | Clean coverage report on watch rerun (default: true) |
2626
| `--coverage.reportsDirectory <path>` | Directory to write coverage report to (default: ./coverage) |
2727
| `--coverage.reporter <name>` | Coverage reporters to use. Visit [`coverage.reporter`](https://vitest.dev/config/#coverage-reporter) for more information (default: `["text", "html", "clover", "json"]`) |
28-
| `--coverage.reportOnFailure` | Generate coverage report even when tests fail (default: false) |
29-
| `--coverage.allowExternal` | Collect coverage of files outside the project root (default: false) |
30-
| `--coverage.skipFull` | Do not show files with 100% statement, branch, and function coverage (default: false) |
28+
| `--coverage.reportOnFailure` | Generate coverage report even when tests fail (default: `false`) |
29+
| `--coverage.allowExternal` | Collect coverage of files outside the project root (default: `false`) |
30+
| `--coverage.skipFull` | Do not show files with 100% statement, branch, and function coverage (default: `false`) |
3131
| `--coverage.thresholds.100` | Shortcut to set all coverage thresholds to 100 (default: `false`) |
3232
| `--coverage.thresholds.perFile` | Check thresholds per file. See `--coverage.thresholds.lines`, `--coverage.thresholds.functions`, `--coverage.thresholds.branches` and `--coverage.thresholds.statements` for the actual thresholds (default: `false`) |
3333
| `--coverage.thresholds.autoUpdate` | Update threshold values: "lines", "functions", "branches" and "statements" to configuration file when current coverage is above the configured thresholds (default: `false`) |
@@ -119,3 +119,4 @@
119119
| `--segfaultRetry <times>` | Retry the test suite if it crashes due to a segfault (default: `true`) |
120120
| `--no-color` | Removes colors from the console output |
121121
| `--clearScreen` | Clear terminal screen when re-running tests during watch mode (default: `true`) |
122+
| `--standalone` | Start Vitest without running tests. File filters will be ignored, tests will be running only on change (default: `false`) |

‎packages/ui/client/composables/client/index.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { WebSocketStatus } from '@vueuse/core'
33
import type { ErrorWithDiff, File, ResolvedConfig } from 'vitest'
44
import type { Ref } from 'vue'
55
import { reactive } from 'vue'
6+
import { relative } from 'pathe'
7+
import { generateHash } from '@vitest/runner/utils'
68
import type { RunState } from '../../../types'
79
import { ENTRY_URL, isReport } from '../../constants'
810
import { parseError } from '../error'
@@ -84,7 +86,29 @@ watch(
8486
client.rpc.getConfig(),
8587
client.rpc.getUnhandledErrors(),
8688
])
87-
client.state.collectFiles(files)
89+
if (_config.standalone) {
90+
const filenames = await client.rpc.getTestFiles()
91+
const files = filenames.map<File>(([name, filepath]) => {
92+
const path = relative(_config.root, filepath)
93+
return {
94+
filepath,
95+
name: path,
96+
id: /* #__PURE__ */ generateHash(`${path}${name || ''}`),
97+
mode: 'skip',
98+
type: 'suite',
99+
result: {
100+
state: 'skip',
101+
},
102+
meta: {},
103+
tasks: [],
104+
projectName: name,
105+
}
106+
})
107+
client.state.collectFiles(files)
108+
}
109+
else {
110+
client.state.collectFiles(files)
111+
}
88112
unhandledErrors.value = (errors || []).map(parseError)
89113
config.value = _config
90114
})

‎packages/vitest/src/api/setup.ts

+4
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi
164164
getProvidedContext() {
165165
return 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.getProvidedContext() : ({} as any)
166166
},
167+
async getTestFiles() {
168+
const spec = await ctx.globTestFiles()
169+
return spec.map(([project, file]) => [project.getName(), file]) as [string, string][]
170+
},
167171
},
168172
{
169173
post: msg => ws.send(msg),

‎packages/vitest/src/api/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface WebSocketHandlers {
1515
getCountOfFailedTests: () => number
1616
sendLog: (log: UserConsoleLog) => void
1717
getFiles: () => File[]
18+
getTestFiles: () => Promise<[name: string, file: string][]>
1819
getPaths: () => string[]
1920
getConfig: () => ResolvedConfig
2021
resolveSnapshotPath: (testPath: string) => string

‎packages/vitest/src/node/cli/cli-api.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,17 @@ export async function startVitest(
8888
})
8989

9090
ctx.onAfterSetServer(() => {
91-
ctx.start(cliFilters)
91+
if (ctx.config.standalone)
92+
ctx.init()
93+
else
94+
ctx.start(cliFilters)
9295
})
9396

9497
try {
95-
await ctx.start(cliFilters)
98+
if (ctx.config.standalone)
99+
await ctx.init()
100+
else
101+
await ctx.start(cliFilters)
96102
}
97103
catch (e) {
98104
process.exitCode = 1

‎packages/vitest/src/node/cli/cli-config.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,13 @@ export const cliOptionsConfig: VitestCLIOptions = {
202202
array: true,
203203
},
204204
reportOnFailure: {
205-
description: 'Generate coverage report even when tests fail (default: false)',
205+
description: 'Generate coverage report even when tests fail (default: `false`)',
206206
},
207207
allowExternal: {
208-
description: 'Collect coverage of files outside the project root (default: false)',
208+
description: 'Collect coverage of files outside the project root (default: `false`)',
209209
},
210210
skipFull: {
211-
description: 'Do not show files with 100% statement, branch, and function coverage (default: false)',
211+
description: 'Do not show files with 100% statement, branch, and function coverage (default: `false`)',
212212
},
213213
thresholds: {
214214
description: null,
@@ -601,6 +601,9 @@ export const cliOptionsConfig: VitestCLIOptions = {
601601
clearScreen: {
602602
description: 'Clear terminal screen when re-running tests during watch mode (default: `true`)',
603603
},
604+
standalone: {
605+
description: 'Start Vitest without running tests. File filters will be ignored, tests will be running only on change (default: `false`)',
606+
},
604607

605608
// disable CLI options
606609
cliExclude: null,

‎packages/vitest/src/node/config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ export function resolveConfig(
136136
resolved.shard = { index, count }
137137
}
138138

139+
if (resolved.standalone && !resolved.watch)
140+
throw new Error(`Vitest standalone mode requires --watch`)
141+
139142
if (resolved.maxWorkers)
140143
resolved.maxWorkers = Number(resolved.maxWorkers)
141144

‎packages/vitest/src/node/core.ts

+40-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { CancelReason, File } from '@vitest/runner'
1313
import { ViteNodeServer } from 'vite-node/server'
1414
import type { defineWorkspace } from 'vitest/config'
1515
import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types'
16-
import { hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils'
16+
import { getTasks, hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils'
1717
import { getCoverageProvider } from '../integrations/coverage'
1818
import { CONFIG_NAMES, configFiles, workspacesFiles as workspaceFiles } from '../constants'
1919
import { rootDir } from '../paths'
@@ -418,6 +418,25 @@ export class Vitest {
418418
await this.report('onWatcherStart')
419419
}
420420

421+
async init() {
422+
this._onClose = []
423+
424+
try {
425+
await this.initCoverageProvider()
426+
await this.coverageProvider?.clean(this.config.coverage.clean)
427+
await this.initBrowserProviders()
428+
}
429+
finally {
430+
await this.report('onInit', this)
431+
}
432+
433+
// populate test files cache so watch mode can trigger a file rerun
434+
await this.globTestFiles()
435+
436+
if (this.config.watch)
437+
await this.report('onWatcherStart')
438+
}
439+
421440
private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) {
422441
const addImports = async ([project, filepath]: WorkspaceSpec) => {
423442
if (deps.has(filepath))
@@ -607,14 +626,24 @@ export class Vitest {
607626
if (pattern === '')
608627
this.filenamePattern = undefined
609628

610-
this.configOverride.testNamePattern = pattern ? new RegExp(pattern) : undefined
629+
const testNamePattern = pattern ? new RegExp(pattern) : undefined
630+
this.configOverride.testNamePattern = testNamePattern
631+
// filter only test files that have tests matching the pattern
632+
if (testNamePattern) {
633+
files = files.filter((filepath) => {
634+
const files = this.state.getFiles([filepath])
635+
return !files.length || files.some((file) => {
636+
const tasks = getTasks(file)
637+
return !tasks.length || tasks.some(task => testNamePattern.test(task.name))
638+
})
639+
})
640+
}
611641
await this.rerunFiles(files, trigger)
612642
}
613643

614-
async changeFilenamePattern(pattern: string) {
644+
async changeFilenamePattern(pattern: string, files: string[] = this.state.getFilepaths()) {
615645
this.filenamePattern = pattern
616646

617-
const files = this.state.getFilepaths()
618647
const trigger = this.filenamePattern ? 'change filename pattern' : 'reset filename pattern'
619648

620649
await this.rerunFiles(files, trigger)
@@ -758,8 +787,10 @@ export class Vitest {
758787

759788
const matchingProjects: WorkspaceProject[] = []
760789
await Promise.all(this.projects.map(async (project) => {
761-
if (await project.isTargetFile(id))
790+
if (await project.isTargetFile(id)) {
762791
matchingProjects.push(project)
792+
project.testFilesList?.push(id)
793+
}
763794
}))
764795

765796
if (matchingProjects.length > 0) {
@@ -940,6 +971,10 @@ export class Vitest {
940971
)))
941972
}
942973

974+
public async getTestFilepaths() {
975+
return this.globTestFiles().then(files => files.map(([, file]) => file))
976+
}
977+
943978
public async globTestFiles(filters: string[] = []) {
944979
const files: WorkspaceSpec[] = []
945980
await Promise.all(this.projects.map(async (project) => {

‎packages/vitest/src/node/logger.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,10 @@ export class Logger {
189189
if (this.ctx.coverageProvider)
190190
this.log(c.dim(' Coverage enabled with ') + c.yellow(this.ctx.coverageProvider.name))
191191

192-
this.log()
192+
if (this.ctx.config.standalone)
193+
this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.`))
194+
else
195+
this.log()
193196
}
194197

195198
async printUnhandledErrors(errors: unknown[]) {

‎packages/vitest/src/node/stdin.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import readline from 'node:readline'
22
import type { Writable } from 'node:stream'
33
import c from 'picocolors'
44
import prompt from 'prompts'
5-
import { relative } from 'pathe'
5+
import { relative, resolve } from 'pathe'
66
import { getTests, isWindows, stdout } from '../utils'
77
import { toArray } from '../utils/base'
88
import type { Vitest } from './core'
@@ -73,8 +73,10 @@ export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream =
7373
if (name === 'u')
7474
return ctx.updateSnapshot()
7575
// rerun all tests
76-
if (name === 'a' || name === 'return')
77-
return ctx.changeNamePattern('')
76+
if (name === 'a' || name === 'return') {
77+
const files = await ctx.getTestFilepaths()
78+
return ctx.changeNamePattern('', files, 'rerun all tests')
79+
}
7880
// rerun current pattern tests
7981
if (name === 'r')
8082
return ctx.rerunFiles()
@@ -113,7 +115,13 @@ export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream =
113115
})
114116

115117
on()
116-
await ctx.changeNamePattern(filter?.trim() || '', undefined, 'change pattern')
118+
const files = ctx.state.getFilepaths()
119+
// if running in standalone mode, Vitest instance doesn't know about any test file
120+
const cliFiles = ctx.config.standalone && !files.length
121+
? await ctx.getTestFilepaths()
122+
: undefined
123+
124+
await ctx.changeNamePattern(filter?.trim() || '', cliFiles, 'change pattern')
117125
}
118126

119127
async function inputProjectName() {
@@ -143,8 +151,12 @@ export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream =
143151
on()
144152

145153
latestFilename = filter?.trim() || ''
154+
const lastResults = watchFilter.getLastResults()
146155

147-
await ctx.changeFilenamePattern(latestFilename)
156+
await ctx.changeFilenamePattern(
157+
latestFilename,
158+
filter && lastResults.length ? lastResults.map(i => resolve(ctx.config.root, i)) : undefined,
159+
)
148160
}
149161

150162
let rl: readline.Interface | undefined

‎packages/vitest/src/node/watch-filter.ts

+4
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,8 @@ export class WatchFilter {
179179
// @ts-expect-error -- write() method has different signature on the union type
180180
this.stdout.write(data)
181181
}
182+
183+
public getLastResults() {
184+
return this.results
185+
}
182186
}

‎packages/vitest/src/types/config.ts

+9
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,15 @@ export interface UserConfig extends InlineConfig {
801801
*/
802802
config?: string | false | undefined
803803

804+
/**
805+
* Do not run tests when Vitest starts.
806+
*
807+
* Vitest will only run tests if it's called programmatically or the test file changes.
808+
*
809+
* CLI file filters will be ignored.
810+
*/
811+
standalone?: boolean
812+
804813
/**
805814
* Use happy-dom
806815
*/

‎test/reporters/tests/default.test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ describe('default reporter', async () => {
3838
// one file
3939
vitest.write('p')
4040
await vitest.waitForStdout('Input filename pattern')
41-
vitest.write('a\n')
41+
vitest.write('a')
42+
await vitest.waitForStdout('a.test.ts')
43+
vitest.write('\n')
4244
await vitest.waitForStdout('Filename pattern: a')
4345
await vitest.waitForStdout('Waiting for file changes...')
4446
expect(vitest.stdout).contain('✓ a1 test')

‎test/watch/test/stdin.test.ts

+59-52
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,64 @@
11
import { rmSync, writeFileSync } from 'node:fs'
2-
import { expect, onTestFinished, test } from 'vitest'
2+
import { describe, expect, onTestFinished, test } from 'vitest'
33

44
import { runVitest } from '../../test-utils'
55

6-
const options = { root: 'fixtures', watch: true }
6+
const _options = { root: 'fixtures', watch: true }
77

8-
test('quit watch mode', async () => {
9-
const { vitest, waitForClose } = await runVitest(options)
8+
describe.each([true, false])('standalone mode is %s', (standalone) => {
9+
const options = { ..._options, standalone }
1010

11-
vitest.write('q')
11+
test('quit watch mode', async () => {
12+
const { vitest, waitForClose } = await runVitest(options)
1213

13-
await waitForClose()
14-
})
14+
vitest.write('q')
1515

16-
test('rerun current pattern tests', async () => {
17-
const { vitest } = await runVitest({ ...options, testNamePattern: 'sum' })
16+
await waitForClose()
17+
})
1818

19-
vitest.write('r')
19+
test('filter by filename', async () => {
20+
const { vitest } = await runVitest(options)
2021

21-
await vitest.waitForStdout('RERUN')
22-
await vitest.waitForStdout('Test name pattern: /sum/')
23-
await vitest.waitForStdout('1 passed')
24-
})
22+
vitest.write('p')
2523

26-
test('filter by filename', async () => {
27-
const { vitest } = await runVitest(options)
24+
await vitest.waitForStdout('Input filename pattern')
2825

29-
vitest.write('p')
26+
vitest.write('math')
3027

31-
await vitest.waitForStdout('Input filename pattern')
28+
await vitest.waitForStdout('Pattern matches 1 result')
29+
await vitest.waitForStdout('› math.test.ts')
3230

33-
vitest.write('math')
31+
vitest.write('\n')
3432

35-
await vitest.waitForStdout('Pattern matches 1 result')
36-
await vitest.waitForStdout('› math.test.ts')
33+
await vitest.waitForStdout('Filename pattern: math')
34+
await vitest.waitForStdout('1 passed')
35+
})
3736

38-
vitest.write('\n')
37+
test('filter by test name', async () => {
38+
const { vitest } = await runVitest(options)
3939

40-
await vitest.waitForStdout('Filename pattern: math')
41-
await vitest.waitForStdout('1 passed')
42-
})
40+
vitest.write('t')
4341

44-
test('filter by test name', async () => {
45-
const { vitest } = await runVitest(options)
42+
await vitest.waitForStdout('Input test name pattern')
4643

47-
vitest.write('t')
44+
vitest.write('sum')
45+
if (standalone)
46+
await vitest.waitForStdout('Pattern matches no results')
47+
else
48+
await vitest.waitForStdout('Pattern matches 1 result')
49+
await vitest.waitForStdout('› sum')
4850

49-
await vitest.waitForStdout('Input test name pattern')
51+
vitest.write('\n')
5052

51-
vitest.write('sum')
52-
await vitest.waitForStdout('Pattern matches 1 result')
53-
await vitest.waitForStdout('› sum')
53+
await vitest.waitForStdout('Test name pattern: /sum/')
54+
await vitest.waitForStdout('1 passed')
55+
})
5456

55-
vitest.write('\n')
57+
test.skipIf(process.env.GITHUB_ACTIONS)('cancel test run', async () => {
58+
const { vitest } = await runVitest(options)
5659

57-
await vitest.waitForStdout('Test name pattern: /sum/')
58-
await vitest.waitForStdout('1 passed')
59-
})
60-
61-
test.skipIf(process.env.GITHUB_ACTIONS)('cancel test run', async () => {
62-
const { vitest } = await runVitest(options)
63-
64-
const testPath = 'fixtures/cancel.test.ts'
65-
const testCase = `// Dynamic test case
60+
const testPath = 'fixtures/cancel.test.ts'
61+
const testCase = `// Dynamic test case
6662
import { afterAll, afterEach, test } from 'vitest'
6763
6864
// These should be called even when test is cancelled
@@ -80,17 +76,28 @@ test('2 - test that is cancelled', async () => {
8076
})
8177
`
8278

83-
onTestFinished(() => rmSync(testPath))
84-
writeFileSync(testPath, testCase, 'utf8')
79+
onTestFinished(() => rmSync(testPath))
80+
writeFileSync(testPath, testCase, 'utf8')
81+
82+
// Test case is running, cancel it
83+
await vitest.waitForStdout('[cancel-test]: test')
84+
vitest.write('c')
8585

86-
// Test case is running, cancel it
87-
await vitest.waitForStdout('[cancel-test]: test')
88-
vitest.write('c')
86+
// Test hooks should still be called
87+
await vitest.waitForStdout('CANCELLED')
88+
await vitest.waitForStdout('[cancel-test]: afterAll')
89+
await vitest.waitForStdout('[cancel-test]: afterEach')
8990

90-
// Test hooks should still be called
91-
await vitest.waitForStdout('CANCELLED')
92-
await vitest.waitForStdout('[cancel-test]: afterAll')
93-
await vitest.waitForStdout('[cancel-test]: afterEach')
91+
expect(vitest.stdout).not.include('[cancel-test]: should not run')
92+
})
93+
})
94+
95+
test('rerun current pattern tests', async () => {
96+
const { vitest } = await runVitest({ ..._options, testNamePattern: 'sum' })
9497

95-
expect(vitest.stdout).not.include('[cancel-test]: should not run')
98+
vitest.write('r')
99+
100+
await vitest.waitForStdout('RERUN')
101+
await vitest.waitForStdout('Test name pattern: /sum/')
102+
await vitest.waitForStdout('1 passed')
96103
})

0 commit comments

Comments
 (0)
Please sign in to comment.