test(runtime-vapor): finish createVaporApp unit tests

This commit is contained in:
三咲智子 Kevin Deng 2024-06-05 04:20:20 +08:00
parent 8ccfce5ec7
commit bbd1944ce5
No known key found for this signature in database
4 changed files with 339 additions and 18 deletions

View File

@ -17,31 +17,50 @@ export function makeRender<Component = ObjectComponent | SetupFn>(
},
) {
let host: HTMLElement
function resetHost() {
return (host = initHost())
}
beforeEach(() => {
host = initHost()
resetHost()
})
afterEach(() => {
host.remove()
})
const define = (comp: Component) => {
function define(comp: Component) {
const component = defineComponent(comp as any)
let instance: ComponentInternalInstance
let instance: ComponentInternalInstance | undefined
let app: App
const render = (
function render(
props: RawProps = {},
container: string | ParentNode = '#host',
) => {
container: string | ParentNode = host,
) {
create(props)
return mount(container)
}
function create(props: RawProps = {}) {
app?.unmount()
app = createVaporApp(component, props)
return res()
}
function mount(container: string | ParentNode = host) {
instance = app.mount(container)
return res()
}
const res = () => ({
component,
host,
instance,
app,
create,
mount,
render,
resetHost,
})
return res()

View File

@ -1,6 +1,193 @@
import { type Component, type Plugin, createVaporApp, inject } from '../src'
;``
describe('api: createApp', () => {
import {
type ComponentInternalInstance,
type Plugin,
createComponent,
createTextNode,
createVaporApp,
defineComponent,
getCurrentInstance,
inject,
provide,
resolveComponent,
resolveDirective,
withDirectives,
} from '../src'
import { warn } from '../src/warning'
import { makeRender } from './_utils'
const define = makeRender()
describe('api: createVaporApp', () => {
test('mount', () => {
const Comp = defineComponent({
props: {
count: { default: 0 },
},
setup(props) {
return createTextNode(() => [props.count])
},
})
const root1 = document.createElement('div')
createVaporApp(Comp).mount(root1)
expect(root1.innerHTML).toBe(`0`)
//#5571 mount multiple apps to the same host element
createVaporApp(Comp).mount(root1)
expect(
`There is already an app instance mounted on the host container`,
).toHaveBeenWarned()
// mount with props
const root2 = document.createElement('div')
const app2 = createVaporApp(Comp, { count: () => 1 })
app2.mount(root2)
expect(root2.innerHTML).toBe(`1`)
// remount warning
const root3 = document.createElement('div')
app2.mount(root3)
expect(root3.innerHTML).toBe(``)
expect(`already been mounted`).toHaveBeenWarned()
})
test('unmount', () => {
const Comp = defineComponent({
props: {
count: { default: 0 },
},
setup(props) {
return createTextNode(() => [props.count])
},
})
const root = document.createElement('div')
const app = createVaporApp(Comp)
// warning
app.unmount()
expect(`that is not mounted`).toHaveBeenWarned()
app.mount(root)
app.unmount()
expect(root.innerHTML).toBe(``)
})
test('provide', () => {
const Root = define({
setup() {
// test override
provide('foo', 3)
return createComponent(Child)
},
})
const Child = defineComponent({
setup() {
const foo = inject('foo')
const bar = inject('bar')
try {
inject('__proto__')
} catch (e: any) {}
return createTextNode(() => [`${foo},${bar}`])
},
})
const { app, mount, create, host } = Root.create(null)
app.provide('foo', 1)
app.provide('bar', 2)
mount()
expect(host.innerHTML).toBe(`3,2`)
expect('[Vue warn]: injection "__proto__" not found.').toHaveBeenWarned()
const { app: app2 } = create()
app2.provide('bar', 1)
app2.provide('bar', 2)
expect(`App already provides property with key "bar".`).toHaveBeenWarned()
})
test('runWithContext', () => {
const { app } = define({
setup() {
provide('foo', 'should not be seen')
return document.createElement('div')
},
}).create()
app.provide('foo', 1)
expect(app.runWithContext(() => inject('foo'))).toBe(1)
expect(
app.runWithContext(() => {
app.runWithContext(() => {})
return inject('foo')
}),
).toBe(1)
// ensure the context is restored
inject('foo')
expect('inject() can only be used inside setup').toHaveBeenWarned()
})
test('component', () => {
const { app, mount, host } = define({
setup() {
const FooBar = resolveComponent('foo-bar')
const BarBaz = resolveComponent('bar-baz')
// @ts-expect-error TODO support string
return [createComponent(FooBar), createComponent(BarBaz)]
},
}).create()
const FooBar = () => createTextNode(['foobar!'])
app.component('FooBar', FooBar)
expect(app.component('FooBar')).toBe(FooBar)
app.component('BarBaz', () => createTextNode(['barbaz!']))
app.component('BarBaz', () => createTextNode(['barbaz!']))
expect(
'Component "BarBaz" has already been registered in target app.',
).toHaveBeenWarnedTimes(1)
mount()
expect(host.innerHTML).toBe(`foobar!barbaz!`)
})
test('directive', () => {
const spy1 = vi.fn()
const spy2 = vi.fn()
const { app, mount } = define({
setup() {
const FooBar = resolveDirective('foo-bar')
const BarBaz = resolveDirective('bar-baz')
return withDirectives(document.createElement('div'), [
[FooBar],
[BarBaz],
])
},
}).create()
const FooBar = { mounted: spy1 }
app.directive('FooBar', FooBar)
expect(app.directive('FooBar')).toBe(FooBar)
app.directive('BarBaz', { mounted: spy2 })
app.directive('BarBaz', { mounted: spy2 })
expect(
'Directive "BarBaz" has already been registered in target app.',
).toHaveBeenWarnedTimes(1)
mount()
expect(spy1).toHaveBeenCalled()
expect(spy2).toHaveBeenCalled()
app.directive('bind', FooBar)
expect(
`Do not use built-in directive ids as custom directive id: bind`,
).toHaveBeenWarned()
})
test('use', () => {
const PluginA: Plugin = app => app.provide('foo', 1)
const PluginB: Plugin = {
@ -14,22 +201,20 @@ describe('api: createApp', () => {
}
const PluginD: any = undefined
const Root: Component = {
const { app, host, mount } = define({
setup() {
const foo = inject('foo')
const bar = inject('bar')
return document.createTextNode(`${foo},${bar}`)
},
}
}).create()
const app = createVaporApp(Root)
app.use(PluginA)
app.use(PluginB, 1, 1)
app.use(PluginC)
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe(`1,2`)
mount()
expect(host.innerHTML).toBe(`1,2`)
app.use(PluginA)
expect(
@ -42,4 +227,87 @@ describe('api: createApp', () => {
`function.`,
).toHaveBeenWarnedTimes(1)
})
test('config.errorHandler', () => {
const error = new Error()
let instance: ComponentInternalInstance
const handler = vi.fn((err, _instance, info) => {
expect(err).toBe(error)
expect(_instance).toBe(instance)
expect(info).toBe(`render function`)
})
const { app, mount } = define({
setup() {
instance = getCurrentInstance()!
},
render() {
throw error
},
}).create()
app.config.errorHandler = handler
mount()
expect(handler).toHaveBeenCalled()
})
test('config.warnHandler', () => {
let instance: ComponentInternalInstance
const handler = vi.fn((msg, _instance, trace) => {
expect(msg).toMatch(`warn message`)
expect(_instance).toBe(instance)
expect(trace).toMatch(`Hello`)
})
const { app, mount } = define({
name: 'Hello',
setup() {
instance = getCurrentInstance()!
warn('warn message')
},
}).create()
app.config.warnHandler = handler
mount()
expect(handler).toHaveBeenCalledTimes(1)
})
describe('config.isNativeTag', () => {
const isNativeTag = vi.fn(tag => tag === 'div')
test('Component.name', () => {
const { app, mount } = define({
name: 'div',
render(): any {},
}).create()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false,
})
mount()
expect(
`Do not use built-in or reserved HTML elements as component id: div`,
).toHaveBeenWarned()
})
test('register using app.component', () => {
const { app, mount } = define({
render(): any {},
}).create()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false,
})
app.component('div', () => createTextNode(['div']))
mount()
expect(
`Do not use built-in or reserved HTML elements as component id: div`,
).toHaveBeenWarned()
})
})
})

View File

@ -7,7 +7,12 @@ import {
} from './component'
import { warn } from './warning'
import { type Directive, version } from '.'
import { render, setupComponent, unmountComponent } from './apiRender'
import {
normalizeContainer,
render,
setupComponent,
unmountComponent,
} from './apiRender'
import type { InjectionKey } from './apiInject'
import type { RawProps } from './componentProps'
import { validateDirectiveName } from './directives'
@ -29,6 +34,7 @@ export function createVaporApp(
const app: App = {
_context: context,
_container: null,
version,
@ -93,6 +99,15 @@ export function createVaporApp(
mount(rootContainer): any {
if (!instance) {
rootContainer = normalizeContainer(rootContainer)
// #5571
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`,
)
}
instance = createComponentInstance(
rootComponent,
rootProps,
@ -103,6 +118,11 @@ export function createVaporApp(
)
setupComponent(instance)
render(instance, rootContainer)
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return instance
} else if (__DEV__) {
warn(
@ -116,6 +136,7 @@ export function createVaporApp(
unmount() {
if (instance) {
unmountComponent(instance)
delete (app._container as any).__vue_app__
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
@ -199,6 +220,7 @@ export interface App {
runWithContext<T>(fn: () => T): T
_context: AppContext
_container: ParentNode | null
}
export interface AppConfig {

View File

@ -3,6 +3,7 @@ import {
componentKey,
createSetupContext,
setCurrentInstance,
validateComponentName,
} from './component'
import { insert, querySelector, remove } from './dom/element'
import { flushPostFlushCbs, queuePostFlushCb } from './scheduler'
@ -35,6 +36,12 @@ export function setupComponent(
instance.scope.run(() => {
const { component, props } = instance
if (__DEV__) {
if (component.name) {
validateComponentName(component.name, instance.appContext.config)
}
}
const setupFn = isFunction(component) ? component : component.setup
let stateOrNode: Block | undefined
if (setupFn) {
@ -65,7 +72,12 @@ export function setupComponent(
}
if (!block && component.render) {
pauseTracking()
block = component.render(instance.setupState)
block = callWithErrorHandling(
component.render,
instance,
VaporErrorCodes.RENDER_FUNCTION,
[instance.setupState],
)
resetTracking()
}
@ -91,7 +103,7 @@ export function render(
flushPostFlushCbs()
}
function normalizeContainer(container: string | ParentNode): ParentNode {
export function normalizeContainer(container: string | ParentNode): ParentNode {
return typeof container === 'string'
? (querySelector(container) as ParentNode)
: container