effect.spec.ts 24 KB

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