| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- import {
- EffectScope,
- type GenericComponentInstance,
- currentInstance,
- getCurrentScope,
- nextTick,
- onBeforeUpdate,
- onUpdated,
- ref,
- watchEffect,
- watchPostEffect,
- watchSyncEffect,
- } from '@vue/runtime-dom'
- import { renderEffect, template } from '../src'
- import { RenderEffect } from '../src/renderEffect'
- import { onEffectCleanup } from '@vue/reactivity'
- import { makeRender } from './_utils'
- const define = makeRender<any>()
- const createDemo = (setupFn: () => any, renderFn: (ctx: any) => any) =>
- define({
- setup: () => {
- const returned = setupFn()
- Object.defineProperty(returned, '__isScriptSetup', {
- enumerable: false,
- value: true,
- })
- return returned
- },
- render: (ctx: any) => {
- const t0 = template('<div></div>')
- renderFn(ctx)
- return t0()
- },
- })
- describe('renderEffect', () => {
- test('initializes noLifecycle effect with raw effect function', () => {
- let calls = 0
- const fn = () => {
- calls++
- }
- const effect = new RenderEffect(fn, true)
- expect(effect.fn).toBe(fn)
- expect(effect.updateJob).toBe(undefined)
- effect.run()
- expect(calls).toBe(1)
- })
- test('creates update lifecycle job lazily', async () => {
- const effect = new RenderEffect(() => {})
- expect(effect.updateJob).toBe(undefined)
- const effects: RenderEffect[] = []
- const calls: string[] = []
- const { instance } = createDemo(
- () => {
- const source = ref(0)
- const update = () => source.value++
- onUpdated(() => calls.push(`updated ${source.value}`))
- return { source, update }
- },
- ctx => {
- const effect = new RenderEffect(() => {
- calls.push(`render ${ctx.source}`)
- })
- effects.push(effect)
- effect.run()
- },
- ).render()
- expect(effects[0].updateJob).toBe(undefined)
- expect(calls).toEqual(['render 0'])
- const { update } = instance?.setupState as any
- update()
- await nextTick()
- expect(effects[0].updateJob).toEqual(expect.any(Function))
- expect(calls).toEqual(['render 0', 'render 1', 'updated 1'])
- })
- test('creates update lifecycle job after hooks are registered late', async () => {
- const effects: RenderEffect[] = []
- const calls: string[] = []
- const { instance } = createDemo(
- () => {
- const source = ref(0)
- const update = () => source.value++
- const effect = new RenderEffect(() => {
- calls.push(`render ${source.value}`)
- })
- effects.push(effect)
- effect.run()
- onUpdated(() => calls.push(`updated ${source.value}`))
- return { update }
- },
- () => {},
- ).render()
- expect(effects[0].updateJob).toBe(undefined)
- expect(calls).toEqual(['render 0'])
- const { update } = instance?.setupState as any
- update()
- await nextTick()
- expect(effects[0].updateJob).toEqual(expect.any(Function))
- expect(calls).toEqual(['render 0', 'render 1', 'updated 1'])
- })
- test('basic', async () => {
- let dummy: any
- const source = ref(0)
- renderEffect(() => {
- dummy = source.value
- })
- expect(dummy).toBe(0)
- await nextTick()
- expect(dummy).toBe(0)
- source.value++
- expect(dummy).toBe(0)
- await nextTick()
- expect(dummy).toBe(1)
- source.value++
- expect(dummy).toBe(1)
- await nextTick()
- expect(dummy).toBe(2)
- source.value++
- expect(dummy).toBe(2)
- await nextTick()
- expect(dummy).toBe(3)
- })
- test('should run with the scheduling order', async () => {
- const calls: string[] = []
- const { instance } = createDemo(
- () => {
- // setup
- const source = ref(0)
- const renderSource = ref(0)
- const change = () => source.value++
- const changeRender = () => renderSource.value++
- // Life Cycle Hooks
- onUpdated(() => {
- calls.push(`updated ${source.value}`)
- })
- onBeforeUpdate(() => {
- calls.push(`beforeUpdate ${source.value}`)
- })
- // Watch API
- watchPostEffect(() => {
- const current = source.value
- calls.push(`post ${current}`)
- onEffectCleanup(() => calls.push(`post cleanup ${current}`))
- })
- watchEffect(() => {
- const current = source.value
- calls.push(`pre ${current}`)
- onEffectCleanup(() => calls.push(`pre cleanup ${current}`))
- })
- watchSyncEffect(() => {
- const current = source.value
- calls.push(`sync ${current}`)
- onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
- })
- return { source, change, renderSource, changeRender }
- },
- // render
- _ctx => {
- // Render Watch API
- renderEffect(() => {
- const current = _ctx.renderSource
- calls.push(`renderEffect ${current}`)
- onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
- })
- },
- ).render()
- const { change, changeRender } = instance?.setupState as any
- await nextTick()
- expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
- calls.length = 0
- // Update
- changeRender()
- change()
- expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
- calls.length = 0
- await nextTick()
- expect(calls).toEqual([
- 'pre cleanup 0',
- 'pre 1',
- 'renderEffect cleanup 0',
- 'beforeUpdate 1',
- 'renderEffect 1',
- 'post cleanup 0',
- 'post 1',
- 'updated 1',
- ])
- calls.length = 0
- // Update
- changeRender()
- change()
- expect(calls).toEqual(['sync cleanup 1', 'sync 2'])
- calls.length = 0
- await nextTick()
- expect(calls).toEqual([
- 'pre cleanup 1',
- 'pre 2',
- 'renderEffect cleanup 1',
- 'beforeUpdate 2',
- 'renderEffect 2',
- 'post cleanup 1',
- 'post 2',
- 'updated 2',
- ])
- })
- test('errors should include the execution location with beforeUpdate hook', async () => {
- const { instance } = createDemo(
- // setup
- () => {
- const source = ref()
- const update = () => source.value++
- onBeforeUpdate(() => {
- throw 'error in beforeUpdate'
- })
- return { source, update }
- },
- // render
- ctx => {
- renderEffect(() => {
- ctx.source
- })
- },
- ).render()
- const { update } = instance?.setupState as any
- await expect(async () => {
- update()
- await nextTick()
- }).rejects.toThrow('error in beforeUpdate')
- expect(
- '[Vue warn]: Unhandled error during execution of beforeUpdate hook',
- ).toHaveBeenWarned()
- expect(
- '[Vue warn]: Unhandled error during execution of component update',
- ).toHaveBeenWarned()
- })
- test('should restore update state when render throws during update', async () => {
- const calls: string[] = []
- const { instance } = createDemo(
- // setup
- () => {
- const source = ref(0)
- const update = () => source.value++
- onBeforeUpdate(() => calls.push(`beforeUpdate ${source.value}`))
- onUpdated(() => calls.push(`updated ${source.value}`))
- return { source, update }
- },
- // render
- ctx => {
- renderEffect(() => {
- calls.push(`render ${ctx.source}`)
- if (ctx.source === 1) {
- throw new Error('error in render')
- }
- })
- },
- ).render()
- const { update } = instance?.setupState as any
- expect(calls).toEqual(['render 0'])
- calls.length = 0
- update()
- await expect(nextTick()).rejects.toThrow('error in render')
- expect(
- '[Vue warn]: Unhandled error during execution of component update',
- ).toHaveBeenWarned()
- expect(currentInstance).toBe(null)
- expect((instance as any).isUpdating).toBe(false)
- calls.length = 0
- update()
- await nextTick()
- expect(calls).toEqual(['beforeUpdate 2', 'render 2', 'updated 2'])
- expect((instance as any).isUpdating).toBe(false)
- })
- test('errors should include the execution location with updated hook', async () => {
- const { instance } = createDemo(
- // setup
- () => {
- const source = ref(0)
- const update = () => source.value++
- onUpdated(() => {
- throw 'error in updated'
- })
- return { source, update }
- },
- // render
- ctx => {
- renderEffect(() => {
- ctx.source
- })
- },
- ).render()
- const { update } = instance?.setupState as any
- await expect(async () => {
- update()
- await nextTick()
- }).rejects.toThrow('error in updated')
- expect(
- '[Vue warn]: Unhandled error during execution of updated',
- ).toHaveBeenWarned()
- })
- test('should be called with the current instance and current scope', async () => {
- const source = ref(0)
- const scope = new EffectScope()
- let instanceSnap: GenericComponentInstance | null = null
- let scopeSnap: EffectScope | undefined = undefined
- const { instance } = define(() => {
- scope.run(() => {
- renderEffect(() => {
- source.value
- instanceSnap = currentInstance
- scopeSnap = getCurrentScope()
- })
- })
- return []
- }).render()
- expect(instanceSnap).toBe(instance)
- expect(scopeSnap).toBe(scope)
- source.value++
- await nextTick()
- expect(instanceSnap).toBe(instance)
- expect(scopeSnap).toBe(scope)
- })
- })
|