Skip to content

Commit

Permalink
feat: self/window/global share state with globalThis (#1256)
Browse files Browse the repository at this point in the history
* feat: self/window/global share state with globalThis

* chore: fix infinite loop in --no-threads

* chore: add cleanup function to test-utils
  • Loading branch information
sheremet-va committed May 7, 2022
1 parent 34e177f commit fbd7974
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 52 deletions.
7 changes: 6 additions & 1 deletion 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, {
Expand Down
24 changes: 7 additions & 17 deletions packages/vitest/src/integrations/env/happy-dom.ts
@@ -1,36 +1,26 @@
import { importModule } from 'local-pkg'
import type { Environment } from '../../types'
import { getWindowKeys } from './utils'
import { populateGlobal } from './utils'

export default <Environment>({
name: 'happy-dom',
async setup(global) {
// 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<string, any>()
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<string | symbol, any>(
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)
},
}
},
Expand Down
22 changes: 6 additions & 16 deletions 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 <Environment>({
name: 'jsdom',
Expand Down Expand Up @@ -40,26 +40,16 @@ export default <Environment>({
},
)

const keys = getWindowKeys(global, dom.window)
const { keys, allowRewrite } = populateGlobal(global, dom.window)

const overrideObject = new Map<string, any>()
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<string | symbol, any>(
allowRewrite.map(([key]) => [key, global[key]]),
)

return {
teardown(global) {
keys.forEach(key => delete global[key])
originals.forEach((v, k) => global[k] = v)
},
}
},
Expand Down
119 changes: 115 additions & 4 deletions 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<string | symbol, any>()
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<string | symbol>(['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,
}
}
9 changes: 6 additions & 3 deletions packages/vitest/src/integrations/vi.ts
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions test/core/test/dom.test.ts
Expand Up @@ -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)
})
89 changes: 89 additions & 0 deletions 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')
})
11 changes: 0 additions & 11 deletions test/core/test/jsdom.test.ts

This file was deleted.

0 comments on commit fbd7974

Please sign in to comment.