componentProps.spec.ts 22 KB

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