apiOptions.spec.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. import {
  2. h,
  3. nodeOps,
  4. render,
  5. serializeInner,
  6. triggerEvent,
  7. TestElement,
  8. nextTick,
  9. renderToString,
  10. ref,
  11. defineComponent,
  12. createApp
  13. } from '@vue/runtime-test'
  14. import { mockWarn } from '@vue/shared'
  15. describe('api: options', () => {
  16. test('data', async () => {
  17. const Comp = defineComponent({
  18. data() {
  19. return {
  20. foo: 1
  21. }
  22. },
  23. render() {
  24. return h(
  25. 'div',
  26. {
  27. onClick: () => {
  28. this.foo++
  29. }
  30. },
  31. this.foo
  32. )
  33. }
  34. })
  35. const root = nodeOps.createElement('div')
  36. render(h(Comp), root)
  37. expect(serializeInner(root)).toBe(`<div>1</div>`)
  38. triggerEvent(root.children[0] as TestElement, 'click')
  39. await nextTick()
  40. expect(serializeInner(root)).toBe(`<div>2</div>`)
  41. })
  42. test('computed', async () => {
  43. const Comp = defineComponent({
  44. data() {
  45. return {
  46. foo: 1
  47. }
  48. },
  49. computed: {
  50. bar(): number {
  51. return this.foo + 1
  52. },
  53. baz: (vm): number => vm.bar + 1
  54. },
  55. render() {
  56. return h(
  57. 'div',
  58. {
  59. onClick: () => {
  60. this.foo++
  61. }
  62. },
  63. this.bar + this.baz
  64. )
  65. }
  66. })
  67. const root = nodeOps.createElement('div')
  68. render(h(Comp), root)
  69. expect(serializeInner(root)).toBe(`<div>5</div>`)
  70. triggerEvent(root.children[0] as TestElement, 'click')
  71. await nextTick()
  72. expect(serializeInner(root)).toBe(`<div>7</div>`)
  73. })
  74. test('methods', async () => {
  75. const Comp = defineComponent({
  76. data() {
  77. return {
  78. foo: 1
  79. }
  80. },
  81. methods: {
  82. inc() {
  83. this.foo++
  84. }
  85. },
  86. render() {
  87. return h(
  88. 'div',
  89. {
  90. onClick: this.inc
  91. },
  92. this.foo
  93. )
  94. }
  95. })
  96. const root = nodeOps.createElement('div')
  97. render(h(Comp), root)
  98. expect(serializeInner(root)).toBe(`<div>1</div>`)
  99. triggerEvent(root.children[0] as TestElement, 'click')
  100. await nextTick()
  101. expect(serializeInner(root)).toBe(`<div>2</div>`)
  102. })
  103. test('watch', async () => {
  104. function returnThis(this: any) {
  105. return this
  106. }
  107. const spyA = jest.fn(returnThis)
  108. const spyB = jest.fn(returnThis)
  109. const spyC = jest.fn(returnThis)
  110. let ctx: any
  111. const Comp = {
  112. data() {
  113. return {
  114. foo: 1,
  115. bar: 2,
  116. baz: {
  117. qux: 3
  118. }
  119. }
  120. },
  121. watch: {
  122. // string method name
  123. foo: 'onFooChange',
  124. // direct function
  125. bar: spyB,
  126. baz: {
  127. handler: spyC,
  128. deep: true
  129. }
  130. },
  131. methods: {
  132. onFooChange: spyA
  133. },
  134. render() {
  135. ctx = this
  136. }
  137. }
  138. const root = nodeOps.createElement('div')
  139. render(h(Comp), root)
  140. function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
  141. expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
  142. expect(spy).toHaveReturnedWith(ctx)
  143. }
  144. ctx.foo++
  145. await nextTick()
  146. expect(spyA).toHaveBeenCalledTimes(1)
  147. assertCall(spyA, 0, [2, 1])
  148. ctx.bar++
  149. await nextTick()
  150. expect(spyB).toHaveBeenCalledTimes(1)
  151. assertCall(spyB, 0, [3, 2])
  152. ctx.baz.qux++
  153. await nextTick()
  154. expect(spyC).toHaveBeenCalledTimes(1)
  155. // new and old objects have same identity
  156. assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
  157. })
  158. test('watch array', async () => {
  159. function returnThis(this: any) {
  160. return this
  161. }
  162. const spyA = jest.fn(returnThis)
  163. const spyB = jest.fn(returnThis)
  164. const spyC = jest.fn(returnThis)
  165. let ctx: any
  166. const Comp = {
  167. data() {
  168. return {
  169. foo: 1,
  170. bar: 2,
  171. baz: {
  172. qux: 3
  173. }
  174. }
  175. },
  176. watch: {
  177. // string method name
  178. foo: ['onFooChange'],
  179. // direct function
  180. bar: [spyB],
  181. baz: [
  182. {
  183. handler: spyC,
  184. deep: true
  185. }
  186. ]
  187. },
  188. methods: {
  189. onFooChange: spyA
  190. },
  191. render() {
  192. ctx = this
  193. }
  194. }
  195. const root = nodeOps.createElement('div')
  196. render(h(Comp), root)
  197. function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
  198. expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
  199. expect(spy).toHaveReturnedWith(ctx)
  200. }
  201. ctx.foo++
  202. await nextTick()
  203. expect(spyA).toHaveBeenCalledTimes(1)
  204. assertCall(spyA, 0, [2, 1])
  205. ctx.bar++
  206. await nextTick()
  207. expect(spyB).toHaveBeenCalledTimes(1)
  208. assertCall(spyB, 0, [3, 2])
  209. ctx.baz.qux++
  210. await nextTick()
  211. expect(spyC).toHaveBeenCalledTimes(1)
  212. // new and old objects have same identity
  213. assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
  214. })
  215. test('provide/inject', () => {
  216. const Root = {
  217. data() {
  218. return {
  219. a: 1
  220. }
  221. },
  222. provide() {
  223. return {
  224. a: this.a
  225. }
  226. },
  227. render() {
  228. return [h(ChildA), h(ChildB), h(ChildC), h(ChildD)]
  229. }
  230. } as any
  231. const ChildA = {
  232. inject: ['a'],
  233. render() {
  234. return this.a
  235. }
  236. } as any
  237. const ChildB = {
  238. // object alias
  239. inject: { b: 'a' },
  240. render() {
  241. return this.b
  242. }
  243. } as any
  244. const ChildC = {
  245. inject: {
  246. b: {
  247. from: 'a'
  248. }
  249. },
  250. render() {
  251. return this.b
  252. }
  253. } as any
  254. const ChildD = {
  255. inject: {
  256. b: {
  257. from: 'c',
  258. default: 2
  259. }
  260. },
  261. render() {
  262. return this.b
  263. }
  264. } as any
  265. expect(renderToString(h(Root))).toBe(`1112`)
  266. })
  267. test('lifecycle', async () => {
  268. const count = ref(0)
  269. const root = nodeOps.createElement('div')
  270. const calls: string[] = []
  271. const Root = {
  272. beforeCreate() {
  273. calls.push('root beforeCreate')
  274. },
  275. created() {
  276. calls.push('root created')
  277. },
  278. beforeMount() {
  279. calls.push('root onBeforeMount')
  280. },
  281. mounted() {
  282. calls.push('root onMounted')
  283. },
  284. beforeUpdate() {
  285. calls.push('root onBeforeUpdate')
  286. },
  287. updated() {
  288. calls.push('root onUpdated')
  289. },
  290. beforeUnmount() {
  291. calls.push('root onBeforeUnmount')
  292. },
  293. unmounted() {
  294. calls.push('root onUnmounted')
  295. },
  296. render() {
  297. return h(Mid, { count: count.value })
  298. }
  299. }
  300. const Mid = {
  301. beforeCreate() {
  302. calls.push('mid beforeCreate')
  303. },
  304. created() {
  305. calls.push('mid created')
  306. },
  307. beforeMount() {
  308. calls.push('mid onBeforeMount')
  309. },
  310. mounted() {
  311. calls.push('mid onMounted')
  312. },
  313. beforeUpdate() {
  314. calls.push('mid onBeforeUpdate')
  315. },
  316. updated() {
  317. calls.push('mid onUpdated')
  318. },
  319. beforeUnmount() {
  320. calls.push('mid onBeforeUnmount')
  321. },
  322. unmounted() {
  323. calls.push('mid onUnmounted')
  324. },
  325. render(this: any) {
  326. return h(Child, { count: this.$props.count })
  327. }
  328. }
  329. const Child = {
  330. beforeCreate() {
  331. calls.push('child beforeCreate')
  332. },
  333. created() {
  334. calls.push('child created')
  335. },
  336. beforeMount() {
  337. calls.push('child onBeforeMount')
  338. },
  339. mounted() {
  340. calls.push('child onMounted')
  341. },
  342. beforeUpdate() {
  343. calls.push('child onBeforeUpdate')
  344. },
  345. updated() {
  346. calls.push('child onUpdated')
  347. },
  348. beforeUnmount() {
  349. calls.push('child onBeforeUnmount')
  350. },
  351. unmounted() {
  352. calls.push('child onUnmounted')
  353. },
  354. render(this: any) {
  355. return h('div', this.$props.count)
  356. }
  357. }
  358. // mount
  359. render(h(Root), root)
  360. expect(calls).toEqual([
  361. 'root beforeCreate',
  362. 'root created',
  363. 'root onBeforeMount',
  364. 'mid beforeCreate',
  365. 'mid created',
  366. 'mid onBeforeMount',
  367. 'child beforeCreate',
  368. 'child created',
  369. 'child onBeforeMount',
  370. 'child onMounted',
  371. 'mid onMounted',
  372. 'root onMounted'
  373. ])
  374. calls.length = 0
  375. // update
  376. count.value++
  377. await nextTick()
  378. expect(calls).toEqual([
  379. 'root onBeforeUpdate',
  380. 'mid onBeforeUpdate',
  381. 'child onBeforeUpdate',
  382. 'child onUpdated',
  383. 'mid onUpdated',
  384. 'root onUpdated'
  385. ])
  386. calls.length = 0
  387. // unmount
  388. render(null, root)
  389. expect(calls).toEqual([
  390. 'root onBeforeUnmount',
  391. 'mid onBeforeUnmount',
  392. 'child onBeforeUnmount',
  393. 'child onUnmounted',
  394. 'mid onUnmounted',
  395. 'root onUnmounted'
  396. ])
  397. })
  398. test('mixins', () => {
  399. const calls: string[] = []
  400. const mixinA = {
  401. data() {
  402. return {
  403. a: 1
  404. }
  405. },
  406. created(this: any) {
  407. calls.push('mixinA created')
  408. expect(this.a).toBe(1)
  409. expect(this.b).toBe(2)
  410. expect(this.c).toBe(3)
  411. },
  412. mounted() {
  413. calls.push('mixinA mounted')
  414. }
  415. }
  416. const mixinB = {
  417. data() {
  418. return {
  419. b: 2
  420. }
  421. },
  422. created(this: any) {
  423. calls.push('mixinB created')
  424. expect(this.a).toBe(1)
  425. expect(this.b).toBe(2)
  426. expect(this.c).toBe(3)
  427. },
  428. mounted() {
  429. calls.push('mixinB mounted')
  430. }
  431. }
  432. const Comp = {
  433. mixins: [mixinA, mixinB],
  434. data() {
  435. return {
  436. c: 3
  437. }
  438. },
  439. created(this: any) {
  440. calls.push('comp created')
  441. expect(this.a).toBe(1)
  442. expect(this.b).toBe(2)
  443. expect(this.c).toBe(3)
  444. },
  445. mounted() {
  446. calls.push('comp mounted')
  447. },
  448. render(this: any) {
  449. return `${this.a}${this.b}${this.c}`
  450. }
  451. }
  452. expect(renderToString(h(Comp))).toBe(`123`)
  453. expect(calls).toEqual([
  454. 'mixinA created',
  455. 'mixinB created',
  456. 'comp created',
  457. 'mixinA mounted',
  458. 'mixinB mounted',
  459. 'comp mounted'
  460. ])
  461. })
  462. test('extends', () => {
  463. const calls: string[] = []
  464. const Base = {
  465. data() {
  466. return {
  467. a: 1
  468. }
  469. },
  470. mounted() {
  471. calls.push('base')
  472. }
  473. }
  474. const Comp = {
  475. extends: Base,
  476. data() {
  477. return {
  478. b: 2
  479. }
  480. },
  481. mounted() {
  482. calls.push('comp')
  483. },
  484. render(this: any) {
  485. return `${this.a}${this.b}`
  486. }
  487. }
  488. expect(renderToString(h(Comp))).toBe(`12`)
  489. expect(calls).toEqual(['base', 'comp'])
  490. })
  491. test('accessing setup() state from options', async () => {
  492. const Comp = defineComponent({
  493. setup() {
  494. return {
  495. count: ref(0)
  496. }
  497. },
  498. data() {
  499. return {
  500. plusOne: (this as any).count + 1
  501. }
  502. },
  503. computed: {
  504. plusTwo(): number {
  505. return this.count + 2
  506. }
  507. },
  508. methods: {
  509. inc() {
  510. this.count++
  511. }
  512. },
  513. render() {
  514. return h(
  515. 'div',
  516. {
  517. onClick: this.inc
  518. },
  519. `${this.count},${this.plusOne},${this.plusTwo}`
  520. )
  521. }
  522. })
  523. const root = nodeOps.createElement('div')
  524. render(h(Comp), root)
  525. expect(serializeInner(root)).toBe(`<div>0,1,2</div>`)
  526. triggerEvent(root.children[0] as TestElement, 'click')
  527. await nextTick()
  528. expect(serializeInner(root)).toBe(`<div>1,1,3</div>`)
  529. })
  530. test('optionMergeStrategies', () => {
  531. let merged: string
  532. const App = defineComponent({
  533. render() {},
  534. mixins: [{ foo: 'mixin' }],
  535. extends: { foo: 'extends' },
  536. foo: 'local',
  537. mounted() {
  538. merged = this.$options.foo
  539. }
  540. })
  541. const app = createApp(App)
  542. app.mixin({
  543. foo: 'global'
  544. })
  545. app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
  546. app.mount(nodeOps.createElement('div'))
  547. expect(merged!).toBe('global,extends,mixin,local')
  548. })
  549. describe('warnings', () => {
  550. mockWarn()
  551. test('Expected a function as watch handler', () => {
  552. const Comp = {
  553. watch: {
  554. foo: 'notExistingMethod'
  555. },
  556. render() {}
  557. }
  558. const root = nodeOps.createElement('div')
  559. render(h(Comp), root)
  560. expect(
  561. 'Invalid watch handler specified by key "notExistingMethod"'
  562. ).toHaveBeenWarned()
  563. })
  564. test('Invalid watch option', () => {
  565. const Comp = {
  566. watch: { foo: true },
  567. render() {}
  568. }
  569. const root = nodeOps.createElement('div')
  570. // @ts-ignore
  571. render(h(Comp), root)
  572. expect('Invalid watch option: "foo"').toHaveBeenWarned()
  573. })
  574. test('computed with setter and no getter', () => {
  575. const Comp = {
  576. computed: {
  577. foo: {
  578. set() {}
  579. }
  580. },
  581. render() {}
  582. }
  583. const root = nodeOps.createElement('div')
  584. render(h(Comp), root)
  585. expect('Computed property "foo" has no getter.').toHaveBeenWarned()
  586. })
  587. test('assigning to computed with no setter', () => {
  588. let instance: any
  589. const Comp = {
  590. computed: {
  591. foo: {
  592. get() {}
  593. }
  594. },
  595. mounted() {
  596. instance = this
  597. },
  598. render() {}
  599. }
  600. const root = nodeOps.createElement('div')
  601. render(h(Comp), root)
  602. instance.foo = 1
  603. expect(
  604. 'Computed property "foo" was assigned to but it has no setter.'
  605. ).toHaveBeenWarned()
  606. })
  607. test('data property is already declared in props', () => {
  608. const Comp = {
  609. props: { foo: Number },
  610. data: () => ({
  611. foo: 1
  612. }),
  613. render() {}
  614. }
  615. const root = nodeOps.createElement('div')
  616. render(h(Comp), root)
  617. expect(
  618. `Data property "foo" is already defined in Props.`
  619. ).toHaveBeenWarned()
  620. })
  621. test('computed property is already declared in data', () => {
  622. const Comp = {
  623. data: () => ({
  624. foo: 1
  625. }),
  626. computed: {
  627. foo() {}
  628. },
  629. render() {}
  630. }
  631. const root = nodeOps.createElement('div')
  632. render(h(Comp), root)
  633. expect(
  634. `Computed property "foo" is already defined in Data.`
  635. ).toHaveBeenWarned()
  636. })
  637. test('computed property is already declared in props', () => {
  638. const Comp = {
  639. props: { foo: Number },
  640. computed: {
  641. foo() {}
  642. },
  643. render() {}
  644. }
  645. const root = nodeOps.createElement('div')
  646. render(h(Comp), root)
  647. expect(
  648. `Computed property "foo" is already defined in Props.`
  649. ).toHaveBeenWarned()
  650. })
  651. test('methods property is not a function', () => {
  652. const Comp = {
  653. methods: {
  654. foo: 1
  655. },
  656. render() {}
  657. }
  658. const root = nodeOps.createElement('div')
  659. render(h(Comp), root)
  660. expect(
  661. `Method "foo" has type "number" in the component definition. ` +
  662. `Did you reference the function correctly?`
  663. ).toHaveBeenWarned()
  664. })
  665. test('methods property is already declared in data', () => {
  666. const Comp = {
  667. data: () => ({
  668. foo: 2
  669. }),
  670. methods: {
  671. foo() {}
  672. },
  673. render() {}
  674. }
  675. const root = nodeOps.createElement('div')
  676. render(h(Comp), root)
  677. expect(
  678. `Methods property "foo" is already defined in Data.`
  679. ).toHaveBeenWarned()
  680. })
  681. test('methods property is already declared in props', () => {
  682. const Comp = {
  683. props: {
  684. foo: Number
  685. },
  686. methods: {
  687. foo() {}
  688. },
  689. render() {}
  690. }
  691. const root = nodeOps.createElement('div')
  692. render(h(Comp), root)
  693. expect(
  694. `Methods property "foo" is already defined in Props.`
  695. ).toHaveBeenWarned()
  696. })
  697. test('methods property is already declared in computed', () => {
  698. const Comp = {
  699. computed: {
  700. foo: {
  701. get() {},
  702. set() {}
  703. }
  704. },
  705. methods: {
  706. foo() {}
  707. },
  708. render() {}
  709. }
  710. const root = nodeOps.createElement('div')
  711. render(h(Comp), root)
  712. expect(
  713. `Methods property "foo" is already defined in Computed.`
  714. ).toHaveBeenWarned()
  715. })
  716. test('inject property is already declared in data', () => {
  717. const Comp = {
  718. data() {
  719. return {
  720. a: 1
  721. }
  722. },
  723. provide() {
  724. return {
  725. a: this.a
  726. }
  727. },
  728. render() {
  729. return [h(ChildA)]
  730. }
  731. } as any
  732. const ChildA = {
  733. data() {
  734. return {
  735. a: 1
  736. }
  737. },
  738. inject: ['a'],
  739. render() {
  740. return this.a
  741. }
  742. } as any
  743. const root = nodeOps.createElement('div')
  744. render(h(Comp), root)
  745. expect(
  746. `Inject property "a" is already defined in Data.`
  747. ).toHaveBeenWarned()
  748. })
  749. test('inject property is already declared in props', () => {
  750. const Comp = {
  751. data() {
  752. return {
  753. a: 1
  754. }
  755. },
  756. provide() {
  757. return {
  758. a: this.a
  759. }
  760. },
  761. render() {
  762. return [h(ChildA)]
  763. }
  764. } as any
  765. const ChildA = {
  766. props: { a: Number },
  767. inject: ['a'],
  768. render() {
  769. return this.a
  770. }
  771. } as any
  772. const root = nodeOps.createElement('div')
  773. render(h(Comp), root)
  774. expect(
  775. `Inject property "a" is already defined in Props.`
  776. ).toHaveBeenWarned()
  777. })
  778. test('inject property is already declared in computed', () => {
  779. const Comp = {
  780. data() {
  781. return {
  782. a: 1
  783. }
  784. },
  785. provide() {
  786. return {
  787. a: this.a
  788. }
  789. },
  790. render() {
  791. return [h(ChildA)]
  792. }
  793. } as any
  794. const ChildA = {
  795. computed: {
  796. a: {
  797. get() {},
  798. set() {}
  799. }
  800. },
  801. inject: ['a'],
  802. render() {
  803. return this.a
  804. }
  805. } as any
  806. const root = nodeOps.createElement('div')
  807. render(h(Comp), root)
  808. expect(
  809. `Inject property "a" is already defined in Computed.`
  810. ).toHaveBeenWarned()
  811. })
  812. test('inject property is already declared in methods', () => {
  813. const Comp = {
  814. data() {
  815. return {
  816. a: 1
  817. }
  818. },
  819. provide() {
  820. return {
  821. a: this.a
  822. }
  823. },
  824. render() {
  825. return [h(ChildA)]
  826. }
  827. } as any
  828. const ChildA = {
  829. methods: {
  830. a: () => null
  831. },
  832. inject: ['a'],
  833. render() {
  834. return this.a
  835. }
  836. } as any
  837. const root = nodeOps.createElement('div')
  838. render(h(Comp), root)
  839. expect(
  840. `Inject property "a" is already defined in Methods.`
  841. ).toHaveBeenWarned()
  842. })
  843. })
  844. })