apiOptions.spec.ts 19 KB

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