effect.spec.ts 19 KB

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