import Vue from 'vue'
import {
Observer,
observe,
set as setProp,
del as delProp
} from 'core/observer/index'
import Dep from 'core/observer/dep'
import { hasOwn } from 'core/util/index'
describe('Observer', () => {
it('create on non-observables', () => {
const ob1 = observe(1)
expect(ob1).toBeUndefined()
const ob2 = observe(new Vue())
expect(ob2).toBeUndefined()
const ob3 = observe(Object.freeze({}))
expect(ob3).toBeUndefined()
})
it('create on object', () => {
const obj: any = {
a: {},
b: {}
}
const ob1 = observe(obj)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
expect(obj.a.__ob__ instanceof Observer).toBe(true)
expect(obj.b.__ob__ instanceof Observer).toBe(true)
const ob2 = observe(obj)!
expect(ob2).toBe(ob1)
})
it('create on null', () => {
const obj: any = Object.create(null)
obj.a = {}
obj.b = {}
const ob1 = observe(obj)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
expect(obj.a.__ob__ instanceof Observer).toBe(true)
expect(obj.b.__ob__ instanceof Observer).toBe(true)
const ob2 = observe(obj)!
expect(ob2).toBe(ob1)
})
it('create on already observed object', () => {
const obj: any = {}
let val = 0
let getCount = 0
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get() {
getCount++
return val
},
set(v) {
val = v
}
})
const ob1 = observe(obj)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
getCount = 0
obj.a
expect(getCount).toBe(1)
obj.a
expect(getCount).toBe(2)
const ob2 = observe(obj)!
expect(ob2).toBe(ob1)
obj.a = 10
expect(val).toBe(10)
})
it('create on property with only getter', () => {
const obj: any = {}
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get() {
return 123
}
})
const ob1 = observe(obj)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
expect(obj.a).toBe(123)
const ob2 = observe(obj)!
expect(ob2).toBe(ob1)
try {
obj.a = 101
} catch (e) {}
expect(obj.a).toBe(123)
})
it('create on property with only setter', () => {
const obj: any = {}
let val = 10
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
set(v) {
val = v
}
})
const ob1 = observe(obj)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
expect(obj.a).toBe(undefined)
const ob2 = observe(obj)!
expect(ob2).toBe(ob1)
obj.a = 100
expect(val).toBe(100)
})
it('create on property which is marked not configurable', () => {
const obj: any = {}
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: true,
value: 10
})
const ob1 = observe(obj)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
})
it('create on array', () => {
const arr: any = [{}, {}]
const ob1 = observe(arr)!
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(arr)
expect(arr.__ob__).toBe(ob1)
expect(arr[0].__ob__ instanceof Observer).toBe(true)
expect(arr[1].__ob__ instanceof Observer).toBe(true)
})
it('observing object prop change', () => {
const obj: any = { a: { b: 2 }, c: NaN }
observe(obj)!
const watcher: any = {
deps: [],
addDep(dep) {
this.deps.push(dep)
dep.addSub(this)
},
update: vi.fn()
}
Dep.target = watcher
obj.a.b
Dep.target = null
expect(watcher.deps.length).toBe(3)
obj.a.b = 3
expect(watcher.update.mock.calls.length).toBe(1)
obj.a = { b: 4 }
expect(watcher.update.mock.calls.length).toBe(2)
watcher.deps = []
Dep.target = watcher
obj.a.b
obj.c
Dep.target = null
expect(watcher.deps.length).toBe(4)
obj.a.b = 5
expect(watcher.update.mock.calls.length).toBe(3)
obj.c = NaN
expect(watcher.update.mock.calls.length).toBe(3)
})
it('observing object prop change on defined property', () => {
const obj: any = { val: 2 }
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get() {
return this.val
},
set(v) {
this.val = v
return this.val
}
})
observe(obj)!
expect(obj.a).toBe(2)
obj.a = 3
expect(obj.val).toBe(3)
obj.val = 5
expect(obj.a).toBe(5)
})
it('observing set/delete', () => {
const obj1: any = { a: 1 }
const ob1 = observe(obj1) as any
const dep1 = ob1.dep
vi.spyOn(dep1, 'notify')
setProp(obj1, 'b', 2)
expect(obj1.b).toBe(2)
expect(dep1.notify.mock.calls.length).toBe(1)
delProp(obj1, 'a')
expect(hasOwn(obj1, 'a')).toBe(false)
expect(dep1.notify.mock.calls.length).toBe(2)
setProp(obj1, 'b', 3)
expect(obj1.b).toBe(3)
expect(dep1.notify.mock.calls.length).toBe(2)
setProp(obj1, 'c', 1)
expect(obj1.c).toBe(1)
expect(dep1.notify.mock.calls.length).toBe(3)
delProp(obj1, 'a')
expect(dep1.notify.mock.calls.length).toBe(3)
const obj2 = { a: 1 }
delProp(obj2, 'a')
expect(hasOwn(obj2, 'a')).toBe(false)
const obj3: any = Object.create(null)
obj3.a = 1
const ob3 = observe(obj3) as any
const dep3 = ob3.dep
vi.spyOn(dep3, 'notify')
setProp(obj3, 'b', 2)
expect(obj3.b).toBe(2)
expect(dep3.notify.mock.calls.length).toBe(1)
delProp(obj3, 'a')
expect(hasOwn(obj3, 'a')).toBe(false)
expect(dep3.notify.mock.calls.length).toBe(2)
const arr2: any = ['a']
const ob2 = observe(arr2) as any
const dep2 = ob2.dep
vi.spyOn(dep2, 'notify')
setProp(arr2, 'b', 2)
expect(arr2.b).toBe(2)
expect(dep2.notify.mock.calls.length).toBe(1)
delProp(arr2, 'b')
expect(hasOwn(arr2, 'b')).toBe(false)
expect(dep2.notify.mock.calls.length).toBe(2)
})
it('warning set/delete on a Vue instance', done => {
const vm = new Vue({
template: '<div>{{a}}</div>',
data: { a: 1 }
}).$mount()
expect(vm.$el.outerHTML).toBe('<div>1</div>')
Vue.set(vm, 'a', 2)
waitForUpdate(() => {
expect(vm.$el.outerHTML).toBe('<div>2</div>')
expect(
'Avoid adding reactive properties to a Vue instance'
).not.toHaveBeenWarned()
Vue.delete(vm, 'a')
})
.then(() => {
expect('Avoid deleting properties on a Vue instance').toHaveBeenWarned()
expect(vm.$el.outerHTML).toBe('<div>2</div>')
Vue.set(vm, 'b', 123)
expect(
'Avoid adding reactive properties to a Vue instance'
).toHaveBeenWarned()
})
.then(done)
})
it('warning set/delete on Vue instance root $data', done => {
const data = { a: 1 }
const vm = new Vue({
template: '<div>{{a}}</div>',
data
}).$mount()
expect(vm.$el.outerHTML).toBe('<div>1</div>')
expect(Vue.set(data, 'a', 2)).toBe(2)
waitForUpdate(() => {
expect(vm.$el.outerHTML).toBe('<div>2</div>')
expect(
'Avoid adding reactive properties to a Vue instance'
).not.toHaveBeenWarned()
Vue.delete(data, 'a')
})
.then(() => {
expect('Avoid deleting properties on a Vue instance').toHaveBeenWarned()
expect(vm.$el.outerHTML).toBe('<div>2</div>')
expect(Vue.set(data, 'b', 123)).toBe(123)
expect(
'Avoid adding reactive properties to a Vue instance'
).toHaveBeenWarned()
})
.then(done)
})
it('observing array mutation', () => {
const arr: any[] = []
const ob = observe(arr) as any
const dep = ob.dep
vi.spyOn(dep, 'notify')
const objs = [{}, {}, {}]
arr.push(objs[0])
arr.pop()
arr.unshift(objs[1])
arr.shift()
arr.splice(0, 0, objs[2])
arr.sort()
arr.reverse()
expect(dep.notify.mock.calls.length).toBe(7)
objs.forEach((obj: any) => {
expect(obj.__ob__ instanceof Observer).toBe(true)
})
})
it('warn set/delete on non valid values', () => {
try {
setProp(null, 'foo', 1)
} catch (e) {}
expect(
`Cannot set reactive property on undefined, null, or primitive value`
).toHaveBeenWarned()
try {
delProp(null, 'foo')
} catch (e) {}
expect(
`Cannot delete reactive property on undefined, null, or primitive value`
).toHaveBeenWarned()
})
it('should lazy invoke existing getters', () => {
const obj: any = {}
let called = false
Object.defineProperty(obj, 'getterProp', {
enumerable: true,
get: () => {
called = true
return 'some value'
}
})
observe(obj)!
expect(called).toBe(false)
})
})