props.spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  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('warn object/array default values', () => {
  96. new Vue({
  97. props: {
  98. a: {
  99. default: { b: 1 }
  100. }
  101. },
  102. propsData: {
  103. a: undefined
  104. }
  105. })
  106. expect('Props with type Object/Array must use a factory function').toHaveBeenWarned()
  107. })
  108. it('warn missing required', () => {
  109. new Vue({
  110. template: '<test></test>',
  111. components: {
  112. test: {
  113. props: { a: { required: true }},
  114. template: '<div>{{a}}</div>'
  115. }
  116. }
  117. }).$mount()
  118. expect('Missing required prop: "a"').toHaveBeenWarned()
  119. })
  120. describe('assertions', () => {
  121. function makeInstance (value, type, validator, required) {
  122. return new Vue({
  123. template: '<test :test="val"></test>',
  124. data: {
  125. val: value
  126. },
  127. components: {
  128. test: {
  129. template: '<div></div>',
  130. props: {
  131. test: {
  132. type,
  133. validator,
  134. required
  135. }
  136. }
  137. }
  138. }
  139. }).$mount()
  140. }
  141. it('string', () => {
  142. makeInstance('hello', String)
  143. expect(console.error.calls.count()).toBe(0)
  144. makeInstance(123, String)
  145. expect('Expected String').toHaveBeenWarned()
  146. })
  147. it('number', () => {
  148. makeInstance(123, Number)
  149. expect(console.error.calls.count()).toBe(0)
  150. makeInstance('123', Number)
  151. expect('Expected Number').toHaveBeenWarned()
  152. })
  153. it('boolean', () => {
  154. makeInstance(true, Boolean)
  155. expect(console.error.calls.count()).toBe(0)
  156. makeInstance('123', Boolean)
  157. expect('Expected Boolean').toHaveBeenWarned()
  158. })
  159. it('function', () => {
  160. makeInstance(() => {}, Function)
  161. expect(console.error.calls.count()).toBe(0)
  162. makeInstance(123, Function)
  163. expect('Expected Function').toHaveBeenWarned()
  164. })
  165. it('object', () => {
  166. makeInstance({}, Object)
  167. expect(console.error.calls.count()).toBe(0)
  168. makeInstance([], Object)
  169. expect('Expected Object').toHaveBeenWarned()
  170. })
  171. it('array', () => {
  172. makeInstance([], Array)
  173. expect(console.error.calls.count()).toBe(0)
  174. makeInstance({}, Array)
  175. expect('Expected Array').toHaveBeenWarned()
  176. })
  177. it('custom constructor', () => {
  178. function Class () {}
  179. makeInstance(new Class(), Class)
  180. expect(console.error.calls.count()).toBe(0)
  181. makeInstance({}, Class)
  182. expect('type check failed').toHaveBeenWarned()
  183. })
  184. it('multiple types', () => {
  185. makeInstance([], [Array, Number, Boolean])
  186. expect(console.error.calls.count()).toBe(0)
  187. makeInstance({}, [Array, Number, Boolean])
  188. expect('Expected Array, Number, Boolean, got Object').toHaveBeenWarned()
  189. })
  190. it('custom validator', () => {
  191. makeInstance(123, null, v => v === 123)
  192. expect(console.error.calls.count()).toBe(0)
  193. makeInstance(123, null, v => v === 234)
  194. expect('custom validator check failed').toHaveBeenWarned()
  195. })
  196. it('type check + custom validator', () => {
  197. makeInstance(123, Number, v => v === 123)
  198. expect(console.error.calls.count()).toBe(0)
  199. makeInstance(123, Number, v => v === 234)
  200. expect('custom validator check failed').toHaveBeenWarned()
  201. makeInstance(123, String, v => v === 123)
  202. expect('Expected String').toHaveBeenWarned()
  203. })
  204. it('multiple types + custom validator', () => {
  205. makeInstance(123, [Number, String, Boolean], v => v === 123)
  206. expect(console.error.calls.count()).toBe(0)
  207. makeInstance(123, [Number, String, Boolean], v => v === 234)
  208. expect('custom validator check failed').toHaveBeenWarned()
  209. makeInstance(123, [String, Boolean], v => v === 123)
  210. expect('Expected String, Boolean').toHaveBeenWarned()
  211. })
  212. it('optional with type + null/undefined', () => {
  213. makeInstance(undefined, String)
  214. expect(console.error.calls.count()).toBe(0)
  215. makeInstance(null, String)
  216. expect(console.error.calls.count()).toBe(0)
  217. })
  218. it('required with type + null/undefined', () => {
  219. makeInstance(undefined, String, null, true)
  220. expect(console.error.calls.count()).toBe(1)
  221. expect('Expected String').toHaveBeenWarned()
  222. makeInstance(null, Boolean, null, true)
  223. expect(console.error.calls.count()).toBe(2)
  224. expect('Expected Boolean').toHaveBeenWarned()
  225. })
  226. it('optional prop of any type (type: true or prop: true)', () => {
  227. makeInstance(1, true)
  228. expect(console.error.calls.count()).toBe(0)
  229. makeInstance('any', true)
  230. expect(console.error.calls.count()).toBe(0)
  231. makeInstance({}, true)
  232. expect(console.error.calls.count()).toBe(0)
  233. makeInstance(undefined, true)
  234. expect(console.error.calls.count()).toBe(0)
  235. makeInstance(null, true)
  236. expect(console.error.calls.count()).toBe(0)
  237. })
  238. })
  239. it('should work with v-bind', () => {
  240. const vm = new Vue({
  241. template: `<test v-bind="{ a: 1, b: 2 }"></test>`,
  242. components: {
  243. test: {
  244. props: ['a', 'b'],
  245. template: '<div>{{ a }} {{ b }}</div>'
  246. }
  247. }
  248. }).$mount()
  249. expect(vm.$el.textContent).toBe('1 2')
  250. })
  251. it('should warn data fields already defined as a prop', () => {
  252. new Vue({
  253. template: '<test a="1"></test>',
  254. components: {
  255. test: {
  256. template: '<div></div>',
  257. data: function () {
  258. return { a: 123 }
  259. },
  260. props: {
  261. a: null
  262. }
  263. }
  264. }
  265. }).$mount()
  266. expect('already declared as a prop').toHaveBeenWarned()
  267. })
  268. it('treat boolean props properly', () => {
  269. const vm = new Vue({
  270. template: '<comp ref="child" prop-a prop-b="prop-b"></comp>',
  271. components: {
  272. comp: {
  273. template: '<div></div>',
  274. props: {
  275. propA: Boolean,
  276. propB: Boolean,
  277. propC: Boolean
  278. }
  279. }
  280. }
  281. }).$mount()
  282. expect(vm.$refs.child.propA).toBe(true)
  283. expect(vm.$refs.child.propB).toBe(true)
  284. expect(vm.$refs.child.propC).toBe(false)
  285. })
  286. it('should respect default value of a Boolean prop', function () {
  287. const vm = new Vue({
  288. template: '<test></test>',
  289. components: {
  290. test: {
  291. props: {
  292. prop: {
  293. type: Boolean,
  294. default: true
  295. }
  296. },
  297. template: '<div>{{prop}}</div>'
  298. }
  299. }
  300. }).$mount()
  301. expect(vm.$el.textContent).toBe('true')
  302. })
  303. it('non reactive values passed down as prop should not be converted', done => {
  304. const a = Object.freeze({
  305. nested: {
  306. msg: 'hello'
  307. }
  308. })
  309. const parent = new Vue({
  310. template: '<comp :a="a.nested"></comp>',
  311. data: {
  312. a: a
  313. },
  314. components: {
  315. comp: {
  316. template: '<div></div>',
  317. props: ['a']
  318. }
  319. }
  320. }).$mount()
  321. const child = parent.$children[0]
  322. expect(child.a.msg).toBe('hello')
  323. expect(child.a.__ob__).toBeUndefined() // should not be converted
  324. parent.a = Object.freeze({
  325. nested: {
  326. msg: 'yo'
  327. }
  328. })
  329. waitForUpdate(() => {
  330. expect(child.a.msg).toBe('yo')
  331. expect(child.a.__ob__).toBeUndefined()
  332. }).then(done)
  333. })
  334. it('should not warn for non-required, absent prop', function () {
  335. new Vue({
  336. template: '<test></test>',
  337. components: {
  338. test: {
  339. template: '<div></div>',
  340. props: {
  341. prop: {
  342. type: String
  343. }
  344. }
  345. }
  346. }
  347. }).$mount()
  348. expect(console.error.calls.count()).toBe(0)
  349. })
  350. // #3453
  351. it('should not fire watcher on object/array props when parent re-renders', done => {
  352. const spy = jasmine.createSpy()
  353. const vm = new Vue({
  354. data: {
  355. arr: []
  356. },
  357. template: '<test :prop="arr">hi</test>',
  358. components: {
  359. test: {
  360. props: ['prop'],
  361. watch: {
  362. prop: spy
  363. },
  364. template: '<div><slot></slot></div>'
  365. }
  366. }
  367. }).$mount()
  368. vm.$forceUpdate()
  369. waitForUpdate(() => {
  370. expect(spy).not.toHaveBeenCalled()
  371. }).then(done)
  372. })
  373. // #4090
  374. it('should not trigger wathcer on default value', done => {
  375. const spy = jasmine.createSpy()
  376. const vm = new Vue({
  377. template: `<test :value="a" :test="b"></test>`,
  378. data: {
  379. a: 1,
  380. b: undefined
  381. },
  382. components: {
  383. test: {
  384. template: '<div>{{ value }}</div>',
  385. props: {
  386. value: { type: Number },
  387. test: {
  388. type: Object,
  389. default: () => ({})
  390. }
  391. },
  392. watch: {
  393. test: spy
  394. }
  395. }
  396. }
  397. }).$mount()
  398. vm.a++
  399. waitForUpdate(() => {
  400. expect(spy).not.toHaveBeenCalled()
  401. vm.b = {}
  402. }).then(() => {
  403. expect(spy.calls.count()).toBe(1)
  404. }).then(() => {
  405. vm.b = undefined
  406. }).then(() => {
  407. expect(spy.calls.count()).toBe(2)
  408. vm.a++
  409. }).then(() => {
  410. expect(spy.calls.count()).toBe(2)
  411. }).then(done)
  412. })
  413. it('warn reserved props', () => {
  414. new Vue({
  415. props: {
  416. key: String
  417. }
  418. })
  419. expect(`"key" is a reserved attribute`).toHaveBeenWarned()
  420. })
  421. })