model_spec.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. var _ = require('../../../../src/util')
  2. var Vue = require('../../../../src/vue')
  3. // unset jQuery to bypass jQuery check for normal test cases
  4. jQuery = null
  5. /**
  6. * Mock event helper
  7. */
  8. function trigger (target, event, process) {
  9. var e = document.createEvent('HTMLEvents')
  10. e.initEvent(event, true, true)
  11. if (process) process(e)
  12. target.dispatchEvent(e)
  13. }
  14. /**
  15. * setting <select>'s value in IE9 doesn't work
  16. * we have to manually loop through the options
  17. */
  18. function updateSelect (el, value) {
  19. /* jshint eqeqeq: false */
  20. var options = el.options
  21. var i = options.length
  22. while (i--) {
  23. if (options[i].value == value) {
  24. options[i].selected = true
  25. break
  26. }
  27. }
  28. }
  29. if (_.inBrowser) {
  30. describe('v-model', function () {
  31. var el
  32. beforeEach(function () {
  33. el = document.createElement('div')
  34. el.style.display = 'none'
  35. document.body.appendChild(el)
  36. spyOn(_, 'warn')
  37. })
  38. it('radio buttons', function (done) {
  39. var vm = new Vue({
  40. el: el,
  41. data: {
  42. test: 'a'
  43. },
  44. template:
  45. '<input type="radio" value="a" v-model="test" name="test">' +
  46. '<input type="radio" value="b" v-model="test" name="test">'
  47. })
  48. expect(el.childNodes[0].checked).toBe(true)
  49. expect(el.childNodes[1].checked).toBe(false)
  50. vm.test = 'b'
  51. _.nextTick(function () {
  52. expect(el.childNodes[0].checked).toBe(false)
  53. expect(el.childNodes[1].checked).toBe(true)
  54. el.childNodes[0].click()
  55. expect(el.childNodes[0].checked).toBe(true)
  56. expect(el.childNodes[1].checked).toBe(false)
  57. expect(vm.test).toBe('a')
  58. vm._directives[1].unbind()
  59. el.childNodes[1].click()
  60. expect(vm.test).toBe('a')
  61. done()
  62. })
  63. })
  64. it('radio default value', function () {
  65. var vm = new Vue({
  66. el: el,
  67. data: {},
  68. template: '<input type="radio" checked value="a" v-model="test">'
  69. })
  70. expect(vm.test).toBe('a')
  71. })
  72. it('checkbox', function (done) {
  73. var vm = new Vue({
  74. el: el,
  75. data: {
  76. test: true
  77. },
  78. template: '<input type="checkbox" v-model="test">'
  79. })
  80. expect(el.firstChild.checked).toBe(true)
  81. vm.test = false
  82. _.nextTick(function () {
  83. expect(el.firstChild.checked).toBe(false)
  84. expect(vm.test).toBe(false)
  85. el.firstChild.click()
  86. expect(el.firstChild.checked).toBe(true)
  87. expect(vm.test).toBe(true)
  88. vm._directives[0].unbind()
  89. el.firstChild.click()
  90. expect(el.firstChild.checked).toBe(false)
  91. expect(vm.test).toBe(true)
  92. done()
  93. })
  94. })
  95. it('checkbox default value', function () {
  96. var vm = new Vue({
  97. el: el,
  98. data: {},
  99. template: '<input type="checkbox" checked v-model="test">'
  100. })
  101. expect(vm.test).toBe(true)
  102. })
  103. it('select', function (done) {
  104. var vm = new Vue({
  105. el: el,
  106. data: {
  107. test: 'b'
  108. },
  109. template:
  110. '<select v-model="test">' +
  111. '<option>a</option>' +
  112. '<option>b</option>' +
  113. '<option>c</option>' +
  114. '</select>'
  115. })
  116. expect(vm.test).toBe('b')
  117. expect(el.firstChild.value).toBe('b')
  118. expect(el.firstChild.childNodes[1].selected).toBe(true)
  119. vm.test = 'c'
  120. _.nextTick(function () {
  121. expect(el.firstChild.value).toBe('c')
  122. expect(el.firstChild.childNodes[2].selected).toBe(true)
  123. updateSelect(el.firstChild, 'a')
  124. trigger(el.firstChild, 'change')
  125. expect(vm.test).toBe('a')
  126. done()
  127. })
  128. })
  129. it('select default value', function () {
  130. var vm = new Vue({
  131. el: el,
  132. data: {
  133. test: 'a'
  134. },
  135. template:
  136. '<select v-model="test">' +
  137. '<option>a</option>' +
  138. '<option selected>b</option>' +
  139. '</select>'
  140. })
  141. expect(vm.test).toBe('b')
  142. expect(el.firstChild.value).toBe('b')
  143. expect(el.firstChild.childNodes[1].selected).toBe(true)
  144. })
  145. it('select + empty default value', function () {
  146. var vm = new Vue({
  147. el: el,
  148. template: '<select v-model="test"><option value="" selected>null</option><<option value="1">1</option></select>'
  149. })
  150. expect(vm.test).toBe('')
  151. trigger(vm.$el.firstChild, 'change')
  152. expect(vm.test).toBe('')
  153. })
  154. it('select + multiple', function (done) {
  155. var vm = new Vue({
  156. el: el,
  157. data: {
  158. test: [2] // test number soft equal
  159. },
  160. template:
  161. '<select v-model="test" multiple>' +
  162. '<option>1</option>' +
  163. '<option>2</option>' +
  164. '<option>3</option>' +
  165. '</select>'
  166. })
  167. var opts = el.firstChild.options
  168. expect(opts[0].selected).toBe(false)
  169. expect(opts[1].selected).toBe(true)
  170. expect(opts[2].selected).toBe(false)
  171. vm.test = [1, '3'] // mix of number/string
  172. _.nextTick(function () {
  173. expect(opts[0].selected).toBe(true)
  174. expect(opts[1].selected).toBe(false)
  175. expect(opts[2].selected).toBe(true)
  176. opts[0].selected = false
  177. opts[1].selected = true
  178. trigger(el.firstChild, 'change')
  179. expect(vm.test[0]).toBe('2')
  180. expect(vm.test[1]).toBe('3')
  181. done()
  182. })
  183. })
  184. it('select + multiple default value', function () {
  185. var vm = new Vue({
  186. el: el,
  187. data: {},
  188. template:
  189. '<select v-model="test" multiple>' +
  190. '<option>a</option>' +
  191. '<option selected>b</option>' +
  192. '<option selected>c</option>' +
  193. '</select>'
  194. })
  195. expect(vm.test[0]).toBe('b')
  196. expect(vm.test[1]).toBe('c')
  197. })
  198. it('select + options', function (done) {
  199. var vm = new Vue({
  200. el: el,
  201. data: {
  202. test: 'b',
  203. opts: ['a', 'b', 'c']
  204. },
  205. template: '<select v-model="test" options="opts"></select>'
  206. })
  207. var opts = el.firstChild.options
  208. expect(opts.length).toBe(3)
  209. expect(opts[0].selected).toBe(false)
  210. expect(opts[1].selected).toBe(true)
  211. expect(opts[2].selected).toBe(false)
  212. vm.opts = ['b', 'c']
  213. _.nextTick(function () {
  214. expect(opts.length).toBe(2)
  215. expect(opts[0].selected).toBe(true)
  216. expect(opts[1].selected).toBe(false)
  217. // should teardown option watcher when unbind
  218. expect(vm._watcherList.length).toBe(2)
  219. vm._directives[0]._teardown()
  220. expect(vm._watcherList.length).toBe(0)
  221. done()
  222. })
  223. })
  224. it('select + options + text', function () {
  225. var vm = new Vue({
  226. el: el,
  227. data: {
  228. test: 'b',
  229. opts: [
  230. { text: 'Select an option', value: null, disabled: true },
  231. { text: 'A', value: 'a' },
  232. { text: 'B', value: 'b' }
  233. ]
  234. },
  235. template: '<select v-model="test" options="opts"></select>'
  236. })
  237. expect(el.firstChild.innerHTML).toBe(
  238. '<option disabled="">Select an option</option>' +
  239. '<option value="a">A</option>' +
  240. '<option value="b">B</option>'
  241. )
  242. var opts = el.firstChild.options
  243. expect(opts[0].disabled).toBe(true)
  244. expect(opts[0].selected).toBe(false)
  245. expect(opts[1].selected).toBe(false)
  246. expect(opts[2].selected).toBe(true)
  247. })
  248. it('select + options + optgroup', function () {
  249. var vm = new Vue({
  250. el: el,
  251. data: {
  252. test: 'b',
  253. opts: [
  254. { label: 'A', options: ['a','b'] },
  255. { label: 'B', options: ['c'] }
  256. ]
  257. },
  258. template: '<select v-model="test" options="opts"></select>'
  259. })
  260. expect(el.firstChild.innerHTML).toBe(
  261. '<optgroup label="A">' +
  262. '<option value="a">a</option><option value="b">b</option>' +
  263. '</optgroup>' +
  264. '<optgroup label="B">' +
  265. '<option value="c">c</option>' +
  266. '</optgroup>'
  267. )
  268. var opts = el.firstChild.options
  269. expect(opts[0].selected).toBe(false)
  270. expect(opts[1].selected).toBe(true)
  271. expect(opts[2].selected).toBe(false)
  272. })
  273. it('select + number', function () {
  274. var vm = new Vue({
  275. el: el,
  276. data: {
  277. test: '1'
  278. },
  279. template: '<select v-model="test" number><option value="1">1</option></select>'
  280. })
  281. expect(vm.test).toBe('1')
  282. trigger(vm.$el.firstChild, 'change')
  283. expect(vm.test).toBe(1)
  284. })
  285. it('select + number + multiple', function () {
  286. var vm = new Vue({
  287. el: el,
  288. data: {
  289. test: []
  290. },
  291. template: '<select v-model="test" multiple number><option>1</option><option>2</option></select>'
  292. })
  293. ;[].forEach.call(el.querySelectorAll('option'), function (o) {
  294. o.selected = true
  295. })
  296. trigger(el.firstChild, 'change')
  297. expect(vm.test[0]).toBe(1)
  298. expect(vm.test[1]).toBe(2)
  299. })
  300. it('select + number initial value', function () {
  301. var vm = new Vue({
  302. el: el,
  303. data: {
  304. test: '1'
  305. },
  306. template: '<select v-model="test" number><option value="1" selected>1</option></select>'
  307. })
  308. expect(vm.test).toBe(1)
  309. })
  310. it('select + options + filter', function () {
  311. var vm = new Vue({
  312. el: el,
  313. data: {
  314. opts: ['a','b']
  315. },
  316. filters: {
  317. aFilter: function (opts){
  318. return opts.map(function (val,i){
  319. return val + i
  320. })
  321. }
  322. },
  323. template: '<select v-model="test" options="opts | aFilter"></select>'
  324. })
  325. expect(el.firstChild.innerHTML).toBe(
  326. '<option value="a0">a0</option>' +
  327. '<option value="b1">b1</option>'
  328. )
  329. })
  330. it('text', function (done) {
  331. var vm = new Vue({
  332. el: el,
  333. data: {
  334. test: 'b'
  335. },
  336. template: '<input v-model="test">'
  337. })
  338. expect(el.firstChild.value).toBe('b')
  339. vm.test = 'a'
  340. _.nextTick(function () {
  341. expect(el.firstChild.value).toBe('a')
  342. el.firstChild.value = 'c'
  343. trigger(el.firstChild, 'input')
  344. expect(vm.test).toBe('c')
  345. vm._directives[0].unbind()
  346. el.firstChild.value = 'd'
  347. trigger(el.firstChild, 'input')
  348. expect(vm.test).toBe('c')
  349. done()
  350. })
  351. })
  352. it('text default value', function () {
  353. var vm = new Vue({
  354. el: el,
  355. data: {
  356. test: 'b'
  357. },
  358. template: '<input v-model="test | test" value="a">',
  359. filters: {
  360. test: {
  361. read: function (v) {
  362. return v.slice(0, -1)
  363. },
  364. write: function (v) {
  365. return v + 'c'
  366. }
  367. }
  368. }
  369. })
  370. expect(vm.test).toBe('ac')
  371. expect(el.firstChild.value).toBe('a')
  372. })
  373. it('text lazy', function () {
  374. var vm = new Vue({
  375. el: el,
  376. data: {
  377. test: 'b'
  378. },
  379. template: '<input v-model="test" lazy>'
  380. })
  381. expect(el.firstChild.value).toBe('b')
  382. expect(vm.test).toBe('b')
  383. el.firstChild.value = 'c'
  384. trigger(el.firstChild, 'input')
  385. expect(vm.test).toBe('b')
  386. trigger(el.firstChild, 'change')
  387. expect(vm.test).toBe('c')
  388. })
  389. it('text with filters', function (done) {
  390. var vm = new Vue({
  391. el: el,
  392. data: {
  393. test: 'b'
  394. },
  395. filters: {
  396. test: {
  397. write: function (val) {
  398. return val.toLowerCase()
  399. }
  400. }
  401. },
  402. template: '<input v-model="test | uppercase | test">'
  403. })
  404. expect(el.firstChild.value).toBe('B')
  405. el.firstChild.value = 'cc'
  406. trigger(el.firstChild, 'input')
  407. _.nextTick(function () {
  408. expect(el.firstChild.value).toBe('CC')
  409. expect(vm.test).toBe('cc')
  410. done()
  411. })
  412. })
  413. // when there's only write filter, should allow
  414. // out of sync between the input field and actual data
  415. it('text with only write filter', function (done) {
  416. var vm = new Vue({
  417. el: el,
  418. data: {
  419. test: 'b'
  420. },
  421. filters: {
  422. test: {
  423. write: function (val) {
  424. return val.toUpperCase()
  425. }
  426. }
  427. },
  428. template: '<input v-model="test | test">'
  429. })
  430. el.firstChild.value = 'cc'
  431. trigger(el.firstChild, 'input')
  432. _.nextTick(function () {
  433. expect(el.firstChild.value).toBe('cc')
  434. expect(vm.test).toBe('CC')
  435. done()
  436. })
  437. })
  438. it('number', function () {
  439. var vm = new Vue({
  440. el: el,
  441. data: {
  442. test: 1
  443. },
  444. template: '<input v-model="test" value="2" number>'
  445. })
  446. expect(vm.test).toBe(2)
  447. el.firstChild.value = 3
  448. trigger(el.firstChild, 'input')
  449. expect(vm.test).toBe(3)
  450. })
  451. it('IE9 cut and delete', function (done) {
  452. var ie9 = _.isIE9
  453. _.isIE9 = true
  454. var vm = new Vue({
  455. el: el,
  456. data: {
  457. test: 'aaa'
  458. },
  459. template: '<input v-model="test">'
  460. })
  461. var input = el.firstChild
  462. input.value = 'aa'
  463. trigger(input, 'cut')
  464. _.nextTick(function () {
  465. expect(vm.test).toBe('aa')
  466. input.value = 'a'
  467. trigger(input, 'keyup', function (e) {
  468. e.keyCode = 8
  469. })
  470. expect(vm.test).toBe('a')
  471. // teardown
  472. vm._directives[0].unbind()
  473. input.value = 'bbb'
  474. trigger(input, 'keyup', function (e) {
  475. e.keyCode = 8
  476. })
  477. expect(vm.test).toBe('a')
  478. _.isIE9 = ie9
  479. done()
  480. })
  481. })
  482. it('text + compositionevents', function (done) {
  483. var vm = new Vue({
  484. el: el,
  485. data: {
  486. test: 'aaa',
  487. test2: 'bbb'
  488. },
  489. template: '<input v-model="test"><input v-model="test2 | uppercase">'
  490. })
  491. var input = el.firstChild
  492. var input2 = el.childNodes[1]
  493. trigger(input, 'compositionstart')
  494. trigger(input2, 'compositionstart')
  495. input.value = input2.value = 'ccc'
  496. // input before composition unlock should not call set
  497. trigger(input, 'input')
  498. trigger(input2, 'input')
  499. expect(vm.test).toBe('aaa')
  500. expect(vm.test2).toBe('bbb')
  501. // after composition unlock it should work
  502. trigger(input, 'compositionend')
  503. trigger(input2, 'compositionend')
  504. trigger(input, 'input')
  505. trigger(input2, 'input')
  506. expect(vm.test).toBe('ccc')
  507. expect(vm.test2).toBe('ccc')
  508. // IE complains about "unspecified error" when calling
  509. // setSelectionRange() on an input element that's been
  510. // removed from the DOM, so we wait until the
  511. // selection range callback has fired to end this test.
  512. _.nextTick(done)
  513. })
  514. it('textarea', function () {
  515. var vm = new Vue({
  516. el: el,
  517. data: {
  518. test: 'b',
  519. b: 'BB'
  520. },
  521. template: '<textarea v-model="test">a {{b}} c</textarea>'
  522. })
  523. expect(vm.test).toBe('a BB c')
  524. expect(el.firstChild.value).toBe('a BB c')
  525. })
  526. it('warn invalid tag', function () {
  527. var vm = new Vue({
  528. el: el,
  529. template: '<div v-model="test"></div>'
  530. })
  531. expect(hasWarned(_, 'does not support element type')).toBe(true)
  532. })
  533. it('warn invalid option value', function () {
  534. var vm = new Vue({
  535. el: el,
  536. data: { a: 123 },
  537. template: '<select v-model="test" options="a"></select>'
  538. })
  539. expect(hasWarned(_, 'Invalid options value')).toBe(true)
  540. })
  541. it('warn read-only filters', function () {
  542. var vm = new Vue({
  543. el: el,
  544. template: '<input v-model="abc | test">',
  545. filters: {
  546. test: function (v) {
  547. return v
  548. }
  549. }
  550. })
  551. expect(hasWarned(_, 'read-only filter')).toBe(true)
  552. })
  553. it('support jQuery change event', function (done) {
  554. // restore jQuery
  555. jQuery = $
  556. var vm = new Vue({
  557. el: el,
  558. data: {
  559. test: 'b'
  560. },
  561. template: '<input v-model="test" lazy>'
  562. })
  563. expect(el.firstChild.value).toBe('b')
  564. vm.test = 'a'
  565. _.nextTick(function () {
  566. expect(el.firstChild.value).toBe('a')
  567. el.firstChild.value = 'c'
  568. jQuery(el.firstChild).trigger('change')
  569. expect(vm.test).toBe('c')
  570. vm._directives[0].unbind()
  571. el.firstChild.value = 'd'
  572. jQuery(el.firstChild).trigger('change')
  573. expect(vm.test).toBe('c')
  574. // unset jQuery
  575. jQuery = null
  576. done()
  577. })
  578. })
  579. it('support debounce', function (done) {
  580. var spy = jasmine.createSpy()
  581. var vm = new Vue({
  582. el: el,
  583. data: {
  584. test: 'a'
  585. },
  586. watch: {
  587. test: spy
  588. },
  589. template: '<input v-model="test" debounce="100">'
  590. })
  591. el.firstChild.value = 'b'
  592. trigger(el.firstChild, 'input')
  593. setTimeout(function () {
  594. el.firstChild.value = 'c'
  595. trigger(el.firstChild, 'input')
  596. }, 10)
  597. setTimeout(function () {
  598. el.firstChild.value = 'd'
  599. trigger(el.firstChild, 'input')
  600. }, 20)
  601. setTimeout(function () {
  602. expect(spy.calls.count()).toBe(0)
  603. expect(vm.test).toBe('a')
  604. }, 30)
  605. setTimeout(function () {
  606. expect(spy.calls.count()).toBe(1)
  607. expect(vm.test).toBe('d')
  608. done()
  609. }, 200)
  610. })
  611. })
  612. }