model-text.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. import Vue from 'vue'
  2. import { isIE9, isIE, isAndroid } from 'core/util/env'
  3. describe('Directive v-model text', () => {
  4. it('should update value both ways', done => {
  5. const vm = new Vue({
  6. data: {
  7. test: 'b'
  8. },
  9. template: '<input v-model="test">'
  10. }).$mount()
  11. expect(vm.$el.value).toBe('b')
  12. vm.test = 'a'
  13. waitForUpdate(() => {
  14. expect(vm.$el.value).toBe('a')
  15. vm.$el.value = 'c'
  16. triggerEvent(vm.$el, 'input')
  17. expect(vm.test).toBe('c')
  18. }).then(done)
  19. })
  20. it('should work with space ended expression in v-model', () => {
  21. const vm = new Vue({
  22. data: {
  23. obj: {
  24. test: 'b'
  25. }
  26. },
  27. template: '<input v-model="obj.test ">'
  28. }).$mount()
  29. triggerEvent(vm.$el, 'input')
  30. expect(vm.obj['test ']).toBe(undefined)
  31. expect(vm.obj.test).toBe('b')
  32. })
  33. it('.lazy modifier', () => {
  34. const vm = new Vue({
  35. data: {
  36. test: 'b'
  37. },
  38. template: '<input v-model.lazy="test">'
  39. }).$mount()
  40. expect(vm.$el.value).toBe('b')
  41. expect(vm.test).toBe('b')
  42. vm.$el.value = 'c'
  43. triggerEvent(vm.$el, 'input')
  44. expect(vm.test).toBe('b')
  45. triggerEvent(vm.$el, 'change')
  46. expect(vm.test).toBe('c')
  47. })
  48. it('.number modifier', () => {
  49. const vm = new Vue({
  50. data: {
  51. test: 1
  52. },
  53. template: '<input v-model.number="test">'
  54. }).$mount()
  55. expect(vm.test).toBe(1)
  56. vm.$el.value = '2'
  57. triggerEvent(vm.$el, 'input')
  58. expect(vm.test).toBe(2)
  59. // should let strings pass through
  60. vm.$el.value = 'f'
  61. triggerEvent(vm.$el, 'input')
  62. expect(vm.test).toBe('f')
  63. })
  64. it('.trim modifier', () => {
  65. const vm = new Vue({
  66. data: {
  67. test: 'hi'
  68. },
  69. template: '<input v-model.trim="test">'
  70. }).$mount()
  71. expect(vm.test).toBe('hi')
  72. vm.$el.value = ' what '
  73. triggerEvent(vm.$el, 'input')
  74. expect(vm.test).toBe('what')
  75. })
  76. it('.number focus and typing', done => {
  77. const vm = new Vue({
  78. data: {
  79. test: 0,
  80. update: 0
  81. },
  82. template:
  83. '<div>' +
  84. '<input ref="input" v-model.number="test">{{ update }}' +
  85. '<input ref="blur">' +
  86. '</div>'
  87. }).$mount()
  88. document.body.appendChild(vm.$el)
  89. vm.$refs.input.focus()
  90. expect(vm.test).toBe(0)
  91. vm.$refs.input.value = '1.0'
  92. triggerEvent(vm.$refs.input, 'input')
  93. expect(vm.test).toBe(1)
  94. vm.update++
  95. waitForUpdate(() => {
  96. expect(vm.$refs.input.value).toBe('1.0')
  97. vm.$refs.blur.focus()
  98. vm.update++
  99. })
  100. .then(() => {
  101. expect(vm.$refs.input.value).toBe('1')
  102. })
  103. .then(done)
  104. })
  105. it('.trim focus and typing', done => {
  106. const vm = new Vue({
  107. data: {
  108. test: 'abc',
  109. update: 0
  110. },
  111. template:
  112. '<div>' +
  113. '<input ref="input" v-model.trim="test" type="text">{{ update }}' +
  114. '<input ref="blur"/>' +
  115. '</div>'
  116. }).$mount()
  117. document.body.appendChild(vm.$el)
  118. vm.$refs.input.focus()
  119. vm.$refs.input.value = ' abc '
  120. triggerEvent(vm.$refs.input, 'input')
  121. expect(vm.test).toBe('abc')
  122. vm.update++
  123. waitForUpdate(() => {
  124. expect(vm.$refs.input.value).toBe(' abc ')
  125. vm.$refs.blur.focus()
  126. vm.update++
  127. })
  128. .then(() => {
  129. expect(vm.$refs.input.value).toBe('abc')
  130. })
  131. .then(done)
  132. })
  133. it('multiple inputs', done => {
  134. const spy = vi.fn()
  135. const vm = new Vue({
  136. data: {
  137. selections: [
  138. [1, 2, 3],
  139. [4, 5]
  140. ],
  141. inputList: [
  142. {
  143. name: 'questionA',
  144. data: ['a', 'b', 'c']
  145. },
  146. {
  147. name: 'questionB',
  148. data: ['1', '2']
  149. }
  150. ]
  151. },
  152. watch: {
  153. selections: spy
  154. },
  155. template:
  156. '<div>' +
  157. '<div v-for="(inputGroup, idx) in inputList">' +
  158. '<div>' +
  159. '<span v-for="(item, index) in inputGroup.data">' +
  160. '<input v-bind:name="item" type="text" v-model.number="selections[idx][index]" v-bind:id="idx+\'-\'+index"/>' +
  161. '<label>{{item}}</label>' +
  162. '</span>' +
  163. '</div>' +
  164. '</div>' +
  165. '<span ref="rs">{{selections}}</span>' +
  166. '</div>'
  167. }).$mount()
  168. const inputs = vm.$el.getElementsByTagName('input')
  169. inputs[1].value = 'test'
  170. triggerEvent(inputs[1], 'input')
  171. waitForUpdate(() => {
  172. expect(spy).toHaveBeenCalled()
  173. expect(vm.selections).toEqual([
  174. [1, 'test', 3],
  175. [4, 5]
  176. ])
  177. }).then(done)
  178. })
  179. if (isIE9) {
  180. it('IE9 selectionchange', done => {
  181. const vm = new Vue({
  182. data: {
  183. test: 'foo'
  184. },
  185. template: '<input v-model="test">'
  186. }).$mount()
  187. const input = vm.$el
  188. input.value = 'bar'
  189. document.body.appendChild(input)
  190. input.focus()
  191. triggerEvent(input, 'selectionchange')
  192. waitForUpdate(() => {
  193. expect(vm.test).toBe('bar')
  194. input.value = 'a'
  195. triggerEvent(input, 'selectionchange')
  196. expect(vm.test).toBe('a')
  197. }).then(done)
  198. })
  199. }
  200. it('compositionevents', function (done) {
  201. const vm = new Vue({
  202. data: {
  203. test: 'foo'
  204. },
  205. template: '<input v-model="test">'
  206. }).$mount()
  207. const input = vm.$el
  208. triggerEvent(input, 'compositionstart')
  209. input.value = 'baz'
  210. // input before composition unlock should not call set
  211. triggerEvent(input, 'input')
  212. expect(vm.test).toBe('foo')
  213. // after composition unlock it should work
  214. triggerEvent(input, 'compositionend')
  215. triggerEvent(input, 'input')
  216. expect(vm.test).toBe('baz')
  217. done()
  218. })
  219. it('warn invalid tag', () => {
  220. new Vue({
  221. data: {
  222. test: 'foo'
  223. },
  224. template: '<div v-model="test"></div>'
  225. }).$mount()
  226. expect(
  227. '<div v-model="test">: v-model is not supported on this element type'
  228. ).toHaveBeenWarned()
  229. })
  230. // #3468
  231. it('should have higher priority than user v-on events', () => {
  232. const spy = vi.fn()
  233. const vm = new Vue({
  234. data: {
  235. a: 'a'
  236. },
  237. template: '<input v-model="a" @input="onInput">',
  238. methods: {
  239. onInput(e) {
  240. spy(this.a)
  241. }
  242. }
  243. }).$mount()
  244. vm.$el.value = 'b'
  245. triggerEvent(vm.$el, 'input')
  246. expect(spy).toHaveBeenCalledWith('b')
  247. })
  248. it('warn binding to v-for alias', () => {
  249. new Vue({
  250. data: {
  251. strings: ['hi']
  252. },
  253. template: `
  254. <div>
  255. <div v-for="str in strings">
  256. <input v-model="str">
  257. </div>
  258. </div>
  259. `
  260. }).$mount()
  261. expect(
  262. 'You are binding v-model directly to a v-for iteration alias'
  263. ).toHaveBeenWarned()
  264. })
  265. it('warn if v-model and v-bind:value conflict', () => {
  266. new Vue({
  267. data: {
  268. test: 'foo'
  269. },
  270. template: '<input type="text" v-model="test" v-bind:value="test">'
  271. }).$mount()
  272. expect('v-bind:value="test" conflicts with v-model').toHaveBeenWarned()
  273. })
  274. it('warn if v-model and :value conflict', () => {
  275. new Vue({
  276. data: {
  277. test: 'foo'
  278. },
  279. template: '<input type="text" v-model="test" :value="test">'
  280. }).$mount()
  281. expect(':value="test" conflicts with v-model').toHaveBeenWarned()
  282. })
  283. it('should not warn on radio, checkbox, or custom component', () => {
  284. new Vue({
  285. data: { test: '' },
  286. components: {
  287. foo: {
  288. props: ['model', 'value'],
  289. model: { prop: 'model', event: 'change' },
  290. template: `<div/>`
  291. }
  292. },
  293. template: `
  294. <div>
  295. <input type="checkbox" v-model="test" :value="test">
  296. <input type="radio" v-model="test" :value="test">
  297. <foo v-model="test" :value="test"/>
  298. </div>
  299. `
  300. }).$mount()
  301. expect('conflicts with v-model').not.toHaveBeenWarned()
  302. })
  303. it('should not warn on input with dynamic type binding', () => {
  304. new Vue({
  305. data: {
  306. type: 'checkbox',
  307. test: 'foo'
  308. },
  309. template: '<input :type="type" v-model="test" :value="test">'
  310. }).$mount()
  311. expect('conflicts with v-model').not.toHaveBeenWarned()
  312. })
  313. if (!isAndroid) {
  314. it('does not trigger extra input events with single compositionend', () => {
  315. const spy = vi.fn()
  316. const vm = new Vue({
  317. data: {
  318. a: 'a'
  319. },
  320. template: '<input v-model="a" @input="onInput">',
  321. methods: {
  322. onInput(e) {
  323. spy(e.target.value)
  324. }
  325. }
  326. }).$mount()
  327. expect(spy.mock.calls.length).toBe(0)
  328. vm.$el.value = 'b'
  329. triggerEvent(vm.$el, 'input')
  330. expect(spy.mock.calls.length).toBe(1)
  331. triggerEvent(vm.$el, 'compositionend')
  332. expect(spy.mock.calls.length).toBe(1)
  333. })
  334. it('triggers extra input on compositionstart + end', () => {
  335. const spy = vi.fn()
  336. const vm = new Vue({
  337. data: {
  338. a: 'a'
  339. },
  340. template: '<input v-model="a" @input="onInput">',
  341. methods: {
  342. onInput(e) {
  343. spy(e.target.value)
  344. }
  345. }
  346. }).$mount()
  347. expect(spy.mock.calls.length).toBe(0)
  348. vm.$el.value = 'b'
  349. triggerEvent(vm.$el, 'input')
  350. expect(spy.mock.calls.length).toBe(1)
  351. triggerEvent(vm.$el, 'compositionstart')
  352. triggerEvent(vm.$el, 'compositionend')
  353. expect(spy.mock.calls.length).toBe(2)
  354. })
  355. // #4392
  356. it('should not update value with modifiers when in focus if post-conversion values are the same', done => {
  357. const vm = new Vue({
  358. data: {
  359. a: 1,
  360. foo: false
  361. },
  362. template: '<div>{{ foo }}<input ref="input" v-model.number="a"></div>'
  363. }).$mount()
  364. document.body.appendChild(vm.$el)
  365. vm.$refs.input.focus()
  366. vm.$refs.input.value = '1.000'
  367. vm.foo = true
  368. waitForUpdate(() => {
  369. expect(vm.$refs.input.value).toBe('1.000')
  370. }).then(done)
  371. })
  372. // #6552
  373. // This was original introduced due to the microtask between DOM events issue
  374. // but fixed after switching to MessageChannel.
  375. it('should not block input when another input listener with modifier is used', done => {
  376. const vm = new Vue({
  377. data: {
  378. a: 'a',
  379. foo: false
  380. },
  381. template: `
  382. <div>
  383. <input ref="input" v-model="a" @input.capture="onInput">{{ a }}
  384. <div v-if="foo">foo</div>
  385. </div>
  386. `,
  387. methods: {
  388. onInput(e) {
  389. this.foo = true
  390. }
  391. }
  392. }).$mount()
  393. document.body.appendChild(vm.$el)
  394. vm.$refs.input.focus()
  395. vm.$refs.input.value = 'b'
  396. triggerEvent(vm.$refs.input, 'input')
  397. // not using wait for update here because there will be two update cycles
  398. // one caused by onInput in the first listener
  399. setTimeout(() => {
  400. expect(vm.a).toBe('b')
  401. expect(vm.$refs.input.value).toBe('b')
  402. done()
  403. }, 16)
  404. })
  405. it('should create and make reactive non-existent properties', done => {
  406. const vm = new Vue({
  407. data: {
  408. foo: {}
  409. },
  410. template: '<input v-model="foo.bar">'
  411. }).$mount()
  412. expect(vm.$el.value).toBe('')
  413. vm.$el.value = 'a'
  414. triggerEvent(vm.$el, 'input')
  415. expect(vm.foo.bar).toBe('a')
  416. vm.foo.bar = 'b'
  417. waitForUpdate(() => {
  418. expect(vm.$el.value).toBe('b')
  419. vm.foo = {}
  420. })
  421. .then(() => {
  422. expect(vm.$el.value).toBe('')
  423. })
  424. .then(done)
  425. })
  426. }
  427. if (isIE && !isIE9) {
  428. // #7138
  429. it('should not fire input on initial render of textarea with placeholder in IE10/11', done => {
  430. const el = document.createElement('div')
  431. document.body.appendChild(el)
  432. const vm = new Vue({
  433. el,
  434. data: { foo: null },
  435. template: `<textarea v-model="foo" placeholder="bar"></textarea>`
  436. })
  437. setTimeout(() => {
  438. expect(vm.foo).toBe(null)
  439. done()
  440. }, 17)
  441. })
  442. // #9042
  443. it('should not block the first input event when placeholder is empty', done => {
  444. const el = document.createElement('div')
  445. document.body.appendChild(el)
  446. const vm = new Vue({
  447. el,
  448. data: { evtCount: 0 },
  449. template: `<textarea placeholder="" @input="evtCount++"></textarea>`
  450. })
  451. triggerEvent(vm.$el, 'input')
  452. setTimeout(() => {
  453. expect(vm.evtCount).toBe(1)
  454. done()
  455. }, 17)
  456. })
  457. }
  458. })