componentProps.spec.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. /**
  2. * @vitest-environment jsdom
  3. */
  4. import {
  5. type ComponentInternalInstance,
  6. type FunctionalComponent,
  7. type SetupContext,
  8. createApp,
  9. defineComponent,
  10. getCurrentInstance,
  11. h,
  12. inject,
  13. nodeOps,
  14. provide,
  15. ref,
  16. render,
  17. serializeInner,
  18. toRefs,
  19. watch,
  20. } from '@vue/runtime-test'
  21. import { render as domRender, nextTick } from 'vue'
  22. describe('component props', () => {
  23. test('stateful', () => {
  24. let props: any
  25. let attrs: any
  26. let proxy: any
  27. const Comp = defineComponent({
  28. props: ['fooBar', 'barBaz'],
  29. render() {
  30. props = this.$props
  31. attrs = this.$attrs
  32. proxy = this
  33. },
  34. })
  35. const root = nodeOps.createElement('div')
  36. render(h(Comp, { fooBar: 1, bar: 2 }), root)
  37. expect(proxy.fooBar).toBe(1)
  38. expect(props).toEqual({ fooBar: 1 })
  39. expect(attrs).toEqual({ bar: 2 })
  40. // test passing kebab-case and resolving to camelCase
  41. render(h(Comp, { 'foo-bar': 2, bar: 3, baz: 4 }), root)
  42. expect(proxy.fooBar).toBe(2)
  43. expect(props).toEqual({ fooBar: 2 })
  44. expect(attrs).toEqual({ bar: 3, baz: 4 })
  45. // test updating kebab-case should not delete it (#955)
  46. render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4, barBaz: 5 }), root)
  47. expect(proxy.fooBar).toBe(3)
  48. expect(proxy.barBaz).toBe(5)
  49. expect(props).toEqual({ fooBar: 3, barBaz: 5 })
  50. expect(attrs).toEqual({ bar: 3, baz: 4 })
  51. render(h(Comp, { qux: 5 }), root)
  52. expect(proxy.fooBar).toBeUndefined()
  53. // remove the props with camelCase key (#1412)
  54. expect(proxy.barBaz).toBeUndefined()
  55. expect(props).toEqual({})
  56. expect(attrs).toEqual({ qux: 5 })
  57. })
  58. test('stateful with setup', () => {
  59. let props: any
  60. let attrs: any
  61. const Comp = defineComponent({
  62. props: ['foo'],
  63. setup(_props, { attrs: _attrs }) {
  64. return () => {
  65. props = _props
  66. attrs = _attrs
  67. }
  68. },
  69. })
  70. const root = nodeOps.createElement('div')
  71. render(h(Comp, { foo: 1, bar: 2 }), root)
  72. expect(props).toEqual({ foo: 1 })
  73. expect(attrs).toEqual({ bar: 2 })
  74. render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
  75. expect(props).toEqual({ foo: 2 })
  76. expect(attrs).toEqual({ bar: 3, baz: 4 })
  77. render(h(Comp, { qux: 5 }), root)
  78. expect(props).toEqual({})
  79. expect(attrs).toEqual({ qux: 5 })
  80. })
  81. test('functional with declaration', () => {
  82. let props: any
  83. let attrs: any
  84. const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
  85. props = _props
  86. attrs = _attrs
  87. }
  88. Comp.props = ['foo']
  89. const root = nodeOps.createElement('div')
  90. render(h(Comp, { foo: 1, bar: 2 }), root)
  91. expect(props).toEqual({ foo: 1 })
  92. expect(attrs).toEqual({ bar: 2 })
  93. render(h(Comp, { foo: 2, bar: 3, baz: 4 }), root)
  94. expect(props).toEqual({ foo: 2 })
  95. expect(attrs).toEqual({ bar: 3, baz: 4 })
  96. render(h(Comp, { qux: 5 }), root)
  97. expect(props).toEqual({})
  98. expect(attrs).toEqual({ qux: 5 })
  99. })
  100. test('functional without declaration', () => {
  101. let props: any
  102. let attrs: any
  103. const Comp: FunctionalComponent = (_props, { attrs: _attrs }) => {
  104. props = _props
  105. attrs = _attrs
  106. }
  107. const root = nodeOps.createElement('div')
  108. render(h(Comp, { foo: 1 }), root)
  109. expect(props).toEqual({ foo: 1 })
  110. expect(attrs).toEqual({ foo: 1 })
  111. expect(props).toBe(attrs)
  112. render(h(Comp, { bar: 2 }), root)
  113. expect(props).toEqual({ bar: 2 })
  114. expect(attrs).toEqual({ bar: 2 })
  115. expect(props).toBe(attrs)
  116. })
  117. test('boolean casting', () => {
  118. let proxy: any
  119. const Comp = {
  120. props: {
  121. foo: Boolean,
  122. bar: Boolean,
  123. baz: Boolean,
  124. qux: Boolean,
  125. },
  126. render() {
  127. proxy = this
  128. },
  129. }
  130. render(
  131. h(Comp, {
  132. // absent should cast to false
  133. bar: '', // empty string should cast to true
  134. baz: 'baz', // same string should cast to true
  135. qux: 'ok', // other values should be left in-tact (but raise warning)
  136. }),
  137. nodeOps.createElement('div'),
  138. )
  139. expect(proxy.foo).toBe(false)
  140. expect(proxy.bar).toBe(true)
  141. expect(proxy.baz).toBe(true)
  142. expect(proxy.qux).toBe('ok')
  143. expect('type check failed for prop "qux"').toHaveBeenWarned()
  144. })
  145. test('default value', () => {
  146. let proxy: any
  147. const defaultFn = vi.fn(() => ({ a: 1 }))
  148. const defaultBaz = vi.fn(() => ({ b: 1 }))
  149. const Comp = {
  150. props: {
  151. foo: {
  152. default: 1,
  153. },
  154. bar: {
  155. default: defaultFn,
  156. },
  157. baz: {
  158. type: Function,
  159. default: defaultBaz,
  160. },
  161. },
  162. render() {
  163. proxy = this
  164. },
  165. }
  166. const root = nodeOps.createElement('div')
  167. render(h(Comp, { foo: 2 }), root)
  168. expect(proxy.foo).toBe(2)
  169. const prevBar = proxy.bar
  170. expect(proxy.bar).toEqual({ a: 1 })
  171. expect(proxy.baz).toEqual(defaultBaz)
  172. expect(defaultFn).toHaveBeenCalledTimes(1)
  173. expect(defaultBaz).toHaveBeenCalledTimes(0)
  174. // #999: updates should not cause default factory of unchanged prop to be
  175. // called again
  176. render(h(Comp, { foo: 3 }), root)
  177. expect(proxy.foo).toBe(3)
  178. expect(proxy.bar).toEqual({ a: 1 })
  179. expect(proxy.bar).toBe(prevBar)
  180. expect(defaultFn).toHaveBeenCalledTimes(1)
  181. render(h(Comp, { bar: { b: 2 } }), root)
  182. expect(proxy.foo).toBe(1)
  183. expect(proxy.bar).toEqual({ b: 2 })
  184. expect(defaultFn).toHaveBeenCalledTimes(1)
  185. render(h(Comp, { foo: 3, bar: { b: 3 } }), root)
  186. expect(proxy.foo).toBe(3)
  187. expect(proxy.bar).toEqual({ b: 3 })
  188. expect(defaultFn).toHaveBeenCalledTimes(1)
  189. render(h(Comp, { bar: { b: 4 } }), root)
  190. expect(proxy.foo).toBe(1)
  191. expect(proxy.bar).toEqual({ b: 4 })
  192. expect(defaultFn).toHaveBeenCalledTimes(1)
  193. })
  194. test('using inject in default value factory', () => {
  195. const Child = defineComponent({
  196. props: {
  197. test: {
  198. default: () => inject('test', 'default'),
  199. },
  200. },
  201. setup(props) {
  202. return () => {
  203. return h('div', props.test)
  204. }
  205. },
  206. })
  207. const Comp = {
  208. setup() {
  209. provide('test', 'injected')
  210. return () => h(Child)
  211. },
  212. }
  213. const root = nodeOps.createElement('div')
  214. render(h(Comp), root)
  215. expect(serializeInner(root)).toBe(`<div>injected</div>`)
  216. })
  217. test('optimized props updates', async () => {
  218. const Child = defineComponent({
  219. props: ['foo'],
  220. template: `<div>{{ foo }}</div>`,
  221. })
  222. const foo = ref(1)
  223. const id = ref('a')
  224. const Comp = defineComponent({
  225. setup() {
  226. return {
  227. foo,
  228. id,
  229. }
  230. },
  231. components: { Child },
  232. template: `<Child :foo="foo" :id="id"/>`,
  233. })
  234. // Note this one is using the main Vue render so it can compile template
  235. // on the fly
  236. const root = document.createElement('div')
  237. domRender(h(Comp), root)
  238. expect(root.innerHTML).toBe('<div id="a">1</div>')
  239. foo.value++
  240. await nextTick()
  241. expect(root.innerHTML).toBe('<div id="a">2</div>')
  242. id.value = 'b'
  243. await nextTick()
  244. expect(root.innerHTML).toBe('<div id="b">2</div>')
  245. })
  246. describe('validator', () => {
  247. test('validator should be called with two arguments', async () => {
  248. const mockFn = vi.fn((...args: any[]) => true)
  249. const Comp = defineComponent({
  250. props: {
  251. foo: {
  252. type: Number,
  253. validator: (value, props) => mockFn(value, props),
  254. },
  255. bar: {
  256. type: Number,
  257. },
  258. },
  259. template: `<div />`,
  260. })
  261. // Note this one is using the main Vue render so it can compile template
  262. // on the fly
  263. const root = document.createElement('div')
  264. domRender(h(Comp, { foo: 1, bar: 2 }), root)
  265. expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
  266. })
  267. test('validator should not be able to mutate other props', async () => {
  268. const mockFn = vi.fn((...args: any[]) => true)
  269. const Comp = defineComponent({
  270. props: {
  271. foo: {
  272. type: Number,
  273. validator: (value, props) => !!(props.bar = 1),
  274. },
  275. bar: {
  276. type: Number,
  277. validator: value => mockFn(value),
  278. },
  279. },
  280. template: `<div />`,
  281. })
  282. // Note this one is using the main Vue render so it can compile template
  283. // on the fly
  284. const root = document.createElement('div')
  285. domRender(h(Comp, { foo: 1, bar: 2 }), root)
  286. expect(
  287. `Set operation on key "bar" failed: target is readonly.`,
  288. ).toHaveBeenWarnedLast()
  289. expect(mockFn).toHaveBeenCalledWith(2)
  290. })
  291. })
  292. test('warn props mutation', () => {
  293. let instance: ComponentInternalInstance
  294. let setupProps: any
  295. const Comp = {
  296. props: ['foo'],
  297. setup(props: any) {
  298. instance = getCurrentInstance()!
  299. setupProps = props
  300. return () => null
  301. },
  302. }
  303. render(h(Comp, { foo: 1 }), nodeOps.createElement('div'))
  304. expect(setupProps.foo).toBe(1)
  305. expect(instance!.props.foo).toBe(1)
  306. setupProps.foo = 2
  307. expect(`Set operation on key "foo" failed`).toHaveBeenWarned()
  308. expect(() => {
  309. ;(instance!.proxy as any).foo = 2
  310. }).toThrow(TypeError)
  311. expect(`Attempting to mutate prop "foo"`).toHaveBeenWarned()
  312. // should not throw when overriding properties other than props
  313. expect(() => {
  314. ;(instance!.proxy as any).hasOwnProperty = () => {}
  315. }).not.toThrow(TypeError)
  316. })
  317. test('warn absent required props', () => {
  318. const Comp = {
  319. props: {
  320. bool: { type: Boolean, required: true },
  321. str: { type: String, required: true },
  322. num: { type: Number, required: true },
  323. },
  324. setup() {
  325. return () => null
  326. },
  327. }
  328. render(h(Comp), nodeOps.createElement('div'))
  329. expect(`Missing required prop: "bool"`).toHaveBeenWarned()
  330. expect(`Missing required prop: "str"`).toHaveBeenWarned()
  331. expect(`Missing required prop: "num"`).toHaveBeenWarned()
  332. })
  333. test('warn on type mismatch', () => {
  334. class MyClass {}
  335. const Comp = {
  336. props: {
  337. bool: { type: Boolean },
  338. str: { type: String },
  339. num: { type: Number },
  340. arr: { type: Array },
  341. obj: { type: Object },
  342. cls: { type: MyClass },
  343. fn: { type: Function },
  344. skipCheck: { type: [Boolean, Function], skipCheck: true },
  345. empty: { type: [] },
  346. },
  347. setup() {
  348. return () => null
  349. },
  350. }
  351. render(
  352. h(Comp, {
  353. bool: 'true',
  354. str: 100,
  355. num: '100',
  356. arr: {},
  357. obj: 'false',
  358. cls: {},
  359. fn: true,
  360. skipCheck: 'foo',
  361. empty: [1, 2, 3],
  362. }),
  363. nodeOps.createElement('div'),
  364. )
  365. expect(
  366. `Invalid prop: type check failed for prop "bool". Expected Boolean, got String`,
  367. ).toHaveBeenWarned()
  368. expect(
  369. `Invalid prop: type check failed for prop "str". Expected String with value "100", got Number with value 100.`,
  370. ).toHaveBeenWarned()
  371. expect(
  372. `Invalid prop: type check failed for prop "num". Expected Number with value 100, got String with value "100".`,
  373. ).toHaveBeenWarned()
  374. expect(
  375. `Invalid prop: type check failed for prop "arr". Expected Array, got Object`,
  376. ).toHaveBeenWarned()
  377. expect(
  378. `Invalid prop: type check failed for prop "obj". Expected Object, got String with value "false"`,
  379. ).toHaveBeenWarned()
  380. expect(
  381. `Invalid prop: type check failed for prop "fn". Expected Function, got Boolean with value true.`,
  382. ).toHaveBeenWarned()
  383. expect(
  384. `Invalid prop: type check failed for prop "cls". Expected MyClass, got Object`,
  385. ).toHaveBeenWarned()
  386. expect(
  387. `Invalid prop: type check failed for prop "skipCheck". Expected Boolean | Function, got String with value "foo".`,
  388. ).not.toHaveBeenWarned()
  389. expect(
  390. `Prop type [] for prop "empty" won't match anything. Did you mean to use type Array instead?`,
  391. ).toHaveBeenWarned()
  392. })
  393. // #3495
  394. test('should not warn required props using kebab-case', async () => {
  395. const Comp = {
  396. props: {
  397. fooBar: { type: String, required: true },
  398. },
  399. setup() {
  400. return () => null
  401. },
  402. }
  403. render(
  404. h(Comp, {
  405. 'foo-bar': 'hello',
  406. }),
  407. nodeOps.createElement('div'),
  408. )
  409. expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
  410. })
  411. test('merging props from mixins and extends', () => {
  412. let setupProps: any
  413. let renderProxy: any
  414. const E = {
  415. props: ['base'],
  416. }
  417. const M1 = {
  418. props: ['m1'],
  419. }
  420. const M2 = {
  421. props: { m2: null },
  422. }
  423. const Comp = {
  424. props: ['self'],
  425. mixins: [M1, M2],
  426. extends: E,
  427. setup(props: any) {
  428. setupProps = props
  429. },
  430. render(this: any) {
  431. renderProxy = this
  432. return h('div', [this.self, this.base, this.m1, this.m2])
  433. },
  434. }
  435. const root = nodeOps.createElement('div')
  436. const props = {
  437. self: 'from self, ',
  438. base: 'from base, ',
  439. m1: 'from mixin 1, ',
  440. m2: 'from mixin 2',
  441. }
  442. render(h(Comp, props), root)
  443. expect(serializeInner(root)).toMatch(
  444. `from self, from base, from mixin 1, from mixin 2`,
  445. )
  446. expect(setupProps).toMatchObject(props)
  447. expect(renderProxy.$props).toMatchObject(props)
  448. })
  449. test('merging props from global mixins', () => {
  450. let setupProps: any
  451. let renderProxy: any
  452. const M1 = {
  453. props: ['m1'],
  454. }
  455. const M2 = {
  456. props: { m2: null },
  457. }
  458. const Comp = {
  459. props: ['self'],
  460. setup(props: any) {
  461. setupProps = props
  462. },
  463. render(this: any) {
  464. renderProxy = this
  465. return h('div', [this.self, this.m1, this.m2])
  466. },
  467. }
  468. const props = {
  469. self: 'from self, ',
  470. m1: 'from mixin 1, ',
  471. m2: 'from mixin 2',
  472. }
  473. const app = createApp(Comp, props)
  474. app.mixin(M1)
  475. app.mixin(M2)
  476. const root = nodeOps.createElement('div')
  477. app.mount(root)
  478. expect(serializeInner(root)).toMatch(
  479. `from self, from mixin 1, from mixin 2`,
  480. )
  481. expect(setupProps).toMatchObject(props)
  482. expect(renderProxy.$props).toMatchObject(props)
  483. })
  484. test('props type support BigInt', () => {
  485. const Comp = {
  486. props: {
  487. foo: BigInt,
  488. },
  489. render(this: any) {
  490. return h('div', [this.foo])
  491. },
  492. }
  493. const root = nodeOps.createElement('div')
  494. render(
  495. h(Comp, {
  496. foo: BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000),
  497. }),
  498. root,
  499. )
  500. expect(serializeInner(root)).toMatch('<div>60000000100000111</div>')
  501. })
  502. // #3474
  503. test('should cache the value returned from the default factory to avoid unnecessary watcher trigger', async () => {
  504. let count = 0
  505. const Comp = {
  506. props: {
  507. foo: {
  508. type: Object,
  509. default: () => ({ val: 1 }),
  510. },
  511. bar: Number,
  512. },
  513. setup(props: any) {
  514. watch(
  515. () => props.foo,
  516. () => {
  517. count++
  518. },
  519. )
  520. return () => h('h1', [props.foo.val, props.bar])
  521. },
  522. }
  523. const foo = ref()
  524. const bar = ref(0)
  525. const app = createApp({
  526. render: () => h(Comp, { foo: foo.value, bar: bar.value }),
  527. })
  528. const root = nodeOps.createElement('div')
  529. app.mount(root)
  530. expect(serializeInner(root)).toMatch(`<h1>10</h1>`)
  531. expect(count).toBe(0)
  532. bar.value++
  533. await nextTick()
  534. expect(serializeInner(root)).toMatch(`<h1>11</h1>`)
  535. expect(count).toBe(0)
  536. })
  537. // #3288
  538. test('declared prop key should be present even if not passed', async () => {
  539. let initialKeys: string[] = []
  540. const changeSpy = vi.fn()
  541. const passFoo = ref(false)
  542. const Comp = {
  543. render() {},
  544. props: {
  545. foo: String,
  546. },
  547. setup(props: any) {
  548. initialKeys = Object.keys(props)
  549. const { foo } = toRefs(props)
  550. watch(foo, changeSpy)
  551. },
  552. }
  553. const Parent = () => (passFoo.value ? h(Comp, { foo: 'ok' }) : h(Comp))
  554. const root = nodeOps.createElement('div')
  555. createApp(Parent).mount(root)
  556. expect(initialKeys).toMatchObject(['foo'])
  557. passFoo.value = true
  558. await nextTick()
  559. expect(changeSpy).toHaveBeenCalledTimes(1)
  560. })
  561. // #3371
  562. test(`avoid double-setting props when casting`, async () => {
  563. const Parent = {
  564. setup(props: any, { slots }: SetupContext) {
  565. const childProps = ref()
  566. const registerChildProps = (props: any) => {
  567. childProps.value = props
  568. }
  569. provide('register', registerChildProps)
  570. return () => {
  571. // access the child component's props
  572. childProps.value && childProps.value.foo
  573. return slots.default!()
  574. }
  575. },
  576. }
  577. const Child = {
  578. props: {
  579. foo: {
  580. type: Boolean,
  581. required: false,
  582. },
  583. },
  584. setup(props: { foo: boolean }) {
  585. const register = inject('register') as any
  586. // 1. change the reactivity data of the parent component
  587. // 2. register its own props to the parent component
  588. register(props)
  589. return () => 'foo'
  590. },
  591. }
  592. const App = {
  593. setup() {
  594. return () => h(Parent, () => h(Child as any, { foo: '' }, () => null))
  595. },
  596. }
  597. const root = nodeOps.createElement('div')
  598. render(h(App), root)
  599. await nextTick()
  600. expect(serializeInner(root)).toBe(`foo`)
  601. })
  602. test('support null in required + multiple-type declarations', () => {
  603. const Comp = {
  604. props: {
  605. foo: { type: [Function, null], required: true },
  606. },
  607. render() {},
  608. }
  609. const root = nodeOps.createElement('div')
  610. expect(() => {
  611. render(h(Comp, { foo: () => {} }), root)
  612. }).not.toThrow()
  613. expect(() => {
  614. render(h(Comp, { foo: null }), root)
  615. }).not.toThrow()
  616. })
  617. // #5016
  618. test('handling attr with undefined value', () => {
  619. const Comp = {
  620. render(this: any) {
  621. return JSON.stringify(this.$attrs) + Object.keys(this.$attrs)
  622. },
  623. }
  624. const root = nodeOps.createElement('div')
  625. let attrs: any = { foo: undefined }
  626. render(h(Comp, attrs), root)
  627. expect(serializeInner(root)).toBe(
  628. JSON.stringify(attrs) + Object.keys(attrs),
  629. )
  630. render(h(Comp, (attrs = { foo: 'bar' })), root)
  631. expect(serializeInner(root)).toBe(
  632. JSON.stringify(attrs) + Object.keys(attrs),
  633. )
  634. })
  635. // #691ef
  636. test('should not mutate original props long-form definition object', () => {
  637. const props = {
  638. msg: {
  639. type: String,
  640. },
  641. }
  642. const Comp = defineComponent({
  643. props,
  644. render() {},
  645. })
  646. const root = nodeOps.createElement('div')
  647. render(h(Comp, { msg: 'test' }), root)
  648. expect(Object.keys(props.msg).length).toBe(1)
  649. })
  650. })