model-select.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. import Vue from 'vue'
  2. import { looseEqual } from 'shared/util'
  3. // Android 4.4 Chrome 30 has the bug that a multi-select option cannot be
  4. // deselected by setting its "selected" prop via JavaScript.
  5. function hasMultiSelectBug () {
  6. const s = document.createElement('select')
  7. s.setAttribute('multiple', '')
  8. const o = document.createElement('option')
  9. s.appendChild(o)
  10. o.selected = true
  11. o.selected = false
  12. return o.selected !== false
  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. const options = el.options
  20. let i = options.length
  21. while (i--) {
  22. if (looseEqual(getValue(options[i]), value)) {
  23. options[i].selected = true
  24. break
  25. }
  26. }
  27. }
  28. function getValue (option) {
  29. return '_value' in option
  30. ? option._value
  31. : option.value || option.text
  32. }
  33. describe('Directive v-model select', () => {
  34. it('should work', done => {
  35. const vm = new Vue({
  36. data: {
  37. test: 'b'
  38. },
  39. template:
  40. '<select v-model="test">' +
  41. '<option>a</option>' +
  42. '<option>b</option>' +
  43. '<option>c</option>' +
  44. '</select>'
  45. }).$mount()
  46. document.body.appendChild(vm.$el)
  47. expect(vm.test).toBe('b')
  48. expect(vm.$el.value).toBe('b')
  49. expect(vm.$el.childNodes[1].selected).toBe(true)
  50. vm.test = 'c'
  51. waitForUpdate(function () {
  52. expect(vm.$el.value).toBe('c')
  53. expect(vm.$el.childNodes[2].selected).toBe(true)
  54. updateSelect(vm.$el, 'a')
  55. triggerEvent(vm.$el, 'change')
  56. expect(vm.test).toBe('a')
  57. }).then(done)
  58. })
  59. it('should work with value bindings', done => {
  60. const vm = new Vue({
  61. data: {
  62. test: 2
  63. },
  64. template:
  65. '<select v-model="test">' +
  66. '<option value="1">a</option>' +
  67. '<option :value="2">b</option>' +
  68. '<option :value="3">c</option>' +
  69. '</select>'
  70. }).$mount()
  71. document.body.appendChild(vm.$el)
  72. expect(vm.$el.value).toBe('2')
  73. expect(vm.$el.childNodes[1].selected).toBe(true)
  74. vm.test = 3
  75. waitForUpdate(function () {
  76. expect(vm.$el.value).toBe('3')
  77. expect(vm.$el.childNodes[2].selected).toBe(true)
  78. updateSelect(vm.$el, '1')
  79. triggerEvent(vm.$el, 'change')
  80. expect(vm.test).toBe('1')
  81. updateSelect(vm.$el, '2')
  82. triggerEvent(vm.$el, 'change')
  83. expect(vm.test).toBe(2)
  84. }).then(done)
  85. })
  86. it('should work with value bindings (object loose equal)', done => {
  87. const vm = new Vue({
  88. data: {
  89. test: { a: 2 }
  90. },
  91. template:
  92. '<select v-model="test">' +
  93. '<option value="1">a</option>' +
  94. '<option :value="{ a: 2 }">b</option>' +
  95. '<option :value="{ a: 3 }">c</option>' +
  96. '</select>'
  97. }).$mount()
  98. document.body.appendChild(vm.$el)
  99. expect(vm.$el.childNodes[1].selected).toBe(true)
  100. vm.test = { a: 3 }
  101. waitForUpdate(function () {
  102. expect(vm.$el.childNodes[2].selected).toBe(true)
  103. updateSelect(vm.$el, '1')
  104. triggerEvent(vm.$el, 'change')
  105. expect(vm.test).toBe('1')
  106. updateSelect(vm.$el, { a: 2 })
  107. triggerEvent(vm.$el, 'change')
  108. expect(vm.test).toEqual({ a: 2 })
  109. }).then(done)
  110. })
  111. it('should work with value bindings (Array loose equal)', done => {
  112. const vm = new Vue({
  113. data: {
  114. test: [{ a: 2 }]
  115. },
  116. template:
  117. '<select v-model="test">' +
  118. '<option value="1">a</option>' +
  119. '<option :value="[{ a: 2 }]">b</option>' +
  120. '<option :value="[{ a: 3 }]">c</option>' +
  121. '</select>'
  122. }).$mount()
  123. document.body.appendChild(vm.$el)
  124. expect(vm.$el.childNodes[1].selected).toBe(true)
  125. vm.test = [{ a: 3 }]
  126. waitForUpdate(function () {
  127. expect(vm.$el.childNodes[2].selected).toBe(true)
  128. updateSelect(vm.$el, '1')
  129. triggerEvent(vm.$el, 'change')
  130. expect(vm.test).toBe('1')
  131. updateSelect(vm.$el, [{ a: 2 }])
  132. triggerEvent(vm.$el, 'change')
  133. expect(vm.test).toEqual([{ a: 2 }])
  134. }).then(done)
  135. })
  136. it('should work with v-for', done => {
  137. const vm = new Vue({
  138. data: {
  139. test: 'b',
  140. opts: ['a', 'b', 'c']
  141. },
  142. template:
  143. '<select v-model="test">' +
  144. '<option v-for="o in opts">{{ o }}</option>' +
  145. '</select>'
  146. }).$mount()
  147. document.body.appendChild(vm.$el)
  148. expect(vm.test).toBe('b')
  149. expect(vm.$el.value).toBe('b')
  150. expect(vm.$el.childNodes[1].selected).toBe(true)
  151. vm.test = 'c'
  152. waitForUpdate(function () {
  153. expect(vm.$el.value).toBe('c')
  154. expect(vm.$el.childNodes[2].selected).toBe(true)
  155. updateSelect(vm.$el, 'a')
  156. triggerEvent(vm.$el, 'change')
  157. expect(vm.test).toBe('a')
  158. // update v-for opts
  159. vm.opts = ['d', 'a']
  160. }).then(() => {
  161. expect(vm.$el.childNodes[0].selected).toBe(false)
  162. expect(vm.$el.childNodes[1].selected).toBe(true)
  163. }).then(done)
  164. })
  165. it('should work with v-for & value bindings', done => {
  166. const vm = new Vue({
  167. data: {
  168. test: 2,
  169. opts: [1, 2, 3]
  170. },
  171. template:
  172. '<select v-model="test">' +
  173. '<option v-for="o in opts" :value="o">option {{ o }}</option>' +
  174. '</select>'
  175. }).$mount()
  176. document.body.appendChild(vm.$el)
  177. expect(vm.$el.value).toBe('2')
  178. expect(vm.$el.childNodes[1].selected).toBe(true)
  179. vm.test = 3
  180. waitForUpdate(function () {
  181. expect(vm.$el.value).toBe('3')
  182. expect(vm.$el.childNodes[2].selected).toBe(true)
  183. updateSelect(vm.$el, 1)
  184. triggerEvent(vm.$el, 'change')
  185. expect(vm.test).toBe(1)
  186. // update v-for opts
  187. vm.opts = [0, 1]
  188. }).then(() => {
  189. expect(vm.$el.childNodes[0].selected).toBe(false)
  190. expect(vm.$el.childNodes[1].selected).toBe(true)
  191. }).then(done)
  192. })
  193. it('should work with select which has no default selected options', (done) => {
  194. const spy = vi.fn()
  195. const vm = new Vue({
  196. data: {
  197. id: 4,
  198. list: [1, 2, 3],
  199. testChange: 5
  200. },
  201. template:
  202. '<div>' +
  203. '<select @change="test" v-model="id">' +
  204. '<option v-for="item in list" :value="item">{{item}}</option>' +
  205. '</select>' +
  206. '{{testChange}}' +
  207. '</div>',
  208. methods: {
  209. test: spy
  210. }
  211. }).$mount()
  212. document.body.appendChild(vm.$el)
  213. vm.testChange = 10
  214. waitForUpdate(() => {
  215. expect(spy.mock.calls.length).toBe(0)
  216. }).then(done)
  217. })
  218. if (!hasMultiSelectBug()) {
  219. it('multiple', done => {
  220. const vm = new Vue({
  221. data: {
  222. test: ['b']
  223. },
  224. template:
  225. '<select v-model="test" multiple>' +
  226. '<option>a</option>' +
  227. '<option>b</option>' +
  228. '<option>c</option>' +
  229. '</select>'
  230. }).$mount()
  231. const opts = vm.$el.options
  232. expect(opts[0].selected).toBe(false)
  233. expect(opts[1].selected).toBe(true)
  234. expect(opts[2].selected).toBe(false)
  235. vm.test = ['a', 'c']
  236. waitForUpdate(() => {
  237. expect(opts[0].selected).toBe(true)
  238. expect(opts[1].selected).toBe(false)
  239. expect(opts[2].selected).toBe(true)
  240. opts[0].selected = false
  241. opts[1].selected = true
  242. triggerEvent(vm.$el, 'change')
  243. expect(vm.test).toEqual(['b', 'c'])
  244. }).then(done)
  245. })
  246. it('multiple + v-for', done => {
  247. const vm = new Vue({
  248. data: {
  249. test: ['b'],
  250. opts: ['a', 'b', 'c']
  251. },
  252. template:
  253. '<select v-model="test" multiple>' +
  254. '<option v-for="o in opts">{{ o }}</option>' +
  255. '</select>'
  256. }).$mount()
  257. const opts = vm.$el.options
  258. expect(opts[0].selected).toBe(false)
  259. expect(opts[1].selected).toBe(true)
  260. expect(opts[2].selected).toBe(false)
  261. vm.test = ['a', 'c']
  262. waitForUpdate(() => {
  263. expect(opts[0].selected).toBe(true)
  264. expect(opts[1].selected).toBe(false)
  265. expect(opts[2].selected).toBe(true)
  266. opts[0].selected = false
  267. opts[1].selected = true
  268. triggerEvent(vm.$el, 'change')
  269. expect(vm.test).toEqual(['b', 'c'])
  270. // update v-for opts
  271. vm.opts = ['c', 'd']
  272. }).then(() => {
  273. expect(opts[0].selected).toBe(true)
  274. expect(opts[1].selected).toBe(false)
  275. expect(vm.test).toEqual(['c']) // should remove 'd' which no longer has a matching option
  276. }).then(done)
  277. })
  278. }
  279. it('should work with multiple binding', (done) => {
  280. const spy = vi.fn()
  281. const vm = new Vue({
  282. data: {
  283. isMultiple: true,
  284. selections: ['1']
  285. },
  286. template:
  287. '<select v-model="selections" :multiple="isMultiple">' +
  288. '<option value="1">item 1</option>' +
  289. '<option value="2">item 2</option>' +
  290. '</select>',
  291. watch: {
  292. selections: spy
  293. }
  294. }).$mount()
  295. document.body.appendChild(vm.$el)
  296. vm.$el.options[1].selected = true
  297. triggerEvent(vm.$el, 'change')
  298. waitForUpdate(() => {
  299. expect(spy).toHaveBeenCalled()
  300. expect(vm.selections).toEqual(['1', '2'])
  301. }).then(done)
  302. })
  303. it('should not have multiple attr with falsy values except \'\'', () => {
  304. const vm = new Vue({
  305. template:
  306. '<div>' +
  307. '<select id="undefined" :multiple="undefined"></select>' +
  308. '<select id="null" :multiple="null"></select>' +
  309. '<select id="false" :multiple="false"></select>' +
  310. '<select id="string" :multiple="\'\'"></select>' +
  311. '</div>'
  312. }).$mount()
  313. expect(vm.$el.querySelector('#undefined').multiple).toEqual(false)
  314. expect(vm.$el.querySelector('#null').multiple).toEqual(false)
  315. expect(vm.$el.querySelector('#false').multiple).toEqual(false)
  316. expect(vm.$el.querySelector('#string').multiple).toEqual(true)
  317. })
  318. it('multiple with static template', () => {
  319. const vm = new Vue({
  320. template:
  321. '<select multiple>' +
  322. '<option selected>a</option>' +
  323. '<option selected>b</option>' +
  324. '<option selected>c</option>' +
  325. '</select>'
  326. }).$mount()
  327. const opts = vm.$el.options
  328. expect(opts[0].selected).toBe(true)
  329. expect(opts[1].selected).toBe(true)
  330. expect(opts[2].selected).toBe(true)
  331. })
  332. it('multiple selects', (done) => {
  333. const spy = vi.fn()
  334. const vm = new Vue({
  335. data: {
  336. selections: ['', ''],
  337. selectBoxes: [
  338. [
  339. { value: 'foo', text: 'foo' },
  340. { value: 'bar', text: 'bar' }
  341. ],
  342. [
  343. { value: 'day', text: 'day' },
  344. { value: 'night', text: 'night' }
  345. ]
  346. ]
  347. },
  348. watch: {
  349. selections: spy
  350. },
  351. template:
  352. '<div>' +
  353. '<select v-for="(item, index) in selectBoxes" v-model="selections[index]">' +
  354. '<option v-for="element in item" v-bind:value="element.value" v-text="element.text"></option>' +
  355. '</select>' +
  356. '<span ref="rs">{{selections}}</span>' +
  357. '</div>'
  358. }).$mount()
  359. document.body.appendChild(vm.$el)
  360. const selects = vm.$el.getElementsByTagName('select')
  361. const select0 = selects[0]
  362. select0.options[0].selected = true
  363. triggerEvent(select0, 'change')
  364. waitForUpdate(() => {
  365. expect(spy).toHaveBeenCalled()
  366. expect(vm.selections).toEqual(['foo', ''])
  367. }).then(done)
  368. })
  369. it('.number modifier', () => {
  370. const vm = new Vue({
  371. data: {
  372. test: 2
  373. },
  374. template:
  375. '<select v-model.number="test">' +
  376. '<option value="1">a</option>' +
  377. '<option :value="2">b</option>' +
  378. '<option :value="3">c</option>' +
  379. '</select>'
  380. }).$mount()
  381. document.body.appendChild(vm.$el)
  382. updateSelect(vm.$el, '1')
  383. triggerEvent(vm.$el, 'change')
  384. expect(vm.test).toBe(1)
  385. })
  386. it('should respect different primitive type value', (done) => {
  387. const vm = new Vue({
  388. data: {
  389. test: 0
  390. },
  391. template:
  392. '<select v-model.number="test">' +
  393. '<option value="">a</option>' +
  394. '<option value="0">b</option>' +
  395. '<option value="1">c</option>' +
  396. '<option value="false">c</option>' +
  397. '<option value="true">c</option>' +
  398. '</select>'
  399. }).$mount()
  400. const opts = vm.$el.options
  401. expect(opts[0].selected).toBe(false)
  402. expect(opts[1].selected).toBe(true)
  403. expect(opts[2].selected).toBe(false)
  404. expect(opts[3].selected).toBe(false)
  405. expect(opts[4].selected).toBe(false)
  406. vm.test = 1
  407. waitForUpdate(() => {
  408. expect(opts[0].selected).toBe(false)
  409. expect(opts[1].selected).toBe(false)
  410. expect(opts[2].selected).toBe(true)
  411. expect(opts[3].selected).toBe(false)
  412. expect(opts[4].selected).toBe(false)
  413. vm.test = ''
  414. }).then(() => {
  415. expect(opts[0].selected).toBe(true)
  416. expect(opts[1].selected).toBe(false)
  417. expect(opts[2].selected).toBe(false)
  418. expect(opts[3].selected).toBe(false)
  419. expect(opts[4].selected).toBe(false)
  420. vm.test = false
  421. }).then(() => {
  422. expect(opts[0].selected).toBe(false)
  423. expect(opts[1].selected).toBe(false)
  424. expect(opts[2].selected).toBe(false)
  425. expect(opts[3].selected).toBe(true)
  426. expect(opts[4].selected).toBe(false)
  427. vm.test = true
  428. }).then(() => {
  429. expect(opts[0].selected).toBe(false)
  430. expect(opts[1].selected).toBe(false)
  431. expect(opts[2].selected).toBe(false)
  432. expect(opts[3].selected).toBe(false)
  433. expect(opts[4].selected).toBe(true)
  434. }).then(done)
  435. })
  436. it('should warn multiple with non-Array value', done => {
  437. new Vue({
  438. data: {
  439. test: 'meh'
  440. },
  441. template:
  442. '<select v-model="test" multiple></select>'
  443. }).$mount()
  444. // IE warns on a setTimeout as well
  445. setTimeout(() => {
  446. expect('<select multiple v-model="test"> expects an Array value for its binding, but got String')
  447. .toHaveBeenWarned()
  448. done()
  449. }, 0)
  450. })
  451. it('should work with option value that has circular reference', done => {
  452. const circular = {}
  453. circular.self = circular
  454. const vm = new Vue({
  455. data: {
  456. test: 'b',
  457. circular
  458. },
  459. template:
  460. '<select v-model="test">' +
  461. '<option :value="circular">a</option>' +
  462. '<option>b</option>' +
  463. '<option>c</option>' +
  464. '</select>'
  465. }).$mount()
  466. document.body.appendChild(vm.$el)
  467. expect(vm.test).toBe('b')
  468. expect(vm.$el.value).toBe('b')
  469. expect(vm.$el.childNodes[1].selected).toBe(true)
  470. vm.test = circular
  471. waitForUpdate(function () {
  472. expect(vm.$el.childNodes[0].selected).toBe(true)
  473. }).then(done)
  474. })
  475. // #6112
  476. it('should not set non-matching value to undefined if options did not change', done => {
  477. const vm = new Vue({
  478. data: {
  479. test: '1'
  480. },
  481. template:
  482. '<select v-model="test">' +
  483. '<option>a</option>' +
  484. '</select>'
  485. }).$mount()
  486. vm.test = '2'
  487. waitForUpdate(() => {
  488. expect(vm.test).toBe('2')
  489. }).then(done)
  490. })
  491. // #6193
  492. it('should not trigger change event when matching option can be found for each value', done => {
  493. const spy = vi.fn()
  494. const vm = new Vue({
  495. data: {
  496. options: ['1']
  497. },
  498. computed: {
  499. test: {
  500. get () {
  501. return '1'
  502. },
  503. set () {
  504. spy()
  505. }
  506. }
  507. },
  508. template:
  509. '<select v-model="test">' +
  510. '<option :key="opt" v-for="opt in options" :value="opt">{{ opt }}</option>' +
  511. '</select>'
  512. }).$mount()
  513. vm.options = ['1', '2']
  514. waitForUpdate(() => {
  515. expect(spy).not.toHaveBeenCalled()
  516. }).then(done)
  517. })
  518. // #6903
  519. describe('should correctly handle v-model when the vnodes are the same', () => {
  520. function makeInstance (foo) {
  521. return new Vue({
  522. data: {
  523. foo: foo,
  524. options: ['b', 'c', 'd'],
  525. value: 'c'
  526. },
  527. template:
  528. '<div>' +
  529. '<select v-if="foo" data-attr>' +
  530. '<option selected>a</option>' +
  531. '</select>' +
  532. '<select v-else v-model="value">' +
  533. '<option v-for="option in options" :value="option">{{ option }}</option>' +
  534. '</select>' +
  535. '</div>'
  536. }).$mount()
  537. }
  538. it('register v-model', done => {
  539. const vm = makeInstance(true)
  540. expect(vm.$el.firstChild.selectedIndex).toBe(0)
  541. vm.foo = false
  542. waitForUpdate(() => {
  543. expect(vm.$el.firstChild.selectedIndex).toBe(1)
  544. }).then(done)
  545. })
  546. it('remove v-model', done => {
  547. const vm = makeInstance(false)
  548. expect(vm.$el.firstChild.selectedIndex).toBe(1)
  549. vm.foo = true
  550. waitForUpdate(() => {
  551. expect(vm.$el.firstChild.selectedIndex).toBe(0)
  552. }).then(done)
  553. })
  554. })
  555. // #7928
  556. it('should correctly handle option with date value', done => {
  557. const vm = new Vue({
  558. data: {
  559. dates: [
  560. new Date(1520000000000),
  561. new Date(1522000000000),
  562. new Date(1516000000000)
  563. ],
  564. selectedDate: null
  565. },
  566. template:
  567. '<div>' +
  568. '<select v-model="selectedDate">' +
  569. '<option v-for="(date, i) in dates" :key="i" :value="date">' +
  570. '{{date}}' +
  571. '</option>' +
  572. '</select>' +
  573. '</div>'
  574. }).$mount()
  575. vm.selectedDate = vm.dates[2]
  576. waitForUpdate(() => {
  577. expect(vm.$el.firstChild.selectedIndex).toBe(2)
  578. }).then(done)
  579. })
  580. })