-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
chore: migrate jest-environment-jsdom to TypeScript #8003
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
Changes from 25 commits
c857e4c
0cfe10b
57a3567
9b279a6
2c3afe4
ae3c45c
bd4e070
71048f6
cc4eacb
b3effe8
9cf040b
a40ea8d
b215884
0a9e4c4
4e8e638
3c4486f
d50560b
3968bc4
6105073
5ffc083
f45cc88
1f9081a
f1ed385
e162e48
eebb6f8
a743367
3beb007
5fb6bfd
58fc28b
c7c84f7
06539b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,146 @@ | ||||||
| /** | ||||||
| * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. | ||||||
| * | ||||||
| * This source code is licensed under the MIT license found in the | ||||||
| * LICENSE file in the root directory of this source tree. | ||||||
| */ | ||||||
|
|
||||||
| import {Script} from 'vm'; | ||||||
| import {Global, Config} from '@jest/types'; | ||||||
| import mock, {ModuleMocker} from 'jest-mock'; | ||||||
| import {installCommonGlobals} from 'jest-util'; | ||||||
| import {JestFakeTimers as FakeTimers} from '@jest/fake-timers'; | ||||||
| import {JestEnvironment, EnvironmentContext} from '@jest/environment'; | ||||||
| import {JSDOM, VirtualConsole} from 'jsdom'; | ||||||
|
|
||||||
| // The `Window` interface does not have an `Error.stackTraceLimit` property, but | ||||||
| // `JSDOMEnvironment` assumes it is there. | ||||||
| interface Win extends Window { | ||||||
| Error: { | ||||||
| stackTraceLimit: number; | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| function isWin(globals: Win | Global.Global): globals is Win { | ||||||
| return (globals as Win).document !== undefined; | ||||||
| } | ||||||
|
|
||||||
| class JSDOMEnvironment implements JestEnvironment { | ||||||
| dom: JSDOM | null; | ||||||
| fakeTimers: FakeTimers<number> | null; | ||||||
| // @ts-ignore | ||||||
| global: Global.Global | Win | null; | ||||||
| errorEventListener: ((event: Event & {error: unknown}) => void) | null; | ||||||
| moduleMocker: ModuleMocker | null; | ||||||
|
|
||||||
| constructor(config: Config.ProjectConfig, options: EnvironmentContext = {}) { | ||||||
| this.dom = new JSDOM('<!DOCTYPE html>', { | ||||||
| pretendToBeVisual: true, | ||||||
| runScripts: 'dangerously', | ||||||
| url: config.testURL, | ||||||
| virtualConsole: new VirtualConsole().sendTo(options.console || console), | ||||||
| ...config.testEnvironmentOptions, | ||||||
| }); | ||||||
|
|
||||||
| // `defaultView` returns a `Window` type, which is missing the | ||||||
| // `Error.stackTraceLimit` property. See the `Win` interface above. | ||||||
| const global = (this.global = this.dom.window.document | ||||||
| .defaultView as Win | null); | ||||||
SimenB marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
|
|
||||||
| if (!global || !this.global) { | ||||||
SimenB marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| this.fakeTimers = null; | ||||||
| this.errorEventListener = null; | ||||||
| this.moduleMocker = null; | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // 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; | ||||||
| // `global` is of type `Win`, but `installCommonGlobals` expects | ||||||
| // `NodeJS.Global`, so using `any` for now | ||||||
| installCommonGlobals(global as any, config.globals); | ||||||
|
|
||||||
| // Report uncaught errors. | ||||||
| this.errorEventListener = event => { | ||||||
| if (userErrorListenerCount === 0 && event.error) { | ||||||
| (process as NodeJS.EventEmitter).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; | ||||||
| const originalRemoveListener = global.removeEventListener; | ||||||
| let userErrorListenerCount = 0; | ||||||
| global.addEventListener = function(name: string) { | ||||||
| if (name === 'error') { | ||||||
| userErrorListenerCount++; | ||||||
| } | ||||||
| // TODO: remove `any` type assertion | ||||||
| return originalAddListener.apply(this, arguments as any); | ||||||
| }; | ||||||
| global.removeEventListener = function(name: string) { | ||||||
| if (name === 'error') { | ||||||
| userErrorListenerCount--; | ||||||
| } | ||||||
| // TODO: remove `any` type assertion | ||||||
| return originalRemoveListener.apply(this, arguments as any); | ||||||
| }; | ||||||
|
|
||||||
| // `global` is of type `Win`, but `ModuleMocker` expects `NodeJS.Global`, so | ||||||
| // using `any` for now | ||||||
| this.moduleMocker = new mock.ModuleMocker(global as any); | ||||||
|
|
||||||
| const timerConfig = { | ||||||
| idToRef: (id: number) => id, | ||||||
| refToId: (ref: number) => ref, | ||||||
| }; | ||||||
|
|
||||||
| this.fakeTimers = new FakeTimers({ | ||||||
| config, | ||||||
| // `global` is of type `Win`, but `FakeTimers` expects `NodeJS.Global`, so | ||||||
| // using `any` for now | ||||||
| global: global as any, | ||||||
| moduleMocker: this.moduleMocker, | ||||||
| timerConfig, | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| setup() { | ||||||
| return Promise.resolve(); | ||||||
| } | ||||||
|
|
||||||
| teardown() { | ||||||
| if (this.fakeTimers) { | ||||||
| this.fakeTimers.dispose(); | ||||||
| } | ||||||
| if (this.global) { | ||||||
| if (this.errorEventListener && isWin(this.global)) { | ||||||
| this.global.removeEventListener('error', this.errorEventListener); | ||||||
| } | ||||||
| // Dispose "document" to prevent "load" event from triggering. | ||||||
| Object.defineProperty(this.global, 'document', {value: null}); | ||||||
| if (isWin(this.global)) { | ||||||
| this.global.close(); | ||||||
| } | ||||||
| } | ||||||
| this.errorEventListener = null; | ||||||
| this.global = null; | ||||||
| this.dom = null; | ||||||
| this.fakeTimers = null; | ||||||
| return Promise.resolve(); | ||||||
| } | ||||||
|
|
||||||
| runScript(script: Script) { | ||||||
| if (this.dom) { | ||||||
| // Explicitly returning `unknown` since `runVMScript` currently returns | ||||||
| // `void`, which is wrong | ||||||
| return this.dom.runVMScript(script) as unknown; | ||||||
|
||||||
| return this.dom.runVMScript(script) as unknown; | |
| return this.dom.runVMScript(script) as any; |
We know what the type here will be, no need to assert it.
(it'll have the EVAL_RESULT_VARIABLE object thing due to https://github.com/facebook/jest/blob/438a178c8addf2806bebf44726d080921ad9984f/packages/jest-transform/src/ScriptTransformer.ts#L559-L565)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do need to assert it since runVMScript() returns void, which is not true, and runScript() is also expected to receive {[ScriptTransformer.EVAL_RESULT_VARIABLE]: ModuleWrapper} | null (via the JestEnvironment interface). I have a new solution for this, committing in a sec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We know what the type here will be
This contrasts with #8003 (comment) - we can't both set the return type to the EVAL_RESULT_VARIABLE thing, AND the return value of the provided script... Sorry, I'm a bit confused here.
If we want the return type of runScript to be the the same as the return type of the provided script, we could use a generic to let the user of jest-environment-jsdom set the return type? Just an idea. But I feel there is something I am missing here... :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value of the provided script is, 100%, the EVAL_RESULT_VARIABLE thing (unless the environment has been torn down, in which cse it will be null). We construct the script ourselves. The function assigned to EVAL_RESULT_VARIABLE, we do not know what will return when invoked (that's the unknown part), but we don't care about that result
If we want the return type of
runScriptto be the the same as the return type of the provided script, we could use a generic to let the user ofjest-environment-jsdomset the return type? Just an idea. But I feel there is something I am missing here... :)
I don't think that's needed - the script we get is constructed from a string source we've read (and potentially transformed) from disk. So we cannot statically know what it will return
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "extends": "../../tsconfig.json", | ||
| "compilerOptions": { | ||
| "outDir": "build", | ||
| "rootDir": "src" | ||
| }, | ||
| "references": [ | ||
| {"path": "../jest-environment"}, | ||
| {"path": "../jest-fake-timers"}, | ||
| {"path": "../jest-mock"}, | ||
| {"path": "../jest-types"}, | ||
| {"path": "../jest-util"} | ||
| ] | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.