edge-cases.spec.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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. // #3533
  27. // a static node is reused in createElm, which changes its elm reference
  28. // and is inserted into a different parent.
  29. // later when patching the next element a DOM insertion uses it as the
  30. // reference node, causing a parent mismatch.
  31. it('should handle static node edge case when it\'s reused AND used as a reference node for insertion', done => {
  32. const vm = new Vue({
  33. data: {
  34. ok: true
  35. },
  36. template: `
  37. <div>
  38. <button @click="ok = !ok">toggle</button>
  39. <div class="b" v-if="ok">123</div>
  40. <div class="c">
  41. <div><span/></div><p>{{ 1 }}</p>
  42. </div>
  43. <div class="d">
  44. <label>{{ 2 }}</label>
  45. </div>
  46. <div class="b" v-if="ok">123</div>
  47. </div>
  48. `
  49. }).$mount()
  50. expect(vm.$el.querySelector('.c').textContent).toBe('1')
  51. expect(vm.$el.querySelector('.d').textContent).toBe('2')
  52. vm.ok = false
  53. waitForUpdate(() => {
  54. expect(vm.$el.querySelector('.c').textContent).toBe('1')
  55. expect(vm.$el.querySelector('.d').textContent).toBe('2')
  56. }).then(done)
  57. })
  58. it('should handle slot nodes being reused across render', done => {
  59. const vm = new Vue({
  60. template: `
  61. <foo ref="foo">
  62. <div>slot</div>
  63. </foo>
  64. `,
  65. components: {
  66. foo: {
  67. data () {
  68. return { ok: true }
  69. },
  70. render (h) {
  71. const children = [
  72. this.ok ? h('div', 'toggler ') : null,
  73. h('div', [this.$slots.default, h('span', ' 1')]),
  74. h('div', [h('label', ' 2')])
  75. ]
  76. return h('div', children)
  77. }
  78. }
  79. }
  80. }).$mount()
  81. expect(vm.$el.textContent).toContain('toggler slot 1 2')
  82. vm.$refs.foo.ok = false
  83. waitForUpdate(() => {
  84. expect(vm.$el.textContent).toContain('slot 1 2')
  85. vm.$refs.foo.ok = true
  86. }).then(() => {
  87. expect(vm.$el.textContent).toContain('toggler slot 1 2')
  88. vm.$refs.foo.ok = false
  89. }).then(() => {
  90. expect(vm.$el.textContent).toContain('slot 1 2')
  91. vm.$refs.foo.ok = true
  92. }).then(done)
  93. })
  94. it('should synchronize vm\' vnode', done => {
  95. const comp = {
  96. data: () => ({ swap: true }),
  97. render (h) {
  98. return this.swap
  99. ? h('a', 'atag')
  100. : h('span', 'span')
  101. }
  102. }
  103. const wrapper = {
  104. render: h => h('comp'),
  105. components: { comp }
  106. }
  107. const vm = new Vue({
  108. render (h) {
  109. const children = [
  110. h('wrapper'),
  111. h('div', 'row')
  112. ]
  113. if (this.swap) {
  114. children.reverse()
  115. }
  116. return h('div', children)
  117. },
  118. data: () => ({ swap: false }),
  119. components: { wrapper }
  120. }).$mount()
  121. expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
  122. const wrapperVm = vm.$children[0]
  123. const compVm = wrapperVm.$children[0]
  124. vm.swap = true
  125. waitForUpdate(() => {
  126. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  127. expect(vm.$el.innerHTML).toBe('<div>row</div><a>atag</a>')
  128. vm.swap = false
  129. }).then(() => {
  130. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  131. expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
  132. compVm.swap = false
  133. }).then(() => {
  134. expect(vm.$el.innerHTML).toBe('<span>span</span><div>row</div>')
  135. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  136. vm.swap = true
  137. }).then(() => {
  138. expect(vm.$el.innerHTML).toBe('<div>row</div><span>span</span>')
  139. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  140. vm.swap = true
  141. }).then(done)
  142. })
  143. // #4530
  144. it('should not reset value when patching between dynamic/static bindings', done => {
  145. const vm = new Vue({
  146. data: { ok: true },
  147. template: `
  148. <div>
  149. <input type="button" v-if="ok" value="a">
  150. <input type="button" :value="'b'">
  151. </div>
  152. `
  153. }).$mount()
  154. expect(vm.$el.children[0].value).toBe('a')
  155. vm.ok = false
  156. waitForUpdate(() => {
  157. expect(vm.$el.children[0].value).toBe('b')
  158. vm.ok = true
  159. }).then(() => {
  160. expect(vm.$el.children[0].value).toBe('a')
  161. }).then(done)
  162. })
  163. // #6313
  164. it('should not replace node when switching between text-like inputs', done => {
  165. const vm = new Vue({
  166. data: { show: false },
  167. template: `
  168. <div>
  169. <input :type="show ? 'text' : 'password'">
  170. </div>
  171. `
  172. }).$mount()
  173. const node = vm.$el.children[0]
  174. expect(vm.$el.children[0].type).toBe('password')
  175. vm.$el.children[0].value = 'test'
  176. vm.show = true
  177. waitForUpdate(() => {
  178. expect(vm.$el.children[0]).toBe(node)
  179. expect(vm.$el.children[0].value).toBe('test')
  180. expect(vm.$el.children[0].type).toBe('text')
  181. vm.show = false
  182. }).then(() => {
  183. expect(vm.$el.children[0]).toBe(node)
  184. expect(vm.$el.children[0].value).toBe('test')
  185. expect(vm.$el.children[0].type).toBe('password')
  186. }).then(done)
  187. })
  188. it('should properly patch nested HOC when root element is replaced', done => {
  189. const vm = new Vue({
  190. template: `<foo class="hello" ref="foo" />`,
  191. components: {
  192. foo: {
  193. template: `<bar ref="bar" />`,
  194. components: {
  195. bar: {
  196. template: `<div v-if="ok"></div><span v-else></span>`,
  197. data () {
  198. return { ok: true }
  199. }
  200. }
  201. }
  202. }
  203. }
  204. }).$mount()
  205. expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('DIV')
  206. expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
  207. vm.$refs.foo.$refs.bar.ok = false
  208. waitForUpdate(() => {
  209. expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('SPAN')
  210. expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
  211. }).then(done)
  212. })
  213. // #6790
  214. it('should not render undefined for empty nested arrays', () => {
  215. const vm = new Vue({
  216. template: `<div><template v-for="i in emptyArr"></template></div>`,
  217. data: { emptyArr: [] }
  218. }).$mount()
  219. expect(vm.$el.textContent).toBe('')
  220. })
  221. // #6803
  222. it('backwards compat with checkbox code generated before 2.4', () => {
  223. const spy = jasmine.createSpy()
  224. const vm = new Vue({
  225. data: {
  226. label: 'foobar',
  227. name: 'foobar'
  228. },
  229. computed: {
  230. value: {
  231. get () {
  232. return 1
  233. },
  234. set: spy
  235. }
  236. },
  237. render (h) {
  238. const _vm = this
  239. return h('div', {},
  240. [h('input', {
  241. directives: [{
  242. name: 'model',
  243. rawName: 'v-model',
  244. value: (_vm.value),
  245. expression: 'value'
  246. }],
  247. attrs: {
  248. 'type': 'radio',
  249. 'name': _vm.name
  250. },
  251. domProps: {
  252. 'value': _vm.label,
  253. 'checked': _vm._q(_vm.value, _vm.label)
  254. },
  255. on: {
  256. '__c': function ($event) {
  257. _vm.value = _vm.label
  258. }
  259. }
  260. })])
  261. }
  262. }).$mount()
  263. document.body.appendChild(vm.$el)
  264. vm.$el.children[0].click()
  265. expect(spy).toHaveBeenCalled()
  266. })
  267. // #7041
  268. it('transition children with only deep bindings should be patched on update', done => {
  269. const vm = new Vue({
  270. template: `
  271. <div>
  272. <transition>
  273. <div :style="style"></div>
  274. </transition>
  275. </div>
  276. `,
  277. data: () => ({
  278. style: { color: 'red' }
  279. })
  280. }).$mount()
  281. expect(vm.$el.children[0].style.color).toBe('red')
  282. vm.style.color = 'green'
  283. waitForUpdate(() => {
  284. expect(vm.$el.children[0].style.color).toBe('green')
  285. }).then(done)
  286. })
  287. })