diff --git a/examples/react-testing-lib/src/utils/test-utils.tsx b/examples/react-testing-lib/src/utils/test-utils.tsx index b4bde9c16ba..9ab25948491 100644 --- a/examples/react-testing-lib/src/utils/test-utils.tsx +++ b/examples/react-testing-lib/src/utils/test-utils.tsx @@ -1,5 +1,10 @@ /* eslint-disable import/export */ -import { render } from '@testing-library/react' +import { cleanup, render } from '@testing-library/react' +import { afterEach } from 'vitest' + +afterEach(() => { + cleanup() +}) const customRender = (ui: React.ReactElement, options = {}) => render(ui, { diff --git a/packages/vitest/src/integrations/env/happy-dom.ts b/packages/vitest/src/integrations/env/happy-dom.ts index 8817d0b5b90..deeb328181d 100644 --- a/packages/vitest/src/integrations/env/happy-dom.ts +++ b/packages/vitest/src/integrations/env/happy-dom.ts @@ -1,6 +1,6 @@ import { importModule } from 'local-pkg' import type { Environment } from '../../types' -import { getWindowKeys } from './utils' +import { populateGlobal } from './utils' export default ({ name: 'happy-dom', @@ -8,29 +8,19 @@ export default ({ // happy-dom v3 introduced a breaking change to Window, but // provides GlobalWindow as a way to use previous behaviour const { Window, GlobalWindow } = await importModule('happy-dom') as typeof import('happy-dom') - const win: any = new (GlobalWindow || Window)() + const win = new (GlobalWindow || Window)() - const keys = getWindowKeys(global, win) + const { keys, allowRewrite } = populateGlobal(global, win) - const overrideObject = new Map() - for (const key of keys) { - Object.defineProperty(global, key, { - get() { - if (overrideObject.has(key)) - return overrideObject.get(key) - return win[key] - }, - set(v) { - overrideObject.set(key, v) - }, - configurable: true, - }) - } + const originals = new Map( + allowRewrite.map(([key]) => [key, global[key]]), + ) return { teardown(global) { win.happyDOM.cancelAsync() keys.forEach(key => delete global[key]) + originals.forEach((v, k) => global[k] = v) }, } }, diff --git a/packages/vitest/src/integrations/env/jsdom.ts b/packages/vitest/src/integrations/env/jsdom.ts index 0e457b99847..5b8324ea111 100644 --- a/packages/vitest/src/integrations/env/jsdom.ts +++ b/packages/vitest/src/integrations/env/jsdom.ts @@ -1,6 +1,6 @@ import { importModule } from 'local-pkg' import type { Environment, JSDOMOptions } from '../../types' -import { getWindowKeys } from './utils' +import { populateGlobal } from './utils' export default ({ name: 'jsdom', @@ -40,26 +40,16 @@ export default ({ }, ) - const keys = getWindowKeys(global, dom.window) + const { keys, allowRewrite } = populateGlobal(global, dom.window) - const overrideObject = new Map() - for (const key of keys) { - Object.defineProperty(global, key, { - get() { - if (overrideObject.has(key)) - return overrideObject.get(key) - return dom.window[key] - }, - set(v) { - overrideObject.set(key, v) - }, - configurable: true, - }) - } + const originals = new Map( + allowRewrite.map(([key]) => [key, global[key]]), + ) return { teardown(global) { keys.forEach(key => delete global[key]) + originals.forEach((v, k) => global[k] = v) }, } }, diff --git a/packages/vitest/src/integrations/env/utils.ts b/packages/vitest/src/integrations/env/utils.ts index 07947d6a92a..cd971cfb787 100644 --- a/packages/vitest/src/integrations/env/utils.ts +++ b/packages/vitest/src/integrations/env/utils.ts @@ -1,20 +1,131 @@ import { KEYS } from './jsdom-keys' -const allowRewrite = new Set([ +const allowRewrite = [ 'Event', 'EventTarget', -]) +] + +const skipKeys = [ + 'window', + 'self', +] export function getWindowKeys(global: any, win: any) { const keys = new Set(KEYS.concat(Object.getOwnPropertyNames(win)) .filter((k) => { - if (k.startsWith('_')) + if (k.startsWith('_') || skipKeys.includes(k)) return false if (k in global) - return allowRewrite.has(k) + return allowRewrite.includes(k) return true })) return keys } + +export function populateGlobal(global: any, win: any) { + const keys = getWindowKeys(global, win) + + const overrideObject = new Map() + for (const key of keys) { + Object.defineProperty(global, key, { + get() { + if (overrideObject.has(key)) + return overrideObject.get(key) + return win[key] + }, + set(v) { + overrideObject.set(key, v) + }, + configurable: true, + }) + } + + const globalKeys = new Set(['window', 'self', 'GLOBAL', 'global']) + + globalKeys.forEach((key) => { + if (!win[key]) + return + + const proxy = new Proxy(win[key], { + get(target, p, receiver) { + if (overrideObject.has(p)) + return overrideObject.get(p) + return Reflect.get(target, p, receiver) + }, + set(target, p, value, receiver) { + try { + // if property is defined with configurable: false, + // this will throw an error, but `self.prop = value` should not throw + // this matches browser behaviour where it silently ignores the error + // and returns previously defined value, which is a hell for debugging + Object.defineProperty(global, p, { + get: () => overrideObject.get(p), + set: value => overrideObject.set(p, value), + configurable: true, + }) + overrideObject.set(p, value) + Reflect.set(target, p, value, receiver) + } + catch { + // ignore + } + return true + }, + deleteProperty(target, p) { + Reflect.deleteProperty(global, p) + overrideObject.delete(p) + return Reflect.deleteProperty(target, p) + }, + defineProperty(target, p, attributes) { + if (attributes.writable && 'value' in attributes) { + // skip - already covered by "set" + } + else if (attributes.get) { + overrideObject.delete(p) + Reflect.defineProperty(global, p, attributes) + } + return Reflect.defineProperty(target, p, attributes) + }, + }) + + Object.defineProperty(global, key, { + get() { + return proxy + }, + configurable: true, + }) + }) + + global.globalThis = new Proxy(global.globalThis, { + set(target, key, value, receiver) { + overrideObject.set(key, value) + return Reflect.set(target, key, value, receiver) + }, + deleteProperty(target, key) { + overrideObject.delete(key) + return Reflect.deleteProperty(target, key) + }, + defineProperty(target, p, attributes) { + if (attributes.writable && 'value' in attributes) { + // skip - already covered by "set" + } + else if (attributes.get && !globalKeys.has(p)) { + globalKeys.forEach((key) => { + if (win[key]) + Object.defineProperty(win[key], p, attributes) + }) + } + return Reflect.defineProperty(target, p, attributes) + }, + }) + + skipKeys.forEach(k => keys.add(k)) + + return { + keys, + skipKeys, + allowRewrite, + } +} diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 66b113d10d7..f5942488adc 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -210,11 +210,14 @@ class VitestUtils { * `IntersectionObserver`. */ public stubGlobal(name: string | symbol | number, value: any) { - // @ts-expect-error we can do anything! - globalThis[name] = value - if (globalThis.window) + if (globalThis.window) { // @ts-expect-error we can do anything! globalThis.window[name] = value + } + else { + // @ts-expect-error we can do anything! + globalThis[name] = value + } return this } diff --git a/test/core/test/dom.test.ts b/test/core/test/dom.test.ts index 8d23b413f38..49e7bafeb61 100644 --- a/test/core/test/dom.test.ts +++ b/test/core/test/dom.test.ts @@ -18,3 +18,9 @@ it('dispatchEvent doesn\'t throw', () => { const event = new Event('click') expect(() => target.dispatchEvent(event)).not.toThrow() }) + +it('Image works as expected', () => { + const img = new Image(100) + + expect(img.width).toBe(100) +}) diff --git a/test/core/test/happy-dom.test.ts b/test/core/test/happy-dom.test.ts new file mode 100644 index 00000000000..add3b597ff4 --- /dev/null +++ b/test/core/test/happy-dom.test.ts @@ -0,0 +1,89 @@ +/** + * @vitest-environment happy-dom + */ + +/* eslint-disable vars-on-top */ + +import { expect, it } from 'vitest' + +declare global { + // eslint-disable-next-line no-var + var __property: unknown +} + +it('defined on self/window are defined on global', () => { + expect(self).toBeDefined() + expect(window).toBeDefined() + + expect(self.__property).not.toBeDefined() + expect(window.__property).not.toBeDefined() + expect(globalThis.__property).not.toBeDefined() + + globalThis.__property = 'defined_value' + + expect(__property).toBe('defined_value') + expect(self.__property).toBe('defined_value') + expect(window.__property).toBe('defined_value') + expect(globalThis.__property).toBe('defined_value') + + self.__property = 'test_value' + + expect(__property).toBe('test_value') + expect(self.__property).toBe('test_value') + expect(window.__property).toBe('test_value') + expect(globalThis.__property).toBe('test_value') + + window.__property = 'new_value' + + expect(__property).toBe('new_value') + expect(self.__property).toBe('new_value') + expect(window.__property).toBe('new_value') + expect(globalThis.__property).toBe('new_value') + + globalThis.__property = 'global_value' + + expect(__property).toBe('global_value') + expect(self.__property).toBe('global_value') + expect(window.__property).toBe('global_value') + expect(globalThis.__property).toBe('global_value') + + const obj = {} + + self.__property = obj + + expect(self.__property).toBe(obj) + expect(window.__property).toBe(obj) + expect(globalThis.__property).toBe(obj) +}) + +it('usage with defineProperty', () => { + Object.defineProperty(self, '__property', { + get: () => 'self_property', + configurable: true, + }) + + expect(__property).toBe('self_property') + expect(self.__property).toBe('self_property') + expect(globalThis.__property).toBe('self_property') + expect(window.__property).toBe('self_property') + + Object.defineProperty(window, '__property', { + get: () => 'window_property', + configurable: true, + }) + + expect(__property).toBe('window_property') + expect(self.__property).toBe('window_property') + expect(globalThis.__property).toBe('window_property') + expect(window.__property).toBe('window_property') + + Object.defineProperty(globalThis, '__property', { + get: () => 'global_property', + configurable: true, + }) + + expect(__property).toBe('global_property') + expect(self.__property).toBe('global_property') + expect(globalThis.__property).toBe('global_property') + expect(window.__property).toBe('global_property') +}) diff --git a/test/core/test/jsdom.test.ts b/test/core/test/jsdom.test.ts deleted file mode 100644 index 61113287792..00000000000 --- a/test/core/test/jsdom.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import { expect, test } from 'vitest' - -test('Image works as expected', () => { - const img = new Image(100) - - expect(img.width).toBe(100) -})