-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce helper package for implementing a custom JSDOM enviro…
…ment (#14717)
- Loading branch information
Showing
10 changed files
with
295 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
**/__mocks__/** | ||
**/__tests__/** | ||
__typetests__ | ||
src | ||
tsconfig.json | ||
tsconfig.tsbuildinfo | ||
api-extractor.json | ||
.eslintcache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"name": "@jest/environment-jsdom-abstract", | ||
"version": "30.0.0-alpha.2", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/jestjs/jest.git", | ||
"directory": "packages/jest-environment-jsdom-abstract" | ||
}, | ||
"license": "MIT", | ||
"main": "./build/index.js", | ||
"types": "./build/index.d.ts", | ||
"exports": { | ||
".": { | ||
"types": "./build/index.d.ts", | ||
"require": "./build/index.js", | ||
"import": "./build/index.mjs", | ||
"default": "./build/index.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
"dependencies": { | ||
"@jest/environment": "workspace:*", | ||
"@jest/fake-timers": "workspace:*", | ||
"@jest/types": "workspace:*", | ||
"@types/jsdom": "^21.1.1", | ||
"@types/node": "*", | ||
"jest-mock": "workspace:*", | ||
"jest-util": "workspace:*" | ||
}, | ||
"devDependencies": { | ||
"jsdom": "^22.0.0" | ||
}, | ||
"peerDependencies": { | ||
"canvas": "^2.5.0", | ||
"jsdom": "*" | ||
}, | ||
"peerDependenciesMeta": { | ||
"canvas": { | ||
"optional": true | ||
} | ||
}, | ||
"engines": { | ||
"node": "^16.10.0 || ^18.12.0 || >=20.0.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import type {Context} from 'vm'; | ||
import type * as jsdom from 'jsdom'; | ||
import type { | ||
EnvironmentContext, | ||
JestEnvironment, | ||
JestEnvironmentConfig, | ||
} from '@jest/environment'; | ||
import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers'; | ||
import type {Global} from '@jest/types'; | ||
import {ModuleMocker} from 'jest-mock'; | ||
import {installCommonGlobals} from 'jest-util'; | ||
|
||
// The `Window` interface does not have an `Error.stackTraceLimit` property, but | ||
// `JSDOMEnvironment` assumes it is there. | ||
type Win = Window & | ||
Global.Global & { | ||
Error: { | ||
stackTraceLimit: number; | ||
}; | ||
}; | ||
|
||
function isString(value: unknown): value is string { | ||
return typeof value === 'string'; | ||
} | ||
|
||
export default abstract class BaseJSDOMEnvironment | ||
implements JestEnvironment<number> | ||
{ | ||
dom: jsdom.JSDOM | null; | ||
fakeTimers: LegacyFakeTimers<number> | null; | ||
fakeTimersModern: ModernFakeTimers | null; | ||
global: Win; | ||
private errorEventListener: ((event: Event & {error: Error}) => void) | null; | ||
moduleMocker: ModuleMocker | null; | ||
customExportConditions = ['browser']; | ||
private readonly _configuredExportConditions?: Array<string>; | ||
|
||
protected constructor( | ||
config: JestEnvironmentConfig, | ||
context: EnvironmentContext, | ||
jsdomModule: typeof jsdom, | ||
) { | ||
const {projectConfig} = config; | ||
|
||
const {JSDOM, ResourceLoader, VirtualConsole} = jsdomModule; | ||
|
||
const virtualConsole = new VirtualConsole(); | ||
virtualConsole.sendTo(context.console, {omitJSDOMErrors: true}); | ||
virtualConsole.on('jsdomError', error => { | ||
context.console.error(error); | ||
}); | ||
|
||
this.dom = new JSDOM( | ||
typeof projectConfig.testEnvironmentOptions.html === 'string' | ||
? projectConfig.testEnvironmentOptions.html | ||
: '<!DOCTYPE html>', | ||
{ | ||
pretendToBeVisual: true, | ||
resources: | ||
typeof projectConfig.testEnvironmentOptions.userAgent === 'string' | ||
? new ResourceLoader({ | ||
userAgent: projectConfig.testEnvironmentOptions.userAgent, | ||
}) | ||
: undefined, | ||
runScripts: 'dangerously', | ||
url: 'http://localhost/', | ||
virtualConsole, | ||
...projectConfig.testEnvironmentOptions, | ||
}, | ||
); | ||
const global = (this.global = this.dom.window as unknown as Win); | ||
|
||
if (global == null) { | ||
throw new Error('JSDOM did not return a Window object'); | ||
} | ||
|
||
// TODO: remove at some point - for "universal" code (code should use `globalThis`) | ||
global.global = global; | ||
|
||
// Node's error-message stack size is limited at 10, but it's pretty useful | ||
// to see more than that when a test fails. | ||
this.global.Error.stackTraceLimit = 100; | ||
installCommonGlobals(global, projectConfig.globals); | ||
|
||
// TODO: remove this ASAP, but it currently causes tests to run really slow | ||
global.Buffer = Buffer; | ||
|
||
// Report uncaught errors. | ||
this.errorEventListener = event => { | ||
if (userErrorListenerCount === 0 && event.error != null) { | ||
process.emit('uncaughtException', event.error); | ||
} | ||
}; | ||
global.addEventListener('error', this.errorEventListener); | ||
|
||
// However, don't report them as uncaught if the user listens to 'error' event. | ||
// In that case, we assume the might have custom error handling logic. | ||
const originalAddListener = global.addEventListener.bind(global); | ||
const originalRemoveListener = global.removeEventListener.bind(global); | ||
let userErrorListenerCount = 0; | ||
global.addEventListener = function ( | ||
...args: Parameters<typeof originalAddListener> | ||
) { | ||
if (args[0] === 'error') { | ||
userErrorListenerCount++; | ||
} | ||
return originalAddListener.apply(this, args); | ||
}; | ||
global.removeEventListener = function ( | ||
...args: Parameters<typeof originalRemoveListener> | ||
) { | ||
if (args[0] === 'error') { | ||
userErrorListenerCount--; | ||
} | ||
return originalRemoveListener.apply(this, args); | ||
}; | ||
|
||
if ('customExportConditions' in projectConfig.testEnvironmentOptions) { | ||
const {customExportConditions} = projectConfig.testEnvironmentOptions; | ||
if ( | ||
Array.isArray(customExportConditions) && | ||
customExportConditions.every(isString) | ||
) { | ||
this._configuredExportConditions = customExportConditions; | ||
} else { | ||
throw new Error( | ||
'Custom export conditions specified but they are not an array of strings', | ||
); | ||
} | ||
} | ||
|
||
this.moduleMocker = new ModuleMocker(global); | ||
|
||
this.fakeTimers = new LegacyFakeTimers({ | ||
config: projectConfig, | ||
global: global as unknown as typeof globalThis, | ||
moduleMocker: this.moduleMocker, | ||
timerConfig: { | ||
idToRef: (id: number) => id, | ||
refToId: (ref: number) => ref, | ||
}, | ||
}); | ||
|
||
this.fakeTimersModern = new ModernFakeTimers({ | ||
config: projectConfig, | ||
global: global as unknown as typeof globalThis, | ||
}); | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
async setup(): Promise<void> {} | ||
|
||
async teardown(): Promise<void> { | ||
if (this.fakeTimers) { | ||
this.fakeTimers.dispose(); | ||
} | ||
if (this.fakeTimersModern) { | ||
this.fakeTimersModern.dispose(); | ||
} | ||
if (this.global != null) { | ||
if (this.errorEventListener) { | ||
this.global.removeEventListener('error', this.errorEventListener); | ||
} | ||
this.global.close(); | ||
} | ||
this.errorEventListener = null; | ||
// @ts-expect-error: this.global not allowed to be `null` | ||
this.global = null; | ||
this.dom = null; | ||
this.fakeTimers = null; | ||
this.fakeTimersModern = null; | ||
} | ||
|
||
exportConditions(): Array<string> { | ||
return this._configuredExportConditions ?? this.customExportConditions; | ||
} | ||
|
||
getVmContext(): Context | null { | ||
if (this.dom) { | ||
return this.dom.getInternalVMContext(); | ||
} | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "build", | ||
"rootDir": "src" | ||
}, | ||
"include": ["./src/**/*"], | ||
"exclude": ["./**/__mocks__/**/*", "./**/__tests__/**/*"], | ||
"references": [ | ||
{"path": "../jest-environment"}, | ||
{"path": "../jest-fake-timers"}, | ||
{"path": "../jest-mock"}, | ||
{"path": "../jest-types"}, | ||
{"path": "../jest-util"} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.