diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index 4a309db077fc..3bdc80e09fd5 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -140,8 +140,17 @@ async function executeTests(method: 'run' | 'collect', files: string[]) { debug('prepare time', state.durations.prepare, 'ms') try { - await setupCommonEnv(config) - await startCoverageInsideWorker(config.coverage, executor) + await Promise.all([ + setupCommonEnv(config), + startCoverageInsideWorker(config.coverage, executor), + (async () => { + const VitestIndex = await import('vitest') + Object.defineProperty(window, '__vitest_index__', { + value: VitestIndex, + enumerable: false, + }) + })(), + ]) for (const file of files) { state.filepath = file @@ -168,7 +177,13 @@ async function executeTests(method: 'run' | 'collect', files: string[]) { }, 'Cleanup Error') } state.environmentTeardownRun = true - await stopCoverageInsideWorker(config.coverage, executor) + await stopCoverageInsideWorker(config.coverage, executor).catch((error) => { + client.rpc.onUnhandledError({ + name: error.name, + message: error.message, + stack: String(error.stack), + }, 'Coverage Error').catch(() => {}) + }) debug('finished running tests') done(files) diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index f2516aca04fd..10c5f535418e 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -10,6 +10,7 @@ import { type Plugin, coverageConfigDefaults } from 'vitest/config' import { toArray } from '@vitest/utils' import { defaultBrowserPort } from 'vitest/config' import { dynamicImportPlugin } from '@vitest/mocker/node' +import MagicString from 'magic-string' import BrowserContext from './plugins/pluginContext' import type { BrowserServer } from './server' import { resolveOrchestrator } from './serverOrchestrator' @@ -349,6 +350,22 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { } }, }, + { + name: 'vitest:browser:in-source-tests', + transform(code, id) { + if (!project.isTestFile(id) || !code.includes('import.meta.vitest')) { + return + } + const s = new MagicString(code, { filename: cleanUrl(id) }) + s.prepend( + `import.meta.vitest = __vitest_index__;\n`, + ) + return { + code: s.toString(), + map: s.generateMap({ hires: true }), + } + }, + }, // TODO: remove this when @testing-library/vue supports ESM { name: 'vitest:browser:support-testing-library', @@ -445,3 +462,8 @@ function resolveCoverageFolder(project: WorkspaceProject) { return [resolve(root, subdir), `/${basename(root)}/${subdir}/`] } + +const postfixRE = /[?#].*$/ +function cleanUrl(url: string): string { + return url.replace(postfixRE, '') +} diff --git a/packages/ui/client/composables/explorer/collector.ts b/packages/ui/client/composables/explorer/collector.ts index 55178f0a494d..1c9473eee873 100644 --- a/packages/ui/client/composables/explorer/collector.ts +++ b/packages/ui/client/composables/explorer/collector.ts @@ -304,7 +304,7 @@ function collectData(summary: CollectorInfo) { file.prepareDuration = f.prepareDuration file.environmentLoad = f.environmentLoad file.collectDuration = f.collectDuration - file.duration = f.result?.duration + file.duration = f.result?.duration != null ? Math.round(f.result?.duration) : undefined file.state = f.result?.state } time += Math.max(0, f.collectDuration || 0) diff --git a/packages/ui/client/composables/explorer/utils.ts b/packages/ui/client/composables/explorer/utils.ts index 73f948d67528..01d02b32a0e0 100644 --- a/packages/ui/client/composables/explorer/utils.ts +++ b/packages/ui/client/composables/explorer/utils.ts @@ -69,7 +69,7 @@ export function createOrUpdateFileNode( tasks: [], typecheck: !!file.meta && 'typecheck' in file.meta, indent: 0, - duration: file.result?.duration, + duration: file.result?.duration != null ? Math.round(file.result?.duration) : undefined, filepath: file.filepath, projectName: file.projectName || '', projectNameColor: getProjectNameColor(file.projectName), @@ -132,6 +132,9 @@ export function createOrUpdateNode( ) { const node = explorerTree.nodes.get(parentId) as ParentTreeNode | undefined let taskNode: UITaskTreeNode | undefined + const duration = task.result?.duration != null + ? Math.round(task.result?.duration) + : undefined if (node) { taskNode = explorerTree.nodes.get(task.id) if (taskNode) { @@ -141,7 +144,7 @@ export function createOrUpdateNode( } taskNode.mode = task.mode - taskNode.duration = task.result?.duration + taskNode.duration = duration taskNode.state = task.result?.state } else { @@ -156,7 +159,7 @@ export function createOrUpdateNode( expandable: false, expanded: false, indent: node.indent + 1, - duration: task.result?.duration, + duration, state: task.result?.state, } as TestTreeNode | CustomTestTreeNode } @@ -174,7 +177,7 @@ export function createOrUpdateNode( children: new Set(), tasks: [], indent: node.indent + 1, - duration: task.result?.duration, + duration, state: task.result?.state, } as SuiteTreeNode } diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 5065cc3498bd..e3ae04c9cffb 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -23,14 +23,21 @@ describe('running browser tests', async () => { console.error(stderr) }) - expect(browserResultJson.testResults).toHaveLength(18) - expect(passedTests).toHaveLength(16) + expect(browserResultJson.testResults).toHaveLength(19) + expect(passedTests).toHaveLength(17) expect(failedTests).toHaveLength(2) expect(stderr).not.toContain('has been externalized for browser compatibility') expect(stderr).not.toContain('Unhandled Error') }) + test('runs in-source tests', () => { + expect(stdout).toContain('src/actions.ts') + const actionsTest = passedTests.find(t => t.name.includes('/actions.ts')) + expect(actionsTest).toBeDefined() + expect(actionsTest.assertionResults).toHaveLength(1) + }) + test('correctly prints error', () => { expect(stderr).toContain('expected 1 to be 2') expect(stderr).toMatch(/- 2\s+\+ 1/) diff --git a/test/browser/src/actions.ts b/test/browser/src/actions.ts index 55ae85133596..bb783e81d065 100644 --- a/test/browser/src/actions.ts +++ b/test/browser/src/actions.ts @@ -1,3 +1,11 @@ export function plus(a: number, b: number) { return a + b } + +if (import.meta.vitest) { + const { test, expect } = import.meta.vitest + + test('in-source plus works correctly', () => { + expect(plus(1, 2)).toBe(3) + }) +} diff --git a/test/browser/tsconfig.json b/test/browser/tsconfig.json index 0468f44e9342..083a747ea373 100644 --- a/test/browser/tsconfig.json +++ b/test/browser/tsconfig.json @@ -9,7 +9,8 @@ "types": [ "vite/client", "@vitest/browser/providers/playwright", - "vitest-browser-react" + "vitest-browser-react", + "vitest/import-meta" ], "esModuleInterop": true } diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index ba958fb752ba..d51f62255d0c 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -28,6 +28,7 @@ export default defineConfig({ }, test: { include: ['test/**.test.{ts,js,tsx}'], + includeSource: ['src/*.ts'], // having a snapshot environment doesn't affect browser tests snapshotEnvironment: './custom-snapshot-env.ts', browser: { diff --git a/test/browser/vitest.config.unit.mts b/test/browser/vitest.config.unit.mts index a2a1707127d2..0618ce0ca59a 100644 --- a/test/browser/vitest.config.unit.mts +++ b/test/browser/vitest.config.unit.mts @@ -8,7 +8,7 @@ export default defineConfig({ singleFork: true, }, }, - hookTimeout: process.env.CI ? 120_000 : 10_000, - testTimeout: process.env.CI ? 120_000 : 10_000, + hookTimeout: process.env.CI ? 120_000 : 20_000, + testTimeout: process.env.CI ? 120_000 : 20_000, }, })