462 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			462 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import {
 | 
						|
  h,
 | 
						|
  render,
 | 
						|
  getCurrentInstance,
 | 
						|
  nodeOps,
 | 
						|
  createApp,
 | 
						|
  shallowReadonly
 | 
						|
} from '@vue/runtime-test'
 | 
						|
import { ComponentInternalInstance, ComponentOptions } from '../src/component'
 | 
						|
 | 
						|
describe('component: proxy', () => {
 | 
						|
  test('data', () => {
 | 
						|
    let instance: ComponentInternalInstance
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      data() {
 | 
						|
        return {
 | 
						|
          foo: 1
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instance = getCurrentInstance()!
 | 
						|
        instanceProxy = this
 | 
						|
      },
 | 
						|
      render() {
 | 
						|
        return null
 | 
						|
      }
 | 
						|
    }
 | 
						|
    render(h(Comp), nodeOps.createElement('div'))
 | 
						|
    expect(instanceProxy.foo).toBe(1)
 | 
						|
    instanceProxy.foo = 2
 | 
						|
    expect(instance!.data.foo).toBe(2)
 | 
						|
  })
 | 
						|
 | 
						|
  test('setupState', () => {
 | 
						|
    let instance: ComponentInternalInstance
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      setup() {
 | 
						|
        return {
 | 
						|
          foo: 1
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instance = getCurrentInstance()!
 | 
						|
        instanceProxy = this
 | 
						|
      },
 | 
						|
      render() {
 | 
						|
        return null
 | 
						|
      }
 | 
						|
    }
 | 
						|
    render(h(Comp), nodeOps.createElement('div'))
 | 
						|
    expect(instanceProxy.foo).toBe(1)
 | 
						|
    instanceProxy.foo = 2
 | 
						|
    expect(instance!.setupState.foo).toBe(2)
 | 
						|
  })
 | 
						|
 | 
						|
  test('should not expose non-declared props', () => {
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      setup() {
 | 
						|
        return () => null
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
    render(h(Comp, { count: 1 }), nodeOps.createElement('div'))
 | 
						|
    expect('count' in instanceProxy).toBe(false)
 | 
						|
  })
 | 
						|
 | 
						|
  test('public properties', async () => {
 | 
						|
    let instance: ComponentInternalInstance
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      setup() {
 | 
						|
        return () => null
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instance = getCurrentInstance()!
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
    render(h(Comp), nodeOps.createElement('div'))
 | 
						|
    expect(instanceProxy.$data).toBe(instance!.data)
 | 
						|
    expect(instanceProxy.$props).toBe(shallowReadonly(instance!.props))
 | 
						|
    expect(instanceProxy.$attrs).toBe(shallowReadonly(instance!.attrs))
 | 
						|
    expect(instanceProxy.$slots).toBe(shallowReadonly(instance!.slots))
 | 
						|
    expect(instanceProxy.$refs).toBe(shallowReadonly(instance!.refs))
 | 
						|
    expect(instanceProxy.$parent).toBe(
 | 
						|
      instance!.parent && instance!.parent.proxy
 | 
						|
    )
 | 
						|
    expect(instanceProxy.$root).toBe(instance!.root.proxy)
 | 
						|
    expect(instanceProxy.$emit).toBe(instance!.emit)
 | 
						|
    expect(instanceProxy.$el).toBe(instance!.vnode.el)
 | 
						|
    expect(instanceProxy.$options).toBe(instance!.type as ComponentOptions)
 | 
						|
    expect(() => (instanceProxy.$data = {})).toThrow(TypeError)
 | 
						|
    expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
 | 
						|
 | 
						|
    const nextTickThis = await instanceProxy.$nextTick(function (this: any) {
 | 
						|
      return this
 | 
						|
    })
 | 
						|
    expect(nextTickThis).toBe(instanceProxy)
 | 
						|
  })
 | 
						|
 | 
						|
  test('user attached properties', async () => {
 | 
						|
    let instance: ComponentInternalInstance
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      setup() {
 | 
						|
        return () => null
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instance = getCurrentInstance()!
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
    render(h(Comp), nodeOps.createElement('div'))
 | 
						|
    instanceProxy.foo = 1
 | 
						|
    expect(instanceProxy.foo).toBe(1)
 | 
						|
    expect(instance!.ctx.foo).toBe(1)
 | 
						|
 | 
						|
    // should also allow properties that start with $
 | 
						|
    const obj = (instanceProxy.$store = {})
 | 
						|
    expect(instanceProxy.$store).toBe(obj)
 | 
						|
    expect(instance!.ctx.$store).toBe(obj)
 | 
						|
  })
 | 
						|
 | 
						|
  test('globalProperties', () => {
 | 
						|
    let instance: ComponentInternalInstance
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      setup() {
 | 
						|
        return () => null
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instance = getCurrentInstance()!
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp)
 | 
						|
    app.config.globalProperties.foo = 1
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
 | 
						|
    expect(instanceProxy.foo).toBe(1)
 | 
						|
 | 
						|
    // set should overwrite globalProperties with local
 | 
						|
    instanceProxy.foo = 2
 | 
						|
    // expect(instanceProxy.foo).toBe(2)
 | 
						|
    expect(instance!.ctx.foo).toBe(2)
 | 
						|
    // should not affect global
 | 
						|
    expect(app.config.globalProperties.foo).toBe(1)
 | 
						|
  })
 | 
						|
 | 
						|
  test('has check', () => {
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      render() {},
 | 
						|
      props: {
 | 
						|
        msg: String
 | 
						|
      },
 | 
						|
      data() {
 | 
						|
        return {
 | 
						|
          foo: 0
 | 
						|
        }
 | 
						|
      },
 | 
						|
      setup() {
 | 
						|
        return {
 | 
						|
          bar: 1
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp, { msg: 'hello' })
 | 
						|
    app.config.globalProperties.global = 1
 | 
						|
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
 | 
						|
    // props
 | 
						|
    expect('msg' in instanceProxy).toBe(true)
 | 
						|
    // data
 | 
						|
    expect('foo' in instanceProxy).toBe(true)
 | 
						|
    // ctx
 | 
						|
    expect('bar' in instanceProxy).toBe(true)
 | 
						|
    // public properties
 | 
						|
    expect('$el' in instanceProxy).toBe(true)
 | 
						|
    // global properties
 | 
						|
    expect('global' in instanceProxy).toBe(true)
 | 
						|
 | 
						|
    // non-existent
 | 
						|
    expect('$foobar' in instanceProxy).toBe(false)
 | 
						|
    expect('baz' in instanceProxy).toBe(false)
 | 
						|
 | 
						|
    // #4962 triggering getter should not cause non-existent property to
 | 
						|
    // pass the has check
 | 
						|
    instanceProxy.baz
 | 
						|
    expect('baz' in instanceProxy).toBe(false)
 | 
						|
 | 
						|
    // set non-existent (goes into proxyTarget sink)
 | 
						|
    instanceProxy.baz = 1
 | 
						|
    expect('baz' in instanceProxy).toBe(true)
 | 
						|
 | 
						|
    // dev mode ownKeys check for console inspection
 | 
						|
    // should only expose own keys
 | 
						|
    expect(Object.keys(instanceProxy)).toMatchObject([
 | 
						|
      'msg',
 | 
						|
      'bar',
 | 
						|
      'foo',
 | 
						|
      'baz'
 | 
						|
    ])
 | 
						|
  })
 | 
						|
 | 
						|
  test('allow updating proxy with Object.defineProperty', () => {
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      render() {},
 | 
						|
      setup() {
 | 
						|
        return {
 | 
						|
          isDisplayed: true
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp)
 | 
						|
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
 | 
						|
    Object.defineProperty(instanceProxy, 'isDisplayed', { value: false })
 | 
						|
 | 
						|
    expect(instanceProxy.isDisplayed).toBe(false)
 | 
						|
 | 
						|
    Object.defineProperty(instanceProxy, 'isDisplayed', { value: true })
 | 
						|
 | 
						|
    expect(instanceProxy.isDisplayed).toBe(true)
 | 
						|
 | 
						|
    Object.defineProperty(instanceProxy, 'isDisplayed', {
 | 
						|
      get() {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
    })
 | 
						|
 | 
						|
    expect(instanceProxy.isDisplayed).toBe(false)
 | 
						|
 | 
						|
    Object.defineProperty(instanceProxy, 'isDisplayed', {
 | 
						|
      get() {
 | 
						|
        return true
 | 
						|
      }
 | 
						|
    })
 | 
						|
 | 
						|
    expect(instanceProxy.isDisplayed).toBe(true)
 | 
						|
  })
 | 
						|
 | 
						|
  test('allow jest spying on proxy methods with Object.defineProperty', () => {
 | 
						|
    // #5417
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      render() {},
 | 
						|
      setup() {
 | 
						|
        return {
 | 
						|
          toggle() {
 | 
						|
            return 'a'
 | 
						|
          }
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp)
 | 
						|
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
 | 
						|
    // access 'toggle' to ensure key is cached
 | 
						|
    const v1 = instanceProxy.toggle()
 | 
						|
    expect(v1).toEqual('a')
 | 
						|
 | 
						|
    // reconfigure "toggle" to be getter based.
 | 
						|
    let getCalledTimes = 0
 | 
						|
    Object.defineProperty(instanceProxy, 'toggle', {
 | 
						|
      get() {
 | 
						|
        getCalledTimes++
 | 
						|
        return () => 'b'
 | 
						|
      }
 | 
						|
    })
 | 
						|
 | 
						|
    // getter should not be evaluated on initial definition
 | 
						|
    expect(getCalledTimes).toEqual(0)
 | 
						|
 | 
						|
    // invoke "toggle" after "defineProperty"
 | 
						|
    const v2 = instanceProxy.toggle()
 | 
						|
    expect(v2).toEqual('b')
 | 
						|
    expect(getCalledTimes).toEqual(1)
 | 
						|
 | 
						|
    // expect toggle getter not to be cached. it can't be
 | 
						|
    instanceProxy.toggle()
 | 
						|
    expect(getCalledTimes).toEqual(2)
 | 
						|
 | 
						|
    // attaching jest spy, triggers the getter once, cache it and override the property.
 | 
						|
    // also uses Object.defineProperty
 | 
						|
    const spy = jest.spyOn(instanceProxy, 'toggle')
 | 
						|
    expect(getCalledTimes).toEqual(3)
 | 
						|
 | 
						|
    // expect getter to not evaluate the jest spy caches its value
 | 
						|
    const v3 = instanceProxy.toggle()
 | 
						|
    expect(v3).toEqual('b')
 | 
						|
    expect(spy).toHaveBeenCalled()
 | 
						|
    expect(getCalledTimes).toEqual(3)
 | 
						|
  })
 | 
						|
 | 
						|
  test('defineProperty on proxy property with value descriptor', () => {
 | 
						|
    // #5417
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      render() {},
 | 
						|
      setup() {
 | 
						|
        return {
 | 
						|
          toggle: 'a'
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp)
 | 
						|
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
 | 
						|
    const v1 = instanceProxy.toggle
 | 
						|
    expect(v1).toEqual('a')
 | 
						|
 | 
						|
    Object.defineProperty(instanceProxy, 'toggle', {
 | 
						|
      value: 'b'
 | 
						|
    })
 | 
						|
    const v2 = instanceProxy.toggle
 | 
						|
    expect(v2).toEqual('b')
 | 
						|
 | 
						|
    // expect null to be a settable value
 | 
						|
    Object.defineProperty(instanceProxy, 'toggle', {
 | 
						|
      value: null
 | 
						|
    })
 | 
						|
    const v3 = instanceProxy.toggle
 | 
						|
    expect(v3).toBeNull()
 | 
						|
  })
 | 
						|
 | 
						|
  test('defineProperty on public instance proxy should work with SETUP,DATA,CONTEXT,PROPS', () => {
 | 
						|
    // #5417
 | 
						|
    let instanceProxy: any
 | 
						|
    const Comp = {
 | 
						|
      props: ['fromProp'],
 | 
						|
      data() {
 | 
						|
        return { name: 'data.name' }
 | 
						|
      },
 | 
						|
      computed: {
 | 
						|
        greet() {
 | 
						|
          return 'Hi ' + (this as any).name
 | 
						|
        }
 | 
						|
      },
 | 
						|
      render() {},
 | 
						|
      setup() {
 | 
						|
        return {
 | 
						|
          fromSetup: true
 | 
						|
        }
 | 
						|
      },
 | 
						|
      mounted() {
 | 
						|
        instanceProxy = this
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp, {
 | 
						|
      fromProp: true
 | 
						|
    })
 | 
						|
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
    expect(instanceProxy.greet).toEqual('Hi data.name')
 | 
						|
 | 
						|
    // define property on data
 | 
						|
    Object.defineProperty(instanceProxy, 'name', {
 | 
						|
      get() {
 | 
						|
        return 'getter.name'
 | 
						|
      }
 | 
						|
    })
 | 
						|
 | 
						|
    // computed is same still cached
 | 
						|
    expect(instanceProxy.greet).toEqual('Hi data.name')
 | 
						|
 | 
						|
    // trigger computed
 | 
						|
    instanceProxy.name = ''
 | 
						|
 | 
						|
    // expect "greet" to evaluated and use name from context getter
 | 
						|
    expect(instanceProxy.greet).toEqual('Hi getter.name')
 | 
						|
 | 
						|
    // defineProperty on computed ( context )
 | 
						|
    Object.defineProperty(instanceProxy, 'greet', {
 | 
						|
      get() {
 | 
						|
        return 'Hi greet.getter.computed'
 | 
						|
      }
 | 
						|
    })
 | 
						|
    expect(instanceProxy.greet).toEqual('Hi greet.getter.computed')
 | 
						|
 | 
						|
    // defineProperty on setupState
 | 
						|
    expect(instanceProxy.fromSetup).toBe(true)
 | 
						|
    Object.defineProperty(instanceProxy, 'fromSetup', {
 | 
						|
      get() {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
    })
 | 
						|
    expect(instanceProxy.fromSetup).toBe(false)
 | 
						|
 | 
						|
    // defineProperty on Props
 | 
						|
    expect(instanceProxy.fromProp).toBe(true)
 | 
						|
    Object.defineProperty(instanceProxy, 'fromProp', {
 | 
						|
      get() {
 | 
						|
        return false
 | 
						|
      }
 | 
						|
    })
 | 
						|
    expect(instanceProxy.fromProp).toBe(false)
 | 
						|
  })
 | 
						|
 | 
						|
  // #864
 | 
						|
  test('should not warn declared but absent props', () => {
 | 
						|
    const Comp = {
 | 
						|
      props: ['test'],
 | 
						|
      render(this: any) {
 | 
						|
        return this.test
 | 
						|
      }
 | 
						|
    }
 | 
						|
    render(h(Comp), nodeOps.createElement('div'))
 | 
						|
    expect(
 | 
						|
      `was accessed during render but is not defined`
 | 
						|
    ).not.toHaveBeenWarned()
 | 
						|
  })
 | 
						|
 | 
						|
  test('should allow symbol to access on render', () => {
 | 
						|
    const Comp = {
 | 
						|
      render() {
 | 
						|
        if ((this as any)[Symbol.unscopables]) {
 | 
						|
          return '1'
 | 
						|
        }
 | 
						|
        return '2'
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const app = createApp(Comp)
 | 
						|
    app.mount(nodeOps.createElement('div'))
 | 
						|
 | 
						|
    expect(
 | 
						|
      `Property ${JSON.stringify(
 | 
						|
        Symbol.unscopables
 | 
						|
      )} was accessed during render ` + `but is not defined on instance.`
 | 
						|
    ).toHaveBeenWarned()
 | 
						|
  })
 | 
						|
})
 |