From 7425f150b992bfe7b3f73812a27d2cf447d909e5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 13 May 2026 13:46:34 +0900 Subject: [PATCH 1/5] test: fix e2e fixture path (#10335) --- test/e2e/test/reporters/junit.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 () => { From e8e4a1b4ac49c00ece33150755245906b10e51d5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 13 May 2026 16:03:18 +0900 Subject: [PATCH 2/5] test: use `vi.defineHelper` for vue render wrapper (#10337) --- packages/ui/client/test.ts | 7 ++++--- packages/ui/vitest.config.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) 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/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() From 964b67f144fd644a28f6345d93fe615a382a809a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 13 May 2026 16:06:39 +0900 Subject: [PATCH 3/5] test: rework `test/ui` (#10237) Co-authored-by: Codex --- eslint.config.js | 1 + test/ui/README.md | 7 +- .../visual-regression.test.ts | 16 - .../main/browser/visual-regression.test.ts | 10 + .../{ => main/node}/annotated.test.ts | 4 +- .../fixtures/{ => main/node}/console.test.ts | 0 .../fixtures/{ => main/node}/coverage.test.ts | 0 test/ui/fixtures/{ => main/node}/coverage.ts | 0 .../fixtures/{ => main/node}/cute-puppy.jpg | Bin .../ui/fixtures/{ => main/node}/error.test.ts | 0 test/ui/fixtures/{ => main/node}/example.txt | 0 .../fixtures/{ => main/node}/sample.test.ts | 0 .../fixtures/{ => main/node}/snapshot.test.ts | 0 .../{ => main/node}/task-name.test.ts | 0 test/ui/fixtures/main/vitest.config.ts | 49 ++ .../trace-stream}/basic.test.ts | 0 .../trace-stream}/helper.ts | 0 .../trace-stream}/range.test.ts | 0 .../trace-stream}/vitest.config.ts | 0 .../trace}/assets/trace-pixel.svg | 0 .../trace}/assets/trace-style.css | 0 .../trace}/basic.test.ts | 0 .../trace}/retry.test.ts | 0 .../trace}/scroll.test.ts | 0 .../trace}/viewport.test.ts | 0 .../trace}/vitest.config.ts | 0 test/ui/package.json | 4 +- test/ui/test/helper.ts | 95 ++ test/ui/test/html-report.spec.ts | 270 ------ test/ui/test/trace-stream.spec.ts | 25 +- test/ui/test/trace.spec.ts | 71 +- test/ui/test/ui-security.spec.ts | 70 -- test/ui/test/ui.spec.ts | 822 +++++++++++------- test/ui/vitest.config.ts | 42 - 34 files changed, 700 insertions(+), 786 deletions(-) delete mode 100644 test/ui/fixtures-browser/visual-regression.test.ts create mode 100644 test/ui/fixtures/main/browser/visual-regression.test.ts rename test/ui/fixtures/{ => main/node}/annotated.test.ts (92%) rename test/ui/fixtures/{ => main/node}/console.test.ts (100%) rename test/ui/fixtures/{ => main/node}/coverage.test.ts (100%) rename test/ui/fixtures/{ => main/node}/coverage.ts (100%) rename test/ui/fixtures/{ => main/node}/cute-puppy.jpg (100%) rename test/ui/fixtures/{ => main/node}/error.test.ts (100%) rename test/ui/fixtures/{ => main/node}/example.txt (100%) rename test/ui/fixtures/{ => main/node}/sample.test.ts (100%) rename test/ui/fixtures/{ => main/node}/snapshot.test.ts (100%) rename test/ui/fixtures/{ => main/node}/task-name.test.ts (100%) create mode 100644 test/ui/fixtures/main/vitest.config.ts rename test/ui/{fixtures-trace-stream => fixtures/trace-stream}/basic.test.ts (100%) rename test/ui/{fixtures-trace-stream => fixtures/trace-stream}/helper.ts (100%) rename test/ui/{fixtures-trace-stream => fixtures/trace-stream}/range.test.ts (100%) rename test/ui/{fixtures-trace-stream => fixtures/trace-stream}/vitest.config.ts (100%) rename test/ui/{fixtures-trace => fixtures/trace}/assets/trace-pixel.svg (100%) rename test/ui/{fixtures-trace => fixtures/trace}/assets/trace-style.css (100%) rename test/ui/{fixtures-trace => fixtures/trace}/basic.test.ts (100%) rename test/ui/{fixtures-trace => fixtures/trace}/retry.test.ts (100%) rename test/ui/{fixtures-trace => fixtures/trace}/scroll.test.ts (100%) rename test/ui/{fixtures-trace => fixtures/trace}/viewport.test.ts (100%) rename test/ui/{fixtures-trace => fixtures/trace}/vitest.config.ts (100%) create mode 100644 test/ui/test/helper.ts delete mode 100644 test/ui/test/html-report.spec.ts delete mode 100644 test/ui/test/ui-security.spec.ts delete mode 100644 test/ui/vitest.config.ts 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/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}`), - }, - }, - }, - }, - }], - }, -}) From c8a43c1b63a968ddb7a2fa03fcd4b84f8de57d5a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 13 May 2026 16:07:39 +0900 Subject: [PATCH 4/5] chore(ui): guide for ui client dev with html report (#10312) --- packages/ui/.gitignore | 1 + packages/ui/CONTRIBUTING.md | 17 ++++++++++++- packages/ui/vite.config.ts | 48 ++++++++++++++++++++++++++----------- 3 files changed, 51 insertions(+), 15 deletions(-) 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/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() + }) + }, + } +} From fab1b6020bb1118b52308bdf02f4fef0567f347a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 13 May 2026 16:08:33 +0900 Subject: [PATCH 5/5] fix: make `attachmentsDir` root only config (#10334) Co-authored-by: Codex --- docs/config/attachmentsdir.md | 6 ++-- docs/guide/projects.md | 1 + packages/vitest/src/node/project.ts | 2 ++ test/e2e/test/annotations.test.ts | 51 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) 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/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", + ] + `) +})