Skip to content
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

fix: calling global functions in happy-dom, refactor sharing global state #1262

Merged
merged 4 commits into from May 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/vitest/src/integrations/chai/jest-expect.ts
Expand Up @@ -20,20 +20,20 @@ if (!Object.prototype.hasOwnProperty.call(global, MATCHERS_OBJECT)) {
expectedAssertionsNumber: null,
expectedAssertionsNumberErrorGen: null,
}
Object.defineProperty(global, MATCHERS_OBJECT, {
Object.defineProperty(globalThis, MATCHERS_OBJECT, {
value: {
state: defaultState,
},
})
}

export const getState = <State extends MatcherState = MatcherState>(): State =>
(global as any)[MATCHERS_OBJECT].state
(globalThis as any)[MATCHERS_OBJECT].state

export const setState = <State extends MatcherState = MatcherState>(
state: Partial<State>,
): void => {
Object.assign((global as any)[MATCHERS_OBJECT].state, state)
Object.assign((globalThis as any)[MATCHERS_OBJECT].state, state)
}

// Jest Expect Compact
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/integrations/env/happy-dom.ts
Expand Up @@ -10,7 +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)
const { keys, allowRewrite } = populateGlobal(global, win, { bindFunctions: true })

const originals = new Map<string | symbol, any>(
allowRewrite.map(([key]) => [key, global[key]]),
Expand Down
110 changes: 64 additions & 46 deletions packages/vitest/src/integrations/env/utils.ts
Expand Up @@ -8,6 +8,8 @@ const allowRewrite = [
const skipKeys = [
'window',
'self',
'top',
'parent',
]

export function getWindowKeys(global: any, win: any) {
Expand All @@ -24,15 +26,23 @@ export function getWindowKeys(global: any, win: any) {
return keys
}

export function populateGlobal(global: any, win: any) {
interface PopulateOptions {
bindFunctions?: boolean
}

export function populateGlobal(global: any, win: any, options: PopulateOptions = {}) {
const { bindFunctions = false } = options
const keys = getWindowKeys(global, win)

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

const globalKeys = new Set<string | symbol>(['window', 'self', 'GLOBAL', 'global'])
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

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
return globalProxy
},
configurable: true,
})
})

global.globalThis = new Proxy(global.globalThis, {
const globalThisProxy = new Proxy(global.globalThis, {
set(target, key, value, receiver) {
overrideObject.set(key, value)
return Reflect.set(target, key, value, receiver)
Expand All @@ -121,6 +134,11 @@ export function populateGlobal(global: any, win: any) {
},
})

global.globalThis = globalThisProxy

if (global.global)
global.global = globalThisProxy

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

return {
Expand Down
90 changes: 89 additions & 1 deletion test/core/test/dom.test.ts
@@ -1,7 +1,7 @@
/**
* @vitest-environment jsdom
*/
import { expect, it } from 'vitest'
import { expect, it, vi } from 'vitest'

it('jsdom', () => {
expect(window).toBeDefined()
Expand All @@ -24,3 +24,91 @@ it('Image works as expected', () => {

expect(img.width).toBe(100)
})

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')
})

it('can call global functions without window works as expected', async () => {
const noop = vi.fn()

expect(() => addEventListener('abort', noop)).not.toThrow()
expect(() => scrollTo()).not.toThrow()
expect(() => requestAnimationFrame(noop)).not.toThrow()
expect(() => window.requestAnimationFrame(noop)).not.toThrow()
expect(() => self.requestAnimationFrame(noop)).not.toThrow()
expect(() => globalThis.requestAnimationFrame(noop)).not.toThrow()
})
13 changes: 12 additions & 1 deletion test/core/test/happy-dom.test.ts
Expand Up @@ -4,7 +4,7 @@

/* eslint-disable vars-on-top */

import { expect, it } from 'vitest'
import { expect, it, vi } from 'vitest'

declare global {
// eslint-disable-next-line no-var
Expand Down Expand Up @@ -87,3 +87,14 @@ it('usage with defineProperty', () => {
expect(globalThis.__property).toBe('global_property')
expect(window.__property).toBe('global_property')
})

it('can call global functions without window works as expected', async () => {
const noop = vi.fn()

expect(() => addEventListener('abort', noop)).not.toThrow()
expect(() => scrollTo()).not.toThrow()
expect(() => requestAnimationFrame(noop)).not.toThrow()
expect(() => window.requestAnimationFrame(noop)).not.toThrow()
expect(() => self.requestAnimationFrame(noop)).not.toThrow()
expect(() => globalThis.requestAnimationFrame(noop)).not.toThrow()
})