mirror of https://github.com/vuejs/core.git
test(runtime-vapor): finish createVaporApp unit tests
This commit is contained in:
parent
8ccfce5ec7
commit
bbd1944ce5
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue