diff --git a/packages/vitest/src/utils/coverage.ts b/packages/vitest/src/utils/coverage.ts index 1983d9dfab77..7e45f6db0780 100644 --- a/packages/vitest/src/utils/coverage.ts +++ b/packages/vitest/src/utils/coverage.ts @@ -263,14 +263,16 @@ function resolveConfig(configModule: any) { if (mod.$type === 'object') return mod - if (mod.$type === 'function-call') { - // "export default defineConfig({ test: {...} })" - if (mod.$args[0].$type === 'object') - return mod.$args[0] - - // "export default defineConfig(() => ({ test: {...} }))" - if (mod.$args[0].$type === 'arrow-function-expression' && mod.$args[0].$body.$type === 'object') - return mod.$args[0].$body + // "export default defineConfig(...)" + let config = resolveDefineConfig(mod) + if (config) + return config + + // "export default mergeConfig(..., defineConfig(...))" + if (mod.$type === 'function-call' && mod.$callee === 'mergeConfig') { + config = resolveMergeConfig(mod) + if (config) + return config } } catch (error) { @@ -280,3 +282,33 @@ function resolveConfig(configModule: any) { throw new Error('Failed to update coverage thresholds. Configuration file is too complex.') } + +function resolveDefineConfig(mod: any) { + if (mod.$type === 'function-call' && mod.$callee === 'defineConfig') { + // "export default defineConfig({ test: {...} })" + if (mod.$args[0].$type === 'object') + return mod.$args[0] + + if (mod.$args[0].$type === 'arrow-function-expression') { + if (mod.$args[0].$body.$type === 'object') { + // "export default defineConfig(() => ({ test: {...} }))" + return mod.$args[0].$body + } + + // "export default defineConfig(() => mergeConfig({...}, ...))" + const config = resolveMergeConfig(mod.$args[0].$body) + if (config) + return config + } + } +} + +function resolveMergeConfig(mod: any): any { + if (mod.$type === 'function-call' && mod.$callee === 'mergeConfig') { + for (const arg of mod.$args) { + const config = resolveDefineConfig(arg) + if (config) + return config + } + } +} diff --git a/test/coverage-test/option-tests/thresholds-auto-update.test.ts b/test/coverage-test/option-tests/thresholds-auto-update.test.ts new file mode 100644 index 000000000000..12e5620a6602 --- /dev/null +++ b/test/coverage-test/option-tests/thresholds-auto-update.test.ts @@ -0,0 +1,162 @@ +import { parseModule } from 'magicast' +import type { CoverageMap } from 'istanbul-lib-coverage' +import { createCoverageSummary } from 'istanbul-lib-coverage' + +import { expect, test } from 'vitest' +import { defineConfig } from 'vitest/config' +import { BaseCoverageProvider } from 'vitest/coverage' + +const initialThresholds = { lines: 1, branches: 2, functions: 3, statements: 4 } +const coveredThresholds = { lines: 50, branches: 60, functions: 70, statements: 80 } +const initialConfig = JSON.stringify(defineConfig({ + test: { + coverage: { + thresholds: initialThresholds, + }, + }, +}), null, 2) + +test('updates thresholds on "export default {..}"', async () => { + const config = parseModule(`export default ${initialConfig}`) + + const updatedConfig = await updateThresholds(config) + + expect(updatedConfig).toMatchInlineSnapshot(` + "export default { + "test": { + "coverage": { + "thresholds": { + "lines": 50, + "branches": 60, + "functions": 70, + "statements": 80 + } + } + } + }" + `) +}) + +test('updates thresholds on "export default defineConfig({...})"', async () => { + const config = parseModule(`export default defineConfig(${initialConfig})`) + + const updatedConfig = await updateThresholds(config) + + expect(updatedConfig).toMatchInlineSnapshot(` + "export default defineConfig({ + "test": { + "coverage": { + "thresholds": { + "lines": 50, + "branches": 60, + "functions": 70, + "statements": 80 + } + } + } + })" + `) +}) + +test('updates thresholds on "export default defineConfig(() => ({...}))"', async () => { + const config = parseModule(`export default defineConfig(() => (${initialConfig}))`) + + const updatedConfig = await updateThresholds(config) + + expect(updatedConfig).toMatchInlineSnapshot(` + "export default defineConfig(() => ({ + "test": { + "coverage": { + "thresholds": { + "lines": 50, + "branches": 60, + "functions": 70, + "statements": 80 + } + } + } + }))" + `) +}) + +test('updates thresholds on "export default mergeConfig({...}, defineConfig({...}))"', async () => { + const config = parseModule(`export default mergeConfig(baseConfig, defineConfig(${initialConfig}))`) + + const updatedConfig = await updateThresholds(config) + + expect(updatedConfig).toMatchInlineSnapshot(` + "export default mergeConfig(baseConfig, defineConfig({ + "test": { + "coverage": { + "thresholds": { + "lines": 50, + "branches": 60, + "functions": 70, + "statements": 80 + } + } + } + }))" + `) +}) + +test('updates thresholds on "export default defineConfig(() => mergeConfig({...}, defineConfig({...})))"', async () => { + const config = parseModule(`export default defineConfig((configEnv) => mergeConfig(baseConfig, defineConfig(${initialConfig})))`) + + const updatedConfig = await updateThresholds(config) + + expect(updatedConfig).toMatchInlineSnapshot(` + "export default defineConfig((configEnv) => mergeConfig(baseConfig, defineConfig({ + "test": { + "coverage": { + "thresholds": { + "lines": 50, + "branches": 60, + "functions": 70, + "statements": 80 + } + } + } + })))" + `) +}) + +test('throws when configuration is too complex to analyze', async () => { + const config = parseModule(` +import config from "./some-path" +export default config + `) + + await expect(updateThresholds(config)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to update coverage thresholds. Configuration file is too complex.]`) +}) + +async function updateThresholds(configurationFile: ReturnType) { + const summaryData = { total: 0, covered: 0, skipped: 0 } + const thresholds = [{ + name: 'global', + thresholds: initialThresholds, + coverageMap: { + getCoverageSummary: () => createCoverageSummary({ + lines: { pct: coveredThresholds.lines, ...summaryData }, + statements: { pct: coveredThresholds.statements, ...summaryData }, + branches: { pct: coveredThresholds.branches, ...summaryData }, + functions: { pct: coveredThresholds.functions, ...summaryData }, + }), + } as CoverageMap, + }] + + return new Promise((resolve, reject) => { + const provider = new BaseCoverageProvider() + + try { + provider.updateThresholds({ + thresholds, + configurationFile, + onUpdate: () => resolve(configurationFile.generate().code), + }) + } + catch (error) { + reject(error) + } + }) +} diff --git a/test/coverage-test/testing-options.mjs b/test/coverage-test/testing-options.mjs index 069632058a96..e7727ff6b95d 100644 --- a/test/coverage-test/testing-options.mjs +++ b/test/coverage-test/testing-options.mjs @@ -194,6 +194,13 @@ const testCases = [ include: ['coverage-report-tests/merge-reports.test.ts'], }, }, + { + testConfig: { + name: 'thresholds autoUpdate', + include: ['option-tests/thresholds-auto-update.test.ts'], + coverage: { provider: 'v8', enabled: false }, + }, + }, ] for (const provider of ['v8', 'istanbul']) {