Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/guide/browser/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ export default defineConfig({

::: info
Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`browser.api`](/config/browser/api) option.

The CLI does not print the Vite server URL automatically. You can press "b" to print the URL when running in watch mode.
:::

If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure.
Expand Down
11 changes: 8 additions & 3 deletions docs/guide/learn/setup-teardown.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,20 @@ test('items starts with 3 fruits', () => {
expect(items).toHaveLength(3)
})

test('can remove an item', () => {
items.pop()
expect(items).toHaveLength(2)
})

test('can add an item', () => {
items.push('date')
expect(items).toHaveLength(4)
// afterEach will reset items for the next test,
// so this mutation won't leak into other tests
// beforeEach reset the array to 3 items before this test ran,
// proving that mutations from the previous test do not leak.
})
```

Without these hooks, the second test's `push` would affect any test that runs after it, which is a classic source of flaky tests. The hooks guarantee clean state for every test.
Without these hooks, mutations like `pop` or `push` from earlier tests would affect subsequent ones, which is a classic source of flaky tests, while the hooks guarantee clean state for every test.

## One-Time Setup

Expand Down
6 changes: 6 additions & 0 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ $ cd subdir && vitest --config ../vitest.config.ts # [!code ++]

Assignments to properties on `globalThis` or `window` in `jsdom` and `happy-dom` environments are now propagated to the underlying DOM implementation. Mutable properties such as `innerWidth` can affect APIs implemented by the DOM environment, for example `happy-dom`'s `matchMedia`.

### Browser Orchestrator URL Requires a Session

Vitest no longer serves the browser orchestrator UI from a bare `/__vitest_test__/` URL. Browser runner URLs are now session-bound and must include the `sessionId` generated by Vitest, for example `/__vitest_test__/?sessionId=...`.

If you manually opened the browser preview by copying the Vite server URL or visiting `/__vitest_test__/` directly, use the URL opened or printed by Vitest instead.

## Migrating to Vitest 4.0 {#vitest-4}

::: warning Prerequisites
Expand Down
5 changes: 1 addition & 4 deletions packages/browser-preview/src/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { SelectorOptions } from 'vitest/browser'
import type { BrowserProvider, BrowserProviderOption, TestProject } from 'vitest/node'
import { nextTick } from 'node:process'
import { defineBrowserProvider } from '@vitest/browser'
import { resolve } from 'pathe'
import { distRoot } from './constants'
Expand Down Expand Up @@ -34,9 +33,6 @@ export class PreviewBrowserProvider implements BrowserProvider {
'You\'ve enabled headless mode for "preview" provider but it doesn\'t support it. Use "playwright" or "webdriverio" instead: https://vitest.dev/guide/browser/#configuration',
)
}
nextTick(() => {
project.vitest.logger.printBrowserBanner(project)
})
}

isOpen(): boolean {
Expand All @@ -49,6 +45,7 @@ export class PreviewBrowserProvider implements BrowserProvider {

async openPage(_sessionId: string, url: string): Promise<void> {
this.open = true
this.project.vitest.logger.log(`Browser runner started at ${url}\n`)
if (!this.project.browser) {
throw new Error('Browser is not initialized')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export class IframeOrchestrator {

private createTestIframe(iframeId: string) {
const iframe = document.createElement('iframe')
const src = `/?sessionId=${getBrowserState().sessionId}&iframeId=${iframeId}`
const src = `/?sessionId=${getBrowserState().sessionId}&iframeId=${encodeURIComponent(iframeId)}`
const config = getConfig()

iframe.setAttribute('loading', 'eager')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export function createOrchestratorMiddleware(parentServer: ParentBrowserProject)

res.write(html, 'utf-8')
res.end()
return
}
res.statusCode = 404
res.end('Not found')
}
}
21 changes: 5 additions & 16 deletions packages/browser/src/node/serverOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,14 @@ export async function resolveOrchestrator(
url: URL,
res: ServerResponse<IncomingMessage>,
): Promise<string | undefined> {
let sessionId = url.searchParams.get('sessionId')
// it's possible to open the page without a context
if (!sessionId) {
const contexts = [...globalServer.children].flatMap(p => [...p.state.orchestrators.keys()])
sessionId = contexts.at(-1) ?? 'none'
}

// it's ok to not have a session here, especially in the preview provider
// because the user could refresh the page which would remove the session id from the url

const session = globalServer.vitest._browserSessions.getSession(sessionId!)
const browserProject = (session?.project.browser as ProjectBrowser | undefined) || [...globalServer.children][0]

if (!browserProject) {
const sessionId = url.searchParams.get('sessionId')
const session = sessionId && globalServer.vitest._browserSessions.getSession(sessionId)
if (!session) {
return
}

// ignore unknown pages
if (sessionId && sessionId !== 'none' && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
const browserProject = session.project.browser as ProjectBrowser | undefined
if (!browserProject) {
return
}

Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/node/serverTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export async function resolveTester(
)
}

const sessionId = url.searchParams.get('sessionId') || 'none'
const session = globalServer.vitest._browserSessions.getSession(sessionId)
const sessionId = url.searchParams.get('sessionId')
const session = sessionId && globalServer.vitest._browserSessions.getSession(sessionId)

if (!session) {
res.statusCode = 400
Expand Down
23 changes: 0 additions & 23 deletions packages/vitest/src/node/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,29 +268,6 @@ export class Logger {
}
}

printBrowserBanner(project: TestProject): void {
if (!project.browser) {
return
}

const resolvedUrls = project.browser.vite.resolvedUrls
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
if (!origin) {
return
}

const output = project.isRootProject()
? ''
: formatProjectName(project)
const provider = project.browser.provider?.name
const providerString = provider === 'preview' ? '' : ` by ${c.reset(c.bold(provider))}`
this.log(
c.dim(
`${output}Browser runner started${providerString} ${c.dim('at')} ${c.blue(new URL('/__vitest_test__/', origin))}\n`,
),
)
}

printUnhandledErrors(errors: ReadonlyArray<unknown>): void {
const errorMessage = c.red(
c.bold(
Expand Down
9 changes: 0 additions & 9 deletions packages/vitest/src/node/stdin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const keys = [
['p', 'filter by a filename'],
['t', 'filter by a test name regex pattern'],
['w', 'filter by a project name'],
['b', 'start the browser server if not started yet'],
['q', 'quit'],
]
const cancelKeys = ['space', 'c', 'h', ...keys.map(key => key[0]).flat()]
Expand Down Expand Up @@ -151,14 +150,6 @@ export function registerConsoleShortcuts(
if (name === 'p') {
return inputFilePattern()
}
if (name === 'b') {
await ctx._initBrowserServers()
ctx.projects.forEach((project) => {
ctx.logger.log()
ctx.logger.printBrowserBanner(project)
})
return null
}
}

async function keypressHandler(str: string, key: any) {
Expand Down
5 changes: 5 additions & 0 deletions test/browser/fixtures/file-path-encoding/a+b.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { expect, test } from 'vitest'

test('runs a test from a file whose path contains a plus sign', () => {
expect(true).toBe(true)
})
10 changes: 10 additions & 0 deletions test/browser/fixtures/file-path-encoding/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
browser: {
enabled: true,
headless: true,
},
},
})
20 changes: 20 additions & 0 deletions test/browser/specs/file-path-encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expect, test } from 'vitest'
import { instances, provider, runBrowserTests } from './utils'

test('runs tests from files whose path contains a plus sign', async () => {
const { stderr, stdout, exitCode } = await runBrowserTests({
root: './fixtures/file-path-encoding',
reporters: ['verbose'],
browser: {
provider,
instances,
},
})

expect(stderr).toBe('')
expect(exitCode).toBe(0)

instances.forEach(({ browser }) => {
expect(stdout).toReportPassedTest('a+b.test.ts', browser)
})
})
11 changes: 11 additions & 0 deletions test/ui/test/browser-preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { assertTestCounts } from './helper'

test.describe('orchestrator UI on preview provider', () => {
test('basic', async ({ page }) => {
let previewUrl: string | undefined
globalThis.__hackOpenBrowser = async (url: string) => {
previewUrl = url
await page.goto(url)
}
const vitest = await startVitest(
Expand All @@ -21,6 +23,15 @@ test.describe('orchestrator UI on preview provider', () => {
},
)

// valid `sessionId` is required for orchestrator UI
expect(new URL(previewUrl!).searchParams.get('sessionId')).toBeDefined()
const res1 = await page.request.get(new URL('/__vitest_test__/', previewUrl!).toString())
expect(res1.status()).toBe(404)
expect(await res1.text()).toBe('Not found')
const res2 = await page.request.get(new URL('/__vitest_test__/?sessionId=invalid', previewUrl!).toString())
expect(res2.status()).toBe(404)
expect(await res2.text()).toBe('Not found')

// results in dashboard
await assertTestCounts(page, { pass: 1, fail: 0 })

Expand Down
Loading