Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement istanbul coverage support for browser testing #3040

Merged
merged 24 commits into from Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0ef4875
feat: implement coverage support for browser testing
sheremet-va Mar 20, 2023
56e2176
chore: remove query when instrumenting coverage
sheremet-va Mar 20, 2023
c885538
fix: colec and run only a single test at a time
sheremet-va Mar 21, 2023
a5e33ba
chore: use /@id/ for importing
sheremet-va Mar 21, 2023
485f875
chore: cleanup
sheremet-va Mar 22, 2023
9341732
chre: don't run coverage in browser tests bu default
sheremet-va Mar 22, 2023
e251be9
chore: don't run no-op coverage apis on browser
AriPerkkio Mar 23, 2023
740cc82
test: browser with coverage
AriPerkkio Mar 24, 2023
730bf08
fix: browser to preserve coverage ignore hints comments
AriPerkkio Mar 24, 2023
17caf00
fix: browser plugin to resolve ids without query params
AriPerkkio Mar 24, 2023
1f981ac
fix: browser to use unique query parameter
AriPerkkio Mar 26, 2023
5bd1498
fix: throw error if c8 is used with --browser
AriPerkkio Mar 26, 2023
40975d4
fix: throw error if c8 is used with browser via poolMatchGlobs
AriPerkkio Mar 26, 2023
c66bef0
chore: lockfile
sheremet-va Mar 27, 2023
5ab4065
chore: add workaround for .mjs extension
sheremet-va Mar 27, 2023
4718952
test: browser watch mode
AriPerkkio Mar 27, 2023
7c0a961
fix: browser watch mode
AriPerkkio Mar 28, 2023
adfaea2
fix: browser with --root
AriPerkkio Mar 28, 2023
27ae18e
chore: allow test.aliases in browser mode
sheremet-va Mar 28, 2023
23b83ce
test: add test.alias to browser tests
sheremet-va Mar 28, 2023
af95db3
test: remove browser from global typechecking
sheremet-va Mar 28, 2023
b743282
test(watch): promise instead of polling
AriPerkkio Mar 28, 2023
59e22ee
chore: cleanup browser runner test
sheremet-va Mar 28, 2023
4473b2b
test: skip browser watch on windows
AriPerkkio Mar 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 15 additions & 3 deletions packages/browser/src/client/main.ts
Expand Up @@ -57,6 +57,8 @@ ws.addEventListener('open', async () => {
const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils')
const safeRpc = createSafeRpc(client, getSafeTimers)

// @ts-expect-error untyped global for internal use
globalThis.__vitest_browser__ = true
// @ts-expect-error mocking vitest apis
globalThis.__vitest_worker__ = {
config,
Expand All @@ -82,11 +84,20 @@ async function runTests(paths: string[], config: any) {
const viteClientPath = '/@vite/client'
await import(viteClientPath)

const { startTests, setupCommonEnv, setupSnapshotEnvironment } = await importId('vitest/browser') as typeof import('vitest/browser')
const {
startTests,
setupCommonEnv,
setupSnapshotEnvironment,
takeCoverageInsideWorker,
} = await importId('vitest/browser') as typeof import('vitest/browser')

const executor = {
executeId: (id: string) => importId(id),
}

if (!runner) {
const { VitestTestRunner } = await importId('vitest/runners') as typeof import('vitest/runners')
const BrowserRunner = createBrowserRunner(VitestTestRunner)
const BrowserRunner = createBrowserRunner(VitestTestRunner, { takeCoverage: () => takeCoverageInsideWorker(config.coverage, executor) })
runner = new BrowserRunner({ config, browserHashMap })
}

Expand All @@ -104,7 +115,8 @@ async function runTests(paths: string[], config: any) {
const now = `${new Date().getTime()}`
files.forEach(i => browserHashMap.set(i, now))

await startTests(files, runner)
for (const file of files)
await startTests([file], runner)
}
finally {
await rpcDone()
Expand Down
16 changes: 13 additions & 3 deletions packages/browser/src/client/runner.ts
Expand Up @@ -7,7 +7,11 @@ interface BrowserRunnerOptions {
browserHashMap: Map<string, string>
}

export function createBrowserRunner(original: any) {
interface CoverageHandler {
takeCoverage: () => Promise<unknown>
}

export function createBrowserRunner(original: any, coverageModule: CoverageHandler | null) {
return class BrowserTestRunner extends original {
public config: ResolvedConfig
hashMap = new Map<string, string>()
Expand All @@ -25,6 +29,12 @@ export function createBrowserRunner(original: any) {
})
}

async onAfterRunSuite() {
await super.onAfterRunSuite?.()
const coverage = await coverageModule?.takeCoverage?.()
await rpc().onAfterSuiteRun({ coverage })
}

onCollected(files: File[]): unknown {
return rpc().onCollected(files)
}
Expand All @@ -41,8 +51,8 @@ export function createBrowserRunner(original: any) {
this.hashMap.set(filepath, hash)
}
const importpath = match
? `/@fs/${filepath.slice(match[1].length)}?v=${hash}`
: `${filepath}?v=${hash}`
? `/@fs/${filepath.slice(match[1].length)}?vitest=${hash}`
: `${filepath}?vitest=${hash}`
await import(importpath)
}
}
Expand Down
5 changes: 5 additions & 0 deletions packages/browser/src/node/index.ts
Expand Up @@ -18,6 +18,11 @@ export default (base = '/'): Plugin[] => {
{
enforce: 'pre',
name: 'vitest:browser',
async config(viteConfig) {
// Enables using ignore hint for coverage providers with @preserve keyword
viteConfig.esbuild ||= {}
viteConfig.esbuild.legalComments = 'inline'
},
async configureServer(server) {
server.middlewares.use(
base,
Expand Down
1 change: 1 addition & 0 deletions packages/coverage-c8/rollup.config.js
Expand Up @@ -10,6 +10,7 @@ import pkg from './package.json'

const entries = {
index: 'src/index.ts',
provider: 'src/provider.ts',
}

const external = [
Expand Down
13 changes: 9 additions & 4 deletions packages/coverage-c8/src/index.ts
@@ -1,6 +1,11 @@
export * from './takeCoverage'
import * as coverage from './takeCoverage'

export async function getProvider() {
const { C8CoverageProvider } = await import('./provider')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removal of named export getProvider caused a breaking change. The @vitest/coverage-istanbul is still exporting it as both named and default. Was this intentional @sheremet-va?

There are some reports where users are using different versions of vitest and @vitest/coverage-c8 and the API breaks:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not intentional. Should've either not remove it or update peerDependencies. The next version is locked to be 0.30.0 though, so we will have to wait for a fix, but PR is welcome.

Copy link
Member

@AriPerkkio AriPerkkio Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a fix we can just bump the peerDependency version for 0.30.0 release and it will prevent the usage of mixed versions. I'll set PR soon.

I'm also thinking about a feature that would include the versions of vitest and @vitest/* packages in the logs of unhandled crashes, like seen here on #3118 and here on #3125. It would help users to see that they are not using the latest versions when they think they are.

return new C8CoverageProvider()
export default {
...coverage,
async getProvider() {
// to not bundle the provider
AriPerkkio marked this conversation as resolved.
Show resolved Hide resolved
const name = './provider.js'
const { C8CoverageProvider } = await import(name) as typeof import('./provider')
return new C8CoverageProvider()
},
}
1 change: 1 addition & 0 deletions packages/coverage-istanbul/rollup.config.js
Expand Up @@ -10,6 +10,7 @@ import pkg from './package.json'

const entries = {
index: 'src/index.ts',
provider: 'src/provider.ts',
}

const external = [
Expand Down
9 changes: 8 additions & 1 deletion packages/coverage-istanbul/src/index.ts
@@ -1,7 +1,9 @@
import { COVERAGE_STORE_KEY } from './constants'

export async function getProvider() {
const { IstanbulCoverageProvider } = await import('./provider')
// to not bundle the provider
const providerPath = './provider.js'
const { IstanbulCoverageProvider } = await import(providerPath) as typeof import('./provider')
return new IstanbulCoverageProvider()
}

Expand All @@ -15,3 +17,8 @@ export function takeCoverage() {

return coverage
}

export default {
getProvider,
takeCoverage,
}
3 changes: 3 additions & 0 deletions packages/vitest/src/api/setup.ts
Expand Up @@ -46,6 +46,9 @@ export function setup(ctx: Vitest, server?: ViteDevServer) {
ctx.state.updateTasks(packs)
await ctx.report('onTaskUpdate', packs)
},
onAfterSuiteRun(meta) {
ctx.coverageProvider?.onAfterSuiteRun(meta)
},
getFiles() {
return ctx.state.getFiles()
},
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/api/types.ts
@@ -1,5 +1,5 @@
import type { TransformResult } from 'vite'
import type { File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types'
import type { AfterSuiteRunMeta, File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types'

export interface TransformResultWithSource extends TransformResult {
source?: string
Expand All @@ -8,6 +8,7 @@ export interface TransformResultWithSource extends TransformResult {
export interface WebSocketHandlers {
onCollected(files?: File[]): Promise<void>
onTaskUpdate(packs: TaskResultPack[]): void
onAfterSuiteRun(meta: AfterSuiteRunMeta): void
onDone(name: string): void
sendLog(log: UserConsoleLog): void
getFiles(): File[]
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/browser.ts
@@ -1,3 +1,4 @@
export { startTests } from '@vitest/runner'
export { setupCommonEnv } from './runtime/setup.common'
export { setupSnapshotEnvironment } from './integrations/snapshot/env'
export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'
2 changes: 2 additions & 0 deletions packages/vitest/src/integrations/browser/server.ts
Expand Up @@ -6,6 +6,7 @@ import type { Vitest } from '../../node'
import type { UserConfig } from '../../types/config'
import { ensurePackageInstalled } from '../../node/pkg'
import { resolveApiServerConfig } from '../../node/config'
import { CoverageTransform } from '../../node/plugins/coverageTransform'

export async function createBrowserServer(ctx: Vitest, options: UserConfig) {
const root = ctx.config.root
Expand All @@ -31,6 +32,7 @@ export async function createBrowserServer(ctx: Vitest, options: UserConfig) {
},
plugins: [
(await import('@vitest/browser')).default('/'),
CoverageTransform(ctx),
{
enforce: 'post',
name: 'vitest:browser:config',
Expand Down
7 changes: 4 additions & 3 deletions packages/vitest/src/integrations/coverage.ts
@@ -1,4 +1,3 @@
import { importModule } from 'local-pkg'
import type { CoverageOptions, CoverageProvider, CoverageProviderModule } from '../types'

interface Loader {
Expand All @@ -16,8 +15,10 @@ async function resolveCoverageProviderModule(options: CoverageOptions | undefine

const provider = options.provider

if (provider === 'c8' || provider === 'istanbul')
return await importModule<CoverageProviderModule>(CoverageProviderMap[provider])
if (provider === 'c8' || provider === 'istanbul') {
const { default: coverageModule } = await loader.executeId(CoverageProviderMap[provider])
return coverageModule
}

let customProviderModule

Expand Down
10 changes: 10 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -113,6 +113,9 @@ export function resolveConfig(
}
}

if (resolved.coverage.provider === 'c8' && resolved.coverage.enabled && isBrowserEnabled(resolved))
throw new Error('@vitest/coverage-c8 does not work with --browser. Use @vitest/coverage-istanbul instead')

resolved.deps = resolved.deps || {}
// vitenode will try to import such file with native node,
// but then our mocker will not work properly
Expand Down Expand Up @@ -263,3 +266,10 @@ export function resolveConfig(

return resolved
}

export function isBrowserEnabled(config: ResolvedConfig) {
if (config.browser.enabled)
return true

return config.poolMatchGlobs?.length && config.poolMatchGlobs.some(([, pool]) => pool === 'browser')
}
6 changes: 2 additions & 4 deletions packages/vitest/src/node/core.ts
Expand Up @@ -18,7 +18,7 @@ import { createPool } from './pool'
import type { ProcessPool } from './pool'
import { createBenchmarkReporters, createReporters } from './reporters/utils'
import { StateManager } from './state'
import { resolveConfig } from './config'
import { isBrowserEnabled, resolveConfig } from './config'
import { Logger } from './logger'
import { VitestCache } from './cache'
import { VitestServer } from './server'
Expand Down Expand Up @@ -725,9 +725,7 @@ export class Vitest {
}

isBrowserEnabled() {
if (this.config.browser.enabled)
return true
return this.config.poolMatchGlobs?.length && this.config.poolMatchGlobs.some(([, pool]) => pool === 'browser')
return isBrowserEnabled(this.config)
}

// The server needs to be running for communication
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/node/plugins/coverageTransform.ts
@@ -1,12 +1,13 @@
import type { Plugin as VitePlugin } from 'vite'
import { normalizeRequestId } from 'vite-node/utils'

import type { Vitest } from '../core'

export function CoverageTransform(ctx: Vitest): VitePlugin | null {
return {
name: 'vitest:coverage-transform',
transform(srcCode, id) {
return ctx.coverageProvider?.onFileTransform?.(srcCode, id, this)
return ctx.coverageProvider?.onFileTransform?.(srcCode, normalizeRequestId(id), this)
},
}
}