From e903c58844bd0295e9100d48ba7ff74cf67c7e45 Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Mon, 20 Apr 2026 17:52:46 -0500 Subject: [PATCH] Fix e2e hook starvation under describe.concurrent (#47) Replace crossSpawn.sync with async spawn in runCommand so the event loop is free between awaits. This prevents beforeEach hooks from timing out when concurrent tests spawn CLI processes. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/e2e/cli.test.ts | 32 +++---- packages/eslint-config/e2e/cli.test.ts | 93 ++++++++++---------- packages/markdownlint-config/e2e/cli.test.ts | 20 ++--- packages/test-utils/src/fixture.ts | 46 ++++++---- packages/test-utils/test/fixture.test.ts | 12 +-- packages/tsconfig/e2e/flatten.test.ts | 4 +- packages/vitest-config/e2e/cli.test.ts | 30 +++---- 7 files changed, 124 insertions(+), 113 deletions(-) diff --git a/packages/cli/e2e/cli.test.ts b/packages/cli/e2e/cli.test.ts index f5cc8be..eb3ddb2 100644 --- a/packages/cli/e2e/cli.test.ts +++ b/packages/cli/e2e/cli.test.ts @@ -29,8 +29,8 @@ const writeJson = (dir: string, name: string, data: unknown): void => { False positive with extendWithFixture indirection: https://github.com/vitest-dev/eslint-plugin-vitest/issues/891 */ describe.concurrent('gtb CLI', () => { - it('prints help with --help', ({ fixture, expect }) => { - const result = fixture.run('gtb', ['--help']); + it('prints help with --help', async ({ fixture, expect }) => { + const result = await fixture.run('gtb', ['--help']); expect(result).toMatchObject({ exitCode: 0 }); expect(result.stdout).toContain('Usage: gtb'); @@ -42,15 +42,15 @@ describe.concurrent('gtb CLI', () => { expect(result.stdout).toContain('turbo:check'); }); - it('prints help with no arguments', ({ fixture, expect }) => { - const result = fixture.run('gtb', []); + it('prints help with no arguments', async ({ fixture, expect }) => { + const result = await fixture.run('gtb', []); expect(result).toMatchObject({ exitCode: 0 }); expect(result.stdout).toContain('Usage: gtb'); }); - it('exits non-zero for unknown command', ({ fixture, expect }) => { - const result = fixture.run('gtb', ['nonexistent']); + it('exits non-zero for unknown command', async ({ fixture, expect }) => { + const result = await fixture.run('gtb', ['nonexistent']); expect(result.exitCode).not.toBe(0); }); @@ -61,7 +61,7 @@ describe.concurrent('gtb CLI', () => { False positive when callback omits custom fixture properties: https://github.com/vitest-dev/eslint-plugin-vitest/issues/891 */ describe.concurrent('gtb pack:npm', () => { - it('produces tarball for publishable package', ({ expect }) => { + it('produces tarball for publishable package', async ({ expect }) => { using fixture = createFixture(); writeJson(fixture.projectDir, 'package.json', { name: '@test/my-lib', @@ -69,7 +69,7 @@ describe.concurrent('gtb pack:npm', () => { version: '1.0.0', }); - const result = fixture.run('gtb', ['pack:npm']); + const result = await fixture.run('gtb', ['pack:npm']); expect(result).toMatchObject({ exitCode: 0 }); @@ -81,7 +81,7 @@ describe.concurrent('gtb pack:npm', () => { expect(tarballs[0]).toMatch(/^test-my-lib-.*\.tgz$/v); }); - it('skips private package', ({ expect }) => { + it('skips private package', async ({ expect }) => { using fixture = createFixture(); writeJson(fixture.projectDir, 'package.json', { name: '@test/internal', @@ -90,7 +90,7 @@ describe.concurrent('gtb pack:npm', () => { version: '1.0.0', }); - const result = fixture.run('gtb', ['pack:npm']); + const result = await fixture.run('gtb', ['pack:npm']); expect(result).toMatchObject({ exitCode: 0 }); expect(existsSync(path.join(fixture.projectDir, 'dist', 'packages', 'npm'))).toBe( @@ -98,14 +98,14 @@ describe.concurrent('gtb pack:npm', () => { ); }); - it('skips package without publishConfig.directory', ({ expect }) => { + it('skips package without publishConfig.directory', async ({ expect }) => { using fixture = createFixture(); writeJson(fixture.projectDir, 'package.json', { name: '@test/my-lib', version: '1.0.0', }); - const result = fixture.run('gtb', ['pack:npm']); + const result = await fixture.run('gtb', ['pack:npm']); expect(result).toMatchObject({ exitCode: 0 }); expect(existsSync(path.join(fixture.projectDir, 'dist', 'packages', 'npm'))).toBe( @@ -113,7 +113,7 @@ describe.concurrent('gtb pack:npm', () => { ); }); - it('generates dist/source manifests before packing', ({ expect }) => { + it('generates dist/source manifests before packing', async ({ expect }) => { using fixture = createFixture(); writeJson(fixture.projectDir, 'package.json', { bugs: 'https://github.com/test/repo/issues', @@ -134,7 +134,7 @@ describe.concurrent('gtb pack:npm', () => { version: '1.0.0', }); - const result = fixture.run('gtb', ['pack:npm']); + const result = await fixture.run('gtb', ['pack:npm']); expect(result).toMatchObject({ exitCode: 0 }); @@ -158,7 +158,7 @@ describe.concurrent('gtb pack:npm', () => { expect(output).not.toHaveProperty('publishConfig'); }); - it('cleans dist/packages/npm before packing', ({ expect }) => { + it('cleans dist/packages/npm before packing', async ({ expect }) => { using fixture = createFixture(); writeJson(fixture.projectDir, 'package.json', { name: '@test/my-lib', @@ -169,7 +169,7 @@ describe.concurrent('gtb pack:npm', () => { mkdirSync(distDir, { recursive: true }); writeFileSync(path.join(distDir, 'stale-0.0.0.tgz'), ''); - const result = fixture.run('gtb', ['pack:npm']); + const result = await fixture.run('gtb', ['pack:npm']); expect(result).toMatchObject({ exitCode: 0 }); diff --git a/packages/eslint-config/e2e/cli.test.ts b/packages/eslint-config/e2e/cli.test.ts index af17c94..50cd7a6 100644 --- a/packages/eslint-config/e2e/cli.test.ts +++ b/packages/eslint-config/e2e/cli.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { createIsolatedFixture, runCommand } from '@gtbuchanan/test-utils'; import { it as base, describe } from 'vitest'; @@ -58,36 +58,38 @@ const createFixture = () => { const eslint = path.join(fixture.hookDir, 'node_modules/.bin/eslint'); - const run = ({ config, env, files, flags = [] }: RunOptions) => { - writeFileSync(path.join(fixture.projectDir, 'eslint.config.ts'), config ?? createRequireConfig); - writeFileSync(path.join(fixture.projectDir, 'tsconfig.json'), tsconfig); - writeFileSync(path.join(fixture.projectDir, 'tsconfig.root.json'), tsconfigRoot); + const run = async ({ config, env, files, flags = [] }: RunOptions) => { + const runDir = mkdtempSync(path.join(fixture.projectDir, 'run-')); + writeFileSync(path.join(runDir, 'eslint.config.ts'), config ?? createRequireConfig); + writeFileSync(path.join(runDir, 'tsconfig.json'), tsconfig); + writeFileSync(path.join(runDir, 'tsconfig.root.json'), tsconfigRoot); const fileNames = Object.keys(files); for (const [name, content] of Object.entries(files)) { - const filePath = path.join(fixture.projectDir, name); + const filePath = path.join(runDir, name); mkdirSync(path.join(filePath, '..'), { recursive: true }); writeFileSync(filePath, content); } - return runCommand(eslint, [...flags, ...fileNames], { - cwd: fixture.projectDir, + const result = await runCommand(eslint, [...flags, ...fileNames], { + cwd: runDir, env: { ...process.env, NODE_PATH: fixture.nodePath, ...env, }, }); - }; - const readFile = (name: string): string => - readFileSync(path.join(fixture.projectDir, name), 'utf8'); + return { + ...result, + readFile: (name: string) => readFileSync(path.join(runDir, name), 'utf8'), + }; + }; return { eslint, nodePath: fixture.nodePath, projectDir: fixture.projectDir, - readFile, run, [Symbol.dispose]() { fixture[Symbol.dispose](); @@ -109,7 +111,8 @@ const it = base.extend<{ fixture: Fixture }>({ }); describe.concurrent('eslint CLI integration', () => { - it('fails with bare import (proves isolation works)', ({ fixture, expect }) => { + it('fails with bare import (proves isolation works)', async ({ fixture, expect }) => { + const runDir = mkdtempSync(path.join(fixture.projectDir, 'run-')); const bareConfig = [ 'import { configure } from "@gtbuchanan/eslint-config";', 'export default configure({', @@ -118,32 +121,30 @@ describe.concurrent('eslint CLI integration', () => { '});', ].join('\n'); - writeFileSync(path.join(fixture.projectDir, 'eslint.config.ts'), bareConfig); - - const filePath = path.join(fixture.projectDir, 'clean.mjs'); - writeFileSync(filePath, "export const greeting = 'hello';\n"); + writeFileSync(path.join(runDir, 'eslint.config.ts'), bareConfig); + writeFileSync(path.join(runDir, 'clean.mjs'), "export const greeting = 'hello';\n"); const { NODE_PATH: _nodePath, ...envWithoutNodePath } = process.env; - const { exitCode, stderr } = runCommand( + const { exitCode, stderr } = await runCommand( fixture.eslint, ['clean.mjs'], - { cwd: fixture.projectDir, env: envWithoutNodePath }, + { cwd: runDir, env: envWithoutNodePath }, ); expect(exitCode).not.toBe(0); expect(stderr).toContain('@gtbuchanan/eslint-config'); }); - it('passes for a clean file', ({ fixture, expect }) => { - const result = fixture.run({ + it('passes for a clean file', async ({ fixture, expect }) => { + const result = await fixture.run({ files: { 'clean.ts': "export const greeting = 'hello';\n" }, }); expect(result).toMatchObject({ exitCode: 0 }); }); - it('detects process.exit via eslint-plugin-n', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('detects process.exit via eslint-plugin-n', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'bad.ts': 'process.exit(0);\n' }, }); @@ -151,8 +152,8 @@ describe.concurrent('eslint CLI integration', () => { expect(stdout).toContain('n/no-process-exit'); }); - it('applies oxlint overlay as last config', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('applies oxlint overlay as last config', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'test.ts': 'export const greeting = 42;\n' }, }); @@ -161,17 +162,17 @@ describe.concurrent('eslint CLI integration', () => { expect(stdout).not.toContain('Error'); }); - it('respects global ignores for dist/', ({ fixture, expect }) => { + it('respects global ignores for dist/', async ({ fixture, expect }) => { const longLine = `export const x = '${'a'.repeat(101)}';\n`; - const { exitCode } = fixture.run({ + const { exitCode } = await fixture.run({ files: { 'dist/bad.mjs': longLine }, }); expect(exitCode).toBe(0); }); - it('detects duplicate keys in JSON files', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('detects duplicate keys in JSON files', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'bad.json': '{\n "key": 1,\n "key": 2\n}\n' }, }); @@ -179,16 +180,16 @@ describe.concurrent('eslint CLI integration', () => { expect(stdout).toContain('json/no-duplicate-keys'); }); - it('passes for a valid JSON file', ({ fixture, expect }) => { - const { exitCode } = fixture.run({ + it('passes for a valid JSON file', async ({ fixture, expect }) => { + const { exitCode } = await fixture.run({ files: { 'valid.json': '{\n "key": "value"\n}\n' }, }); expect(exitCode).toBe(0); }); - it('warns on unsorted keys in JSON files', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('warns on unsorted keys in JSON files', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'unsorted.json': '{\n "beta": 1,\n "alpha": 2\n}\n' }, }); @@ -197,8 +198,8 @@ describe.concurrent('eslint CLI integration', () => { expect(stdout).toContain('json/sort-keys'); }); - it('allows comments in tsconfig.json via JSONC', ({ fixture, expect }) => { - const { exitCode } = fixture.run({ + it('allows comments in tsconfig.json via JSONC', async ({ fixture, expect }) => { + const { exitCode } = await fixture.run({ files: { 'tsconfig.json': '{\n // A comment\n "compilerOptions": {}\n}\n', }, @@ -207,8 +208,8 @@ describe.concurrent('eslint CLI integration', () => { expect(exitCode).toBe(0); }); - it('downgrades errors to warnings with onlyWarn', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('downgrades errors to warnings with onlyWarn', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ config: createRequireOnlyWarnConfig, files: { 'bad.ts': 'process.exit(0);\n' }, }); @@ -218,13 +219,13 @@ describe.concurrent('eslint CLI integration', () => { expect(stdout).toContain('n/no-process-exit'); }); - it('formats JSON, Markdown, YAML, and CSS via Prettier plugins', ({ fixture, expect }) => { + it('formats JSON, Markdown, YAML, and CSS via Prettier plugins', async ({ fixture, expect }) => { const unsortedJson = '{\n "z": 1,\n "a": [1, 2]\n}\n'; const longMarkdown = `# Title\n\n${'word '.repeat(40).trim()}\n`; const doubleQuotedYaml = 'key: "value"\n'; const unsortedCss = '.box {\n display: flex;\n color: red;\n}\n'; - const result = fixture.run({ + const result = await fixture.run({ files: { 'config.yml': doubleQuotedYaml, 'data.json': unsortedJson, @@ -237,31 +238,31 @@ describe.concurrent('eslint CLI integration', () => { expect(result).toMatchObject({ exitCode: 0 }); // JSON: sort-json sorts keys, multiline-arrays expands arrays - expect(fixture.readFile('data.json')).toBe( + expect(result.readFile('data.json')).toBe( ['{', ' "a": [', ' 1,', ' 2', ' ],', ' "z": 1', '}', ''].join('\n'), ); // Markdown: proseWrap 'preserve' keeps long lines unwrapped - expect(fixture.readFile('doc.md')).toBe(longMarkdown); + expect(result.readFile('doc.md')).toBe(longMarkdown); // YAML: singleQuote from prettierDefaults converts double quotes - expect(fixture.readFile('config.yml')).toBe("key: 'value'\n"); + expect(result.readFile('config.yml')).toBe("key: 'value'\n"); // CSS: alphabetical property sorting (color before display) - expect(fixture.readFile('style.css')).toBe( + expect(result.readFile('style.css')).toBe( '.box {\n color: red;\n display: flex;\n}\n', ); }); - it('formats XML with whitespace-insensitive mode', ({ fixture, expect }) => { + it('formats XML with whitespace-insensitive mode', async ({ fixture, expect }) => { const uglyXml = '1.0'; - const result = fixture.run({ + const result = await fixture.run({ files: { 'app.csproj': uglyXml }, flags: ['--fix'], }); expect(result).toMatchObject({ exitCode: 0 }); - expect(fixture.readFile('app.csproj')).toContain('\n'); + expect(result.readFile('app.csproj')).toContain('\n'); }); }); diff --git a/packages/markdownlint-config/e2e/cli.test.ts b/packages/markdownlint-config/e2e/cli.test.ts index 4d0cf4c..12766fc 100644 --- a/packages/markdownlint-config/e2e/cli.test.ts +++ b/packages/markdownlint-config/e2e/cli.test.ts @@ -41,7 +41,7 @@ const runMarkdownlint = ( }; describe('pre-commit isolation', () => { - it('fails with bare import (proves isolation works)', ({ expect }) => { + it('fails with bare import (proves isolation works)', async ({ expect }) => { using fixture = createIsolatedFixture({ hookPackages: ['markdownlint-cli2'], packageName: '@gtbuchanan/markdownlint-config', @@ -52,7 +52,7 @@ describe('pre-commit isolation', () => { writeFileSync(path.join(fixture.projectDir, 'test.md'), '# Hello\n\nTest.\n'); const { NODE_PATH: _nodePath, ...envWithoutNodePath } = process.env; - const result = runCommand(cli2, ['test.md'], { + const result = await runCommand(cli2, ['test.md'], { cwd: fixture.projectDir, env: envWithoutNodePath, }); @@ -60,13 +60,13 @@ describe('pre-commit isolation', () => { expect(result).not.toMatchObject({ exitCode: 0 }); }); - it('passes for a clean markdown file', ({ expect }) => { + it('passes for a clean markdown file', async ({ expect }) => { using fixture = createIsolatedFixture({ hookPackages: ['markdownlint-cli2'], packageName: '@gtbuchanan/markdownlint-config', }); - const result = runMarkdownlint( + const result = await runMarkdownlint( fixture, createRequireConfig, '# Hello\n\nThis is a test.\n', @@ -75,14 +75,14 @@ describe('pre-commit isolation', () => { expect(result).toMatchObject({ exitCode: 0 }); }); - it('detects violations not suppressed by prettier style', ({ expect }) => { + it('detects violations not suppressed by prettier style', async ({ expect }) => { using fixture = createIsolatedFixture({ hookPackages: ['markdownlint-cli2'], packageName: '@gtbuchanan/markdownlint-config', }); // MD024: no-duplicate-heading (not disabled by prettier style) - const result = runMarkdownlint( + const result = await runMarkdownlint( fixture, createRequireConfig, '# Duplicate\n\n# Duplicate\n', @@ -92,7 +92,7 @@ describe('pre-commit isolation', () => { expect(result.stderr).toMatch(/MD024/v); }); - it('suppresses rules disabled by prettier style', ({ expect }) => { + it('suppresses rules disabled by prettier style', async ({ expect }) => { using fixture = createIsolatedFixture({ hookPackages: ['markdownlint-cli2'], packageName: '@gtbuchanan/markdownlint-config', @@ -100,12 +100,12 @@ describe('pre-commit isolation', () => { // Heading-style is disabled — mixed styles should not error const markdown = ['# ATX heading', '', 'Setext heading', '--------------', ''].join('\n'); - const result = runMarkdownlint(fixture, createRequireConfig, markdown); + const result = await runMarkdownlint(fixture, createRequireConfig, markdown); expect(result).toMatchObject({ exitCode: 0 }); }); - it('ignores .changeset/ files with configureCli2', ({ expect }) => { + it('ignores .changeset/ files with configureCli2', async ({ expect }) => { using fixture = createIsolatedFixture({ hookPackages: ['markdownlint-cli2'], packageName: '@gtbuchanan/markdownlint-config', @@ -123,7 +123,7 @@ describe('pre-commit isolation', () => { // Also create a valid file so markdownlint has something to lint writeFileSync(path.join(fixture.projectDir, 'test.md'), '# Valid\n\nContent.\n'); - const result = runCommand(cli2, ['**/*.md'], { + const result = await runCommand(cli2, ['**/*.md'], { cwd: fixture.projectDir, env: { ...process.env, NODE_PATH: fixture.nodePath }, }); diff --git a/packages/test-utils/src/fixture.ts b/packages/test-utils/src/fixture.ts index 1ef2bc8..ad45e74 100644 --- a/packages/test-utils/src/fixture.ts +++ b/packages/test-utils/src/fixture.ts @@ -1,4 +1,4 @@ -import type { SpawnSyncOptions } from 'node:child_process'; +import type { SpawnOptions, SpawnSyncOptions } from 'node:child_process'; import { globSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, @@ -94,7 +94,7 @@ const locateTarballFrom = ( return path.join(match.dir, match.name); }; -/** Result of a synchronous child process execution. */ +/** Result of a child process execution. */ export interface CommandResult { readonly exitCode: number; readonly stderr: string; @@ -127,26 +127,36 @@ export const createGitEnv = (identity?: { email: string; name: string }): GitEnv }), }); -/** Spawns a command synchronously, captures stdout/stderr, and returns the result. */ +/** Spawns a command, captures stdout/stderr, and returns the result. */ export const runCommand = ( command: string, args: readonly string[], - options: SpawnSyncOptions, -): CommandResult => { - const result = crossSpawn.sync(command, [...args], { + options: SpawnOptions, +): Promise => new Promise((resolve, reject) => { + const child = crossSpawn(command, args, { ...options, - encoding: 'utf8', stdio: 'pipe', }); - if (result.error) { - throw result.error; - } - return { - exitCode: result.status ?? 1, - stderr: result.stderr, - stdout: result.stdout, - }; -}; + + let stdout = ''; + let stderr = ''; + + child.stdout?.setEncoding('utf8').on('data', (chunk: string) => { + stdout += chunk; + }); + child.stderr?.setEncoding('utf8').on('data', (chunk: string) => { + stderr += chunk; + }); + + child.on('error', reject); + child.on('close', (code) => { + resolve({ + exitCode: code ?? 1, + stderr, + stdout, + }); + }); +}); // --- Isolated fixture (split hook/deps/project dirs via NODE_PATH) --- @@ -242,7 +252,7 @@ interface ProjectFixtureOptions { */ export interface ProjectFixture { readonly projectDir: string; - readonly run: (command: string, args: readonly string[]) => CommandResult; + readonly run: (command: string, args: readonly string[]) => Promise; readonly writeFile: (name: string, content: string) => string; readonly [Symbol.dispose]: () => void; } @@ -268,7 +278,7 @@ export const createProjectFixture = ( return filePath; }; - const run = (command: string, args: readonly string[]): CommandResult => { + const run = (command: string, args: readonly string[]): Promise => { const binPath = path.join(projectDir, 'node_modules', '.bin', command); return runCommand(binPath, args, { cwd: projectDir }); }; diff --git a/packages/test-utils/test/fixture.test.ts b/packages/test-utils/test/fixture.test.ts index 251f0ff..d186c5e 100644 --- a/packages/test-utils/test/fixture.test.ts +++ b/packages/test-utils/test/fixture.test.ts @@ -79,23 +79,23 @@ describe(createGitEnv, () => { }); describe(runCommand, () => { - it('captures stdout', ({ expect }) => { - const result = runCommand('node', ['-e', 'console.log("hello")'], {}); + it('captures stdout', async ({ expect }) => { + const result = await runCommand('node', ['-e', 'console.log("hello")'], {}); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe('hello'); }); - it('captures stderr separately', ({ expect }) => { - const result = runCommand('node', ['-e', 'console.error("oops")'], {}); + it('captures stderr separately', async ({ expect }) => { + const result = await runCommand('node', ['-e', 'console.error("oops")'], {}); expect(result.exitCode).toBe(0); expect(result.stderr.trim()).toBe('oops'); expect(result.stdout.trim()).toBe(''); }); - it('captures non-zero exit code', ({ expect }) => { - const result = runCommand('node', ['-e', 'process.exit(42)'], {}); + it('captures non-zero exit code', async ({ expect }) => { + const result = await runCommand('node', ['-e', 'process.exit(42)'], {}); expect(result.exitCode).toBe(42); }); diff --git a/packages/tsconfig/e2e/flatten.test.ts b/packages/tsconfig/e2e/flatten.test.ts index db0e706..eb52a37 100644 --- a/packages/tsconfig/e2e/flatten.test.ts +++ b/packages/tsconfig/e2e/flatten.test.ts @@ -49,7 +49,7 @@ describe('tsconfig flattening', () => { }); }); - it('works without @tsconfig/strictest installed', ({ fixture, expect }) => { + it('works without @tsconfig/strictest installed', async ({ fixture, expect }) => { fixture.writeFile( 'tsconfig.json', JSON.stringify({ @@ -62,7 +62,7 @@ describe('tsconfig flattening', () => { 'export const add = (a: number, b: number): number => a + b;\n', ); - const result = fixture.run('tsc', ['--noEmit']); + const result = await fixture.run('tsc', ['--noEmit']); expect(result).toMatchObject({ exitCode: 0 }); }); diff --git a/packages/vitest-config/e2e/cli.test.ts b/packages/vitest-config/e2e/cli.test.ts index d22cfa1..6d0474e 100644 --- a/packages/vitest-config/e2e/cli.test.ts +++ b/packages/vitest-config/e2e/cli.test.ts @@ -78,8 +78,8 @@ const it = base.extend<{ fixture: Fixture }>({ }); describe('vitest CLI integration', () => { - it('enforces hasAssertions via setup file', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('enforces hasAssertions via setup file', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'no-assert.test.ts': [ 'import { it } from "vitest";', @@ -92,8 +92,8 @@ describe('vitest CLI integration', () => { expect(stdout).toMatch(/expected .*? assertion/v); }); - it('enforces console-fail-test via setup file', ({ fixture, expect }) => { - const { exitCode, stderr, stdout } = fixture.run({ + it('enforces console-fail-test via setup file', async ({ fixture, expect }) => { + const { exitCode, stderr, stdout } = await fixture.run({ files: { 'console.test.ts': [ 'import { it } from "vitest";', @@ -109,8 +109,8 @@ describe('vitest CLI integration', () => { expect(stdout + stderr).toMatch(/console method/v); }); - it('resolves #src/ subpath imports', ({ fixture, expect }) => { - const { exitCode } = fixture.run({ + it('resolves #src/ subpath imports', async ({ fixture, expect }) => { + const { exitCode } = await fixture.run({ files: { 'alias.test.ts': [ 'import { it } from "vitest";', @@ -127,7 +127,7 @@ describe('vitest CLI integration', () => { expect(exitCode).toBe(0); }); - it('uses v8 coverage provider', ({ fixture, expect }) => { + it('uses v8 coverage provider', async ({ fixture, expect }) => { writeFileSync( path.join(fixture.projectDir, 'src/add.ts'), 'export const add = (a: number, b: number) => a + b;\n', @@ -143,7 +143,7 @@ describe('vitest CLI integration', () => { ].join('\n'), ); - const { exitCode } = runCommand( + const { exitCode } = await runCommand( fixture.vitest, ['run', '--reporter=verbose', '--coverage', 'cov.test.ts'], { cwd: fixture.projectDir, env: fixture.env }, @@ -153,7 +153,7 @@ describe('vitest CLI integration', () => { expect(existsSync(path.join(fixture.projectDir, 'dist/coverage'))).toBe(true); }); - it('writes repo-relative lcov paths for package coverage', ({ fixture, expect }) => { + it('writes repo-relative lcov paths for package coverage', async ({ fixture, expect }) => { const appDir = path.join(fixture.projectDir, 'packages', 'app'); mkdirSync(path.join(appDir, 'src'), { recursive: true }); mkdirSync(path.join(appDir, 'test'), { recursive: true }); @@ -184,11 +184,11 @@ describe('vitest CLI integration', () => { ].join('\n'), ); - const initResult = runCommand('git', ['init'], { cwd: fixture.projectDir }); + const initResult = await runCommand('git', ['init'], { cwd: fixture.projectDir }); expect(initResult).toMatchObject({ exitCode: 0 }); - const { exitCode } = runCommand( + const { exitCode } = await runCommand( fixture.vitest, ['run', '--reporter=verbose', '--coverage', 'test/cov.test.ts'], { @@ -213,8 +213,8 @@ describe('vitest CLI integration', () => { expect(normalizedLcov).toContain('SF:packages/app/src/add.ts'); }); - it('auto-resets mocks between tests (mockReset: true)', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('auto-resets mocks between tests (mockReset: true)', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'mock-reset.test.ts': [ 'import { vi, it, describe } from "vitest";', @@ -239,8 +239,8 @@ describe('vitest CLI integration', () => { expect(stdout).toContain('2 passed'); }); - it('auto-unstubs env vars between tests (unstubEnvs: true)', ({ fixture, expect }) => { - const { exitCode, stdout } = fixture.run({ + it('auto-unstubs env vars between tests (unstubEnvs: true)', async ({ fixture, expect }) => { + const { exitCode, stdout } = await fixture.run({ files: { 'unstub-envs.test.ts': [ 'import { vi, it, describe } from "vitest";',