effect.spec.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. import {
  2. ref,
  3. reactive,
  4. effect,
  5. stop,
  6. toRaw,
  7. TrackOpTypes,
  8. TriggerOpTypes,
  9. DebuggerEvent,
  10. markRaw,
  11. shallowReactive,
  12. readonly,
  13. ReactiveEffectRunner
  14. } from '../src/index'
  15. import { ITERATE_KEY } from '../src/effect'
  16. describe('reactivity/effect', () => {
  17. it('should run the passed function once (wrapped by a effect)', () => {
  18. const fnSpy = jest.fn(() => {})
  19. effect(fnSpy)
  20. expect(fnSpy).toHaveBeenCalledTimes(1)
  21. })
  22. it('should observe basic properties', () => {
  23. let dummy
  24. const counter = reactive({ num: 0 })
  25. effect(() => (dummy = counter.num))
  26. expect(dummy).toBe(0)
  27. counter.num = 7
  28. expect(dummy).toBe(7)
  29. })
  30. it('should observe multiple properties', () => {
  31. let dummy
  32. const counter = reactive({ num1: 0, num2: 0 })
  33. effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))
  34. expect(dummy).toBe(0)
  35. counter.num1 = counter.num2 = 7
  36. expect(dummy).toBe(21)
  37. })
  38. it('should handle multiple effects', () => {
  39. let dummy1, dummy2
  40. const counter = reactive({ num: 0 })
  41. effect(() => (dummy1 = counter.num))
  42. effect(() => (dummy2 = counter.num))
  43. expect(dummy1).toBe(0)
  44. expect(dummy2).toBe(0)
  45. counter.num++
  46. expect(dummy1).toBe(1)
  47. expect(dummy2).toBe(1)
  48. })
  49. it('should observe nested properties', () => {
  50. let dummy
  51. const counter = reactive({ nested: { num: 0 } })
  52. effect(() => (dummy = counter.nested.num))
  53. expect(dummy).toBe(0)
  54. counter.nested.num = 8
  55. expect(dummy).toBe(8)
  56. })
  57. it('should observe delete operations', () => {
  58. let dummy
  59. const obj = reactive<{
  60. prop?: string
  61. }>({ prop: 'value' })
  62. effect(() => (dummy = obj.prop))
  63. expect(dummy).toBe('value')
  64. delete obj.prop
  65. expect(dummy).toBe(undefined)
  66. })
  67. it('should observe has operations', () => {
  68. let dummy
  69. const obj = reactive<{ prop?: string | number }>({ prop: 'value' })
  70. effect(() => (dummy = 'prop' in obj))
  71. expect(dummy).toBe(true)
  72. delete obj.prop
  73. expect(dummy).toBe(false)
  74. obj.prop = 12
  75. expect(dummy).toBe(true)
  76. })
  77. it('should observe properties on the prototype chain', () => {
  78. let dummy
  79. const counter = reactive<{ num?: number }>({ num: 0 })
  80. const parentCounter = reactive({ num: 2 })
  81. Object.setPrototypeOf(counter, parentCounter)
  82. effect(() => (dummy = counter.num))
  83. expect(dummy).toBe(0)
  84. delete counter.num
  85. expect(dummy).toBe(2)
  86. parentCounter.num = 4
  87. expect(dummy).toBe(4)
  88. counter.num = 3
  89. expect(dummy).toBe(3)
  90. })
  91. it('should observe has operations on the prototype chain', () => {
  92. let dummy
  93. const counter = reactive<{ num?: number }>({ num: 0 })
  94. const parentCounter = reactive<{ num?: number }>({ num: 2 })
  95. Object.setPrototypeOf(counter, parentCounter)
  96. effect(() => (dummy = 'num' in counter))
  97. expect(dummy).toBe(true)
  98. delete counter.num
  99. expect(dummy).toBe(true)
  100. delete parentCounter.num
  101. expect(dummy).toBe(false)
  102. counter.num = 3
  103. expect(dummy).toBe(true)
  104. })
  105. it('should observe inherited property accessors', () => {
  106. let dummy, parentDummy, hiddenValue: any
  107. const obj = reactive<{ prop?: number }>({})
  108. const parent = reactive({
  109. set prop(value) {
  110. hiddenValue = value
  111. },
  112. get prop() {
  113. return hiddenValue
  114. }
  115. })
  116. Object.setPrototypeOf(obj, parent)
  117. effect(() => (dummy = obj.prop))
  118. effect(() => (parentDummy = parent.prop))
  119. expect(dummy).toBe(undefined)
  120. expect(parentDummy).toBe(undefined)
  121. obj.prop = 4
  122. expect(dummy).toBe(4)
  123. // this doesn't work, should it?
  124. // expect(parentDummy).toBe(4)
  125. parent.prop = 2
  126. expect(dummy).toBe(2)
  127. expect(parentDummy).toBe(2)
  128. })
  129. it('should observe function call chains', () => {
  130. let dummy
  131. const counter = reactive({ num: 0 })
  132. effect(() => (dummy = getNum()))
  133. function getNum() {
  134. return counter.num
  135. }
  136. expect(dummy).toBe(0)
  137. counter.num = 2
  138. expect(dummy).toBe(2)
  139. })
  140. it('should observe iteration', () => {
  141. let dummy
  142. const list = reactive(['Hello'])
  143. effect(() => (dummy = list.join(' ')))
  144. expect(dummy).toBe('Hello')
  145. list.push('World!')
  146. expect(dummy).toBe('Hello World!')
  147. list.shift()
  148. expect(dummy).toBe('World!')
  149. })
  150. it('should observe implicit array length changes', () => {
  151. let dummy
  152. const list = reactive(['Hello'])
  153. effect(() => (dummy = list.join(' ')))
  154. expect(dummy).toBe('Hello')
  155. list[1] = 'World!'
  156. expect(dummy).toBe('Hello World!')
  157. list[3] = 'Hello!'
  158. expect(dummy).toBe('Hello World! Hello!')
  159. })
  160. it('should observe sparse array mutations', () => {
  161. let dummy
  162. const list = reactive<string[]>([])
  163. list[1] = 'World!'
  164. effect(() => (dummy = list.join(' ')))
  165. expect(dummy).toBe(' World!')
  166. list[0] = 'Hello'
  167. expect(dummy).toBe('Hello World!')
  168. list.pop()
  169. expect(dummy).toBe('Hello')
  170. })
  171. it('should observe enumeration', () => {
  172. let dummy = 0
  173. const numbers = reactive<Record<string, number>>({ num1: 3 })
  174. effect(() => {
  175. dummy = 0
  176. for (let key in numbers) {
  177. dummy += numbers[key]
  178. }
  179. })
  180. expect(dummy).toBe(3)
  181. numbers.num2 = 4
  182. expect(dummy).toBe(7)
  183. delete numbers.num1
  184. expect(dummy).toBe(4)
  185. })
  186. it('should observe symbol keyed properties', () => {
  187. const key = Symbol('symbol keyed prop')
  188. let dummy, hasDummy
  189. const obj = reactive<{ [key]?: string }>({ [key]: 'value' })
  190. effect(() => (dummy = obj[key]))
  191. effect(() => (hasDummy = key in obj))
  192. expect(dummy).toBe('value')
  193. expect(hasDummy).toBe(true)
  194. obj[key] = 'newValue'
  195. expect(dummy).toBe('newValue')
  196. delete obj[key]
  197. expect(dummy).toBe(undefined)
  198. expect(hasDummy).toBe(false)
  199. })
  200. it('should not observe well-known symbol keyed properties', () => {
  201. const key = Symbol.isConcatSpreadable
  202. let dummy
  203. const array: any = reactive([])
  204. effect(() => (dummy = array[key]))
  205. expect(array[key]).toBe(undefined)
  206. expect(dummy).toBe(undefined)
  207. array[key] = true
  208. expect(array[key]).toBe(true)
  209. expect(dummy).toBe(undefined)
  210. })
  211. it('should observe function valued properties', () => {
  212. const oldFunc = () => {}
  213. const newFunc = () => {}
  214. let dummy
  215. const obj = reactive({ func: oldFunc })
  216. effect(() => (dummy = obj.func))
  217. expect(dummy).toBe(oldFunc)
  218. obj.func = newFunc
  219. expect(dummy).toBe(newFunc)
  220. })
  221. it('should observe chained getters relying on this', () => {
  222. const obj = reactive({
  223. a: 1,
  224. get b() {
  225. return this.a
  226. }
  227. })
  228. let dummy
  229. effect(() => (dummy = obj.b))
  230. expect(dummy).toBe(1)
  231. obj.a++
  232. expect(dummy).toBe(2)
  233. })
  234. it('should observe methods relying on this', () => {
  235. const obj = reactive({
  236. a: 1,
  237. b() {
  238. return this.a
  239. }
  240. })
  241. let dummy
  242. effect(() => (dummy = obj.b()))
  243. expect(dummy).toBe(1)
  244. obj.a++
  245. expect(dummy).toBe(2)
  246. })
  247. it('should not observe set operations without a value change', () => {
  248. let hasDummy, getDummy
  249. const obj = reactive({ prop: 'value' })
  250. const getSpy = jest.fn(() => (getDummy = obj.prop))
  251. const hasSpy = jest.fn(() => (hasDummy = 'prop' in obj))
  252. effect(getSpy)
  253. effect(hasSpy)
  254. expect(getDummy).toBe('value')
  255. expect(hasDummy).toBe(true)
  256. obj.prop = 'value'
  257. expect(getSpy).toHaveBeenCalledTimes(1)
  258. expect(hasSpy).toHaveBeenCalledTimes(1)
  259. expect(getDummy).toBe('value')
  260. expect(hasDummy).toBe(true)
  261. })
  262. it('should not observe raw mutations', () => {
  263. let dummy
  264. const obj = reactive<{ prop?: string }>({})
  265. effect(() => (dummy = toRaw(obj).prop))
  266. expect(dummy).toBe(undefined)
  267. obj.prop = 'value'
  268. expect(dummy).toBe(undefined)
  269. })
  270. it('should not be triggered by raw mutations', () => {
  271. let dummy
  272. const obj = reactive<{ prop?: string }>({})
  273. effect(() => (dummy = obj.prop))
  274. expect(dummy).toBe(undefined)
  275. toRaw(obj).prop = 'value'
  276. expect(dummy).toBe(undefined)
  277. })
  278. it('should not be triggered by inherited raw setters', () => {
  279. let dummy, parentDummy, hiddenValue: any
  280. const obj = reactive<{ prop?: number }>({})
  281. const parent = reactive({
  282. set prop(value) {
  283. hiddenValue = value
  284. },
  285. get prop() {
  286. return hiddenValue
  287. }
  288. })
  289. Object.setPrototypeOf(obj, parent)
  290. effect(() => (dummy = obj.prop))
  291. effect(() => (parentDummy = parent.prop))
  292. expect(dummy).toBe(undefined)
  293. expect(parentDummy).toBe(undefined)
  294. toRaw(obj).prop = 4
  295. expect(dummy).toBe(undefined)
  296. expect(parentDummy).toBe(undefined)
  297. })
  298. it('should avoid implicit infinite recursive loops with itself', () => {
  299. const counter = reactive({ num: 0 })
  300. const counterSpy = jest.fn(() => counter.num++)
  301. effect(counterSpy)
  302. expect(counter.num).toBe(1)
  303. expect(counterSpy).toHaveBeenCalledTimes(1)
  304. counter.num = 4
  305. expect(counter.num).toBe(5)
  306. expect(counterSpy).toHaveBeenCalledTimes(2)
  307. })
  308. it('should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift', () => {
  309. ;(['push', 'unshift'] as const).forEach(key => {
  310. const arr = reactive<number[]>([])
  311. const counterSpy1 = jest.fn(() => (arr[key] as any)(1))
  312. const counterSpy2 = jest.fn(() => (arr[key] as any)(2))
  313. effect(counterSpy1)
  314. effect(counterSpy2)
  315. expect(arr.length).toBe(2)
  316. expect(counterSpy1).toHaveBeenCalledTimes(1)
  317. expect(counterSpy2).toHaveBeenCalledTimes(1)
  318. })
  319. ;(['pop', 'shift'] as const).forEach(key => {
  320. const arr = reactive<number[]>([1, 2, 3, 4])
  321. const counterSpy1 = jest.fn(() => (arr[key] as any)())
  322. const counterSpy2 = jest.fn(() => (arr[key] as any)())
  323. effect(counterSpy1)
  324. effect(counterSpy2)
  325. expect(arr.length).toBe(2)
  326. expect(counterSpy1).toHaveBeenCalledTimes(1)
  327. expect(counterSpy2).toHaveBeenCalledTimes(1)
  328. })
  329. })
  330. it('should allow explicitly recursive raw function loops', () => {
  331. const counter = reactive({ num: 0 })
  332. const numSpy = jest.fn(() => {
  333. counter.num++
  334. if (counter.num < 10) {
  335. numSpy()
  336. }
  337. })
  338. effect(numSpy)
  339. expect(counter.num).toEqual(10)
  340. expect(numSpy).toHaveBeenCalledTimes(10)
  341. })
  342. it('should avoid infinite loops with other effects', () => {
  343. const nums = reactive({ num1: 0, num2: 1 })
  344. const spy1 = jest.fn(() => (nums.num1 = nums.num2))
  345. const spy2 = jest.fn(() => (nums.num2 = nums.num1))
  346. effect(spy1)
  347. effect(spy2)
  348. expect(nums.num1).toBe(1)
  349. expect(nums.num2).toBe(1)
  350. expect(spy1).toHaveBeenCalledTimes(1)
  351. expect(spy2).toHaveBeenCalledTimes(1)
  352. nums.num2 = 4
  353. expect(nums.num1).toBe(4)
  354. expect(nums.num2).toBe(4)
  355. expect(spy1).toHaveBeenCalledTimes(2)
  356. expect(spy2).toHaveBeenCalledTimes(2)
  357. nums.num1 = 10
  358. expect(nums.num1).toBe(10)
  359. expect(nums.num2).toBe(10)
  360. expect(spy1).toHaveBeenCalledTimes(3)
  361. expect(spy2).toHaveBeenCalledTimes(3)
  362. })
  363. it('should return a new reactive version of the function', () => {
  364. function greet() {
  365. return 'Hello World'
  366. }
  367. const effect1 = effect(greet)
  368. const effect2 = effect(greet)
  369. expect(typeof effect1).toBe('function')
  370. expect(typeof effect2).toBe('function')
  371. expect(effect1).not.toBe(greet)
  372. expect(effect1).not.toBe(effect2)
  373. })
  374. it('should discover new branches while running automatically', () => {
  375. let dummy
  376. const obj = reactive({ prop: 'value', run: false })
  377. const conditionalSpy = jest.fn(() => {
  378. dummy = obj.run ? obj.prop : 'other'
  379. })
  380. effect(conditionalSpy)
  381. expect(dummy).toBe('other')
  382. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  383. obj.prop = 'Hi'
  384. expect(dummy).toBe('other')
  385. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  386. obj.run = true
  387. expect(dummy).toBe('Hi')
  388. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  389. obj.prop = 'World'
  390. expect(dummy).toBe('World')
  391. expect(conditionalSpy).toHaveBeenCalledTimes(3)
  392. })
  393. it('should discover new branches when running manually', () => {
  394. let dummy
  395. let run = false
  396. const obj = reactive({ prop: 'value' })
  397. const runner = effect(() => {
  398. dummy = run ? obj.prop : 'other'
  399. })
  400. expect(dummy).toBe('other')
  401. runner()
  402. expect(dummy).toBe('other')
  403. run = true
  404. runner()
  405. expect(dummy).toBe('value')
  406. obj.prop = 'World'
  407. expect(dummy).toBe('World')
  408. })
  409. it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
  410. let dummy
  411. const obj = reactive({ prop: 'value', run: true })
  412. const conditionalSpy = jest.fn(() => {
  413. dummy = obj.run ? obj.prop : 'other'
  414. })
  415. effect(conditionalSpy)
  416. expect(dummy).toBe('value')
  417. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  418. obj.run = false
  419. expect(dummy).toBe('other')
  420. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  421. obj.prop = 'value2'
  422. expect(dummy).toBe('other')
  423. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  424. })
  425. it('should handle deep effect recursion using cleanup fallback', () => {
  426. const results = reactive([0])
  427. const effects: { fx: ReactiveEffectRunner; index: number }[] = []
  428. for (let i = 1; i < 40; i++) {
  429. ;(index => {
  430. const fx = effect(() => {
  431. results[index] = results[index - 1] * 2
  432. })
  433. effects.push({ fx, index })
  434. })(i)
  435. }
  436. expect(results[39]).toBe(0)
  437. results[0] = 1
  438. expect(results[39]).toBe(Math.pow(2, 39))
  439. })
  440. it('should register deps independently during effect recursion', () => {
  441. const input = reactive({ a: 1, b: 2, c: 0 })
  442. const output = reactive({ fx1: 0, fx2: 0 })
  443. const fx1Spy = jest.fn(() => {
  444. let result = 0
  445. if (input.c < 2) result += input.a
  446. if (input.c > 1) result += input.b
  447. output.fx1 = result
  448. })
  449. const fx1 = effect(fx1Spy)
  450. const fx2Spy = jest.fn(() => {
  451. let result = 0
  452. if (input.c > 1) result += input.a
  453. if (input.c < 3) result += input.b
  454. output.fx2 = result + output.fx1
  455. })
  456. const fx2 = effect(fx2Spy)
  457. expect(fx1).not.toBeNull()
  458. expect(fx2).not.toBeNull()
  459. expect(output.fx1).toBe(1)
  460. expect(output.fx2).toBe(2 + 1)
  461. expect(fx1Spy).toHaveBeenCalledTimes(1)
  462. expect(fx2Spy).toHaveBeenCalledTimes(1)
  463. fx1Spy.mockClear()
  464. fx2Spy.mockClear()
  465. input.b = 3
  466. expect(output.fx1).toBe(1)
  467. expect(output.fx2).toBe(3 + 1)
  468. expect(fx1Spy).toHaveBeenCalledTimes(0)
  469. expect(fx2Spy).toHaveBeenCalledTimes(1)
  470. fx1Spy.mockClear()
  471. fx2Spy.mockClear()
  472. input.c = 1
  473. expect(output.fx1).toBe(1)
  474. expect(output.fx2).toBe(3 + 1)
  475. expect(fx1Spy).toHaveBeenCalledTimes(1)
  476. expect(fx2Spy).toHaveBeenCalledTimes(1)
  477. fx1Spy.mockClear()
  478. fx2Spy.mockClear()
  479. input.c = 2
  480. expect(output.fx1).toBe(3)
  481. expect(output.fx2).toBe(1 + 3 + 3)
  482. expect(fx1Spy).toHaveBeenCalledTimes(1)
  483. // Invoked twice due to change of fx1.
  484. expect(fx2Spy).toHaveBeenCalledTimes(2)
  485. fx1Spy.mockClear()
  486. fx2Spy.mockClear()
  487. input.c = 3
  488. expect(output.fx1).toBe(3)
  489. expect(output.fx2).toBe(1 + 3)
  490. expect(fx1Spy).toHaveBeenCalledTimes(1)
  491. expect(fx2Spy).toHaveBeenCalledTimes(1)
  492. fx1Spy.mockClear()
  493. fx2Spy.mockClear()
  494. input.a = 10
  495. expect(output.fx1).toBe(3)
  496. expect(output.fx2).toBe(10 + 3)
  497. expect(fx1Spy).toHaveBeenCalledTimes(0)
  498. expect(fx2Spy).toHaveBeenCalledTimes(1)
  499. })
  500. it('should not double wrap if the passed function is a effect', () => {
  501. const runner = effect(() => {})
  502. const otherRunner = effect(runner)
  503. expect(runner).not.toBe(otherRunner)
  504. expect(runner.effect.fn).toBe(otherRunner.effect.fn)
  505. })
  506. it('should not run multiple times for a single mutation', () => {
  507. let dummy
  508. const obj = reactive<Record<string, number>>({})
  509. const fnSpy = jest.fn(() => {
  510. for (const key in obj) {
  511. dummy = obj[key]
  512. }
  513. dummy = obj.prop
  514. })
  515. effect(fnSpy)
  516. expect(fnSpy).toHaveBeenCalledTimes(1)
  517. obj.prop = 16
  518. expect(dummy).toBe(16)
  519. expect(fnSpy).toHaveBeenCalledTimes(2)
  520. })
  521. it('should allow nested effects', () => {
  522. const nums = reactive({ num1: 0, num2: 1, num3: 2 })
  523. const dummy: any = {}
  524. const childSpy = jest.fn(() => (dummy.num1 = nums.num1))
  525. const childeffect = effect(childSpy)
  526. const parentSpy = jest.fn(() => {
  527. dummy.num2 = nums.num2
  528. childeffect()
  529. dummy.num3 = nums.num3
  530. })
  531. effect(parentSpy)
  532. expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 })
  533. expect(parentSpy).toHaveBeenCalledTimes(1)
  534. expect(childSpy).toHaveBeenCalledTimes(2)
  535. // this should only call the childeffect
  536. nums.num1 = 4
  537. expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 })
  538. expect(parentSpy).toHaveBeenCalledTimes(1)
  539. expect(childSpy).toHaveBeenCalledTimes(3)
  540. // this calls the parenteffect, which calls the childeffect once
  541. nums.num2 = 10
  542. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 })
  543. expect(parentSpy).toHaveBeenCalledTimes(2)
  544. expect(childSpy).toHaveBeenCalledTimes(4)
  545. // this calls the parenteffect, which calls the childeffect once
  546. nums.num3 = 7
  547. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 })
  548. expect(parentSpy).toHaveBeenCalledTimes(3)
  549. expect(childSpy).toHaveBeenCalledTimes(5)
  550. })
  551. it('should observe json methods', () => {
  552. let dummy = <Record<string, number>>{}
  553. const obj = reactive<Record<string, number>>({})
  554. effect(() => {
  555. dummy = JSON.parse(JSON.stringify(obj))
  556. })
  557. obj.a = 1
  558. expect(dummy.a).toBe(1)
  559. })
  560. it('should observe class method invocations', () => {
  561. class Model {
  562. count: number
  563. constructor() {
  564. this.count = 0
  565. }
  566. inc() {
  567. this.count++
  568. }
  569. }
  570. const model = reactive(new Model())
  571. let dummy
  572. effect(() => {
  573. dummy = model.count
  574. })
  575. expect(dummy).toBe(0)
  576. model.inc()
  577. expect(dummy).toBe(1)
  578. })
  579. it('lazy', () => {
  580. const obj = reactive({ foo: 1 })
  581. let dummy
  582. const runner = effect(() => (dummy = obj.foo), { lazy: true })
  583. expect(dummy).toBe(undefined)
  584. expect(runner()).toBe(1)
  585. expect(dummy).toBe(1)
  586. obj.foo = 2
  587. expect(dummy).toBe(2)
  588. })
  589. it('scheduler', () => {
  590. let dummy
  591. let run: any
  592. const scheduler = jest.fn(() => {
  593. run = runner
  594. })
  595. const obj = reactive({ foo: 1 })
  596. const runner = effect(
  597. () => {
  598. dummy = obj.foo
  599. },
  600. { scheduler }
  601. )
  602. expect(scheduler).not.toHaveBeenCalled()
  603. expect(dummy).toBe(1)
  604. // should be called on first trigger
  605. obj.foo++
  606. expect(scheduler).toHaveBeenCalledTimes(1)
  607. // should not run yet
  608. expect(dummy).toBe(1)
  609. // manually run
  610. run()
  611. // should have run
  612. expect(dummy).toBe(2)
  613. })
  614. it('events: onTrack', () => {
  615. let events: DebuggerEvent[] = []
  616. let dummy
  617. const onTrack = jest.fn((e: DebuggerEvent) => {
  618. events.push(e)
  619. })
  620. const obj = reactive({ foo: 1, bar: 2 })
  621. const runner = effect(
  622. () => {
  623. dummy = obj.foo
  624. dummy = 'bar' in obj
  625. dummy = Object.keys(obj)
  626. },
  627. { onTrack }
  628. )
  629. expect(dummy).toEqual(['foo', 'bar'])
  630. expect(onTrack).toHaveBeenCalledTimes(3)
  631. expect(events).toEqual([
  632. {
  633. effect: runner.effect,
  634. target: toRaw(obj),
  635. type: TrackOpTypes.GET,
  636. key: 'foo'
  637. },
  638. {
  639. effect: runner.effect,
  640. target: toRaw(obj),
  641. type: TrackOpTypes.HAS,
  642. key: 'bar'
  643. },
  644. {
  645. effect: runner.effect,
  646. target: toRaw(obj),
  647. type: TrackOpTypes.ITERATE,
  648. key: ITERATE_KEY
  649. }
  650. ])
  651. })
  652. it('events: onTrigger', () => {
  653. let events: DebuggerEvent[] = []
  654. let dummy
  655. const onTrigger = jest.fn((e: DebuggerEvent) => {
  656. events.push(e)
  657. })
  658. const obj = reactive<{ foo?: number }>({ foo: 1 })
  659. const runner = effect(
  660. () => {
  661. dummy = obj.foo
  662. },
  663. { onTrigger }
  664. )
  665. obj.foo!++
  666. expect(dummy).toBe(2)
  667. expect(onTrigger).toHaveBeenCalledTimes(1)
  668. expect(events[0]).toEqual({
  669. effect: runner.effect,
  670. target: toRaw(obj),
  671. type: TriggerOpTypes.SET,
  672. key: 'foo',
  673. oldValue: 1,
  674. newValue: 2
  675. })
  676. delete obj.foo
  677. expect(dummy).toBeUndefined()
  678. expect(onTrigger).toHaveBeenCalledTimes(2)
  679. expect(events[1]).toEqual({
  680. effect: runner.effect,
  681. target: toRaw(obj),
  682. type: TriggerOpTypes.DELETE,
  683. key: 'foo',
  684. oldValue: 2
  685. })
  686. })
  687. it('stop', () => {
  688. let dummy
  689. const obj = reactive({ prop: 1 })
  690. const runner = effect(() => {
  691. dummy = obj.prop
  692. })
  693. obj.prop = 2
  694. expect(dummy).toBe(2)
  695. stop(runner)
  696. obj.prop = 3
  697. expect(dummy).toBe(2)
  698. // stopped effect should still be manually callable
  699. runner()
  700. expect(dummy).toBe(3)
  701. })
  702. // #5707
  703. // when an effect completes its run, it should clear the tracking bits of
  704. // its tracked deps. However, if the effect stops itself, the deps list is
  705. // emptied so their bits are never cleared.
  706. it('edge case: self-stopping effect tracking ref', () => {
  707. const c = ref(true)
  708. const runner = effect(() => {
  709. // reference ref
  710. if (!c.value) {
  711. // stop itself while running
  712. stop(runner)
  713. }
  714. })
  715. // trigger run
  716. c.value = !c.value
  717. // should clear bits
  718. expect((c as any).dep.w).toBe(0)
  719. expect((c as any).dep.n).toBe(0)
  720. })
  721. it('events: onStop', () => {
  722. const onStop = jest.fn()
  723. const runner = effect(() => {}, {
  724. onStop
  725. })
  726. stop(runner)
  727. expect(onStop).toHaveBeenCalled()
  728. })
  729. it('stop: a stopped effect is nested in a normal effect', () => {
  730. let dummy
  731. const obj = reactive({ prop: 1 })
  732. const runner = effect(() => {
  733. dummy = obj.prop
  734. })
  735. stop(runner)
  736. obj.prop = 2
  737. expect(dummy).toBe(1)
  738. // observed value in inner stopped effect
  739. // will track outer effect as an dependency
  740. effect(() => {
  741. runner()
  742. })
  743. expect(dummy).toBe(2)
  744. // notify outer effect to run
  745. obj.prop = 3
  746. expect(dummy).toBe(3)
  747. })
  748. it('markRaw', () => {
  749. const obj = reactive({
  750. foo: markRaw({
  751. prop: 0
  752. })
  753. })
  754. let dummy
  755. effect(() => {
  756. dummy = obj.foo.prop
  757. })
  758. expect(dummy).toBe(0)
  759. obj.foo.prop++
  760. expect(dummy).toBe(0)
  761. obj.foo = { prop: 1 }
  762. expect(dummy).toBe(1)
  763. })
  764. it('should not be triggered when the value and the old value both are NaN', () => {
  765. const obj = reactive({
  766. foo: NaN
  767. })
  768. const fnSpy = jest.fn(() => obj.foo)
  769. effect(fnSpy)
  770. obj.foo = NaN
  771. expect(fnSpy).toHaveBeenCalledTimes(1)
  772. })
  773. it('should trigger all effects when array length is set to 0', () => {
  774. const observed: any = reactive([1])
  775. let dummy, record
  776. effect(() => {
  777. dummy = observed.length
  778. })
  779. effect(() => {
  780. record = observed[0]
  781. })
  782. expect(dummy).toBe(1)
  783. expect(record).toBe(1)
  784. observed[1] = 2
  785. expect(observed[1]).toBe(2)
  786. observed.unshift(3)
  787. expect(dummy).toBe(3)
  788. expect(record).toBe(3)
  789. observed.length = 0
  790. expect(dummy).toBe(0)
  791. expect(record).toBeUndefined()
  792. })
  793. it('should not be triggered when set with the same proxy', () => {
  794. const obj = reactive({ foo: 1 })
  795. const observed: any = reactive({ obj })
  796. const fnSpy = jest.fn(() => observed.obj)
  797. effect(fnSpy)
  798. expect(fnSpy).toHaveBeenCalledTimes(1)
  799. observed.obj = obj
  800. expect(fnSpy).toHaveBeenCalledTimes(1)
  801. const obj2 = reactive({ foo: 1 })
  802. const observed2: any = shallowReactive({ obj2 })
  803. const fnSpy2 = jest.fn(() => observed2.obj2)
  804. effect(fnSpy2)
  805. expect(fnSpy2).toHaveBeenCalledTimes(1)
  806. observed2.obj2 = obj2
  807. expect(fnSpy2).toHaveBeenCalledTimes(1)
  808. })
  809. describe('readonly + reactive for Map', () => {
  810. test('should work with readonly(reactive(Map))', () => {
  811. const m = reactive(new Map())
  812. const roM = readonly(m)
  813. const fnSpy = jest.fn(() => roM.get(1))
  814. effect(fnSpy)
  815. expect(fnSpy).toHaveBeenCalledTimes(1)
  816. m.set(1, 1)
  817. expect(fnSpy).toHaveBeenCalledTimes(2)
  818. })
  819. test('should work with observed value as key', () => {
  820. const key = reactive({})
  821. const m = reactive(new Map())
  822. m.set(key, 1)
  823. const roM = readonly(m)
  824. const fnSpy = jest.fn(() => roM.get(key))
  825. effect(fnSpy)
  826. expect(fnSpy).toHaveBeenCalledTimes(1)
  827. m.set(key, 1)
  828. expect(fnSpy).toHaveBeenCalledTimes(1)
  829. m.set(key, 2)
  830. expect(fnSpy).toHaveBeenCalledTimes(2)
  831. })
  832. })
  833. })