observer.spec.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import Vue from 'vue'
  2. import {
  3. Observer,
  4. observe,
  5. set as setProp,
  6. del as delProp
  7. } from 'core/observer/index'
  8. import Dep from 'core/observer/dep'
  9. import { hasOwn } from 'core/util/index'
  10. describe('Observer', () => {
  11. it('create on non-observables', () => {
  12. // skip primitive value
  13. const ob1 = observe(1)
  14. expect(ob1).toBeUndefined()
  15. // avoid vue instance
  16. const ob2 = observe(new Vue())
  17. expect(ob2).toBeUndefined()
  18. // avoid frozen objects
  19. const ob3 = observe(Object.freeze({}))
  20. expect(ob3).toBeUndefined()
  21. })
  22. it('create on object', () => {
  23. // on object
  24. const obj = {
  25. a: {},
  26. b: {}
  27. }
  28. const ob1 = observe(obj)
  29. expect(ob1 instanceof Observer).toBe(true)
  30. expect(ob1.value).toBe(obj)
  31. expect(obj.__ob__).toBe(ob1)
  32. // should've walked children
  33. expect(obj.a.__ob__ instanceof Observer).toBe(true)
  34. expect(obj.b.__ob__ instanceof Observer).toBe(true)
  35. // should return existing ob on already observed objects
  36. const ob2 = observe(obj)
  37. expect(ob2).toBe(ob1)
  38. })
  39. it('create on null', () => {
  40. // on null
  41. const obj = Object.create(null)
  42. obj.a = {}
  43. obj.b = {}
  44. const ob1 = observe(obj)
  45. expect(ob1 instanceof Observer).toBe(true)
  46. expect(ob1.value).toBe(obj)
  47. expect(obj.__ob__).toBe(ob1)
  48. // should've walked children
  49. expect(obj.a.__ob__ instanceof Observer).toBe(true)
  50. expect(obj.b.__ob__ instanceof Observer).toBe(true)
  51. // should return existing ob on already observed objects
  52. const ob2 = observe(obj)
  53. expect(ob2).toBe(ob1)
  54. })
  55. it('create on already observed object', () => {
  56. // on object
  57. const obj = {}
  58. let val = 0
  59. let getCount = 0
  60. Object.defineProperty(obj, 'a', {
  61. configurable: true,
  62. enumerable: true,
  63. get () {
  64. getCount++
  65. return val
  66. },
  67. set (v) { val = v }
  68. })
  69. const ob1 = observe(obj)
  70. expect(ob1 instanceof Observer).toBe(true)
  71. expect(ob1.value).toBe(obj)
  72. expect(obj.__ob__).toBe(ob1)
  73. getCount = 0
  74. // Each read of 'a' should result in only one get underlying get call
  75. obj.a
  76. expect(getCount).toBe(1)
  77. obj.a
  78. expect(getCount).toBe(2)
  79. // should return existing ob on already observed objects
  80. const ob2 = observe(obj)
  81. expect(ob2).toBe(ob1)
  82. // should call underlying setter
  83. obj.a = 10
  84. expect(val).toBe(10)
  85. })
  86. it('create on property with only getter', () => {
  87. // on object
  88. const obj = {}
  89. Object.defineProperty(obj, 'a', {
  90. configurable: true,
  91. enumerable: true,
  92. get () { return 123 }
  93. })
  94. const ob1 = observe(obj)
  95. expect(ob1 instanceof Observer).toBe(true)
  96. expect(ob1.value).toBe(obj)
  97. expect(obj.__ob__).toBe(ob1)
  98. // should be able to read
  99. expect(obj.a).toBe(123)
  100. // should return existing ob on already observed objects
  101. const ob2 = observe(obj)
  102. expect(ob2).toBe(ob1)
  103. // since there is no setter, you shouldn't be able to write to it
  104. // PhantomJS throws when a property with no setter is set
  105. // but other real browsers don't
  106. try {
  107. obj.a = 101
  108. } catch (e) {}
  109. expect(obj.a).toBe(123)
  110. })
  111. it('create on property with only setter', () => {
  112. // on object
  113. const obj = {}
  114. let val = 10
  115. Object.defineProperty(obj, 'a', { // eslint-disable-line accessor-pairs
  116. configurable: true,
  117. enumerable: true,
  118. set (v) { val = v }
  119. })
  120. const ob1 = observe(obj)
  121. expect(ob1 instanceof Observer).toBe(true)
  122. expect(ob1.value).toBe(obj)
  123. expect(obj.__ob__).toBe(ob1)
  124. // reads should return undefined
  125. expect(obj.a).toBe(undefined)
  126. // should return existing ob on already observed objects
  127. const ob2 = observe(obj)
  128. expect(ob2).toBe(ob1)
  129. // writes should call the set function
  130. obj.a = 100
  131. expect(val).toBe(100)
  132. })
  133. it('create on property which is marked not configurable', () => {
  134. // on object
  135. const obj = {}
  136. Object.defineProperty(obj, 'a', {
  137. configurable: false,
  138. enumerable: true,
  139. val: 10
  140. })
  141. const ob1 = observe(obj)
  142. expect(ob1 instanceof Observer).toBe(true)
  143. expect(ob1.value).toBe(obj)
  144. expect(obj.__ob__).toBe(ob1)
  145. })
  146. it('create on array', () => {
  147. // on object
  148. const arr = [{}, {}]
  149. const ob1 = observe(arr)
  150. expect(ob1 instanceof Observer).toBe(true)
  151. expect(ob1.value).toBe(arr)
  152. expect(arr.__ob__).toBe(ob1)
  153. // should've walked children
  154. expect(arr[0].__ob__ instanceof Observer).toBe(true)
  155. expect(arr[1].__ob__ instanceof Observer).toBe(true)
  156. })
  157. it('observing object prop change', () => {
  158. const obj = { a: { b: 2 }, c: NaN }
  159. observe(obj)
  160. // mock a watcher!
  161. const watcher = {
  162. deps: [],
  163. addDep (dep) {
  164. this.deps.push(dep)
  165. dep.addSub(this)
  166. },
  167. update: jasmine.createSpy()
  168. }
  169. // collect dep
  170. Dep.target = watcher
  171. obj.a.b
  172. Dep.target = null
  173. expect(watcher.deps.length).toBe(3) // obj.a + a + a.b
  174. obj.a.b = 3
  175. expect(watcher.update.calls.count()).toBe(1)
  176. // swap object
  177. obj.a = { b: 4 }
  178. expect(watcher.update.calls.count()).toBe(2)
  179. watcher.deps = []
  180. Dep.target = watcher
  181. obj.a.b
  182. obj.c
  183. Dep.target = null
  184. expect(watcher.deps.length).toBe(4)
  185. // set on the swapped object
  186. obj.a.b = 5
  187. expect(watcher.update.calls.count()).toBe(3)
  188. // should not trigger on NaN -> NaN set
  189. obj.c = NaN
  190. expect(watcher.update.calls.count()).toBe(3)
  191. })
  192. it('observing object prop change on defined property', () => {
  193. const obj = { val: 2 }
  194. Object.defineProperty(obj, 'a', {
  195. configurable: true,
  196. enumerable: true,
  197. get () { return this.val },
  198. set (v) {
  199. this.val = v
  200. return this.val
  201. }
  202. })
  203. observe(obj)
  204. expect(obj.a).toBe(2) // Make sure 'this' is preserved
  205. obj.a = 3
  206. expect(obj.val).toBe(3) // make sure 'setter' was called
  207. obj.val = 5
  208. expect(obj.a).toBe(5) // make sure 'getter' was called
  209. })
  210. it('observing set/delete', () => {
  211. const obj1 = { a: 1 }
  212. const ob1 = observe(obj1)
  213. const dep1 = ob1.dep
  214. spyOn(dep1, 'notify')
  215. setProp(obj1, 'b', 2)
  216. expect(obj1.b).toBe(2)
  217. expect(dep1.notify.calls.count()).toBe(1)
  218. delProp(obj1, 'a')
  219. expect(hasOwn(obj1, 'a')).toBe(false)
  220. expect(dep1.notify.calls.count()).toBe(2)
  221. // set existing key, should be a plain set and not
  222. // trigger own ob's notify
  223. setProp(obj1, 'b', 3)
  224. expect(obj1.b).toBe(3)
  225. expect(dep1.notify.calls.count()).toBe(2)
  226. // set non-existing key
  227. setProp(obj1, 'c', 1)
  228. expect(obj1.c).toBe(1)
  229. expect(dep1.notify.calls.count()).toBe(3)
  230. // should ignore deleting non-existing key
  231. delProp(obj1, 'a')
  232. expect(dep1.notify.calls.count()).toBe(3)
  233. // should work on non-observed objects
  234. const obj2 = { a: 1 }
  235. delProp(obj2, 'a')
  236. expect(hasOwn(obj2, 'a')).toBe(false)
  237. // should work on Object.create(null)
  238. const obj3 = Object.create(null)
  239. obj3.a = 1
  240. const ob3 = observe(obj3)
  241. const dep3 = ob3.dep
  242. spyOn(dep3, 'notify')
  243. setProp(obj3, 'b', 2)
  244. expect(obj3.b).toBe(2)
  245. expect(dep3.notify.calls.count()).toBe(1)
  246. delProp(obj3, 'a')
  247. expect(hasOwn(obj3, 'a')).toBe(false)
  248. expect(dep3.notify.calls.count()).toBe(2)
  249. // set and delete non-numeric key on array
  250. const arr2 = ['a']
  251. const ob2 = observe(arr2)
  252. const dep2 = ob2.dep
  253. spyOn(dep2, 'notify')
  254. setProp(arr2, 'b', 2)
  255. expect(arr2.b).toBe(2)
  256. expect(dep2.notify.calls.count()).toBe(1)
  257. delProp(arr2, 'b')
  258. expect(hasOwn(arr2, 'b')).toBe(false)
  259. expect(dep2.notify.calls.count()).toBe(2)
  260. })
  261. it('warning set/delete on a Vue instance', done => {
  262. const vm = new Vue({
  263. template: '<div>{{a}}</div>',
  264. data: { a: 1 }
  265. }).$mount()
  266. expect(vm.$el.outerHTML).toBe('<div>1</div>')
  267. Vue.set(vm, 'a', 2)
  268. waitForUpdate(() => {
  269. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  270. expect('Avoid adding reactive properties to a Vue instance').not.toHaveBeenWarned()
  271. Vue.delete(vm, 'a')
  272. }).then(() => {
  273. expect('Avoid deleting properties on a Vue instance').toHaveBeenWarned()
  274. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  275. Vue.set(vm, 'b', 123)
  276. expect('Avoid adding reactive properties to a Vue instance').toHaveBeenWarned()
  277. }).then(done)
  278. })
  279. it('warning set/delete on Vue instance root $data', done => {
  280. const data = { a: 1 }
  281. const vm = new Vue({
  282. template: '<div>{{a}}</div>',
  283. data
  284. }).$mount()
  285. expect(vm.$el.outerHTML).toBe('<div>1</div>')
  286. expect(Vue.set(data, 'a', 2)).toBe(2)
  287. waitForUpdate(() => {
  288. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  289. expect('Avoid adding reactive properties to a Vue instance').not.toHaveBeenWarned()
  290. Vue.delete(data, 'a')
  291. }).then(() => {
  292. expect('Avoid deleting properties on a Vue instance').toHaveBeenWarned()
  293. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  294. expect(Vue.set(data, 'b', 123)).toBe(123)
  295. expect('Avoid adding reactive properties to a Vue instance').toHaveBeenWarned()
  296. }).then(done)
  297. })
  298. it('observing array mutation', () => {
  299. const arr = []
  300. const ob = observe(arr)
  301. const dep = ob.dep
  302. spyOn(dep, 'notify')
  303. const objs = [{}, {}, {}]
  304. arr.push(objs[0])
  305. arr.pop()
  306. arr.unshift(objs[1])
  307. arr.shift()
  308. arr.splice(0, 0, objs[2])
  309. arr.sort()
  310. arr.reverse()
  311. expect(dep.notify.calls.count()).toBe(7)
  312. // inserted elements should be observed
  313. objs.forEach(obj => {
  314. expect(obj.__ob__ instanceof Observer).toBe(true)
  315. })
  316. })
  317. it('warn set/delete on non valid values', () => {
  318. try {
  319. setProp(null, 'foo', 1)
  320. } catch (e) {}
  321. expect(`Cannot set reactive property on undefined, null, or primitive value`).toHaveBeenWarned()
  322. try {
  323. delProp(null, 'foo')
  324. } catch (e) {}
  325. expect(`Cannot delete reactive property on undefined, null, or primitive value`).toHaveBeenWarned()
  326. })
  327. it('should lazy invoke existing getters', () => {
  328. const obj = {}
  329. let called = false
  330. Object.defineProperty(obj, 'getterProp', {
  331. enumerable: true,
  332. get: () => {
  333. called = true
  334. return 'some value'
  335. }
  336. })
  337. observe(obj)
  338. expect(called).toBe(false)
  339. })
  340. })