test(vapor): enable more test cases

This commit is contained in:
Evan You 2025-01-30 10:12:36 +08:00
parent 6979952613
commit e49c5a17da
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
10 changed files with 202 additions and 163 deletions

View File

@ -9,9 +9,10 @@ import {
isPromise,
} from '@vue/shared'
import {
type ComponentInternalInstance,
type SetupContext,
createSetupContext,
getCurrentInstance,
getCurrentGenericInstance,
setCurrentInstance,
unsetCurrentInstance,
} from './component'
@ -381,6 +382,7 @@ export function withDefaults<
return null as any
}
// TODO return type for Vapor components
export function useSlots(): SetupContext['slots'] {
return getContext().slots
}
@ -390,11 +392,16 @@ export function useAttrs(): SetupContext['attrs'] {
}
function getContext(): SetupContext {
const i = getCurrentInstance()!
const i = getCurrentGenericInstance()!
if (__DEV__ && !i) {
warn(`useContext() called without active instance.`)
}
return i.setupContext || (i.setupContext = createSetupContext(i))
if (i.vapor) {
return i as any // vapor instance act as its own setup context
} else {
const ii = i as ComponentInternalInstance
return ii.setupContext || (ii.setupContext = createSetupContext(ii))
}
}
/**
@ -496,7 +503,7 @@ export function createPropsRestProxy(
* @internal
*/
export function withAsyncContext(getAwaitable: () => any): [any, () => void] {
const ctx = getCurrentInstance()!
const ctx = getCurrentGenericInstance()!
if (__DEV__ && !ctx) {
warn(
`withAsyncContext called without active current instance. ` +

View File

@ -1,13 +1,18 @@
import { createComponent, defineVaporComponent, template } from '../src'
import { ref, useAttrs, useSlots } from '@vue/runtime-dom'
import {
currentInstance,
onMounted,
ref,
useAttrs,
useSlots,
withAsyncContext,
} from '@vue/runtime-dom'
import { makeRender } from './_utils'
import type { VaporComponentInstance } from '../src/component'
const define = makeRender<any>()
describe.todo('SFC <script setup> helpers', () => {
test.todo('should warn runtime usage', () => {})
describe('SFC <script setup> helpers', () => {
test('useSlots / useAttrs (no args)', () => {
let slots: VaporComponentInstance['slots'] | undefined
let attrs: VaporComponentInstance['attrs'] | undefined
@ -59,23 +64,51 @@ describe.todo('SFC <script setup> helpers', () => {
expect(attrs).toBe(ctx!.attrs)
})
describe.todo('mergeDefaults', () => {
test.todo('object syntax', () => {})
test.todo('array syntax', () => {})
test.todo('merging with skipFactory', () => {})
test.todo('should warn missing', () => {})
})
describe('mergeModels', () => {
test.todo('array syntax', () => {})
test.todo('object syntax', () => {})
test.todo('overwrite', () => {})
})
test.todo('createPropsRestProxy', () => {})
describe.todo('withAsyncContext', () => {
test.todo('basic', async () => {})
test('basic', async () => {
const spy = vi.fn()
let beforeInstance: VaporComponentInstance | null = null
let afterInstance: VaporComponentInstance | null = null
let resolve: (msg: string) => void
const Comp = defineVaporComponent({
async setup() {
let __temp: any, __restore: any
beforeInstance = currentInstance as VaporComponentInstance
const msg =
(([__temp, __restore] = withAsyncContext(
() =>
new Promise(r => {
resolve = r
}),
)),
(__temp = await __temp),
__restore(),
__temp)
// register the lifecycle after an await statement
onMounted(spy)
afterInstance = currentInstance as VaporComponentInstance
return document.createTextNode(msg)
},
})
const { html } = define(Comp).render()
expect(spy).not.toHaveBeenCalled()
resolve!('hello')
// wait a macro task tick for all micro ticks to resolve
await new Promise(r => setTimeout(r))
// mount hook should have been called
expect(spy).toHaveBeenCalled()
// should retain same instance before/after the await call
expect(beforeInstance).toBe(afterInstance)
expect(html()).toBe('hello')
})
test.todo('error handling', async () => {})
test.todo('should not leak instance on multiple awaits', async () => {})
test.todo('should not leak on multiple awaits + error', async () => {})

View File

@ -8,43 +8,50 @@ import {
watch,
watchEffect,
} from '@vue/runtime-dom'
import { createComponent, defineVaporComponent, renderEffect } from '../src'
import {
createComponent,
createIf,
createTemplateRefSetter,
defineVaporComponent,
renderEffect,
template,
} from '../src'
import { makeRender } from './_utils'
import type { VaporComponentInstance } from '../src/component'
import type { RefEl } from '../src/apiTemplateRef'
const define = makeRender()
// only need to port test cases related to in-component usage
describe('apiWatch', () => {
// #7030
it.todo(
// need if support
'should not fire on child component unmount w/ flush: pre',
async () => {
const visible = ref(true)
const cb = vi.fn()
const Parent = defineVaporComponent({
props: ['visible'],
setup() {
// @ts-expect-error
return visible.value ? h(Comp) : null
},
})
const Comp = {
setup() {
watch(visible, cb, { flush: 'pre' })
return []
},
}
define(Parent).render({
visible: () => visible.value,
})
expect(cb).not.toHaveBeenCalled()
visible.value = false
await nextTick()
expect(cb).not.toHaveBeenCalled()
},
)
it(// need if support
'should not fire on child component unmount w/ flush: pre', async () => {
const visible = ref(true)
const cb = vi.fn()
const Parent = defineVaporComponent({
props: ['visible'],
setup() {
return createIf(
() => visible.value,
() => createComponent(Comp),
)
},
})
const Comp = {
setup() {
watch(visible, cb, { flush: 'pre' })
return []
},
}
define(Parent).render({
visible: () => visible.value,
})
expect(cb).not.toHaveBeenCalled()
visible.value = false
await nextTick()
expect(cb).not.toHaveBeenCalled()
})
// #7030
it('flush: pre watcher in child component should not fire before parent update', async () => {
@ -184,41 +191,41 @@ describe('apiWatch', () => {
})
// #1852
it.todo(
// need if + templateRef
'flush: post watcher should fire after template refs updated',
async () => {
const toggle = ref(false)
let dom: Element | null = null
it('flush: post watcher should fire after template refs updated', async () => {
const toggle = ref(false)
let dom: Element | null = null
const App = {
setup() {
const domRef = ref<Element | null>(null)
const App = {
setup() {
const domRef = ref<Element | null>(null)
watch(
toggle,
() => {
dom = domRef.value
},
{ flush: 'post' },
)
watch(
toggle,
() => {
dom = domRef.value
},
{ flush: 'post' },
)
return () => {
// @ts-expect-error
return toggle.value ? h('p', { ref: domRef }) : null
}
},
}
const setRef = createTemplateRefSetter()
return createIf(
() => toggle.value,
() => {
const n = template('<p>')()
setRef(n as RefEl, domRef)
return n
},
)
},
}
// @ts-expect-error
render(h(App), nodeOps.createElement('div'))
expect(dom).toBe(null)
define(App).render()
expect(dom).toBe(null)
toggle.value = true
await nextTick()
expect(dom!.tagName).toBe('P')
},
)
toggle.value = true
await nextTick()
expect(dom!.tagName).toBe('P')
})
test('should not leak `this.proxy` to setup()', () => {
const source = vi.fn()

View File

@ -87,7 +87,7 @@ describe('component: slots', () => {
expect(instance.slots).toHaveProperty('two')
})
test.todo('should work with createFlorSlots', async () => {
test('should work with createFlorSlots', async () => {
const loop = ref([1, 2, 3])
let instance: any
@ -101,7 +101,6 @@ describe('component: slots', () => {
return createComponent(Child, null, {
$: [
() =>
// @ts-expect-error
createForSlots(loop.value, (item, i) => ({
name: item,
fn: () => template(item + i)(),

View File

@ -406,7 +406,7 @@ describe('api: template ref', () => {
})
// compiled output of v-for + template ref
test.todo('ref in v-for', async () => {
test('ref in v-for', async () => {
const show = ref(true)
const list = reactive([1, 2, 3])
const listRefs = ref([])
@ -466,7 +466,7 @@ describe('api: template ref', () => {
expect(mapRefs()).toMatchObject(['2', '3', '4'])
})
test.todo('named ref in v-for', async () => {
test('named ref in v-for', async () => {
const show = ref(true)
const list = reactive([1, 2, 3])
const listRefs = ref([])
@ -530,67 +530,64 @@ describe('api: template ref', () => {
})
// #6697 v-for ref behaves differently under production and development
test.todo(
'named ref in v-for , should be responsive when rendering',
async () => {
const list = ref([1, 2, 3])
const listRefs = ref([])
test('named ref in v-for , should be responsive when rendering', async () => {
const list = ref([1, 2, 3])
const listRefs = ref([])
const t0 = template('<div><div></div><ul></ul></div>')
const t1 = template('<li></li>')
const { render } = define({
setup() {
return { listRefs }
},
render() {
const n0 = t0()
const n1 = n0.firstChild
const n2 = n1!.nextSibling!
const n3 = createFor(
() => list.value,
state => {
const n4 = t1()
createTemplateRefSetter()(
n4 as Element,
'listRefs',
undefined,
true,
)
renderEffect(() => {
const [item] = state
setText(n4, item)
})
return n4
},
)
insert(n3, n2 as unknown as ParentNode)
renderEffect(() => {
setText(n1!, String(listRefs.value))
})
return n0
},
})
const t0 = template('<div><div></div><ul></ul></div>')
const t1 = template('<li></li>')
const { render } = define({
setup() {
return { listRefs }
},
render() {
const n0 = t0()
const n1 = n0.firstChild
const n2 = n1!.nextSibling!
const n3 = createFor(
() => list.value,
state => {
const n4 = t1()
createTemplateRefSetter()(
n4 as Element,
'listRefs',
undefined,
true,
)
renderEffect(() => {
const [item] = state
setText(n4, item)
})
return n4
},
)
insert(n3, n2 as unknown as ParentNode)
renderEffect(() => {
setText(n1!, String(listRefs.value))
})
return n0
},
})
const { host } = render()
const { host } = render()
await nextTick()
expect(String(listRefs.value)).toBe(
'[object HTMLLIElement],[object HTMLLIElement],[object HTMLLIElement]',
)
expect(host.innerHTML).toBe(
'<div><div>[object HTMLLIElement],[object HTMLLIElement],[object HTMLLIElement]</div><ul><li>1</li><li>2</li><li>3</li><!--for--></ul></div>',
)
await nextTick()
expect(String(listRefs.value)).toBe(
'[object HTMLLIElement],[object HTMLLIElement],[object HTMLLIElement]',
)
expect(host.innerHTML).toBe(
'<div><div>[object HTMLLIElement],[object HTMLLIElement],[object HTMLLIElement]</div><ul><li>1</li><li>2</li><li>3</li><!--for--></ul></div>',
)
list.value.splice(0, 1)
await nextTick()
expect(String(listRefs.value)).toBe(
'[object HTMLLIElement],[object HTMLLIElement]',
)
expect(host.innerHTML).toBe(
'<div><div>[object HTMLLIElement],[object HTMLLIElement]</div><ul><li>2</li><li>3</li><!--for--></ul></div>',
)
},
)
list.value.splice(0, 1)
await nextTick()
expect(String(listRefs.value)).toBe(
'[object HTMLLIElement],[object HTMLLIElement]',
)
expect(host.innerHTML).toBe(
'<div><div>[object HTMLLIElement],[object HTMLLIElement]</div><ul><li>2</li><li>3</li><!--for--></ul></div>',
)
})
test('string ref inside slots', () => {
const { component: Child } = define({

View File

@ -209,7 +209,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'render function')
})
test.todo('in function ref', () => {
test('in function ref', () => {
const err = new Error('foo')
const ref = () => {
throw err

View File

@ -319,7 +319,7 @@ export const createFor = (
}
export function createForSlots(
source: any[] | Record<any, any> | number | Set<any> | Map<any, any>,
source: Source,
getSlot: (item: any, key: any, index?: number) => DynamicSlot,
): DynamicSlot[] {
const sourceLength = getLength(source)

View File

@ -6,6 +6,7 @@ import {
isVaporComponent,
} from './component'
import {
ErrorCodes,
type SchedulerJob,
callWithErrorHandling,
queuePostFlushCb,
@ -67,13 +68,10 @@ export function setRef(
if (isFunction(ref)) {
const invokeRefSetter = (value?: Element | Record<string, any>) => {
callWithErrorHandling(
ref,
currentInstance,
// @ts-expect-error
null,
[value, refs],
)
callWithErrorHandling(ref, currentInstance, ErrorCodes.FUNCTION_REF, [
value,
refs,
])
}
invokeRefSetter(refValue)

View File

@ -35,16 +35,17 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
}
},
ownKeys(target) {
const keys = Object.keys(target)
let keys = Object.keys(target)
const dynamicSources = target.$
if (dynamicSources) {
keys = keys.filter(k => k !== '$')
for (const source of dynamicSources) {
if (isFunction(source)) {
const slot = source()
if (isArray(slot)) {
for (const s of slot) keys.push(s.name)
for (const s of slot) keys.push(String(s.name))
} else {
keys.push(slot.name)
keys.push(String(slot.name))
}
} else {
keys.push(...Object.keys(source))
@ -73,9 +74,9 @@ export function getSlot(
if (slot) {
if (isArray(slot)) {
for (const s of slot) {
if (s.name === key) return s.fn
if (String(s.name) === key) return s.fn
}
} else if (slot.name === key) {
} else if (String(slot.name) === key) {
return slot.fn
}
}
@ -150,6 +151,3 @@ export function createSlot(
return fragment
}
// TODO
export function createForSlots(): any {}

View File

@ -6,7 +6,7 @@ export { defineVaporComponent } from './apiDefineComponent'
export { insert, prepend, remove } from './block'
export { createComponent, createComponentWithFallback } from './component'
export { renderEffect } from './renderEffect'
export { createSlot, createForSlots } from './componentSlots'
export { createSlot } from './componentSlots'
export { template, children, next } from './dom/template'
export { createTextNode } from './dom/node'
export {
@ -22,5 +22,5 @@ export {
} from './dom/prop'
export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
export { createIf } from './apiCreateIf'
export { createFor } from './apiCreateFor'
export { createFor, createForSlots } from './apiCreateFor'
export { createTemplateRefSetter } from './apiTemplateRef'