Skip to content

Commit

Permalink
feat(browser): allow custom HTML path, respect plugins `transformInde…
Browse files Browse the repository at this point in the history
…xHtml` (#6725)
  • Loading branch information
sheremet-va authored Oct 25, 2024
1 parent 5df7414 commit 169028f
Show file tree
Hide file tree
Showing 22 changed files with 450 additions and 102 deletions.
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

0 comments on commit 169028f

Please sign in to comment.