Repository: vuejs/vue
Pull request: add observer tests

NOTE: bring vue 1.x observer test


Changed files:
filename: test/unit/modules/observer/observer.spec.js
patch: @@ -0,0 +1,320 @@
+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 = {
+      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 = 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 = {}
+    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 = {}
+    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 = {}
+    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 = {}
+    Object.defineProperty(obj, 'a', {
+      configurable: false,
+      enumerable: true,
+      val: 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 = [{}, {}]
+    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 = { a: { b: 2 }}
+    observe(obj)
+    // mock a watcher!
+    const watcher = {
+      deps: [],
+      addDep (dep) {
+        this.deps.push(dep)
+        dep.addSub(this)
+      },
+      update: jasmine.createSpy()
+    }
+    // collect dep
+    Dep.target = watcher
+    obj.a.b
+    Dep.target = null
+    expect(watcher.deps.length).toBe(3) // obj.a + a.b + b
+    obj.a.b = 3
+    expect(watcher.update.calls.count()).toBe(1)
+    // swap object
+    obj.a = { b: 4 }
+    expect(watcher.update.calls.count()).toBe(2)
+    watcher.deps = []
+    Dep.target = watcher
+    obj.a.b
+    Dep.target = null
+    expect(watcher.deps.length).toBe(3)
+    // set on the swapped object
+    obj.a.b = 5
+    expect(watcher.update.calls.count()).toBe(3)
+  })
+
+  it('observing object prop change on defined property', () => {
+    const obj = { val: 2 }
+    Object.defineProperty(obj, 'a', {
+      configurable: true,
+      enumerable: true,
+      get () { return this.val },
+      set (v) {
+        this.val = v
+        return this.val
+      }
+    })
+
+    observe(obj)
+    // mock a watcher!
+    const watcher = {
+      deps: [],
+      addDep: function (dep) {
+        this.deps.push(dep)
+        dep.addSub(this)
+      },
+      update: jasmine.createSpy()
+    }
+    // collect dep
+    Dep.target = watcher
+    expect(obj.a).toBe(2) // Make sure 'this' is preserved
+    Dep.target = null
+    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 = { a: 1 }
+    const ob1 = observe(obj1)
+    const dep1 = ob1.dep
+    spyOn(dep1, 'notify')
+    setProp(obj1, 'b', 2)
+    expect(obj1.b).toBe(2)
+    expect(dep1.notify.calls.count()).toBe(1)
+    delProp(obj1, 'a')
+    expect(hasOwn(obj1, 'a')).toBe(false)
+    expect(dep1.notify.calls.count()).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.calls.count()).toBe(2)
+    // set non-existing key
+    setProp(obj1, 'c', 1)
+    expect(obj1.c).toBe(1)
+    expect(dep1.notify.calls.count()).toBe(3)
+    // should ignore deleting non-existing key
+    delProp(obj1, 'a')
+    expect(dep1.notify.calls.count()).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 = Object.create(null)
+    obj3.a = 1
+    const ob3 = observe(obj3)
+    const dep3 = ob3.dep
+    spyOn(dep3, 'notify')
+    setProp(obj3, 'b', 2)
+    expect(obj3.b).toBe(2)
+    expect(dep3.notify.calls.count()).toBe(1)
+    delProp(obj3, 'a')
+    expect(hasOwn(obj3, 'a')).toBe(false)
+    expect(dep3.notify.calls.count()).toBe(2)
+  })
+
+  it('observing set/delete in Vm object', done => {
+    const vm = new Vue({
+      template: '<div>{{a}}</div>',
+      data: { a: 1 }
+    })
+    vm.$mount()
+    expect(vm.$el.outerHTML).toBe('<div>1</div>')
+    Vue.set(vm, 'a', 2)
+    waitForUpdate(() => {
+      expect(vm.$el.outerHTML).toBe('<div>2</div>')
+      Vue.delete(vm, 'a')
+    }).then(() => {
+      expect(vm.$el.outerHTML).toBe('<div></div>')
+      done()
+    }).catch(done)
+  })
+
+  it('observing array mutation', () => {
+    const arr = []
+    const ob = observe(arr)
+    const dep = ob.dep
+    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.calls.count()).toBe(7)
+    // inserted elements should be observed
+    objs.forEach(obj => {
+      expect(obj.__ob__ instanceof Observer).toBe(true)
+    })
+  })
+})