test(vapor): renderEffect

This commit is contained in:
Evan You 2024-12-09 18:35:41 +08:00
parent 2bbb6d2fc5
commit ec23ab9e3a
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
4 changed files with 43 additions and 34 deletions

View File

@ -1,18 +1,17 @@
import { import { createVaporApp, defineVaporComponent } from '../src'
type App, import type { App } from '@vue/runtime-dom'
type Component, import type {
type ComponentInternalInstance, ObjectVaporComponent,
type ObjectComponent, VaporComponent,
type SetupFn, VaporComponentInstance,
createVaporApp, VaporSetupFn,
defineComponent, } from '../src/component'
} from '../src/_old' import type { RawProps } from '../src/componentProps'
import type { RawProps } from '../src/_old/componentProps'
export interface RenderContext { export interface RenderContext {
component: Component component: VaporComponent
host: HTMLElement host: HTMLElement
instance: ComponentInternalInstance | undefined instance: VaporComponentInstance | undefined
app: App app: App
create: (props?: RawProps) => RenderContext create: (props?: RawProps) => RenderContext
mount: (container?: string | ParentNode) => RenderContext mount: (container?: string | ParentNode) => RenderContext
@ -21,7 +20,7 @@ export interface RenderContext {
html: () => string html: () => string
} }
export function makeRender<C = ObjectComponent | SetupFn>( export function makeRender<C = ObjectVaporComponent | VaporSetupFn>(
initHost = (): HTMLDivElement => { initHost = (): HTMLDivElement => {
const host = document.createElement('div') const host = document.createElement('div')
host.setAttribute('id', 'host') host.setAttribute('id', 'host')
@ -42,8 +41,8 @@ export function makeRender<C = ObjectComponent | SetupFn>(
}) })
function define(comp: C) { function define(comp: C) {
const component = defineComponent(comp as any) const component = defineVaporComponent(comp as any)
let instance: ComponentInternalInstance | undefined let instance: VaporComponentInstance | undefined
let app: App let app: App
function render( function render(
@ -61,7 +60,7 @@ export function makeRender<C = ObjectComponent | SetupFn>(
} }
function mount(container: string | ParentNode = host) { function mount(container: string | ParentNode = host) {
instance = app.mount(container) instance = app.mount(container) as any as VaporComponentInstance
return res() return res()
} }

View File

@ -1,21 +1,18 @@
import { import {
EffectScope, EffectScope,
type GenericComponentInstance,
currentInstance,
getCurrentScope, getCurrentScope,
nextTick, nextTick,
onBeforeUpdate, onBeforeUpdate,
onEffectCleanup,
onUpdated, onUpdated,
ref, ref,
renderEffect,
template,
watchEffect, watchEffect,
watchPostEffect, watchPostEffect,
watchSyncEffect, watchSyncEffect,
} from '../src/_old' } from '@vue/runtime-dom'
import { import { renderEffect, template } from '../src'
type ComponentInternalInstance, import { onEffectCleanup } from '@vue/reactivity'
currentInstance,
} from '../src/_old/component'
import { makeRender } from './_utils' import { makeRender } from './_utils'
const define = makeRender<any>() const define = makeRender<any>()
@ -110,8 +107,10 @@ describe('renderEffect', () => {
}) })
}, },
).render() ).render()
const { change, changeRender } = instance?.setupState as any const { change, changeRender } = instance?.setupState as any
await nextTick()
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0']) expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
calls.length = 0 calls.length = 0
@ -126,8 +125,8 @@ describe('renderEffect', () => {
expect(calls).toEqual([ expect(calls).toEqual([
'pre cleanup 0', 'pre cleanup 0',
'pre 1', 'pre 1',
'beforeUpdate 1',
'renderEffect cleanup 0', 'renderEffect cleanup 0',
'beforeUpdate 1',
'renderEffect 1', 'renderEffect 1',
'post cleanup 0', 'post cleanup 0',
'post 1', 'post 1',
@ -146,8 +145,8 @@ describe('renderEffect', () => {
expect(calls).toEqual([ expect(calls).toEqual([
'pre cleanup 1', 'pre cleanup 1',
'pre 2', 'pre 2',
'beforeUpdate 2',
'renderEffect cleanup 1', 'renderEffect cleanup 1',
'beforeUpdate 2',
'renderEffect 2', 'renderEffect 2',
'post cleanup 1', 'post cleanup 1',
'post 2', 'post 2',
@ -174,6 +173,7 @@ describe('renderEffect', () => {
}, },
).render() ).render()
const { update } = instance?.setupState as any const { update } = instance?.setupState as any
await expect(async () => { await expect(async () => {
update() update()
await nextTick() await nextTick()
@ -182,6 +182,9 @@ describe('renderEffect', () => {
expect( expect(
'[Vue warn]: Unhandled error during execution of beforeUpdate hook', '[Vue warn]: Unhandled error during execution of beforeUpdate hook',
).toHaveBeenWarned() ).toHaveBeenWarned()
expect(
'[Vue warn]: Unhandled error during execution of component update',
).toHaveBeenWarned()
}) })
test('errors should include the execution location with updated hook', async () => { test('errors should include the execution location with updated hook', async () => {
@ -204,6 +207,7 @@ describe('renderEffect', () => {
).render() ).render()
const { update } = instance?.setupState as any const { update } = instance?.setupState as any
await expect(async () => { await expect(async () => {
update() update()
await nextTick() await nextTick()
@ -217,15 +221,17 @@ describe('renderEffect', () => {
test('should be called with the current instance and current scope', async () => { test('should be called with the current instance and current scope', async () => {
const source = ref(0) const source = ref(0)
const scope = new EffectScope() const scope = new EffectScope()
let instanceSnap: ComponentInternalInstance | null = null let instanceSnap: GenericComponentInstance | null = null
let scopeSnap: EffectScope | undefined = undefined let scopeSnap: EffectScope | undefined = undefined
const { instance } = define(() => { const { instance } = define(() => {
scope.run(() => { scope.run(() => {
renderEffect(() => { renderEffect(() => {
source.value
instanceSnap = currentInstance instanceSnap = currentInstance
scopeSnap = getCurrentScope() scopeSnap = getCurrentScope()
}) })
}) })
return []
}).render() }).render()
expect(instanceSnap).toBe(instance) expect(instanceSnap).toBe(instance)

View File

@ -34,8 +34,7 @@ export const createVaporApp: CreateAppFunction<
ParentNode, ParentNode,
VaporComponent VaporComponent
> = comp => { > = comp => {
if (!_createApp) if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, i => i)
_createApp = createAppAPI(mountApp, unmountApp, i => i.exposed)
const app = _createApp(comp) const app = _createApp(comp)
const mount = app.mount const mount = app.mount
app.mount = (container, ...args: any[]) => { app.mount = (container, ...args: any[]) => {

View File

@ -1,4 +1,4 @@
import { ReactiveEffect } from '@vue/reactivity' import { ReactiveEffect, getCurrentScope } from '@vue/reactivity'
import { import {
type SchedulerJob, type SchedulerJob,
currentInstance, currentInstance,
@ -12,7 +12,8 @@ import { invokeArrayFns } from '@vue/shared'
export function renderEffect(fn: () => void, noLifecycle = false): void { export function renderEffect(fn: () => void, noLifecycle = false): void {
const instance = currentInstance as VaporComponentInstance const instance = currentInstance as VaporComponentInstance
if (__DEV__ && !isVaporComponent(instance)) { const scope = getCurrentScope()
if (__DEV__ && !__TEST__ && !isVaporComponent(instance)) {
warn('renderEffect called without active vapor instance.') warn('renderEffect called without active vapor instance.')
} }
@ -21,7 +22,9 @@ export function renderEffect(fn: () => void, noLifecycle = false): void {
: () => { : () => {
const prev = currentInstance const prev = currentInstance
simpleSetCurrentInstance(instance) simpleSetCurrentInstance(instance)
if (scope) scope.on()
if ( if (
instance &&
instance.isMounted && instance.isMounted &&
!instance.isUpdating && !instance.isUpdating &&
(instance.bu || instance.u) (instance.bu || instance.u)
@ -36,17 +39,19 @@ export function renderEffect(fn: () => void, noLifecycle = false): void {
} else { } else {
fn() fn()
} }
if (scope) scope.off()
simpleSetCurrentInstance(prev, instance) simpleSetCurrentInstance(prev, instance)
} }
const effect = new ReactiveEffect(renderEffectFn) const effect = new ReactiveEffect(renderEffectFn)
const job: SchedulerJob = effect.runIfDirty.bind(effect) const job: SchedulerJob = effect.runIfDirty.bind(effect)
job.i = instance if (instance) {
job.id = instance.uid job.i = instance
job.id = instance.uid
}
effect.scheduler = () => queueJob(job) effect.scheduler = () => queueJob(job)
effect.run() effect.run()
// TODO lifecycle
// TODO recurse handling // TODO recurse handling
// TODO measure // TODO measure
} }