import type { TestBenchmark } from 'vitest' import type { JsonTestResult, JsonTestResults, Vitest } from 'vitest/node' import { readdirSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { beforeAll, describe, expect, onTestFailed, test } from 'vitest' import { buildTestTree } from '../../test-utils' import { instances, provider, runBrowserTests } from './utils' function noop() {} describe('running browser tests', async () => { let stderr: string let stdout: string let browserResultJson: JsonTestResults let passedTests: JsonTestResult[] let failedTests: JsonTestResult[] let vitest: Vitest const events: string[] = [] const emittedBenchmarks: Array<{ projectName: string testName: string benchmark: TestBenchmark }> = [] beforeAll(async () => { ;({ stderr, stdout, ctx: vitest, } = await runBrowserTests({ allowOnly: true, reporters: [ { onBrowserInit(project) { events.push(`onBrowserInit ${project.name}`) }, onTestCaseBenchmark(testCase, benchmark) { emittedBenchmarks.push({ projectName: testCase.project.name || '', testName: testCase.fullName, benchmark, }) }, }, 'json', { onInit: noop, onTestRunStart: noop, onTestModuleCollected: noop, onTestRunEnd: noop, onTestRemoved: noop, onWatcherStart: noop, onWatcherRerun: noop, onServerRestart: noop, onUserConsoleLog: noop, }, 'default', ], })) const browserResult = await readFile('./browser.json', 'utf-8') browserResultJson = JSON.parse(browserResult) as JsonTestResults const getPassed = (results: JsonTestResult[]) => results.filter((result) => result.status === 'passed' && !result.message) const getFailed = (results: JsonTestResult[]) => results.filter((result) => result.status === 'failed') passedTests = getPassed(browserResultJson.testResults) failedTests = getFailed(browserResultJson.testResults) }) test('tests are actually running', () => { expect(stderr).toBe('') const testFiles = Array.from(new Set(browserResultJson.testResults.map((t) => t.name))) vitest.projects.forEach((project) => { // the order is non-deterministic expect(events).toContain(`onBrowserInit ${project.name}`) }) // test files are optimized automatically (type-check-only files are excluded) const runtimeTestFiles = testFiles.filter((f) => !f.endsWith('.test-d.ts')) expect(vitest.projects.map((p) => p.vite.config.optimizeDeps.entries)).toEqual( vitest.projects.map(() => expect.arrayContaining(runtimeTestFiles)), ) const testFilesCount = readdirSync('./test').filter( (n) => n.includes('.test.') || n.includes('.test-d.') || n.includes('.bench.'), ).length + 1 // 1 is in-source-test expect(browserResultJson.testResults).toHaveLength(testFilesCount * instances.length) expect(passedTests).toHaveLength(browserResultJson.testResults.length) expect(failedTests).toHaveLength(0) }) test('benchmarks run in a dedicated `(bench)` project per browser instance', () => { const benchProjects = vitest.projects.filter((p) => p.name.endsWith('(bench)')) expect(benchProjects.map((p) => p.name).sort()).toEqual( instances.map(({ browser }) => `${browser} (bench)`).sort(), ) }) test('perProject benchmarks emit tasks with the perProject flag in every browser', () => { const records = emittedBenchmarks.filter( (e) => e.testName === 'perProject registrations flow through the browser RPC (onTestBenchmark)', ) // the test calls `.run()` twice, so each browser produces 2 benchmark records expect(records.length, `perProject emitted: ${records.length}`).toBe(2 * instances.length) for (const record of records) { expect(record.benchmark.tasks, `empty tasks for ${record.projectName}`).toHaveLength(1) const [task] = record.benchmark.tasks expect(task.perProject, `missing perProject flag on ${record.projectName}/${task.name}`).toBe( true, ) expect(task.fromStore).toBeUndefined() } }) test('bench.compare emits one benchmark with both registrations ranked', () => { const records = emittedBenchmarks.filter( (e) => e.testName === 'bench.compare resolves a BenchStorage in the browser', ) expect(records.length).toBe(instances.length) for (const record of records) { expect( record.benchmark.tasks.map((t) => t.name).sort(), `unexpected tasks for ${record.projectName}`, ).toEqual(['a', 'b']) expect(record.benchmark.tasks.map((t) => t.rank).sort()).toEqual([1, 2]) } }) test('writeResult flows through the write-artifact RPC in every browser', () => { const records = emittedBenchmarks.filter( (e) => e.testName === 'writeResult exercises the writeBenchmarkResult RPC round-trip', ) expect(records.length).toBe(instances.length) for (const record of records) { expect(record.benchmark.tasks, `empty tasks for ${record.projectName}`).toHaveLength(1) const [task] = record.benchmark.tasks expect(task.name).toBe('with-write') } }) test('tags are collected', () => { expect(vitest.config.tags).toEqual([ { name: 'e2e', priority: 10 }, { name: 'test', priority: 5 }, { name: 'browser', priority: 1 }, ]) const testModule = vitest.state .getTestModules() .find((m) => m.moduleId.includes('tags.test.ts')) expect.assert(testModule) expect(buildTestTree([testModule], (t) => t.tags)).toMatchInlineSnapshot(` { "test/tags.test.ts": { "suite 1": { "suite 2": { "test 2": [ "browser", "e2e", ], }, "test 1": [ "browser", "test", ], }, }, } `) }) test('runs in-source tests', () => { expect(stdout).toContain('src/actions.ts') const actionsTest = passedTests.find((t) => t.name.includes('/actions.ts')) expect.assert(actionsTest) expect(actionsTest.assertionResults).toHaveLength(1) }) test('unsubscribes cancel listeners after run', async () => { // should not throw birpc closing errors await expect(vitest.cancelCurrentRun('keyboard-input')).resolves.not.toThrow() }) }) describe('console logging tests', async () => { let stderr: string let stdout: string beforeAll(async () => { ;({ stderr, stdout } = await runBrowserTests({ root: './fixtures/print-logs', // assert on Vitest's own console forwarding rather than Vite's client // relay, which does not run when the tester loads the stubbed @vite/client reporters: ['default'], })) }) test('logs are redirected to stdout', () => { expect(stdout).toContain('stdout | test/logs.test.ts > logging to stdout') expect(stdout).toContain('hello from console.log') expect(stdout).toContain('hello from console.info') expect(stdout).toContain('hello from console.debug') expect(stdout).toContain( ` { hello: 'from dir', } `.trim(), ) expect(stdout).toContain( ` { hello: 'from dirxml', } `.trim(), ) expect(stdout).toContain('dom
') expect(stdout).toContain('default: 1') expect(stdout).toContain('default: 2') expect(stdout).toContain('default: 3') expect(stdout).toContain('count: 1') expect(stdout).toContain('count: 2') expect(stdout).toContain('count: 3') expect(stdout).toMatch(/default: [\d.]+ ms/) expect(stdout).toMatch(/time: [\d.]+ ms/) expect(stdout).toMatch(/\[console-time-fake\]: [\d.]+ ms/) expect(stdout).not.toContain('[console-time-fake]: 0 ms') expect(stdout).toContain('hello from one') expect(stdout).toContain(`hello from two { hello: 'object', }`) }) test('logs are redirected to stderr', () => { expect(stderr).toContain('stderr | test/logs.test.ts > logging to stderr') expect(stderr).toContain('hello from console.error') expect(stderr).toContain('hello from console.warn') expect(stderr).toContain('Timer "invalid timeLog" does not exist') expect(stderr).toContain('Timer "invalid timeEnd" does not exist') // safari logs the stack files with @https://... expect(stderr).toMatch(/hello from console.trace\s+(\w+|@)/) }) test(`logs have stack traces`, () => { expect(stdout).toMatch( ` log with a stack ❯ test/logs.test.ts:58:11 `.trim(), ) expect(stderr).toMatch( ` error with a stack ❯ test/logs.test.ts:59:11 `.trim(), ) // console.trace processes the stack trace correctly expect(stderr).toMatch('test/logs.test.ts:60:11') }) test('popup apis should log a warning', () => { expect(stderr).toContain('Vitest encountered a `alert("test")`') expect(stderr).toContain('Vitest encountered a `confirm("test")`') expect(stderr).toContain('Vitest encountered a `print()`') expect(stderr).toContain('Vitest encountered a `prompt("test")`') }) }) test('disableConsoleIntercept', async () => { const result = await runBrowserTests({ root: './fixtures/print-logs', project: [instances[0].browser], disableConsoleIntercept: true, }) expect(result.stderr).toBe('') expect(result.stdout).not.toContain('logging to stdout') expect(result.stdout).not.toContain('hello from console.log') }) test(`stack trace points to correct file in every browser when failed`, async () => { expect.assertions(30) const { stderr } = await runBrowserTests({ root: './fixtures/failing', reporters: [ 'default', { onTestCaseReady(testCase) { if (testCase.name !== 'correctly fails and prints a diff') { return } if (testCase.project.name === 'chromium' || testCase.project.name === 'chrome') { expect(testCase.result().errors?.[0].stacks).toEqual([ { line: 11, column: 13, file: testCase.module.moduleId, method: '', }, ]) } }, }, ], }) expect(stderr).toContain('expected 1 to be 2') expect(stderr).toMatch(/- 2\s+\+ 1/) // expect(stderr).toContain('Expected to be') // expect(stderr).toContain('But got') expect(stderr).toContain('Failure screenshot') expect(stderr).toContain('.vitest/attachments/failure-screenshots/failing') expect(stderr).toContain('Access denied to "/inaccessible/path".') expect(stderr).toMatch(/failing.test.ts:11:13/) // column is 13 in safari, 9 in others expect(stderr).toMatch(/throwError src\/error.ts:8:(13|9)/) expect(stderr).toContain( 'The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.', ) expect(stderr).toMatch(/failing.test.ts:19:28/) expect(stderr).toMatch(/failing.test.ts:20:28/) expect(stderr).toMatch(/failing.test.ts:21:28/) expect(stderr).toMatch(/failing.test.ts:22:13/) expect(stderr).toMatch(/failing.test.ts:23:13/) expect(stderr).toMatch(/failing.test.ts:24:13/) expect(stderr).toMatch(/failing.test.ts:25:13/) expect(stderr).toMatch(/failing.test.ts:26:13/) expect(stderr).toMatch(/failing.test.ts:27:13/) expect(stderr).toMatch(/failing.test.ts:28:13/) expect(stderr).toMatch(/failing.test.ts:29:13/) expect(stderr).toMatch(/failing.test.ts:30:13/) expect(stderr).toMatch(/failing.test.ts:31:13/) expect(stderr).toMatch(/failing.test.ts:32:13/) expect(stderr).toMatch(/failing.test.ts:33:13/) expect(stderr).toMatch(/failing.test.ts:34:13/) expect(stderr).toMatch(/failing.test.ts:35:13/) expect(stderr).toMatch(/bundled-lib\/src\/b.js:2:(9|18)/) expect(stderr).toMatch(/bundled-lib\/src\/index.js:5:(16|17)/) // index() is called from a bundled file expect(stderr).toMatch(/failing.test.ts:39:3/) // "not awaited but with then/catch/finally" test should not produce warnings expect(stderr).not.toMatch(/failing.test.ts:4[3-8]/) }) test('user-event', async () => { const { stdout, stderr } = await runBrowserTests({ root: './fixtures/user-event', }) expect(stderr).toBe('') onTestFailed(() => console.error(stderr)) instances.forEach(({ browser }) => { expect(stdout).toReportPassedTest('cleanup-retry.test.ts', browser) expect(stdout).toReportPassedTest('cleanup1.test.ts', browser) expect(stdout).toReportPassedTest('cleanup2.test.ts', browser) expect(stdout).toReportPassedTest('keyboard.test.ts', browser) expect(stdout).toReportPassedTest('clipboard.test.ts', browser) }) }) test('timeout settings', async () => { const { stderr } = await runBrowserTests({ root: './fixtures/timeout', }) expect(stderr).toContain('Matcher did not succeed in time.') if (provider.name === 'playwright') { expect(stderr).toContain('locator.click: Timeout 500ms exceeded.') expect(stderr).toContain('locator.click: Timeout 345ms exceeded.') } }) test('viewport', async () => { const { stdout, stderr } = await runBrowserTests({ root: './fixtures/viewport', }) expect(stderr).toBe('') instances.forEach(({ browser }) => { expect(stdout).toReportPassedTest('basic.test.ts', browser) }) }) test("in-source tests don't run when the module is imported by the test", async () => { const { stderr, stdout } = await runBrowserTests({}, ['mocking.test.ts']) expect(stderr).toBe('') instances.forEach(({ browser }) => { expect(stdout).toReportPassedTest('test/mocking.test.ts', browser) }) // there is only one file with one test inside // if this stops working, it will report twice as much tests expect(stdout).toContain(`Test Files ${instances.length} passed`) expect(stdout).toContain(`Tests ${instances.length} passed`) }) test('in-source tests run correctly when filtered', async () => { const { stderr, stdout } = await runBrowserTests({}, ['actions.ts']) expect(stderr).toBe('') instances.forEach(({ browser }) => { expect(stdout).toReportPassedTest('src/actions.ts', browser) }) // there is only one file with one test inside expect(stdout).toContain(`Test Files ${instances.length} passed`) expect(stdout).toContain(`Tests ${instances.length} passed`) }) test('re-evaluate setupFiles on each test run even when isolate is false', async () => { const { exitCode, stderr, stdout } = await runBrowserTests({ root: './fixtures/isolate-and-setup-file', }) expect(stderr).toBe('') expect(exitCode).toBe(0) instances.forEach(({ browser }) => { expect(stdout).toReportPassedTest('a.test.ts', browser) expect(stdout).toReportPassedTest('b.test.ts', browser) }) }) test.runIf(provider.name === 'playwright')( 'timeout hooks', async ({ onTestFailed }) => { const { stderr } = await runBrowserTests({ root: './fixtures/timeout-hooks', }) onTestFailed(() => { console.error(stderr) }) const lines = stderr.split('\n') const timeoutErrorsIndexes: number[] = [] lines.forEach((line, index) => { if (line.includes('TimeoutError:')) { timeoutErrorsIndexes.push(index) } }) const snapshot = timeoutErrorsIndexes .map((index) => { return [ lines[index - 1], lines[index].replace(/Timeout \d+ms exceeded/, 'Timeout