edge-cases.spec.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import Vue from 'vue'
  2. describe('vdom patch: edge cases', () => {
  3. // exposed by #3406
  4. // When a static vnode is inside v-for, it's possible for the same vnode
  5. // to be used in multiple places, and its element will be replaced. This
  6. // causes patch errors when node ops depend on the vnode's element position.
  7. it('should handle static vnodes by key', done => {
  8. const vm = new Vue({
  9. data: {
  10. ok: true
  11. },
  12. template: `
  13. <div>
  14. <div v-for="i in 2">
  15. <div v-if="ok">a</div><div>b</div><div v-if="!ok">c</div><div>d</div>
  16. </div>
  17. </div>
  18. `
  19. }).$mount()
  20. expect(vm.$el.textContent).toBe('abdabd')
  21. vm.ok = false
  22. waitForUpdate(() => {
  23. expect(vm.$el.textContent).toBe('bcdbcd')
  24. }).then(done)
  25. })
  26. // exposed by #7705
  27. // methods and function expressions with modifiers should return result instead of undefined
  28. // skipped odd children[1,3, ...] because they are rendered as text nodes with undefined value
  29. it('should return listener\'s result for method name and function expression with and w/o modifiers', done => {
  30. const dummyEvt = { preventDefault: () => {} }
  31. new Vue({
  32. template: `
  33. <div v-test>
  34. <div @click="addFive"></div>
  35. <div @click.prevent="addFive"></div>
  36. <div @click="addFive($event, 5)"></div>
  37. <div @click.prevent="addFive($event, 5)"></div>
  38. </div>
  39. `,
  40. methods: {
  41. addFive ($event, toAdd = 0) {
  42. return toAdd + 5
  43. }
  44. },
  45. directives: {
  46. test: {
  47. bind (el, binding, vnode) {
  48. waitForUpdate(() => {
  49. expect(vnode.children[0].data.on.click()).toBe(5)
  50. expect(vnode.children[2].data.on.click(dummyEvt)).toBe(5)
  51. expect(vnode.children[4].data.on.click()).toBe(10)
  52. expect(vnode.children[6].data.on.click(dummyEvt)).toBe(10)
  53. }).then(done)
  54. }
  55. }
  56. }
  57. }).$mount()
  58. })
  59. // #3533
  60. // a static node is reused in createElm, which changes its elm reference
  61. // and is inserted into a different parent.
  62. // later when patching the next element a DOM insertion uses it as the
  63. // reference node, causing a parent mismatch.
  64. it('should handle static node edge case when it\'s reused AND used as a reference node for insertion', done => {
  65. const vm = new Vue({
  66. data: {
  67. ok: true
  68. },
  69. template: `
  70. <div>
  71. <button @click="ok = !ok">toggle</button>
  72. <div class="b" v-if="ok">123</div>
  73. <div class="c">
  74. <div><span/></div><p>{{ 1 }}</p>
  75. </div>
  76. <div class="d">
  77. <label>{{ 2 }}</label>
  78. </div>
  79. <div class="b" v-if="ok">123</div>
  80. </div>
  81. `
  82. }).$mount()
  83. expect(vm.$el.querySelector('.c').textContent).toBe('1')
  84. expect(vm.$el.querySelector('.d').textContent).toBe('2')
  85. vm.ok = false
  86. waitForUpdate(() => {
  87. expect(vm.$el.querySelector('.c').textContent).toBe('1')
  88. expect(vm.$el.querySelector('.d').textContent).toBe('2')
  89. }).then(done)
  90. })
  91. it('should handle slot nodes being reused across render', done => {
  92. const vm = new Vue({
  93. template: `
  94. <foo ref="foo">
  95. <div>slot</div>
  96. </foo>
  97. `,
  98. components: {
  99. foo: {
  100. data () {
  101. return { ok: true }
  102. },
  103. render (h) {
  104. const children = [
  105. this.ok ? h('div', 'toggler ') : null,
  106. h('div', [this.$slots.default, h('span', ' 1')]),
  107. h('div', [h('label', ' 2')])
  108. ]
  109. return h('div', children)
  110. }
  111. }
  112. }
  113. }).$mount()
  114. expect(vm.$el.textContent).toContain('toggler slot 1 2')
  115. vm.$refs.foo.ok = false
  116. waitForUpdate(() => {
  117. expect(vm.$el.textContent).toContain('slot 1 2')
  118. vm.$refs.foo.ok = true
  119. }).then(() => {
  120. expect(vm.$el.textContent).toContain('toggler slot 1 2')
  121. vm.$refs.foo.ok = false
  122. }).then(() => {
  123. expect(vm.$el.textContent).toContain('slot 1 2')
  124. vm.$refs.foo.ok = true
  125. }).then(done)
  126. })
  127. it('should synchronize vm\' vnode', done => {
  128. const comp = {
  129. data: () => ({ swap: true }),
  130. render (h) {
  131. return this.swap
  132. ? h('a', 'atag')
  133. : h('span', 'span')
  134. }
  135. }
  136. const wrapper = {
  137. render: h => h('comp'),
  138. components: { comp }
  139. }
  140. const vm = new Vue({
  141. render (h) {
  142. const children = [
  143. h('wrapper'),
  144. h('div', 'row')
  145. ]
  146. if (this.swap) {
  147. children.reverse()
  148. }
  149. return h('div', children)
  150. },
  151. data: () => ({ swap: false }),
  152. components: { wrapper }
  153. }).$mount()
  154. expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
  155. const wrapperVm = vm.$children[0]
  156. const compVm = wrapperVm.$children[0]
  157. vm.swap = true
  158. waitForUpdate(() => {
  159. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  160. expect(vm.$el.innerHTML).toBe('<div>row</div><a>atag</a>')
  161. vm.swap = false
  162. }).then(() => {
  163. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  164. expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
  165. compVm.swap = false
  166. }).then(() => {
  167. expect(vm.$el.innerHTML).toBe('<span>span</span><div>row</div>')
  168. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  169. vm.swap = true
  170. }).then(() => {
  171. expect(vm.$el.innerHTML).toBe('<div>row</div><span>span</span>')
  172. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  173. vm.swap = true
  174. }).then(done)
  175. })
  176. // #4530
  177. it('should not reset value when patching between dynamic/static bindings', done => {
  178. const vm = new Vue({
  179. data: { ok: true },
  180. template: `
  181. <div>
  182. <input type="button" v-if="ok" value="a">
  183. <input type="button" :value="'b'">
  184. </div>
  185. `
  186. }).$mount()
  187. expect(vm.$el.children[0].value).toBe('a')
  188. vm.ok = false
  189. waitForUpdate(() => {
  190. expect(vm.$el.children[0].value).toBe('b')
  191. vm.ok = true
  192. }).then(() => {
  193. expect(vm.$el.children[0].value).toBe('a')
  194. }).then(done)
  195. })
  196. // #6313
  197. it('should not replace node when switching between text-like inputs', done => {
  198. const vm = new Vue({
  199. data: { show: false },
  200. template: `
  201. <div>
  202. <input :type="show ? 'text' : 'password'">
  203. </div>
  204. `
  205. }).$mount()
  206. const node = vm.$el.children[0]
  207. expect(vm.$el.children[0].type).toBe('password')
  208. vm.$el.children[0].value = 'test'
  209. vm.show = true
  210. waitForUpdate(() => {
  211. expect(vm.$el.children[0]).toBe(node)
  212. expect(vm.$el.children[0].value).toBe('test')
  213. expect(vm.$el.children[0].type).toBe('text')
  214. vm.show = false
  215. }).then(() => {
  216. expect(vm.$el.children[0]).toBe(node)
  217. expect(vm.$el.children[0].value).toBe('test')
  218. expect(vm.$el.children[0].type).toBe('password')
  219. }).then(done)
  220. })
  221. it('should properly patch nested HOC when root element is replaced', done => {
  222. const vm = new Vue({
  223. template: `<foo class="hello" ref="foo" />`,
  224. components: {
  225. foo: {
  226. template: `<bar ref="bar" />`,
  227. components: {
  228. bar: {
  229. template: `<div v-if="ok"></div><span v-else></span>`,
  230. data () {
  231. return { ok: true }
  232. }
  233. }
  234. }
  235. }
  236. }
  237. }).$mount()
  238. expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('DIV')
  239. expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
  240. vm.$refs.foo.$refs.bar.ok = false
  241. waitForUpdate(() => {
  242. expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('SPAN')
  243. expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
  244. }).then(done)
  245. })
  246. // #6790
  247. it('should not render undefined for empty nested arrays', () => {
  248. const vm = new Vue({
  249. template: `<div><template v-for="i in emptyArr"></template></div>`,
  250. data: { emptyArr: [] }
  251. }).$mount()
  252. expect(vm.$el.textContent).toBe('')
  253. })
  254. // #6803
  255. it('backwards compat with checkbox code generated before 2.4', () => {
  256. const spy = jasmine.createSpy()
  257. const vm = new Vue({
  258. data: {
  259. label: 'foobar',
  260. name: 'foobar'
  261. },
  262. computed: {
  263. value: {
  264. get () {
  265. return 1
  266. },
  267. set: spy
  268. }
  269. },
  270. render (h) {
  271. const _vm = this
  272. return h('div', {},
  273. [h('input', {
  274. directives: [{
  275. name: 'model',
  276. rawName: 'v-model',
  277. value: (_vm.value),
  278. expression: 'value'
  279. }],
  280. attrs: {
  281. 'type': 'radio',
  282. 'name': _vm.name
  283. },
  284. domProps: {
  285. 'value': _vm.label,
  286. 'checked': _vm._q(_vm.value, _vm.label)
  287. },
  288. on: {
  289. '__c': function ($event) {
  290. _vm.value = _vm.label
  291. }
  292. }
  293. })])
  294. }
  295. }).$mount()
  296. document.body.appendChild(vm.$el)
  297. vm.$el.children[0].click()
  298. expect(spy).toHaveBeenCalled()
  299. })
  300. // #7041
  301. it('transition children with only deep bindings should be patched on update', done => {
  302. const vm = new Vue({
  303. template: `
  304. <div>
  305. <transition>
  306. <div :style="style"></div>
  307. </transition>
  308. </div>
  309. `,
  310. data: () => ({
  311. style: { color: 'red' }
  312. })
  313. }).$mount()
  314. expect(vm.$el.children[0].style.color).toBe('red')
  315. vm.style.color = 'green'
  316. waitForUpdate(() => {
  317. expect(vm.$el.children[0].style.color).toBe('green')
  318. }).then(done)
  319. })
  320. // #7294
  321. it('should cleanup component inline events on patch when no events are present', done => {
  322. const log = jasmine.createSpy()
  323. const vm = new Vue({
  324. data: { ok: true },
  325. template: `
  326. <div>
  327. <foo v-if="ok" @custom="log"/>
  328. <foo v-else/>
  329. </div>
  330. `,
  331. components: {
  332. foo: {
  333. render () {}
  334. }
  335. },
  336. methods: { log }
  337. }).$mount()
  338. vm.ok = false
  339. waitForUpdate(() => {
  340. vm.$children[0].$emit('custom')
  341. expect(log).not.toHaveBeenCalled()
  342. }).then(done)
  343. })
  344. // #6864
  345. it('should not special-case boolean attributes for custom elements', () => {
  346. Vue.config.ignoredElements = [/^custom-/]
  347. const vm = new Vue({
  348. template: `<div><custom-foo selected="1"/></div>`
  349. }).$mount()
  350. expect(vm.$el.querySelector('custom-foo').getAttribute('selected')).toBe('1')
  351. Vue.config.ignoredElements = []
  352. })
  353. // #7805
  354. it('should not cause duplicate init when components share data object', () => {
  355. const Base = {
  356. render (h) {
  357. return h('div', this.$options.name)
  358. }
  359. }
  360. const Foo = {
  361. name: 'Foo',
  362. extends: Base
  363. }
  364. const Bar = {
  365. name: 'Bar',
  366. extends: Base
  367. }
  368. // sometimes we do need to tap into these internal hooks (e.g. in vue-router)
  369. // so make sure it does work
  370. const inlineHookSpy = jasmine.createSpy('inlineInit')
  371. const vm = new Vue({
  372. render (h) {
  373. const data = { staticClass: 'text-red', hook: {
  374. init: inlineHookSpy
  375. }}
  376. return h('div', [
  377. h(Foo, data),
  378. h(Bar, data)
  379. ])
  380. }
  381. }).$mount()
  382. expect(vm.$el.textContent).toBe('FooBar')
  383. expect(inlineHookSpy.calls.count()).toBe(2)
  384. })
  385. // #9549
  386. it('DOM props set throwing should not break app', done => {
  387. const vm = new Vue({
  388. data: {
  389. n: Infinity
  390. },
  391. template: `
  392. <div>
  393. <progress :value="n"/>
  394. {{ n }}
  395. </div>
  396. `
  397. }).$mount()
  398. expect(vm.$el.textContent).toMatch('Infinity')
  399. vm.n = 1
  400. waitForUpdate(() => {
  401. expect(vm.$el.textContent).toMatch('1')
  402. expect(vm.$el.textContent).not.toMatch('Infinity')
  403. }).then(done)
  404. })
  405. })