[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(ui)!: harden UI API access (#10583)

Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>

authored by

Hiroshi Ogawa
Hiroshi Ogawa
Codex
and committed by
GitHub
(Jun 19, 2026, 9:21 AM +0100) 4c26d767 4cbf6b4a

+208 -45
+9
docs/guide/migration.md
··· 43 43 - **`benchmark.outputJson` config and the `--outputJson` CLI flag** are removed. Use `--reporter=json --outputFile=<path>` to capture benchmark results; the JSON reporter now includes a `benchmarks` field on each test case. 44 44 - **`Vitest` instance `mode` property** is now always `'test'`. The previous `'benchmark'` value is no longer used; benchmarks run inside a dedicated project of the same `Vitest` instance. 45 45 46 + ### Vitest UI Requires an Authenticated URL 47 + 48 + Vitest UI now requires token authentication for the HTML page and API access. The `/__vitest__/` URL will show an error until the browser is authenticated. To authenticate, open the URL with a token printed by Vitest, as shown below. Once authenticated, the direct `/__vitest__/` URL will work correctly. 49 + 50 + ```bash 51 + vitest --ui 52 + # UI started at http://localhost:51204/__vitest__/?token=... 53 + ``` 54 + 46 55 ### Removed `test.sequential`, `describe.sequential`, and `sequential` Options 47 56 48 57 Vitest 5.0 removes the deprecated `test.sequential`, `describe.sequential`, and `sequential` test options. Use `concurrent: false` when you need a test or suite to opt out of inherited or globally configured concurrency.
+4
docs/guide/ui.md
··· 18 18 19 19 Then you can visit the Vitest UI at <a href="http://localhost:51204/__vitest__/">`http://localhost:51204/__vitest__/`</a> 20 20 21 + ::: tip 22 + Vitest UI access is protected. If the direct URL shows an error, open the URL with a token printed by Vitest in the terminal, for example `http://localhost:51204/__vitest__/?token=...`. 23 + ::: 24 + 21 25 ::: warning 22 26 The UI is interactive and requires a running Vite server, so make sure to run Vitest in `watch` mode (the default). Alternatively, you can generate a static HTML report that looks identical to the Vitest UI by specifying `html` in config's `reporters` option. 23 27 :::
+3
packages/browser/src/node/plugin.ts
··· 17 17 rolldownVersion, 18 18 distDir as vitestDist, 19 19 } from 'vitest/node' 20 + import { API_TOKEN_FILE } from '../../../vitest/src/node/config/apiToken' 20 21 import { distRoot } from './constants' 21 22 import { createOrchestratorMiddleware } from './middlewares/orchestratorMiddleware' 22 23 import { createTesterMiddleware } from './middlewares/testerMiddleware' ··· 402 403 } 403 404 viteConfig.server.fs ??= {} 404 405 viteConfig.server.fs.allow = viteConfig.server.fs.allow || [] 406 + viteConfig.server.fs.deny ??= [] 407 + viteConfig.server.fs.deny.push(API_TOKEN_FILE) 405 408 viteConfig.server.fs.allow.push( 406 409 ...resolveFsAllow( 407 410 parentServer.vitest.config.root,
+21
packages/ui/node/index.ts
··· 1 1 import type { Vite, Vitest } from 'vitest/node' 2 2 import fs from 'node:fs' 3 + import { parse as parseCookie, serialize as serializeCookie } from 'cookie' 3 4 import { join, resolve } from 'pathe' 4 5 import sirv from 'sirv' 5 6 import c from 'tinyrainbow' ··· 8 9 import { distClientRoot } from './paths' 9 10 10 11 export { distClientRoot } 12 + 13 + const UI_TOKEN_COOKIE = 'vitest-ui-token' 11 14 12 15 export default (ctx: Vitest): Vite.Plugin => { 13 16 if (ctx.version !== version) { ··· 94 97 if (req.url) { 95 98 const url = new URL(req.url, 'http://localhost') 96 99 if (url.pathname === base) { 100 + if (isValidApiRequest(ctx.config, req)) { 101 + res.statusCode = 302 102 + res.setHeader('Set-Cookie', serializeCookie(UI_TOKEN_COOKIE, ctx.config.api.token, { 103 + path: base, 104 + httpOnly: true, 105 + sameSite: 'strict', 106 + })) 107 + res.setHeader('Location', base) 108 + res.end() 109 + return 110 + } 111 + const cookieToken = parseCookie(req.headers.cookie ?? '')[UI_TOKEN_COOKIE] 112 + if (cookieToken !== ctx.config.api.token) { 113 + res.statusCode = 403 114 + res.end('Vitest UI requires authentication. Open the URL with the token printed in the terminal, e.g. http://localhost:51204/__vitest__/?token=...') 115 + return 116 + } 97 117 const html = clientIndexHtml.replace( 98 118 '<!-- !LOAD_METADATA! -->', 99 119 `<script>window.VITEST_API_TOKEN = ${JSON.stringify(ctx.config.api.token)}</script>`, 100 120 ) 101 121 res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate') 122 + res.setHeader('Referrer-Policy', 'no-referrer') 102 123 res.setHeader('Content-Type', 'text/html; charset=utf-8') 103 124 res.write(html) 104 125 res.end()
+1
packages/ui/package.json
··· 79 79 "birpc": "catalog:", 80 80 "codemirror": "^5.65.18", 81 81 "codemirror-theme-vars": "^0.1.2", 82 + "cookie": "^1.0.2", 82 83 "d3-graph-controller": "^3.1.8", 83 84 "floating-vue": "^5.2.2", 84 85 "mime": "^4.1.0",
+3 -12
packages/ui/vite.config.ts
··· 6 6 import { presetAttributify, presetIcons, presetWind3, transformerDirectives } from 'unocss' 7 7 import Unocss from 'unocss/vite' 8 8 import { defineConfig } from 'vite' 9 + import { resolveApiToken } from '../vitest/src/node/config/apiToken' 9 10 10 11 export default defineConfig({ 11 12 base: './', ··· 67 68 }) 68 69 69 70 function devUiScriptPlugin(): Plugin { 70 - const UI_SCRIPT_RE = /<script>(window\.VITEST_API_TOKEN\s*=\s*"[^"]+")<\/script>/ 71 71 const BROWSER_SCRIPT_RE = /<script type="module">([\s\S]*?window\.__vitest_browser_runner__\s*=\s*\{[\s\S]*?window\.VITEST_API_TOKEN\s*=[\s\S]*?)<\/script>/ 72 72 73 - const uiUrl = `http://localhost:${process.env.VITE_PORT || '51204'}/__vitest__/` 74 73 const browserUrl = `http://localhost:${process.env.BROWSER_DEV_PORT || '63315'}/__vitest_test__/` 75 74 76 75 return { ··· 99 98 ] 100 99 } 101 100 102 - const response = await fetch(uiUrl) 103 - if (!response.ok) { 104 - throw new Error(`Failed to fetch VITEST_API_TOKEN from ${uiUrl}`) 105 - } 106 - const testHtml = await response.text() 107 - const tokenScript = testHtml.match(UI_SCRIPT_RE)?.[1] 108 - if (!tokenScript) { 109 - throw new Error('Failed to extract VITEST_API_TOKEN from the response') 110 - } 101 + const token = resolveApiToken(process.cwd()).token 111 102 return [ 112 103 { 113 104 tag: 'script', 114 - children: tokenScript, 105 + children: `window.VITEST_API_TOKEN = ${JSON.stringify(token)}`, 115 106 injectTo: 'head-prepend', 116 107 }, 117 108 ]
+57
packages/vitest/src/node/config/apiToken.ts
··· 1 + import crypto from 'node:crypto' 2 + import { 3 + chmodSync, 4 + existsSync, 5 + mkdirSync, 6 + readFileSync, 7 + writeFileSync, 8 + } from 'node:fs' 9 + import { homedir } from 'node:os' 10 + import { dirname, join } from 'pathe' 11 + import { searchForWorkspaceRoot } from 'vite' 12 + 13 + export const API_TOKEN_FILE = '.vitest-secret-token' 14 + 15 + // Follows env-paths' user data directory conventions: 16 + // https://github.com/sindresorhus/env-paths/blob/v4.0.0/index.js 17 + function getUserDataDir(): string { 18 + if (process.platform === 'win32') { 19 + return process.env.LOCALAPPDATA || join(homedir(), 'AppData/Local') 20 + } 21 + if (process.platform === 'darwin') { 22 + return join(homedir(), 'Library/Application Support') 23 + } 24 + return process.env.XDG_DATA_HOME || join(homedir(), '.local/share') 25 + } 26 + 27 + function resolveTokenFromPath(tokenPath: string): { token: string; tokenCreated: boolean } { 28 + if (existsSync(tokenPath)) { 29 + return { token: readFileSync(tokenPath, 'utf-8').trim(), tokenCreated: false } 30 + } 31 + 32 + const token = crypto.randomUUID() 33 + mkdirSync(dirname(tokenPath), { recursive: true, mode: 0o700 }) 34 + writeFileSync(tokenPath, `${token}\n`, { mode: 0o600 }) 35 + try { 36 + chmodSync(dirname(tokenPath), 0o700) 37 + chmodSync(tokenPath, 0o600) 38 + } 39 + catch {} 40 + return { token, tokenCreated: true } 41 + } 42 + 43 + export function resolveApiToken(root: string): { token: string; tokenCreated: boolean; tokenPath: string } { 44 + const tokenPaths = [ 45 + join(getUserDataDir(), 'vitest', API_TOKEN_FILE), 46 + join(searchForWorkspaceRoot(root), 'node_modules/.vitest', API_TOKEN_FILE), 47 + ] 48 + 49 + for (const tokenPath of tokenPaths) { 50 + try { 51 + return { ...resolveTokenFromPath(tokenPath), tokenPath } 52 + } 53 + catch {} 54 + } 55 + 56 + throw new Error(`Failed to create Vitest API token at ${tokenPaths.join(' or ')}`) 57 + }
+3 -2
packages/vitest/src/node/config/resolveConfig.ts
··· 8 8 UserConfig, 9 9 } from '../types/config' 10 10 import type { CoverageOptions, CoverageReporterWithOptions } from '../types/coverage' 11 - import crypto from 'node:crypto' 12 11 import { existsSync, statSync } from 'node:fs' 13 12 import { pathToFileURL } from 'node:url' 14 13 import { slash, toArray } from '@vitest/utils/helpers' ··· 29 28 import { withLabel } from '../reporters/renderers/utils' 30 29 import { BaseSequencer } from '../sequencers/BaseSequencer' 31 30 import { RandomSequencer } from '../sequencers/RandomSequencer' 31 + import { resolveApiToken } from './apiToken' 32 32 33 33 function resolvePath(path: string, root: string) { 34 34 // local-pkg (mlly)'s resolveModule("./file", { paths: ["/some/root"] }) tries ··· 659 659 660 660 // the server has been created, we don't need to override vite.server options 661 661 const api = resolveApiServerConfig(options, defaultPort) 662 - resolved.api = { ...api, token: crypto.randomUUID() } 662 + const { token, tokenCreated } = resolveApiToken(resolved.root) 663 + resolved.api = { ...api, token, tokenCreated } 663 664 664 665 if (options.related) { 665 666 resolved.related = toArray(options.related).map(file =>
+26
packages/vitest/src/node/create.ts
··· 75 75 76 76 if (ctx.config.api?.port) { 77 77 await server.listen() 78 + if (ctx.config.ui && ctx.config.open) { 79 + // Note: `tokenCreated` is only an approximation of "the browser is not 80 + // authenticated yet". If the user clears cookies while the token file 81 + // persists, the clean URL will block until they re-open the `?token=` 82 + // URL printed in the terminal. 83 + if (ctx.config.api.tokenCreated) { 84 + // First run that generated the token: no browser holds the auth 85 + // cookie yet, so open the authenticated URL to set it. A new tab 86 + // here is fine since no clean-URL tab exists to reuse. 87 + const url = new URL(ctx.config.uiBase, 'http://localhost') 88 + url.searchParams.set('token', ctx.config.api.token) 89 + server.config.server.open = `${url.pathname}${url.search}` 90 + } 91 + else { 92 + // Subsequent runs: open the clean UI base URL (without `?token=`) 93 + // rather than the authenticated URL printed by the logger. On macOS, 94 + // `openBrowser` reuses an existing tab whose URL matches via substring 95 + // and reloads it (Vite's `bin/openChrome.js`). Since the 302 redirect 96 + // strips the token, an already-authenticated tab lives at the clean 97 + // URL, so opening the clean URL matches and reloads it; opening the 98 + // token URL would never match and would spawn a new tab on every 99 + // restart. 100 + server.config.server.open = ctx.config.uiBase 101 + } 102 + server.openBrowser() 103 + } 78 104 } 79 105 80 106 return ctx
+3 -2
packages/vitest/src/node/logger.ts
··· 243 243 if (this.ctx.config.ui) { 244 244 const host = this.ctx.config.api?.host || 'localhost' 245 245 const port = this.ctx.vite.config.server.port 246 - const base = this.ctx.config.uiBase 246 + const url = new URL(this.ctx.config.uiBase, `http://${host}:${port}`) 247 + url.searchParams.set('token', this.ctx.config.api.token) 247 248 248 - this.log(PAD + c.dim(c.green(`UI started at http://${host}:${c.bold(port)}${base}`))) 249 + this.log(PAD + c.dim(c.green(`UI started at ${url}`))) 249 250 } 250 251 else if (this.ctx.config.api?.port) { 251 252 const resolvedUrls = this.ctx.vite.resolvedUrls
+4 -7
packages/vitest/src/node/plugins/index.ts
··· 6 6 import { defaultPort } from '../../constants' 7 7 import { configDefaults } from '../../defaults' 8 8 import { generateScopedClassName } from '../../integrations/css/css-modules' 9 + import { API_TOKEN_FILE } from '../config/apiToken' 9 10 import { resolveApiServerConfig } from '../config/resolveConfig' 10 11 import { Vitest } from '../core' 11 12 import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' ··· 67 68 ;(options as unknown as ResolvedConfig).defines = defines 68 69 ;(options as unknown as ResolvedConfig).viteDefine = originalDefine 69 70 70 - let open: string | boolean | undefined = false 71 - 72 - if (testConfig.ui && testConfig.open) { 73 - open = testConfig.uiBase ?? '/__vitest__/' 74 - } 75 - 76 71 const resolveOptions = getDefaultResolveOptions() 77 72 78 73 let config: ViteConfig = { ··· 88 83 }, 89 84 server: { 90 85 ...testConfig.api, 91 - open, 86 + // auto open UI via `vite.openBrowser` manually later 87 + open: false, 92 88 hmr: false, 93 89 ws: testConfig.api?.middlewareMode ? false : undefined, 94 90 preTransformRequests: false, 95 91 fs: { 96 92 allow: resolveFsAllow(options.root || process.cwd(), testConfig.config), 93 + deny: [API_TOKEN_FILE], 97 94 }, 98 95 }, 99 96 build: {
+2
packages/vitest/src/node/plugins/workspace.ts
··· 7 7 import * as vite from 'vite' 8 8 import { configDefaults } from '../../defaults' 9 9 import { generateScopedClassName } from '../../integrations/css/css-modules' 10 + import { API_TOKEN_FILE } from '../config/apiToken' 10 11 import { VitestFilteredOutProjectError } from '../errors' 11 12 import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' 12 13 import { CoverageTransform } from './coverageTransform' ··· 175 176 project.vitest.config.root, 176 177 project.vitest.vite.config.configFile, 177 178 ), 179 + deny: [API_TOKEN_FILE], 178 180 }, 179 181 }, 180 182 // eslint-disable-next-line ts/ban-ts-comment
+1 -1
packages/vitest/src/node/types/config.ts
··· 1185 1185 defines: Record<string, any> 1186 1186 viteDefine: Record<string, any> 1187 1187 1188 - api: ApiConfig & { token: string } 1188 + api: ApiConfig & { token: string; tokenCreated: boolean } 1189 1189 cliExclude?: string[] 1190 1190 1191 1191 project: string[]
+9 -6
pnpm-lock.yaml
··· 943 943 codemirror-theme-vars: 944 944 specifier: ^0.1.2 945 945 version: 0.1.2 946 + cookie: 947 + specifier: ^1.0.2 948 + version: 1.0.2 946 949 d3-graph-controller: 947 950 specifier: ^3.1.8 948 951 version: 3.1.8 ··· 1287 1290 version: 2.4.11(@vue/compiler-dom@3.5.29)(@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) 1288 1291 happy-dom: 1289 1292 specifier: latest 1290 - version: 20.10.2 1293 + version: 20.10.6 1291 1294 istanbul-lib-coverage: 1292 1295 specifier: 'catalog:' 1293 1296 version: 3.2.2 ··· 1524 1527 version: link:../../packages/browser-preview 1525 1528 happy-dom: 1526 1529 specifier: latest 1527 - version: 20.10.2 1530 + version: 20.10.6 1528 1531 vitest: 1529 1532 specifier: workspace:* 1530 1533 version: link:../../packages/vitest ··· 7280 7283 handle-thing@2.0.1: 7281 7284 resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} 7282 7285 7283 - happy-dom@20.10.2: 7284 - resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==} 7286 + happy-dom@20.10.6: 7287 + resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} 7285 7288 engines: {node: '>=20.0.0'} 7286 7289 7287 7290 happy-dom@20.8.3: ··· 13677 13680 eslint: 10.0.3(jiti@2.6.1) 13678 13681 json-stable-stringify-without-jsonify: 1.0.1 13679 13682 lodash.merge: 4.6.2 13680 - semver: 7.7.4 13683 + semver: 7.8.3 13681 13684 transitivePeerDependencies: 13682 13685 - supports-color 13683 13686 - typescript ··· 16165 16168 16166 16169 handle-thing@2.0.1: {} 16167 16170 16168 - happy-dom@20.10.2: 16171 + happy-dom@20.10.6: 16169 16172 dependencies: 16170 16173 '@types/node': 24.12.0 16171 16174 '@types/whatwg-mimetype': 3.0.2
+2
test/e2e/test/override.test.ts
··· 31 31 allowWrite: true, 32 32 middlewareMode: true, 33 33 token: expect.any(String), 34 + tokenCreated: expect.any(Boolean), 34 35 }) 35 36 }) 36 37 ··· 47 48 allowWrite: true, 48 49 allowExec: true, 49 50 token: expect.any(String), 51 + tokenCreated: expect.any(Boolean), 50 52 }) 51 53 }) 52 54
+1 -1
test/ui/test/editor.spec.ts
··· 21 21 reporters: [], 22 22 }) 23 23 vitest = server.vitest 24 - baseURL = `${server.url}/__vitest__/` 24 + baseURL = server.url 25 25 }) 26 26 27 27 test.afterAll(async () => {
+4 -1
test/ui/test/helper.ts
··· 29 29 const address = vitest.vite.httpServer?.address() 30 30 assert(address && typeof address === 'object', 'Invalid server address') 31 31 32 + const uiUrl = new URL(vitest.config.uiBase, `http://localhost:${address.port}`) 33 + uiUrl.searchParams.set('token', vitest.config.api.token) 34 + 32 35 return { 33 36 vitest, 34 - url: `http://localhost:${address.port}`, 37 + url: uiUrl.toString(), 35 38 } 36 39 } 37 40
+1 -1
test/ui/test/trace-stream.spec.ts
··· 32 32 }, 33 33 ) 34 34 vitest = server.vitest 35 - baseURL = `${server.url}/__vitest__/` 35 + baseURL = server.url 36 36 }) 37 37 38 38 test.afterAll(async () => {
+1 -1
test/ui/test/trace.spec.ts
··· 17 17 open: false, 18 18 }) 19 19 vitest = server.vitest 20 - baseURL = `${server.url}/__vitest__/` 20 + baseURL = server.url 21 21 }) 22 22 23 23 test.afterAll(async () => {
+53 -11
test/ui/test/ui.spec.ts
··· 1 1 import type { Page } from '@playwright/test' 2 2 import type { PreviewServer } from 'vite' 3 3 import type { Vitest } from 'vitest/node' 4 + import { existsSync } from 'node:fs' 4 5 import { expect, test } from '@playwright/test' 6 + import { join } from 'pathe' 7 + import { resolveApiToken } from '../../../packages/vitest/src/node/config/apiToken' 5 8 import { assertDownloadAttachment, assertImageAttachment, assertTestCounts, getExplorerItem, openExplorerFileItem, startHtmlReportPreview, startVitestUi } from './helper' 9 + 10 + const TEST_COUNTS = { 11 + pass: 18, 12 + fail: 3, 13 + files: { 14 + pass: 7, 15 + }, 16 + } 6 17 7 18 test.describe('ui', () => { 8 19 let vitest: Vitest | undefined ··· 18 29 reporters: [], 19 30 }) 20 31 vitest = server.vitest 21 - pageUrl = `${server.url}/__vitest__/` 32 + pageUrl = server.url 22 33 }) 23 34 24 35 test.afterAll(async () => { ··· 31 42 32 43 test('cross origin access', async ({ page }) => { 33 44 await testCrossOriginAccess(page, pageUrl) 45 + }) 46 + 47 + test('blocks unauthenticated ui html requests', async ({ request }) => { 48 + const cleanUrl = new URL(pageUrl) 49 + cleanUrl.search = '' 50 + const cleanPageUrl = cleanUrl.toString() 51 + 52 + const tokenless = await request.get(cleanPageUrl) 53 + expect(tokenless.status()).toBe(403) 54 + await expect(tokenless.text()).resolves.toContain('Vitest UI requires authentication.') 55 + 56 + const badTokenUrl = new URL(cleanPageUrl) 57 + badTokenUrl.searchParams.set('token', 'invalid') 58 + const badToken = await request.get(badTokenUrl.toString()) 59 + expect(badToken.status()).toBe(403) 60 + await expect(badToken.text()).resolves.toContain('Vitest UI requires authentication.') 61 + }) 62 + 63 + test('does not serve the api token file', async ({ request }) => { 64 + const { tokenPath } = resolveApiToken(vitest!.config.root) 65 + expect(existsSync(tokenPath)).toBe(true) 66 + 67 + const fsUrl = new URL(join('/@fs/', tokenPath), pageUrl) 68 + const res = await request.get(fsUrl.toString()) 69 + expect(res.status()).toBe(403) 70 + }) 71 + 72 + test('allows direct ui access after opening authenticated url', async ({ page }) => { 73 + const cleanUrl = new URL(pageUrl) 74 + cleanUrl.search = '' 75 + const cleanPageUrl = cleanUrl.toString() 76 + 77 + await page.goto(pageUrl) 78 + await assertTestCounts(page, { pass: TEST_COUNTS.pass, fail: TEST_COUNTS.fail }) 79 + expect(page.url()).toBe(`${cleanPageUrl}#/`) 80 + 81 + await page.goto(cleanPageUrl) 82 + await assertTestCounts(page, { pass: TEST_COUNTS.pass, fail: TEST_COUNTS.fail }) 83 + expect(page.url()).toBe(`${cleanPageUrl}#/`) 34 84 }) 35 85 36 86 test('coverage', async ({ page }) => { ··· 192 242 await testModuleGraph(page) 193 243 }) 194 244 }) 195 - 196 - const TEST_COUNTS = { 197 - pass: 18, 198 - fail: 3, 199 - files: { 200 - pass: 7, 201 - }, 202 - } 203 245 204 246 async function testBasic(page: Page, pageUrl: string) { 205 247 const pageErrors: unknown[] = [] ··· 618 660 reporters: [], 619 661 }) 620 662 vitest = server.vitest 621 - pageUrl = `${server.url}/__vitest__/` 663 + pageUrl = server.url 622 664 }) 623 665 624 666 test.afterAll(async () => { ··· 661 703 reporters: [], 662 704 }) 663 705 vitest = server.vitest 664 - pageUrl = `${server.url}/__vitest__/` 706 + pageUrl = server.url 665 707 }) 666 708 667 709 test.afterAll(async () => {