props.spec.js 15 KB

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