[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: add --workspace option, fix root resolution in workspaces (#4773)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Vladimir
Ari Perkkiö
and committed by
GitHub
(Dec 19, 2023, 11:34 AM +0100) 67d93eda 8dabef86

+87 -36
+9
docs/config/index.md
··· 2028 2028 - **Default:** `false` 2029 2029 2030 2030 Tells fake timers to clear "native" (i.e. not fake) timers by delegating to their respective handlers. These are not cleared by default, leading to potentially unexpected behavior if timers existed prior to starting fake timers session. 2031 + 2032 + ### workspace 2033 + 2034 + - **Type:** `string` 2035 + - **CLI:** `--workspace=./file.js` 2036 + - **Default:** `vitest.{workspace,projects}.{js,ts,json}` close to the config file or root 2037 + - **Version:** Since Vitest 1.1.0 2038 + 2039 + Path to a [workspace](/guide/workspace) config file relative to [root](#root).
+1 -1
docs/guide/cli.md
··· 78 78 | `--outputFile <filename/-s>` | Write test results to a file when the `--reporter=json` or `--reporter=junit` option is also specified <br /> Via [cac's dot notation] you can specify individual outputs for multiple reporters | 79 79 | `--coverage` | Enable coverage report | 80 80 | `--run` | Do not watch | 81 - | `--mode` | Override Vite mode (default: `test`) | 82 81 | `--mode <name>` | Override Vite mode (default: `test`) | 82 + | `--workspace <path>` | Path to a workspace configuration file | 83 83 | `--globals` | Inject APIs globally | 84 84 | `--dom` | Mock browser API with happy-dom | 85 85 | `--browser [options]` | Run tests in [the browser](/guide/browser) (default: `false`) |
+1 -1
test/workspaces/vitest.workspace.ts
··· 4 4 import type { Plugin } from 'vite' 5 5 6 6 export default defineWorkspace([ 7 - './space_2/*', 7 + 'space_2', 8 8 './space_*/*.config.ts', 9 9 { 10 10 test: {
+9
test/config/test/workspace.test.ts
··· 1 + import { expect, it } from 'vitest' 2 + import { runVitestCli } from '../../test-utils' 3 + 4 + it('correctly runs workspace tests when workspace config path is specified', async () => { 5 + const { stderr, stdout } = await runVitestCli('run', '--root', 'fixtures/workspace', '--workspace', './nested/e2e.projects.js') 6 + expect(stderr).toBe('') 7 + expect(stdout).toContain('1 + 1 = 2') 8 + expect(stdout).not.toContain('2 + 2 = 4') 9 + })
+6
packages/vitest/src/node/cli.ts
··· 29 29 .option('--coverage.all', 'Whether to include all files, including the untested ones into report', { default: true }) 30 30 .option('--run', 'Disable watch mode') 31 31 .option('--mode <name>', 'Override Vite mode (default: test)') 32 + .option('--workspace <path>', 'Path to a workspace configuration file') 32 33 .option('--globals', 'Inject apis globally') 33 34 .option('--dom', 'Mock browser API with happy-dom') 34 35 .option('--browser [options]', 'Run tests in the browser (default: false)') ··· 149 150 argv.config = normalize(argv.config) 150 151 else 151 152 delete argv.config 153 + 154 + if (argv.workspace) 155 + argv.workspace = normalize(argv.workspace) 156 + else 157 + delete argv.workspace 152 158 153 159 if (argv.dir) 154 160 argv.dir = normalize(argv.dir)
+22 -27
packages/vitest/src/node/config.ts
··· 22 22 '@nuxt/test-utils', 23 23 ] 24 24 25 + function resolvePath(path: string, root: string) { 26 + return normalize( 27 + resolveModule(path, { paths: [root] }) 28 + ?? resolve(root, path), 29 + ) 30 + } 31 + 25 32 export function resolveApiServerConfig<Options extends ApiConfig & UserConfig>( 26 33 options: Options, 27 34 ): ApiConfig | undefined { ··· 193 200 resolved.server.deps.moduleDirectories ??= [] 194 201 resolved.server.deps.moduleDirectories.push(...resolved.deps.moduleDirectories) 195 202 196 - if (resolved.runner) { 197 - resolved.runner = resolveModule(resolved.runner, { paths: [resolved.root] }) 198 - ?? resolve(resolved.root, resolved.runner) 199 - } 203 + if (resolved.runner) 204 + resolved.runner = resolvePath(resolved.runner, resolved.root) 200 205 201 206 resolved.testNamePattern = resolved.testNamePattern 202 207 ? resolved.testNamePattern instanceof RegExp ··· 274 279 } 275 280 } 276 281 277 - if (!builtinPools.includes(resolved.pool as BuiltinPool)) { 278 - resolved.pool = normalize( 279 - resolveModule(resolved.pool, { paths: [resolved.root] }) 280 - ?? resolve(resolved.root, resolved.pool), 281 - ) 282 + if (resolved.workspace) { 283 + // if passed down from the CLI and it's relative, resolve relative to CWD 284 + resolved.workspace = options.workspace && options.workspace[0] === '.' 285 + ? resolve(process.cwd(), options.workspace) 286 + : resolvePath(resolved.workspace, resolved.root) 282 287 } 288 + 289 + if (!builtinPools.includes(resolved.pool as BuiltinPool)) 290 + resolved.pool = resolvePath(resolved.pool, resolved.root) 283 291 resolved.poolMatchGlobs = (resolved.poolMatchGlobs || []).map(([glob, pool]) => { 284 - if (!builtinPools.includes(pool as BuiltinPool)) { 285 - pool = normalize( 286 - resolveModule(pool, { paths: [resolved.root] }) 287 - ?? resolve(resolved.root, pool), 288 - ) 289 - } 292 + if (!builtinPools.includes(pool as BuiltinPool)) 293 + pool = resolvePath(pool, resolved.root) 290 294 return [glob, pool] 291 295 }) 292 296 ··· 315 319 } 316 320 317 321 resolved.setupFiles = toArray(resolved.setupFiles || []).map(file => 318 - normalize( 319 - resolveModule(file, { paths: [resolved.root] }) 320 - ?? resolve(resolved.root, file), 321 - ), 322 + resolvePath(file, resolved.root), 322 323 ) 323 324 resolved.globalSetup = toArray(resolved.globalSetup || []).map(file => 324 - normalize( 325 - resolveModule(file, { paths: [resolved.root] }) 326 - ?? resolve(resolved.root, file), 327 - ), 325 + resolvePath(file, resolved.root), 328 326 ) 329 327 resolved.coverage.exclude.push(...resolved.setupFiles.map(file => `${resolved.coverage.allowExternal ? '**/' : ''}${relative(resolved.root, file)}`)) 330 328 ··· 334 332 ] 335 333 336 334 if (resolved.diff) { 337 - resolved.diff = normalize( 338 - resolveModule(resolved.diff, { paths: [resolved.root] }) 339 - ?? resolve(resolved.root, resolved.diff), 340 - ) 335 + resolved.diff = resolvePath(resolved.diff, resolved.root) 341 336 resolved.forceRerunTriggers.push(resolved.diff) 342 337 } 343 338
+16 -4
packages/vitest/src/node/core.ts
··· 182 182 || this.projects[0] 183 183 } 184 184 185 - private async resolveWorkspace(cliOptions: UserConfig) { 185 + private async getWorkspaceConfigPath() { 186 + if (this.config.workspace) 187 + return this.config.workspace 188 + 186 189 const configDir = this.server.config.configFile 187 190 ? dirname(this.server.config.configFile) 188 191 : this.config.root 192 + 189 193 const rootFiles = await fs.readdir(configDir) 194 + 190 195 const workspaceConfigName = workspaceFiles.find((configFile) => { 191 196 return rootFiles.includes(configFile) 192 197 }) 193 198 194 199 if (!workspaceConfigName) 195 - return [await this.createCoreProject()] 200 + return null 196 201 197 - const workspaceConfigPath = join(configDir, workspaceConfigName) 202 + return join(configDir, workspaceConfigName) 203 + } 204 + 205 + private async resolveWorkspace(cliOptions: UserConfig) { 206 + const workspaceConfigPath = await this.getWorkspaceConfigPath() 207 + 208 + if (!workspaceConfigPath) 209 + return [await this.createCoreProject()] 198 210 199 211 const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { 200 212 default: (string | UserWorkspaceConfig)[] ··· 244 256 245 257 const workspacesByFolder = resolvedWorkspacesPaths 246 258 .reduce((configByFolder, filepath) => { 247 - const dir = dirname(filepath) 259 + const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) 248 260 configByFolder[dir] ??= [] 249 261 configByFolder[dir].push(filepath) 250 262 return configByFolder
+5 -1
packages/vitest/src/node/workspace.ts
··· 33 33 ? false 34 34 : workspacePath 35 35 36 - const root = options.root || (typeof workspacePath === 'number' ? undefined : dirname(workspacePath)) 36 + const root = options.root || ( 37 + typeof workspacePath === 'number' 38 + ? undefined 39 + : workspacePath.endsWith('/') ? workspacePath : dirname(workspacePath) 40 + ) 37 41 38 42 const config: ViteInlineConfig = { 39 43 ...options,
+5
packages/vitest/src/types/config.ts
··· 310 310 poolMatchGlobs?: [string, Exclude<Pool, 'browser'>][] 311 311 312 312 /** 313 + * Path to a workspace configuration file 314 + */ 315 + workspace?: string 316 + 317 + /** 313 318 * Update snapshot 314 319 * 315 320 * @default false
+2 -2
packages/vitest/src/node/plugins/workspace.ts
··· 1 - import { dirname, relative } from 'pathe' 1 + import { basename, dirname, relative } from 'pathe' 2 2 import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' 3 3 import { configDefaults } from '../../defaults' 4 4 import { generateScopedClassName } from '../../integrations/css/css-modules' ··· 36 36 let name = testConfig.name 37 37 if (!name) { 38 38 if (typeof options.workspacePath === 'string') 39 - name = dirname(options.workspacePath).split('/').pop() 39 + name = basename(options.workspacePath.endsWith('/') ? options.workspacePath.slice(0, -1) : dirname(options.workspacePath)) 40 40 else 41 41 name = options.workspacePath.toString() 42 42 }
+1
test/config/fixtures/workspace/nested/e2e.projects.js
··· 1 + export default ['project-1']
+5
test/config/fixtures/workspace/project-1/calculator-1.test.ts
··· 1 + import { expect, it } from 'vitest'; 2 + 3 + it('1 + 1 = 2', () => { 4 + expect(1 + 1).toBe(2); 5 + })
+5
test/config/fixtures/workspace/project-2/calculator-2.test.ts
··· 1 + import { expect, it } from 'vitest'; 2 + 3 + it('2 + 2 = 4', () => { 4 + expect(2 + 2).toBe(4); 5 + })