apiCreateApp.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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('config.errorHandler', () => {
  314. const error = new Error()
  315. const count = ref(0)
  316. const handler = vi.fn((err, instance, info) => {
  317. expect(err).toBe(error)
  318. expect(instance.count).toBe(count.value)
  319. expect(info).toBe(`render function`)
  320. })
  321. const Root = {
  322. setup() {
  323. const count = ref(0)
  324. return {
  325. count,
  326. }
  327. },
  328. render() {
  329. throw error
  330. },
  331. }
  332. const app = createApp(Root)
  333. app.config.errorHandler = handler
  334. app.mount(nodeOps.createElement('div'))
  335. expect(handler).toHaveBeenCalled()
  336. })
  337. test('config.warnHandler', () => {
  338. let ctx: any
  339. const handler = vi.fn((msg, instance, trace) => {
  340. expect(msg).toMatch(`Component is missing template or render function`)
  341. expect(instance).toBe(ctx.proxy)
  342. expect(trace).toMatch(`Hello`)
  343. })
  344. const Root = {
  345. name: 'Hello',
  346. setup() {
  347. ctx = getCurrentInstance()
  348. },
  349. }
  350. const app = createApp(Root)
  351. app.config.warnHandler = handler
  352. app.mount(nodeOps.createElement('div'))
  353. expect(handler).toHaveBeenCalledTimes(1)
  354. })
  355. describe('config.isNativeTag', () => {
  356. const isNativeTag = vi.fn(tag => tag === 'div')
  357. test('Component.name', () => {
  358. const Root = {
  359. name: 'div',
  360. render() {
  361. return null
  362. },
  363. }
  364. const app = createApp(Root)
  365. Object.defineProperty(app.config, 'isNativeTag', {
  366. value: isNativeTag,
  367. writable: false,
  368. })
  369. app.mount(nodeOps.createElement('div'))
  370. expect(
  371. `Do not use built-in or reserved HTML elements as component id: div`,
  372. ).toHaveBeenWarned()
  373. })
  374. test('Component.components', () => {
  375. const Root = {
  376. components: {
  377. div: () => 'div',
  378. },
  379. render() {
  380. return null
  381. },
  382. }
  383. const app = createApp(Root)
  384. Object.defineProperty(app.config, 'isNativeTag', {
  385. value: isNativeTag,
  386. writable: false,
  387. })
  388. app.mount(nodeOps.createElement('div'))
  389. expect(
  390. `Do not use built-in or reserved HTML elements as component id: div`,
  391. ).toHaveBeenWarned()
  392. })
  393. test('Component.directives', () => {
  394. const Root = {
  395. directives: {
  396. bind: () => {},
  397. },
  398. render() {
  399. return null
  400. },
  401. }
  402. const app = createApp(Root)
  403. app.mount(nodeOps.createElement('div'))
  404. expect(
  405. `Do not use built-in directive ids as custom directive id: bind`,
  406. ).toHaveBeenWarned()
  407. })
  408. test('register using app.component', () => {
  409. const app = createApp({
  410. render() {},
  411. })
  412. Object.defineProperty(app.config, 'isNativeTag', {
  413. value: isNativeTag,
  414. writable: false,
  415. })
  416. app.component('div', () => 'div')
  417. app.mount(nodeOps.createElement('div'))
  418. expect(
  419. `Do not use built-in or reserved HTML elements as component id: div`,
  420. ).toHaveBeenWarned()
  421. })
  422. })
  423. test('config.optionMergeStrategies', () => {
  424. let merged: string
  425. const App = defineComponent({
  426. render() {},
  427. mixins: [{ foo: 'mixin' }],
  428. extends: { foo: 'extends' },
  429. foo: 'local',
  430. beforeCreate() {
  431. merged = this.$options.foo
  432. },
  433. })
  434. const app = createApp(App)
  435. app.mixin({
  436. foo: 'global',
  437. })
  438. app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
  439. app.mount(nodeOps.createElement('div'))
  440. expect(merged!).toBe('global,extends,mixin,local')
  441. })
  442. test('config.globalProperties', () => {
  443. const app = createApp({
  444. render() {
  445. return this.foo
  446. },
  447. })
  448. app.config.globalProperties.foo = 'hello'
  449. const root = nodeOps.createElement('div')
  450. app.mount(root)
  451. expect(serializeInner(root)).toBe('hello')
  452. })
  453. test('return property "_" should not overwrite "ctx._", __isScriptSetup: false', () => {
  454. const Comp = defineComponent({
  455. setup() {
  456. return {
  457. _: ref(0), // return property "_" should not overwrite "ctx._"
  458. }
  459. },
  460. render() {
  461. return h('input', {
  462. ref: 'input',
  463. })
  464. },
  465. })
  466. const root1 = nodeOps.createElement('div')
  467. createApp(Comp).mount(root1)
  468. expect(
  469. `setup() return property "_" should not start with "$" or "_" which are reserved prefixes for Vue internals.`,
  470. ).toHaveBeenWarned()
  471. })
  472. test('return property "_" should not overwrite "ctx._", __isScriptSetup: true', () => {
  473. const Comp = defineComponent({
  474. setup() {
  475. return {
  476. _: ref(0), // return property "_" should not overwrite "ctx._"
  477. __isScriptSetup: true, // mock __isScriptSetup = true
  478. }
  479. },
  480. render() {
  481. return h('input', {
  482. ref: 'input',
  483. })
  484. },
  485. })
  486. const root1 = nodeOps.createElement('div')
  487. const app = createApp(Comp).mount(root1)
  488. // trigger
  489. app.$refs.input
  490. expect(
  491. `TypeError: Cannot read property '__isScriptSetup' of undefined`,
  492. ).not.toHaveBeenWarned()
  493. })
  494. // #10005
  495. test('flush order edge case on nested createApp', async () => {
  496. const order: string[] = []
  497. const App = defineComponent({
  498. setup(props) {
  499. const message = ref('m1')
  500. watch(
  501. message,
  502. () => {
  503. order.push('post watcher')
  504. },
  505. { flush: 'post' },
  506. )
  507. onMounted(() => {
  508. message.value = 'm2'
  509. createApp(() => '').mount(nodeOps.createElement('div'))
  510. })
  511. return () => {
  512. order.push('render')
  513. return h('div', [message.value])
  514. }
  515. },
  516. })
  517. createApp(App).mount(nodeOps.createElement('div'))
  518. await nextTick()
  519. expect(order).toMatchObject(['render', 'render', 'post watcher'])
  520. })
  521. // config.compilerOptions is tested in packages/vue since it is only
  522. // supported in the full build.
  523. })