import { type Plugin, createApp, defineComponent, getCurrentInstance, h, inject, nextTick, nodeOps, onMounted, provide, ref, resolveComponent, resolveDirective, serializeInner, watch, withDirectives, } from '@vue/runtime-test' describe('api: createApp', () => { test('mount', () => { const Comp = defineComponent({ props: { count: { default: 0, }, }, setup(props) { return () => props.count }, }) const root1 = nodeOps.createElement('div') createApp(Comp).mount(root1) expect(serializeInner(root1)).toBe(`0`) //#5571 mount multiple apps to the same host element createApp(Comp).mount(root1) expect( `There is already an app instance mounted on the host container`, ).toHaveBeenWarned() // mount with props const root2 = nodeOps.createElement('div') const app2 = createApp(Comp, { count: 1 }) app2.mount(root2) expect(serializeInner(root2)).toBe(`1`) // remount warning const root3 = nodeOps.createElement('div') app2.mount(root3) expect(serializeInner(root3)).toBe(``) expect(`already been mounted`).toHaveBeenWarned() }) test('unmount', () => { const Comp = defineComponent({ props: { count: { default: 0, }, }, setup(props) { return () => props.count }, }) const root = nodeOps.createElement('div') const app = createApp(Comp) // warning app.unmount() expect(`that is not mounted`).toHaveBeenWarned() app.mount(root) app.unmount() expect(serializeInner(root)).toBe(``) }) test('provide', () => { const Root = { setup() { // test override provide('foo', 3) return () => h(Child) }, } const Child = { setup() { const foo = inject('foo') const bar = inject('bar') try { inject('__proto__') } catch (e: any) {} return () => `${foo},${bar}` }, } const app = createApp(Root) app.provide('foo', 1) app.provide('bar', 2) const root = nodeOps.createElement('div') app.mount(root) expect(serializeInner(root)).toBe(`3,2`) expect('[Vue warn]: injection "__proto__" not found.').toHaveBeenWarned() const app2 = createApp(Root) app2.provide('bar', 1) app2.provide('bar', 2) expect(`App already provides property with key "bar".`).toHaveBeenWarned() }) test('runWithContext', () => { const app = createApp({ setup() { provide('foo', 'should not be seen') // nested createApp const childApp = createApp({ setup() { provide('foo', 'foo from child') }, }) childApp.provide('foo', 2) expect(childApp.runWithContext(() => inject('foo'))).toBe(2) return () => h('div') }, }) app.provide('foo', 1) expect(app.runWithContext(() => inject('foo'))).toBe(1) const root = nodeOps.createElement('div') app.mount(root) 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 Root = { // local override components: { BarBaz: () => 'barbaz-local!', }, setup() { // resolve in setup const FooBar = resolveComponent('foo-bar') return () => { // resolve in render const BarBaz = resolveComponent('bar-baz') return h('div', [h(FooBar), h(BarBaz)]) } }, } const app = createApp(Root) const FooBar = () => 'foobar!' app.component('FooBar', FooBar) expect(app.component('FooBar')).toBe(FooBar) app.component('BarBaz', () => 'barbaz!') app.component('BarBaz', () => 'barbaz!') expect( 'Component "BarBaz" has already been registered in target app.', ).toHaveBeenWarnedTimes(1) const root = nodeOps.createElement('div') app.mount(root) expect(serializeInner(root)).toBe(`
foobar!barbaz-local!
`) }) test('directive', () => { const spy1 = vi.fn() const spy2 = vi.fn() const spy3 = vi.fn() const Root = { // local override directives: { BarBaz: { mounted: spy3 }, }, setup() { // resolve in setup const FooBar = resolveDirective('foo-bar') return () => { // resolve in render const BarBaz = resolveDirective('bar-baz') return withDirectives(h('div'), [[FooBar], [BarBaz]]) } }, } const app = createApp(Root) 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) const root = nodeOps.createElement('div') app.mount(root) expect(spy1).toHaveBeenCalled() expect(spy2).not.toHaveBeenCalled() expect(spy3).toHaveBeenCalled() app.directive('bind', FooBar) expect( `Do not use built-in directive ids as custom directive id: bind`, ).toHaveBeenWarned() }) test('mixin', () => { const calls: string[] = [] const mixinA = { data() { return { a: 1, } }, created(this: any) { calls.push('mixinA created') expect(this.a).toBe(1) expect(this.b).toBe(2) expect(this.c).toBe(3) }, mounted() { calls.push('mixinA mounted') }, } const mixinB = { name: 'mixinB', data() { return { b: 2, } }, created(this: any) { calls.push('mixinB created') expect(this.a).toBe(1) expect(this.b).toBe(2) expect(this.c).toBe(3) }, mounted() { calls.push('mixinB mounted') }, } const Comp = { data() { return { c: 3, } }, created(this: any) { calls.push('comp created') expect(this.a).toBe(1) expect(this.b).toBe(2) expect(this.c).toBe(3) }, mounted() { calls.push('comp mounted') }, render(this: any) { return `${this.a}${this.b}${this.c}` }, } const app = createApp(Comp) app.mixin(mixinA) app.mixin(mixinB) app.mixin(mixinA) app.mixin(mixinB) expect( 'Mixin has already been applied to target app', ).toHaveBeenWarnedTimes(2) expect( 'Mixin has already been applied to target app: mixinB', ).toHaveBeenWarnedTimes(1) const root = nodeOps.createElement('div') app.mount(root) expect(serializeInner(root)).toBe(`123`) expect(calls).toEqual([ 'mixinA created', 'mixinB created', 'comp created', 'mixinA mounted', 'mixinB mounted', 'comp mounted', ]) }) test('use', () => { const PluginA: Plugin = app => app.provide('foo', 1) const PluginB: Plugin = { install: (app, arg1, arg2) => app.provide('bar', arg1 + arg2), } class PluginC { someProperty = {} static install() { app.provide('baz', 2) } } const PluginD: any = undefined const Root = { setup() { const foo = inject('foo') const bar = inject('bar') return () => `${foo},${bar}` }, } const app = createApp(Root) app.use(PluginA) app.use(PluginB, 1, 1) app.use(PluginC) const root = nodeOps.createElement('div') app.mount(root) expect(serializeInner(root)).toBe(`1,2`) app.use(PluginA) expect( `Plugin has already been applied to target app`, ).toHaveBeenWarnedTimes(1) app.use(PluginD) expect( `A plugin must either be a function or an object with an "install" ` + `function.`, ).toHaveBeenWarnedTimes(1) }) test('onUnmount', () => { const cleanup = vi.fn().mockName('plugin cleanup') const PluginA: Plugin = app => { app.provide('foo', 1) app.onUnmount(cleanup) } const PluginB: Plugin = { install: (app, arg1, arg2) => { app.provide('bar', arg1 + arg2) app.onUnmount(cleanup) }, } const app = createApp({ render: () => `Test`, }) app.use(PluginA) app.use(PluginB) const root = nodeOps.createElement('div') app.mount(root) //also can be added after mount app.onUnmount(cleanup) app.unmount() expect(cleanup).toHaveBeenCalledTimes(3) }) test('config.errorHandler', () => { const error = new Error() const count = ref(0) const handler = vi.fn((err, instance, info) => { expect(err).toBe(error) expect(instance.count).toBe(count.value) expect(info).toBe(`render function`) }) const Root = { setup() { const count = ref(0) return { count, } }, render() { throw error }, } const app = createApp(Root) app.config.errorHandler = handler app.mount(nodeOps.createElement('div')) expect(handler).toHaveBeenCalled() }) test('config.warnHandler', () => { let ctx: any const handler = vi.fn((msg, instance, trace) => { expect(msg).toMatch(`Component is missing template or render function`) expect(instance).toBe(ctx.proxy) expect(trace).toMatch(`Hello`) }) const Root = { name: 'Hello', setup() { ctx = getCurrentInstance() }, } const app = createApp(Root) app.config.warnHandler = handler app.mount(nodeOps.createElement('div')) expect(handler).toHaveBeenCalledTimes(1) }) describe('config.isNativeTag', () => { const isNativeTag = vi.fn(tag => tag === 'div') test('Component.name', () => { const Root = { name: 'div', render() { return null }, } const app = createApp(Root) Object.defineProperty(app.config, 'isNativeTag', { value: isNativeTag, writable: false, }) app.mount(nodeOps.createElement('div')) expect( `Do not use built-in or reserved HTML elements as component id: div`, ).toHaveBeenWarned() }) test('Component.components', () => { const Root = { components: { div: () => 'div', }, render() { return null }, } const app = createApp(Root) Object.defineProperty(app.config, 'isNativeTag', { value: isNativeTag, writable: false, }) app.mount(nodeOps.createElement('div')) expect( `Do not use built-in or reserved HTML elements as component id: div`, ).toHaveBeenWarned() }) test('Component.directives', () => { const Root = { directives: { bind: () => {}, }, render() { return null }, } const app = createApp(Root) app.mount(nodeOps.createElement('div')) expect( `Do not use built-in directive ids as custom directive id: bind`, ).toHaveBeenWarned() }) test('register using app.component', () => { const app = createApp({ render() {}, }) Object.defineProperty(app.config, 'isNativeTag', { value: isNativeTag, writable: false, }) app.component('div', () => 'div') app.mount(nodeOps.createElement('div')) expect( `Do not use built-in or reserved HTML elements as component id: div`, ).toHaveBeenWarned() }) }) test('config.optionMergeStrategies', () => { let merged: string const App = defineComponent({ render() {}, mixins: [{ foo: 'mixin' }], extends: { foo: 'extends' }, foo: 'local', beforeCreate() { merged = this.$options.foo }, }) const app = createApp(App) app.mixin({ foo: 'global', }) app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b app.mount(nodeOps.createElement('div')) expect(merged!).toBe('global,extends,mixin,local') }) test('config.globalProperties', () => { const app = createApp({ render() { return this.foo }, }) app.config.globalProperties.foo = 'hello' const root = nodeOps.createElement('div') app.mount(root) expect(serializeInner(root)).toBe('hello') }) test('config.throwUnhandledErrorInProduction', () => { __DEV__ = false try { const err = new Error() const app = createApp({ setup() { throw err }, }) app.config.throwUnhandledErrorInProduction = true const root = nodeOps.createElement('div') expect(() => app.mount(root)).toThrow(err) } finally { __DEV__ = true } }) test('return property "_" should not overwrite "ctx._", __isScriptSetup: false', () => { const Comp = defineComponent({ setup() { return { _: ref(0), // return property "_" should not overwrite "ctx._" } }, render() { return h('input', { ref: 'input', }) }, }) const root1 = nodeOps.createElement('div') createApp(Comp).mount(root1) expect( `setup() return property "_" should not start with "$" or "_" which are reserved prefixes for Vue internals.`, ).toHaveBeenWarned() }) test('return property "_" should not overwrite "ctx._", __isScriptSetup: true', () => { const Comp = defineComponent({ setup() { return { _: ref(0), // return property "_" should not overwrite "ctx._" __isScriptSetup: true, // mock __isScriptSetup = true } }, render() { return h('input', { ref: 'input', }) }, }) const root1 = nodeOps.createElement('div') const app = createApp(Comp).mount(root1) // trigger app.$refs.input expect( `TypeError: Cannot read property '__isScriptSetup' of undefined`, ).not.toHaveBeenWarned() }) // #10005 test('flush order edge case on nested createApp', async () => { const order: string[] = [] const App = defineComponent({ setup(props) { const message = ref('m1') watch( message, () => { order.push('post watcher') }, { flush: 'post' }, ) onMounted(() => { message.value = 'm2' createApp(() => '').mount(nodeOps.createElement('div')) }) return () => { order.push('render') return h('div', [message.value]) } }, }) createApp(App).mount(nodeOps.createElement('div')) await nextTick() expect(order).toMatchObject(['render', 'render', 'post watcher']) }) // #14215 test("unmount new app should not trigger other app's watcher", async () => { const compWatcherTriggerFn = vi.fn() const data = ref(true) const foo = ref('') const createNewApp = () => { const app = createApp({ render: () => h('new app') }) const wrapper = nodeOps.createElement('div') app.mount(wrapper) return function destroy() { app.unmount() } } const Comp = defineComponent({ setup() { watch(() => foo.value, compWatcherTriggerFn) return () => h('div', 'comp') }, }) const App = defineComponent({ setup() { return () => (data.value ? h(Comp) : null) }, }) createApp(App).mount(nodeOps.createElement('div')) await nextTick() data.value = false const destroy = createNewApp() foo.value = 'bar' destroy() await nextTick() expect(compWatcherTriggerFn).toBeCalledTimes(0) }) // config.compilerOptions is tested in packages/vue since it is only // supported in the full build. })