props.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import Vue from 'vue'
  2. import { hasSymbol } from 'core/util/env'
  3. import testObjectOption from '../../../helpers/test-object-option'
  4. import { ref } from 'v3'
  5. describe('Options props', () => {
  6. testObjectOption('props')
  7. it('array syntax', done => {
  8. const vm = new Vue({
  9. data: {
  10. b: 'bar'
  11. },
  12. template: '<test v-bind:b="b" ref="child"></test>',
  13. components: {
  14. test: {
  15. props: ['b'],
  16. template: '<div>{{b}}</div>'
  17. }
  18. }
  19. }).$mount()
  20. expect(vm.$el.innerHTML).toBe('bar')
  21. vm.b = 'baz'
  22. waitForUpdate(() => {
  23. expect(vm.$el.innerHTML).toBe('baz')
  24. vm.$refs.child.b = 'qux'
  25. })
  26. .then(() => {
  27. expect(vm.$el.innerHTML).toBe('qux')
  28. expect('Avoid mutating a prop directly').toHaveBeenWarned()
  29. })
  30. .then(done)
  31. })
  32. it('object syntax', done => {
  33. const vm = new Vue({
  34. data: {
  35. b: 'bar'
  36. },
  37. template: '<test v-bind:b="b" ref="child"></test>',
  38. components: {
  39. test: {
  40. props: { b: String },
  41. template: '<div>{{b}}</div>'
  42. }
  43. }
  44. }).$mount()
  45. expect(vm.$el.innerHTML).toBe('bar')
  46. vm.b = 'baz'
  47. waitForUpdate(() => {
  48. expect(vm.$el.innerHTML).toBe('baz')
  49. vm.$refs.child.b = 'qux'
  50. })
  51. .then(() => {
  52. expect(vm.$el.innerHTML).toBe('qux')
  53. expect('Avoid mutating a prop directly').toHaveBeenWarned()
  54. })
  55. .then(done)
  56. })
  57. it('warn mixed syntax', () => {
  58. new Vue({
  59. props: [{ b: String }]
  60. })
  61. expect('props must be strings when using array syntax').toHaveBeenWarned()
  62. })
  63. it('default values', () => {
  64. const vm = new Vue({
  65. data: {
  66. b: undefined
  67. },
  68. template: '<test :b="b"></test>',
  69. components: {
  70. test: {
  71. props: {
  72. a: {
  73. default: 'A' // absent
  74. },
  75. b: {
  76. default: 'B' // undefined
  77. }
  78. },
  79. template: '<div>{{a}}{{b}}</div>'
  80. }
  81. }
  82. }).$mount()
  83. expect(vm.$el.textContent).toBe('AB')
  84. })
  85. it('default value reactivity', done => {
  86. const vm = new Vue({
  87. props: {
  88. a: {
  89. default: () => ({ b: 1 })
  90. }
  91. },
  92. propsData: {
  93. a: undefined
  94. },
  95. template: '<div>{{ a.b }}</div>'
  96. }).$mount()
  97. expect(vm.$el.textContent).toBe('1')
  98. vm.a.b = 2
  99. waitForUpdate(() => {
  100. expect(vm.$el.textContent).toBe('2')
  101. }).then(done)
  102. })
  103. it('default value Function', () => {
  104. const func = () => 132
  105. const vm = new Vue({
  106. props: {
  107. a: {
  108. type: Function,
  109. default: func
  110. }
  111. },
  112. propsData: {
  113. a: undefined
  114. }
  115. })
  116. expect(vm.a).toBe(func)
  117. })
  118. it('warn object/array default values', () => {
  119. new Vue({
  120. props: {
  121. a: {
  122. default: { b: 1 }
  123. }
  124. },
  125. propsData: {
  126. a: undefined
  127. }
  128. })
  129. expect(
  130. 'Props with type Object/Array must use a factory function'
  131. ).toHaveBeenWarned()
  132. })
  133. it('warn missing required', () => {
  134. new Vue({
  135. template: '<test></test>',
  136. components: {
  137. test: {
  138. props: { a: { required: true } },
  139. template: '<div>{{a}}</div>'
  140. }
  141. }
  142. }).$mount()
  143. expect('Missing required prop: "a"').toHaveBeenWarned()
  144. })
  145. describe('assertions', () => {
  146. function makeInstance(value, type, validator?, required?) {
  147. return new Vue({
  148. template: '<test :test="val"></test>',
  149. data: {
  150. val: value
  151. },
  152. components: {
  153. test: {
  154. template: '<div></div>',
  155. props: {
  156. test: {
  157. type,
  158. validator,
  159. required
  160. }
  161. }
  162. }
  163. }
  164. }).$mount()
  165. }
  166. it('string', () => {
  167. makeInstance('hello', String)
  168. expect((console.error as any).mock.calls.length).toBe(0)
  169. makeInstance(123, String)
  170. expect(
  171. 'Expected String with value "123", got Number with value 123'
  172. ).toHaveBeenWarned()
  173. })
  174. it('number', () => {
  175. makeInstance(123, Number)
  176. expect((console.error as any).mock.calls.length).toBe(0)
  177. makeInstance('123', Number)
  178. expect(
  179. 'Expected Number with value 123, got String with value "123"'
  180. ).toHaveBeenWarned()
  181. })
  182. it('number & boolean', () => {
  183. makeInstance(123, Number)
  184. expect((console.error as any).mock.calls.length).toBe(0)
  185. makeInstance(false, Number)
  186. expect('Expected Number, got Boolean with value false').toHaveBeenWarned()
  187. })
  188. it('string & boolean', () => {
  189. makeInstance('hello', String)
  190. expect((console.error as any).mock.calls.length).toBe(0)
  191. makeInstance(true, String)
  192. expect('Expected String, got Boolean with value true').toHaveBeenWarned()
  193. })
  194. it('boolean', () => {
  195. makeInstance(true, Boolean)
  196. expect((console.error as any).mock.calls.length).toBe(0)
  197. makeInstance('123', Boolean)
  198. expect('Expected Boolean, got String with value "123"').toHaveBeenWarned()
  199. })
  200. it('function', () => {
  201. makeInstance(() => {}, Function)
  202. expect((console.error as any).mock.calls.length).toBe(0)
  203. makeInstance(123, Function)
  204. expect('Expected Function, got Number with value 123').toHaveBeenWarned()
  205. })
  206. it('object', () => {
  207. makeInstance({}, Object)
  208. expect((console.error as any).mock.calls.length).toBe(0)
  209. makeInstance([], Object)
  210. expect('Expected Object, got Array').toHaveBeenWarned()
  211. })
  212. it('array', () => {
  213. makeInstance([], Array)
  214. expect((console.error as any).mock.calls.length).toBe(0)
  215. makeInstance({}, Array)
  216. expect('Expected Array, got Object').toHaveBeenWarned()
  217. })
  218. it('primitive wrapper objects', () => {
  219. /* eslint-disable no-new-wrappers */
  220. makeInstance(new String('s'), String)
  221. expect((console.error as any).mock.calls.length).toBe(0)
  222. makeInstance(new Number(1), Number)
  223. expect((console.error as any).mock.calls.length).toBe(0)
  224. makeInstance(new Boolean(true), Boolean)
  225. expect((console.error as any).mock.calls.length).toBe(0)
  226. /* eslint-enable no-new-wrappers */
  227. })
  228. if (hasSymbol) {
  229. it('symbol', () => {
  230. makeInstance(Symbol('foo'), Symbol)
  231. expect((console.error as any).mock.calls.length).toBe(0)
  232. makeInstance({}, Symbol)
  233. expect('Expected Symbol, got Object').toHaveBeenWarned()
  234. })
  235. it('warns when expected an explicable type but Symbol was provided', () => {
  236. makeInstance(Symbol('foo'), String)
  237. expect('Expected String, got Symbol').toHaveBeenWarned()
  238. })
  239. it('warns when expected an explicable type but Symbol was provided', () => {
  240. makeInstance(Symbol('foo'), [String, Number])
  241. expect('Expected String, Number, got Symbol').toHaveBeenWarned()
  242. })
  243. }
  244. if (typeof BigInt !== 'undefined') {
  245. /* global BigInt */
  246. it('bigint', () => {
  247. makeInstance(BigInt(100), BigInt)
  248. expect((console.error as any).mock.calls.length).toBe(0)
  249. makeInstance({}, BigInt)
  250. expect('Expected BigInt, got Object').toHaveBeenWarned()
  251. })
  252. }
  253. it('custom constructor', () => {
  254. function Class() {}
  255. makeInstance(new Class(), Class)
  256. expect((console.error as any).mock.calls.length).toBe(0)
  257. makeInstance({}, Class)
  258. expect('type check failed').toHaveBeenWarned()
  259. })
  260. it('multiple types', () => {
  261. makeInstance([], [Array, Number, Boolean])
  262. expect((console.error as any).mock.calls.length).toBe(0)
  263. makeInstance({}, [Array, Number, Boolean])
  264. expect('Expected Array, Number, Boolean, got Object').toHaveBeenWarned()
  265. })
  266. it('custom validator', () => {
  267. makeInstance(123, null, v => v === 123)
  268. expect((console.error as any).mock.calls.length).toBe(0)
  269. makeInstance(123, null, v => v === 234)
  270. expect('custom validator check failed').toHaveBeenWarned()
  271. })
  272. it('type check + custom validator', () => {
  273. makeInstance(123, Number, v => v === 123)
  274. expect((console.error as any).mock.calls.length).toBe(0)
  275. makeInstance(123, Number, v => v === 234)
  276. expect('custom validator check failed').toHaveBeenWarned()
  277. makeInstance(123, String, v => v === 123)
  278. expect(
  279. 'Expected String with value "123", got Number with value 123'
  280. ).toHaveBeenWarned()
  281. })
  282. it('multiple types + custom validator', () => {
  283. makeInstance(123, [Number, String, Boolean], v => v === 123)
  284. expect((console.error as any).mock.calls.length).toBe(0)
  285. makeInstance(123, [Number, String, Boolean], v => v === 234)
  286. expect('custom validator check failed').toHaveBeenWarned()
  287. makeInstance(123, [String, Boolean], v => v === 123)
  288. expect('Expected String, Boolean').toHaveBeenWarned()
  289. })
  290. it('optional with type + null/undefined', () => {
  291. makeInstance(undefined, String)
  292. expect((console.error as any).mock.calls.length).toBe(0)
  293. makeInstance(null, String)
  294. expect((console.error as any).mock.calls.length).toBe(0)
  295. })
  296. it('required with type + null/undefined', () => {
  297. makeInstance(undefined, String, null, true)
  298. expect((console.error as any).mock.calls.length).toBe(1)
  299. expect('Expected String').toHaveBeenWarned()
  300. makeInstance(null, Boolean, null, true)
  301. expect((console.error as any).mock.calls.length).toBe(2)
  302. expect('Expected Boolean').toHaveBeenWarned()
  303. })
  304. it('optional prop of any type (type: true or prop: true)', () => {
  305. makeInstance(1, true)
  306. expect((console.error as any).mock.calls.length).toBe(0)
  307. makeInstance('any', true)
  308. expect((console.error as any).mock.calls.length).toBe(0)
  309. makeInstance({}, true)
  310. expect((console.error as any).mock.calls.length).toBe(0)
  311. makeInstance(undefined, true)
  312. expect((console.error as any).mock.calls.length).toBe(0)
  313. makeInstance(null, true)
  314. expect((console.error as any).mock.calls.length).toBe(0)
  315. })
  316. })
  317. it('should work with v-bind', () => {
  318. const vm = new Vue({
  319. template: `<test v-bind="{ a: 1, b: 2 }"></test>`,
  320. components: {
  321. test: {
  322. props: ['a', 'b'],
  323. template: '<div>{{ a }} {{ b }}</div>'
  324. }
  325. }
  326. }).$mount()
  327. expect(vm.$el.textContent).toBe('1 2')
  328. })
  329. it('should warn data fields already defined as a prop', () => {
  330. new Vue({
  331. template: '<test a="1"></test>',
  332. components: {
  333. test: {
  334. template: '<div></div>',
  335. data: function () {
  336. return { a: 123 }
  337. },
  338. props: {
  339. a: null
  340. }
  341. }
  342. }
  343. }).$mount()
  344. expect('already declared as a prop').toHaveBeenWarned()
  345. })
  346. it('should warn methods already defined as a prop', () => {
  347. new Vue({
  348. template: '<test a="1"></test>',
  349. components: {
  350. test: {
  351. template: '<div></div>',
  352. props: {
  353. a: null
  354. },
  355. methods: {
  356. a() {}
  357. }
  358. }
  359. }
  360. }).$mount()
  361. expect(`Method "a" has already been defined as a prop`).toHaveBeenWarned()
  362. expect(`Avoid mutating a prop directly`).toHaveBeenWarned()
  363. })
  364. it('treat boolean props properly', () => {
  365. const vm = new Vue({
  366. template: '<comp ref="child" prop-a prop-b="prop-b"></comp>',
  367. components: {
  368. comp: {
  369. template: '<div></div>',
  370. props: {
  371. propA: Boolean,
  372. propB: Boolean,
  373. propC: Boolean
  374. }
  375. }
  376. }
  377. }).$mount()
  378. expect(vm.$refs.child.propA).toBe(true)
  379. expect(vm.$refs.child.propB).toBe(true)
  380. expect(vm.$refs.child.propC).toBe(false)
  381. })
  382. it('should respect default value of a Boolean prop', function () {
  383. const vm = new Vue({
  384. template: '<test></test>',
  385. components: {
  386. test: {
  387. props: {
  388. prop: {
  389. type: Boolean,
  390. default: true
  391. }
  392. },
  393. template: '<div>{{prop}}</div>'
  394. }
  395. }
  396. }).$mount()
  397. expect(vm.$el.textContent).toBe('true')
  398. })
  399. it('non reactive values passed down as prop should not be converted', done => {
  400. const a = Object.freeze({
  401. nested: {
  402. msg: 'hello'
  403. }
  404. })
  405. const parent = new Vue({
  406. template: '<comp :a="a.nested"></comp>',
  407. data: {
  408. a: a
  409. },
  410. components: {
  411. comp: {
  412. template: '<div></div>',
  413. props: ['a']
  414. }
  415. }
  416. }).$mount()
  417. const child = parent.$children[0]
  418. expect(child.a.msg).toBe('hello')
  419. expect(child.a.__ob__).toBeUndefined() // should not be converted
  420. parent.a = Object.freeze({
  421. nested: {
  422. msg: 'yo'
  423. }
  424. })
  425. waitForUpdate(() => {
  426. expect(child.a.msg).toBe('yo')
  427. expect(child.a.__ob__).toBeUndefined()
  428. }).then(done)
  429. })
  430. it('should not warn for non-required, absent prop', function () {
  431. new Vue({
  432. template: '<test></test>',
  433. components: {
  434. test: {
  435. template: '<div></div>',
  436. props: {
  437. prop: {
  438. type: String
  439. }
  440. }
  441. }
  442. }
  443. }).$mount()
  444. expect((console.error as any).mock.calls.length).toBe(0)
  445. })
  446. // #3453
  447. it('should not fire watcher on object/array props when parent re-renders', done => {
  448. const spy = vi.fn()
  449. const vm = new Vue({
  450. data: {
  451. arr: []
  452. },
  453. template: '<test :prop="arr">hi</test>',
  454. components: {
  455. test: {
  456. props: ['prop'],
  457. watch: {
  458. prop: spy
  459. },
  460. template: '<div><slot></slot></div>'
  461. }
  462. }
  463. }).$mount()
  464. vm.$forceUpdate()
  465. waitForUpdate(() => {
  466. expect(spy).not.toHaveBeenCalled()
  467. }).then(done)
  468. })
  469. // #4090
  470. it('should not trigger watcher on default value', done => {
  471. const spy = vi.fn()
  472. const vm = new Vue({
  473. template: `<test :value="a" :test="b"></test>`,
  474. data: {
  475. a: 1,
  476. b: undefined
  477. },
  478. components: {
  479. test: {
  480. template: '<div>{{ value }}</div>',
  481. props: {
  482. value: { type: Number },
  483. test: {
  484. type: Object,
  485. default: () => ({})
  486. }
  487. },
  488. watch: {
  489. test: spy
  490. }
  491. }
  492. }
  493. }).$mount()
  494. vm.a++
  495. waitForUpdate(() => {
  496. expect(spy).not.toHaveBeenCalled()
  497. vm.b = {}
  498. })
  499. .then(() => {
  500. expect(spy.mock.calls.length).toBe(1)
  501. })
  502. .then(() => {
  503. vm.b = undefined
  504. })
  505. .then(() => {
  506. expect(spy.mock.calls.length).toBe(2)
  507. vm.a++
  508. })
  509. .then(() => {
  510. expect(spy.mock.calls.length).toBe(2)
  511. })
  512. .then(done)
  513. })
  514. it('warn reserved props', () => {
  515. const specialAttrs = ['key', 'ref', 'slot', 'is', 'slot-scope']
  516. new Vue({
  517. props: specialAttrs
  518. })
  519. specialAttrs.forEach(attr => {
  520. expect(`"${attr}" is a reserved attribute`).toHaveBeenWarned()
  521. })
  522. })
  523. it('should consider order when casting [Boolean, String] multi-type props', () => {
  524. const vm = new Vue({
  525. template: '<test ref="test" booleanOrString stringOrBoolean />',
  526. components: {
  527. test: {
  528. template: '<div></div>',
  529. props: {
  530. booleanOrString: [Boolean, String],
  531. stringOrBoolean: [String, Boolean]
  532. }
  533. }
  534. }
  535. }).$mount()
  536. expect(vm.$refs.test.$props.booleanOrString).toBe(true)
  537. expect(vm.$refs.test.$props.stringOrBoolean).toBe('')
  538. })
  539. it('should warn when a prop type is not a constructor', () => {
  540. new Vue({
  541. template: '<div>{{a}}</div>',
  542. props: {
  543. a: {
  544. type: 'String',
  545. default: 'test'
  546. }
  547. }
  548. }).$mount()
  549. expect(
  550. 'Invalid prop type: "String" is not a constructor'
  551. ).toHaveBeenWarned()
  552. })
  553. // #12930
  554. it('should not unwrap prop values that are raw refs', () => {
  555. let val
  556. const Comp = {
  557. props: ['msg'],
  558. created() {
  559. val = this.msg
  560. },
  561. render() {}
  562. }
  563. const r = ref()
  564. new Vue({
  565. render: h => h(Comp, { props: { msg: r }})
  566. }).$mount()
  567. expect(val).toBe(r)
  568. })
  569. })