apiOptions.spec.ts 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729
  1. /**
  2. * @vitest-environment jsdom
  3. */
  4. import { vi, type Mock } from 'vitest'
  5. import {
  6. h,
  7. nodeOps,
  8. render,
  9. serializeInner,
  10. triggerEvent,
  11. TestElement,
  12. nextTick,
  13. renderToString,
  14. ref,
  15. defineComponent,
  16. createApp,
  17. computed
  18. } from '@vue/runtime-test'
  19. import { render as domRender } from 'vue'
  20. describe('api: options', () => {
  21. test('data', async () => {
  22. const Comp = defineComponent({
  23. data() {
  24. return {
  25. foo: 1
  26. }
  27. },
  28. render() {
  29. return h(
  30. 'div',
  31. {
  32. onClick: () => {
  33. this.foo++
  34. }
  35. },
  36. this.foo
  37. )
  38. }
  39. })
  40. const root = nodeOps.createElement('div')
  41. render(h(Comp), root)
  42. expect(serializeInner(root)).toBe(`<div>1</div>`)
  43. triggerEvent(root.children[0] as TestElement, 'click')
  44. await nextTick()
  45. expect(serializeInner(root)).toBe(`<div>2</div>`)
  46. })
  47. test('computed', async () => {
  48. const Comp = defineComponent({
  49. data() {
  50. return {
  51. foo: 1
  52. }
  53. },
  54. computed: {
  55. bar(): number {
  56. return this.foo + 1
  57. },
  58. baz: (vm: any): number => vm.bar + 1
  59. },
  60. render() {
  61. return h(
  62. 'div',
  63. {
  64. onClick: () => {
  65. this.foo++
  66. }
  67. },
  68. this.bar + this.baz
  69. )
  70. }
  71. })
  72. const root = nodeOps.createElement('div')
  73. render(h(Comp), root)
  74. expect(serializeInner(root)).toBe(`<div>5</div>`)
  75. triggerEvent(root.children[0] as TestElement, 'click')
  76. await nextTick()
  77. expect(serializeInner(root)).toBe(`<div>7</div>`)
  78. })
  79. test('methods', async () => {
  80. const Comp = defineComponent({
  81. data() {
  82. // #3300 method on ctx should be overwritable
  83. this.incBy = this.incBy.bind(this, 2)
  84. return {
  85. foo: 1
  86. }
  87. },
  88. methods: {
  89. inc() {
  90. this.foo++
  91. },
  92. incBy(n = 0) {
  93. this.foo += n
  94. }
  95. },
  96. render() {
  97. return h(
  98. 'div',
  99. {
  100. onClick: this.inc,
  101. onFoo: this.incBy
  102. },
  103. this.foo
  104. )
  105. }
  106. })
  107. const root = nodeOps.createElement('div')
  108. render(h(Comp), root)
  109. expect(serializeInner(root)).toBe(`<div>1</div>`)
  110. triggerEvent(root.children[0] as TestElement, 'click')
  111. await nextTick()
  112. expect(serializeInner(root)).toBe(`<div>2</div>`)
  113. triggerEvent(root.children[0] as TestElement, 'foo')
  114. await nextTick()
  115. expect(serializeInner(root)).toBe(`<div>4</div>`)
  116. })
  117. test('component’s own methods have higher priority than global properties', async () => {
  118. const app = createApp({
  119. methods: {
  120. foo() {
  121. return 'foo'
  122. }
  123. },
  124. render() {
  125. return this.foo()
  126. }
  127. })
  128. app.config.globalProperties.foo = () => 'bar'
  129. const root = nodeOps.createElement('div')
  130. app.mount(root)
  131. expect(serializeInner(root)).toBe(`foo`)
  132. })
  133. test('watch', async () => {
  134. function returnThis(this: any) {
  135. return this
  136. }
  137. const spyA = vi.fn(returnThis)
  138. const spyB = vi.fn(returnThis)
  139. const spyC = vi.fn(returnThis)
  140. const spyD = vi.fn(returnThis)
  141. const spyE = vi.fn(returnThis)
  142. let ctx: any
  143. const Comp = {
  144. data() {
  145. return {
  146. foo: 1,
  147. bar: 2,
  148. baz: {
  149. qux: 3
  150. },
  151. qux: 4,
  152. dot: {
  153. path: 5
  154. }
  155. }
  156. },
  157. watch: {
  158. // string method name
  159. foo: 'onFooChange',
  160. // direct function
  161. bar: spyB,
  162. baz: {
  163. handler: spyC,
  164. deep: true
  165. },
  166. qux: {
  167. handler: 'onQuxChange'
  168. },
  169. 'dot.path': spyE
  170. },
  171. methods: {
  172. onFooChange: spyA,
  173. onQuxChange: spyD
  174. },
  175. render() {
  176. ctx = this
  177. }
  178. }
  179. const root = nodeOps.createElement('div')
  180. render(h(Comp), root)
  181. function assertCall(spy: Mock, callIndex: number, args: any[]) {
  182. expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
  183. expect(spy.mock.results[callIndex].value).toBe(ctx)
  184. }
  185. ctx.foo++
  186. await nextTick()
  187. expect(spyA).toHaveBeenCalledTimes(1)
  188. assertCall(spyA, 0, [2, 1])
  189. ctx.bar++
  190. await nextTick()
  191. expect(spyB).toHaveBeenCalledTimes(1)
  192. assertCall(spyB, 0, [3, 2])
  193. ctx.baz.qux++
  194. await nextTick()
  195. expect(spyC).toHaveBeenCalledTimes(1)
  196. // new and old objects have same identity
  197. assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
  198. ctx.qux++
  199. await nextTick()
  200. expect(spyD).toHaveBeenCalledTimes(1)
  201. assertCall(spyD, 0, [5, 4])
  202. ctx.dot.path++
  203. await nextTick()
  204. expect(spyE).toHaveBeenCalledTimes(1)
  205. assertCall(spyE, 0, [6, 5])
  206. })
  207. test('watch array', async () => {
  208. function returnThis(this: any) {
  209. return this
  210. }
  211. const spyA = vi.fn(returnThis)
  212. const spyB = vi.fn(returnThis)
  213. const spyC = vi.fn(returnThis)
  214. let ctx: any
  215. const Comp = {
  216. data() {
  217. return {
  218. foo: 1,
  219. bar: 2,
  220. baz: {
  221. qux: 3
  222. }
  223. }
  224. },
  225. watch: {
  226. // string method name
  227. foo: ['onFooChange'],
  228. // direct function
  229. bar: [spyB],
  230. baz: [
  231. {
  232. handler: spyC,
  233. deep: true
  234. }
  235. ]
  236. },
  237. methods: {
  238. onFooChange: spyA
  239. },
  240. render() {
  241. ctx = this
  242. }
  243. }
  244. const root = nodeOps.createElement('div')
  245. render(h(Comp), root)
  246. function assertCall(spy: Mock, callIndex: number, args: any[]) {
  247. expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
  248. expect(spy.mock.results[callIndex].value).toBe(ctx)
  249. }
  250. ctx.foo++
  251. await nextTick()
  252. expect(spyA).toHaveBeenCalledTimes(1)
  253. assertCall(spyA, 0, [2, 1])
  254. ctx.bar++
  255. await nextTick()
  256. expect(spyB).toHaveBeenCalledTimes(1)
  257. assertCall(spyB, 0, [3, 2])
  258. ctx.baz.qux++
  259. await nextTick()
  260. expect(spyC).toHaveBeenCalledTimes(1)
  261. // new and old objects have same identity
  262. assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
  263. })
  264. // #3966
  265. test('watch merging from mixins', async () => {
  266. const mixinA = {
  267. data() {
  268. return {
  269. fromMixinA: ''
  270. }
  271. },
  272. watch: {
  273. obj: {
  274. handler(this: any, to: any) {
  275. this.fromMixinA = to
  276. }
  277. }
  278. }
  279. }
  280. const mixinB = {
  281. data() {
  282. return {
  283. fromMixinB: ''
  284. }
  285. },
  286. watch: {
  287. obj: 'setMixinB'
  288. },
  289. methods: {
  290. setMixinB(this: any, to: any) {
  291. this.fromMixinB = to
  292. }
  293. }
  294. }
  295. let vm: any
  296. const Comp = {
  297. render() {},
  298. mixins: [mixinA, mixinB],
  299. data: () => ({
  300. obj: 'foo',
  301. fromComp: ''
  302. }),
  303. watch: {
  304. obj(this: any, to: any) {
  305. this.fromComp = to
  306. }
  307. },
  308. mounted() {
  309. vm = this
  310. }
  311. }
  312. const root = nodeOps.createElement('div')
  313. render(h(Comp), root)
  314. vm.obj = 'bar'
  315. await nextTick()
  316. expect(vm.fromComp).toBe('bar')
  317. expect(vm.fromMixinA).toBe('bar')
  318. expect(vm.fromMixinB).toBe('bar')
  319. })
  320. test('provide/inject', () => {
  321. const symbolKey = Symbol()
  322. const Root = defineComponent({
  323. data() {
  324. return {
  325. a: 1
  326. }
  327. },
  328. provide() {
  329. return {
  330. a: this.a,
  331. [symbolKey]: 2
  332. }
  333. },
  334. render() {
  335. return [
  336. h(ChildA),
  337. h(ChildB),
  338. h(ChildC),
  339. h(ChildD),
  340. h(ChildE),
  341. h(ChildF),
  342. h(ChildG),
  343. h(ChildH),
  344. h(ChildI),
  345. h(ChildJ)
  346. ]
  347. }
  348. })
  349. const defineChild = (injectOptions: any, injectedKey = 'b') =>
  350. ({
  351. inject: injectOptions,
  352. render() {
  353. return this[injectedKey]
  354. }
  355. } as any)
  356. const ChildA = defineChild(['a'], 'a')
  357. const ChildB = defineChild({ b: 'a' })
  358. const ChildC = defineChild({
  359. b: {
  360. from: 'a'
  361. }
  362. })
  363. const ChildD = defineChild(
  364. {
  365. a: {
  366. default: () => 0
  367. }
  368. },
  369. 'a'
  370. )
  371. const ChildE = defineChild({
  372. b: {
  373. from: 'c',
  374. default: 2
  375. }
  376. })
  377. const ChildF = defineChild({
  378. b: {
  379. from: 'c',
  380. default: () => 3
  381. }
  382. })
  383. const ChildG = defineChild({
  384. b: {
  385. default: 4
  386. }
  387. })
  388. const ChildH = defineChild({
  389. b: {
  390. default: () => 5
  391. }
  392. })
  393. const ChildI = defineChild({
  394. b: symbolKey
  395. })
  396. const ChildJ = defineChild({
  397. b: {
  398. from: symbolKey
  399. }
  400. })
  401. expect(renderToString(h(Root))).toBe(`1111234522`)
  402. })
  403. test('provide/inject refs', async () => {
  404. const n = ref(0)
  405. const np = computed(() => n.value + 1)
  406. const Parent = defineComponent({
  407. provide() {
  408. return {
  409. n,
  410. np
  411. }
  412. },
  413. render: () => h(Child)
  414. })
  415. const Child = defineComponent({
  416. inject: ['n', 'np'],
  417. render(this: any) {
  418. return this.n + this.np
  419. }
  420. })
  421. const app = createApp(Parent)
  422. // TODO remove in 3.3
  423. app.config.unwrapInjectedRef = true
  424. const root = nodeOps.createElement('div')
  425. app.mount(root)
  426. expect(serializeInner(root)).toBe(`1`)
  427. n.value++
  428. await nextTick()
  429. expect(serializeInner(root)).toBe(`3`)
  430. })
  431. // TODO remove in 3.3
  432. test('provide/inject refs (compat)', async () => {
  433. const n = ref(0)
  434. const np = computed(() => n.value + 1)
  435. const Parent = defineComponent({
  436. provide() {
  437. return {
  438. n,
  439. np
  440. }
  441. },
  442. render: () => h(Child)
  443. })
  444. const Child = defineComponent({
  445. inject: ['n', 'np'],
  446. render(this: any) {
  447. return this.n.value + this.np.value
  448. }
  449. })
  450. const app = createApp(Parent)
  451. const root = nodeOps.createElement('div')
  452. app.mount(root)
  453. expect(serializeInner(root)).toBe(`1`)
  454. n.value++
  455. await nextTick()
  456. expect(serializeInner(root)).toBe(`3`)
  457. expect(`injected property "n" is a ref`).toHaveBeenWarned()
  458. expect(`injected property "np" is a ref`).toHaveBeenWarned()
  459. })
  460. test('provide accessing data in extends', () => {
  461. const Base = defineComponent({
  462. data() {
  463. return {
  464. a: 1
  465. }
  466. },
  467. provide() {
  468. return {
  469. a: this.a
  470. }
  471. }
  472. })
  473. const Child = {
  474. inject: ['a'],
  475. render() {
  476. return (this as any).a
  477. }
  478. }
  479. const Root = defineComponent({
  480. extends: Base,
  481. render() {
  482. return h(Child)
  483. }
  484. })
  485. expect(renderToString(h(Root))).toBe(`1`)
  486. })
  487. test('lifecycle', async () => {
  488. const count = ref(0)
  489. const root = nodeOps.createElement('div')
  490. const calls: string[] = []
  491. const Root = {
  492. beforeCreate() {
  493. calls.push('root beforeCreate')
  494. },
  495. created() {
  496. calls.push('root created')
  497. },
  498. beforeMount() {
  499. calls.push('root onBeforeMount')
  500. },
  501. mounted() {
  502. calls.push('root onMounted')
  503. },
  504. beforeUpdate() {
  505. calls.push('root onBeforeUpdate')
  506. },
  507. updated() {
  508. calls.push('root onUpdated')
  509. },
  510. beforeUnmount() {
  511. calls.push('root onBeforeUnmount')
  512. },
  513. unmounted() {
  514. calls.push('root onUnmounted')
  515. },
  516. render() {
  517. return h(Mid, { count: count.value })
  518. }
  519. }
  520. const Mid = {
  521. beforeCreate() {
  522. calls.push('mid beforeCreate')
  523. },
  524. created() {
  525. calls.push('mid created')
  526. },
  527. beforeMount() {
  528. calls.push('mid onBeforeMount')
  529. },
  530. mounted() {
  531. calls.push('mid onMounted')
  532. },
  533. beforeUpdate() {
  534. calls.push('mid onBeforeUpdate')
  535. },
  536. updated() {
  537. calls.push('mid onUpdated')
  538. },
  539. beforeUnmount() {
  540. calls.push('mid onBeforeUnmount')
  541. },
  542. unmounted() {
  543. calls.push('mid onUnmounted')
  544. },
  545. render(this: any) {
  546. return h(Child, { count: this.$props.count })
  547. }
  548. }
  549. const Child = {
  550. beforeCreate() {
  551. calls.push('child beforeCreate')
  552. },
  553. created() {
  554. calls.push('child created')
  555. },
  556. beforeMount() {
  557. calls.push('child onBeforeMount')
  558. },
  559. mounted() {
  560. calls.push('child onMounted')
  561. },
  562. beforeUpdate() {
  563. calls.push('child onBeforeUpdate')
  564. },
  565. updated() {
  566. calls.push('child onUpdated')
  567. },
  568. beforeUnmount() {
  569. calls.push('child onBeforeUnmount')
  570. },
  571. unmounted() {
  572. calls.push('child onUnmounted')
  573. },
  574. render(this: any) {
  575. return h('div', this.$props.count)
  576. }
  577. }
  578. // mount
  579. render(h(Root), root)
  580. expect(calls).toEqual([
  581. 'root beforeCreate',
  582. 'root created',
  583. 'root onBeforeMount',
  584. 'mid beforeCreate',
  585. 'mid created',
  586. 'mid onBeforeMount',
  587. 'child beforeCreate',
  588. 'child created',
  589. 'child onBeforeMount',
  590. 'child onMounted',
  591. 'mid onMounted',
  592. 'root onMounted'
  593. ])
  594. calls.length = 0
  595. // update
  596. count.value++
  597. await nextTick()
  598. expect(calls).toEqual([
  599. 'root onBeforeUpdate',
  600. 'mid onBeforeUpdate',
  601. 'child onBeforeUpdate',
  602. 'child onUpdated',
  603. 'mid onUpdated',
  604. 'root onUpdated'
  605. ])
  606. calls.length = 0
  607. // unmount
  608. render(null, root)
  609. expect(calls).toEqual([
  610. 'root onBeforeUnmount',
  611. 'mid onBeforeUnmount',
  612. 'child onBeforeUnmount',
  613. 'child onUnmounted',
  614. 'mid onUnmounted',
  615. 'root onUnmounted'
  616. ])
  617. })
  618. test('mixins', () => {
  619. const calls: string[] = []
  620. const mixinA = {
  621. data() {
  622. return {
  623. a: 1
  624. }
  625. },
  626. created(this: any) {
  627. calls.push('mixinA created')
  628. expect(this.a).toBe(1)
  629. expect(this.b).toBe(2)
  630. expect(this.c).toBe(4)
  631. },
  632. mounted() {
  633. calls.push('mixinA mounted')
  634. }
  635. }
  636. const mixinB = {
  637. props: {
  638. bP: {
  639. type: String
  640. }
  641. },
  642. data() {
  643. return {
  644. b: 2
  645. }
  646. },
  647. created(this: any) {
  648. calls.push('mixinB created')
  649. expect(this.a).toBe(1)
  650. expect(this.b).toBe(2)
  651. expect(this.bP).toBeUndefined()
  652. expect(this.c).toBe(4)
  653. expect(this.cP1).toBeUndefined()
  654. },
  655. mounted() {
  656. calls.push('mixinB mounted')
  657. }
  658. }
  659. const mixinC = defineComponent({
  660. props: ['cP1', 'cP2'],
  661. data() {
  662. return {
  663. c: 3
  664. }
  665. },
  666. created() {
  667. calls.push('mixinC created')
  668. // component data() should overwrite mixin field with same key
  669. expect(this.c).toBe(4)
  670. expect(this.cP1).toBeUndefined()
  671. },
  672. mounted() {
  673. calls.push('mixinC mounted')
  674. }
  675. })
  676. const Comp = defineComponent({
  677. props: {
  678. aaa: String
  679. },
  680. mixins: [defineComponent(mixinA), defineComponent(mixinB), mixinC],
  681. data() {
  682. return {
  683. c: 4,
  684. z: 4
  685. }
  686. },
  687. created() {
  688. calls.push('comp created')
  689. expect(this.a).toBe(1)
  690. expect(this.b).toBe(2)
  691. expect(this.bP).toBeUndefined()
  692. expect(this.c).toBe(4)
  693. expect(this.cP2).toBeUndefined()
  694. expect(this.z).toBe(4)
  695. },
  696. mounted() {
  697. calls.push('comp mounted')
  698. },
  699. render() {
  700. return `${this.a}${this.b}${this.c}`
  701. }
  702. })
  703. expect(renderToString(h(Comp))).toBe(`124`)
  704. expect(calls).toEqual([
  705. 'mixinA created',
  706. 'mixinB created',
  707. 'mixinC created',
  708. 'comp created',
  709. 'mixinA mounted',
  710. 'mixinB mounted',
  711. 'mixinC mounted',
  712. 'comp mounted'
  713. ])
  714. })
  715. test('render from mixin', () => {
  716. const Comp = {
  717. mixins: [
  718. {
  719. render: () => 'from mixin'
  720. }
  721. ]
  722. }
  723. expect(renderToString(h(Comp))).toBe('from mixin')
  724. })
  725. test('chained mixins in extends', () => {
  726. const calls: string[] = []
  727. const mixinA = {
  728. beforeCreate() {
  729. calls.push('mixinA beforeCreate')
  730. },
  731. created() {
  732. calls.push('mixinA created')
  733. }
  734. }
  735. const extendA = {
  736. mixins: [mixinA],
  737. beforeCreate() {
  738. calls.push('extendA beforeCreate')
  739. },
  740. created() {
  741. calls.push('extendA created')
  742. }
  743. }
  744. const Comp = {
  745. extends: extendA,
  746. render: () => '123',
  747. beforeCreate() {
  748. calls.push('self beforeCreate')
  749. },
  750. created() {
  751. calls.push('self created')
  752. }
  753. }
  754. expect(renderToString(h(Comp))).toBe(`123`)
  755. expect(calls).toEqual([
  756. 'mixinA beforeCreate',
  757. 'extendA beforeCreate',
  758. 'self beforeCreate',
  759. 'mixinA created',
  760. 'extendA created',
  761. 'self created'
  762. ])
  763. })
  764. test('chained extends in mixins', () => {
  765. const calls: string[] = []
  766. const extendA = {
  767. beforeCreate() {
  768. calls.push('extendA beforeCreate')
  769. },
  770. created() {
  771. calls.push('extendA created')
  772. }
  773. }
  774. const mixinA = {
  775. extends: extendA,
  776. beforeCreate() {
  777. calls.push('mixinA beforeCreate')
  778. },
  779. created() {
  780. calls.push('mixinA created')
  781. }
  782. }
  783. const Comp = {
  784. mixins: [mixinA],
  785. render: () => '123',
  786. beforeCreate() {
  787. calls.push('self beforeCreate')
  788. },
  789. created() {
  790. calls.push('self created')
  791. }
  792. }
  793. expect(renderToString(h(Comp))).toBe(`123`)
  794. expect(calls).toEqual([
  795. 'extendA beforeCreate',
  796. 'mixinA beforeCreate',
  797. 'self beforeCreate',
  798. 'extendA created',
  799. 'mixinA created',
  800. 'self created'
  801. ])
  802. })
  803. test('extends', () => {
  804. const calls: string[] = []
  805. const Base = {
  806. data() {
  807. return {
  808. a: 1,
  809. b: 1
  810. }
  811. },
  812. methods: {
  813. sayA() {}
  814. },
  815. mounted(this: any) {
  816. expect(this.a).toBe(1)
  817. expect(this.b).toBe(2)
  818. calls.push('base')
  819. }
  820. }
  821. const Comp = defineComponent({
  822. extends: defineComponent(Base),
  823. data() {
  824. return {
  825. b: 2
  826. }
  827. },
  828. mounted() {
  829. calls.push('comp')
  830. },
  831. render() {
  832. return `${this.a}${this.b}`
  833. }
  834. })
  835. expect(renderToString(h(Comp))).toBe(`12`)
  836. expect(calls).toEqual(['base', 'comp'])
  837. })
  838. test('extends with mixins', () => {
  839. const calls: string[] = []
  840. const Base = {
  841. data() {
  842. return {
  843. a: 1,
  844. x: 'base'
  845. }
  846. },
  847. methods: {
  848. sayA() {}
  849. },
  850. mounted(this: any) {
  851. expect(this.a).toBe(1)
  852. expect(this.b).toBeTruthy()
  853. expect(this.c).toBe(2)
  854. calls.push('base')
  855. }
  856. }
  857. const Mixin = {
  858. data() {
  859. return {
  860. b: true,
  861. x: 'mixin'
  862. }
  863. },
  864. mounted(this: any) {
  865. expect(this.a).toBe(1)
  866. expect(this.b).toBeTruthy()
  867. expect(this.c).toBe(2)
  868. calls.push('mixin')
  869. }
  870. }
  871. const Comp = defineComponent({
  872. extends: defineComponent(Base),
  873. mixins: [defineComponent(Mixin)],
  874. data() {
  875. return {
  876. c: 2
  877. }
  878. },
  879. mounted() {
  880. calls.push('comp')
  881. },
  882. render() {
  883. return `${this.a}${this.b}${this.c}${this.x}`
  884. }
  885. })
  886. expect(renderToString(h(Comp))).toBe(`1true2mixin`)
  887. expect(calls).toEqual(['base', 'mixin', 'comp'])
  888. })
  889. test('beforeCreate/created in extends and mixins', () => {
  890. const calls: string[] = []
  891. const BaseA = {
  892. beforeCreate() {
  893. calls.push('beforeCreateA')
  894. },
  895. created() {
  896. calls.push('createdA')
  897. }
  898. }
  899. const BaseB = {
  900. extends: BaseA,
  901. beforeCreate() {
  902. calls.push('beforeCreateB')
  903. },
  904. created() {
  905. calls.push('createdB')
  906. }
  907. }
  908. const MixinA = {
  909. beforeCreate() {
  910. calls.push('beforeCreateC')
  911. },
  912. created() {
  913. calls.push('createdC')
  914. }
  915. }
  916. const MixinB = {
  917. mixins: [MixinA],
  918. beforeCreate() {
  919. calls.push('beforeCreateD')
  920. },
  921. created() {
  922. calls.push('createdD')
  923. }
  924. }
  925. const Comp = {
  926. extends: BaseB,
  927. mixins: [MixinB],
  928. beforeCreate() {
  929. calls.push('selfBeforeCreate')
  930. },
  931. created() {
  932. calls.push('selfCreated')
  933. },
  934. render() {}
  935. }
  936. renderToString(h(Comp))
  937. expect(calls).toEqual([
  938. 'beforeCreateA',
  939. 'beforeCreateB',
  940. 'beforeCreateC',
  941. 'beforeCreateD',
  942. 'selfBeforeCreate',
  943. 'createdA',
  944. 'createdB',
  945. 'createdC',
  946. 'createdD',
  947. 'selfCreated'
  948. ])
  949. })
  950. test('flatten merged options', async () => {
  951. const MixinBase = {
  952. msg1: 'base'
  953. }
  954. const ExtendsBase = {
  955. msg2: 'base'
  956. }
  957. const Mixin = {
  958. mixins: [MixinBase]
  959. }
  960. const Extends = {
  961. extends: ExtendsBase
  962. }
  963. const Comp = defineComponent({
  964. extends: defineComponent(Extends),
  965. mixins: [defineComponent(Mixin)],
  966. render() {
  967. return `${this.$options.msg1},${this.$options.msg2}`
  968. }
  969. })
  970. expect(renderToString(h(Comp))).toBe('base,base')
  971. })
  972. test('extends template', () => {
  973. const Comp = {
  974. extends: {
  975. template: `<h1>Foo</h1>`
  976. }
  977. }
  978. const root = document.createElement('div') as any
  979. domRender(h(Comp), root)
  980. expect(root.innerHTML).toBe(`<h1>Foo</h1>`)
  981. })
  982. test('options defined in component have higher priority', async () => {
  983. const Mixin = {
  984. msg1: 'base'
  985. }
  986. const Extends = {
  987. msg2: 'base'
  988. }
  989. const Comp = defineComponent({
  990. msg1: 'local',
  991. msg2: 'local',
  992. extends: defineComponent(Extends),
  993. mixins: [defineComponent(Mixin)],
  994. render() {
  995. return `${this.$options.msg1},${this.$options.msg2}`
  996. }
  997. })
  998. expect(renderToString(h(Comp))).toBe('local,local')
  999. })
  1000. test('accessing setup() state from options', async () => {
  1001. const Comp = defineComponent({
  1002. setup() {
  1003. return {
  1004. count: ref(0)
  1005. }
  1006. },
  1007. data() {
  1008. return {
  1009. plusOne: (this as any).count + 1
  1010. }
  1011. },
  1012. computed: {
  1013. plusTwo(): number {
  1014. return this.count + 2
  1015. }
  1016. },
  1017. methods: {
  1018. inc() {
  1019. this.count++
  1020. }
  1021. },
  1022. render() {
  1023. return h(
  1024. 'div',
  1025. {
  1026. onClick: this.inc
  1027. },
  1028. `${this.count},${this.plusOne},${this.plusTwo}`
  1029. )
  1030. }
  1031. })
  1032. const root = nodeOps.createElement('div')
  1033. render(h(Comp), root)
  1034. expect(serializeInner(root)).toBe(`<div>0,1,2</div>`)
  1035. triggerEvent(root.children[0] as TestElement, 'click')
  1036. await nextTick()
  1037. expect(serializeInner(root)).toBe(`<div>1,1,3</div>`)
  1038. })
  1039. // #1016
  1040. test('watcher initialization should be deferred in mixins', async () => {
  1041. const mixin1 = {
  1042. data() {
  1043. return {
  1044. mixin1Data: 'mixin1'
  1045. }
  1046. },
  1047. methods: {}
  1048. }
  1049. const watchSpy = vi.fn()
  1050. const mixin2 = {
  1051. watch: {
  1052. mixin3Data: watchSpy
  1053. }
  1054. }
  1055. const mixin3 = {
  1056. data() {
  1057. return {
  1058. mixin3Data: 'mixin3'
  1059. }
  1060. },
  1061. methods: {}
  1062. }
  1063. let vm: any
  1064. const Comp = {
  1065. mixins: [mixin1, mixin2, mixin3],
  1066. render() {},
  1067. created() {
  1068. vm = this
  1069. }
  1070. }
  1071. const root = nodeOps.createElement('div')
  1072. render(h(Comp), root)
  1073. // should have no warnings
  1074. vm.mixin3Data = 'hello'
  1075. await nextTick()
  1076. expect(watchSpy.mock.calls[0].slice(0, 2)).toEqual(['hello', 'mixin3'])
  1077. })
  1078. test('injection from closest ancestor', () => {
  1079. const Root = defineComponent({
  1080. provide: {
  1081. a: 'root'
  1082. },
  1083. render() {
  1084. return [h(Mid), ' ', h(MidWithProvide), ' ', h(MidWithMixinProvide)]
  1085. }
  1086. })
  1087. const Mid = {
  1088. render() {
  1089. return h(Child)
  1090. }
  1091. } as any
  1092. const MidWithProvide = {
  1093. provide: {
  1094. a: 'midWithProvide'
  1095. },
  1096. render() {
  1097. return h(Child)
  1098. }
  1099. } as any
  1100. const mixin = {
  1101. provide: {
  1102. a: 'midWithMixinProvide'
  1103. }
  1104. }
  1105. const MidWithMixinProvide = {
  1106. mixins: [mixin],
  1107. render() {
  1108. return h(Child)
  1109. }
  1110. } as any
  1111. const Child = {
  1112. inject: ['a'],
  1113. render() {
  1114. return this.a
  1115. }
  1116. } as any
  1117. expect(renderToString(h(Root))).toBe(
  1118. 'root midWithProvide midWithMixinProvide'
  1119. )
  1120. })
  1121. describe('options merge strategies', () => {
  1122. test('this.$options.data', () => {
  1123. const mixin = {
  1124. data() {
  1125. return { foo: 1, bar: 2 }
  1126. }
  1127. }
  1128. createApp({
  1129. mixins: [mixin],
  1130. data() {
  1131. return {
  1132. foo: 3,
  1133. baz: 4
  1134. }
  1135. },
  1136. created() {
  1137. expect(this.$options.data).toBeInstanceOf(Function)
  1138. expect(this.$options.data()).toEqual({
  1139. foo: 3,
  1140. bar: 2,
  1141. baz: 4
  1142. })
  1143. },
  1144. render: () => null
  1145. }).mount(nodeOps.createElement('div'))
  1146. })
  1147. test('this.$options.inject', () => {
  1148. const mixin = {
  1149. inject: ['a']
  1150. }
  1151. const app = createApp({
  1152. mixins: [mixin],
  1153. inject: { b: 'b', c: { from: 'd' } },
  1154. created() {
  1155. expect(this.$options.inject.a).toEqual('a')
  1156. expect(this.$options.inject.b).toEqual('b')
  1157. expect(this.$options.inject.c).toEqual({ from: 'd' })
  1158. expect(this.a).toBe(1)
  1159. expect(this.b).toBe(2)
  1160. expect(this.c).toBe(3)
  1161. },
  1162. render: () => null
  1163. })
  1164. app.provide('a', 1)
  1165. app.provide('b', 2)
  1166. app.provide('d', 3)
  1167. app.mount(nodeOps.createElement('div'))
  1168. })
  1169. test('this.$options.provide', () => {
  1170. const mixin = {
  1171. provide: {
  1172. a: 1
  1173. }
  1174. }
  1175. createApp({
  1176. mixins: [mixin],
  1177. provide() {
  1178. return {
  1179. b: 2
  1180. }
  1181. },
  1182. created() {
  1183. expect(this.$options.provide).toBeInstanceOf(Function)
  1184. expect(this.$options.provide()).toEqual({ a: 1, b: 2 })
  1185. },
  1186. render: () => null
  1187. }).mount(nodeOps.createElement('div'))
  1188. })
  1189. test('this.$options[lifecycle-name]', () => {
  1190. const mixin = {
  1191. mounted() {},
  1192. beforeUnmount() {},
  1193. unmounted() {}
  1194. }
  1195. createApp({
  1196. mixins: [mixin],
  1197. mounted() {},
  1198. beforeUnmount() {},
  1199. unmounted() {},
  1200. created() {
  1201. expect(this.$options.mounted).toBeInstanceOf(Array)
  1202. expect(this.$options.mounted.length).toBe(2)
  1203. expect(this.$options.beforeUnmount).toBeInstanceOf(Array)
  1204. expect(this.$options.beforeUnmount.length).toBe(2)
  1205. expect(this.$options.unmounted).toBeInstanceOf(Array)
  1206. expect(this.$options.unmounted.length).toBe(2)
  1207. },
  1208. render: () => null
  1209. }).mount(nodeOps.createElement('div'))
  1210. })
  1211. test('this.$options[asset-name]', () => {
  1212. const mixin = {
  1213. components: {
  1214. a: {}
  1215. },
  1216. directives: {
  1217. d1: {}
  1218. }
  1219. }
  1220. createApp({
  1221. mixins: [mixin],
  1222. components: {
  1223. b: {}
  1224. },
  1225. directives: {
  1226. d2: {}
  1227. },
  1228. created() {
  1229. expect('a' in this.$options.components).toBe(true)
  1230. expect('b' in this.$options.components).toBe(true)
  1231. expect('d1' in this.$options.directives).toBe(true)
  1232. expect('d2' in this.$options.directives).toBe(true)
  1233. },
  1234. render: () => null
  1235. }).mount(nodeOps.createElement('div'))
  1236. })
  1237. test('this.$options.methods', () => {
  1238. const mixin = {
  1239. methods: {
  1240. fn1() {}
  1241. }
  1242. }
  1243. createApp({
  1244. mixins: [mixin],
  1245. methods: {
  1246. fn2() {}
  1247. },
  1248. created() {
  1249. expect(this.$options.methods.fn1).toBeInstanceOf(Function)
  1250. expect(this.$options.methods.fn2).toBeInstanceOf(Function)
  1251. },
  1252. render: () => null
  1253. }).mount(nodeOps.createElement('div'))
  1254. })
  1255. test('this.$options.computed', () => {
  1256. const mixin = {
  1257. computed: {
  1258. c1() {}
  1259. }
  1260. }
  1261. createApp({
  1262. mixins: [mixin],
  1263. computed: {
  1264. c2() {}
  1265. },
  1266. created() {
  1267. expect(this.$options.computed.c1).toBeInstanceOf(Function)
  1268. expect(this.$options.computed.c2).toBeInstanceOf(Function)
  1269. },
  1270. render: () => null
  1271. }).mount(nodeOps.createElement('div'))
  1272. })
  1273. // #2791
  1274. test('modify $options in the beforeCreate hook', async () => {
  1275. const count = ref(0)
  1276. const mixin = {
  1277. data() {
  1278. return { foo: 1 }
  1279. },
  1280. beforeCreate(this: any) {
  1281. if (!this.$options.computed) {
  1282. this.$options.computed = {}
  1283. }
  1284. this.$options.computed.value = () => count.value
  1285. }
  1286. }
  1287. const root = nodeOps.createElement('div')
  1288. createApp({
  1289. mixins: [mixin],
  1290. render(this: any) {
  1291. return this.value
  1292. }
  1293. }).mount(root)
  1294. expect(serializeInner(root)).toBe('0')
  1295. count.value++
  1296. await nextTick()
  1297. expect(serializeInner(root)).toBe('1')
  1298. })
  1299. })
  1300. describe('warnings', () => {
  1301. test('Expected a function as watch handler', () => {
  1302. const Comp = {
  1303. watch: {
  1304. foo: 'notExistingMethod',
  1305. foo2: {
  1306. handler: 'notExistingMethod2'
  1307. }
  1308. },
  1309. render() {}
  1310. }
  1311. const root = nodeOps.createElement('div')
  1312. render(h(Comp), root)
  1313. expect(
  1314. 'Invalid watch handler specified by key "notExistingMethod"'
  1315. ).toHaveBeenWarned()
  1316. expect(
  1317. 'Invalid watch handler specified by key "notExistingMethod2"'
  1318. ).toHaveBeenWarned()
  1319. })
  1320. test('Invalid watch option', () => {
  1321. const Comp = {
  1322. watch: { foo: true },
  1323. render() {}
  1324. }
  1325. const root = nodeOps.createElement('div')
  1326. // @ts-expect-error
  1327. render(h(Comp), root)
  1328. expect('Invalid watch option: "foo"').toHaveBeenWarned()
  1329. })
  1330. test('computed with setter and no getter', () => {
  1331. const Comp = {
  1332. computed: {
  1333. foo: {
  1334. set() {}
  1335. }
  1336. },
  1337. render() {}
  1338. }
  1339. const root = nodeOps.createElement('div')
  1340. render(h(Comp), root)
  1341. expect('Computed property "foo" has no getter.').toHaveBeenWarned()
  1342. })
  1343. test('assigning to computed with no setter', () => {
  1344. let instance: any
  1345. const Comp = {
  1346. computed: {
  1347. foo: {
  1348. get() {}
  1349. }
  1350. },
  1351. mounted() {
  1352. instance = this
  1353. },
  1354. render() {}
  1355. }
  1356. const root = nodeOps.createElement('div')
  1357. render(h(Comp), root)
  1358. instance.foo = 1
  1359. expect(
  1360. 'Write operation failed: computed property "foo" is readonly'
  1361. ).toHaveBeenWarned()
  1362. })
  1363. test('inject property is already declared in props', () => {
  1364. const Comp = {
  1365. data() {
  1366. return {
  1367. a: 1
  1368. }
  1369. },
  1370. provide() {
  1371. return {
  1372. a: this.a
  1373. }
  1374. },
  1375. render() {
  1376. return [h(ChildA)]
  1377. }
  1378. } as any
  1379. const ChildA = {
  1380. props: { a: Number },
  1381. inject: ['a'],
  1382. render() {
  1383. return this.a
  1384. }
  1385. } as any
  1386. const root = nodeOps.createElement('div')
  1387. render(h(Comp), root)
  1388. expect(
  1389. `Inject property "a" is already defined in Props.`
  1390. ).toHaveBeenWarned()
  1391. })
  1392. test('methods property is not a function', () => {
  1393. const Comp = {
  1394. methods: {
  1395. foo: 1
  1396. },
  1397. render() {}
  1398. }
  1399. const root = nodeOps.createElement('div')
  1400. render(h(Comp), root)
  1401. expect(
  1402. `Method "foo" has type "number" in the component definition. ` +
  1403. `Did you reference the function correctly?`
  1404. ).toHaveBeenWarned()
  1405. })
  1406. test('methods property is already declared in props', () => {
  1407. const Comp = {
  1408. props: {
  1409. foo: Number
  1410. },
  1411. methods: {
  1412. foo() {}
  1413. },
  1414. render() {}
  1415. }
  1416. const root = nodeOps.createElement('div')
  1417. render(h(Comp), root)
  1418. expect(
  1419. `Methods property "foo" is already defined in Props.`
  1420. ).toHaveBeenWarned()
  1421. })
  1422. test('methods property is already declared in inject', () => {
  1423. const Comp = {
  1424. data() {
  1425. return {
  1426. a: 1
  1427. }
  1428. },
  1429. provide() {
  1430. return {
  1431. a: this.a
  1432. }
  1433. },
  1434. render() {
  1435. return [h(ChildA)]
  1436. }
  1437. } as any
  1438. const ChildA = {
  1439. methods: {
  1440. a: () => null
  1441. },
  1442. inject: ['a'],
  1443. render() {
  1444. return this.a
  1445. }
  1446. } as any
  1447. const root = nodeOps.createElement('div')
  1448. render(h(Comp), root)
  1449. expect(
  1450. `Methods property "a" is already defined in Inject.`
  1451. ).toHaveBeenWarned()
  1452. })
  1453. test('data property is already declared in props', () => {
  1454. const Comp = {
  1455. props: { foo: Number },
  1456. data: () => ({
  1457. foo: 1
  1458. }),
  1459. render() {}
  1460. }
  1461. const root = nodeOps.createElement('div')
  1462. render(h(Comp), root)
  1463. expect(
  1464. `Data property "foo" is already defined in Props.`
  1465. ).toHaveBeenWarned()
  1466. })
  1467. test('data property is already declared in inject', () => {
  1468. const Comp = {
  1469. data() {
  1470. return {
  1471. a: 1
  1472. }
  1473. },
  1474. provide() {
  1475. return {
  1476. a: this.a
  1477. }
  1478. },
  1479. render() {
  1480. return [h(ChildA)]
  1481. }
  1482. } as any
  1483. const ChildA = {
  1484. data() {
  1485. return {
  1486. a: 1
  1487. }
  1488. },
  1489. inject: ['a'],
  1490. render() {
  1491. return this.a
  1492. }
  1493. } as any
  1494. const root = nodeOps.createElement('div')
  1495. render(h(Comp), root)
  1496. expect(
  1497. `Data property "a" is already defined in Inject.`
  1498. ).toHaveBeenWarned()
  1499. })
  1500. test('data property is already declared in methods', () => {
  1501. const Comp = {
  1502. data: () => ({
  1503. foo: 1
  1504. }),
  1505. methods: {
  1506. foo() {}
  1507. },
  1508. render() {}
  1509. }
  1510. const root = nodeOps.createElement('div')
  1511. render(h(Comp), root)
  1512. expect(
  1513. `Data property "foo" is already defined in Methods.`
  1514. ).toHaveBeenWarned()
  1515. })
  1516. test('computed property is already declared in props', () => {
  1517. const Comp = {
  1518. props: { foo: Number },
  1519. computed: {
  1520. foo() {}
  1521. },
  1522. render() {}
  1523. }
  1524. const root = nodeOps.createElement('div')
  1525. render(h(Comp), root)
  1526. expect(
  1527. `Computed property "foo" is already defined in Props.`
  1528. ).toHaveBeenWarned()
  1529. })
  1530. test('computed property is already declared in inject', () => {
  1531. const Comp = {
  1532. data() {
  1533. return {
  1534. a: 1
  1535. }
  1536. },
  1537. provide() {
  1538. return {
  1539. a: this.a
  1540. }
  1541. },
  1542. render() {
  1543. return [h(ChildA)]
  1544. }
  1545. } as any
  1546. const ChildA = {
  1547. computed: {
  1548. a: {
  1549. get() {},
  1550. set() {}
  1551. }
  1552. },
  1553. inject: ['a'],
  1554. render() {
  1555. return this.a
  1556. }
  1557. } as any
  1558. const root = nodeOps.createElement('div')
  1559. render(h(Comp), root)
  1560. expect(
  1561. `Computed property "a" is already defined in Inject.`
  1562. ).toHaveBeenWarned()
  1563. })
  1564. test('computed property is already declared in methods', () => {
  1565. const Comp = {
  1566. computed: {
  1567. foo() {}
  1568. },
  1569. methods: {
  1570. foo() {}
  1571. },
  1572. render() {}
  1573. }
  1574. const root = nodeOps.createElement('div')
  1575. render(h(Comp), root)
  1576. expect(
  1577. `Computed property "foo" is already defined in Methods.`
  1578. ).toHaveBeenWarned()
  1579. })
  1580. test('computed property is already declared in data', () => {
  1581. const Comp = {
  1582. data: () => ({
  1583. foo: 1
  1584. }),
  1585. computed: {
  1586. foo() {}
  1587. },
  1588. render() {}
  1589. }
  1590. const root = nodeOps.createElement('div')
  1591. render(h(Comp), root)
  1592. expect(
  1593. `Computed property "foo" is already defined in Data.`
  1594. ).toHaveBeenWarned()
  1595. })
  1596. })
  1597. })