model-select.spec.ts 17 KB

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