edge-cases.spec.ts 13 KB

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