componentProps.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  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. symStr: { type: String },
  364. sym: { type: Symbol },
  365. num: { type: Number },
  366. arr: { type: Array },
  367. obj: { type: Object },
  368. cls: { type: MyClass },
  369. fn: { type: Function },
  370. skipCheck: { type: [Boolean, Function], skipCheck: true },
  371. empty: { type: [] },
  372. },
  373. setup() {
  374. return () => null
  375. },
  376. }
  377. render(
  378. h(Comp, {
  379. bool: 'true',
  380. str: 100,
  381. symStr: Symbol(),
  382. sym: 'symbol',
  383. num: '100',
  384. arr: {},
  385. obj: 'false',
  386. cls: {},
  387. fn: true,
  388. skipCheck: 'foo',
  389. empty: [1, 2, 3],
  390. }),
  391. nodeOps.createElement('div'),
  392. )
  393. expect(
  394. `Invalid prop: type check failed for prop "bool". Expected Boolean, got String`,
  395. ).toHaveBeenWarned()
  396. expect(
  397. `Invalid prop: type check failed for prop "str". Expected String with value "100", got Number with value 100.`,
  398. ).toHaveBeenWarned()
  399. expect(
  400. `Invalid prop: type check failed for prop "symStr". Expected String, got Symbol`,
  401. ).toHaveBeenWarned()
  402. expect(
  403. `Invalid prop: type check failed for prop "sym". Expected Symbol, got String with value "symbol".`,
  404. ).toHaveBeenWarned()
  405. expect(
  406. `Invalid prop: type check failed for prop "num". Expected Number with value 100, got String with value "100".`,
  407. ).toHaveBeenWarned()
  408. expect(
  409. `Invalid prop: type check failed for prop "arr". Expected Array, got Object`,
  410. ).toHaveBeenWarned()
  411. expect(
  412. `Invalid prop: type check failed for prop "obj". Expected Object, got String with value "false"`,
  413. ).toHaveBeenWarned()
  414. expect(
  415. `Invalid prop: type check failed for prop "fn". Expected Function, got Boolean with value true.`,
  416. ).toHaveBeenWarned()
  417. expect(
  418. `Invalid prop: type check failed for prop "cls". Expected MyClass, got Object`,
  419. ).toHaveBeenWarned()
  420. expect(
  421. `Invalid prop: type check failed for prop "skipCheck". Expected Boolean | Function, got String with value "foo".`,
  422. ).not.toHaveBeenWarned()
  423. expect(
  424. `Prop type [] for prop "empty" won't match anything. Did you mean to use type Array instead?`,
  425. ).toHaveBeenWarned()
  426. })
  427. // #3495
  428. test('should not warn required props using kebab-case', async () => {
  429. const Comp = {
  430. props: {
  431. fooBar: { type: String, required: true },
  432. },
  433. setup() {
  434. return () => null
  435. },
  436. }
  437. render(
  438. h(Comp, {
  439. 'foo-bar': 'hello',
  440. }),
  441. nodeOps.createElement('div'),
  442. )
  443. expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
  444. })
  445. test('merging props from mixins and extends', () => {
  446. let setupProps: any
  447. let renderProxy: any
  448. const E = {
  449. props: ['base'],
  450. }
  451. const M1 = {
  452. props: ['m1'],
  453. }
  454. const M2 = {
  455. props: { m2: null },
  456. }
  457. const Comp = {
  458. props: ['self'],
  459. mixins: [M1, M2],
  460. extends: E,
  461. setup(props: any) {
  462. setupProps = props
  463. },
  464. render(this: any) {
  465. renderProxy = this
  466. return h('div', [this.self, this.base, this.m1, this.m2])
  467. },
  468. }
  469. const root = nodeOps.createElement('div')
  470. const props = {
  471. self: 'from self, ',
  472. base: 'from base, ',
  473. m1: 'from mixin 1, ',
  474. m2: 'from mixin 2',
  475. }
  476. render(h(Comp, props), root)
  477. expect(serializeInner(root)).toMatch(
  478. `from self, from base, from mixin 1, from mixin 2`,
  479. )
  480. expect(setupProps).toMatchObject(props)
  481. expect(renderProxy.$props).toMatchObject(props)
  482. })
  483. test('merging props from global mixins', () => {
  484. let setupProps: any
  485. let renderProxy: any
  486. const M1 = {
  487. props: ['m1'],
  488. }
  489. const M2 = {
  490. props: { m2: null },
  491. }
  492. const Comp = {
  493. props: ['self'],
  494. setup(props: any) {
  495. setupProps = props
  496. },
  497. render(this: any) {
  498. renderProxy = this
  499. return h('div', [this.self, this.m1, this.m2])
  500. },
  501. }
  502. const props = {
  503. self: 'from self, ',
  504. m1: 'from mixin 1, ',
  505. m2: 'from mixin 2',
  506. }
  507. const app = createApp(Comp, props)
  508. app.mixin(M1)
  509. app.mixin(M2)
  510. const root = nodeOps.createElement('div')
  511. app.mount(root)
  512. expect(serializeInner(root)).toMatch(
  513. `from self, from mixin 1, from mixin 2`,
  514. )
  515. expect(setupProps).toMatchObject(props)
  516. expect(renderProxy.$props).toMatchObject(props)
  517. })
  518. test('merging props from global mixins and extends', () => {
  519. let renderProxy: any
  520. let extendedRenderProxy: any
  521. const defaultProp = ' from global'
  522. const props = {
  523. globalProp: {
  524. type: String,
  525. default: defaultProp,
  526. },
  527. }
  528. const globalMixin = {
  529. props,
  530. }
  531. const Comp = {
  532. render(this: any) {
  533. renderProxy = this
  534. return h('div', ['Comp', this.globalProp])
  535. },
  536. }
  537. const ExtendedComp = {
  538. extends: Comp,
  539. render(this: any) {
  540. extendedRenderProxy = this
  541. return h('div', ['ExtendedComp', this.globalProp])
  542. },
  543. }
  544. const app = createApp(
  545. {
  546. render: () => [h(ExtendedComp), h(Comp)],
  547. },
  548. {},
  549. )
  550. app.mixin(globalMixin)
  551. const root = nodeOps.createElement('div')
  552. app.mount(root)
  553. expect(serializeInner(root)).toMatch(
  554. `<div>ExtendedComp from global</div><div>Comp from global</div>`,
  555. )
  556. expect(renderProxy.$props).toMatchObject({ globalProp: defaultProp })
  557. expect(extendedRenderProxy.$props).toMatchObject({
  558. globalProp: defaultProp,
  559. })
  560. })
  561. test('merging props for a component that is also used as a mixin', () => {
  562. const CompA = {
  563. render(this: any) {
  564. return this.foo
  565. },
  566. }
  567. const mixin = {
  568. props: {
  569. foo: {
  570. default: 'from mixin',
  571. },
  572. },
  573. }
  574. const CompB = {
  575. mixins: [mixin, CompA],
  576. render(this: any) {
  577. return this.foo
  578. },
  579. }
  580. const app = createApp({
  581. render() {
  582. return [h(CompA), ', ', h(CompB)]
  583. },
  584. })
  585. app.mixin({
  586. props: {
  587. foo: {
  588. default: 'from global mixin',
  589. },
  590. },
  591. })
  592. const root = nodeOps.createElement('div')
  593. app.mount(root)
  594. expect(serializeInner(root)).toMatch(`from global mixin, from mixin`)
  595. })
  596. test('props type support BigInt', () => {
  597. const Comp = {
  598. props: {
  599. foo: BigInt,
  600. },
  601. render(this: any) {
  602. return h('div', [this.foo])
  603. },
  604. }
  605. const root = nodeOps.createElement('div')
  606. render(
  607. h(Comp, {
  608. foo: BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000),
  609. }),
  610. root,
  611. )
  612. expect(serializeInner(root)).toMatch('<div>60000000100000111</div>')
  613. })
  614. // #3474
  615. test('should cache the value returned from the default factory to avoid unnecessary watcher trigger', async () => {
  616. let count = 0
  617. const Comp = {
  618. props: {
  619. foo: {
  620. type: Object,
  621. default: () => ({ val: 1 }),
  622. },
  623. bar: Number,
  624. },
  625. setup(props: any) {
  626. watch(
  627. () => props.foo,
  628. () => {
  629. count++
  630. },
  631. )
  632. return () => h('h1', [props.foo.val, props.bar])
  633. },
  634. }
  635. const foo = ref()
  636. const bar = ref(0)
  637. const app = createApp({
  638. render: () => h(Comp, { foo: foo.value, bar: bar.value }),
  639. })
  640. const root = nodeOps.createElement('div')
  641. app.mount(root)
  642. expect(serializeInner(root)).toMatch(`<h1>10</h1>`)
  643. expect(count).toBe(0)
  644. bar.value++
  645. await nextTick()
  646. expect(serializeInner(root)).toMatch(`<h1>11</h1>`)
  647. expect(count).toBe(0)
  648. })
  649. // #3288
  650. test('declared prop key should be present even if not passed', async () => {
  651. let initialKeys: string[] = []
  652. const changeSpy = vi.fn()
  653. const passFoo = ref(false)
  654. const Comp = {
  655. render() {},
  656. props: {
  657. foo: String,
  658. },
  659. setup(props: any) {
  660. initialKeys = Object.keys(props)
  661. const { foo } = toRefs(props)
  662. watch(foo, changeSpy)
  663. },
  664. }
  665. const Parent = () => (passFoo.value ? h(Comp, { foo: 'ok' }) : h(Comp))
  666. const root = nodeOps.createElement('div')
  667. createApp(Parent).mount(root)
  668. expect(initialKeys).toMatchObject(['foo'])
  669. passFoo.value = true
  670. await nextTick()
  671. expect(changeSpy).toHaveBeenCalledTimes(1)
  672. })
  673. // #3371
  674. test(`avoid double-setting props when casting`, async () => {
  675. const Parent = {
  676. setup(props: any, { slots }: SetupContext) {
  677. const childProps = ref()
  678. const registerChildProps = (props: any) => {
  679. childProps.value = props
  680. }
  681. provide('register', registerChildProps)
  682. return () => {
  683. // access the child component's props
  684. childProps.value && childProps.value.foo
  685. return slots.default!()
  686. }
  687. },
  688. }
  689. const Child = {
  690. props: {
  691. foo: {
  692. type: Boolean,
  693. required: false,
  694. },
  695. },
  696. setup(props: { foo: boolean }) {
  697. const register = inject('register') as any
  698. // 1. change the reactivity data of the parent component
  699. // 2. register its own props to the parent component
  700. register(props)
  701. return () => 'foo'
  702. },
  703. }
  704. const App = {
  705. setup() {
  706. return () => h(Parent, () => h(Child as any, { foo: '' }, () => null))
  707. },
  708. }
  709. const root = nodeOps.createElement('div')
  710. render(h(App), root)
  711. await nextTick()
  712. expect(serializeInner(root)).toBe(`foo`)
  713. })
  714. test('support null in required + multiple-type declarations', () => {
  715. const Comp = {
  716. props: {
  717. foo: { type: [Function, null], required: true },
  718. },
  719. render() {},
  720. }
  721. const root = nodeOps.createElement('div')
  722. expect(() => {
  723. render(h(Comp, { foo: () => {} }), root)
  724. }).not.toThrow()
  725. expect(() => {
  726. render(h(Comp, { foo: null }), root)
  727. }).not.toThrow()
  728. })
  729. // #5016
  730. test('handling attr with undefined value', () => {
  731. const Comp = {
  732. render(this: any) {
  733. return JSON.stringify(this.$attrs) + Object.keys(this.$attrs)
  734. },
  735. }
  736. const root = nodeOps.createElement('div')
  737. let attrs: any = { foo: undefined }
  738. render(h(Comp, attrs), root)
  739. expect(serializeInner(root)).toBe(
  740. JSON.stringify(attrs) + Object.keys(attrs),
  741. )
  742. render(h(Comp, (attrs = { foo: 'bar' })), root)
  743. expect(serializeInner(root)).toBe(
  744. JSON.stringify(attrs) + Object.keys(attrs),
  745. )
  746. })
  747. // #6915
  748. test('should not mutate original props long-form definition object', () => {
  749. const props = {
  750. msg: {
  751. type: String,
  752. },
  753. }
  754. const Comp = defineComponent({
  755. props,
  756. render() {},
  757. })
  758. const root = nodeOps.createElement('div')
  759. render(h(Comp, { msg: 'test' }), root)
  760. expect(Object.keys(props.msg).length).toBe(1)
  761. })
  762. test('should warn against reserved prop names', () => {
  763. const Comp = defineComponent({
  764. props: {
  765. key: String,
  766. ref: String,
  767. $foo: String,
  768. },
  769. render() {},
  770. })
  771. const root = nodeOps.createElement('div')
  772. render(h(Comp, { msg: 'test' }), root)
  773. expect(`Invalid prop name: "key"`).toHaveBeenWarned()
  774. expect(`Invalid prop name: "ref"`).toHaveBeenWarned()
  775. expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
  776. })
  777. // #5517
  778. test('events should not be props when component updating', async () => {
  779. let props: any
  780. function eventHandler() {}
  781. const foo = ref(1)
  782. const Child = defineComponent({
  783. setup(_props) {
  784. props = _props
  785. },
  786. emits: ['event'],
  787. props: ['foo'],
  788. template: `<div>{{ foo }}</div>`,
  789. })
  790. const Comp = defineComponent({
  791. setup() {
  792. return {
  793. foo,
  794. eventHandler,
  795. }
  796. },
  797. components: { Child },
  798. template: `<Child @event="eventHandler" :foo="foo" />`,
  799. })
  800. const root = document.createElement('div')
  801. domRender(h(Comp), root)
  802. expect(props).not.toHaveProperty('onEvent')
  803. foo.value++
  804. await nextTick()
  805. expect(props).not.toHaveProperty('onEvent')
  806. })
  807. })