diff --git a/docs/config/attachmentsdir.md b/docs/config/attachmentsdir.md
index 14867d75fe4f..c81a38ca0e91 100644
--- a/docs/config/attachmentsdir.md
+++ b/docs/config/attachmentsdir.md
@@ -3,9 +3,11 @@ title: attachmentsDir | Config
outline: deep
---
-# attachmentsDir
+# attachmentsDir
- **Type:** `string`
- **Default:** `'.vitest/attachments'`
-Directory path for storing attachments created by [`context.annotate`](/guide/test-context#annotate) relative to the project root.
+Directory path for storing file attachments created by [`context.annotate`](/guide/test-context#annotate).
+
+This option is resolved relative to the root Vitest config. When using [`projects`](/guide/projects), all projects share the same `attachmentsDir`; it cannot be configured per project.
diff --git a/docs/guide/projects.md b/docs/guide/projects.md
index ae68f4076870..794a41ddd869 100644
--- a/docs/guide/projects.md
+++ b/docs/guide/projects.md
@@ -289,6 +289,7 @@ Some of the configuration options are not allowed in a project config. Most nota
- `coverage`: coverage is done for the whole process
- `reporters`: only root-level reporters can be supported
- `resolveSnapshotPath`: only root-level resolver is respected
+- `attachmentsDir`: attachments are stored in one root-level directory shared by all projects
- all other options that don't affect test runners
All configuration options that are not supported inside a project configuration are marked with a icon next to their name. They can only be defined once in the root config file.
diff --git a/eslint.config.js b/eslint.config.js
index 0578c31a81da..2ccb501c8045 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -14,6 +14,7 @@ export default antfu(
'**/assets/**',
'**/*.d.ts',
'**/*.timestamp-*',
+ '**/test-results',
'test/unit/src/self',
'test/unit/test/mocking/already-hoisted.test.ts',
'test/cache/cache/.vitest-base/results.json',
diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore
index ef785f62a311..c59f4873a32f 100644
--- a/packages/ui/.gitignore
+++ b/packages/ui/.gitignore
@@ -1 +1,2 @@
__screenshots__
+html/
diff --git a/packages/ui/CONTRIBUTING.md b/packages/ui/CONTRIBUTING.md
index c22b3e26f064..544c905212ea 100644
--- a/packages/ui/CONTRIBUTING.md
+++ b/packages/ui/CONTRIBUTING.md
@@ -38,7 +38,7 @@ Use this setup for developing Browser Mode UI features with Vite HMR. It serves
Start a browser-mode Vitest server:
```bash
-pnpm -C packages/ui test:ui --browser.headless --ui --open=false
+pnpm -C packages/ui test:ui --ui --open=false
```
Start the UI dev server in browser preview mode:
@@ -54,3 +54,18 @@ The UI dev server fetches browser runner state from the browser runner server on
```bash
BROWSER_DEV_PORT=63316 BROWSER_DEV=true pnpm -C packages/ui dev:client
```
+
+## HTML report
+
+Use this setup for developing static HTML report UI with Vite HMR.
+
+```bash
+HTML_REPORT_DIR= pnpm -C packages/ui dev:client
+```
+
+For example,
+
+```bash
+pnpm -C packages/ui test:ui --reporter=html --run
+HTML_REPORT_DIR="$PWD/packages/ui/html" pnpm -C packages/ui dev:client
+```
diff --git a/packages/ui/client/test.ts b/packages/ui/client/test.ts
index e38268090ddb..13815a19c578 100644
--- a/packages/ui/client/test.ts
+++ b/packages/ui/client/test.ts
@@ -1,15 +1,16 @@
import type { ComponentRenderOptions, RenderResult } from 'vitest-browser-vue'
import { vTooltip } from 'floating-vue'
+import { vi } from 'vitest'
import {
render as _render,
} from 'vitest-browser-vue'
export { page } from 'vitest/browser'
-export function render(
+export const render = vi.defineHelper((
component: C,
options?: ComponentRenderOptions,
-): PromiseLike> {
+): PromiseLike> => {
return _render(component, {
...options,
global: {
@@ -18,4 +19,4 @@ export function render(
},
},
})
-}
+})
diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts
index 8dda4f2b156f..7d96aa1405d4 100644
--- a/packages/ui/vite.config.ts
+++ b/packages/ui/vite.config.ts
@@ -1,15 +1,12 @@
import type { Plugin } from 'vite'
+import fs from 'node:fs'
+import path from 'node:path'
import Vue from '@vitejs/plugin-vue'
import { resolve } from 'pathe'
import { presetAttributify, presetIcons, presetWind3, transformerDirectives } from 'unocss'
import Unocss from 'unocss/vite'
import { defineConfig } from 'vite'
-// for debug:
-// open a static file serve to share the report json
-// and ui using the link to load the report json data
-// const debugLink = 'http://127.0.0.1:4173/__vitest__'
-
export default defineConfig({
base: './',
resolve: {
@@ -41,15 +38,9 @@ export default defineConfig({
],
safelist: 'absolute origin-top mt-[8px]'.split(' '),
}),
- devUiScriptPlugin(),
- // uncomment to see the HTML reporter preview
- // {
- // name: 'debug-html-report',
- // apply: 'serve',
- // transformIndexHtml(html) {
- // return html.replace('', ``)
- // },
- // },
+ process.env.HTML_REPORT_DIR
+ ? devHtmlReportPlugin({ htmlDir: process.env.HTML_REPORT_DIR })
+ : devUiScriptPlugin(),
{
// workaround `crossorigin` issues on some browsers
// https://github.com/vitejs/vite/issues/6648
@@ -119,3 +110,32 @@ function devUiScriptPlugin(): Plugin {
},
}
}
+
+function devHtmlReportPlugin({ htmlDir }: { htmlDir: string }): Plugin {
+ const REPORT_FILE = 'html.meta.json.gz'
+ return {
+ name: 'dev-html-report',
+ apply(_config, env) {
+ return !!htmlDir && env.command === 'serve' && env.mode !== 'test'
+ },
+ async transformIndexHtml() {
+ return [
+ {
+ tag: 'script',
+ children: `window.METADATA_PATH="${REPORT_FILE}"`,
+ },
+ ]
+ },
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ const url = new URL(req.url || '', `http://localhost`)
+ if (url.pathname === `/${REPORT_FILE}`) {
+ const data = fs.readFileSync(path.join(htmlDir, REPORT_FILE))
+ res.end(data)
+ return
+ }
+ next()
+ })
+ },
+ }
+}
diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts
index 7a7a6562e9bf..38ba88515e53 100644
--- a/packages/ui/vitest.config.ts
+++ b/packages/ui/vitest.config.ts
@@ -29,6 +29,7 @@ const testConfig = defineConfig({
browser: {
enabled: true,
traceView: true,
+ headless: true,
provider:
providerName === 'preview'
? preview()
diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts
index 69385e895ebf..3ff810a697ec 100644
--- a/packages/vitest/src/node/project.ts
+++ b/packages/vitest/src/node/project.ts
@@ -550,7 +550,9 @@ export class TestProject {
this.vitest,
{
...options,
+ // root-only configs
coverage: this.vitest.config.coverage,
+ attachmentsDir: this.vitest.config.attachmentsDir,
},
server.config,
)
diff --git a/test/e2e/test/annotations.test.ts b/test/e2e/test/annotations.test.ts
index 34103eb3d2b3..2ae2ee6f67e5 100644
--- a/test/e2e/test/annotations.test.ts
+++ b/test/e2e/test/annotations.test.ts
@@ -1,5 +1,7 @@
import type { TestArtifact } from '@vitest/runner'
import type { TestAnnotation } from 'vitest'
+import { readdirSync } from 'node:fs'
+import path from 'node:path'
import { playwright } from '@vitest/browser-playwright'
import { describe, expect, test } from 'vitest'
import { runInlineTests } from '../../test-utils'
@@ -665,3 +667,52 @@ describe('reporters', () => {
})
})
})
+
+test('attachmentsDir is root only', async () => {
+ const result = await runInlineTests(
+ {
+ 'packages/client/basic.test.ts': `
+import { test } from 'vitest'
+test("hello", ({ annotate }) => {
+ annotate("hello annotation", { path: "./hello.txt" })
+})
+`,
+ 'packages/client/hello.txt': `HELLO`,
+ 'packages/server/basic.test.ts': `
+import { test } from 'vitest'
+test("world", ({ annotate }) => {
+ annotate("world annotation", { path: "./world.txt" })
+})
+`,
+ 'packages/server/world.txt': `WORLD`,
+ },
+ {
+ projects: ['./packages/*'],
+ },
+ )
+ expect(result.stderr).toMatchInlineSnapshot(`""`)
+ expect(result.errorTree({ project: true })).toMatchInlineSnapshot(`
+ {
+ "client": {
+ "basic.test.ts": {
+ "hello": "passed",
+ },
+ },
+ "server": {
+ "basic.test.ts": {
+ "world": "passed",
+ },
+ },
+ }
+ `)
+ const files = readdirSync(path.join(result.root, '.vitest/attachments'))
+ const contents = files.sort().map(file =>
+ result.fs.readFile(path.join('.vitest/attachments', file)),
+ )
+ expect(contents).toMatchInlineSnapshot(`
+ [
+ "HELLO",
+ "WORLD",
+ ]
+ `)
+})
diff --git a/test/e2e/test/reporters/junit.test.ts b/test/e2e/test/reporters/junit.test.ts
index 03ea6d1d3fb7..0295667d9963 100644
--- a/test/e2e/test/reporters/junit.test.ts
+++ b/test/e2e/test/reporters/junit.test.ts
@@ -163,13 +163,17 @@ test.each([true, false])('addFileAttribute %s', async (t) => {
})
test('many errors without warning', async () => {
- const { stderr } = await runVitestCli(
+ const result = await runVitestCli(
'run',
'--reporter=junit',
'--root',
- resolve(import.meta.dirname, '../fixtures/reporters/many-errors'),
+ resolve(import.meta.dirname, '../../fixtures/reporters/many-errors'),
)
- expect(stderr).not.toContain('MaxListenersExceededWarning')
+ expect(stabilizeReport(result.stdout).split('\n')[1]).toMatchInlineSnapshot(
+ `""`,
+ )
+ expect(result.stderr).not.toContain('MaxListenersExceededWarning')
+ expect(result.exitCode).not.toBe(0)
})
test('CLI reporter option preserves config file options', async () => {
diff --git a/test/ui/README.md b/test/ui/README.md
index 36d7af9d76bc..d7f9bec6a375 100644
--- a/test/ui/README.md
+++ b/test/ui/README.md
@@ -1,10 +1,11 @@
# test/ui
```sh
-# run e2e
+# run e2e on playwright
pnpm test
+pnpm test --ui
# run fixture projects
-pnpm test-fixtures --ui
-pnpm test-fixtures --root fixtures-trace
+pnpm test-fixtures --root fixtures/main --ui
+pnpm test-fixtures --root fixtures/trace
```
diff --git a/test/ui/fixtures-browser/visual-regression.test.ts b/test/ui/fixtures-browser/visual-regression.test.ts
deleted file mode 100644
index b517d7bb0600..000000000000
--- a/test/ui/fixtures-browser/visual-regression.test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { test } from 'vitest'
-import { server } from 'vitest/browser'
-
-test('visual regression test', async ({ expect, onTestFinished }) => {
- const screenshotName = 'visual-regression-screenshot.png'
-
- onTestFinished(async () => {
- if (server.config.snapshotOptions.updateSnapshot !== 'none') {
- await server.commands.removeFile(`fixtures-browser/${screenshotName}`)
- }
- })
-
- await expect(expect(document.body).toMatchScreenshot(screenshotName)).rejects.toThrow(
- 'No existing reference screenshot found',
- )
-})
diff --git a/test/ui/fixtures/main/browser/visual-regression.test.ts b/test/ui/fixtures/main/browser/visual-regression.test.ts
new file mode 100644
index 000000000000..b9acf20f1102
--- /dev/null
+++ b/test/ui/fixtures/main/browser/visual-regression.test.ts
@@ -0,0 +1,10 @@
+import { test } from 'vitest'
+import { server } from 'vitest/browser'
+
+test('visual regression test', async ({ expect }) => {
+ // reset screenshots to ensure consistent assertion results with new screenshot
+ await (server.commands as any).rm(`__screenshots__`)
+ await expect(expect(document.body).toMatchScreenshot()).rejects.toThrow(
+ 'No existing reference screenshot found',
+ )
+})
diff --git a/test/ui/fixtures/annotated.test.ts b/test/ui/fixtures/main/node/annotated.test.ts
similarity index 92%
rename from test/ui/fixtures/annotated.test.ts
rename to test/ui/fixtures/main/node/annotated.test.ts
index b040c871e766..e1eca2f07c84 100644
--- a/test/ui/fixtures/annotated.test.ts
+++ b/test/ui/fixtures/main/node/annotated.test.ts
@@ -11,13 +11,13 @@ test('annotated typed test', async ({ annotate }) => {
test('annotated file test', async ({ annotate }) => {
await annotate('file annotation', {
- path: './fixtures/example.txt'
+ path: './example.txt'
})
})
test('annotated image test', async ({ annotate }) => {
await annotate('image annotation', {
- path: './fixtures/cute-puppy.jpg'
+ path: './cute-puppy.jpg'
})
})
diff --git a/test/ui/fixtures/console.test.ts b/test/ui/fixtures/main/node/console.test.ts
similarity index 100%
rename from test/ui/fixtures/console.test.ts
rename to test/ui/fixtures/main/node/console.test.ts
diff --git a/test/ui/fixtures/coverage.test.ts b/test/ui/fixtures/main/node/coverage.test.ts
similarity index 100%
rename from test/ui/fixtures/coverage.test.ts
rename to test/ui/fixtures/main/node/coverage.test.ts
diff --git a/test/ui/fixtures/coverage.ts b/test/ui/fixtures/main/node/coverage.ts
similarity index 100%
rename from test/ui/fixtures/coverage.ts
rename to test/ui/fixtures/main/node/coverage.ts
diff --git a/test/ui/fixtures/cute-puppy.jpg b/test/ui/fixtures/main/node/cute-puppy.jpg
similarity index 100%
rename from test/ui/fixtures/cute-puppy.jpg
rename to test/ui/fixtures/main/node/cute-puppy.jpg
diff --git a/test/ui/fixtures/error.test.ts b/test/ui/fixtures/main/node/error.test.ts
similarity index 100%
rename from test/ui/fixtures/error.test.ts
rename to test/ui/fixtures/main/node/error.test.ts
diff --git a/test/ui/fixtures/example.txt b/test/ui/fixtures/main/node/example.txt
similarity index 100%
rename from test/ui/fixtures/example.txt
rename to test/ui/fixtures/main/node/example.txt
diff --git a/test/ui/fixtures/sample.test.ts b/test/ui/fixtures/main/node/sample.test.ts
similarity index 100%
rename from test/ui/fixtures/sample.test.ts
rename to test/ui/fixtures/main/node/sample.test.ts
diff --git a/test/ui/fixtures/snapshot.test.ts b/test/ui/fixtures/main/node/snapshot.test.ts
similarity index 100%
rename from test/ui/fixtures/snapshot.test.ts
rename to test/ui/fixtures/main/node/snapshot.test.ts
diff --git a/test/ui/fixtures/task-name.test.ts b/test/ui/fixtures/main/node/task-name.test.ts
similarity index 100%
rename from test/ui/fixtures/task-name.test.ts
rename to test/ui/fixtures/main/node/task-name.test.ts
diff --git a/test/ui/fixtures/main/vitest.config.ts b/test/ui/fixtures/main/vitest.config.ts
new file mode 100644
index 000000000000..31536b2f774a
--- /dev/null
+++ b/test/ui/fixtures/main/vitest.config.ts
@@ -0,0 +1,49 @@
+import path, { resolve } from "node:path";
+import { playwright } from "@vitest/browser-playwright";
+import { defineConfig } from "vitest/config";
+import type { BrowserCommand } from "vitest/node";
+import fs from "node:fs"
+
+const rmCommand: BrowserCommand<[filepath: string]> = async (ctx, filePath) => {
+ const resolved = resolve(ctx.project.config.root, filePath)
+ fs.rmSync(resolved, { recursive: true, force: true })
+}
+
+export default defineConfig({
+ test: {
+ coverage: {
+ reportOnFailure: true,
+ },
+ tags: [{ name: "db" }, { name: "flaky" }],
+ projects: [
+ {
+ extends: true,
+ test: {
+ name: "node",
+ root: path.join(import.meta.dirname, "node"),
+ // TODO: https://github.com/vitest-dev/vitest/issues/10326
+ attachmentsDir: path.join(import.meta.dirname, ".vitest/attachments"),
+ environment: "happy-dom",
+ },
+ },
+ {
+ extends: true,
+ test: {
+ name: "browser",
+ root: path.join(import.meta.dirname, "browser"),
+ attachmentsDir: path.join(import.meta.dirname, ".vitest/attachments"),
+ browser: {
+ enabled: true,
+ headless: true,
+ provider: playwright(),
+ instances: [{ browser: "chromium" }],
+ screenshotFailures: false,
+ commands: {
+ rm: rmCommand,
+ }
+ },
+ },
+ },
+ ],
+ },
+});
diff --git a/test/ui/fixtures-trace-stream/basic.test.ts b/test/ui/fixtures/trace-stream/basic.test.ts
similarity index 100%
rename from test/ui/fixtures-trace-stream/basic.test.ts
rename to test/ui/fixtures/trace-stream/basic.test.ts
diff --git a/test/ui/fixtures-trace-stream/helper.ts b/test/ui/fixtures/trace-stream/helper.ts
similarity index 100%
rename from test/ui/fixtures-trace-stream/helper.ts
rename to test/ui/fixtures/trace-stream/helper.ts
diff --git a/test/ui/fixtures-trace-stream/range.test.ts b/test/ui/fixtures/trace-stream/range.test.ts
similarity index 100%
rename from test/ui/fixtures-trace-stream/range.test.ts
rename to test/ui/fixtures/trace-stream/range.test.ts
diff --git a/test/ui/fixtures-trace-stream/vitest.config.ts b/test/ui/fixtures/trace-stream/vitest.config.ts
similarity index 100%
rename from test/ui/fixtures-trace-stream/vitest.config.ts
rename to test/ui/fixtures/trace-stream/vitest.config.ts
diff --git a/test/ui/fixtures-trace/assets/trace-pixel.svg b/test/ui/fixtures/trace/assets/trace-pixel.svg
similarity index 100%
rename from test/ui/fixtures-trace/assets/trace-pixel.svg
rename to test/ui/fixtures/trace/assets/trace-pixel.svg
diff --git a/test/ui/fixtures-trace/assets/trace-style.css b/test/ui/fixtures/trace/assets/trace-style.css
similarity index 100%
rename from test/ui/fixtures-trace/assets/trace-style.css
rename to test/ui/fixtures/trace/assets/trace-style.css
diff --git a/test/ui/fixtures-trace/basic.test.ts b/test/ui/fixtures/trace/basic.test.ts
similarity index 100%
rename from test/ui/fixtures-trace/basic.test.ts
rename to test/ui/fixtures/trace/basic.test.ts
diff --git a/test/ui/fixtures-trace/retry.test.ts b/test/ui/fixtures/trace/retry.test.ts
similarity index 100%
rename from test/ui/fixtures-trace/retry.test.ts
rename to test/ui/fixtures/trace/retry.test.ts
diff --git a/test/ui/fixtures-trace/scroll.test.ts b/test/ui/fixtures/trace/scroll.test.ts
similarity index 100%
rename from test/ui/fixtures-trace/scroll.test.ts
rename to test/ui/fixtures/trace/scroll.test.ts
diff --git a/test/ui/fixtures-trace/viewport.test.ts b/test/ui/fixtures/trace/viewport.test.ts
similarity index 100%
rename from test/ui/fixtures-trace/viewport.test.ts
rename to test/ui/fixtures/trace/viewport.test.ts
diff --git a/test/ui/fixtures-trace/vitest.config.ts b/test/ui/fixtures/trace/vitest.config.ts
similarity index 100%
rename from test/ui/fixtures-trace/vitest.config.ts
rename to test/ui/fixtures/trace/vitest.config.ts
diff --git a/test/ui/package.json b/test/ui/package.json
index 4688ad3a9862..6e9a5f78b92b 100644
--- a/test/ui/package.json
+++ b/test/ui/package.json
@@ -3,9 +3,7 @@
"type": "module",
"private": true,
"scripts": {
- "test": "GITHUB_ACTIONS=false playwright test",
- "test-e2e": "GITHUB_ACTIONS=false playwright test",
- "test-e2e-ui": "GITHUB_ACTIONS=false playwright test --ui",
+ "test": "playwright test",
"test-fixtures": "vitest"
},
"devDependencies": {
diff --git a/test/ui/test/helper.ts b/test/ui/test/helper.ts
new file mode 100644
index 000000000000..46efcab661ce
--- /dev/null
+++ b/test/ui/test/helper.ts
@@ -0,0 +1,95 @@
+import type { Page } from '@playwright/test'
+import type { InlineConfig, PreviewServer } from 'vite'
+import type { CliOptions, Vitest } from 'vitest/node'
+import assert from 'node:assert'
+import { readFileSync } from 'node:fs'
+import { Writable } from 'node:stream'
+import { expect } from '@playwright/test'
+import { preview } from 'vite'
+import { startVitest } from 'vitest/node'
+
+export async function startVitestUi(
+ cliOptions: CliOptions,
+ viteOverrides: InlineConfig = {},
+): Promise<{ vitest: Vitest; url: string }> {
+ // silence Vitest logs
+ const stdout = new Writable({ write: (_, __, callback) => callback() })
+ const stderr = new Writable({ write: (_, __, callback) => callback() })
+ const vitest = await startVitest('test', undefined, cliOptions, viteOverrides, { stdout, stderr })
+
+ const address = vitest.vite.httpServer?.address()
+ assert(address && typeof address === 'object', 'Invalid server address')
+
+ return {
+ vitest,
+ url: `http://localhost:${address.port}`,
+ }
+}
+
+export async function startHtmlReportPreview(
+ cliOptions: CliOptions,
+ previewOptions: InlineConfig,
+): Promise<{ previewServer: PreviewServer; url: string }> {
+ const stdout = new Writable({ write: (_, __, callback) => callback() })
+ const stderr = new Writable({ write: (_, __, callback) => callback() })
+ await startVitest('test', undefined, cliOptions, {}, { stdout, stderr })
+
+ const previewServer = await preview(previewOptions)
+ const address = previewServer.httpServer?.address()
+ assert(address && typeof address === 'object', 'Invalid server address')
+
+ return {
+ previewServer,
+ url: `http://localhost:${address.port}`,
+ }
+}
+
+export async function assertTestCounts(page: Page, { pass, fail }: { pass: number; fail: number }) {
+ await expect
+ .soft(page.getByTestId('tests-entry'))
+ .toContainText(
+ `${pass} Pass ${fail} Fail ${pass + fail} Total`,
+ )
+}
+
+export function getExplorerItem(page: Page, name: string) {
+ return page.getByTestId('explorer-item').and(page.getByLabel(name, { exact: true }))
+}
+
+export async function openExplorerItem(page: Page, name: string) {
+ await getExplorerItem(page, name).click()
+}
+
+export async function openExplorerFileItem(page: Page, name: string) {
+ const item = getExplorerItem(page, name)
+ await item.hover()
+ await item.getByTestId('btn-open-details').click()
+}
+
+export async function assertDownloadAttachment(
+ page: Page,
+ options: {
+ name: string
+ suggestedFilename: string
+ content: string
+ },
+) {
+ const annotation = page.getByRole('note').filter({ hasText: options.name })
+ const downloadPromise = page.waitForEvent('download')
+ await annotation.getByRole('link').click()
+ const download = await downloadPromise
+ expect(download.suggestedFilename()).toBe(options.suggestedFilename)
+ const downloadPath = await download.path()
+ expect(readFileSync(downloadPath, 'utf-8')).toBe(options.content)
+}
+
+export async function assertImageAttachment(
+ page: Page,
+ options: {
+ name: string
+ },
+) {
+ const annotation = page.getByRole('note').filter({ hasText: options.name })
+ await expect(annotation.getByRole('link')).toHaveAttribute('href', /.+/)
+ await expect(annotation.getByRole('img')).not.toHaveJSProperty('naturalWidth', 0)
+}
diff --git a/test/ui/test/html-report.spec.ts b/test/ui/test/html-report.spec.ts
deleted file mode 100644
index 628331afea84..000000000000
--- a/test/ui/test/html-report.spec.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-import type { Page } from '@playwright/test'
-import type { PreviewServer } from 'vite'
-import { readFileSync } from 'node:fs'
-import { Writable } from 'node:stream'
-import { expect, test } from '@playwright/test'
-import { preview } from 'vite'
-import { startVitest } from 'vitest/node'
-
-const port = 9001
-const pageUrl = `http://localhost:${port}/custom/base/`
-
-test.describe('html report', () => {
- let previewServer: PreviewServer
-
- test.beforeAll(async () => {
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
- // generate vitest html report
- await startVitest(
- 'test',
- [],
- {
- run: true,
- reporters: 'html',
- coverage: {
- enabled: true,
- },
- },
- {},
- {
- stdout,
- stderr,
- },
- )
-
- // run vite preview server
- previewServer = await preview({
- base: '/custom/base/',
- build: { outDir: 'html' },
- preview: { port, strictPort: true },
- })
- })
-
- test.afterAll(async () => {
- await previewServer?.close()
- })
-
- test('basic', async ({ page }) => {
- const pageErrors: unknown[] = []
- page.on('pageerror', error => pageErrors.push(error))
-
- await page.goto(pageUrl)
-
- // dashboard
- await assertTestCounts(page, { pass: 17, fail: 3 })
-
- // unhandled errors
- await expect(page.getByTestId('unhandled-errors')).toContainText(
- 'Vitest caught 2 errors during the test run. This might cause false positive tests. '
- + 'Resolve unhandled errors to make sure your tests are not affected.',
- )
-
- await expect(page.getByTestId('unhandled-errors-details')).toContainText('Error: error')
- await expect(page.getByTestId('unhandled-errors-details')).toContainText('Unknown Error: 1')
-
- // report
- const sample = page.getByTestId('results-panel').getByLabel('sample.test.ts')
- await sample.hover()
- await sample.getByTestId('btn-open-details').click({ force: true })
- await page.getByText('All tests passed in this file').click()
-
- // graph tab
- await page.getByTestId('btn-graph').click()
- await expect(page.locator('[data-testid=graph] text')).toContainText('sample.test.ts')
-
- // console tab
- await page.getByTestId('btn-console').click()
- await expect(page.getByTestId('console')).toContainText('log test')
-
- expect(pageErrors).toEqual([])
- })
-
- test('coverage', async ({ page }) => {
- await page.goto(pageUrl)
- await page.getByLabel('Show coverage').click()
- await page.frameLocator('#vitest-ui-coverage').getByRole('heading', { name: 'All files' }).click()
- })
-
- test('error', async ({ page }) => {
- await page.goto(pageUrl)
- const sample = page.getByTestId('results-panel').getByLabel('fixtures/error.test.ts')
- await sample.hover()
- await sample.getByTestId('btn-open-details').click({ force: true })
- await expect(page.getByTestId('diff')).toContainText('- Expected + Received + ')
- })
-
- test('annotations in the report tab', async ({ page }) => {
- await page.goto(pageUrl)
-
- await test.step('annotated test', async () => {
- const item = page.getByLabel('annotated test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotations = page.getByRole('note')
- await expect(annotations).toHaveCount(2)
-
- await expect(annotations.first()).toContainText('hello world')
- await expect(annotations.first()).toContainText('notice')
- await expect(annotations.first()).toContainText('fixtures/annotated.test.ts:4:9')
-
- await expect(annotations.last()).toContainText('second annotation')
- await expect(annotations.last()).toContainText('notice')
- await expect(annotations.last()).toContainText('fixtures/annotated.test.ts:5:9')
- })
-
- await test.step('annotated typed test', async () => {
- const item = page.getByLabel('annotated typed test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('beware!')
- await expect(annotation).toContainText('warning')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:9:9')
- })
-
- await test.step('annotated file test', async () => {
- const item = page.getByLabel('annotated file test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('file annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:13:9')
- await expect(annotation.getByRole('link')).toHaveAttribute('href', /data\/\w+/)
- })
-
- await test.step('annotated image test', async () => {
- const item = page.getByLabel('annotated image test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('image annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:19:9')
- await expect(annotation.getByRole('link')).toHaveAttribute('href', /data\/\w+/)
- const img = annotation.getByRole('img')
- await expect(img).toHaveAttribute('src', /data\/\w+/)
- await expect(img).not.toHaveJSProperty('naturalWidth', 0)
- })
-
- await test.step('annotated with body base64', async () => {
- const item = page.getByLabel('annotated with body base64')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('body base64 annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:25:9')
-
- const downloadPromise = page.waitForEvent('download')
- await annotation.getByRole('link').click()
- const download = await downloadPromise
- expect(download.suggestedFilename()).toBe('body-base64-annotation.md')
- const downloadPath = await download.path()
- const content = readFileSync(downloadPath, 'utf-8')
- expect(content).toBe('Hello base64 **markdown**')
- })
-
- await test.step('annotated with body utf-8', async () => {
- const item = page.getByLabel('annotated with body utf-8')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('body utf-8 annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:32:9')
-
- const downloadPromise = page.waitForEvent('download')
- await annotation.getByRole('link').click()
- const download = await downloadPromise
- expect(download.suggestedFilename()).toBe('body-utf-8-annotation.md')
- const downloadPath = await download.path()
- const content = readFileSync(downloadPath, 'utf-8')
- expect(content).toBe('Hello utf-8 **markdown**')
- })
- })
-
- test('annotations', async ({ page }) => {
- await page.goto(pageUrl)
- const item = page.getByLabel('fixtures/annotated.test.ts')
- await item.hover()
- await item.getByTestId('btn-open-details').click({ force: true })
- await page.getByTestId('btn-code').click({ force: true })
-
- const annotations = page.getByRole('note')
- await expect(annotations).toHaveCount(7)
-
- await expect(annotations.first()).toHaveText('notice: hello world')
- await expect(annotations.nth(1)).toHaveText('notice: second annotation')
- await expect(annotations.nth(2)).toHaveText('warning: beware!')
- await expect(annotations.nth(3)).toHaveText(/notice: file annotation/)
- await expect(annotations.nth(4)).toHaveText('notice: image annotation')
- await expect(annotations.nth(5)).toHaveText(/notice: body base64 annotation/)
- await expect(annotations.nth(6)).toHaveText(/notice: body utf-8 annotation/)
-
- await expect(annotations.nth(3).getByRole('link')).toHaveAttribute('href', /data\/\w+/)
- await expect(annotations.nth(4).getByRole('link')).toHaveAttribute('href', /data\/\w+/)
- await expect(annotations.nth(5).getByRole('link')).toHaveAttribute('href', /^data:text\/markdown;base64,/)
- await expect(annotations.nth(6).getByRole('link')).toHaveAttribute('href', /^data:text\/markdown,/)
- })
-
- test('tags filter', async ({ page }) => {
- await page.goto(pageUrl)
-
- await page.getByPlaceholder('Search...').fill('tag:db')
-
- // only one test with the tag "db"
- await expect(page.getByText('PASS (1)')).toBeVisible()
- await expect(page.getByTestId('explorer-item').filter({ hasText: 'has tags' })).toBeVisible()
-
- await page.getByPlaceholder('Search...').fill('tag:db && !flaky')
- await expect(page.getByText('No matched test')).toBeVisible()
-
- await page.getByPlaceholder('Search...').fill('tag:unknown')
- await expect(page.getByText('The tag pattern "unknown" is not defined in the configuration')).toBeVisible()
- })
-
- test('visual regression in the report tab', async ({ page }) => {
- await page.goto(pageUrl)
-
- await test.step('attachments get processed', async () => {
- const item = page.getByLabel('visual regression test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const artifact = page.getByRole('note')
- await expect(artifact).toHaveCount(1)
-
- await expect(artifact.getByRole('heading')).toContainText('Visual Regression')
- await expect(artifact).toContainText('fixtures-browser/visual-regression.test.ts:13:3')
- await expect(artifact.getByRole('tablist')).toHaveText('Reference')
- await expect(artifact.getByRole('tabpanel').getByRole('link')).toHaveAttribute('href', /data\/\w+\.png/)
- const vrImg = artifact.getByRole('tabpanel').getByRole('img')
- await expect(vrImg).toHaveAttribute('src', /data\/\w+\.png/)
- await expect(vrImg).not.toHaveJSProperty('naturalWidth', 0)
- })
- })
-})
-
-async function assertTestCounts(page: Page, options: { pass: number; fail: number }) {
- await expect.soft(page.getByTestId('tests-entry'))
- .toContainText(`${options.pass} Pass ${options.fail} Fail ${options.pass + options.fail} Total`)
-}
diff --git a/test/ui/test/trace-stream.spec.ts b/test/ui/test/trace-stream.spec.ts
index 855224ed00e9..061ba1ecdae6 100644
--- a/test/ui/test/trace-stream.spec.ts
+++ b/test/ui/test/trace-stream.spec.ts
@@ -1,32 +1,23 @@
-import type { Page } from '@playwright/test'
import type { Vitest } from 'vitest/node'
-import assert from 'node:assert'
import { mkdir, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
-import { Writable } from 'node:stream'
import { expect, test } from '@playwright/test'
import { resolve } from 'pathe'
-import { startVitest } from 'vitest/node'
+import { getExplorerItem, startVitestUi } from './helper'
test.describe('trace stream', () => {
let vitest: Vitest | undefined
let baseURL: string
- const root = path.join(import.meta.dirname, '../fixtures-trace-stream')
+ const root = path.join(import.meta.dirname, '../fixtures/trace-stream')
const gatesDir = path.join(root, 'node_modules/.vitest-e2e')
test.beforeAll(async () => {
await rm(gatesDir, { recursive: true, force: true })
await mkdir(gatesDir, { recursive: true })
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
-
// start standalone to hold off running tests
- vitest = await startVitest(
- 'test',
- undefined,
+ const server = await startVitestUi(
{
root,
watch: true,
@@ -39,11 +30,9 @@ test.describe('trace stream', () => {
'import.meta.env.TEST_GATE_FILE': 'true',
},
},
- { stdout, stderr },
)
- const address = vitest.vite.httpServer?.address()
- assert(address && typeof address === 'object', 'Invalid server address')
- baseURL = `http://localhost:${address.port}/__vitest__/`
+ vitest = server.vitest
+ baseURL = `${server.url}/__vitest__/`
})
test.afterAll(async () => {
@@ -170,7 +159,3 @@ test.describe('trace stream', () => {
])
})
})
-
-function getExplorerItem(page: Page, name: string) {
- return page.getByTestId('explorer-item').and(page.getByLabel(name, { exact: true }))
-}
diff --git a/test/ui/test/trace.spec.ts b/test/ui/test/trace.spec.ts
index 3df5e898d770..0db1a0872f7b 100644
--- a/test/ui/test/trace.spec.ts
+++ b/test/ui/test/trace.spec.ts
@@ -1,35 +1,23 @@
import type { Page } from '@playwright/test'
import type { PreviewServer } from 'vite'
import type { Vitest } from 'vitest/node'
-import assert from 'node:assert'
-import { Writable } from 'node:stream'
import { expect, test } from '@playwright/test'
-import { preview } from 'vite'
-import { startVitest } from 'vitest/node'
+import { assertTestCounts, openExplorerItem, startHtmlReportPreview, startVitestUi } from './helper'
test.describe('ui', () => {
let vitest: Vitest | undefined
let baseURL: string
test.beforeAll(async () => {
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
- vitest = await startVitest(
- 'test',
- undefined,
- {
- root: './fixtures-trace',
- watch: true,
- ui: true,
- open: false,
- },
- {},
- { stdout, stderr },
- )
- const address = vitest.vite.httpServer?.address()
- assert(address && typeof address === 'object', 'Invalid server address')
- baseURL = `http://localhost:${address.port}/__vitest__/`
+ const root = './fixtures/trace'
+ const server = await startVitestUi({
+ root,
+ watch: true,
+ ui: true,
+ open: false,
+ })
+ vitest = server.vitest
+ baseURL = `${server.url}/__vitest__/`
})
test.afterAll(async () => {
@@ -38,7 +26,7 @@ test.describe('ui', () => {
test.beforeEach(async ({ page }) => {
await page.goto(baseURL)
- await testReady(page)
+ await assertTestCounts(page, { pass: 11, fail: 0 })
})
test('basic', async ({ page }) => {
@@ -75,14 +63,10 @@ test.describe('html reporter', () => {
let baseURL: string
test.beforeAll(async () => {
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
- await startVitest(
- 'test',
- undefined,
+ const root = './fixtures/trace'
+ const server = await startHtmlReportPreview(
{
- root: './fixtures-trace',
+ root,
run: true,
ui: false,
reporters: 'html',
@@ -93,16 +77,13 @@ test.describe('html reporter', () => {
},
},
},
- {},
- { stdout, stderr },
+ {
+ root,
+ build: { outDir: 'html' },
+ },
)
- previewServer = await preview({
- root: './fixtures-trace',
- build: { outDir: 'html' },
- })
- const address = previewServer.httpServer?.address()
- assert(address && typeof address === 'object', 'Invalid server address')
- baseURL = `http://localhost:${address.port}/`
+ previewServer = server.previewServer
+ baseURL = `${server.url}/`
})
test.afterAll(async () => {
@@ -111,7 +92,7 @@ test.describe('html reporter', () => {
test.beforeEach(async ({ page }) => {
await page.goto(baseURL)
- await testReady(page)
+ await assertTestCounts(page, { pass: 11, fail: 0 })
})
test('basic', async ({ page }) => {
@@ -144,16 +125,6 @@ test.describe('html reporter', () => {
})
})
-async function testReady(page: Page) {
- const count = 11
- await expect.soft(page.getByTestId('tests-entry'))
- .toContainText(`${count} Pass 0 Fail ${count} Total`)
-}
-
-async function openExplorerItem(page: Page, name: string) {
- await page.getByTestId('explorer-item').and(page.getByLabel(name, { exact: true })).click()
-}
-
async function testBasic(page: Page) {
// selecting test case opens trace viewer
const traceView = page.getByTestId('trace-view')
diff --git a/test/ui/test/ui-security.spec.ts b/test/ui/test/ui-security.spec.ts
deleted file mode 100644
index c6136ee0e3c1..000000000000
--- a/test/ui/test/ui-security.spec.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { Vitest } from 'vitest/node'
-import { Writable } from 'node:stream'
-import { expect, test } from '@playwright/test'
-import { startVitest } from 'vitest/node'
-
-const port = 9002
-const pageUrl = `http://localhost:${port}/__vitest__/`
-
-test.describe('ui', () => {
- let vitest: Vitest | undefined
-
- test.beforeAll(async () => {
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
- vitest = await startVitest('test', [], {
- watch: true,
- ui: true,
- open: false,
- api: {
- port,
- allowExec: false,
- allowWrite: false,
- },
- reporters: [],
- }, {}, {
- stdout,
- stderr,
- })
- expect(vitest).toBeDefined()
- })
-
- test.afterAll(async () => {
- await vitest?.close()
- })
-
- test('cannot execute files from the ui', async ({ page }) => {
- await page.goto(pageUrl)
-
- await expect(page.getByTestId('btn-run-all')).toBeDisabled()
-
- const item = page.getByTestId('explorer-item').nth(0)
- await item.hover()
- await expect(item.getByTestId('btn-run-test')).toBeDisabled()
-
- await page.getByPlaceholder('Search...').fill('snapshot')
-
- const snapshotItem = page.getByTestId('explorer-item').filter({ hasText: 'snapshot.test.ts' })
- await snapshotItem.hover()
- await expect(snapshotItem.getByTestId('btn-fix-snapshot')).not.toBeVisible()
- })
-
- test('cannot write files', async ({ page }) => {
- await page.goto(pageUrl)
-
- const item = page.getByTestId('explorer-item').nth(0)
- await item.hover()
- await item.getByTestId('btn-open-details').click()
-
- await page.getByText('Code').click()
-
- const editor = page.getByTestId('btn-code')
- await expect(editor).toBeVisible()
-
- await editor.click()
- await page.keyboard.type('\n// some comment')
-
- await expect(editor).not.toContainText('// some comment')
- })
-})
diff --git a/test/ui/test/ui.spec.ts b/test/ui/test/ui.spec.ts
index 3d87d9024ca9..46c9a82fca82 100644
--- a/test/ui/test/ui.spec.ts
+++ b/test/ui/test/ui.spec.ts
@@ -1,412 +1,578 @@
import type { Page } from '@playwright/test'
+import type { PreviewServer } from 'vite'
import type { Vitest } from 'vitest/node'
-import { readFileSync } from 'node:fs'
-import { Writable } from 'node:stream'
import { expect, test } from '@playwright/test'
-import { startVitest } from 'vitest/node'
-
-const port = 9000
-const pageUrl = `http://localhost:${port}/__vitest__/`
+import { assertDownloadAttachment, assertImageAttachment, assertTestCounts, getExplorerItem, openExplorerFileItem, startHtmlReportPreview, startVitestUi } from './helper'
test.describe('ui', () => {
let vitest: Vitest | undefined
+ let pageUrl: string
test.beforeAll(async () => {
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
- vitest = await startVitest('test', [], {
+ const server = await startVitestUi({
+ root: './fixtures/main',
watch: true,
ui: true,
open: false,
- api: { port },
coverage: { enabled: true },
reporters: [],
- }, {}, {
- stdout,
- stderr,
})
- expect(vitest).toBeDefined()
+ vitest = server.vitest
+ pageUrl = `${server.url}/__vitest__/`
})
test.afterAll(async () => {
await vitest?.close()
})
- test('security', async ({ page }, testInfo) => {
- const response = await page.goto('https://example.com/', { timeout: 5000 }).catch(() => null)
-
- testInfo.skip(!response, 'External resource is not available')
-
- // request html
- const htmlResult = await page.evaluate(async (pageUrl) => {
- try {
- const res = await fetch(pageUrl)
- return res.status
- }
- catch (e) {
- return e instanceof Error ? e.message : e
- }
- }, pageUrl)
- expect(htmlResult).toBe('Failed to fetch')
-
- // request websocket
- const wsResult = await page.evaluate(async (pageUrl) => {
- const ws = new WebSocket(new URL('/__vitest_api__', pageUrl))
- return new Promise((resolve) => {
- ws.addEventListener('open', () => {
- resolve('open')
- })
- ws.addEventListener('error', () => {
- resolve('error')
- })
- })
- }, pageUrl)
- expect(wsResult).toBe('error')
+ test('basic', async ({ page }) => {
+ await testBasic(page, pageUrl)
})
- test('basic', async ({ page }) => {
- const pageErrors: unknown[] = []
- page.on('pageerror', error => pageErrors.push(error))
+ test('cross origin access', async ({ page }) => {
+ await testCrossOriginAccess(page, pageUrl)
+ })
+ test('coverage', async ({ page }) => {
await page.goto(pageUrl)
+ await testCoverage(page)
+ })
- // dashboard
- await assertTestCounts(page, { pass: 17, fail: 3 })
+ test('console', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testConsole(page)
+ })
- // unhandled errors
- await expect(page.getByTestId('unhandled-errors')).toContainText(
- 'Vitest caught 2 errors during the test run. This might cause false positive tests. '
- + 'Resolve unhandled errors to make sure your tests are not affected.',
- )
+ test('error', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testError(page)
+ })
+
+ test('filter', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testFilter(page, { mode: 'ui' })
+ })
+
+ test('tags filter', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testTagsFilter(page)
+ })
+
+ test('dashboard entries filter tests correctly', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testDashboardFilter(page)
+ })
+
+ test('annotations in the report tab', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testAnnotationsInReport(page)
+ })
+
+ test('annotations in the editor tab', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testAnnotationsInCode(page)
+ })
- await expect(page.getByTestId('unhandled-errors-details')).toContainText('Error: error')
- await expect(page.getByTestId('unhandled-errors-details')).toContainText('Unknown Error: 1')
+ test('visual regression in the report tab', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testVisualRegression(page)
+ })
- // report
- const sample = page.getByTestId('results-panel').getByLabel('sample.test.ts')
- await sample.hover()
- await sample.getByTestId('btn-open-details').click({ force: true })
- await page.getByText('All tests passed in this file').click()
+ test('can edit file', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testWriteFile(page, { enabled: true })
+ })
- // graph tab
- await page.getByTestId('btn-graph').click()
- await expect(page.locator('[data-testid=graph] text')).toContainText('sample.test.ts')
+ test('can execute', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testExecute(page, { mode: 'ui' })
+ })
+})
- // console tab
- await page.getByTestId('btn-console').click()
- await expect(page.getByTestId('console')).toContainText('log test')
+test.describe('html report', () => {
+ let previewServer: PreviewServer
+ let pageUrl: string
- expect(pageErrors).toEqual([])
+ test.beforeAll(async () => {
+ const server = await startHtmlReportPreview(
+ {
+ root: './fixtures/main',
+ run: true,
+ reporters: 'html',
+ coverage: {
+ enabled: true,
+ },
+ },
+ {
+ root: './fixtures/main',
+ base: '/custom/base/',
+ build: { outDir: 'html' },
+ },
+ )
+ previewServer = server.previewServer
+ pageUrl = `${server.url}/custom/base/`
+ })
+
+ test.afterAll(async () => {
+ await previewServer?.close()
+ })
+
+ test('basic', async ({ page }) => {
+ await testBasic(page, pageUrl)
})
test('coverage', async ({ page }) => {
await page.goto(pageUrl)
- await page.getByLabel('Show coverage').click()
- await page.frameLocator('#vitest-ui-coverage').getByRole('heading', { name: 'All files' }).click()
+ await testCoverage(page)
})
test('console', async ({ page }) => {
await page.goto(pageUrl)
- const item = page.getByLabel('fixtures/console.test.ts')
- await item.hover()
- await item.getByTestId('btn-open-details').click({ force: true })
- await page.getByTestId('btn-console').click()
- await page.getByText('/(?\\w)/').click()
+ await testConsole(page)
+ })
- expect(await page.getByText('beforeAll').all()).toHaveLength(6)
- expect(await page.getByText('afterAll').all()).toHaveLength(6)
+ test('error', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testError(page)
})
- test('annotations in the report tab', async ({ page }) => {
+ test('filter', async ({ page }) => {
await page.goto(pageUrl)
+ await testFilter(page, { mode: 'static' })
+ })
- await test.step('annotated test', async () => {
- const item = page.getByLabel('annotated test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
+ test('tags filter', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testTagsFilter(page)
+ })
- const annotations = page.getByRole('note')
- await expect(annotations).toHaveCount(2)
+ test('dashboard entries filter tests correctly', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testDashboardFilter(page)
+ })
- await expect(annotations.first()).toContainText('hello world')
- await expect(annotations.first()).toContainText('notice')
- await expect(annotations.first()).toContainText('fixtures/annotated.test.ts:4:9')
+ test('annotations in the report tab', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testAnnotationsInReport(page)
+ })
- await expect(annotations.last()).toContainText('second annotation')
- await expect(annotations.last()).toContainText('notice')
- await expect(annotations.last()).toContainText('fixtures/annotated.test.ts:5:9')
- })
+ test('annotations in the editor tab', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testAnnotationsInCode(page)
+ })
- await test.step('annotated typed test', async () => {
- const item = page.getByLabel('annotated typed test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
+ test('visual regression in the report tab', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testVisualRegression(page)
+ })
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
+ test('cannot edit file', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testWriteFile(page, { enabled: false })
+ })
- await expect(annotation).toContainText('beware!')
- await expect(annotation).toContainText('warning')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:9:9')
- })
+ test('cannot execute', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testExecute(page, { mode: 'static' })
+ })
+})
- await test.step('annotated file test', async () => {
- const item = page.getByLabel('annotated file test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
+async function testBasic(page: Page, pageUrl: string) {
+ const pageErrors: unknown[] = []
+ page.on('pageerror', error => pageErrors.push(error))
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
+ await page.goto(pageUrl)
- await expect(annotation).toContainText('file annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:13:9')
- await expect(annotation.getByRole('link')).toHaveAttribute('href', /__vitest_attachment__\?path=/)
- })
+ // dashboard
+ await assertTestCounts(page, { pass: 17, fail: 3 })
- await test.step('annotated image test', async () => {
- const item = page.getByLabel('annotated image test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
+ // unhandled errors
+ await expect(page.getByTestId('unhandled-errors')).toContainText(
+ 'Vitest caught 2 errors during the test run. This might cause false positive tests. '
+ + 'Resolve unhandled errors to make sure your tests are not affected.',
+ )
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
+ await expect(page.getByTestId('unhandled-errors-details')).toContainText('Error: error')
+ await expect(page.getByTestId('unhandled-errors-details')).toContainText('Unknown Error: 1')
- await expect(annotation).toContainText('image annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:19:9')
- await expect(annotation.getByRole('link')).toHaveAttribute('href', /__vitest_attachment__\?path=/)
- await expect(annotation.getByRole('img')).toHaveAttribute('src', /__vitest_attachment__\?path=/)
- })
+ // report
+ await openExplorerFileItem(page, 'sample.test.ts')
+ await page.getByText('All tests passed in this file').click()
- await test.step('annotated with body base64', async () => {
- const item = page.getByLabel('annotated with body base64')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('body base64 annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:25:9')
-
- const downloadPromise = page.waitForEvent('download')
- await annotation.getByRole('link').click()
- const download = await downloadPromise
- expect(download.suggestedFilename()).toBe('body-base64-annotation.md')
- const downloadPath = await download.path()
- const content = readFileSync(downloadPath, 'utf-8')
- expect(content).toBe('Hello base64 **markdown**')
- })
+ // graph tab
+ await page.getByTestId('btn-graph').click()
+ await expect(page.locator('[data-testid=graph] text')).toContainText('sample.test.ts')
- await test.step('annotated with body utf-8', async () => {
- const item = page.getByLabel('annotated with body utf-8')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
-
- const annotation = page.getByRole('note')
- await expect(annotation).toHaveCount(1)
-
- await expect(annotation).toContainText('body utf-8 annotation')
- await expect(annotation).toContainText('notice')
- await expect(annotation).toContainText('fixtures/annotated.test.ts:32:9')
-
- const downloadPromise = page.waitForEvent('download')
- await annotation.getByRole('link').click()
- const download = await downloadPromise
- expect(download.suggestedFilename()).toBe('body-utf-8-annotation.md')
- const downloadPath = await download.path()
- const content = readFileSync(downloadPath, 'utf-8')
- expect(content).toBe('Hello utf-8 **markdown**')
- })
- })
+ // console tab
+ await page.getByTestId('btn-console').click()
+ await expect(page.getByTestId('console')).toContainText('log test')
- test('annotations in the editor tab', async ({ page }) => {
- await page.goto(pageUrl)
- const item = page.getByLabel('fixtures/annotated.test.ts')
- await item.hover()
- await item.getByTestId('btn-open-details').click({ force: true })
- await page.getByTestId('btn-code').click({ force: true })
+ expect(pageErrors).toEqual([])
+}
+
+async function testCoverage(page: Page) {
+ await page.getByLabel('Show coverage').click()
+ await page.frameLocator('#vitest-ui-coverage').getByRole('heading', { name: 'All files' }).click()
+}
+
+async function testAnnotationsInReport(page: Page) {
+ await test.step('annotated test', async () => {
+ await getExplorerItem(page, 'annotated test').click()
const annotations = page.getByRole('note')
- await expect(annotations).toHaveCount(7)
+ await expect(annotations).toHaveCount(2)
- await expect(annotations.first()).toHaveText('notice: hello world')
- await expect(annotations.nth(1)).toHaveText('notice: second annotation')
- await expect(annotations.nth(2)).toHaveText('warning: beware!')
- await expect(annotations.nth(3)).toHaveText(/notice: file annotation/)
- await expect(annotations.nth(4)).toHaveText('notice: image annotation')
- await expect(annotations.nth(5)).toHaveText(/notice: body base64 annotation/)
- await expect(annotations.nth(6)).toHaveText(/notice: body utf-8 annotation/)
+ await expect(annotations.first()).toContainText('hello world')
+ await expect(annotations.first()).toContainText('notice')
+ await expect(annotations.first()).toContainText('annotated.test.ts:4:9')
- await expect(annotations.nth(3).getByRole('link')).toHaveAttribute('href', /__vitest_attachment__\?path=/)
- await expect(annotations.nth(4).getByRole('link')).toHaveAttribute('href', /__vitest_attachment__\?path=/)
- await expect(annotations.nth(5).getByRole('link')).toHaveAttribute('href', /^data:text\/markdown;base64,/)
- await expect(annotations.nth(6).getByRole('link')).toHaveAttribute('href', /^data:text\/markdown,/)
+ await expect(annotations.last()).toContainText('second annotation')
+ await expect(annotations.last()).toContainText('notice')
+ await expect(annotations.last()).toContainText('annotated.test.ts:5:9')
})
- test('error', async ({ page }) => {
- await page.goto(pageUrl)
- const item = page.getByLabel('fixtures/error.test.ts')
- await item.hover()
- await item.getByTestId('btn-open-details').click({ force: true })
- await expect(page.getByTestId('diff')).toContainText('- Expected + Received + ')
+ await test.step('annotated typed test', async () => {
+ await getExplorerItem(page, 'annotated typed test').click()
+
+ const annotation = page.getByRole('note')
+ await expect(annotation).toHaveCount(1)
- await getExplorerItem(page, 'colored error message').click()
- await expect(page.getByTestId('report')).toHaveText('Error: this-is-blue - /fixtures/error.test.ts:12:17')
+ await expect(annotation).toContainText('beware!')
+ await expect(annotation).toContainText('warning')
+ await expect(annotation).toContainText('annotated.test.ts:9:9')
})
- test('file-filter', async ({ page }) => {
- await page.goto(pageUrl)
+ await test.step('annotated file test', async () => {
+ await getExplorerItem(page, 'annotated file test').click()
- // match all files when no filter
- await page.getByPlaceholder('Search...').fill('')
- await page.getByText('PASS (6)').click()
- await expect(page.getByTestId('results-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeVisible()
-
- // match nothing
- await page.getByPlaceholder('Search...').fill('nothing')
- await page.getByText('No matched test').click()
-
- // searching "add" will match "sample.test.ts" since it includes a test case named "add"
- await page.getByPlaceholder('Search...').fill('add')
- await page.getByText('PASS (1)').click()
- await expect(page.getByTestId('results-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeVisible()
-
- // match only failing files when fail filter applied
- await page.getByPlaceholder('Search...').fill('')
- await page.getByText(/^Fail$/, { exact: true }).click()
- await page.getByText('FAIL (2)').click()
- await expect(page.getByTestId('results-panel').getByText('fixtures/error.test.ts', { exact: true })).toBeVisible()
- await expect(page.getByTestId('results-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeHidden()
-
- // match only pass files when fail filter applied
- await page.getByPlaceholder('Search...').fill('console')
- await page.getByText(/^Fail$/, { exact: true }).click()
- await page.locator('span').filter({ hasText: /^Pass$/ }).click()
- await page.getByText('PASS (1)').click()
- await expect(page.getByTestId('results-panel').getByText('fixtures/console.test.ts', { exact: true })).toBeVisible()
- await expect(page.getByTestId('results-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeHidden()
-
- // html entities in task names are escaped
- await page.locator('span').filter({ hasText: /^Pass$/ }).click()
- await page.getByPlaceholder('Search...').fill('')
- // for some reason, the tree is collapsed by default: we need to click on the nav buttons to expand it
- await page.getByTestId('collapse-all').click()
- await page.getByTestId('expand-all').click()
- await expect(page.getByText('')).toBeVisible()
- await expect(page.getByTestId('results-panel').getByText('fixtures/task-name.test.ts', { exact: true })).toBeVisible()
-
- // html entities in task names are escaped
- await page.getByPlaceholder('Search...').fill('<>\'"')
- await expect(page.getByText('<>\'"')).toBeVisible()
- await expect(page.getByTestId('results-panel').getByText('fixtures/task-name.test.ts', { exact: true })).toBeVisible()
-
- // pass files with special chars
- await page.getByPlaceholder('Search...').fill('char () - Square root of nine (9)')
- await expect(page.getByText('char () - Square root of nine (9)')).toBeVisible()
- const testItem = page.getByTestId('explorer-item').filter({ hasText: 'char () - Square root of nine (9)' })
- await testItem.hover()
- await testItem.getByLabel('Run current test').click()
- await expect(page.getByText('The test has passed without any errors')).toBeVisible()
+ const annotation = page.getByRole('note')
+ await expect(annotation).toHaveCount(1)
+
+ await expect(annotation).toContainText('file annotation')
+ await expect(annotation).toContainText('notice')
+ await expect(annotation).toContainText('annotated.test.ts:13:9')
+ await assertDownloadAttachment(page, {
+ name: 'file annotation',
+ suggestedFilename: 'file-annotation.txt',
+ content: 'hello world\n',
+ })
})
- test('tags filter', async ({ page }) => {
- await page.goto(pageUrl)
+ await test.step('annotated image test', async () => {
+ await getExplorerItem(page, 'annotated image test').click()
- await page.getByPlaceholder('Search...').fill('tag:db')
+ const annotation = page.getByRole('note')
+ await expect(annotation).toHaveCount(1)
+
+ await expect(annotation).toContainText('image annotation')
+ await expect(annotation).toContainText('notice')
+ await expect(annotation).toContainText('annotated.test.ts:19:9')
+ await assertImageAttachment(page, {
+ name: 'image annotation',
+ })
+ })
- // only one test with the tag "db"
- await expect(page.getByText('PASS (1)')).toBeVisible()
- await expect(page.getByTestId('explorer-item').filter({ hasText: 'has tags' })).toBeVisible()
+ await test.step('annotated with body base64', async () => {
+ await getExplorerItem(page, 'annotated with body base64').click()
- await page.getByPlaceholder('Search...').fill('tag:db && !flaky')
- await expect(page.getByText('No matched test')).toBeVisible()
+ const annotation = page.getByRole('note')
+ await expect(annotation).toHaveCount(1)
- await page.getByPlaceholder('Search...').fill('tag:unknown')
- await expect(page.getByText('The tag pattern "unknown" is not defined in the configuration')).toBeVisible()
+ await expect(annotation).toContainText('body base64 annotation')
+ await expect(annotation).toContainText('notice')
+ await expect(annotation).toContainText('annotated.test.ts:25:9')
+ await assertDownloadAttachment(page, {
+ name: 'body base64 annotation',
+ suggestedFilename: 'body-base64-annotation.md',
+ content: 'Hello base64 **markdown**',
+ })
})
- test('dashboard entries filter tests correctly', async ({ page }) => {
- await page.goto(pageUrl)
+ await test.step('annotated with body utf-8', async () => {
+ await getExplorerItem(page, 'annotated with body utf-8').click()
+
+ const annotation = page.getByRole('note')
+ await expect(annotation).toHaveCount(1)
+
+ await expect(annotation).toContainText('body utf-8 annotation')
+ await expect(annotation).toContainText('notice')
+ await expect(annotation).toContainText('annotated.test.ts:32:9')
+ await assertDownloadAttachment(page, {
+ name: 'body utf-8 annotation',
+ suggestedFilename: 'body-utf-8-annotation.md',
+ content: 'Hello utf-8 **markdown**',
+ })
+ })
+}
- // Initial state should show all tests
- await expect(page.getByTestId('pass-entry')).toBeVisible()
- await expect(page.getByTestId('fail-entry')).toBeVisible()
- await expect(page.getByTestId('total-entry')).toBeVisible()
+async function testAnnotationsInCode(page: Page) {
+ await openExplorerFileItem(page, 'annotated.test.ts')
+ await page.getByTestId('btn-code').click()
+
+ const annotations = page.getByRole('note')
+ await expect(annotations).toHaveCount(7)
+
+ await expect(annotations.first()).toHaveText('notice: hello world')
+ await expect(annotations.nth(1)).toHaveText('notice: second annotation')
+ await expect(annotations.nth(2)).toHaveText('warning: beware!')
+ await expect(annotations.nth(3)).toHaveText(/notice: file annotation/)
+ await expect(annotations.nth(4)).toHaveText('notice: image annotation')
+ await expect(annotations.nth(5)).toHaveText(/notice: body base64 annotation/)
+ await expect(annotations.nth(6)).toHaveText(/notice: body utf-8 annotation/)
+
+ await assertDownloadAttachment(page, {
+ name: 'file annotation',
+ suggestedFilename: 'file-annotation.txt',
+ content: 'hello world\n',
+ })
+ await assertDownloadAttachment(page, {
+ name: 'body base64 annotation',
+ suggestedFilename: 'body-base64-annotation.md',
+ content: 'Hello base64 **markdown**',
+ })
+ await assertDownloadAttachment(page, {
+ name: 'body utf-8 annotation',
+ suggestedFilename: 'body-utf-8-annotation.md',
+ content: 'Hello utf-8 **markdown**',
+ })
+ await assertImageAttachment(page, {
+ name: 'image annotation',
+ })
+}
- // Click "Pass" entry and verify only passing tests are shown
- await page.getByTestId('pass-entry').click()
- await expect(page.getByLabel(/pass/i)).toBeChecked()
+async function testConsole(page: Page) {
+ await openExplorerFileItem(page, 'console.test.ts')
+ await page.getByTestId('btn-console').click()
+ await page.getByText('/(?\\w)/').click()
- // Click "Fail" entry and verify only failing tests are shown
- await page.getByTestId('fail-entry').click()
- await expect(page.getByLabel(/fail/i)).toBeChecked()
+ await expect(page.getByText('beforeAll')).toHaveCount(6)
+ await expect(page.getByText('afterAll')).toHaveCount(6)
+}
- // Click "Skip" entry if there are skipped tests
- if (await page.getByTestId('skipped-entry').isVisible()) {
- await page.getByTestId('skipped-entry').click()
- await expect(page.getByLabel(/skip/i)).toBeChecked()
- }
+async function testError(page: Page) {
+ await openExplorerFileItem(page, 'error.test.ts')
+ await expect(page.getByTestId('diff')).toContainText('- Expected + Received + ')
- // Click "Total" entry to reset filters and show all tests again
- await page.getByTestId('total-entry').click()
- await expect(page.getByLabel(/pass/i)).not.toBeChecked()
- await expect(page.getByLabel(/fail/i)).not.toBeChecked()
- await expect(page.getByLabel(/skip/i)).not.toBeChecked()
- })
+ await getExplorerItem(page, 'colored error message').click()
+ await expect(page.getByTestId('report')).toHaveText('Error: this-is-blue - /node/error.test.ts:12:17')
+}
- test('visual regression in the report tab', async ({ page }) => {
- await page.goto(pageUrl)
+async function testTagsFilter(page: Page) {
+ await page.getByPlaceholder('Search...').fill('tag:db')
- await test.step('attachments get processed', async () => {
- const item = page.getByLabel('visual regression test')
- await item.click({ force: true })
- await page.getByTestId('btn-report').click({ force: true })
+ // only one test with the tag "db"
+ await expect(page.getByText('PASS (1)')).toBeVisible()
+ await expect(page.getByTestId('explorer-item').filter({ hasText: 'has tags' })).toBeVisible()
+
+ await page.getByPlaceholder('Search...').fill('tag:db && !flaky')
+ await expect(page.getByText('No matched test')).toBeVisible()
+
+ await page.getByPlaceholder('Search...').fill('tag:unknown')
+ await expect(page.getByText('The tag pattern "unknown" is not defined in the configuration')).toBeVisible()
+}
- const artifact = page.getByRole('note')
- await expect(artifact).toHaveCount(1)
+async function testVisualRegression(page: Page) {
+ await getExplorerItem(page, 'visual regression test').click()
+
+ const artifact = page.getByRole('note')
+ await expect(artifact).toHaveCount(1)
+
+ await expect(artifact.getByRole('heading')).toContainText('Visual Regression')
+ await expect(artifact).toContainText('visual-regression.test.ts:7:3')
+ await expect(artifact.getByRole('tablist')).toHaveText('Reference')
+ await expect(artifact.getByRole('tabpanel').getByRole('img')).not.toHaveJSProperty('naturalWidth', 0)
+}
+
+async function testDashboardFilter(page: Page) {
+ // Initial state should show all tests
+ await expect(page.getByTestId('pass-entry')).toBeVisible()
+ await expect(page.getByTestId('fail-entry')).toBeVisible()
+ await expect(page.getByTestId('total-entry')).toBeVisible()
+
+ // Click "Pass" entry and verify only passing tests are shown
+ await page.getByTestId('pass-entry').click()
+ await expect(page.getByLabel(/pass/i)).toBeChecked()
+
+ // Click "Fail" entry and verify only failing tests are shown
+ await page.getByTestId('fail-entry').click()
+ await expect(page.getByLabel(/fail/i)).toBeChecked()
+
+ // TODO: test skip
+ // Click "Skip" entry if there are skipped tests
+ if (await page.getByTestId('skipped-entry').isVisible()) {
+ await page.getByTestId('skipped-entry').click()
+ await expect(page.getByLabel(/skip/i)).toBeChecked()
+ }
+
+ // Click "Total" entry to reset filters and show all tests again
+ await page.getByTestId('total-entry').click()
+ await expect(page.getByLabel(/pass/i)).not.toBeChecked()
+ await expect(page.getByLabel(/fail/i)).not.toBeChecked()
+ await expect(page.getByLabel(/skip/i)).not.toBeChecked()
+}
- await expect(artifact.getByRole('heading')).toContainText('Visual Regression')
- await expect(artifact).toContainText('fixtures-browser/visual-regression.test.ts:13:3')
- await expect(artifact.getByRole('tablist')).toHaveText('Reference')
- await expect(artifact.getByRole('tabpanel').getByRole('link')).toHaveAttribute('href', /__vitest_attachment__\?path=.*?\.png/)
- await expect(artifact.getByRole('tabpanel').getByRole('img')).toHaveAttribute('src', /__vitest_attachment__\?path=.*?\.png/)
+async function testFilter(page: Page, options: { mode: 'ui' | 'static' }) {
+ // match all files when no filter
+ await page.getByPlaceholder('Search...').fill('')
+ await page.getByText('PASS (6)').click()
+ await expect(page.getByTestId('results-panel').getByText('sample.test.ts', { exact: true })).toBeVisible()
+
+ // match nothing
+ await page.getByPlaceholder('Search...').fill('nothing')
+ await page.getByText('No matched test').click()
+
+ // searching "add" will match "sample.test.ts" since it includes a test case named "add"
+ await page.getByPlaceholder('Search...').fill('add')
+ await page.getByText('PASS (1)').click()
+ await expect(page.getByTestId('results-panel').getByText('sample.test.ts', { exact: true })).toBeVisible()
+
+ // match only failing files when fail filter applied
+ await page.getByPlaceholder('Search...').fill('')
+ await page.getByText(/^Fail$/, { exact: true }).click()
+ await page.getByText('FAIL (2)').click()
+ await expect(page.getByTestId('results-panel').getByText('error.test.ts', { exact: true })).toBeVisible()
+ await expect(page.getByTestId('results-panel').getByText('sample.test.ts', { exact: true })).toBeHidden()
+
+ // match only pass files when fail filter applied
+ await page.getByPlaceholder('Search...').fill('console')
+ await page.getByText(/^Fail$/, { exact: true }).click()
+ await page.locator('span').filter({ hasText: /^Pass$/ }).click()
+ await page.getByText('PASS (1)').click()
+ await expect(page.getByTestId('results-panel').getByText('console.test.ts', { exact: true })).toBeVisible()
+ await expect(page.getByTestId('results-panel').getByText('sample.test.ts', { exact: true })).toBeHidden()
+
+ // html entities in task names are escaped
+ await page.locator('span').filter({ hasText: /^Pass$/ }).click()
+ await page.getByPlaceholder('Search...').fill('')
+ // for some reason, the tree is collapsed by default: we need to click on the nav buttons to expand it
+ await page.getByTestId('collapse-all').click()
+ await page.getByTestId('expand-all').click()
+ await expect(page.getByText('')).toBeVisible()
+ await expect(page.getByTestId('results-panel').getByText('task-name.test.ts', { exact: true })).toBeVisible()
+
+ // html entities in task names are escaped
+ await page.getByPlaceholder('Search...').fill('<>\'"')
+ await expect(page.getByText('<>\'"')).toBeVisible()
+ await expect(page.getByTestId('results-panel').getByText('task-name.test.ts', { exact: true })).toBeVisible()
+
+ // pass files with special chars
+ await page.getByPlaceholder('Search...').fill('char () - Square root of nine (9)')
+ const testItem = getExplorerItem(page, 'char () - Square root of nine (9)')
+ await expect(testItem).toBeVisible()
+ if (options.mode === 'ui') {
+ await testItem.hover()
+ await testItem.getByLabel('Run current test').click()
+ await expect(page.getByText('The test has passed without any errors')).toBeVisible()
+ }
+}
+
+async function testCrossOriginAccess(page: Page, pageUrl: string) {
+ await page.route('https://example.com/**', (route) => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'text/html',
+ body: 'Faked Cross Origin Site
',
})
})
-})
+ await page.goto('https://example.com/', { timeout: 5000 })
+
+ // request html
+ const htmlResult = await page.evaluate(async (pageUrl) => {
+ try {
+ const res = await fetch(pageUrl)
+ return res.status
+ }
+ catch (e) {
+ return e instanceof Error ? e.message : e
+ }
+ }, pageUrl)
+ expect(htmlResult).toBe('Failed to fetch')
+
+ // request websocket
+ const wsResult = await page.evaluate(async (pageUrl) => {
+ const ws = new WebSocket(new URL('/__vitest_api__', pageUrl))
+ return new Promise((resolve) => {
+ ws.addEventListener('open', () => {
+ resolve('open')
+ })
+ ws.addEventListener('error', () => {
+ resolve('error')
+ })
+ })
+ }, pageUrl)
+ expect(wsResult).toBe('error')
+}
-// TODO: consolidate in https://github.com/vitest-dev/vitest/pull/10237
-function getExplorerItem(page: Page, name: string) {
- return page.getByTestId('explorer-item').and(page.getByLabel(name, { exact: true }))
+async function testWriteFile(page: Page, options: { enabled: boolean }) {
+ await getExplorerItem(page, 'add').click()
+ const codeTabButton = page.getByTestId('btn-code')
+ await expect(codeTabButton).toHaveText('Code')
+ await codeTabButton.click()
+ const editor = page.getByTestId('editor')
+ await expect(editor).toContainText('expect(1 + 1).toEqual(2)')
+ await page.keyboard.type('\n// edited \n')
+ if (options.enabled) {
+ await expect(editor).toContainText('// edited')
+ }
+ else {
+ await expect(editor).not.toContainText('// edited')
+ }
}
-async function assertTestCounts(page: Page, options: { pass: number; fail: number }) {
- await expect.soft(page.getByTestId('tests-entry'))
- .toContainText(`${options.pass} Pass ${options.fail} Fail ${options.pass + options.fail} Total`)
+async function testExecute(page: Page, options: { mode: 'ui' | 'ui-disallow' | 'static' }) {
+ if (options.mode === 'ui') {
+ await expect(page.getByTestId('btn-run-all')).toBeEnabled()
+
+ const item = getExplorerItem(page, 'add')
+ await item.hover()
+ await expect(item.getByTestId('btn-run-test')).toBeEnabled()
+
+ await page.getByPlaceholder('Search...').fill('snapshot')
+ const snapshotItem = getExplorerItem(page, 'snapshot.test.ts')
+ await snapshotItem.hover()
+ await expect(snapshotItem.getByTestId('btn-fix-snapshot')).toBeVisible()
+ }
+ if (options.mode === 'ui-disallow') {
+ await expect(page.getByTestId('btn-run-all')).toBeDisabled()
+
+ const item = getExplorerItem(page, 'add')
+ await item.hover()
+ await expect(item.getByTestId('btn-run-test')).toBeDisabled()
+
+ await page.getByPlaceholder('Search...').fill('snapshot')
+ const snapshotItem = getExplorerItem(page, 'snapshot.test.ts')
+ await snapshotItem.hover()
+ await expect(snapshotItem.getByTestId('btn-fix-snapshot')).not.toBeVisible()
+ }
+ if (options.mode === 'static') {
+ await expect(page.getByTestId('btn-run-all')).not.toBeVisible()
+
+ const item = getExplorerItem(page, 'add')
+ await item.hover()
+ await expect(item.getByTestId('btn-run-test')).not.toBeVisible()
+
+ await page.getByPlaceholder('Search...').fill('snapshot')
+ const snapshotItem = getExplorerItem(page, 'snapshot.test.ts')
+ await snapshotItem.hover()
+ await expect(snapshotItem.getByTestId('btn-fix-snapshot')).not.toBeVisible()
+ }
}
test.describe('standalone', () => {
let vitest: Vitest | undefined
+ let pageUrl: string
test.beforeAll(async () => {
- // silence Vitest logs
- const stdout = new Writable({ write: (_, __, callback) => callback() })
- const stderr = new Writable({ write: (_, __, callback) => callback() })
- vitest = await startVitest('test', [], {
+ const server = await startVitestUi({
+ root: './fixtures/main',
watch: true,
ui: true,
standalone: true,
open: false,
- api: { port },
reporters: [],
- }, {}, {
- stdout,
- stderr,
})
- expect(vitest).toBeDefined()
+ vitest = server.vitest
+ pageUrl = `${server.url}/__vitest__/`
})
test.afterAll(async () => {
@@ -417,16 +583,52 @@ test.describe('standalone', () => {
await page.goto(pageUrl)
// initially no stats
- await expect(page.locator('[aria-labelledby=tests]')).toContainText('0 Pass 0 Fail 0 Total')
+ await assertTestCounts(page, { pass: 0, fail: 0 })
// run single file
- await page.getByText('fixtures/sample.test.ts').hover()
+ await getExplorerItem(page, 'sample.test.ts').hover()
await page.getByRole('button', { name: 'Run current file' }).click()
// check results
- await page.getByText('PASS (1)').click()
+ await page.getByRole('button', { name: 'Show dashboard' }).click()
+ await assertTestCounts(page, { pass: 2, fail: 0 })
expect(vitest?.state.getFiles().map(f => [f.name, f.result?.state])).toEqual([
- ['fixtures/sample.test.ts', 'pass'],
+ ['sample.test.ts', 'pass'],
])
})
})
+
+test.describe('security', () => {
+ let vitest: Vitest | undefined
+ let pageUrl: string
+
+ test.beforeAll(async () => {
+ const server = await startVitestUi({
+ root: './fixtures/main',
+ watch: true,
+ ui: true,
+ open: false,
+ api: {
+ allowExec: false,
+ allowWrite: false,
+ },
+ reporters: [],
+ })
+ vitest = server.vitest
+ pageUrl = `${server.url}/__vitest__/`
+ })
+
+ test.afterAll(async () => {
+ await vitest?.close()
+ })
+
+ test('cannot write file', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testWriteFile(page, { enabled: false })
+ })
+
+ test('cannot execute', async ({ page }) => {
+ await page.goto(pageUrl)
+ await testExecute(page, { mode: 'ui-disallow' })
+ })
+})
diff --git a/test/ui/vitest.config.ts b/test/ui/vitest.config.ts
deleted file mode 100644
index e577e582b87b..000000000000
--- a/test/ui/vitest.config.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { resolve } from 'node:path'
-import { playwright } from '@vitest/browser-playwright'
-import { defaultExclude, defineConfig } from 'vitest/config'
-
-export default defineConfig({
- test: {
- exclude: ['**/fixtures-trace/**', '**/fixtures-trace-stream/**', ...defaultExclude],
- coverage: {
- reportOnFailure: true,
- },
- tags: [
- { name: 'db' },
- { name: 'flaky' },
- ],
- projects: [{
- extends: true,
- test: {
- name: 'fixtures',
- dir: './fixtures',
- environment: 'happy-dom',
- },
- }, {
- extends: true,
- test: {
- name: 'browser',
- dir: './fixtures-browser',
- browser: {
- enabled: true,
- headless: true,
- provider: playwright(),
- instances: [{ browser: 'chromium' }],
- screenshotFailures: false,
- expect: {
- toMatchScreenshot: {
- resolveScreenshotPath: ({ root, testFileDirectory, arg, ext }) => resolve(root, testFileDirectory, `${arg}${ext}`),
- },
- },
- },
- },
- }],
- },
-})