effect.spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. import {
  2. reactive,
  3. effect,
  4. stop,
  5. toRaw,
  6. TrackOpTypes,
  7. TriggerOpTypes,
  8. DebuggerEvent,
  9. markRaw,
  10. shallowReactive
  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. // @ts-ignore
  60. delete obj.prop
  61. expect(dummy).toBe(undefined)
  62. })
  63. it('should observe has operations', () => {
  64. let dummy
  65. const obj = reactive<{ prop: string | number }>({ prop: 'value' })
  66. effect(() => (dummy = 'prop' in obj))
  67. expect(dummy).toBe(true)
  68. // @ts-ignore
  69. delete obj.prop
  70. expect(dummy).toBe(false)
  71. obj.prop = 12
  72. expect(dummy).toBe(true)
  73. })
  74. it('should observe properties on the prototype chain', () => {
  75. let dummy
  76. const counter = reactive({ num: 0 })
  77. const parentCounter = reactive({ num: 2 })
  78. Object.setPrototypeOf(counter, parentCounter)
  79. effect(() => (dummy = counter.num))
  80. expect(dummy).toBe(0)
  81. // @ts-ignore
  82. delete counter.num
  83. expect(dummy).toBe(2)
  84. parentCounter.num = 4
  85. expect(dummy).toBe(4)
  86. counter.num = 3
  87. expect(dummy).toBe(3)
  88. })
  89. it('should observe has operations on the prototype chain', () => {
  90. let dummy
  91. const counter = reactive({ num: 0 })
  92. const parentCounter = reactive({ num: 2 })
  93. Object.setPrototypeOf(counter, parentCounter)
  94. effect(() => (dummy = 'num' in counter))
  95. expect(dummy).toBe(true)
  96. // @ts-ignore
  97. delete counter.num
  98. expect(dummy).toBe(true)
  99. // @ts-ignore
  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]: '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. // @ts-ignore
  197. delete obj[key]
  198. expect(dummy).toBe(undefined)
  199. expect(hasDummy).toBe(false)
  200. })
  201. it('should not observe well-known symbol keyed properties', () => {
  202. const key = Symbol.isConcatSpreadable
  203. let dummy
  204. const array: any = reactive([])
  205. effect(() => (dummy = array[key]))
  206. expect(array[key]).toBe(undefined)
  207. expect(dummy).toBe(undefined)
  208. array[key] = true
  209. expect(array[key]).toBe(true)
  210. expect(dummy).toBe(undefined)
  211. })
  212. it('should observe function valued properties', () => {
  213. const oldFunc = () => {}
  214. const newFunc = () => {}
  215. let dummy
  216. const obj = reactive({ func: oldFunc })
  217. effect(() => (dummy = obj.func))
  218. expect(dummy).toBe(oldFunc)
  219. obj.func = newFunc
  220. expect(dummy).toBe(newFunc)
  221. })
  222. it('should observe chained getters relying on this', () => {
  223. const obj = reactive({
  224. a: 1,
  225. get b() {
  226. return this.a
  227. }
  228. })
  229. let dummy
  230. effect(() => (dummy = obj.b))
  231. expect(dummy).toBe(1)
  232. obj.a++
  233. expect(dummy).toBe(2)
  234. })
  235. it('should observe methods relying on this', () => {
  236. const obj = reactive({
  237. a: 1,
  238. b() {
  239. return this.a
  240. }
  241. })
  242. let dummy
  243. effect(() => (dummy = obj.b()))
  244. expect(dummy).toBe(1)
  245. obj.a++
  246. expect(dummy).toBe(2)
  247. })
  248. it('should not observe set operations without a value change', () => {
  249. let hasDummy, getDummy
  250. const obj = reactive({ prop: 'value' })
  251. const getSpy = jest.fn(() => (getDummy = obj.prop))
  252. const hasSpy = jest.fn(() => (hasDummy = 'prop' in obj))
  253. effect(getSpy)
  254. effect(hasSpy)
  255. expect(getDummy).toBe('value')
  256. expect(hasDummy).toBe(true)
  257. obj.prop = 'value'
  258. expect(getSpy).toHaveBeenCalledTimes(1)
  259. expect(hasSpy).toHaveBeenCalledTimes(1)
  260. expect(getDummy).toBe('value')
  261. expect(hasDummy).toBe(true)
  262. })
  263. it('should not observe raw mutations', () => {
  264. let dummy
  265. const obj = reactive<{ prop?: string }>({})
  266. effect(() => (dummy = toRaw(obj).prop))
  267. expect(dummy).toBe(undefined)
  268. obj.prop = 'value'
  269. expect(dummy).toBe(undefined)
  270. })
  271. it('should not be triggered by raw mutations', () => {
  272. let dummy
  273. const obj = reactive<{ prop?: string }>({})
  274. effect(() => (dummy = obj.prop))
  275. expect(dummy).toBe(undefined)
  276. toRaw(obj).prop = 'value'
  277. expect(dummy).toBe(undefined)
  278. })
  279. it('should not be triggered by inherited raw setters', () => {
  280. let dummy, parentDummy, hiddenValue: any
  281. const obj = reactive<{ prop?: number }>({})
  282. const parent = reactive({
  283. set prop(value) {
  284. hiddenValue = value
  285. },
  286. get prop() {
  287. return hiddenValue
  288. }
  289. })
  290. Object.setPrototypeOf(obj, parent)
  291. effect(() => (dummy = obj.prop))
  292. effect(() => (parentDummy = parent.prop))
  293. expect(dummy).toBe(undefined)
  294. expect(parentDummy).toBe(undefined)
  295. toRaw(obj).prop = 4
  296. expect(dummy).toBe(undefined)
  297. expect(parentDummy).toBe(undefined)
  298. })
  299. it('should avoid implicit infinite recursive loops with itself', () => {
  300. const counter = reactive({ num: 0 })
  301. const counterSpy = jest.fn(() => counter.num++)
  302. effect(counterSpy)
  303. expect(counter.num).toBe(1)
  304. expect(counterSpy).toHaveBeenCalledTimes(1)
  305. counter.num = 4
  306. expect(counter.num).toBe(5)
  307. expect(counterSpy).toHaveBeenCalledTimes(2)
  308. })
  309. it('should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift', () => {
  310. ;(['push', 'unshift'] as const).forEach(key => {
  311. const arr = reactive<number[]>([])
  312. const counterSpy1 = jest.fn(() => (arr[key] as any)(1))
  313. const counterSpy2 = jest.fn(() => (arr[key] as any)(2))
  314. effect(counterSpy1)
  315. effect(counterSpy2)
  316. expect(arr.length).toBe(2)
  317. expect(counterSpy1).toHaveBeenCalledTimes(1)
  318. expect(counterSpy2).toHaveBeenCalledTimes(1)
  319. })
  320. ;(['pop', 'shift'] as const).forEach(key => {
  321. const arr = reactive<number[]>([1, 2, 3, 4])
  322. const counterSpy1 = jest.fn(() => (arr[key] as any)())
  323. const counterSpy2 = jest.fn(() => (arr[key] as any)())
  324. effect(counterSpy1)
  325. effect(counterSpy2)
  326. expect(arr.length).toBe(2)
  327. expect(counterSpy1).toHaveBeenCalledTimes(1)
  328. expect(counterSpy2).toHaveBeenCalledTimes(1)
  329. })
  330. })
  331. it('should allow explicitly recursive raw function loops', () => {
  332. const counter = reactive({ num: 0 })
  333. const numSpy = jest.fn(() => {
  334. counter.num++
  335. if (counter.num < 10) {
  336. numSpy()
  337. }
  338. })
  339. effect(numSpy)
  340. expect(counter.num).toEqual(10)
  341. expect(numSpy).toHaveBeenCalledTimes(10)
  342. })
  343. it('should avoid infinite loops with other effects', () => {
  344. const nums = reactive({ num1: 0, num2: 1 })
  345. const spy1 = jest.fn(() => (nums.num1 = nums.num2))
  346. const spy2 = jest.fn(() => (nums.num2 = nums.num1))
  347. effect(spy1)
  348. effect(spy2)
  349. expect(nums.num1).toBe(1)
  350. expect(nums.num2).toBe(1)
  351. expect(spy1).toHaveBeenCalledTimes(1)
  352. expect(spy2).toHaveBeenCalledTimes(1)
  353. nums.num2 = 4
  354. expect(nums.num1).toBe(4)
  355. expect(nums.num2).toBe(4)
  356. expect(spy1).toHaveBeenCalledTimes(2)
  357. expect(spy2).toHaveBeenCalledTimes(2)
  358. nums.num1 = 10
  359. expect(nums.num1).toBe(10)
  360. expect(nums.num2).toBe(10)
  361. expect(spy1).toHaveBeenCalledTimes(3)
  362. expect(spy2).toHaveBeenCalledTimes(3)
  363. })
  364. it('should return a new reactive version of the function', () => {
  365. function greet() {
  366. return 'Hello World'
  367. }
  368. const effect1 = effect(greet)
  369. const effect2 = effect(greet)
  370. expect(typeof effect1).toBe('function')
  371. expect(typeof effect2).toBe('function')
  372. expect(effect1).not.toBe(greet)
  373. expect(effect1).not.toBe(effect2)
  374. })
  375. it('should discover new branches while running automatically', () => {
  376. let dummy
  377. const obj = reactive({ prop: 'value', run: false })
  378. const conditionalSpy = jest.fn(() => {
  379. dummy = obj.run ? obj.prop : 'other'
  380. })
  381. effect(conditionalSpy)
  382. expect(dummy).toBe('other')
  383. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  384. obj.prop = 'Hi'
  385. expect(dummy).toBe('other')
  386. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  387. obj.run = true
  388. expect(dummy).toBe('Hi')
  389. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  390. obj.prop = 'World'
  391. expect(dummy).toBe('World')
  392. expect(conditionalSpy).toHaveBeenCalledTimes(3)
  393. })
  394. it('should discover new branches when running manually', () => {
  395. let dummy
  396. let run = false
  397. const obj = reactive({ prop: 'value' })
  398. const runner = effect(() => {
  399. dummy = run ? obj.prop : 'other'
  400. })
  401. expect(dummy).toBe('other')
  402. runner()
  403. expect(dummy).toBe('other')
  404. run = true
  405. runner()
  406. expect(dummy).toBe('value')
  407. obj.prop = 'World'
  408. expect(dummy).toBe('World')
  409. })
  410. it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
  411. let dummy
  412. const obj = reactive({ prop: 'value', run: true })
  413. const conditionalSpy = jest.fn(() => {
  414. dummy = obj.run ? obj.prop : 'other'
  415. })
  416. effect(conditionalSpy)
  417. expect(dummy).toBe('value')
  418. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  419. obj.run = false
  420. expect(dummy).toBe('other')
  421. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  422. obj.prop = 'value2'
  423. expect(dummy).toBe('other')
  424. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  425. })
  426. it('should not double wrap if the passed function is a effect', () => {
  427. const runner = effect(() => {})
  428. const otherRunner = effect(runner)
  429. expect(runner).not.toBe(otherRunner)
  430. expect(runner.raw).toBe(otherRunner.raw)
  431. })
  432. it('should not run multiple times for a single mutation', () => {
  433. let dummy
  434. const obj = reactive<Record<string, number>>({})
  435. const fnSpy = jest.fn(() => {
  436. for (const key in obj) {
  437. dummy = obj[key]
  438. }
  439. dummy = obj.prop
  440. })
  441. effect(fnSpy)
  442. expect(fnSpy).toHaveBeenCalledTimes(1)
  443. obj.prop = 16
  444. expect(dummy).toBe(16)
  445. expect(fnSpy).toHaveBeenCalledTimes(2)
  446. })
  447. it('should allow nested effects', () => {
  448. const nums = reactive({ num1: 0, num2: 1, num3: 2 })
  449. const dummy: any = {}
  450. const childSpy = jest.fn(() => (dummy.num1 = nums.num1))
  451. const childeffect = effect(childSpy)
  452. const parentSpy = jest.fn(() => {
  453. dummy.num2 = nums.num2
  454. childeffect()
  455. dummy.num3 = nums.num3
  456. })
  457. effect(parentSpy)
  458. expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 })
  459. expect(parentSpy).toHaveBeenCalledTimes(1)
  460. expect(childSpy).toHaveBeenCalledTimes(2)
  461. // this should only call the childeffect
  462. nums.num1 = 4
  463. expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 })
  464. expect(parentSpy).toHaveBeenCalledTimes(1)
  465. expect(childSpy).toHaveBeenCalledTimes(3)
  466. // this calls the parenteffect, which calls the childeffect once
  467. nums.num2 = 10
  468. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 })
  469. expect(parentSpy).toHaveBeenCalledTimes(2)
  470. expect(childSpy).toHaveBeenCalledTimes(4)
  471. // this calls the parenteffect, which calls the childeffect once
  472. nums.num3 = 7
  473. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 })
  474. expect(parentSpy).toHaveBeenCalledTimes(3)
  475. expect(childSpy).toHaveBeenCalledTimes(5)
  476. })
  477. it('should observe json methods', () => {
  478. let dummy = <Record<string, number>>{}
  479. const obj = reactive<Record<string, number>>({})
  480. effect(() => {
  481. dummy = JSON.parse(JSON.stringify(obj))
  482. })
  483. obj.a = 1
  484. expect(dummy.a).toBe(1)
  485. })
  486. it('should observe class method invocations', () => {
  487. class Model {
  488. count: number
  489. constructor() {
  490. this.count = 0
  491. }
  492. inc() {
  493. this.count++
  494. }
  495. }
  496. const model = reactive(new Model())
  497. let dummy
  498. effect(() => {
  499. dummy = model.count
  500. })
  501. expect(dummy).toBe(0)
  502. model.inc()
  503. expect(dummy).toBe(1)
  504. })
  505. it('lazy', () => {
  506. const obj = reactive({ foo: 1 })
  507. let dummy
  508. const runner = effect(() => (dummy = obj.foo), { lazy: true })
  509. expect(dummy).toBe(undefined)
  510. expect(runner()).toBe(1)
  511. expect(dummy).toBe(1)
  512. obj.foo = 2
  513. expect(dummy).toBe(2)
  514. })
  515. it('scheduler', () => {
  516. let runner: any, dummy
  517. const scheduler = jest.fn(_runner => {
  518. runner = _runner
  519. })
  520. const obj = reactive({ foo: 1 })
  521. effect(
  522. () => {
  523. dummy = obj.foo
  524. },
  525. { scheduler }
  526. )
  527. expect(scheduler).not.toHaveBeenCalled()
  528. expect(dummy).toBe(1)
  529. // should be called on first trigger
  530. obj.foo++
  531. expect(scheduler).toHaveBeenCalledTimes(1)
  532. // should not run yet
  533. expect(dummy).toBe(1)
  534. // manually run
  535. runner()
  536. // should have run
  537. expect(dummy).toBe(2)
  538. })
  539. it('events: onTrack', () => {
  540. let events: DebuggerEvent[] = []
  541. let dummy
  542. const onTrack = jest.fn((e: DebuggerEvent) => {
  543. events.push(e)
  544. })
  545. const obj = reactive({ foo: 1, bar: 2 })
  546. const runner = effect(
  547. () => {
  548. dummy = obj.foo
  549. dummy = 'bar' in obj
  550. dummy = Object.keys(obj)
  551. },
  552. { onTrack }
  553. )
  554. expect(dummy).toEqual(['foo', 'bar'])
  555. expect(onTrack).toHaveBeenCalledTimes(3)
  556. expect(events).toEqual([
  557. {
  558. effect: runner,
  559. target: toRaw(obj),
  560. type: TrackOpTypes.GET,
  561. key: 'foo'
  562. },
  563. {
  564. effect: runner,
  565. target: toRaw(obj),
  566. type: TrackOpTypes.HAS,
  567. key: 'bar'
  568. },
  569. {
  570. effect: runner,
  571. target: toRaw(obj),
  572. type: TrackOpTypes.ITERATE,
  573. key: ITERATE_KEY
  574. }
  575. ])
  576. })
  577. it('events: onTrigger', () => {
  578. let events: DebuggerEvent[] = []
  579. let dummy
  580. const onTrigger = jest.fn((e: DebuggerEvent) => {
  581. events.push(e)
  582. })
  583. const obj = reactive({ foo: 1 })
  584. const runner = effect(
  585. () => {
  586. dummy = obj.foo
  587. },
  588. { onTrigger }
  589. )
  590. obj.foo++
  591. expect(dummy).toBe(2)
  592. expect(onTrigger).toHaveBeenCalledTimes(1)
  593. expect(events[0]).toEqual({
  594. effect: runner,
  595. target: toRaw(obj),
  596. type: TriggerOpTypes.SET,
  597. key: 'foo',
  598. oldValue: 1,
  599. newValue: 2
  600. })
  601. // @ts-ignore
  602. delete obj.foo
  603. expect(dummy).toBeUndefined()
  604. expect(onTrigger).toHaveBeenCalledTimes(2)
  605. expect(events[1]).toEqual({
  606. effect: runner,
  607. target: toRaw(obj),
  608. type: TriggerOpTypes.DELETE,
  609. key: 'foo',
  610. oldValue: 2
  611. })
  612. })
  613. it('stop', () => {
  614. let dummy
  615. const obj = reactive({ prop: 1 })
  616. const runner = effect(() => {
  617. dummy = obj.prop
  618. })
  619. obj.prop = 2
  620. expect(dummy).toBe(2)
  621. stop(runner)
  622. obj.prop = 3
  623. expect(dummy).toBe(2)
  624. // stopped effect should still be manually callable
  625. runner()
  626. expect(dummy).toBe(3)
  627. })
  628. it('stop with scheduler', () => {
  629. let dummy
  630. const obj = reactive({ prop: 1 })
  631. const queue: (() => void)[] = []
  632. const runner = effect(
  633. () => {
  634. dummy = obj.prop
  635. },
  636. {
  637. scheduler: e => queue.push(e)
  638. }
  639. )
  640. obj.prop = 2
  641. expect(dummy).toBe(1)
  642. expect(queue.length).toBe(1)
  643. stop(runner)
  644. // a scheduled effect should not execute anymore after stopped
  645. queue.forEach(e => e())
  646. expect(dummy).toBe(1)
  647. })
  648. it('events: onStop', () => {
  649. const onStop = jest.fn()
  650. const runner = effect(() => {}, {
  651. onStop
  652. })
  653. stop(runner)
  654. expect(onStop).toHaveBeenCalled()
  655. })
  656. it('stop: a stopped effect is nested in a normal effect', () => {
  657. let dummy
  658. const obj = reactive({ prop: 1 })
  659. const runner = effect(() => {
  660. dummy = obj.prop
  661. })
  662. stop(runner)
  663. obj.prop = 2
  664. expect(dummy).toBe(1)
  665. // observed value in inner stopped effect
  666. // will track outer effect as an dependency
  667. effect(() => {
  668. runner()
  669. })
  670. expect(dummy).toBe(2)
  671. // notify outer effect to run
  672. obj.prop = 3
  673. expect(dummy).toBe(3)
  674. })
  675. it('markRaw', () => {
  676. const obj = reactive({
  677. foo: markRaw({
  678. prop: 0
  679. })
  680. })
  681. let dummy
  682. effect(() => {
  683. dummy = obj.foo.prop
  684. })
  685. expect(dummy).toBe(0)
  686. obj.foo.prop++
  687. expect(dummy).toBe(0)
  688. obj.foo = { prop: 1 }
  689. expect(dummy).toBe(1)
  690. })
  691. it('should not be trigger when the value and the old value both are NaN', () => {
  692. const obj = reactive({
  693. foo: NaN
  694. })
  695. const fnSpy = jest.fn(() => obj.foo)
  696. effect(fnSpy)
  697. obj.foo = NaN
  698. expect(fnSpy).toHaveBeenCalledTimes(1)
  699. })
  700. it('should trigger all effects when array length is set to 0', () => {
  701. const observed: any = reactive([1])
  702. let dummy, record
  703. effect(() => {
  704. dummy = observed.length
  705. })
  706. effect(() => {
  707. record = observed[0]
  708. })
  709. expect(dummy).toBe(1)
  710. expect(record).toBe(1)
  711. observed[1] = 2
  712. expect(observed[1]).toBe(2)
  713. observed.unshift(3)
  714. expect(dummy).toBe(3)
  715. expect(record).toBe(3)
  716. observed.length = 0
  717. expect(dummy).toBe(0)
  718. expect(record).toBeUndefined()
  719. })
  720. it('should trigger once effect when set the equal proxy', () => {
  721. const obj = reactive({ foo: 1 })
  722. const observed: any = reactive({ obj })
  723. const fnSpy = jest.fn(() => observed.obj)
  724. effect(fnSpy)
  725. observed.obj = obj
  726. expect(fnSpy).toHaveBeenCalledTimes(1)
  727. const obj2 = reactive({ foo: 1 })
  728. const observed2: any = shallowReactive({ obj2 })
  729. const fnSpy2 = jest.fn(() => observed2.obj2)
  730. effect(fnSpy2)
  731. observed2.obj2 = obj2
  732. expect(fnSpy2).toHaveBeenCalledTimes(1)
  733. })
  734. })