apiCreateApp.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. import {
  2. type Plugin,
  3. createApp,
  4. defineComponent,
  5. getCurrentInstance,
  6. h,
  7. inject,
  8. nextTick,
  9. nodeOps,
  10. onMounted,
  11. provide,
  12. ref,
  13. resolveComponent,
  14. resolveDirective,
  15. serializeInner,
  16. watch,
  17. withDirectives,
  18. } from '@vue/runtime-test'
  19. describe('api: createApp', () => {
  20. test('mount', () => {
  21. const Comp = defineComponent({
  22. props: {
  23. count: {
  24. default: 0,
  25. },
  26. },
  27. setup(props) {
  28. return () => props.count
  29. },
  30. })
  31. const root1 = nodeOps.createElement('div')
  32. createApp(Comp).mount(root1)
  33. expect(serializeInner(root1)).toBe(`0`)
  34. //#5571 mount multiple apps to the same host element
  35. createApp(Comp).mount(root1)
  36. expect(
  37. `There is already an app instance mounted on the host container`,
  38. ).toHaveBeenWarned()
  39. // mount with props
  40. const root2 = nodeOps.createElement('div')
  41. const app2 = createApp(Comp, { count: 1 })
  42. app2.mount(root2)
  43. expect(serializeInner(root2)).toBe(`1`)
  44. // remount warning
  45. const root3 = nodeOps.createElement('div')
  46. app2.mount(root3)
  47. expect(serializeInner(root3)).toBe(``)
  48. expect(`already been mounted`).toHaveBeenWarned()
  49. })
  50. test('unmount', () => {
  51. const Comp = defineComponent({
  52. props: {
  53. count: {
  54. default: 0,
  55. },
  56. },
  57. setup(props) {
  58. return () => props.count
  59. },
  60. })
  61. const root = nodeOps.createElement('div')
  62. const app = createApp(Comp)
  63. // warning
  64. app.unmount()
  65. expect(`that is not mounted`).toHaveBeenWarned()
  66. app.mount(root)
  67. app.unmount()
  68. expect(serializeInner(root)).toBe(``)
  69. })
  70. test('provide', () => {
  71. const Root = {
  72. setup() {
  73. // test override
  74. provide('foo', 3)
  75. return () => h(Child)
  76. },
  77. }
  78. const Child = {
  79. setup() {
  80. const foo = inject('foo')
  81. const bar = inject('bar')
  82. try {
  83. inject('__proto__')
  84. } catch (e: any) {}
  85. return () => `${foo},${bar}`
  86. },
  87. }
  88. const app = createApp(Root)
  89. app.provide('foo', 1)
  90. app.provide('bar', 2)
  91. const root = nodeOps.createElement('div')
  92. app.mount(root)
  93. expect(serializeInner(root)).toBe(`3,2`)
  94. expect('[Vue warn]: injection "__proto__" not found.').toHaveBeenWarned()
  95. const app2 = createApp(Root)
  96. app2.provide('bar', 1)
  97. app2.provide('bar', 2)
  98. expect(`App already provides property with key "bar".`).toHaveBeenWarned()
  99. })
  100. test('runWithContext', () => {
  101. const app = createApp({
  102. setup() {
  103. provide('foo', 'should not be seen')
  104. // nested createApp
  105. const childApp = createApp({
  106. setup() {
  107. provide('foo', 'foo from child')
  108. },
  109. })
  110. childApp.provide('foo', 2)
  111. expect(childApp.runWithContext(() => inject('foo'))).toBe(2)
  112. return () => h('div')
  113. },
  114. })
  115. app.provide('foo', 1)
  116. expect(app.runWithContext(() => inject('foo'))).toBe(1)
  117. const root = nodeOps.createElement('div')
  118. app.mount(root)
  119. expect(
  120. app.runWithContext(() => {
  121. app.runWithContext(() => {})
  122. return inject('foo')
  123. }),
  124. ).toBe(1)
  125. // ensure the context is restored
  126. inject('foo')
  127. expect('inject() can only be used inside setup').toHaveBeenWarned()
  128. })
  129. test('component', () => {
  130. const Root = {
  131. // local override
  132. components: {
  133. BarBaz: () => 'barbaz-local!',
  134. },
  135. setup() {
  136. // resolve in setup
  137. const FooBar = resolveComponent('foo-bar')
  138. return () => {
  139. // resolve in render
  140. const BarBaz = resolveComponent('bar-baz')
  141. return h('div', [h(FooBar), h(BarBaz)])
  142. }
  143. },
  144. }
  145. const app = createApp(Root)
  146. const FooBar = () => 'foobar!'
  147. app.component('FooBar', FooBar)
  148. expect(app.component('FooBar')).toBe(FooBar)
  149. app.component('BarBaz', () => 'barbaz!')
  150. app.component('BarBaz', () => 'barbaz!')
  151. expect(
  152. 'Component "BarBaz" has already been registered in target app.',
  153. ).toHaveBeenWarnedTimes(1)
  154. const root = nodeOps.createElement('div')
  155. app.mount(root)
  156. expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
  157. })
  158. test('directive', () => {
  159. const spy1 = vi.fn()
  160. const spy2 = vi.fn()
  161. const spy3 = vi.fn()
  162. const Root = {
  163. // local override
  164. directives: {
  165. BarBaz: { mounted: spy3 },
  166. },
  167. setup() {
  168. // resolve in setup
  169. const FooBar = resolveDirective('foo-bar')
  170. return () => {
  171. // resolve in render
  172. const BarBaz = resolveDirective('bar-baz')
  173. return withDirectives(h('div'), [[FooBar], [BarBaz]])
  174. }
  175. },
  176. }
  177. const app = createApp(Root)
  178. const FooBar = { mounted: spy1 }
  179. app.directive('FooBar', FooBar)
  180. expect(app.directive('FooBar')).toBe(FooBar)
  181. app.directive('BarBaz', {
  182. mounted: spy2,
  183. })
  184. app.directive('BarBaz', {
  185. mounted: spy2,
  186. })
  187. expect(
  188. 'Directive "BarBaz" has already been registered in target app.',
  189. ).toHaveBeenWarnedTimes(1)
  190. const root = nodeOps.createElement('div')
  191. app.mount(root)
  192. expect(spy1).toHaveBeenCalled()
  193. expect(spy2).not.toHaveBeenCalled()
  194. expect(spy3).toHaveBeenCalled()
  195. app.directive('bind', FooBar)
  196. expect(
  197. `Do not use built-in directive ids as custom directive id: bind`,
  198. ).toHaveBeenWarned()
  199. })
  200. test('mixin', () => {
  201. const calls: string[] = []
  202. const mixinA = {
  203. data() {
  204. return {
  205. a: 1,
  206. }
  207. },
  208. created(this: any) {
  209. calls.push('mixinA created')
  210. expect(this.a).toBe(1)
  211. expect(this.b).toBe(2)
  212. expect(this.c).toBe(3)
  213. },
  214. mounted() {
  215. calls.push('mixinA mounted')
  216. },
  217. }
  218. const mixinB = {
  219. name: 'mixinB',
  220. data() {
  221. return {
  222. b: 2,
  223. }
  224. },
  225. created(this: any) {
  226. calls.push('mixinB created')
  227. expect(this.a).toBe(1)
  228. expect(this.b).toBe(2)
  229. expect(this.c).toBe(3)
  230. },
  231. mounted() {
  232. calls.push('mixinB mounted')
  233. },
  234. }
  235. const Comp = {
  236. data() {
  237. return {
  238. c: 3,
  239. }
  240. },
  241. created(this: any) {
  242. calls.push('comp created')
  243. expect(this.a).toBe(1)
  244. expect(this.b).toBe(2)
  245. expect(this.c).toBe(3)
  246. },
  247. mounted() {
  248. calls.push('comp mounted')
  249. },
  250. render(this: any) {
  251. return `${this.a}${this.b}${this.c}`
  252. },
  253. }
  254. const app = createApp(Comp)
  255. app.mixin(mixinA)
  256. app.mixin(mixinB)
  257. app.mixin(mixinA)
  258. app.mixin(mixinB)
  259. expect(
  260. 'Mixin has already been applied to target app',
  261. ).toHaveBeenWarnedTimes(2)
  262. expect(
  263. 'Mixin has already been applied to target app: mixinB',
  264. ).toHaveBeenWarnedTimes(1)
  265. const root = nodeOps.createElement('div')
  266. app.mount(root)
  267. expect(serializeInner(root)).toBe(`123`)
  268. expect(calls).toEqual([
  269. 'mixinA created',
  270. 'mixinB created',
  271. 'comp created',
  272. 'mixinA mounted',
  273. 'mixinB mounted',
  274. 'comp mounted',
  275. ])
  276. })
  277. test('use', () => {
  278. const PluginA: Plugin = app => app.provide('foo', 1)
  279. const PluginB: Plugin = {
  280. install: (app, arg1, arg2) => app.provide('bar', arg1 + arg2),
  281. }
  282. class PluginC {
  283. someProperty = {}
  284. static install() {
  285. app.provide('baz', 2)
  286. }
  287. }
  288. const PluginD: any = undefined
  289. const Root = {
  290. setup() {
  291. const foo = inject('foo')
  292. const bar = inject('bar')
  293. return () => `${foo},${bar}`
  294. },
  295. }
  296. const app = createApp(Root)
  297. app.use(PluginA)
  298. app.use(PluginB, 1, 1)
  299. app.use(PluginC)
  300. const root = nodeOps.createElement('div')
  301. app.mount(root)
  302. expect(serializeInner(root)).toBe(`1,2`)
  303. app.use(PluginA)
  304. expect(
  305. `Plugin has already been applied to target app`,
  306. ).toHaveBeenWarnedTimes(1)
  307. app.use(PluginD)
  308. expect(
  309. `A plugin must either be a function or an object with an "install" ` +
  310. `function.`,
  311. ).toHaveBeenWarnedTimes(1)
  312. })
  313. test('onUnmount', () => {
  314. const cleanup = vi.fn().mockName('plugin cleanup')
  315. const PluginA: Plugin = app => {
  316. app.provide('foo', 1)
  317. app.onUnmount(cleanup)
  318. }
  319. const PluginB: Plugin = {
  320. install: (app, arg1, arg2) => {
  321. app.provide('bar', arg1 + arg2)
  322. app.onUnmount(cleanup)
  323. },
  324. }
  325. const app = createApp({
  326. render: () => `Test`,
  327. })
  328. app.use(PluginA)
  329. app.use(PluginB)
  330. const root = nodeOps.createElement('div')
  331. app.mount(root)
  332. //also can be added after mount
  333. app.onUnmount(cleanup)
  334. app.unmount()
  335. expect(cleanup).toHaveBeenCalledTimes(3)
  336. })
  337. test('config.errorHandler', () => {
  338. const error = new Error()
  339. const count = ref(0)
  340. const handler = vi.fn((err, instance, info) => {
  341. expect(err).toBe(error)
  342. expect(instance.count).toBe(count.value)
  343. expect(info).toBe(`render function`)
  344. })
  345. const Root = {
  346. setup() {
  347. const count = ref(0)
  348. return {
  349. count,
  350. }
  351. },
  352. render() {
  353. throw error
  354. },
  355. }
  356. const app = createApp(Root)
  357. app.config.errorHandler = handler
  358. app.mount(nodeOps.createElement('div'))
  359. expect(handler).toHaveBeenCalled()
  360. })
  361. test('config.warnHandler', () => {
  362. let ctx: any
  363. const handler = vi.fn((msg, instance, trace) => {
  364. expect(msg).toMatch(`Component is missing template or render function`)
  365. expect(instance).toBe(ctx.proxy)
  366. expect(trace).toMatch(`Hello`)
  367. })
  368. const Root = {
  369. name: 'Hello',
  370. setup() {
  371. ctx = getCurrentInstance()
  372. },
  373. }
  374. const app = createApp(Root)
  375. app.config.warnHandler = handler
  376. app.mount(nodeOps.createElement('div'))
  377. expect(handler).toHaveBeenCalledTimes(1)
  378. })
  379. describe('config.isNativeTag', () => {
  380. const isNativeTag = vi.fn(tag => tag === 'div')
  381. test('Component.name', () => {
  382. const Root = {
  383. name: 'div',
  384. render() {
  385. return null
  386. },
  387. }
  388. const app = createApp(Root)
  389. Object.defineProperty(app.config, 'isNativeTag', {
  390. value: isNativeTag,
  391. writable: false,
  392. })
  393. app.mount(nodeOps.createElement('div'))
  394. expect(
  395. `Do not use built-in or reserved HTML elements as component id: div`,
  396. ).toHaveBeenWarned()
  397. })
  398. test('Component.components', () => {
  399. const Root = {
  400. components: {
  401. div: () => 'div',
  402. },
  403. render() {
  404. return null
  405. },
  406. }
  407. const app = createApp(Root)
  408. Object.defineProperty(app.config, 'isNativeTag', {
  409. value: isNativeTag,
  410. writable: false,
  411. })
  412. app.mount(nodeOps.createElement('div'))
  413. expect(
  414. `Do not use built-in or reserved HTML elements as component id: div`,
  415. ).toHaveBeenWarned()
  416. })
  417. test('Component.directives', () => {
  418. const Root = {
  419. directives: {
  420. bind: () => {},
  421. },
  422. render() {
  423. return null
  424. },
  425. }
  426. const app = createApp(Root)
  427. app.mount(nodeOps.createElement('div'))
  428. expect(
  429. `Do not use built-in directive ids as custom directive id: bind`,
  430. ).toHaveBeenWarned()
  431. })
  432. test('register using app.component', () => {
  433. const app = createApp({
  434. render() {},
  435. })
  436. Object.defineProperty(app.config, 'isNativeTag', {
  437. value: isNativeTag,
  438. writable: false,
  439. })
  440. app.component('div', () => 'div')
  441. app.mount(nodeOps.createElement('div'))
  442. expect(
  443. `Do not use built-in or reserved HTML elements as component id: div`,
  444. ).toHaveBeenWarned()
  445. })
  446. })
  447. test('config.optionMergeStrategies', () => {
  448. let merged: string
  449. const App = defineComponent({
  450. render() {},
  451. mixins: [{ foo: 'mixin' }],
  452. extends: { foo: 'extends' },
  453. foo: 'local',
  454. beforeCreate() {
  455. merged = this.$options.foo
  456. },
  457. })
  458. const app = createApp(App)
  459. app.mixin({
  460. foo: 'global',
  461. })
  462. app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
  463. app.mount(nodeOps.createElement('div'))
  464. expect(merged!).toBe('global,extends,mixin,local')
  465. })
  466. test('config.globalProperties', () => {
  467. const app = createApp({
  468. render() {
  469. return this.foo
  470. },
  471. })
  472. app.config.globalProperties.foo = 'hello'
  473. const root = nodeOps.createElement('div')
  474. app.mount(root)
  475. expect(serializeInner(root)).toBe('hello')
  476. })
  477. test('config.throwUnhandledErrorInProduction', () => {
  478. __DEV__ = false
  479. try {
  480. const err = new Error()
  481. const app = createApp({
  482. setup() {
  483. throw err
  484. },
  485. })
  486. app.config.throwUnhandledErrorInProduction = true
  487. const root = nodeOps.createElement('div')
  488. expect(() => app.mount(root)).toThrow(err)
  489. } finally {
  490. __DEV__ = true
  491. }
  492. })
  493. test('return property "_" should not overwrite "ctx._", __isScriptSetup: false', () => {
  494. const Comp = defineComponent({
  495. setup() {
  496. return {
  497. _: ref(0), // return property "_" should not overwrite "ctx._"
  498. }
  499. },
  500. render() {
  501. return h('input', {
  502. ref: 'input',
  503. })
  504. },
  505. })
  506. const root1 = nodeOps.createElement('div')
  507. createApp(Comp).mount(root1)
  508. expect(
  509. `setup() return property "_" should not start with "$" or "_" which are reserved prefixes for Vue internals.`,
  510. ).toHaveBeenWarned()
  511. })
  512. test('return property "_" should not overwrite "ctx._", __isScriptSetup: true', () => {
  513. const Comp = defineComponent({
  514. setup() {
  515. return {
  516. _: ref(0), // return property "_" should not overwrite "ctx._"
  517. __isScriptSetup: true, // mock __isScriptSetup = true
  518. }
  519. },
  520. render() {
  521. return h('input', {
  522. ref: 'input',
  523. })
  524. },
  525. })
  526. const root1 = nodeOps.createElement('div')
  527. const app = createApp(Comp).mount(root1)
  528. // trigger
  529. app.$refs.input
  530. expect(
  531. `TypeError: Cannot read property '__isScriptSetup' of undefined`,
  532. ).not.toHaveBeenWarned()
  533. })
  534. // #10005
  535. test('flush order edge case on nested createApp', async () => {
  536. const order: string[] = []
  537. const App = defineComponent({
  538. setup(props) {
  539. const message = ref('m1')
  540. watch(
  541. message,
  542. () => {
  543. order.push('post watcher')
  544. },
  545. { flush: 'post' },
  546. )
  547. onMounted(() => {
  548. message.value = 'm2'
  549. createApp(() => '').mount(nodeOps.createElement('div'))
  550. })
  551. return () => {
  552. order.push('render')
  553. return h('div', [message.value])
  554. }
  555. },
  556. })
  557. createApp(App).mount(nodeOps.createElement('div'))
  558. await nextTick()
  559. expect(order).toMatchObject(['render', 'render', 'post watcher'])
  560. })
  561. // #14215
  562. test("unmount new app should not trigger other app's watcher", async () => {
  563. const compWatcherTriggerFn = vi.fn()
  564. const data = ref(true)
  565. const foo = ref('')
  566. const createNewApp = () => {
  567. const app = createApp({ render: () => h('new app') })
  568. const wrapper = nodeOps.createElement('div')
  569. app.mount(wrapper)
  570. return function destroy() {
  571. app.unmount()
  572. }
  573. }
  574. const Comp = defineComponent({
  575. setup() {
  576. watch(() => foo.value, compWatcherTriggerFn)
  577. return () => h('div', 'comp')
  578. },
  579. })
  580. const App = defineComponent({
  581. setup() {
  582. return () => (data.value ? h(Comp) : null)
  583. },
  584. })
  585. createApp(App).mount(nodeOps.createElement('div'))
  586. await nextTick()
  587. data.value = false
  588. const destroy = createNewApp()
  589. foo.value = 'bar'
  590. destroy()
  591. await nextTick()
  592. expect(compWatcherTriggerFn).toBeCalledTimes(0)
  593. })
  594. // config.compilerOptions is tested in packages/vue since it is only
  595. // supported in the full build.
  596. })