apiCreateApp.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  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. return () => h('div')
  105. },
  106. })
  107. app.provide('foo', 1)
  108. expect(app.runWithContext(() => inject('foo'))).toBe(1)
  109. expect(
  110. app.runWithContext(() => {
  111. app.runWithContext(() => {})
  112. return inject('foo')
  113. }),
  114. ).toBe(1)
  115. // ensure the context is restored
  116. inject('foo')
  117. expect('inject() can only be used inside setup').toHaveBeenWarned()
  118. })
  119. test('component', () => {
  120. const Root = {
  121. // local override
  122. components: {
  123. BarBaz: () => 'barbaz-local!',
  124. },
  125. setup() {
  126. // resolve in setup
  127. const FooBar = resolveComponent('foo-bar')
  128. return () => {
  129. // resolve in render
  130. const BarBaz = resolveComponent('bar-baz')
  131. return h('div', [h(FooBar), h(BarBaz)])
  132. }
  133. },
  134. }
  135. const app = createApp(Root)
  136. const FooBar = () => 'foobar!'
  137. app.component('FooBar', FooBar)
  138. expect(app.component('FooBar')).toBe(FooBar)
  139. app.component('BarBaz', () => 'barbaz!')
  140. app.component('BarBaz', () => 'barbaz!')
  141. expect(
  142. 'Component "BarBaz" has already been registered in target app.',
  143. ).toHaveBeenWarnedTimes(1)
  144. const root = nodeOps.createElement('div')
  145. app.mount(root)
  146. expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
  147. })
  148. test('directive', () => {
  149. const spy1 = vi.fn()
  150. const spy2 = vi.fn()
  151. const spy3 = vi.fn()
  152. const Root = {
  153. // local override
  154. directives: {
  155. BarBaz: { mounted: spy3 },
  156. },
  157. setup() {
  158. // resolve in setup
  159. const FooBar = resolveDirective('foo-bar')
  160. return () => {
  161. // resolve in render
  162. const BarBaz = resolveDirective('bar-baz')
  163. return withDirectives(h('div'), [[FooBar], [BarBaz]])
  164. }
  165. },
  166. }
  167. const app = createApp(Root)
  168. const FooBar = { mounted: spy1 }
  169. app.directive('FooBar', FooBar)
  170. expect(app.directive('FooBar')).toBe(FooBar)
  171. app.directive('BarBaz', {
  172. mounted: spy2,
  173. })
  174. app.directive('BarBaz', {
  175. mounted: spy2,
  176. })
  177. expect(
  178. 'Directive "BarBaz" has already been registered in target app.',
  179. ).toHaveBeenWarnedTimes(1)
  180. const root = nodeOps.createElement('div')
  181. app.mount(root)
  182. expect(spy1).toHaveBeenCalled()
  183. expect(spy2).not.toHaveBeenCalled()
  184. expect(spy3).toHaveBeenCalled()
  185. app.directive('bind', FooBar)
  186. expect(
  187. `Do not use built-in directive ids as custom directive id: bind`,
  188. ).toHaveBeenWarned()
  189. })
  190. test('mixin', () => {
  191. const calls: string[] = []
  192. const mixinA = {
  193. data() {
  194. return {
  195. a: 1,
  196. }
  197. },
  198. created(this: any) {
  199. calls.push('mixinA created')
  200. expect(this.a).toBe(1)
  201. expect(this.b).toBe(2)
  202. expect(this.c).toBe(3)
  203. },
  204. mounted() {
  205. calls.push('mixinA mounted')
  206. },
  207. }
  208. const mixinB = {
  209. name: 'mixinB',
  210. data() {
  211. return {
  212. b: 2,
  213. }
  214. },
  215. created(this: any) {
  216. calls.push('mixinB created')
  217. expect(this.a).toBe(1)
  218. expect(this.b).toBe(2)
  219. expect(this.c).toBe(3)
  220. },
  221. mounted() {
  222. calls.push('mixinB mounted')
  223. },
  224. }
  225. const Comp = {
  226. data() {
  227. return {
  228. c: 3,
  229. }
  230. },
  231. created(this: any) {
  232. calls.push('comp created')
  233. expect(this.a).toBe(1)
  234. expect(this.b).toBe(2)
  235. expect(this.c).toBe(3)
  236. },
  237. mounted() {
  238. calls.push('comp mounted')
  239. },
  240. render(this: any) {
  241. return `${this.a}${this.b}${this.c}`
  242. },
  243. }
  244. const app = createApp(Comp)
  245. app.mixin(mixinA)
  246. app.mixin(mixinB)
  247. app.mixin(mixinA)
  248. app.mixin(mixinB)
  249. expect(
  250. 'Mixin has already been applied to target app',
  251. ).toHaveBeenWarnedTimes(2)
  252. expect(
  253. 'Mixin has already been applied to target app: mixinB',
  254. ).toHaveBeenWarnedTimes(1)
  255. const root = nodeOps.createElement('div')
  256. app.mount(root)
  257. expect(serializeInner(root)).toBe(`123`)
  258. expect(calls).toEqual([
  259. 'mixinA created',
  260. 'mixinB created',
  261. 'comp created',
  262. 'mixinA mounted',
  263. 'mixinB mounted',
  264. 'comp mounted',
  265. ])
  266. })
  267. test('use', () => {
  268. const PluginA: Plugin = app => app.provide('foo', 1)
  269. const PluginB: Plugin = {
  270. install: (app, arg1, arg2) => app.provide('bar', arg1 + arg2),
  271. }
  272. class PluginC {
  273. someProperty = {}
  274. static install() {
  275. app.provide('baz', 2)
  276. }
  277. }
  278. const PluginD: any = undefined
  279. const Root = {
  280. setup() {
  281. const foo = inject('foo')
  282. const bar = inject('bar')
  283. return () => `${foo},${bar}`
  284. },
  285. }
  286. const app = createApp(Root)
  287. app.use(PluginA)
  288. app.use(PluginB, 1, 1)
  289. app.use(PluginC)
  290. const root = nodeOps.createElement('div')
  291. app.mount(root)
  292. expect(serializeInner(root)).toBe(`1,2`)
  293. app.use(PluginA)
  294. expect(
  295. `Plugin has already been applied to target app`,
  296. ).toHaveBeenWarnedTimes(1)
  297. app.use(PluginD)
  298. expect(
  299. `A plugin must either be a function or an object with an "install" ` +
  300. `function.`,
  301. ).toHaveBeenWarnedTimes(1)
  302. })
  303. test('onUnmount', () => {
  304. const cleanup = vi.fn().mockName('plugin cleanup')
  305. const PluginA: Plugin = app => {
  306. app.provide('foo', 1)
  307. app.onUnmount(cleanup)
  308. }
  309. const PluginB: Plugin = {
  310. install: (app, arg1, arg2) => {
  311. app.provide('bar', arg1 + arg2)
  312. app.onUnmount(cleanup)
  313. },
  314. }
  315. const app = createApp({
  316. render: () => `Test`,
  317. })
  318. app.use(PluginA)
  319. app.use(PluginB)
  320. const root = nodeOps.createElement('div')
  321. app.mount(root)
  322. //also can be added after mount
  323. app.onUnmount(cleanup)
  324. app.unmount()
  325. expect(cleanup).toHaveBeenCalledTimes(3)
  326. })
  327. test('config.errorHandler', () => {
  328. const error = new Error()
  329. const count = ref(0)
  330. const handler = vi.fn((err, instance, info) => {
  331. expect(err).toBe(error)
  332. expect(instance.count).toBe(count.value)
  333. expect(info).toBe(`render function`)
  334. })
  335. const Root = {
  336. setup() {
  337. const count = ref(0)
  338. return {
  339. count,
  340. }
  341. },
  342. render() {
  343. throw error
  344. },
  345. }
  346. const app = createApp(Root)
  347. app.config.errorHandler = handler
  348. app.mount(nodeOps.createElement('div'))
  349. expect(handler).toHaveBeenCalled()
  350. })
  351. test('config.warnHandler', () => {
  352. let ctx: any
  353. const handler = vi.fn((msg, instance, trace) => {
  354. expect(msg).toMatch(`Component is missing template or render function`)
  355. expect(instance).toBe(ctx.proxy)
  356. expect(trace).toMatch(`Hello`)
  357. })
  358. const Root = {
  359. name: 'Hello',
  360. setup() {
  361. ctx = getCurrentInstance()
  362. },
  363. }
  364. const app = createApp(Root)
  365. app.config.warnHandler = handler
  366. app.mount(nodeOps.createElement('div'))
  367. expect(handler).toHaveBeenCalledTimes(1)
  368. })
  369. describe('config.isNativeTag', () => {
  370. const isNativeTag = vi.fn(tag => tag === 'div')
  371. test('Component.name', () => {
  372. const Root = {
  373. name: 'div',
  374. render() {
  375. return null
  376. },
  377. }
  378. const app = createApp(Root)
  379. Object.defineProperty(app.config, 'isNativeTag', {
  380. value: isNativeTag,
  381. writable: false,
  382. })
  383. app.mount(nodeOps.createElement('div'))
  384. expect(
  385. `Do not use built-in or reserved HTML elements as component id: div`,
  386. ).toHaveBeenWarned()
  387. })
  388. test('Component.components', () => {
  389. const Root = {
  390. components: {
  391. div: () => 'div',
  392. },
  393. render() {
  394. return null
  395. },
  396. }
  397. const app = createApp(Root)
  398. Object.defineProperty(app.config, 'isNativeTag', {
  399. value: isNativeTag,
  400. writable: false,
  401. })
  402. app.mount(nodeOps.createElement('div'))
  403. expect(
  404. `Do not use built-in or reserved HTML elements as component id: div`,
  405. ).toHaveBeenWarned()
  406. })
  407. test('Component.directives', () => {
  408. const Root = {
  409. directives: {
  410. bind: () => {},
  411. },
  412. render() {
  413. return null
  414. },
  415. }
  416. const app = createApp(Root)
  417. app.mount(nodeOps.createElement('div'))
  418. expect(
  419. `Do not use built-in directive ids as custom directive id: bind`,
  420. ).toHaveBeenWarned()
  421. })
  422. test('register using app.component', () => {
  423. const app = createApp({
  424. render() {},
  425. })
  426. Object.defineProperty(app.config, 'isNativeTag', {
  427. value: isNativeTag,
  428. writable: false,
  429. })
  430. app.component('div', () => 'div')
  431. app.mount(nodeOps.createElement('div'))
  432. expect(
  433. `Do not use built-in or reserved HTML elements as component id: div`,
  434. ).toHaveBeenWarned()
  435. })
  436. })
  437. test('config.optionMergeStrategies', () => {
  438. let merged: string
  439. const App = defineComponent({
  440. render() {},
  441. mixins: [{ foo: 'mixin' }],
  442. extends: { foo: 'extends' },
  443. foo: 'local',
  444. beforeCreate() {
  445. merged = this.$options.foo
  446. },
  447. })
  448. const app = createApp(App)
  449. app.mixin({
  450. foo: 'global',
  451. })
  452. app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
  453. app.mount(nodeOps.createElement('div'))
  454. expect(merged!).toBe('global,extends,mixin,local')
  455. })
  456. test('config.globalProperties', () => {
  457. const app = createApp({
  458. render() {
  459. return this.foo
  460. },
  461. })
  462. app.config.globalProperties.foo = 'hello'
  463. const root = nodeOps.createElement('div')
  464. app.mount(root)
  465. expect(serializeInner(root)).toBe('hello')
  466. })
  467. test('config.throwUnhandledErrorInProduction', () => {
  468. __DEV__ = false
  469. try {
  470. const err = new Error()
  471. const app = createApp({
  472. setup() {
  473. throw err
  474. },
  475. })
  476. app.config.throwUnhandledErrorInProduction = true
  477. const root = nodeOps.createElement('div')
  478. expect(() => app.mount(root)).toThrow(err)
  479. } finally {
  480. __DEV__ = true
  481. }
  482. })
  483. test('return property "_" should not overwrite "ctx._", __isScriptSetup: false', () => {
  484. const Comp = defineComponent({
  485. setup() {
  486. return {
  487. _: ref(0), // return property "_" should not overwrite "ctx._"
  488. }
  489. },
  490. render() {
  491. return h('input', {
  492. ref: 'input',
  493. })
  494. },
  495. })
  496. const root1 = nodeOps.createElement('div')
  497. createApp(Comp).mount(root1)
  498. expect(
  499. `setup() return property "_" should not start with "$" or "_" which are reserved prefixes for Vue internals.`,
  500. ).toHaveBeenWarned()
  501. })
  502. test('return property "_" should not overwrite "ctx._", __isScriptSetup: true', () => {
  503. const Comp = defineComponent({
  504. setup() {
  505. return {
  506. _: ref(0), // return property "_" should not overwrite "ctx._"
  507. __isScriptSetup: true, // mock __isScriptSetup = true
  508. }
  509. },
  510. render() {
  511. return h('input', {
  512. ref: 'input',
  513. })
  514. },
  515. })
  516. const root1 = nodeOps.createElement('div')
  517. const app = createApp(Comp).mount(root1)
  518. // trigger
  519. app.$refs.input
  520. expect(
  521. `TypeError: Cannot read property '__isScriptSetup' of undefined`,
  522. ).not.toHaveBeenWarned()
  523. })
  524. // #10005
  525. test('flush order edge case on nested createApp', async () => {
  526. const order: string[] = []
  527. const App = defineComponent({
  528. setup(props) {
  529. const message = ref('m1')
  530. watch(
  531. message,
  532. () => {
  533. order.push('post watcher')
  534. },
  535. { flush: 'post' },
  536. )
  537. onMounted(() => {
  538. message.value = 'm2'
  539. createApp(() => '').mount(nodeOps.createElement('div'))
  540. })
  541. return () => {
  542. order.push('render')
  543. return h('div', [message.value])
  544. }
  545. },
  546. })
  547. createApp(App).mount(nodeOps.createElement('div'))
  548. await nextTick()
  549. expect(order).toMatchObject(['render', 'render', 'post watcher'])
  550. })
  551. // config.compilerOptions is tested in packages/vue since it is only
  552. // supported in the full build.
  553. })