renderEffect.spec.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import {
  2. EffectScope,
  3. type GenericComponentInstance,
  4. currentInstance,
  5. getCurrentScope,
  6. nextTick,
  7. onBeforeUpdate,
  8. onUpdated,
  9. ref,
  10. watchEffect,
  11. watchPostEffect,
  12. watchSyncEffect,
  13. } from '@vue/runtime-dom'
  14. import { renderEffect, template } from '../src'
  15. import { onEffectCleanup } from '@vue/reactivity'
  16. import { makeRender } from './_utils'
  17. const define = makeRender<any>()
  18. const createDemo = (setupFn: () => any, renderFn: (ctx: any) => any) =>
  19. define({
  20. setup: () => {
  21. const returned = setupFn()
  22. Object.defineProperty(returned, '__isScriptSetup', {
  23. enumerable: false,
  24. value: true,
  25. })
  26. return returned
  27. },
  28. render: (ctx: any) => {
  29. const t0 = template('<div></div>')
  30. renderFn(ctx)
  31. return t0()
  32. },
  33. })
  34. describe('renderEffect', () => {
  35. test('basic', async () => {
  36. let dummy: any
  37. const source = ref(0)
  38. renderEffect(() => {
  39. dummy = source.value
  40. })
  41. expect(dummy).toBe(0)
  42. await nextTick()
  43. expect(dummy).toBe(0)
  44. source.value++
  45. expect(dummy).toBe(0)
  46. await nextTick()
  47. expect(dummy).toBe(1)
  48. source.value++
  49. expect(dummy).toBe(1)
  50. await nextTick()
  51. expect(dummy).toBe(2)
  52. source.value++
  53. expect(dummy).toBe(2)
  54. await nextTick()
  55. expect(dummy).toBe(3)
  56. })
  57. test('should run with the scheduling order', async () => {
  58. const calls: string[] = []
  59. const { instance } = createDemo(
  60. () => {
  61. // setup
  62. const source = ref(0)
  63. const renderSource = ref(0)
  64. const change = () => source.value++
  65. const changeRender = () => renderSource.value++
  66. // Life Cycle Hooks
  67. onUpdated(() => {
  68. calls.push(`updated ${source.value}`)
  69. })
  70. onBeforeUpdate(() => {
  71. calls.push(`beforeUpdate ${source.value}`)
  72. })
  73. // Watch API
  74. watchPostEffect(() => {
  75. const current = source.value
  76. calls.push(`post ${current}`)
  77. onEffectCleanup(() => calls.push(`post cleanup ${current}`))
  78. })
  79. watchEffect(() => {
  80. const current = source.value
  81. calls.push(`pre ${current}`)
  82. onEffectCleanup(() => calls.push(`pre cleanup ${current}`))
  83. })
  84. watchSyncEffect(() => {
  85. const current = source.value
  86. calls.push(`sync ${current}`)
  87. onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
  88. })
  89. return { source, change, renderSource, changeRender }
  90. },
  91. // render
  92. _ctx => {
  93. // Render Watch API
  94. renderEffect(() => {
  95. const current = _ctx.renderSource
  96. calls.push(`renderEffect ${current}`)
  97. onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
  98. })
  99. },
  100. ).render()
  101. const { change, changeRender } = instance?.setupState as any
  102. await nextTick()
  103. expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
  104. calls.length = 0
  105. // Update
  106. changeRender()
  107. change()
  108. expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
  109. calls.length = 0
  110. await nextTick()
  111. expect(calls).toEqual([
  112. 'pre cleanup 0',
  113. 'pre 1',
  114. 'renderEffect cleanup 0',
  115. 'beforeUpdate 1',
  116. 'renderEffect 1',
  117. 'post cleanup 0',
  118. 'post 1',
  119. 'updated 1',
  120. ])
  121. calls.length = 0
  122. // Update
  123. changeRender()
  124. change()
  125. expect(calls).toEqual(['sync cleanup 1', 'sync 2'])
  126. calls.length = 0
  127. await nextTick()
  128. expect(calls).toEqual([
  129. 'pre cleanup 1',
  130. 'pre 2',
  131. 'renderEffect cleanup 1',
  132. 'beforeUpdate 2',
  133. 'renderEffect 2',
  134. 'post cleanup 1',
  135. 'post 2',
  136. 'updated 2',
  137. ])
  138. })
  139. test('errors should include the execution location with beforeUpdate hook', async () => {
  140. const { instance } = createDemo(
  141. // setup
  142. () => {
  143. const source = ref()
  144. const update = () => source.value++
  145. onBeforeUpdate(() => {
  146. throw 'error in beforeUpdate'
  147. })
  148. return { source, update }
  149. },
  150. // render
  151. ctx => {
  152. renderEffect(() => {
  153. ctx.source
  154. })
  155. },
  156. ).render()
  157. const { update } = instance?.setupState as any
  158. await expect(async () => {
  159. update()
  160. await nextTick()
  161. }).rejects.toThrow('error in beforeUpdate')
  162. expect(
  163. '[Vue warn]: Unhandled error during execution of beforeUpdate hook',
  164. ).toHaveBeenWarned()
  165. expect(
  166. '[Vue warn]: Unhandled error during execution of component update',
  167. ).toHaveBeenWarned()
  168. })
  169. test('errors should include the execution location with updated hook', async () => {
  170. const { instance } = createDemo(
  171. // setup
  172. () => {
  173. const source = ref(0)
  174. const update = () => source.value++
  175. onUpdated(() => {
  176. throw 'error in updated'
  177. })
  178. return { source, update }
  179. },
  180. // render
  181. ctx => {
  182. renderEffect(() => {
  183. ctx.source
  184. })
  185. },
  186. ).render()
  187. const { update } = instance?.setupState as any
  188. await expect(async () => {
  189. update()
  190. await nextTick()
  191. }).rejects.toThrow('error in updated')
  192. expect(
  193. '[Vue warn]: Unhandled error during execution of updated',
  194. ).toHaveBeenWarned()
  195. })
  196. test('should be called with the current instance and current scope', async () => {
  197. const source = ref(0)
  198. const scope = new EffectScope()
  199. let instanceSnap: GenericComponentInstance | null = null
  200. let scopeSnap: EffectScope | undefined = undefined
  201. const { instance } = define(() => {
  202. scope.run(() => {
  203. renderEffect(() => {
  204. source.value
  205. instanceSnap = currentInstance
  206. scopeSnap = getCurrentScope()
  207. })
  208. })
  209. return []
  210. }).render()
  211. expect(instanceSnap).toBe(instance)
  212. expect(scopeSnap).toBe(scope)
  213. source.value++
  214. await nextTick()
  215. expect(instanceSnap).toBe(instance)
  216. expect(scopeSnap).toBe(scope)
  217. })
  218. })