From 169028f03abf5e80d77924f4ed9ae6107647c4c0 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 25 Oct 2024 14:38:16 +0200 Subject: [PATCH] feat(browser): allow custom HTML path, respect plugins `transformIndexHtml` (#6725) --- docs/config/index.md | 8 ++ packages/browser/rollup.config.js | 2 +- .../src/client/public/esm-client-injector.js | 98 ++++++++-------- .../browser/src/client/tester/tester.html | 7 +- packages/browser/src/node/plugin.ts | 105 +++++++++++++++++- packages/browser/src/node/pool.ts | 3 +- packages/browser/src/node/server.ts | 41 +++++-- .../browser/src/node/serverOrchestrator.ts | 11 +- packages/browser/src/node/serverTester.ts | 48 ++++---- packages/browser/src/node/utils.ts | 2 +- packages/vitest/src/node/cli/cli-config.ts | 1 + packages/vitest/src/node/types/browser.ts | 5 + .../browser-custom-html/browser-basic.test.ts | 9 ++ .../browser-custom.test.ts | 9 ++ .../browser-custom-html/custom-html.html | 11 ++ .../vitest.config.correct.ts | 14 +++ ...vitest.config.custom-transformIndexHtml.ts | 40 +++++++ ...itest.config.default-transformIndexHtml.ts | 39 +++++++ .../vitest.config.error-hook.ts | 22 ++++ .../vitest.config.non-existing.ts | 13 +++ test/config/test/browser-html.test.ts | 61 ++++++++++ test/test-utils/index.ts | 3 + 22 files changed, 450 insertions(+), 102 deletions(-) create mode 100644 test/config/fixtures/browser-custom-html/browser-basic.test.ts create mode 100644 test/config/fixtures/browser-custom-html/browser-custom.test.ts create mode 100644 test/config/fixtures/browser-custom-html/custom-html.html create mode 100644 test/config/fixtures/browser-custom-html/vitest.config.correct.ts create mode 100644 test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts create mode 100644 test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts create mode 100644 test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts create mode 100644 test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts create mode 100644 test/config/test/browser-html.test.ts diff --git a/docs/config/index.md b/docs/config/index.md index f9321433d458..9755350a05bf 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1640,6 +1640,14 @@ Run the browser in a `headless` mode. If you are running Vitest in CI, it will b Run every test in a separate iframe. +#### browser.testerHtmlPath + +- **Type:** `string` +- **Default:** `@vitest/browser/tester.html` +- **Version:** Since Vitest 2.1.4 + +A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook. + #### browser.api - **Type:** `number | { port?, strictPort?, host? }` diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index 46687ba7507c..7b00fc5bfa04 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -107,7 +107,7 @@ export default () => input: './src/client/tester/state.ts', output: { file: 'dist/state.js', - format: 'esm', + format: 'iife', }, plugins: [ esbuild({ diff --git a/packages/browser/src/client/public/esm-client-injector.js b/packages/browser/src/client/public/esm-client-injector.js index 27afb1bb9b06..fe4fef717e92 100644 --- a/packages/browser/src/client/public/esm-client-injector.js +++ b/packages/browser/src/client/public/esm-client-injector.js @@ -1,50 +1,52 @@ -const moduleCache = new Map(); - -function wrapModule(module) { - if (typeof module === "function") { - const promise = new Promise((resolve, reject) => { - if (typeof __vitest_mocker__ === "undefined") - return module().then(resolve, reject); - __vitest_mocker__.prepare().finally(() => { - module().then(resolve, reject); +(() => { + const moduleCache = new Map(); + + function wrapModule(module) { + if (typeof module === "function") { + const promise = new Promise((resolve, reject) => { + if (typeof __vitest_mocker__ === "undefined") + return module().then(resolve, reject); + __vitest_mocker__.prepare().finally(() => { + module().then(resolve, reject); + }); }); - }); - moduleCache.set(promise, { promise, evaluated: false }); - return promise.finally(() => moduleCache.delete(promise)); + moduleCache.set(promise, { promise, evaluated: false }); + return promise.finally(() => moduleCache.delete(promise)); + } + return module; } - return module; -} - -window.__vitest_browser_runner__ = { - wrapModule, - wrapDynamicImport: wrapModule, - moduleCache, - config: { __VITEST_CONFIG__ }, - viteConfig: { __VITEST_VITE_CONFIG__ }, - files: { __VITEST_FILES__ }, - type: { __VITEST_TYPE__ }, - contextId: { __VITEST_CONTEXT_ID__ }, - testerId: { __VITEST_TESTER_ID__ }, - provider: { __VITEST_PROVIDER__ }, - providedContext: { __VITEST_PROVIDED_CONTEXT__ }, -}; - -const config = __vitest_browser_runner__.config; - -if (config.testNamePattern) - config.testNamePattern = parseRegexp(config.testNamePattern); - -function parseRegexp(input) { - // Parse input - const m = input.match(/(\/?)(.+)\1([a-z]*)/i); - - // match nothing - if (!m) return /$^/; - - // Invalid flags - if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) - return RegExp(input); - - // Create the regular expression - return new RegExp(m[2], m[3]); -} + + window.__vitest_browser_runner__ = { + wrapModule, + wrapDynamicImport: wrapModule, + moduleCache, + config: { __VITEST_CONFIG__ }, + viteConfig: { __VITEST_VITE_CONFIG__ }, + files: { __VITEST_FILES__ }, + type: { __VITEST_TYPE__ }, + contextId: { __VITEST_CONTEXT_ID__ }, + testerId: { __VITEST_TESTER_ID__ }, + provider: { __VITEST_PROVIDER__ }, + providedContext: { __VITEST_PROVIDED_CONTEXT__ }, + }; + + const config = __vitest_browser_runner__.config; + + if (config.testNamePattern) + config.testNamePattern = parseRegexp(config.testNamePattern); + + function parseRegexp(input) { + // Parse input + const m = input.match(/(\/?)(.+)\1([a-z]*)/i); + + // match nothing + if (!m) return /$^/; + + // Invalid flags + if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) + return RegExp(input); + + // Create the regular expression + return new RegExp(m[2], m[3]); + } +})(); diff --git a/packages/browser/src/client/tester/tester.html b/packages/browser/src/client/tester/tester.html index 1bf9f3ff32f1..8586e2166f22 100644 --- a/packages/browser/src/client/tester/tester.html +++ b/packages/browser/src/client/tester/tester.html @@ -4,7 +4,7 @@ - {__VITEST_TITLE__} + Vitest Browser Tester - {__VITEST_INJECTOR__} - - {__VITEST_INTERNAL_SCRIPTS__} - {__VITEST_SCRIPTS__} - {__VITEST_APPEND__} diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index 0df69a83d153..2140c4bcc803 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -1,4 +1,5 @@ import type { Stats } from 'node:fs' +import type { HtmlTagDescriptor } from 'vite' import type { WorkspaceProject } from 'vitest/node' import type { BrowserServer } from './server' import { lstatSync, readFileSync } from 'node:fs' @@ -72,9 +73,11 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { return } - const html = await resolveTester(browserServer, url, res) - res.write(html, 'utf-8') - res.end() + const html = await resolveTester(browserServer, url, res, next) + if (html) { + res.write(html, 'utf-8') + res.end() + } }) server.middlewares.use( @@ -394,6 +397,102 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { } }, }, + { + name: 'vitest:browser:transform-tester-html', + enforce: 'pre', + async transformIndexHtml(html, ctx) { + if (!ctx.path.startsWith(browserServer.prefixTesterUrl)) { + return + } + + if (!browserServer.testerScripts) { + const testerScripts = await browserServer.formatScripts( + project.config.browser.testerScripts, + ) + browserServer.testerScripts = testerScripts + } + const stateJs = typeof browserServer.stateJs === 'string' + ? browserServer.stateJs + : await browserServer.stateJs + + const testerScripts: HtmlTagDescriptor[] = [] + if (resolve(distRoot, 'client/tester/tester.html') !== browserServer.testerFilepath) { + const manifestContent = browserServer.manifest instanceof Promise + ? await browserServer.manifest + : browserServer.manifest + const testerEntry = manifestContent['tester/tester.html'] + + testerScripts.push({ + tag: 'script', + attrs: { + type: 'module', + crossorigin: '', + src: `${browserServer.base}${testerEntry.file}`, + }, + injectTo: 'head', + }) + + for (const importName of testerEntry.imports || []) { + const entryManifest = manifestContent[importName] + if (entryManifest) { + testerScripts.push( + { + tag: 'link', + attrs: { + href: `${browserServer.base}${entryManifest.file}`, + rel: 'modulepreload', + crossorigin: '', + }, + injectTo: 'head', + }, + ) + } + } + } + + return [ + { + tag: 'script', + children: '{__VITEST_INJECTOR__}', + injectTo: 'head-prepend' as const, + }, + { + tag: 'script', + children: stateJs, + injectTo: 'head-prepend', + } as const, + { + tag: 'script', + attrs: { + type: 'module', + src: browserServer.errorCatcherUrl, + }, + injectTo: 'head' as const, + }, + browserServer.locatorsUrl + ? { + tag: 'script', + attrs: { + type: 'module', + src: browserServer.locatorsUrl, + }, + injectTo: 'head', + } as const + : null, + ...browserServer.testerScripts, + ...testerScripts, + { + tag: 'script', + attrs: { + 'type': 'module', + 'data-vitest-append': '', + }, + children: '{__VITEST_APPEND__}', + injectTo: 'body', + } as const, + ].filter(s => s != null) + }, + }, { name: 'vitest:browser:support-testing-library', config() { diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index d079502cce07..c0bc9f0f8828 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -101,8 +101,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { url.searchParams.set('contextId', contextId) const page = provider .openPage(contextId, url.toString(), () => setBreakpoint(contextId, files[0])) - .then(() => waitPromise) - promises.push(page) + promises.push(page, waitPromise) } }) diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index cd17ff03c5a3..a07991d95647 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -1,5 +1,5 @@ -import type { ErrorWithDiff } from '@vitest/utils' -import type { SerializedConfig } from 'vitest' +import type { HtmlTagDescriptor } from 'vite' +import type { ErrorWithDiff, SerializedConfig } from 'vitest' import type { BrowserProvider, BrowserScript, @@ -8,6 +8,7 @@ import type { Vite, WorkspaceProject, } from 'vitest/node' +import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { slash } from '@vitest/utils' @@ -22,10 +23,11 @@ export class BrowserServer implements IBrowserServer { public prefixTesterUrl: string public orchestratorScripts: string | undefined - public testerScripts: string | undefined + public testerScripts: HtmlTagDescriptor[] | undefined public manifest: Promise | Vite.Manifest public testerHtml: Promise | string + public testerFilepath: string public orchestratorHtml: Promise | string public injectorJs: Promise | string public errorCatcherUrl: string @@ -76,8 +78,16 @@ export class BrowserServer implements IBrowserServer { ) })().then(manifest => (this.manifest = manifest)) + const testerHtmlPath = project.config.browser.testerHtmlPath + ? resolve(project.config.root, project.config.browser.testerHtmlPath) + : resolve(distRoot, 'client/tester/tester.html') + if (!existsSync(testerHtmlPath)) { + throw new Error(`Tester HTML file "${testerHtmlPath}" doesn't exist.`) + } + this.testerFilepath = testerHtmlPath + this.testerHtml = readFile( - resolve(distRoot, 'client/tester/tester.html'), + testerHtmlPath, 'utf8', ).then(html => (this.testerHtml = html)) this.orchestratorHtml = (project.config.browser.ui @@ -124,11 +134,11 @@ export class BrowserServer implements IBrowserServer { scripts: BrowserScript[] | undefined, ) { if (!scripts?.length) { - return '' + return [] } const server = this.vite const promises = scripts.map( - async ({ content, src, async, id, type = 'module' }, index) => { + async ({ content, src, async, id, type = 'module' }, index): Promise => { const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`) await server.moduleGraph.ensureEntryFromUrl(transformId) @@ -136,12 +146,23 @@ export class BrowserServer implements IBrowserServer { = content && type === 'module' ? (await server.pluginContainer.transform(content, transformId)).code : content - return `` + return { + tag: 'script', + attrs: { + type, + ...(async ? { async: '' } : {}), + ...(srcLink + ? { + src: srcLink.startsWith('http') ? srcLink : slash(`/@fs/${srcLink}`), + } + : {}), + }, + injectTo: 'head', + children: contentProcessed || '', + } }, ) - return (await Promise.all(promises)).join('\n') + return (await Promise.all(promises)) } async initBrowserProvider() { diff --git a/packages/browser/src/node/serverOrchestrator.ts b/packages/browser/src/node/serverOrchestrator.ts index 60d39b1e04d6..ece1471e0f8e 100644 --- a/packages/browser/src/node/serverOrchestrator.ts +++ b/packages/browser/src/node/serverOrchestrator.ts @@ -38,9 +38,16 @@ export async function resolveOrchestrator( res.removeHeader('Content-Security-Policy') if (!server.orchestratorScripts) { - server.orchestratorScripts = await server.formatScripts( + server.orchestratorScripts = (await server.formatScripts( project.config.browser.orchestratorScripts, - ) + )).map((script) => { + let html = '` + return html + }).join('\n') } let baseHtml = typeof server.orchestratorHtml === 'string' diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index e6796fe311e6..0773787cd67e 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -1,4 +1,5 @@ import type { IncomingMessage, ServerResponse } from 'node:http' +import type { Connect } from 'vite' import type { BrowserServer } from './server' import crypto from 'node:crypto' import { stringify } from 'flatted' @@ -8,7 +9,8 @@ export async function resolveTester( server: BrowserServer, url: URL, res: ServerResponse, -): Promise { + next: Connect.NextFunction, +): Promise { const csp = res.getHeader('Content-Security-Policy') if (typeof csp === 'string') { // add frame-ancestors to allow the iframe to be loaded by Vitest, @@ -51,36 +53,24 @@ export async function resolveTester( __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())), }) - if (!server.testerScripts) { - const testerScripts = await server.formatScripts( - project.config.browser.testerScripts, - ) - const clientScript = `` - const stateJs = typeof server.stateJs === 'string' - ? server.stateJs - : await server.stateJs - const stateScript = `` - server.testerScripts = `${stateScript}${clientScript}${testerScripts}` - } - const testerHtml = typeof server.testerHtml === 'string' ? server.testerHtml : await server.testerHtml - return replacer(testerHtml, { - __VITEST_FAVICON__: server.faviconUrl, - __VITEST_TITLE__: 'Vitest Browser Tester', - __VITEST_SCRIPTS__: server.testerScripts, - __VITEST_INJECTOR__: ``, - __VITEST_INTERNAL_SCRIPTS__: [ - ``, - server.locatorsUrl ? `` : '', - ].join('\n'), - __VITEST_APPEND__: ``, - }) + try { + const indexhtml = await server.vite.transformIndexHtml(url.pathname, testerHtml) + return replacer(indexhtml, { + __VITEST_INJECTOR__: injector, + __VITEST_APPEND__: ` + __vitest_browser_runner__.runningFiles = ${tests} + __vitest_browser_runner__.iframeId = ${iframeId} + __vitest_browser_runner__.${method === 'run' ? 'runTests' : 'collectTests'}(__vitest_browser_runner__.runningFiles) + document.querySelector('script[data-vitest-append]').remove() + `, + }) + } + catch (err) { + context?.reject(err) + next(err) + } } diff --git a/packages/browser/src/node/utils.ts b/packages/browser/src/node/utils.ts index 4af728a1f9fb..b1b3c206b7f1 100644 --- a/packages/browser/src/node/utils.ts +++ b/packages/browser/src/node/utils.ts @@ -1,7 +1,7 @@ import type { BrowserProviderModule, ResolvedBrowserOptions, WorkspaceProject } from 'vitest/node' export function replacer(code: string, values: Record) { - return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? '') + return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _) } const builtinProviders = ['webdriverio', 'playwright', 'preview'] diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 4feed9dc8979..638ec22d508a 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -415,6 +415,7 @@ export const cliOptionsConfig: VitestCLIOptions = { screenshotDirectory: null, screenshotFailures: null, locators: null, + testerHtmlPath: null, }, }, pool: { diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index 9bcb14f7c119..3d607c6dae47 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -156,8 +156,13 @@ export interface BrowserConfigOptions { /** * Scripts injected into the tester iframe. + * @deprecated Will be removed in the future, use `testerHtmlPath` instead. */ testerScripts?: BrowserScript[] + /** + * Path to the index.html file that will be used to run tests. + */ + testerHtmlPath?: string /** * Scripts injected into the main window. diff --git a/test/config/fixtures/browser-custom-html/browser-basic.test.ts b/test/config/fixtures/browser-custom-html/browser-basic.test.ts new file mode 100644 index 000000000000..8f02f9a9f850 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/browser-basic.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from 'vitest'; + +test('basic', async () => { + const div = document.createElement('div') + div.textContent = ' Vitest' + document.body.appendChild(div) + expect(document.body.textContent).toContain('HELLO WORLD') + expect(document.body.textContent).toContain('Vitest') +}) diff --git a/test/config/fixtures/browser-custom-html/browser-custom.test.ts b/test/config/fixtures/browser-custom-html/browser-custom.test.ts new file mode 100644 index 000000000000..7cf36e045a30 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/browser-custom.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from 'vitest'; + +test('custom', () => { + expect(window).toHaveProperty('CUSTOM_INJECTED', true) +}) + +test('importmap is injected', () => { + expect(import.meta.resolve('some-lib')).toBe('https://vitest.dev/some-lib') +}) diff --git a/test/config/fixtures/browser-custom-html/custom-html.html b/test/config/fixtures/browser-custom-html/custom-html.html new file mode 100644 index 000000000000..eeb873b38267 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/custom-html.html @@ -0,0 +1,11 @@ + + + + + + Document + + + HELLO WORLD + + \ No newline at end of file diff --git a/test/config/fixtures/browser-custom-html/vitest.config.correct.ts b/test/config/fixtures/browser-custom-html/vitest.config.correct.ts new file mode 100644 index 000000000000..cd2dd2daf2a8 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/vitest.config.correct.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['browser-basic.test.ts'], + browser: { + name: 'chromium', + enabled: true, + headless: true, + provider: 'playwright', + testerHtmlPath: './custom-html.html' + }, + }, +}) \ No newline at end of file diff --git a/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts b/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts new file mode 100644 index 000000000000..276d5b3526d9 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts @@ -0,0 +1,40 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + { + name: 'test:html', + transformIndexHtml() { + return [ + { + tag: 'script', + injectTo: 'head-prepend', + attrs: { + type: 'importmap' + }, + children: JSON.stringify({ + "imports": { + "some-lib": "https://vitest.dev/some-lib", + }, + }) + }, + { + tag: 'script', + children: 'window.CUSTOM_INJECTED = true', + injectTo: 'head', + }, + ] + }, + }, + ], + test: { + include: ['./browser-custom.test.ts'], + browser: { + name: 'chromium', + enabled: true, + headless: true, + provider: 'playwright', + testerHtmlPath: './custom-html.html', + }, + }, +}) \ No newline at end of file diff --git a/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts b/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts new file mode 100644 index 000000000000..07597992f7e4 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts @@ -0,0 +1,39 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + { + name: 'test:html', + transformIndexHtml() { + return [ + { + tag: 'script', + injectTo: 'head-prepend', + attrs: { + type: 'importmap' + }, + children: JSON.stringify({ + "imports": { + "some-lib": "https://vitest.dev/some-lib", + }, + }) + }, + { + tag: 'script', + children: 'window.CUSTOM_INJECTED = true', + injectTo: 'head', + } + ] + }, + }, + ], + test: { + include: ['./browser-custom.test.ts'], + browser: { + name: 'chromium', + enabled: true, + headless: true, + provider: 'playwright', + }, + }, +}) \ No newline at end of file diff --git a/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts b/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts new file mode 100644 index 000000000000..d89eac4068d2 --- /dev/null +++ b/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + { + name: 'test:html', + transformIndexHtml() { + throw new Error('expected error in transformIndexHtml') + }, + }, + ], + test: { + include: ['./browser-basic.test.ts'], + browser: { + name: 'chromium', + enabled: true, + headless: true, + provider: 'playwright', + testerHtmlPath: './custom-html.html' + }, + }, +}) \ No newline at end of file diff --git a/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts b/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts new file mode 100644 index 000000000000..04f863e3a7cb --- /dev/null +++ b/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + browser: { + name: 'chromium', + enabled: true, + headless: true, + provider: 'playwright', + testerHtmlPath: './some-non-existing-path' + }, + }, +}) \ No newline at end of file diff --git a/test/config/test/browser-html.test.ts b/test/config/test/browser-html.test.ts new file mode 100644 index 000000000000..58812f856c07 --- /dev/null +++ b/test/config/test/browser-html.test.ts @@ -0,0 +1,61 @@ +import { resolve } from 'pathe' +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +const root = resolve(import.meta.dirname, '../fixtures/browser-custom-html') + +test('throws an error with non-existing path', async () => { + const { stderr, thrown } = await runVitest({ + root, + config: './vitest.config.non-existing.ts', + }, [], 'test', {}, { fails: true }) + expect(thrown).toBe(true) + expect(stderr).toContain(`Tester HTML file "${resolve(root, './some-non-existing-path')}" doesn't exist.`) +}) + +test('throws an error and exits if there is an error in the html file hook', async () => { + const { stderr, stdout, exitCode } = await runVitest({ + root, + config: './vitest.config.error-hook.ts', + }) + expect(stderr).toContain('expected error in transformIndexHtml') + // error happens when browser is opened + expect(stdout).toContain('Browser runner started by playwright') + expect(exitCode).toBe(1) +}) + +test('allows correct custom html', async () => { + const { stderr, stdout, exitCode } = await runVitest({ + root, + config: './vitest.config.correct.ts', + reporters: ['basic'], + }) + expect(stderr).toBe('') + expect(stdout).toContain('Browser runner started by playwright') + expect(stdout).toContain('✓ browser-basic.test.ts') + expect(exitCode).toBe(0) +}) + +test('allows custom transformIndexHtml with custom html file', async () => { + const { stderr, stdout, exitCode } = await runVitest({ + root, + config: './vitest.config.custom-transformIndexHtml.ts', + reporters: ['basic'], + }) + expect(stderr).toBe('') + expect(stdout).toContain('Browser runner started by playwright') + expect(stdout).toContain('✓ browser-custom.test.ts') + expect(exitCode).toBe(0) +}) + +test('allows custom transformIndexHtml without custom html file', async () => { + const { stderr, stdout, exitCode } = await runVitest({ + root, + config: './vitest.config.default-transformIndexHtml.ts', + reporters: ['basic'], + }) + expect(stderr).toBe('') + expect(stdout).toContain('Browser runner started by playwright') + expect(stdout).toContain('✓ browser-custom.test.ts') + expect(exitCode).toBe(0) +}) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 14f95d49af11..15b8c761a935 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -55,6 +55,7 @@ export async function runVitest( const cli = new Cli({ stdin, stdout, stderr }) let ctx: Vitest | undefined + let thrown = false try { const { reporters, ...rest } = config @@ -88,6 +89,7 @@ export async function runVitest( if (runnerOptions.fails !== true) { console.error(e) } + thrown = true cli.stderr += e.stack } finally { @@ -111,6 +113,7 @@ export async function runVitest( } return { + thrown, ctx, exitCode, vitest: cli,