apiCreateApp.spec.ts 14 KB

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