prop_spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. var Vue = require('src')
  2. describe('prop', function () {
  3. var el
  4. beforeEach(function () {
  5. el = document.createElement('div')
  6. })
  7. it('one way binding', function (done) {
  8. var vm = new Vue({
  9. el: el,
  10. data: {
  11. b: 'B'
  12. },
  13. template: '<test v-bind:b="b" v-ref:child></test>',
  14. components: {
  15. test: {
  16. props: ['b'],
  17. template: '{{b}}'
  18. }
  19. }
  20. })
  21. expect(el.innerHTML).toBe('<test>B</test>')
  22. vm.b = 'BB'
  23. Vue.nextTick(function () {
  24. expect(el.innerHTML).toBe('<test>BB</test>')
  25. vm.$refs.child.b = 'BBB'
  26. expect(vm.b).toBe('BB')
  27. Vue.nextTick(function () {
  28. expect(el.innerHTML).toBe('<test>BBB</test>')
  29. done()
  30. })
  31. })
  32. })
  33. it('with filters', function (done) {
  34. var vm = new Vue({
  35. el: el,
  36. template: '<test :name="a | test"></test>',
  37. data: {
  38. a: 123
  39. },
  40. filters: {
  41. test: function (v) {
  42. return v * 2
  43. }
  44. },
  45. components: {
  46. test: {
  47. props: ['name'],
  48. template: '{{name}}'
  49. }
  50. }
  51. })
  52. expect(el.textContent).toBe('246')
  53. vm.a = 234
  54. Vue.nextTick(function () {
  55. expect(el.textContent).toBe('468')
  56. done()
  57. })
  58. })
  59. it('two-way binding', function (done) {
  60. var vm = new Vue({
  61. el: el,
  62. data: {
  63. b: 'B',
  64. test: {
  65. a: 'A'
  66. }
  67. },
  68. template: '<test v-bind:testt.sync="test" :bb.sync="b" :a.sync=" test.a " v-ref:child></test>',
  69. components: {
  70. test: {
  71. props: ['testt', 'bb', 'a'],
  72. template: '{{testt.a}} {{bb}} {{a}}'
  73. }
  74. }
  75. })
  76. expect(el.firstChild.textContent).toBe('A B A')
  77. vm.test.a = 'AA'
  78. vm.b = 'BB'
  79. Vue.nextTick(function () {
  80. expect(el.firstChild.textContent).toBe('AA BB AA')
  81. vm.test = { a: 'AAA' }
  82. Vue.nextTick(function () {
  83. expect(el.firstChild.textContent).toBe('AAA BB AAA')
  84. vm.$data = {
  85. b: 'BBB',
  86. test: {
  87. a: 'AAAA'
  88. }
  89. }
  90. Vue.nextTick(function () {
  91. expect(el.firstChild.textContent).toBe('AAAA BBB AAAA')
  92. // test two-way
  93. vm.$refs.child.bb = 'B'
  94. vm.$refs.child.testt = { a: 'A' }
  95. Vue.nextTick(function () {
  96. expect(el.firstChild.textContent).toBe('A B A')
  97. expect(vm.test.a).toBe('A')
  98. expect(vm.test).toBe(vm.$refs.child.testt)
  99. expect(vm.b).toBe('B')
  100. vm.$refs.child.a = 'Oops'
  101. Vue.nextTick(function () {
  102. expect(el.firstChild.textContent).toBe('Oops B Oops')
  103. expect(vm.test.a).toBe('Oops')
  104. done()
  105. })
  106. })
  107. })
  108. })
  109. })
  110. })
  111. it('explicit one time binding', function (done) {
  112. var vm = new Vue({
  113. el: el,
  114. data: {
  115. b: 'B'
  116. },
  117. template: '<test :b.once="b" v-ref:child></test>',
  118. components: {
  119. test: {
  120. props: ['b'],
  121. template: '{{b}}'
  122. }
  123. }
  124. })
  125. expect(el.innerHTML).toBe('<test>B</test>')
  126. vm.b = 'BB'
  127. Vue.nextTick(function () {
  128. expect(el.innerHTML).toBe('<test>B</test>')
  129. done()
  130. })
  131. })
  132. it('warn non-settable parent path', function (done) {
  133. var vm = new Vue({
  134. el: el,
  135. data: {
  136. b: 'B'
  137. },
  138. template: '<test :b.sync=" b + \'B\'" v-ref:child></test>',
  139. components: {
  140. test: {
  141. props: ['b'],
  142. template: '{{b}}'
  143. }
  144. }
  145. })
  146. expect('Cannot bind two-way prop with non-settable parent path').toHaveBeenWarned()
  147. expect(el.innerHTML).toBe('<test>BB</test>')
  148. vm.b = 'BB'
  149. Vue.nextTick(function () {
  150. expect(el.innerHTML).toBe('<test>BBB</test>')
  151. vm.$refs.child.b = 'hahaha'
  152. Vue.nextTick(function () {
  153. expect(vm.b).toBe('BB')
  154. expect(el.innerHTML).toBe('<test>hahaha</test>')
  155. done()
  156. })
  157. })
  158. })
  159. it('warn expect two-way', function () {
  160. new Vue({
  161. el: el,
  162. template: '<test :test="ok"></test>',
  163. data: {
  164. ok: 'hi'
  165. },
  166. components: {
  167. test: {
  168. props: {
  169. test: {
  170. twoWay: true
  171. }
  172. }
  173. }
  174. }
  175. })
  176. expect('expects a two-way binding type').toHaveBeenWarned()
  177. })
  178. it('warn $data as prop', function () {
  179. new Vue({
  180. el: el,
  181. template: '<test></test>',
  182. data: {
  183. ok: 'hi'
  184. },
  185. components: {
  186. test: {
  187. props: ['$data']
  188. }
  189. }
  190. })
  191. expect('Do not use $data as prop').toHaveBeenWarned()
  192. })
  193. it('warn invalid keys', function () {
  194. new Vue({
  195. el: el,
  196. template: '<test :a.b.c="test"></test>',
  197. components: {
  198. test: {
  199. props: ['a.b.c']
  200. }
  201. }
  202. })
  203. expect('Invalid prop key').toHaveBeenWarned()
  204. })
  205. it('warn props with no el option', function () {
  206. new Vue({
  207. props: ['a']
  208. })
  209. expect('Props will not be compiled if no `el`').toHaveBeenWarned()
  210. })
  211. it('warn object/array default values', function () {
  212. new Vue({
  213. el: el,
  214. props: {
  215. arr: {
  216. type: Array,
  217. default: []
  218. },
  219. obj: {
  220. type: Object,
  221. default: {}
  222. }
  223. }
  224. })
  225. expect('use a factory function to return the default value').toHaveBeenWarned()
  226. expect(getWarnCount()).toBe(2)
  227. })
  228. it('teardown', function (done) {
  229. var vm = new Vue({
  230. el: el,
  231. data: {
  232. a: 'A',
  233. b: 'B'
  234. },
  235. template: '<test :aa.sync="a" :bb="b"></test>',
  236. components: {
  237. test: {
  238. props: ['aa', 'bb'],
  239. template: '{{aa}} {{bb}}'
  240. }
  241. }
  242. })
  243. var child = vm.$children[0]
  244. expect(el.firstChild.textContent).toBe('A B')
  245. child.aa = 'AA'
  246. vm.b = 'BB'
  247. Vue.nextTick(function () {
  248. expect(el.firstChild.textContent).toBe('AA BB')
  249. expect(vm.a).toBe('AA')
  250. // unbind the two props
  251. child._directives[0].unbind()
  252. child._directives[1].unbind()
  253. child.aa = 'AAA'
  254. vm.b = 'BBB'
  255. Vue.nextTick(function () {
  256. expect(el.firstChild.textContent).toBe('AAA BB')
  257. expect(vm.a).toBe('AA')
  258. done()
  259. })
  260. })
  261. })
  262. it('block instance with replace:true', function () {
  263. new Vue({
  264. el: el,
  265. template: '<test :b="a" :c="d"></test>',
  266. data: {
  267. a: 'AAA',
  268. d: 'DDD'
  269. },
  270. components: {
  271. test: {
  272. props: ['b', 'c'],
  273. template: '<p>{{b}}</p><p>{{c}}</p>',
  274. replace: true
  275. }
  276. }
  277. })
  278. expect(el.innerHTML).toBe('<p>AAA</p><p>DDD</p>')
  279. })
  280. describe('assertions', function () {
  281. function makeInstance (value, type, validator, coerce, required) {
  282. return new Vue({
  283. el: document.createElement('div'),
  284. template: '<test :test="val"></test>',
  285. data: {
  286. val: value
  287. },
  288. components: {
  289. test: {
  290. props: {
  291. test: {
  292. type: type,
  293. validator: validator,
  294. coerce: coerce,
  295. required: required
  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('Expected String').toHaveBeenWarned()
  307. })
  308. it('number', function () {
  309. makeInstance(123, Number)
  310. expect(getWarnCount()).toBe(0)
  311. makeInstance('123', Number)
  312. expect('Expected Number').toHaveBeenWarned()
  313. })
  314. it('boolean', function () {
  315. makeInstance(true, Boolean)
  316. expect(getWarnCount()).toBe(0)
  317. makeInstance('123', Boolean)
  318. expect('Expected Boolean').toHaveBeenWarned()
  319. })
  320. it('function', function () {
  321. makeInstance(function () {}, Function)
  322. expect(getWarnCount()).toBe(0)
  323. makeInstance(123, Function)
  324. expect('Expected Function').toHaveBeenWarned()
  325. })
  326. it('object', function () {
  327. makeInstance({}, Object)
  328. expect(getWarnCount()).toBe(0)
  329. makeInstance([], Object)
  330. expect('Expected Object').toHaveBeenWarned()
  331. })
  332. it('array', function () {
  333. makeInstance([], Array)
  334. expect(getWarnCount()).toBe(0)
  335. makeInstance({}, Array)
  336. expect('Expected Array').toHaveBeenWarned()
  337. })
  338. it('custom constructor', function () {
  339. function Class () {}
  340. makeInstance(new Class(), Class)
  341. expect(getWarnCount()).toBe(0)
  342. makeInstance({}, Class)
  343. expect('Expected custom type').toHaveBeenWarned()
  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('custom validator check failed').toHaveBeenWarned()
  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('custom validator check failed').toHaveBeenWarned()
  364. makeInstance(123, String, function (v) {
  365. return v === 123
  366. })
  367. expect('Expected String').toHaveBeenWarned()
  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('Missing required prop').toHaveBeenWarned()
  392. })
  393. it('optional with type + null/undefined', function () {
  394. makeInstance(undefined, String)
  395. expect(getWarnCount()).toBe(0)
  396. makeInstance(null, String)
  397. expect(getWarnCount()).toBe(0)
  398. })
  399. it('required with type + null/undefined', function () {
  400. makeInstance(undefined, String, null, null, true)
  401. expect(getWarnCount()).toBe(1)
  402. expect('Expected String').toHaveBeenWarned()
  403. makeInstance(null, Boolean, null, null, true)
  404. expect(getWarnCount()).toBe(2)
  405. expect('Expected Boolean').toHaveBeenWarned()
  406. })
  407. })
  408. it('alternative syntax', function () {
  409. new Vue({
  410. el: el,
  411. template: '<test :b="a" :c="d"></test>',
  412. data: {
  413. a: 'AAA',
  414. d: 'DDD'
  415. },
  416. components: {
  417. test: {
  418. props: {
  419. b: String,
  420. c: {
  421. type: Number
  422. },
  423. d: {
  424. required: true
  425. }
  426. },
  427. template: '<p>{{b}}</p><p>{{c}}</p>'
  428. }
  429. }
  430. })
  431. expect('Missing required prop').toHaveBeenWarned()
  432. expect('Expected Number').toHaveBeenWarned()
  433. expect(el.textContent).toBe('AAA')
  434. })
  435. it('mixed syntax', function () {
  436. new Vue({
  437. el: el,
  438. template: '<test :b="a" :c="d"></test>',
  439. data: {
  440. a: 'AAA',
  441. d: 'DDD'
  442. },
  443. components: {
  444. test: {
  445. props: [
  446. 'b',
  447. {
  448. name: 'c',
  449. type: Number
  450. },
  451. {
  452. name: 'd',
  453. required: true
  454. }
  455. ],
  456. template: '<p>{{b}}</p><p>{{c}}</p>'
  457. }
  458. }
  459. })
  460. expect('Missing required prop').toHaveBeenWarned()
  461. expect('Expected Number').toHaveBeenWarned()
  462. expect(el.textContent).toBe('AAA')
  463. })
  464. it('should respect default value of a Boolean prop', function () {
  465. var vm = new Vue({
  466. el: el,
  467. template: '<test></test>',
  468. components: {
  469. test: {
  470. props: {
  471. prop: {
  472. type: Boolean,
  473. default: true
  474. }
  475. },
  476. template: '{{prop}}'
  477. }
  478. }
  479. })
  480. expect(vm.$el.textContent).toBe('true')
  481. })
  482. it('should initialize with default value when not provided & has default data', function (done) {
  483. var vm = new Vue({
  484. el: el,
  485. template: '<test></test>',
  486. components: {
  487. test: {
  488. props: {
  489. prop: {
  490. type: String,
  491. default: 'hello'
  492. },
  493. prop2: {
  494. type: Object,
  495. default: function () {
  496. return { vm: this }
  497. }
  498. }
  499. },
  500. data: function () {
  501. return {
  502. other: 'world'
  503. }
  504. },
  505. template: '{{prop}} {{other}}'
  506. }
  507. }
  508. })
  509. expect(vm.$el.textContent).toBe('hello world')
  510. // object/array default value initializers should be
  511. // called with the correct `this` context
  512. var child = vm.$children[0]
  513. expect(child.prop2.vm).toBe(child)
  514. vm.$children[0].prop = 'bye'
  515. Vue.nextTick(function () {
  516. expect(vm.$el.textContent).toBe('bye world')
  517. done()
  518. })
  519. })
  520. it('should warn data fields already defined as a prop', function () {
  521. var Comp = Vue.extend({
  522. data: function () {
  523. return { a: 123 }
  524. },
  525. props: {
  526. a: null
  527. }
  528. })
  529. new Vue({
  530. el: el,
  531. template: '<comp a="1"></comp>',
  532. components: {
  533. comp: Comp
  534. }
  535. })
  536. expect('already defined as a prop').toHaveBeenWarned()
  537. })
  538. it('should not warn data fields already defined as a prop if it is from instantiation call', function () {
  539. var vm = new Vue({
  540. el: el,
  541. props: {
  542. a: null
  543. },
  544. data: {
  545. a: 123
  546. }
  547. })
  548. expect(getWarnCount()).toBe(0)
  549. expect(vm.a).toBe(123)
  550. })
  551. it('should not warn for non-required, absent prop', function () {
  552. new Vue({
  553. el: el,
  554. template: '<test></test>',
  555. components: {
  556. test: {
  557. props: {
  558. prop: {
  559. type: String
  560. }
  561. }
  562. }
  563. }
  564. })
  565. expect(getWarnCount()).toBe(0)
  566. })
  567. // #1683
  568. it('should properly sync back up when mutating then replace', function (done) {
  569. var vm = new Vue({
  570. el: el,
  571. data: {
  572. items: [1, 2]
  573. },
  574. template: '<comp :items.sync="items"></comp>',
  575. components: {
  576. comp: {
  577. props: ['items']
  578. }
  579. }
  580. })
  581. var child = vm.$children[0]
  582. child.items.push(3)
  583. var newArray = child.items = [4]
  584. Vue.nextTick(function () {
  585. expect(child.items).toBe(newArray)
  586. expect(vm.items).toBe(newArray)
  587. done()
  588. })
  589. })
  590. it('treat boolean props properly', function () {
  591. var vm = new Vue({
  592. el: el,
  593. template: '<comp v-ref:child prop-a prop-b="prop-b"></comp>',
  594. components: {
  595. comp: {
  596. props: {
  597. propA: Boolean,
  598. propB: Boolean,
  599. propC: Boolean
  600. }
  601. }
  602. }
  603. })
  604. expect(vm.$refs.child.propA).toBe(true)
  605. expect(vm.$refs.child.propB).toBe(true)
  606. expect(vm.$refs.child.propC).toBe(false)
  607. })
  608. it('detect possible camelCase prop usage', function () {
  609. new Vue({
  610. el: el,
  611. template: '<comp propA="true" :propB="true" v-bind:propC.sync="true"></comp>',
  612. components: {
  613. comp: {
  614. props: ['propA', 'propB', 'prop-c']
  615. }
  616. }
  617. })
  618. expect(getWarnCount()).toBe(3)
  619. expect('did you mean `prop-a`').toHaveBeenWarned()
  620. expect('did you mean `prop-b`').toHaveBeenWarned()
  621. expect('did you mean `prop-c`').toHaveBeenWarned()
  622. })
  623. it('should use default for undefined values', function () {
  624. var vm = new Vue({
  625. el: el,
  626. template: '<comp :a="a"></comp>',
  627. components: {
  628. comp: {
  629. template: '{{a}}',
  630. props: {
  631. a: {
  632. default: 1
  633. }
  634. }
  635. }
  636. }
  637. })
  638. expect(vm.$el.textContent).toBe('1')
  639. })
  640. it('non reactive values passed down as prop should not be converted', function (done) {
  641. var a = Object.freeze({
  642. nested: {
  643. msg: 'hello'
  644. }
  645. })
  646. var parent = new Vue({
  647. el: el,
  648. template: '<comp :a="a.nested"></comp>',
  649. data: {
  650. a: a
  651. },
  652. components: {
  653. comp: {
  654. props: ['a']
  655. }
  656. }
  657. })
  658. var child = parent.$children[0]
  659. expect(child.a.msg).toBe('hello')
  660. expect(child.a.__ob__).toBeUndefined() // should not be converted
  661. parent.a = Object.freeze({
  662. nested: {
  663. msg: 'yo'
  664. }
  665. })
  666. Vue.nextTick(function () {
  667. expect(child.a.msg).toBe('yo')
  668. expect(child.a.__ob__).toBeUndefined()
  669. done()
  670. })
  671. })
  672. it('inline prop values should be converted', function (done) {
  673. var vm = new Vue({
  674. el: el,
  675. template: '<comp :a="[1, 2, 3]"></comp>',
  676. components: {
  677. comp: {
  678. props: ['a'],
  679. template: '<div v-for="i in a">{{ i }}</div>'
  680. }
  681. }
  682. })
  683. expect(vm.$el.textContent).toBe('123')
  684. vm.$children[0].a.pop()
  685. Vue.nextTick(function () {
  686. expect(vm.$el.textContent).toBe('12')
  687. done()
  688. })
  689. })
  690. // #2549
  691. it('mutating child prop binding should be reactive', function (done) {
  692. var vm = new Vue({
  693. el: el,
  694. template: '<comp :list="list"></comp>',
  695. data: {
  696. list: [1, 2, 3]
  697. },
  698. components: {
  699. comp: {
  700. props: ['list'],
  701. template: '<div v-for="i in list">{{ i }}</div>',
  702. created: function () {
  703. this.list = [2, 3, 4]
  704. }
  705. }
  706. }
  707. })
  708. expect(vm.$el.textContent).toBe('234')
  709. vm.$children[0].list.push(5)
  710. Vue.nextTick(function () {
  711. expect(vm.$el.textContent).toBe('2345')
  712. done()
  713. })
  714. })
  715. it('prop default value should be reactive', function (done) {
  716. var vm = new Vue({
  717. el: el,
  718. template: '<comp :list="list"></comp>',
  719. data: {
  720. list: undefined
  721. },
  722. components: {
  723. comp: {
  724. props: {
  725. list: {
  726. default: function () {
  727. return [2, 3, 4]
  728. }
  729. }
  730. },
  731. template: '<div v-for="i in list">{{ i }}</div>'
  732. }
  733. }
  734. })
  735. expect(vm.$el.textContent).toBe('234')
  736. vm.$children[0].list.push(5)
  737. Vue.nextTick(function () {
  738. expect(vm.$el.textContent).toBe('2345')
  739. done()
  740. })
  741. })
  742. })