prop_spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. var Vue = require('src')
  2. describe('prop', function () {
  3. var el
  4. beforeEach(function () {
  5. el = document.createElement('div')
  6. spyWarns()
  7. })
  8. it('one way binding', function (done) {
  9. var vm = new Vue({
  10. el: el,
  11. data: {
  12. b: 'B'
  13. },
  14. template: '<test v-bind:b="b" v-ref:child></test>',
  15. components: {
  16. test: {
  17. props: ['b'],
  18. template: '{{b}}'
  19. }
  20. }
  21. })
  22. expect(el.innerHTML).toBe('<test>B</test>')
  23. vm.b = 'BB'
  24. Vue.nextTick(function () {
  25. expect(el.innerHTML).toBe('<test>BB</test>')
  26. vm.$refs.child.b = 'BBB'
  27. expect(vm.b).toBe('BB')
  28. Vue.nextTick(function () {
  29. expect(el.innerHTML).toBe('<test>BBB</test>')
  30. done()
  31. })
  32. })
  33. })
  34. it('with filters', function (done) {
  35. var vm = new Vue({
  36. el: el,
  37. template: '<test :name="a | test"></test>',
  38. data: {
  39. a: 123
  40. },
  41. filters: {
  42. test: function (v) {
  43. return v * 2
  44. }
  45. },
  46. components: {
  47. test: {
  48. props: ['name'],
  49. template: '{{name}}'
  50. }
  51. }
  52. })
  53. expect(el.textContent).toBe('246')
  54. vm.a = 234
  55. Vue.nextTick(function () {
  56. expect(el.textContent).toBe('468')
  57. done()
  58. })
  59. })
  60. it('two-way binding', function (done) {
  61. var vm = new Vue({
  62. el: el,
  63. data: {
  64. b: 'B',
  65. test: {
  66. a: 'A'
  67. }
  68. },
  69. template: '<test v-bind:testt.sync="test" :bb.sync="b" :a.sync=" test.a " v-ref:child></test>',
  70. components: {
  71. test: {
  72. props: ['testt', 'bb', 'a'],
  73. template: '{{testt.a}} {{bb}} {{a}}'
  74. }
  75. }
  76. })
  77. expect(el.firstChild.textContent).toBe('A B A')
  78. vm.test.a = 'AA'
  79. vm.b = 'BB'
  80. Vue.nextTick(function () {
  81. expect(el.firstChild.textContent).toBe('AA BB AA')
  82. vm.test = { a: 'AAA' }
  83. Vue.nextTick(function () {
  84. expect(el.firstChild.textContent).toBe('AAA BB AAA')
  85. vm.$data = {
  86. b: 'BBB',
  87. test: {
  88. a: 'AAAA'
  89. }
  90. }
  91. Vue.nextTick(function () {
  92. expect(el.firstChild.textContent).toBe('AAAA BBB AAAA')
  93. // test two-way
  94. vm.$refs.child.bb = 'B'
  95. vm.$refs.child.testt = { a: 'A' }
  96. Vue.nextTick(function () {
  97. expect(el.firstChild.textContent).toBe('A B A')
  98. expect(vm.test.a).toBe('A')
  99. expect(vm.test).toBe(vm.$refs.child.testt)
  100. expect(vm.b).toBe('B')
  101. vm.$refs.child.a = 'Oops'
  102. Vue.nextTick(function () {
  103. expect(el.firstChild.textContent).toBe('Oops B Oops')
  104. expect(vm.test.a).toBe('Oops')
  105. done()
  106. })
  107. })
  108. })
  109. })
  110. })
  111. })
  112. it('explicit one time binding', function (done) {
  113. var vm = new Vue({
  114. el: el,
  115. data: {
  116. b: 'B'
  117. },
  118. template: '<test :b.once="b" v-ref:child></test>',
  119. components: {
  120. test: {
  121. props: ['b'],
  122. template: '{{b}}'
  123. }
  124. }
  125. })
  126. expect(el.innerHTML).toBe('<test>B</test>')
  127. vm.b = 'BB'
  128. Vue.nextTick(function () {
  129. expect(el.innerHTML).toBe('<test>B</test>')
  130. done()
  131. })
  132. })
  133. it('warn non-settable parent path', function (done) {
  134. var vm = new Vue({
  135. el: el,
  136. data: {
  137. b: 'B'
  138. },
  139. template: '<test :b.sync=" b + \'B\'" v-ref:child></test>',
  140. components: {
  141. test: {
  142. props: ['b'],
  143. template: '{{b}}'
  144. }
  145. }
  146. })
  147. expect(hasWarned('Cannot bind two-way prop with non-settable parent path')).toBe(true)
  148. expect(el.innerHTML).toBe('<test>BB</test>')
  149. vm.b = 'BB'
  150. Vue.nextTick(function () {
  151. expect(el.innerHTML).toBe('<test>BBB</test>')
  152. vm.$refs.child.b = 'hahaha'
  153. Vue.nextTick(function () {
  154. expect(vm.b).toBe('BB')
  155. expect(el.innerHTML).toBe('<test>hahaha</test>')
  156. done()
  157. })
  158. })
  159. })
  160. it('warn expect two-way', function () {
  161. new Vue({
  162. el: el,
  163. template: '<test :test="ok"></test>',
  164. data: {
  165. ok: 'hi'
  166. },
  167. components: {
  168. test: {
  169. props: {
  170. test: {
  171. twoWay: true
  172. }
  173. }
  174. }
  175. }
  176. })
  177. expect(hasWarned('expects a two-way binding type')).toBe(true)
  178. })
  179. it('warn $data as prop', function () {
  180. new Vue({
  181. el: el,
  182. template: '<test></test>',
  183. data: {
  184. ok: 'hi'
  185. },
  186. components: {
  187. test: {
  188. props: ['$data']
  189. }
  190. }
  191. })
  192. expect(hasWarned('Do not use $data as prop')).toBe(true)
  193. })
  194. it('warn invalid keys', function () {
  195. new Vue({
  196. el: el,
  197. template: '<test :a.b.c="test"></test>',
  198. components: {
  199. test: {
  200. props: ['a.b.c']
  201. }
  202. }
  203. })
  204. expect(hasWarned('Invalid prop key')).toBe(true)
  205. })
  206. it('warn props with no el option', function () {
  207. new Vue({
  208. props: ['a']
  209. })
  210. expect(hasWarned('Props will not be compiled if no `el`')).toBe(true)
  211. })
  212. it('warn object/array default values', function () {
  213. new Vue({
  214. el: el,
  215. props: {
  216. arr: {
  217. type: Array,
  218. default: []
  219. },
  220. obj: {
  221. type: Object,
  222. default: {}
  223. }
  224. }
  225. })
  226. expect(hasWarned('Use a factory function to return the default value')).toBe(true)
  227. expect(getWarnCount()).toBe(2)
  228. })
  229. it('teardown', function (done) {
  230. var vm = new Vue({
  231. el: el,
  232. data: {
  233. a: 'A',
  234. b: 'B'
  235. },
  236. template: '<test :aa.sync="a" :bb="b"></test>',
  237. components: {
  238. test: {
  239. props: ['aa', 'bb'],
  240. template: '{{aa}} {{bb}}'
  241. }
  242. }
  243. })
  244. var child = vm.$children[0]
  245. expect(el.firstChild.textContent).toBe('A B')
  246. child.aa = 'AA'
  247. vm.b = 'BB'
  248. Vue.nextTick(function () {
  249. expect(el.firstChild.textContent).toBe('AA BB')
  250. expect(vm.a).toBe('AA')
  251. // unbind the two props
  252. child._directives[0].unbind()
  253. child._directives[1].unbind()
  254. child.aa = 'AAA'
  255. vm.b = 'BBB'
  256. Vue.nextTick(function () {
  257. expect(el.firstChild.textContent).toBe('AAA BB')
  258. expect(vm.a).toBe('AA')
  259. done()
  260. })
  261. })
  262. })
  263. it('block instance with replace:true', function () {
  264. new Vue({
  265. el: el,
  266. template: '<test :b="a" :c="d"></test>',
  267. data: {
  268. a: 'AAA',
  269. d: 'DDD'
  270. },
  271. components: {
  272. test: {
  273. props: ['b', 'c'],
  274. template: '<p>{{b}}</p><p>{{c}}</p>',
  275. replace: true
  276. }
  277. }
  278. })
  279. expect(el.innerHTML).toBe('<p>AAA</p><p>DDD</p>')
  280. })
  281. describe('assertions', function () {
  282. function makeInstance (value, type, validator, coerce) {
  283. return new Vue({
  284. el: document.createElement('div'),
  285. template: '<test :test="val"></test>',
  286. data: {
  287. val: value
  288. },
  289. components: {
  290. test: {
  291. props: {
  292. test: {
  293. type: type,
  294. validator: validator,
  295. coerce: coerce
  296. }
  297. }
  298. }
  299. }
  300. })
  301. }
  302. it('string', function () {
  303. makeInstance('hello', String)
  304. expect(getWarnCount()).toBe(0)
  305. makeInstance(123, String)
  306. expect(hasWarned('Expected String')).toBe(true)
  307. })
  308. it('number', function () {
  309. makeInstance(123, Number)
  310. expect(getWarnCount()).toBe(0)
  311. makeInstance('123', Number)
  312. expect(hasWarned('Expected Number')).toBe(true)
  313. })
  314. it('boolean', function () {
  315. makeInstance(true, Boolean)
  316. expect(getWarnCount()).toBe(0)
  317. makeInstance('123', Boolean)
  318. expect(hasWarned('Expected Boolean')).toBe(true)
  319. })
  320. it('function', function () {
  321. makeInstance(function () {}, Function)
  322. expect(getWarnCount()).toBe(0)
  323. makeInstance(123, Function)
  324. expect(hasWarned('Expected Function')).toBe(true)
  325. })
  326. it('object', function () {
  327. makeInstance({}, Object)
  328. expect(getWarnCount()).toBe(0)
  329. makeInstance([], Object)
  330. expect(hasWarned('Expected Object')).toBe(true)
  331. })
  332. it('array', function () {
  333. makeInstance([], Array)
  334. expect(getWarnCount()).toBe(0)
  335. makeInstance({}, Array)
  336. expect(hasWarned('Expected Array')).toBe(true)
  337. })
  338. it('custom constructor', function () {
  339. function Class () {}
  340. makeInstance(new Class(), Class)
  341. expect(getWarnCount()).toBe(0)
  342. makeInstance({}, Class)
  343. expect(hasWarned('Expected custom type')).toBe(true)
  344. })
  345. it('custom validator', function () {
  346. makeInstance(123, null, function (v) {
  347. return v === 123
  348. })
  349. expect(getWarnCount()).toBe(0)
  350. makeInstance(123, null, function (v) {
  351. return v === 234
  352. })
  353. expect(hasWarned('custom validator check failed')).toBe(true)
  354. })
  355. it('type check + custom validator', function () {
  356. makeInstance(123, Number, function (v) {
  357. return v === 123
  358. })
  359. expect(getWarnCount()).toBe(0)
  360. makeInstance(123, Number, function (v) {
  361. return v === 234
  362. })
  363. expect(hasWarned('custom validator check failed')).toBe(true)
  364. makeInstance(123, String, function (v) {
  365. return v === 123
  366. })
  367. expect(hasWarned('Expected String')).toBe(true)
  368. })
  369. it('type check + coerce', function () {
  370. makeInstance(123, String, null, String)
  371. expect(getWarnCount()).toBe(0)
  372. makeInstance('123', Number, null, Number)
  373. expect(getWarnCount()).toBe(0)
  374. makeInstance('123', Boolean, null, function (val) {
  375. return val === '123'
  376. })
  377. expect(getWarnCount()).toBe(0)
  378. })
  379. it('required', function () {
  380. new Vue({
  381. el: document.createElement('div'),
  382. template: '<test></test>',
  383. components: {
  384. test: {
  385. props: {
  386. prop: { required: true }
  387. }
  388. }
  389. }
  390. })
  391. expect(hasWarned('Missing required prop')).toBe(true)
  392. })
  393. })
  394. it('alternative syntax', function () {
  395. new Vue({
  396. el: el,
  397. template: '<test :b="a" :c="d"></test>',
  398. data: {
  399. a: 'AAA',
  400. d: 'DDD'
  401. },
  402. components: {
  403. test: {
  404. props: {
  405. b: String,
  406. c: {
  407. type: Number
  408. },
  409. d: {
  410. required: true
  411. }
  412. },
  413. template: '<p>{{b}}</p><p>{{c}}</p>'
  414. }
  415. }
  416. })
  417. expect(hasWarned('Missing required prop')).toBe(true)
  418. expect(hasWarned('Expected Number')).toBe(true)
  419. expect(el.textContent).toBe('AAA')
  420. })
  421. it('mixed syntax', function () {
  422. new Vue({
  423. el: el,
  424. template: '<test :b="a" :c="d"></test>',
  425. data: {
  426. a: 'AAA',
  427. d: 'DDD'
  428. },
  429. components: {
  430. test: {
  431. props: [
  432. 'b',
  433. {
  434. name: 'c',
  435. type: Number
  436. },
  437. {
  438. name: 'd',
  439. required: true
  440. }
  441. ],
  442. template: '<p>{{b}}</p><p>{{c}}</p>'
  443. }
  444. }
  445. })
  446. expect(hasWarned('Missing required prop')).toBe(true)
  447. expect(hasWarned('Expected Number')).toBe(true)
  448. expect(el.textContent).toBe('AAA')
  449. })
  450. it('should not overwrite default value for an absent Boolean prop', function () {
  451. var vm = new Vue({
  452. el: el,
  453. template: '<test></test>',
  454. components: {
  455. test: {
  456. props: {
  457. prop: Boolean
  458. },
  459. data: function () {
  460. return {
  461. prop: true
  462. }
  463. },
  464. template: '{{prop}}'
  465. }
  466. }
  467. })
  468. expect(vm.$children[0].prop).toBe(true)
  469. expect(vm.$el.textContent).toBe('true')
  470. expect(JSON.stringify(vm.$children[0].$data)).toBe(JSON.stringify({
  471. prop: true
  472. }))
  473. })
  474. it('should respect default value of a Boolean prop', function () {
  475. var vm = new Vue({
  476. el: el,
  477. template: '<test></test>',
  478. components: {
  479. test: {
  480. props: {
  481. prop: {
  482. type: Boolean,
  483. default: true
  484. }
  485. },
  486. template: '{{prop}}'
  487. }
  488. }
  489. })
  490. expect(vm.$el.textContent).toBe('true')
  491. })
  492. it('should initialize with default value when not provided & has default data', function (done) {
  493. var vm = new Vue({
  494. el: el,
  495. template: '<test></test>',
  496. components: {
  497. test: {
  498. props: {
  499. prop: {
  500. type: String,
  501. default: 'hello'
  502. },
  503. prop2: {
  504. type: Object,
  505. default: function () {
  506. return { vm: this }
  507. }
  508. }
  509. },
  510. data: function () {
  511. return {
  512. other: 'world'
  513. }
  514. },
  515. template: '{{prop}} {{other}}'
  516. }
  517. }
  518. })
  519. expect(vm.$el.textContent).toBe('hello world')
  520. // object/array default value initializers should be
  521. // called with the correct `this` context
  522. var child = vm.$children[0]
  523. expect(child.prop2.vm).toBe(child)
  524. vm.$children[0].prop = 'bye'
  525. Vue.nextTick(function () {
  526. expect(vm.$el.textContent).toBe('bye world')
  527. done()
  528. })
  529. })
  530. it('should warn data fields already defined as a prop', function () {
  531. var Comp = Vue.extend({
  532. data: function () {
  533. return { a: 123 }
  534. },
  535. props: {
  536. a: null
  537. }
  538. })
  539. new Vue({
  540. el: el,
  541. template: '<comp a="1"></comp>',
  542. components: {
  543. comp: Comp
  544. }
  545. })
  546. expect(hasWarned('already defined as a prop')).toBe(true)
  547. })
  548. it('should not warn data fields already defined as a prop if it is from instantiation call', function () {
  549. var vm = new Vue({
  550. el: el,
  551. props: {
  552. a: null
  553. },
  554. data: {
  555. a: 123
  556. }
  557. })
  558. expect(getWarnCount()).toBe(0)
  559. expect(vm.a).toBe(123)
  560. })
  561. it('should not warn for non-required, absent prop', function () {
  562. new Vue({
  563. el: el,
  564. template: '<test></test>',
  565. components: {
  566. test: {
  567. props: {
  568. prop: {
  569. type: String
  570. }
  571. }
  572. }
  573. }
  574. })
  575. expect(getWarnCount()).toBe(0)
  576. })
  577. // #1683
  578. it('should properly sync back up when mutating then replace', function (done) {
  579. var vm = new Vue({
  580. el: el,
  581. data: {
  582. items: [1, 2]
  583. },
  584. template: '<comp :items.sync="items"></comp>',
  585. components: {
  586. comp: {
  587. props: ['items']
  588. }
  589. }
  590. })
  591. var child = vm.$children[0]
  592. child.items.push(3)
  593. var newArray = child.items = [4]
  594. Vue.nextTick(function () {
  595. expect(child.items).toBe(newArray)
  596. expect(vm.items).toBe(newArray)
  597. done()
  598. })
  599. })
  600. it('treat boolean props properly', function () {
  601. var vm = new Vue({
  602. el: el,
  603. template: '<comp v-ref:child prop-a></comp>',
  604. components: {
  605. comp: {
  606. props: {
  607. propA: Boolean,
  608. propB: Boolean
  609. }
  610. }
  611. }
  612. })
  613. expect(vm.$refs.child.propA).toBe(true)
  614. expect(vm.$refs.child.propB).toBe(false)
  615. })
  616. })