renderEffect.spec.ts 5.7 KB

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