Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263import type { StackTraceParserOptions } from '@vitest/utils/source-map'import type { HtmlTagDescriptor } from 'vite'import type { ParsedStack, TestError } from 'vitest'import type { BrowserCommand, BrowserScript, CDPSession, ResolvedConfig, TestProject, Vite, Vitest,} from 'vitest/node'import type { BrowserServerState } from './state'import { readFile } from 'node:fs/promises'import { distClientRoot as uiClientRoot } from '@vitest/ui'import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map'import { extractSourcemapFromFile } from '@vitest/utils/source-map/node'import { join, resolve } from 'pathe'import { BrowserServerCDPHandler } from './cdp'import builtinCommands from './commands/index'import { distRoot } from './constants'import { ProjectBrowser } from './project'import { slash } from './utils'
export class ParentBrowserProject { public orchestratorScripts: string | undefined
public faviconUrl: string public prefixOrchestratorUrl: string public prefixTesterUrl: string public manifest: Promise<Vite.Manifest> | Vite.Manifest
public vite!: Vite.ViteDevServer private stackTraceOptions: StackTraceParserOptions public orchestratorHtml: Promise<string> | string public injectorJs: Promise<string> | string public errorCatcherUrl: string public locatorsUrl: string | undefined public matchersUrl: string public stateJs: Promise<string> | string
public initScripts: string[] = []
public commands: Record<string, BrowserCommand<any>> = {} public children: Set<ProjectBrowser> = new Set() public vitest: Vitest
public config: ResolvedConfig
// cache for non-vite source maps private sourceMapCache = new Map<string, any>()
constructor( ctx: { config: ResolvedConfig; vitest: Vitest }, public base: string, ) { this.vitest = ctx.vitest this.config = ctx.config this.stackTraceOptions = { frameFilter: this.config.onStackTrace, getSourceMap: (id) => { if (this.sourceMapCache.has(id)) { return this.sourceMapCache.get(id) }
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult const filePath = id.split('?')[0] // prefer the map stored on disk when the transform pipeline can't // provide a usable one: // - pre-bundled deps: the pipeline map resolves back into the // optimizer cache, while the map esbuild wrote next to the file // points at the real package sources // - an empty `mappings` means the map was intentionally not served // to the browser (`vitest:browser:framework-sourcemaps`), but disk // is still the source of truth for error stack traces if ( result && (!result.map || result.map.mappings === '' || filePath.startsWith(this.vite.config.cacheDir)) ) { const extracted = extractSourcemapFromFile(result.code, filePath) this.sourceMapCache.set(id, extracted?.map) return extracted?.map }
return result?.map }, getUrlId: (id) => { const moduleGraph = this.vite.environments.client.moduleGraph const mod = moduleGraph.getModuleById(id) if (mod) { return id } const resolvedPath = resolve(this.vite.config.root, id.slice(1)) const modUrl = moduleGraph.getModuleById(resolvedPath) if (modUrl) { return resolvedPath } // some browsers (looking at you, safari) don't report queries in stack traces // the next best thing is to try the first id that this file resolves to const files = moduleGraph.getModulesByFile(resolvedPath) if (files && files.size) { return files.values().next().value!.id! } return id }, }
for (const [name, command] of Object.entries(builtinCommands)) { this.commands[name] ??= command }
// validate names because they can't be used as identifiers for (const command in this.config.browser.commands) { if (!/^[a-z_$][\w$]*$/i.test(command)) { throw new Error( `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`, ) } this.commands[command] = this.config.browser.commands[command] }
this.prefixTesterUrl = `${base || '/'}` this.prefixOrchestratorUrl = `${base}__vitest_test__/` this.faviconUrl = `${base}__vitest__/favicon.svg`
this.manifest = (async () => { return JSON.parse(await readFile(`${distRoot}/client/.vite/manifest.json`, 'utf8')) })().then((manifest) => (this.manifest = manifest))
this.orchestratorHtml = ( this.config.browser.ui ? readFile(resolve(uiClientRoot, 'index.html'), 'utf8') : readFile(resolve(distRoot, 'client/orchestrator.html'), 'utf8') ).then((html) => (this.orchestratorHtml = html)) this.injectorJs = readFile(resolve(distRoot, 'client/esm-client-injector.js'), 'utf8').then( (js) => (this.injectorJs = js), ) this.errorCatcherUrl = join('/@fs/', resolve(distRoot, 'client/error-catcher.js'))
this.matchersUrl = join('/@fs/', distRoot, 'expect-element.js') this.stateJs = readFile(resolve(distRoot, 'state.js'), 'utf-8').then( (js) => (this.stateJs = js), ) }
public setServer(vite: Vite.ViteDevServer): void { this.vite = vite }
public spawn(project: TestProject): ProjectBrowser { if (!this.vite) { throw new Error(`Cannot spawn child server without a parent dev server.`) } const clone = new ProjectBrowser(this, project, '/') this.children.add(clone) return clone }
public parseErrorStacktrace(e: TestError, options: StackTraceParserOptions = {}): ParsedStack[] { return parseErrorStacktrace(e, { ...this.stackTraceOptions, ...options, }) }
public parseStacktrace(trace: string, options: StackTraceParserOptions = {}): ParsedStack[] { return parseStacktrace(trace, { ...this.stackTraceOptions, ...options, }) }
public readonly cdps: Map<string, BrowserServerCDPHandler> = new Map() private cdpSessionsPromises = new Map<string, Promise<CDPSession>>()
async ensureCDPHandler(sessionId: string, rpcId: string): Promise<BrowserServerCDPHandler> { const cachedHandler = this.cdps.get(rpcId) if (cachedHandler) { return cachedHandler } const browserSession = this.vitest._browserSessions.getSession(sessionId) if (!browserSession) { throw new Error(`Session "${sessionId}" not found.`) }
const browser = browserSession.project.browser! const provider = browser.provider if (!provider) { throw new Error( `Browser provider is not defined for the project "${browserSession.project.name}".`, ) } if (!provider.getCDPSession) { throw new Error(`CDP is not supported by the provider "${provider.name}".`) }
const session = (await this.cdpSessionsPromises.get(rpcId)) ?? (await (async () => { const promise = provider.getCDPSession!(sessionId).finally(() => { this.cdpSessionsPromises.delete(rpcId) }) this.cdpSessionsPromises.set(rpcId, promise) return promise })())
const rpc = (browser.state as BrowserServerState).testers.get(rpcId) if (!rpc) { throw new Error(`Tester RPC "${rpcId}" was not established.`) }
const handler = new BrowserServerCDPHandler(session, rpc) this.cdps.set(rpcId, handler) return handler }
removeCDPHandler(sessionId: string): void { this.cdps.delete(sessionId) }
async formatScripts(scripts: BrowserScript[] | undefined): Promise<HtmlTagDescriptor[]> { if (!scripts?.length) { return [] } const server = this.vite const promises = scripts.map( async ({ content, src, async, id, type = 'module' }, index): Promise<HtmlTagDescriptor> => { const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`) await server.moduleGraph.ensureEntryFromUrl(transformId) const contentProcessed = content && type === 'module' ? (await server.pluginContainer.transform(content, transformId)).code : content return { tag: 'script', attrs: { type, ...(async ? { async: '' } : {}), ...(srcLink ? { src: srcLink.startsWith('http') ? srcLink : slash(`/@fs/${srcLink}`), } : {}), }, injectTo: 'head', children: contentProcessed || '', } }, ) return await Promise.all(promises) }
resolveTesterUrl(pathname: string): { sessionId: string; testFile: string } { const [sessionId, testFile] = pathname.slice(this.prefixTesterUrl.length).split('/') const decodedTestFile = decodeURIComponent(testFile) return { sessionId, testFile: decodedTestFile } }}