effect.spec.ts 21 KB

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