UNPKG

@happy-dom/jest-environment

Version:
190 lines (165 loc) 5.94 kB
/* eslint-disable filenames/match-exported */ import VM from 'vm'; import * as JestUtil from 'jest-util'; import { ModuleMocker } from 'jest-mock'; import { LegacyFakeTimers, ModernFakeTimers } from '@jest/fake-timers'; import { JestEnvironment, EnvironmentContext } from '@jest/environment'; import { Window, BrowserErrorCaptureEnum, IOptionalBrowserSettings } from 'happy-dom'; import { Script } from 'vm'; import { Global, Config } from '@jest/types'; /** * Happy DOM Jest Environment. */ export default class HappyDOMEnvironment implements JestEnvironment { public fakeTimers: LegacyFakeTimers<number> | null = null; public fakeTimersModern: ModernFakeTimers | null = null; public window: Window; public global: Global.Global; public moduleMocker: ModuleMocker; /** * jest-environment-jsdom" has the default set to ['browser'] * As changing this value would be a breaking change, we will keep it at ['node', 'node-addons'] until we do a major release * * @see https://stackoverflow.com/questions/72428323/jest-referenceerror-vue-is-not-defined */ public customExportConditions = ['node', 'node-addons']; private _configuredExportConditions: string[]; /** * Constructor. * * @param config Jest config. * @param config.globalConfig jest global config. * @param config.projectConfig jest project config. * @param options Options. */ constructor( config: | { globalConfig: Config.GlobalConfig; projectConfig: Config.ProjectConfig } | Config.ProjectConfig, options?: EnvironmentContext ) { let projectConfig: Config.ProjectConfig; let globals: Config.ConfigGlobals; if (isJestConfigVersion29(config)) { // Jest 29 globals = config.globals; projectConfig = config; } else if (isJestConfigVersion28(config)) { // Jest < 29 globals = config.projectConfig.globals; projectConfig = config.projectConfig; } else { throw new Error('Unsupported jest version.'); } this._configuredExportConditions = []; if ('customExportConditions' in projectConfig.testEnvironmentOptions) { const { customExportConditions } = projectConfig.testEnvironmentOptions; if ( Array.isArray(customExportConditions) && customExportConditions.every((condition) => typeof condition === 'string') ) { this._configuredExportConditions = customExportConditions; } else { throw new Error('Custom export conditions specified but they are not an array of strings'); } } // Initialize Window and Global this.window = new Window({ url: 'http://localhost/', ...projectConfig.testEnvironmentOptions, console: options?.console || console, settings: { ...(<IOptionalBrowserSettings>projectConfig.testEnvironmentOptions?.settings), errorCapture: BrowserErrorCaptureEnum.disabled } }); this.global = <Global.Global>(<unknown>this.window); this.moduleMocker = new ModuleMocker(<typeof globalThis>(<unknown>this.window)); // Node's error-message stack size is limited to 10, but it's pretty useful to see more than that when a test fails. this.global.Error.stackTraceLimit = 100; // TODO: Remove this ASAP as it currently causes tests to run really slow. this.global.Buffer = Buffer; // Needed as Jest is using it this.window['global'] = this.global; JestUtil.installCommonGlobals(<typeof globalThis>(<unknown>this.window), globals); // For some reason Jest removes the global setImmediate, so we need to add it back. this.global.setImmediate = global.setImmediate; this.fakeTimers = new LegacyFakeTimers({ config: projectConfig, global: <typeof globalThis>(<unknown>this.window), moduleMocker: this.moduleMocker, timerConfig: { idToRef: (id: number) => id, refToId: (ref: number) => ref } }); this.fakeTimersModern = new ModernFakeTimers({ config: projectConfig, global: <typeof globalThis>(<unknown>this.window) }); // Jest is using the setTimeout function from Happy DOM internally for detecting when a test times out, but this causes window.happyDOM?.waitUntilComplete() and window.happyDOM?.abort() to not work as expected. // Hopefully Jest can fix this in the future as this fix is not very pretty. const happyDOMSetTimeout = this.global.setTimeout; (<(...args: unknown[]) => number>this.global.setTimeout) = (...args: unknown[]): number => { if (new Error('stack').stack!.includes('/jest-jasmine')) { // @ts-ignore return global.setTimeout.call(global, ...args); } // @ts-ignore return happyDOMSetTimeout.call(this.global, ...args); }; } /** * Respect any export conditions specified as options * https://jestjs.io/docs/configuration#testenvironmentoptions-object */ public exportConditions(): string[] { return this._configuredExportConditions ?? this.customExportConditions; } /** * Setup. * * @returns Promise. */ public async setup(): Promise<void> {} /** * Teardown. * * @returns Promise. */ public async teardown(): Promise<void> { this.fakeTimers!.dispose(); this.fakeTimersModern!.dispose(); await (<Window>(<unknown>this.global)).happyDOM.abort(); (<Window>(<unknown>this.global)).close(); this.global = null!; this.moduleMocker = null!; this.fakeTimers = null; this.fakeTimersModern = null; } /** * Runs a script. * * @param script Script. * @returns Result. */ public runScript(script: Script): null { return script.runInContext(this.global); } /** * Returns the VM context. * * @returns Context. */ public getVmContext(): VM.Context { return this.global; } } function isJestConfigVersion29(config: unknown): config is Config.ProjectConfig { return Object.getOwnPropertyDescriptor(config, 'globals') !== undefined; } function isJestConfigVersion28( config: unknown ): config is { globalConfig: Config.GlobalConfig; projectConfig: Config.ProjectConfig } { return Object.getOwnPropertyDescriptor(config, 'projectConfig') !== undefined; }