effect.spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. import {
  2. reactive,
  3. effect,
  4. stop,
  5. toRaw,
  6. TrackOpTypes,
  7. TriggerOpTypes,
  8. DebuggerEvent,
  9. markRaw
  10. } from '../src/index'
  11. import { ITERATE_KEY } from '../src/effect'
  12. describe('reactivity/effect', () => {
  13. it('should run the passed function once (wrapped by a effect)', () => {
  14. const fnSpy = jest.fn(() => {})
  15. effect(fnSpy)
  16. expect(fnSpy).toHaveBeenCalledTimes(1)
  17. })
  18. it('should observe basic properties', () => {
  19. let dummy
  20. const counter = reactive({ num: 0 })
  21. effect(() => (dummy = counter.num))
  22. expect(dummy).toBe(0)
  23. counter.num = 7
  24. expect(dummy).toBe(7)
  25. })
  26. it('should observe multiple properties', () => {
  27. let dummy
  28. const counter = reactive({ num1: 0, num2: 0 })
  29. effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))
  30. expect(dummy).toBe(0)
  31. counter.num1 = counter.num2 = 7
  32. expect(dummy).toBe(21)
  33. })
  34. it('should handle multiple effects', () => {
  35. let dummy1, dummy2
  36. const counter = reactive({ num: 0 })
  37. effect(() => (dummy1 = counter.num))
  38. effect(() => (dummy2 = counter.num))
  39. expect(dummy1).toBe(0)
  40. expect(dummy2).toBe(0)
  41. counter.num++
  42. expect(dummy1).toBe(1)
  43. expect(dummy2).toBe(1)
  44. })
  45. it('should observe nested properties', () => {
  46. let dummy
  47. const counter = reactive({ nested: { num: 0 } })
  48. effect(() => (dummy = counter.nested.num))
  49. expect(dummy).toBe(0)
  50. counter.nested.num = 8
  51. expect(dummy).toBe(8)
  52. })
  53. it('should observe delete operations', () => {
  54. let dummy
  55. const obj = reactive({ prop: 'value' })
  56. effect(() => (dummy = obj.prop))
  57. expect(dummy).toBe('value')
  58. // @ts-ignore
  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. // @ts-ignore
  68. delete obj.prop
  69. expect(dummy).toBe(false)
  70. obj.prop = 12
  71. expect(dummy).toBe(true)
  72. })
  73. it('should observe properties on the prototype chain', () => {
  74. let dummy
  75. const counter = reactive({ num: 0 })
  76. const parentCounter = reactive({ num: 2 })
  77. Object.setPrototypeOf(counter, parentCounter)
  78. effect(() => (dummy = counter.num))
  79. expect(dummy).toBe(0)
  80. // @ts-ignore
  81. delete counter.num
  82. expect(dummy).toBe(2)
  83. parentCounter.num = 4
  84. expect(dummy).toBe(4)
  85. counter.num = 3
  86. expect(dummy).toBe(3)
  87. })
  88. it('should observe has operations on the prototype chain', () => {
  89. let dummy
  90. const counter = reactive({ num: 0 })
  91. const parentCounter = reactive({ num: 2 })
  92. Object.setPrototypeOf(counter, parentCounter)
  93. effect(() => (dummy = 'num' in counter))
  94. expect(dummy).toBe(true)
  95. // @ts-ignore
  96. delete counter.num
  97. expect(dummy).toBe(true)
  98. // @ts-ignore
  99. delete parentCounter.num
  100. expect(dummy).toBe(false)
  101. counter.num = 3
  102. expect(dummy).toBe(true)
  103. })
  104. it('should observe inherited property accessors', () => {
  105. let dummy, parentDummy, hiddenValue: any
  106. const obj = reactive<{ prop?: number }>({})
  107. const parent = reactive({
  108. set prop(value) {
  109. hiddenValue = value
  110. },
  111. get prop() {
  112. return hiddenValue
  113. }
  114. })
  115. Object.setPrototypeOf(obj, parent)
  116. effect(() => (dummy = obj.prop))
  117. effect(() => (parentDummy = parent.prop))
  118. expect(dummy).toBe(undefined)
  119. expect(parentDummy).toBe(undefined)
  120. obj.prop = 4
  121. expect(dummy).toBe(4)
  122. // this doesn't work, should it?
  123. // expect(parentDummy).toBe(4)
  124. parent.prop = 2
  125. expect(dummy).toBe(2)
  126. expect(parentDummy).toBe(2)
  127. })
  128. it('should observe function call chains', () => {
  129. let dummy
  130. const counter = reactive({ num: 0 })
  131. effect(() => (dummy = getNum()))
  132. function getNum() {
  133. return counter.num
  134. }
  135. expect(dummy).toBe(0)
  136. counter.num = 2
  137. expect(dummy).toBe(2)
  138. })
  139. it('should observe iteration', () => {
  140. let dummy
  141. const list = reactive(['Hello'])
  142. effect(() => (dummy = list.join(' ')))
  143. expect(dummy).toBe('Hello')
  144. list.push('World!')
  145. expect(dummy).toBe('Hello World!')
  146. list.shift()
  147. expect(dummy).toBe('World!')
  148. })
  149. it('should observe implicit array length changes', () => {
  150. let dummy
  151. const list = reactive(['Hello'])
  152. effect(() => (dummy = list.join(' ')))
  153. expect(dummy).toBe('Hello')
  154. list[1] = 'World!'
  155. expect(dummy).toBe('Hello World!')
  156. list[3] = 'Hello!'
  157. expect(dummy).toBe('Hello World! Hello!')
  158. })
  159. it('should observe sparse array mutations', () => {
  160. let dummy
  161. const list = reactive<string[]>([])
  162. list[1] = 'World!'
  163. effect(() => (dummy = list.join(' ')))
  164. expect(dummy).toBe(' World!')
  165. list[0] = 'Hello'
  166. expect(dummy).toBe('Hello World!')
  167. list.pop()
  168. expect(dummy).toBe('Hello')
  169. })
  170. it('should observe enumeration', () => {
  171. let dummy = 0
  172. const numbers = reactive<Record<string, number>>({ num1: 3 })
  173. effect(() => {
  174. dummy = 0
  175. for (let key in numbers) {
  176. dummy += numbers[key]
  177. }
  178. })
  179. expect(dummy).toBe(3)
  180. numbers.num2 = 4
  181. expect(dummy).toBe(7)
  182. delete numbers.num1
  183. expect(dummy).toBe(4)
  184. })
  185. it('should observe symbol keyed properties', () => {
  186. const key = Symbol('symbol keyed prop')
  187. let dummy, hasDummy
  188. const obj = reactive({ [key]: 'value' })
  189. effect(() => (dummy = obj[key]))
  190. effect(() => (hasDummy = key in obj))
  191. expect(dummy).toBe('value')
  192. expect(hasDummy).toBe(true)
  193. obj[key] = 'newValue'
  194. expect(dummy).toBe('newValue')
  195. // @ts-ignore
  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 not double wrap if the passed function is a effect', () => {
  426. const runner = effect(() => {})
  427. const otherRunner = effect(runner)
  428. expect(runner).not.toBe(otherRunner)
  429. expect(runner.raw).toBe(otherRunner.raw)
  430. })
  431. it('should not run multiple times for a single mutation', () => {
  432. let dummy
  433. const obj = reactive<Record<string, number>>({})
  434. const fnSpy = jest.fn(() => {
  435. for (const key in obj) {
  436. dummy = obj[key]
  437. }
  438. dummy = obj.prop
  439. })
  440. effect(fnSpy)
  441. expect(fnSpy).toHaveBeenCalledTimes(1)
  442. obj.prop = 16
  443. expect(dummy).toBe(16)
  444. expect(fnSpy).toHaveBeenCalledTimes(2)
  445. })
  446. it('should allow nested effects', () => {
  447. const nums = reactive({ num1: 0, num2: 1, num3: 2 })
  448. const dummy: any = {}
  449. const childSpy = jest.fn(() => (dummy.num1 = nums.num1))
  450. const childeffect = effect(childSpy)
  451. const parentSpy = jest.fn(() => {
  452. dummy.num2 = nums.num2
  453. childeffect()
  454. dummy.num3 = nums.num3
  455. })
  456. effect(parentSpy)
  457. expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 })
  458. expect(parentSpy).toHaveBeenCalledTimes(1)
  459. expect(childSpy).toHaveBeenCalledTimes(2)
  460. // this should only call the childeffect
  461. nums.num1 = 4
  462. expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 })
  463. expect(parentSpy).toHaveBeenCalledTimes(1)
  464. expect(childSpy).toHaveBeenCalledTimes(3)
  465. // this calls the parenteffect, which calls the childeffect once
  466. nums.num2 = 10
  467. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 })
  468. expect(parentSpy).toHaveBeenCalledTimes(2)
  469. expect(childSpy).toHaveBeenCalledTimes(4)
  470. // this calls the parenteffect, which calls the childeffect once
  471. nums.num3 = 7
  472. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 })
  473. expect(parentSpy).toHaveBeenCalledTimes(3)
  474. expect(childSpy).toHaveBeenCalledTimes(5)
  475. })
  476. it('should observe json methods', () => {
  477. let dummy = <Record<string, number>>{}
  478. const obj = reactive<Record<string, number>>({})
  479. effect(() => {
  480. dummy = JSON.parse(JSON.stringify(obj))
  481. })
  482. obj.a = 1
  483. expect(dummy.a).toBe(1)
  484. })
  485. it('should observe class method invocations', () => {
  486. class Model {
  487. count: number
  488. constructor() {
  489. this.count = 0
  490. }
  491. inc() {
  492. this.count++
  493. }
  494. }
  495. const model = reactive(new Model())
  496. let dummy
  497. effect(() => {
  498. dummy = model.count
  499. })
  500. expect(dummy).toBe(0)
  501. model.inc()
  502. expect(dummy).toBe(1)
  503. })
  504. it('lazy', () => {
  505. const obj = reactive({ foo: 1 })
  506. let dummy
  507. const runner = effect(() => (dummy = obj.foo), { lazy: true })
  508. expect(dummy).toBe(undefined)
  509. expect(runner()).toBe(1)
  510. expect(dummy).toBe(1)
  511. obj.foo = 2
  512. expect(dummy).toBe(2)
  513. })
  514. it('scheduler', () => {
  515. let runner: any, dummy
  516. const scheduler = jest.fn(_runner => {
  517. runner = _runner
  518. })
  519. const obj = reactive({ foo: 1 })
  520. effect(
  521. () => {
  522. dummy = obj.foo
  523. },
  524. { scheduler }
  525. )
  526. expect(scheduler).not.toHaveBeenCalled()
  527. expect(dummy).toBe(1)
  528. // should be called on first trigger
  529. obj.foo++
  530. expect(scheduler).toHaveBeenCalledTimes(1)
  531. // should not run yet
  532. expect(dummy).toBe(1)
  533. // manually run
  534. runner()
  535. // should have run
  536. expect(dummy).toBe(2)
  537. })
  538. it('events: onTrack', () => {
  539. let events: DebuggerEvent[] = []
  540. let dummy
  541. const onTrack = jest.fn((e: DebuggerEvent) => {
  542. events.push(e)
  543. })
  544. const obj = reactive({ foo: 1, bar: 2 })
  545. const runner = effect(
  546. () => {
  547. dummy = obj.foo
  548. dummy = 'bar' in obj
  549. dummy = Object.keys(obj)
  550. },
  551. { onTrack }
  552. )
  553. expect(dummy).toEqual(['foo', 'bar'])
  554. expect(onTrack).toHaveBeenCalledTimes(3)
  555. expect(events).toEqual([
  556. {
  557. effect: runner,
  558. target: toRaw(obj),
  559. type: TrackOpTypes.GET,
  560. key: 'foo'
  561. },
  562. {
  563. effect: runner,
  564. target: toRaw(obj),
  565. type: TrackOpTypes.HAS,
  566. key: 'bar'
  567. },
  568. {
  569. effect: runner,
  570. target: toRaw(obj),
  571. type: TrackOpTypes.ITERATE,
  572. key: ITERATE_KEY
  573. }
  574. ])
  575. })
  576. it('events: onTrigger', () => {
  577. let events: DebuggerEvent[] = []
  578. let dummy
  579. const onTrigger = jest.fn((e: DebuggerEvent) => {
  580. events.push(e)
  581. })
  582. const obj = reactive({ foo: 1 })
  583. const runner = effect(
  584. () => {
  585. dummy = obj.foo
  586. },
  587. { onTrigger }
  588. )
  589. obj.foo++
  590. expect(dummy).toBe(2)
  591. expect(onTrigger).toHaveBeenCalledTimes(1)
  592. expect(events[0]).toEqual({
  593. effect: runner,
  594. target: toRaw(obj),
  595. type: TriggerOpTypes.SET,
  596. key: 'foo',
  597. oldValue: 1,
  598. newValue: 2
  599. })
  600. // @ts-ignore
  601. delete obj.foo
  602. expect(dummy).toBeUndefined()
  603. expect(onTrigger).toHaveBeenCalledTimes(2)
  604. expect(events[1]).toEqual({
  605. effect: runner,
  606. target: toRaw(obj),
  607. type: TriggerOpTypes.DELETE,
  608. key: 'foo',
  609. oldValue: 2
  610. })
  611. })
  612. it('stop', () => {
  613. let dummy
  614. const obj = reactive({ prop: 1 })
  615. const runner = effect(() => {
  616. dummy = obj.prop
  617. })
  618. obj.prop = 2
  619. expect(dummy).toBe(2)
  620. stop(runner)
  621. obj.prop = 3
  622. expect(dummy).toBe(2)
  623. // stopped effect should still be manually callable
  624. runner()
  625. expect(dummy).toBe(3)
  626. })
  627. it('stop with scheduler', () => {
  628. let dummy
  629. const obj = reactive({ prop: 1 })
  630. const queue: (() => void)[] = []
  631. const runner = effect(
  632. () => {
  633. dummy = obj.prop
  634. },
  635. {
  636. scheduler: e => queue.push(e)
  637. }
  638. )
  639. obj.prop = 2
  640. expect(dummy).toBe(1)
  641. expect(queue.length).toBe(1)
  642. stop(runner)
  643. // a scheduled effect should not execute anymore after stopped
  644. queue.forEach(e => e())
  645. expect(dummy).toBe(1)
  646. })
  647. it('events: onStop', () => {
  648. const onStop = jest.fn()
  649. const runner = effect(() => {}, {
  650. onStop
  651. })
  652. stop(runner)
  653. expect(onStop).toHaveBeenCalled()
  654. })
  655. it('stop: a stopped effect is nested in a normal effect', () => {
  656. let dummy
  657. const obj = reactive({ prop: 1 })
  658. const runner = effect(() => {
  659. dummy = obj.prop
  660. })
  661. stop(runner)
  662. obj.prop = 2
  663. expect(dummy).toBe(1)
  664. // observed value in inner stopped effect
  665. // will track outer effect as an dependency
  666. effect(() => {
  667. runner()
  668. })
  669. expect(dummy).toBe(2)
  670. // notify outer effect to run
  671. obj.prop = 3
  672. expect(dummy).toBe(3)
  673. })
  674. it('markRaw', () => {
  675. const obj = reactive({
  676. foo: markRaw({
  677. prop: 0
  678. })
  679. })
  680. let dummy
  681. effect(() => {
  682. dummy = obj.foo.prop
  683. })
  684. expect(dummy).toBe(0)
  685. obj.foo.prop++
  686. expect(dummy).toBe(0)
  687. obj.foo = { prop: 1 }
  688. expect(dummy).toBe(1)
  689. })
  690. it('should not be trigger when the value and the old value both are NaN', () => {
  691. const obj = reactive({
  692. foo: NaN
  693. })
  694. const fnSpy = jest.fn(() => obj.foo)
  695. effect(fnSpy)
  696. obj.foo = NaN
  697. expect(fnSpy).toHaveBeenCalledTimes(1)
  698. })
  699. it('should trigger all effects when array length is set to 0', () => {
  700. const observed: any = reactive([1])
  701. let dummy, record
  702. effect(() => {
  703. dummy = observed.length
  704. })
  705. effect(() => {
  706. record = observed[0]
  707. })
  708. expect(dummy).toBe(1)
  709. expect(record).toBe(1)
  710. observed[1] = 2
  711. expect(observed[1]).toBe(2)
  712. observed.unshift(3)
  713. expect(dummy).toBe(3)
  714. expect(record).toBe(3)
  715. observed.length = 0
  716. expect(dummy).toBe(0)
  717. expect(record).toBeUndefined()
  718. })
  719. })