props.spec.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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').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').toHaveBeenWarned()
  170. })
  171. it('boolean', () => {
  172. makeInstance(true, Boolean)
  173. expect(console.error.calls.count()).toBe(0)
  174. makeInstance('123', Boolean)
  175. expect('Expected Boolean').toHaveBeenWarned()
  176. })
  177. it('function', () => {
  178. makeInstance(() => {}, Function)
  179. expect(console.error.calls.count()).toBe(0)
  180. makeInstance(123, Function)
  181. expect('Expected Function').toHaveBeenWarned()
  182. })
  183. it('object', () => {
  184. makeInstance({}, Object)
  185. expect(console.error.calls.count()).toBe(0)
  186. makeInstance([], Object)
  187. expect('Expected Object').toHaveBeenWarned()
  188. })
  189. it('array', () => {
  190. makeInstance([], Array)
  191. expect(console.error.calls.count()).toBe(0)
  192. makeInstance({}, Array)
  193. expect('Expected Array').toHaveBeenWarned()
  194. })
  195. it('primitive wrapper objects', () => {
  196. /* eslint-disable no-new-wrappers */
  197. makeInstance(new String('s'), String)
  198. expect(console.error.calls.count()).toBe(0)
  199. makeInstance(new Number(1), Number)
  200. expect(console.error.calls.count()).toBe(0)
  201. makeInstance(new Boolean(true), Boolean)
  202. expect(console.error.calls.count()).toBe(0)
  203. /* eslint-enable no-new-wrappers */
  204. })
  205. if (hasSymbol) {
  206. it('symbol', () => {
  207. makeInstance(Symbol('foo'), Symbol)
  208. expect(console.error.calls.count()).toBe(0)
  209. makeInstance({}, Symbol)
  210. expect('Expected Symbol').toHaveBeenWarned()
  211. })
  212. }
  213. it('custom constructor', () => {
  214. function Class () {}
  215. makeInstance(new Class(), Class)
  216. expect(console.error.calls.count()).toBe(0)
  217. makeInstance({}, Class)
  218. expect('type check failed').toHaveBeenWarned()
  219. })
  220. it('multiple types', () => {
  221. makeInstance([], [Array, Number, Boolean])
  222. expect(console.error.calls.count()).toBe(0)
  223. makeInstance({}, [Array, Number, Boolean])
  224. expect('Expected Array, Number, Boolean, got Object').toHaveBeenWarned()
  225. })
  226. it('custom validator', () => {
  227. makeInstance(123, null, v => v === 123)
  228. expect(console.error.calls.count()).toBe(0)
  229. makeInstance(123, null, v => v === 234)
  230. expect('custom validator check failed').toHaveBeenWarned()
  231. })
  232. it('type check + custom validator', () => {
  233. makeInstance(123, Number, v => v === 123)
  234. expect(console.error.calls.count()).toBe(0)
  235. makeInstance(123, Number, v => v === 234)
  236. expect('custom validator check failed').toHaveBeenWarned()
  237. makeInstance(123, String, v => v === 123)
  238. expect('Expected String').toHaveBeenWarned()
  239. })
  240. it('multiple types + custom validator', () => {
  241. makeInstance(123, [Number, String, Boolean], v => v === 123)
  242. expect(console.error.calls.count()).toBe(0)
  243. makeInstance(123, [Number, String, Boolean], v => v === 234)
  244. expect('custom validator check failed').toHaveBeenWarned()
  245. makeInstance(123, [String, Boolean], v => v === 123)
  246. expect('Expected String, Boolean').toHaveBeenWarned()
  247. })
  248. it('optional with type + null/undefined', () => {
  249. makeInstance(undefined, String)
  250. expect(console.error.calls.count()).toBe(0)
  251. makeInstance(null, String)
  252. expect(console.error.calls.count()).toBe(0)
  253. })
  254. it('required with type + null/undefined', () => {
  255. makeInstance(undefined, String, null, true)
  256. expect(console.error.calls.count()).toBe(1)
  257. expect('Expected String').toHaveBeenWarned()
  258. makeInstance(null, Boolean, null, true)
  259. expect(console.error.calls.count()).toBe(2)
  260. expect('Expected Boolean').toHaveBeenWarned()
  261. })
  262. it('optional prop of any type (type: true or prop: true)', () => {
  263. makeInstance(1, true)
  264. expect(console.error.calls.count()).toBe(0)
  265. makeInstance('any', true)
  266. expect(console.error.calls.count()).toBe(0)
  267. makeInstance({}, true)
  268. expect(console.error.calls.count()).toBe(0)
  269. makeInstance(undefined, true)
  270. expect(console.error.calls.count()).toBe(0)
  271. makeInstance(null, true)
  272. expect(console.error.calls.count()).toBe(0)
  273. })
  274. })
  275. it('should work with v-bind', () => {
  276. const vm = new Vue({
  277. template: `<test v-bind="{ a: 1, b: 2 }"></test>`,
  278. components: {
  279. test: {
  280. props: ['a', 'b'],
  281. template: '<div>{{ a }} {{ b }}</div>'
  282. }
  283. }
  284. }).$mount()
  285. expect(vm.$el.textContent).toBe('1 2')
  286. })
  287. it('should warn data fields already defined as a prop', () => {
  288. new Vue({
  289. template: '<test a="1"></test>',
  290. components: {
  291. test: {
  292. template: '<div></div>',
  293. data: function () {
  294. return { a: 123 }
  295. },
  296. props: {
  297. a: null
  298. }
  299. }
  300. }
  301. }).$mount()
  302. expect('already declared as a prop').toHaveBeenWarned()
  303. })
  304. it('should warn methods already defined as a prop', () => {
  305. new Vue({
  306. template: '<test a="1"></test>',
  307. components: {
  308. test: {
  309. template: '<div></div>',
  310. props: {
  311. a: null
  312. },
  313. methods: {
  314. a () {
  315. }
  316. }
  317. }
  318. }
  319. }).$mount()
  320. expect(`Method "a" has already been defined as a prop`).toHaveBeenWarned()
  321. expect(`Avoid mutating a prop directly`).toHaveBeenWarned()
  322. })
  323. it('treat boolean props properly', () => {
  324. const vm = new Vue({
  325. template: '<comp ref="child" prop-a prop-b="prop-b"></comp>',
  326. components: {
  327. comp: {
  328. template: '<div></div>',
  329. props: {
  330. propA: Boolean,
  331. propB: Boolean,
  332. propC: Boolean
  333. }
  334. }
  335. }
  336. }).$mount()
  337. expect(vm.$refs.child.propA).toBe(true)
  338. expect(vm.$refs.child.propB).toBe(true)
  339. expect(vm.$refs.child.propC).toBe(false)
  340. })
  341. it('should respect default value of a Boolean prop', function () {
  342. const vm = new Vue({
  343. template: '<test></test>',
  344. components: {
  345. test: {
  346. props: {
  347. prop: {
  348. type: Boolean,
  349. default: true
  350. }
  351. },
  352. template: '<div>{{prop}}</div>'
  353. }
  354. }
  355. }).$mount()
  356. expect(vm.$el.textContent).toBe('true')
  357. })
  358. it('non reactive values passed down as prop should not be converted', done => {
  359. const a = Object.freeze({
  360. nested: {
  361. msg: 'hello'
  362. }
  363. })
  364. const parent = new Vue({
  365. template: '<comp :a="a.nested"></comp>',
  366. data: {
  367. a: a
  368. },
  369. components: {
  370. comp: {
  371. template: '<div></div>',
  372. props: ['a']
  373. }
  374. }
  375. }).$mount()
  376. const child = parent.$children[0]
  377. expect(child.a.msg).toBe('hello')
  378. expect(child.a.__ob__).toBeUndefined() // should not be converted
  379. parent.a = Object.freeze({
  380. nested: {
  381. msg: 'yo'
  382. }
  383. })
  384. waitForUpdate(() => {
  385. expect(child.a.msg).toBe('yo')
  386. expect(child.a.__ob__).toBeUndefined()
  387. }).then(done)
  388. })
  389. it('should not warn for non-required, absent prop', function () {
  390. new Vue({
  391. template: '<test></test>',
  392. components: {
  393. test: {
  394. template: '<div></div>',
  395. props: {
  396. prop: {
  397. type: String
  398. }
  399. }
  400. }
  401. }
  402. }).$mount()
  403. expect(console.error.calls.count()).toBe(0)
  404. })
  405. // #3453
  406. it('should not fire watcher on object/array props when parent re-renders', done => {
  407. const spy = jasmine.createSpy()
  408. const vm = new Vue({
  409. data: {
  410. arr: []
  411. },
  412. template: '<test :prop="arr">hi</test>',
  413. components: {
  414. test: {
  415. props: ['prop'],
  416. watch: {
  417. prop: spy
  418. },
  419. template: '<div><slot></slot></div>'
  420. }
  421. }
  422. }).$mount()
  423. vm.$forceUpdate()
  424. waitForUpdate(() => {
  425. expect(spy).not.toHaveBeenCalled()
  426. }).then(done)
  427. })
  428. // #4090
  429. it('should not trigger watcher on default value', done => {
  430. const spy = jasmine.createSpy()
  431. const vm = new Vue({
  432. template: `<test :value="a" :test="b"></test>`,
  433. data: {
  434. a: 1,
  435. b: undefined
  436. },
  437. components: {
  438. test: {
  439. template: '<div>{{ value }}</div>',
  440. props: {
  441. value: { type: Number },
  442. test: {
  443. type: Object,
  444. default: () => ({})
  445. }
  446. },
  447. watch: {
  448. test: spy
  449. }
  450. }
  451. }
  452. }).$mount()
  453. vm.a++
  454. waitForUpdate(() => {
  455. expect(spy).not.toHaveBeenCalled()
  456. vm.b = {}
  457. }).then(() => {
  458. expect(spy.calls.count()).toBe(1)
  459. }).then(() => {
  460. vm.b = undefined
  461. }).then(() => {
  462. expect(spy.calls.count()).toBe(2)
  463. vm.a++
  464. }).then(() => {
  465. expect(spy.calls.count()).toBe(2)
  466. }).then(done)
  467. })
  468. it('warn reserved props', () => {
  469. const specialAttrs = ['key', 'ref', 'slot', 'is', 'slot-scope']
  470. new Vue({
  471. props: specialAttrs
  472. })
  473. specialAttrs.forEach(attr => {
  474. expect(`"${attr}" is a reserved attribute`).toHaveBeenWarned()
  475. })
  476. })
  477. })