edge-cases.spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. }).then(() => {
  51. expect(vnode.children[2].data.on.click(dummyEvt)).toBe(5)
  52. }).then(() => {
  53. expect(vnode.children[4].data.on.click()).not.toBeDefined()
  54. }).then(() => {
  55. expect(vnode.children[6].data.on.click(dummyEvt)).not.toBeDefined()
  56. }).then(done)
  57. }
  58. }
  59. }
  60. }).$mount()
  61. })
  62. // #3533
  63. // a static node is reused in createElm, which changes its elm reference
  64. // and is inserted into a different parent.
  65. // later when patching the next element a DOM insertion uses it as the
  66. // reference node, causing a parent mismatch.
  67. it('should handle static node edge case when it\'s reused AND used as a reference node for insertion', done => {
  68. const vm = new Vue({
  69. data: {
  70. ok: true
  71. },
  72. template: `
  73. <div>
  74. <button @click="ok = !ok">toggle</button>
  75. <div class="b" v-if="ok">123</div>
  76. <div class="c">
  77. <div><span/></div><p>{{ 1 }}</p>
  78. </div>
  79. <div class="d">
  80. <label>{{ 2 }}</label>
  81. </div>
  82. <div class="b" v-if="ok">123</div>
  83. </div>
  84. `
  85. }).$mount()
  86. expect(vm.$el.querySelector('.c').textContent).toBe('1')
  87. expect(vm.$el.querySelector('.d').textContent).toBe('2')
  88. vm.ok = false
  89. waitForUpdate(() => {
  90. expect(vm.$el.querySelector('.c').textContent).toBe('1')
  91. expect(vm.$el.querySelector('.d').textContent).toBe('2')
  92. }).then(done)
  93. })
  94. it('should handle slot nodes being reused across render', done => {
  95. const vm = new Vue({
  96. template: `
  97. <foo ref="foo">
  98. <div>slot</div>
  99. </foo>
  100. `,
  101. components: {
  102. foo: {
  103. data () {
  104. return { ok: true }
  105. },
  106. render (h) {
  107. const children = [
  108. this.ok ? h('div', 'toggler ') : null,
  109. h('div', [this.$slots.default, h('span', ' 1')]),
  110. h('div', [h('label', ' 2')])
  111. ]
  112. return h('div', children)
  113. }
  114. }
  115. }
  116. }).$mount()
  117. expect(vm.$el.textContent).toContain('toggler slot 1 2')
  118. vm.$refs.foo.ok = false
  119. waitForUpdate(() => {
  120. expect(vm.$el.textContent).toContain('slot 1 2')
  121. vm.$refs.foo.ok = true
  122. }).then(() => {
  123. expect(vm.$el.textContent).toContain('toggler slot 1 2')
  124. vm.$refs.foo.ok = false
  125. }).then(() => {
  126. expect(vm.$el.textContent).toContain('slot 1 2')
  127. vm.$refs.foo.ok = true
  128. }).then(done)
  129. })
  130. it('should synchronize vm\' vnode', done => {
  131. const comp = {
  132. data: () => ({ swap: true }),
  133. render (h) {
  134. return this.swap
  135. ? h('a', 'atag')
  136. : h('span', 'span')
  137. }
  138. }
  139. const wrapper = {
  140. render: h => h('comp'),
  141. components: { comp }
  142. }
  143. const vm = new Vue({
  144. render (h) {
  145. const children = [
  146. h('wrapper'),
  147. h('div', 'row')
  148. ]
  149. if (this.swap) {
  150. children.reverse()
  151. }
  152. return h('div', children)
  153. },
  154. data: () => ({ swap: false }),
  155. components: { wrapper }
  156. }).$mount()
  157. expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
  158. const wrapperVm = vm.$children[0]
  159. const compVm = wrapperVm.$children[0]
  160. vm.swap = true
  161. waitForUpdate(() => {
  162. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  163. expect(vm.$el.innerHTML).toBe('<div>row</div><a>atag</a>')
  164. vm.swap = false
  165. }).then(() => {
  166. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  167. expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
  168. compVm.swap = false
  169. }).then(() => {
  170. expect(vm.$el.innerHTML).toBe('<span>span</span><div>row</div>')
  171. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  172. vm.swap = true
  173. }).then(() => {
  174. expect(vm.$el.innerHTML).toBe('<div>row</div><span>span</span>')
  175. expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
  176. vm.swap = true
  177. }).then(done)
  178. })
  179. // #4530
  180. it('should not reset value when patching between dynamic/static bindings', done => {
  181. const vm = new Vue({
  182. data: { ok: true },
  183. template: `
  184. <div>
  185. <input type="button" v-if="ok" value="a">
  186. <input type="button" :value="'b'">
  187. </div>
  188. `
  189. }).$mount()
  190. expect(vm.$el.children[0].value).toBe('a')
  191. vm.ok = false
  192. waitForUpdate(() => {
  193. expect(vm.$el.children[0].value).toBe('b')
  194. vm.ok = true
  195. }).then(() => {
  196. expect(vm.$el.children[0].value).toBe('a')
  197. }).then(done)
  198. })
  199. // #6313
  200. it('should not replace node when switching between text-like inputs', done => {
  201. const vm = new Vue({
  202. data: { show: false },
  203. template: `
  204. <div>
  205. <input :type="show ? 'text' : 'password'">
  206. </div>
  207. `
  208. }).$mount()
  209. const node = vm.$el.children[0]
  210. expect(vm.$el.children[0].type).toBe('password')
  211. vm.$el.children[0].value = 'test'
  212. vm.show = true
  213. waitForUpdate(() => {
  214. expect(vm.$el.children[0]).toBe(node)
  215. expect(vm.$el.children[0].value).toBe('test')
  216. expect(vm.$el.children[0].type).toBe('text')
  217. vm.show = false
  218. }).then(() => {
  219. expect(vm.$el.children[0]).toBe(node)
  220. expect(vm.$el.children[0].value).toBe('test')
  221. expect(vm.$el.children[0].type).toBe('password')
  222. }).then(done)
  223. })
  224. it('should properly patch nested HOC when root element is replaced', done => {
  225. const vm = new Vue({
  226. template: `<foo class="hello" ref="foo" />`,
  227. components: {
  228. foo: {
  229. template: `<bar ref="bar" />`,
  230. components: {
  231. bar: {
  232. template: `<div v-if="ok"></div><span v-else></span>`,
  233. data () {
  234. return { ok: true }
  235. }
  236. }
  237. }
  238. }
  239. }
  240. }).$mount()
  241. expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('DIV')
  242. expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
  243. vm.$refs.foo.$refs.bar.ok = false
  244. waitForUpdate(() => {
  245. expect(vm.$refs.foo.$refs.bar.$el.tagName).toBe('SPAN')
  246. expect(vm.$refs.foo.$refs.bar.$el.className).toBe(`hello`)
  247. }).then(done)
  248. })
  249. // #6790
  250. it('should not render undefined for empty nested arrays', () => {
  251. const vm = new Vue({
  252. template: `<div><template v-for="i in emptyArr"></template></div>`,
  253. data: { emptyArr: [] }
  254. }).$mount()
  255. expect(vm.$el.textContent).toBe('')
  256. })
  257. // #6803
  258. it('backwards compat with checkbox code generated before 2.4', () => {
  259. const spy = jasmine.createSpy()
  260. const vm = new Vue({
  261. data: {
  262. label: 'foobar',
  263. name: 'foobar'
  264. },
  265. computed: {
  266. value: {
  267. get () {
  268. return 1
  269. },
  270. set: spy
  271. }
  272. },
  273. render (h) {
  274. const _vm = this
  275. return h('div', {},
  276. [h('input', {
  277. directives: [{
  278. name: 'model',
  279. rawName: 'v-model',
  280. value: (_vm.value),
  281. expression: 'value'
  282. }],
  283. attrs: {
  284. 'type': 'radio',
  285. 'name': _vm.name
  286. },
  287. domProps: {
  288. 'value': _vm.label,
  289. 'checked': _vm._q(_vm.value, _vm.label)
  290. },
  291. on: {
  292. '__c': function ($event) {
  293. _vm.value = _vm.label
  294. }
  295. }
  296. })])
  297. }
  298. }).$mount()
  299. document.body.appendChild(vm.$el)
  300. vm.$el.children[0].click()
  301. expect(spy).toHaveBeenCalled()
  302. })
  303. // #7041
  304. it('transition children with only deep bindings should be patched on update', done => {
  305. const vm = new Vue({
  306. template: `
  307. <div>
  308. <transition>
  309. <div :style="style"></div>
  310. </transition>
  311. </div>
  312. `,
  313. data: () => ({
  314. style: { color: 'red' }
  315. })
  316. }).$mount()
  317. expect(vm.$el.children[0].style.color).toBe('red')
  318. vm.style.color = 'green'
  319. waitForUpdate(() => {
  320. expect(vm.$el.children[0].style.color).toBe('green')
  321. }).then(done)
  322. })
  323. // #7294
  324. it('should cleanup component inline events on patch when no events are present', done => {
  325. const log = jasmine.createSpy()
  326. const vm = new Vue({
  327. data: { ok: true },
  328. template: `
  329. <div>
  330. <foo v-if="ok" @custom="log"/>
  331. <foo v-else/>
  332. </div>
  333. `,
  334. components: {
  335. foo: {
  336. render () {}
  337. }
  338. },
  339. methods: { log }
  340. }).$mount()
  341. vm.ok = false
  342. waitForUpdate(() => {
  343. vm.$children[0].$emit('custom')
  344. expect(log).not.toHaveBeenCalled()
  345. }).then(done)
  346. })
  347. // #6864
  348. it('should not special-case boolean attributes for custom elements', () => {
  349. Vue.config.ignoredElements = [/^custom-/]
  350. const vm = new Vue({
  351. template: `<div><custom-foo selected="1"/></div>`
  352. }).$mount()
  353. expect(vm.$el.querySelector('custom-foo').getAttribute('selected')).toBe('1')
  354. Vue.config.ignoredElements = []
  355. })
  356. // #7805
  357. it('should not cause duplicate init when components share data object', () => {
  358. const Base = {
  359. render (h) {
  360. return h('div', this.$options.name)
  361. }
  362. }
  363. const Foo = {
  364. name: 'Foo',
  365. extends: Base
  366. }
  367. const Bar = {
  368. name: 'Bar',
  369. extends: Base
  370. }
  371. const vm = new Vue({
  372. render (h) {
  373. const data = { staticClass: 'text-red' }
  374. return h('div', [
  375. h(Foo, data),
  376. h(Bar, data)
  377. ])
  378. }
  379. }).$mount()
  380. expect(vm.$el.textContent).toBe('FooBar')
  381. })
  382. })