component-keep-alive.spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. import Vue from 'vue'
  2. import injectStyles from '../transition/inject-styles'
  3. import { isIE9 } from 'core/util/env'
  4. import { nextFrame } from 'web/runtime/transition-util'
  5. describe('Component keep-alive', () => {
  6. const { duration, buffer } = injectStyles()
  7. let components, one, two, el
  8. beforeEach(() => {
  9. one = {
  10. template: '<div>one</div>',
  11. created: jasmine.createSpy('one created'),
  12. mounted: jasmine.createSpy('one mounted'),
  13. activated: jasmine.createSpy('one activated'),
  14. deactivated: jasmine.createSpy('one deactivated'),
  15. destroyed: jasmine.createSpy('one destroyed')
  16. }
  17. two = {
  18. template: '<div>two</div>',
  19. created: jasmine.createSpy('two created'),
  20. mounted: jasmine.createSpy('two mounted'),
  21. activated: jasmine.createSpy('two activated'),
  22. deactivated: jasmine.createSpy('two deactivated'),
  23. destroyed: jasmine.createSpy('two destroyed')
  24. }
  25. components = {
  26. one,
  27. two
  28. }
  29. el = document.createElement('div')
  30. document.body.appendChild(el)
  31. })
  32. function assertHookCalls (component, callCounts) {
  33. expect([
  34. component.created.calls.count(),
  35. component.mounted.calls.count(),
  36. component.activated.calls.count(),
  37. component.deactivated.calls.count(),
  38. component.destroyed.calls.count()
  39. ]).toEqual(callCounts)
  40. }
  41. it('should work', done => {
  42. const vm = new Vue({
  43. template: `
  44. <div v-if="ok">
  45. <keep-alive>
  46. <component :is="view"></component>
  47. </keep-alive>
  48. </div>
  49. `,
  50. data: {
  51. view: 'one',
  52. ok: true
  53. },
  54. components
  55. }).$mount()
  56. expect(vm.$el.textContent).toBe('one')
  57. assertHookCalls(one, [1, 1, 1, 0, 0])
  58. assertHookCalls(two, [0, 0, 0, 0, 0])
  59. vm.view = 'two'
  60. waitForUpdate(() => {
  61. expect(vm.$el.textContent).toBe('two')
  62. assertHookCalls(one, [1, 1, 1, 1, 0])
  63. assertHookCalls(two, [1, 1, 1, 0, 0])
  64. vm.view = 'one'
  65. }).then(() => {
  66. expect(vm.$el.textContent).toBe('one')
  67. assertHookCalls(one, [1, 1, 2, 1, 0])
  68. assertHookCalls(two, [1, 1, 1, 1, 0])
  69. vm.view = 'two'
  70. }).then(() => {
  71. expect(vm.$el.textContent).toBe('two')
  72. assertHookCalls(one, [1, 1, 2, 2, 0])
  73. assertHookCalls(two, [1, 1, 2, 1, 0])
  74. vm.ok = false // teardown
  75. }).then(() => {
  76. expect(vm.$el.textContent).toBe('')
  77. assertHookCalls(one, [1, 1, 2, 2, 1])
  78. assertHookCalls(two, [1, 1, 2, 2, 1])
  79. }).then(done)
  80. })
  81. function sharedAssertions (vm, done) {
  82. expect(vm.$el.textContent).toBe('one')
  83. assertHookCalls(one, [1, 1, 1, 0, 0])
  84. assertHookCalls(two, [0, 0, 0, 0, 0])
  85. vm.view = 'two'
  86. waitForUpdate(() => {
  87. expect(vm.$el.textContent).toBe('two')
  88. assertHookCalls(one, [1, 1, 1, 1, 0])
  89. assertHookCalls(two, [1, 1, 0, 0, 0])
  90. vm.view = 'one'
  91. }).then(() => {
  92. expect(vm.$el.textContent).toBe('one')
  93. assertHookCalls(one, [1, 1, 2, 1, 0])
  94. assertHookCalls(two, [1, 1, 0, 0, 1])
  95. vm.view = 'two'
  96. }).then(() => {
  97. expect(vm.$el.textContent).toBe('two')
  98. assertHookCalls(one, [1, 1, 2, 2, 0])
  99. assertHookCalls(two, [2, 2, 0, 0, 1])
  100. vm.ok = false // teardown
  101. }).then(() => {
  102. expect(vm.$el.textContent).toBe('')
  103. assertHookCalls(one, [1, 1, 2, 2, 1])
  104. assertHookCalls(two, [2, 2, 0, 0, 2])
  105. }).then(done)
  106. }
  107. it('include (string)', done => {
  108. const vm = new Vue({
  109. template: `
  110. <div v-if="ok">
  111. <keep-alive include="one">
  112. <component :is="view"></component>
  113. </keep-alive>
  114. </div>
  115. `,
  116. data: {
  117. view: 'one',
  118. ok: true
  119. },
  120. components
  121. }).$mount()
  122. sharedAssertions(vm, done)
  123. })
  124. it('include (regex)', done => {
  125. const vm = new Vue({
  126. template: `
  127. <div v-if="ok">
  128. <keep-alive :include="/^one$/">
  129. <component :is="view"></component>
  130. </keep-alive>
  131. </div>
  132. `,
  133. data: {
  134. view: 'one',
  135. ok: true
  136. },
  137. components
  138. }).$mount()
  139. sharedAssertions(vm, done)
  140. })
  141. it('exclude (string)', done => {
  142. const vm = new Vue({
  143. template: `
  144. <div v-if="ok">
  145. <keep-alive exclude="two">
  146. <component :is="view"></component>
  147. </keep-alive>
  148. </div>
  149. `,
  150. data: {
  151. view: 'one',
  152. ok: true
  153. },
  154. components
  155. }).$mount()
  156. sharedAssertions(vm, done)
  157. })
  158. it('exclude (regex)', done => {
  159. const vm = new Vue({
  160. template: `
  161. <div v-if="ok">
  162. <keep-alive :exclude="/^two$/">
  163. <component :is="view"></component>
  164. </keep-alive>
  165. </div>
  166. `,
  167. data: {
  168. view: 'one',
  169. ok: true
  170. },
  171. components
  172. }).$mount()
  173. sharedAssertions(vm, done)
  174. })
  175. it('include + exclude', done => {
  176. const vm = new Vue({
  177. template: `
  178. <div v-if="ok">
  179. <keep-alive include="one,two" exclude="two">
  180. <component :is="view"></component>
  181. </keep-alive>
  182. </div>
  183. `,
  184. data: {
  185. view: 'one',
  186. ok: true
  187. },
  188. components
  189. }).$mount()
  190. sharedAssertions(vm, done)
  191. })
  192. it('prune cache on include/exclude change', done => {
  193. const vm = new Vue({
  194. template: `
  195. <div>
  196. <keep-alive :include="include">
  197. <component :is="view"></component>
  198. </keep-alive>
  199. </div>
  200. `,
  201. data: {
  202. view: 'one',
  203. include: 'one,two'
  204. },
  205. components
  206. }).$mount()
  207. vm.view = 'two'
  208. waitForUpdate(() => {
  209. assertHookCalls(one, [1, 1, 1, 1, 0])
  210. assertHookCalls(two, [1, 1, 1, 0, 0])
  211. vm.include = 'two'
  212. vm.view = 'one'
  213. }).then(() => {
  214. assertHookCalls(one, [2, 2, 1, 1, 1])
  215. assertHookCalls(two, [1, 1, 1, 1, 0])
  216. }).then(done)
  217. })
  218. // #3882
  219. it('deeply nested keep-alive should be destroyed properly', done => {
  220. one.template = `<div><keep-alive><two></two></keep-alive></div>`
  221. one.components = { two }
  222. const vm = new Vue({
  223. template: `<div><parent v-if="ok"></parent></div>`,
  224. data: { ok: true },
  225. components: {
  226. parent: {
  227. template: `<div><keep-alive><one></one></keep-alive></div>`,
  228. components: { one }
  229. }
  230. }
  231. }).$mount()
  232. assertHookCalls(one, [1, 1, 1, 0, 0])
  233. assertHookCalls(two, [1, 1, 1, 0, 0])
  234. vm.ok = false
  235. waitForUpdate(() => {
  236. assertHookCalls(one, [1, 1, 1, 1, 1])
  237. assertHookCalls(two, [1, 1, 1, 1, 1])
  238. }).then(done)
  239. })
  240. // #4237
  241. it('should update latest props/listners for a re-activated component', done => {
  242. const one = {
  243. props: ['prop'],
  244. template: `<div>one {{ prop }}</div>`
  245. }
  246. const two = {
  247. props: ['prop'],
  248. template: `<div>two {{ prop }}</div>`
  249. }
  250. const vm = new Vue({
  251. data: { view: 'one', n: 1 },
  252. template: `
  253. <div>
  254. <keep-alive>
  255. <component :is="view" :prop="n"></component>
  256. </keep-alive>
  257. </div>
  258. `,
  259. components: { one, two }
  260. }).$mount()
  261. expect(vm.$el.textContent).toBe('one 1')
  262. vm.n++
  263. waitForUpdate(() => {
  264. expect(vm.$el.textContent).toBe('one 2')
  265. vm.view = 'two'
  266. }).then(() => {
  267. expect(vm.$el.textContent).toBe('two 2')
  268. }).then(done)
  269. })
  270. if (!isIE9) {
  271. it('with transition-mode out-in', done => {
  272. let next
  273. const vm = new Vue({
  274. template: `<div>
  275. <transition name="test" mode="out-in" @after-leave="afterLeave">
  276. <keep-alive>
  277. <component :is="view" class="test"></component>
  278. </keep-alive>
  279. </transition>
  280. </div>`,
  281. data: {
  282. view: 'one'
  283. },
  284. components,
  285. methods: {
  286. afterLeave () {
  287. next()
  288. }
  289. }
  290. }).$mount(el)
  291. expect(vm.$el.textContent).toBe('one')
  292. assertHookCalls(one, [1, 1, 1, 0, 0])
  293. assertHookCalls(two, [0, 0, 0, 0, 0])
  294. vm.view = 'two'
  295. waitForUpdate(() => {
  296. expect(vm.$el.innerHTML).toBe(
  297. '<div class="test test-leave test-leave-active">one</div><!---->'
  298. )
  299. assertHookCalls(one, [1, 1, 1, 1, 0])
  300. assertHookCalls(two, [0, 0, 0, 0, 0])
  301. }).thenWaitFor(nextFrame).then(() => {
  302. expect(vm.$el.innerHTML).toBe(
  303. '<div class="test test-leave-active test-leave-to">one</div><!---->'
  304. )
  305. }).thenWaitFor(_next => { next = _next }).then(() => {
  306. expect(vm.$el.innerHTML).toBe('<!---->')
  307. }).thenWaitFor(nextFrame).then(() => {
  308. expect(vm.$el.innerHTML).toBe(
  309. '<div class="test test-enter test-enter-active">two</div>'
  310. )
  311. assertHookCalls(one, [1, 1, 1, 1, 0])
  312. assertHookCalls(two, [1, 1, 1, 0, 0])
  313. }).thenWaitFor(nextFrame).then(() => {
  314. expect(vm.$el.innerHTML).toBe(
  315. '<div class="test test-enter-active test-enter-to">two</div>'
  316. )
  317. }).thenWaitFor(duration + buffer).then(() => {
  318. expect(vm.$el.innerHTML).toBe(
  319. '<div class="test">two</div>'
  320. )
  321. assertHookCalls(one, [1, 1, 1, 1, 0])
  322. assertHookCalls(two, [1, 1, 1, 0, 0])
  323. }).then(() => {
  324. vm.view = 'one'
  325. }).then(() => {
  326. expect(vm.$el.innerHTML).toBe(
  327. '<div class="test test-leave test-leave-active">two</div><!---->'
  328. )
  329. assertHookCalls(one, [1, 1, 1, 1, 0])
  330. assertHookCalls(two, [1, 1, 1, 1, 0])
  331. }).thenWaitFor(nextFrame).then(() => {
  332. expect(vm.$el.innerHTML).toBe(
  333. '<div class="test test-leave-active test-leave-to">two</div><!---->'
  334. )
  335. }).thenWaitFor(_next => { next = _next }).then(() => {
  336. expect(vm.$el.innerHTML).toBe('<!---->')
  337. }).thenWaitFor(nextFrame).then(() => {
  338. expect(vm.$el.innerHTML).toBe(
  339. '<div class="test test-enter test-enter-active">one</div>'
  340. )
  341. assertHookCalls(one, [1, 1, 2, 1, 0])
  342. assertHookCalls(two, [1, 1, 1, 1, 0])
  343. }).thenWaitFor(nextFrame).then(() => {
  344. expect(vm.$el.innerHTML).toBe(
  345. '<div class="test test-enter-active test-enter-to">one</div>'
  346. )
  347. }).thenWaitFor(duration + buffer).then(() => {
  348. expect(vm.$el.innerHTML).toBe(
  349. '<div class="test">one</div>'
  350. )
  351. assertHookCalls(one, [1, 1, 2, 1, 0])
  352. assertHookCalls(two, [1, 1, 1, 1, 0])
  353. }).then(done)
  354. })
  355. it('with transition-mode in-out', done => {
  356. let next
  357. const vm = new Vue({
  358. template: `<div>
  359. <transition name="test" mode="in-out" @after-enter="afterEnter">
  360. <keep-alive>
  361. <component :is="view" class="test"></component>
  362. </keep-alive>
  363. </transition>
  364. </div>`,
  365. data: {
  366. view: 'one'
  367. },
  368. components,
  369. methods: {
  370. afterEnter () {
  371. next()
  372. }
  373. }
  374. }).$mount(el)
  375. expect(vm.$el.textContent).toBe('one')
  376. assertHookCalls(one, [1, 1, 1, 0, 0])
  377. assertHookCalls(two, [0, 0, 0, 0, 0])
  378. vm.view = 'two'
  379. waitForUpdate(() => {
  380. expect(vm.$el.innerHTML).toBe(
  381. '<div class="test">one</div>' +
  382. '<div class="test test-enter test-enter-active">two</div>'
  383. )
  384. assertHookCalls(one, [1, 1, 1, 1, 0])
  385. assertHookCalls(two, [1, 1, 1, 0, 0])
  386. }).thenWaitFor(nextFrame).then(() => {
  387. expect(vm.$el.innerHTML).toBe(
  388. '<div class="test">one</div>' +
  389. '<div class="test test-enter-active test-enter-to">two</div>'
  390. )
  391. }).thenWaitFor(_next => { next = _next }).then(() => {
  392. expect(vm.$el.innerHTML).toBe(
  393. '<div class="test">one</div>' +
  394. '<div class="test">two</div>'
  395. )
  396. }).then(() => {
  397. expect(vm.$el.innerHTML).toBe(
  398. '<div class="test test-leave test-leave-active">one</div>' +
  399. '<div class="test">two</div>'
  400. )
  401. }).thenWaitFor(nextFrame).then(() => {
  402. expect(vm.$el.innerHTML).toBe(
  403. '<div class="test test-leave-active test-leave-to">one</div>' +
  404. '<div class="test">two</div>'
  405. )
  406. }).thenWaitFor(duration + buffer).then(() => {
  407. expect(vm.$el.innerHTML).toBe(
  408. '<div class="test">two</div>'
  409. )
  410. assertHookCalls(one, [1, 1, 1, 1, 0])
  411. assertHookCalls(two, [1, 1, 1, 0, 0])
  412. }).then(() => {
  413. vm.view = 'one'
  414. }).then(() => {
  415. expect(vm.$el.innerHTML).toBe(
  416. '<div class="test">two</div>' +
  417. '<div class="test test-enter test-enter-active">one</div>'
  418. )
  419. assertHookCalls(one, [1, 1, 2, 1, 0])
  420. assertHookCalls(two, [1, 1, 1, 1, 0])
  421. }).thenWaitFor(nextFrame).then(() => {
  422. expect(vm.$el.innerHTML).toBe(
  423. '<div class="test">two</div>' +
  424. '<div class="test test-enter-active test-enter-to">one</div>'
  425. )
  426. }).thenWaitFor(_next => { next = _next }).then(() => {
  427. expect(vm.$el.innerHTML).toBe(
  428. '<div class="test">two</div>' +
  429. '<div class="test">one</div>'
  430. )
  431. }).then(() => {
  432. expect(vm.$el.innerHTML).toBe(
  433. '<div class="test test-leave test-leave-active">two</div>' +
  434. '<div class="test">one</div>'
  435. )
  436. }).thenWaitFor(nextFrame).then(() => {
  437. expect(vm.$el.innerHTML).toBe(
  438. '<div class="test test-leave-active test-leave-to">two</div>' +
  439. '<div class="test">one</div>'
  440. )
  441. }).thenWaitFor(duration + buffer).then(() => {
  442. expect(vm.$el.innerHTML).toBe(
  443. '<div class="test">one</div>'
  444. )
  445. assertHookCalls(one, [1, 1, 2, 1, 0])
  446. assertHookCalls(two, [1, 1, 1, 1, 0])
  447. }).then(done)
  448. })
  449. it('dynamic components, in-out with early cancel', done => {
  450. let next
  451. const vm = new Vue({
  452. template: `<div>
  453. <transition name="test" mode="in-out" @after-enter="afterEnter">
  454. <keep-alive>
  455. <component :is="view" class="test"></component>
  456. </keep-alive>
  457. </transition>
  458. </div>`,
  459. data: { view: 'one' },
  460. components,
  461. methods: {
  462. afterEnter () {
  463. next()
  464. }
  465. }
  466. }).$mount(el)
  467. expect(vm.$el.textContent).toBe('one')
  468. vm.view = 'two'
  469. waitForUpdate(() => {
  470. expect(vm.$el.innerHTML).toBe(
  471. '<div class="test">one</div>' +
  472. '<div class="test test-enter test-enter-active">two</div>'
  473. )
  474. }).thenWaitFor(nextFrame).then(() => {
  475. expect(vm.$el.innerHTML).toBe(
  476. '<div class="test">one</div>' +
  477. '<div class="test test-enter-active test-enter-to">two</div>'
  478. )
  479. // switch again before enter finishes,
  480. // this cancels both enter and leave.
  481. vm.view = 'one'
  482. }).then(() => {
  483. // 1. the pending leaving "one" should be removed instantly.
  484. // 2. the entering "two" should be placed into its final state instantly.
  485. // 3. a new "one" is created and entering
  486. expect(vm.$el.innerHTML).toBe(
  487. '<div class="test">two</div>' +
  488. '<div class="test test-enter test-enter-active">one</div>'
  489. )
  490. }).thenWaitFor(nextFrame).then(() => {
  491. expect(vm.$el.innerHTML).toBe(
  492. '<div class="test">two</div>' +
  493. '<div class="test test-enter-active test-enter-to">one</div>'
  494. )
  495. }).thenWaitFor(_next => { next = _next }).then(() => {
  496. expect(vm.$el.innerHTML).toBe(
  497. '<div class="test">two</div>' +
  498. '<div class="test">one</div>'
  499. )
  500. }).then(() => {
  501. expect(vm.$el.innerHTML).toBe(
  502. '<div class="test test-leave test-leave-active">two</div>' +
  503. '<div class="test">one</div>'
  504. )
  505. }).thenWaitFor(nextFrame).then(() => {
  506. expect(vm.$el.innerHTML).toBe(
  507. '<div class="test test-leave-active test-leave-to">two</div>' +
  508. '<div class="test">one</div>'
  509. )
  510. }).thenWaitFor(duration + buffer).then(() => {
  511. expect(vm.$el.innerHTML).toBe(
  512. '<div class="test">one</div>'
  513. )
  514. }).then(done).then(done)
  515. })
  516. // #4339
  517. it('component with inner transition', done => {
  518. const vm = new Vue({
  519. template: `
  520. <div>
  521. <keep-alive>
  522. <component ref="test" :is="view"></component>
  523. </keep-alive>
  524. </div>
  525. `,
  526. data: { view: 'foo' },
  527. components: {
  528. foo: { template: '<transition><div class="test">foo</div></transition>' },
  529. bar: { template: '<transition name="test"><div class="test">bar</div></transition>' }
  530. }
  531. }).$mount(el)
  532. // should not apply transition on initial render by default
  533. expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
  534. vm.view = 'bar'
  535. waitForUpdate(() => {
  536. expect(vm.$el.innerHTML).toBe(
  537. '<div class="test v-leave v-leave-active">foo</div>' +
  538. '<div class="test test-enter test-enter-active">bar</div>'
  539. )
  540. }).thenWaitFor(nextFrame).then(() => {
  541. expect(vm.$el.innerHTML).toBe(
  542. '<div class="test v-leave-active v-leave-to">foo</div>' +
  543. '<div class="test test-enter-active test-enter-to">bar</div>'
  544. )
  545. }).thenWaitFor(duration + buffer).then(() => {
  546. expect(vm.$el.innerHTML).toBe(
  547. '<div class="test">bar</div>'
  548. )
  549. vm.view = 'foo'
  550. }).then(() => {
  551. expect(vm.$el.innerHTML).toBe(
  552. '<div class="test test-leave test-leave-active">bar</div>' +
  553. '<div class="test v-enter v-enter-active">foo</div>'
  554. )
  555. }).thenWaitFor(nextFrame).then(() => {
  556. expect(vm.$el.innerHTML).toBe(
  557. '<div class="test test-leave-active test-leave-to">bar</div>' +
  558. '<div class="test v-enter-active v-enter-to">foo</div>'
  559. )
  560. }).thenWaitFor(duration + buffer).then(() => {
  561. expect(vm.$el.innerHTML).toBe(
  562. '<div class="test">foo</div>'
  563. )
  564. }).then(done)
  565. })
  566. }
  567. })