Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser): allow custom HTML path, respect plugins transformIndexHtml #6725

Merged
merged 10 commits into from
Oct 25, 2024
Merged
8 changes: 8 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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? }`
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default () =>
input: './src/client/tester/state.ts',
output: {
file: 'dist/state.js',
format: 'esm',
format: 'iife',
},
plugins: [
esbuild({
Expand Down
98 changes: 50 additions & 48 deletions packages/browser/src/client/public/esm-client-injector.js
Original file line number Diff line number Diff line change
@@ -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]);
}
})();
7 changes: 1 addition & 6 deletions packages/browser/src/client/tester/tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="{__VITEST_FAVICON__}" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{__VITEST_TITLE__}</title>
<title>Vitest Browser Tester</title>
<style>
html {
padding: 0;
Expand All @@ -16,13 +16,8 @@
min-height: 100vh;
}
</style>
{__VITEST_INJECTOR__}
<script>{__VITEST_STATE__}</script>
{__VITEST_INTERNAL_SCRIPTS__}
{__VITEST_SCRIPTS__}
</head>
<body>
<script type="module" src="./tester.ts"></script>
{__VITEST_APPEND__}
</body>
</html>
105 changes: 102 additions & 3 deletions packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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() {
Expand Down
3 changes: 1 addition & 2 deletions packages/browser/src/node/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})

Expand Down
41 changes: 31 additions & 10 deletions packages/browser/src/node/server.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'
Expand All @@ -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> | Vite.Manifest
public testerHtml: Promise<string> | string
public testerFilepath: string
public orchestratorHtml: Promise<string> | string
public injectorJs: Promise<string> | string
public errorCatcherUrl: string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -124,24 +134,35 @@ 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<HtmlTagDescriptor> => {
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)
const contentProcessed
= content && type === 'module'
? (await server.pluginContainer.transform(content, transformId)).code
: content
return `<script type="${type}"${async ? ' async' : ''}${
srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ''
}>${contentProcessed || ''}</script>`
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() {
Expand Down
11 changes: 9 additions & 2 deletions packages/browser/src/node/serverOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<script '
for (const attr in script.attrs || {}) {
html += `${attr}="${script.attrs![attr]}" `
}
html += `>${script.children}</script>`
return html
}).join('\n')
}

let baseHtml = typeof server.orchestratorHtml === 'string'
Expand Down
Loading