props.spec.js 12 KB

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