| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- 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', () => {
- // skip primitive value
- const ob1 = observe(1)
- expect(ob1).toBeUndefined()
- // avoid vue instance
- const ob2 = observe(new Vue())
- expect(ob2).toBeUndefined()
- // avoid frozen objects
- const ob3 = observe(Object.freeze({}))
- expect(ob3).toBeUndefined()
- })
- it('create on object', () => {
- // 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)
- // should've walked children
- expect(obj.a.__ob__ instanceof Observer).toBe(true)
- expect(obj.b.__ob__ instanceof Observer).toBe(true)
- // should return existing ob on already observed objects
- const ob2 = observe(obj)!
- expect(ob2).toBe(ob1)
- })
- it('create on null', () => {
- // 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)
- // should've walked children
- expect(obj.a.__ob__ instanceof Observer).toBe(true)
- expect(obj.b.__ob__ instanceof Observer).toBe(true)
- // should return existing ob on already observed objects
- const ob2 = observe(obj)!
- expect(ob2).toBe(ob1)
- })
- it('create on already observed object', () => {
- // on 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
- // Each read of 'a' should result in only one get underlying get call
- obj.a
- expect(getCount).toBe(1)
- obj.a
- expect(getCount).toBe(2)
- // should return existing ob on already observed objects
- const ob2 = observe(obj)!
- expect(ob2).toBe(ob1)
- // should call underlying setter
- obj.a = 10
- expect(val).toBe(10)
- })
- it('create on property with only getter', () => {
- // on object
- 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)
- // should be able to read
- expect(obj.a).toBe(123)
- // should return existing ob on already observed objects
- const ob2 = observe(obj)!
- expect(ob2).toBe(ob1)
- // since there is no setter, you shouldn't be able to write to it
- // PhantomJS throws when a property with no setter is set
- // but other real browsers don't
- try {
- obj.a = 101
- } catch (e) {}
- expect(obj.a).toBe(123)
- })
- it('create on property with only setter', () => {
- // on object
- const obj: any = {}
- let val = 10
- Object.defineProperty(obj, 'a', {
- // eslint-disable-line accessor-pairs
- 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)
- // reads should return undefined
- expect(obj.a).toBe(undefined)
- // should return existing ob on already observed objects
- const ob2 = observe(obj)!
- expect(ob2).toBe(ob1)
- // writes should call the set function
- obj.a = 100
- expect(val).toBe(100)
- })
- it('create on property which is marked not configurable', () => {
- // on object
- 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', () => {
- // on object
- const arr: any = [{}, {}]
- const ob1 = observe(arr)!
- expect(ob1 instanceof Observer).toBe(true)
- expect(ob1.value).toBe(arr)
- expect(arr.__ob__).toBe(ob1)
- // should've walked children
- 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)!
- // mock a watcher!
- const watcher: any = {
- deps: [],
- addDep(dep) {
- this.deps.push(dep)
- dep.addSub(this)
- },
- update: vi.fn()
- }
- // collect dep
- Dep.target = watcher
- obj.a.b
- Dep.target = null
- expect(watcher.deps.length).toBe(3) // obj.a + a + a.b
- obj.a.b = 3
- expect(watcher.update.mock.calls.length).toBe(1)
- // swap object
- 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)
- // set on the swapped object
- obj.a.b = 5
- expect(watcher.update.mock.calls.length).toBe(3)
- // should not trigger on NaN -> NaN set
- 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
- // eslint-disable-next-line no-setter-return
- return this.val
- }
- })
- observe(obj)!
- expect(obj.a).toBe(2) // Make sure 'this' is preserved
- obj.a = 3
- expect(obj.val).toBe(3) // make sure 'setter' was called
- obj.val = 5
- expect(obj.a).toBe(5) // make sure 'getter' was called
- })
- 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)
- // set existing key, should be a plain set and not
- // trigger own ob's notify
- setProp(obj1, 'b', 3)
- expect(obj1.b).toBe(3)
- expect(dep1.notify.mock.calls.length).toBe(2)
- // set non-existing key
- setProp(obj1, 'c', 1)
- expect(obj1.c).toBe(1)
- expect(dep1.notify.mock.calls.length).toBe(3)
- // should ignore deleting non-existing key
- delProp(obj1, 'a')
- expect(dep1.notify.mock.calls.length).toBe(3)
- // should work on non-observed objects
- const obj2 = { a: 1 }
- delProp(obj2, 'a')
- expect(hasOwn(obj2, 'a')).toBe(false)
- // should work on Object.create(null)
- 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)
- // set and delete non-numeric key on array
- 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)
- // inserted elements should be observed
- objs.forEach((obj: any) => {
- expect(obj.__ob__ instanceof Observer).toBe(true)
- })
- })
- it('warn set/delete on non valid values', () => {
- try {
- // @ts-expect-error
- setProp(null, 'foo', 1)
- } catch (e) {}
- expect(
- `Cannot set reactive property on undefined, null, or primitive value`
- ).toHaveBeenWarned()
- try {
- // @ts-expect-error
- 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)
- })
- })
|