Skip to content

Commit

Permalink
feat(browser): run test files in isolated iframes (#5036)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Feb 7, 2024
1 parent b607f1e commit 4f40177
Show file tree
Hide file tree
Showing 29 changed files with 808 additions and 490 deletions.
16 changes: 15 additions & 1 deletion docs/config/index.md
Expand Up @@ -1518,7 +1518,21 @@ Run the browser in a `headless` mode. If you are running Vitest in CI, it will b
- **Default:** `true`
- **CLI:** `--browser.isolate`, `--browser.isolate=false`

Isolate test environment after each test.
Run every test in a separate iframe.

### browser.fileParallelism <Badge type="info">1.3.0+</Badge>

- **Type:** `boolean`
- **Default:** the same as [`fileParallelism`](#fileparallelism-110)
- **CLI:** `--browser.fileParallelism=false`

Create all test iframes at the same time so they are running in parallel.

This makes it impossible to use interactive APIs (like clicking or hovering) because there are several iframes on the screen at the same time, but if your tests don't rely on those APIs, it might be much faster to just run all of them at the same time.

::: tip
If you disabled isolation via [`browser.isolate=false`](#browserisolate), your test files will still run one after another because of the nature of the test runner.
:::

#### browser.api

Expand Down
21 changes: 21 additions & 0 deletions packages/browser/src/client/client.ts
@@ -0,0 +1,21 @@
import type { CancelReason } from '@vitest/runner'
import { createClient } from '@vitest/ws-client'

export const PORT = import.meta.hot ? '51204' : location.port
export const HOST = [location.hostname, PORT].filter(Boolean).join(':')
export const ENTRY_URL = `${
location.protocol === 'https:' ? 'wss:' : 'ws:'
}//${HOST}/__vitest_api__`

let setCancel = (_: CancelReason) => {}
export const onCancel = new Promise<CancelReason>((resolve) => {
setCancel = resolve
})

export const client = createClient(ENTRY_URL, {
handlers: {
onCancel: setCancel,
},
})

export const channel = new BroadcastChannel('vitest')
54 changes: 3 additions & 51 deletions packages/browser/src/client/index.html
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="{__VITEST_FAVICON__}" type="image/svg+xml">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vitest Browser Runner</title>
<style>
Expand All @@ -21,59 +21,11 @@
border: none;
}
</style>
<script>{__VITEST_INJECTOR__}</script>
</head>
<body>
<iframe id="vitest-ui" src=""></iframe>
<script>
const moduleCache = new Map()

// this method receives a module object or "import" promise that it resolves and keeps track of
// and returns a hijacked module object that can be used to mock module exports
function wrapModule(module) {
if (module instanceof Promise) {
moduleCache.set(module, { promise: module, evaluated: false })
return module
// TODO: add a test
.then(m => '__vi_inject__' in m ? m.__vi_inject__ : m)
.finally(() => moduleCache.delete(module))
}
return '__vi_inject__' in module ? module.__vi_inject__ : module
}

function exportAll(exports, sourceModule) {
// #1120 when a module exports itself it causes
// call stack error
if (exports === sourceModule)
return

if (Object(sourceModule) !== sourceModule || Array.isArray(sourceModule))
return

for (const key in sourceModule) {
if (key !== 'default') {
try {
Object.defineProperty(exports, key, {
enumerable: true,
configurable: true,
get: () => sourceModule[key],
})
}
catch (_err) { }
}
}
}

window.__vi_export_all__ = exportAll

// TODO: allow easier rewriting of import.meta.env
window.__vi_import_meta__ = {
env: {},
url: location.href,
}

window.__vi_module_cache__ = moduleCache
window.__vi_wrap_module__ = wrapModule
</script>
<script type="module" src="/main.ts"></script>
<div id="vitest-tester"></div>
</body>
</html>
4 changes: 2 additions & 2 deletions packages/browser/src/client/logger.ts
Expand Up @@ -3,8 +3,8 @@ import { importId } from './utils'

const { Date, console } = globalThis

export async function setupConsoleLogSpy(basePath: string) {
const { stringify, format, inspect } = await importId('vitest/utils', basePath) as typeof import('vitest/utils')
export async function setupConsoleLogSpy() {
const { stringify, format, inspect } = await importId('vitest/utils') as typeof import('vitest/utils')
const { log, info, error, dir, dirxml, trace, time, timeEnd, timeLog, warn, debug, count, countReset } = console
const formatInput = (input: unknown) => {
if (input instanceof Node)
Expand Down

0 comments on commit 4f40177

Please sign in to comment.