Skip to content

Commit

Permalink
feat: add custom vitest environment for jsdom
Browse files Browse the repository at this point in the history
  • Loading branch information
blurfx committed Oct 12, 2023
1 parent ea879a2 commit c8eb684
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 2 deletions.
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@microsoft/api-extractor": "^7.19.4",
"@types/benchmark": "^2.1.1",
"@types/google-protobuf": "^3.15.5",
"@types/jsdom": "^21.1.3",
"@types/long": "^4.0.1",
"@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.30.6",
Expand Down Expand Up @@ -75,6 +76,7 @@
"vite": "^4.4.9",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^0.34.5",
"vitest-environment-custom-jsdom": "file:test/vitest/env",
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-cli": "^4.10.0",
Expand Down
154 changes: 154 additions & 0 deletions test/vitest/env/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { CookieJar, JSDOM, ResourceLoader, VirtualConsole } from 'jsdom';
import { Environment } from 'vitest'
import { populateGlobal } from 'vitest/environments'

function catchWindowErrors(window: Window) {
let userErrorListenerCount = 0
function throwUnhandlerError(e: ErrorEvent) {
if (userErrorListenerCount === 0 && e.error != null)
process.emit('uncaughtException', e.error)
}
const addEventListener = window.addEventListener.bind(window)
const removeEventListener = window.removeEventListener.bind(window)
window.addEventListener('error', throwUnhandlerError)
window.addEventListener = function (...args: Parameters<typeof addEventListener>) {
if (args[0] === 'error')
userErrorListenerCount++
return addEventListener.apply(this, args)
}
window.removeEventListener = function (...args: Parameters<typeof removeEventListener>) {
if (args[0] === 'error' && userErrorListenerCount)
userErrorListenerCount--
return removeEventListener.apply(this, args)
}
return function clearErrorHandlers() {
window.removeEventListener('error', throwUnhandlerError)
}
}

const ALLOWED_KEYS = [
'Uint8Array'
]

Check failure on line 31 in test/vitest/env/index.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Variable name `ALLOWED_KEYS` must match one of the following formats: camelCase, PascalCase

export default <Environment>({
name: 'jsdom',
transformMode: 'web',
async setupVM({ jsdom = {} }) {
const {
html = '<!DOCTYPE html>',
userAgent,
url = 'http://localhost:3000',
contentType = 'text/html',
pretendToBeVisual = true,
includeNodeLocations = false,
runScripts = 'dangerously',
resources,
console = false,
cookieJar = false,
...restOptions
} = jsdom as any
const dom = new JSDOM(
html,
{
pretendToBeVisual,
resources: resources ?? (userAgent ? new ResourceLoader({ userAgent }) : undefined),
runScripts,
url,
virtualConsole: (console && globalThis.console) ? new VirtualConsole().sendTo(globalThis.console) : undefined,
cookieJar: cookieJar ? new CookieJar() : undefined,
includeNodeLocations,
contentType,
userAgent,
...restOptions,
},
)
const clearWindowErrors = catchWindowErrors(dom.window as any)

// TODO: browser doesn't expose Buffer, but a lot of dependencies use it
dom.window.Buffer = Buffer

// inject web globals if they missing in JSDOM but otherwise available in Nodejs
// https://nodejs.org/dist/latest/docs/api/globals.html
const globalNames = [
'structuredClone',
'fetch',
'Request',
'Response',
'BroadcastChannel',
'MessageChannel',
'MessagePort',
] as const
for (const name of globalNames) {
const value = globalThis[name]
if (
typeof value !== 'undefined'
&& typeof dom.window[name] === 'undefined'
)
dom.window[name] = value
}

return {
getVmContext() {
return dom.getInternalVMContext()
},
teardown() {
clearWindowErrors()
dom.window.close()
},
}
},
async setup(global, { jsdom = {} }) {
const {
CookieJar,
JSDOM,
ResourceLoader,
VirtualConsole,
} = await require('jsdom') as typeof import('jsdom')
const {
html = '<!DOCTYPE html>',
userAgent,
url = 'http://localhost:3000',
contentType = 'text/html',
pretendToBeVisual = true,
includeNodeLocations = false,
runScripts = 'dangerously',
resources,
console = false,
cookieJar = false,
...restOptions
} = jsdom as any
const dom = new JSDOM(
html,
{
pretendToBeVisual,
resources: resources ?? (userAgent ? new ResourceLoader({ userAgent }) : undefined),
runScripts,
url,
virtualConsole: (console && global.console) ? new VirtualConsole().sendTo(global.console) : undefined,
cookieJar: cookieJar ? new CookieJar() : undefined,
includeNodeLocations,
contentType,
userAgent,
...restOptions,
},
)

const { keys, originals } = populateGlobal(global, dom.window, { bindFunctions: true });

ALLOWED_KEYS.forEach((key) => {
delete global[key];
global[key] = originals.get(key);
});

const clearWindowErrors = catchWindowErrors(global)

return {
teardown(global) {
clearWindowErrors()
dom.window.close()
keys.forEach(key => delete global[key])
originals.forEach((v, k) => global[k] = v)
},
}
},
})
10 changes: 10 additions & 0 deletions test/vitest/env/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions test/vitest/env/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "vitest-environment-custom-jsdom",
"private": true,
"main": "index.ts"
}
4 changes: 2 additions & 2 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export default defineConfig({
provider: 'istanbul',
reporter: ['lcov', 'text-summary'],
},
environment: 'jsdom',
environment: 'custom-jsdom',
globals: true,
singleThread: true,
testTimeout: 15000,
testTimeout: 5000,
},
plugins: [tsconfigPaths()],
});

0 comments on commit c8eb684

Please sign in to comment.