Skip to content

Commit

Permalink
fix: simplify globals access, fix global != global (#1340)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed May 20, 2022
1 parent 7b56822 commit d32a758
Show file tree
Hide file tree
Showing 5 changed files with 28 additions and 98 deletions.
6 changes: 1 addition & 5 deletions packages/vitest/src/integrations/env/happy-dom.ts
Expand Up @@ -10,11 +10,7 @@ export default <Environment>({
const { Window, GlobalWindow } = await importModule('happy-dom') as typeof import('happy-dom')
const win = new (GlobalWindow || Window)()

const { keys, allowRewrite } = populateGlobal(global, win, { bindFunctions: true })

const originals = new Map<string | symbol, any>(
allowRewrite.map(([key]) => [key, global[key]]),
)
const { keys, originals } = populateGlobal(global, win, { bindFunctions: true })

return {
teardown(global) {
Expand Down
6 changes: 1 addition & 5 deletions packages/vitest/src/integrations/env/jsdom.ts
Expand Up @@ -40,11 +40,7 @@ export default <Environment>({
},
)

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

const originals = new Map<string | symbol, any>(
allowRewrite.map(([key]) => [key, global[key]]),
)
const { keys, originals } = populateGlobal(global, dom.window, { bindFunctions: true })

return {
teardown(global) {
Expand Down
100 changes: 12 additions & 88 deletions packages/vitest/src/integrations/env/utils.ts
Expand Up @@ -34,15 +34,19 @@ export function populateGlobal(global: any, win: any, options: PopulateOptions =
const { bindFunctions = false } = options
const keys = getWindowKeys(global, win)

const originals = new Map<string | symbol, any>(
allowRewrite.map(([key]) => [key, global[key]]),
)

const overrideObject = new Map<string | symbol, any>()
for (const key of keys) {
const shouldBind = bindFunctions && typeof win[key] === 'function'
const bindedFunction = bindFunctions && typeof win[key] === 'function' && win[key].bind(win)
Object.defineProperty(global, key, {
get() {
if (overrideObject.has(key))
return overrideObject.get(key)
if (shouldBind)
return win[key].bind(win)
if (bindedFunction)
return bindedFunction
return win[key]
},
set(v) {
Expand All @@ -52,98 +56,18 @@ export function populateGlobal(global: any, win: any, options: PopulateOptions =
})
}

const globalKeys = new Set<string | symbol>(['window', 'self', 'top', 'parent'])

// we are creating a proxy that intercepts all access to the global object,
// stores new value on `override`, and returns only these values,
// so it actually shares only values defined inside tests
const globalProxy = new Proxy(win.window, {
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)
},
})

globalKeys.forEach((key) => {
if (!win[key])
return

Object.defineProperty(global, key, {
get() {
return globalProxy
},
configurable: true,
})
})

const globalThisProxy = 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)
},
})

global.globalThis = globalThisProxy
global.window = global
global.self = global
global.top = global

if (global.global)
global.global = globalThisProxy
global.global = global

skipKeys.forEach(k => keys.add(k))

return {
keys,
skipKeys,
allowRewrite,
originals,
}
}
7 changes: 7 additions & 0 deletions test/core/test/dom.test.ts
Expand Up @@ -119,3 +119,10 @@ it('can call global functions without window works as expected', async () => {
expect(() => self.requestAnimationFrame(noop)).not.toThrow()
expect(() => globalThis.requestAnimationFrame(noop)).not.toThrow()
})

it('globals are the same', () => {
expect(window.globalThis).toBe(globalThis)
expect(window.Blob).toBe(globalThis.Blob)
expect(window.globalThis.Blob).toBe(globalThis.Blob)
expect(Blob).toBe(globalThis.Blob)
})
7 changes: 7 additions & 0 deletions test/core/test/happy-dom.test.ts
Expand Up @@ -98,3 +98,10 @@ it('can call global functions without window works as expected', async () => {
expect(() => self.requestAnimationFrame(noop)).not.toThrow()
expect(() => globalThis.requestAnimationFrame(noop)).not.toThrow()
})

it('globals are the same', () => {
expect(window.globalThis).toBe(globalThis)
expect(window.Blob).toBe(globalThis.Blob)
expect(window.globalThis.Blob).toBe(globalThis.Blob)
expect(Blob).toBe(globalThis.Blob)
})

0 comments on commit d32a758

Please sign in to comment.