component-keep-alive.spec.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285
  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. it('should invoke hooks on the entire sub tree', done => {
  82. one.template = '<two/>'
  83. one.components = { two }
  84. const vm = new Vue({
  85. template: `
  86. <div>
  87. <keep-alive>
  88. <one v-if="ok"/>
  89. </keep-alive>
  90. </div>
  91. `,
  92. data: {
  93. ok: true
  94. },
  95. components
  96. }).$mount()
  97. expect(vm.$el.textContent).toBe('two')
  98. assertHookCalls(one, [1, 1, 1, 0, 0])
  99. assertHookCalls(two, [1, 1, 1, 0, 0])
  100. vm.ok = false
  101. waitForUpdate(() => {
  102. expect(vm.$el.textContent).toBe('')
  103. assertHookCalls(one, [1, 1, 1, 1, 0])
  104. assertHookCalls(two, [1, 1, 1, 1, 0])
  105. vm.ok = true
  106. }).then(() => {
  107. expect(vm.$el.textContent).toBe('two')
  108. assertHookCalls(one, [1, 1, 2, 1, 0])
  109. assertHookCalls(two, [1, 1, 2, 1, 0])
  110. vm.ok = false
  111. }).then(() => {
  112. expect(vm.$el.textContent).toBe('')
  113. assertHookCalls(one, [1, 1, 2, 2, 0])
  114. assertHookCalls(two, [1, 1, 2, 2, 0])
  115. }).then(done)
  116. })
  117. it('should handle nested keep-alive hooks properly', done => {
  118. one.template = '<keep-alive><two v-if="ok" /></keep-alive>'
  119. one.data = () => ({ ok: true })
  120. one.components = { two }
  121. const vm = new Vue({
  122. template: `
  123. <div>
  124. <keep-alive>
  125. <one v-if="ok" ref="one" />
  126. </keep-alive>
  127. </div>
  128. `,
  129. data: {
  130. ok: true
  131. },
  132. components
  133. }).$mount()
  134. const oneInstance = vm.$refs.one
  135. expect(vm.$el.textContent).toBe('two')
  136. assertHookCalls(one, [1, 1, 1, 0, 0])
  137. assertHookCalls(two, [1, 1, 1, 0, 0])
  138. vm.ok = false
  139. waitForUpdate(() => {
  140. expect(vm.$el.textContent).toBe('')
  141. assertHookCalls(one, [1, 1, 1, 1, 0])
  142. assertHookCalls(two, [1, 1, 1, 1, 0])
  143. }).then(() => {
  144. vm.ok = true
  145. }).then(() => {
  146. expect(vm.$el.textContent).toBe('two')
  147. assertHookCalls(one, [1, 1, 2, 1, 0])
  148. assertHookCalls(two, [1, 1, 2, 1, 0])
  149. }).then(() => {
  150. // toggle sub component when activated
  151. oneInstance.ok = false
  152. }).then(() => {
  153. expect(vm.$el.textContent).toBe('')
  154. assertHookCalls(one, [1, 1, 2, 1, 0])
  155. assertHookCalls(two, [1, 1, 2, 2, 0])
  156. }).then(() => {
  157. oneInstance.ok = true
  158. }).then(() => {
  159. expect(vm.$el.textContent).toBe('two')
  160. assertHookCalls(one, [1, 1, 2, 1, 0])
  161. assertHookCalls(two, [1, 1, 3, 2, 0])
  162. }).then(() => {
  163. vm.ok = false
  164. }).then(() => {
  165. expect(vm.$el.textContent).toBe('')
  166. assertHookCalls(one, [1, 1, 2, 2, 0])
  167. assertHookCalls(two, [1, 1, 3, 3, 0])
  168. }).then(() => {
  169. // toggle sub component when parent is deactivated
  170. oneInstance.ok = false
  171. }).then(() => {
  172. expect(vm.$el.textContent).toBe('')
  173. assertHookCalls(one, [1, 1, 2, 2, 0])
  174. assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
  175. }).then(() => {
  176. oneInstance.ok = true
  177. }).then(() => {
  178. expect(vm.$el.textContent).toBe('')
  179. assertHookCalls(one, [1, 1, 2, 2, 0])
  180. assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
  181. }).then(() => {
  182. vm.ok = true
  183. }).then(() => {
  184. expect(vm.$el.textContent).toBe('two')
  185. assertHookCalls(one, [1, 1, 3, 2, 0])
  186. assertHookCalls(two, [1, 1, 4, 3, 0])
  187. }).then(() => {
  188. oneInstance.ok = false
  189. vm.ok = false
  190. }).then(() => {
  191. expect(vm.$el.textContent).toBe('')
  192. assertHookCalls(one, [1, 1, 3, 3, 0])
  193. assertHookCalls(two, [1, 1, 4, 4, 0])
  194. }).then(() => {
  195. vm.ok = true
  196. }).then(() => {
  197. expect(vm.$el.textContent).toBe('')
  198. assertHookCalls(one, [1, 1, 4, 3, 0])
  199. assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive
  200. }).then(done)
  201. })
  202. function sharedAssertions (vm, done) {
  203. expect(vm.$el.textContent).toBe('one')
  204. assertHookCalls(one, [1, 1, 1, 0, 0])
  205. assertHookCalls(two, [0, 0, 0, 0, 0])
  206. vm.view = 'two'
  207. waitForUpdate(() => {
  208. expect(vm.$el.textContent).toBe('two')
  209. assertHookCalls(one, [1, 1, 1, 1, 0])
  210. assertHookCalls(two, [1, 1, 0, 0, 0])
  211. vm.view = 'one'
  212. }).then(() => {
  213. expect(vm.$el.textContent).toBe('one')
  214. assertHookCalls(one, [1, 1, 2, 1, 0])
  215. assertHookCalls(two, [1, 1, 0, 0, 1])
  216. vm.view = 'two'
  217. }).then(() => {
  218. expect(vm.$el.textContent).toBe('two')
  219. assertHookCalls(one, [1, 1, 2, 2, 0])
  220. assertHookCalls(two, [2, 2, 0, 0, 1])
  221. vm.ok = false // teardown
  222. }).then(() => {
  223. expect(vm.$el.textContent).toBe('')
  224. assertHookCalls(one, [1, 1, 2, 2, 1])
  225. assertHookCalls(two, [2, 2, 0, 0, 2])
  226. }).then(done)
  227. }
  228. it('include (string)', done => {
  229. const vm = new Vue({
  230. template: `
  231. <div v-if="ok">
  232. <keep-alive include="one">
  233. <component :is="view"></component>
  234. </keep-alive>
  235. </div>
  236. `,
  237. data: {
  238. view: 'one',
  239. ok: true
  240. },
  241. components
  242. }).$mount()
  243. sharedAssertions(vm, done)
  244. })
  245. it('include (regex)', done => {
  246. const vm = new Vue({
  247. template: `
  248. <div v-if="ok">
  249. <keep-alive :include="/^one$/">
  250. <component :is="view"></component>
  251. </keep-alive>
  252. </div>
  253. `,
  254. data: {
  255. view: 'one',
  256. ok: true
  257. },
  258. components
  259. }).$mount()
  260. sharedAssertions(vm, done)
  261. })
  262. it('include (array)', done => {
  263. const vm = new Vue({
  264. template: `
  265. <div v-if="ok">
  266. <keep-alive :include="['one']">
  267. <component :is="view"></component>
  268. </keep-alive>
  269. </div>
  270. `,
  271. data: {
  272. view: 'one',
  273. ok: true
  274. },
  275. components
  276. }).$mount()
  277. sharedAssertions(vm, done)
  278. })
  279. it('exclude (string)', done => {
  280. const vm = new Vue({
  281. template: `
  282. <div v-if="ok">
  283. <keep-alive exclude="two">
  284. <component :is="view"></component>
  285. </keep-alive>
  286. </div>
  287. `,
  288. data: {
  289. view: 'one',
  290. ok: true
  291. },
  292. components
  293. }).$mount()
  294. sharedAssertions(vm, done)
  295. })
  296. it('exclude (regex)', done => {
  297. const vm = new Vue({
  298. template: `
  299. <div v-if="ok">
  300. <keep-alive :exclude="/^two$/">
  301. <component :is="view"></component>
  302. </keep-alive>
  303. </div>
  304. `,
  305. data: {
  306. view: 'one',
  307. ok: true
  308. },
  309. components
  310. }).$mount()
  311. sharedAssertions(vm, done)
  312. })
  313. it('exclude (array)', done => {
  314. const vm = new Vue({
  315. template: `
  316. <div v-if="ok">
  317. <keep-alive :exclude="['two']">
  318. <component :is="view"></component>
  319. </keep-alive>
  320. </div>
  321. `,
  322. data: {
  323. view: 'one',
  324. ok: true
  325. },
  326. components
  327. }).$mount()
  328. sharedAssertions(vm, done)
  329. })
  330. it('include + exclude', done => {
  331. const vm = new Vue({
  332. template: `
  333. <div v-if="ok">
  334. <keep-alive include="one,two" exclude="two">
  335. <component :is="view"></component>
  336. </keep-alive>
  337. </div>
  338. `,
  339. data: {
  340. view: 'one',
  341. ok: true
  342. },
  343. components
  344. }).$mount()
  345. sharedAssertions(vm, done)
  346. })
  347. it('prune cache on include/exclude change', done => {
  348. const vm = new Vue({
  349. template: `
  350. <div>
  351. <keep-alive :include="include">
  352. <component :is="view"></component>
  353. </keep-alive>
  354. </div>
  355. `,
  356. data: {
  357. view: 'one',
  358. include: 'one,two'
  359. },
  360. components
  361. }).$mount()
  362. vm.view = 'two'
  363. waitForUpdate(() => {
  364. assertHookCalls(one, [1, 1, 1, 1, 0])
  365. assertHookCalls(two, [1, 1, 1, 0, 0])
  366. vm.include = 'two'
  367. }).then(() => {
  368. assertHookCalls(one, [1, 1, 1, 1, 1])
  369. assertHookCalls(two, [1, 1, 1, 0, 0])
  370. vm.view = 'one'
  371. }).then(() => {
  372. assertHookCalls(one, [2, 2, 1, 1, 1])
  373. assertHookCalls(two, [1, 1, 1, 1, 0])
  374. }).then(done)
  375. })
  376. it('prune cache on include/exclude change + view switch', done => {
  377. const vm = new Vue({
  378. template: `
  379. <div>
  380. <keep-alive :include="include">
  381. <component :is="view"></component>
  382. </keep-alive>
  383. </div>
  384. `,
  385. data: {
  386. view: 'one',
  387. include: 'one,two'
  388. },
  389. components
  390. }).$mount()
  391. vm.view = 'two'
  392. waitForUpdate(() => {
  393. assertHookCalls(one, [1, 1, 1, 1, 0])
  394. assertHookCalls(two, [1, 1, 1, 0, 0])
  395. vm.include = 'one'
  396. vm.view = 'one'
  397. }).then(() => {
  398. assertHookCalls(one, [1, 1, 2, 1, 0])
  399. // two should be pruned
  400. assertHookCalls(two, [1, 1, 1, 1, 1])
  401. }).then(done)
  402. })
  403. it('should not prune currently active instance', done => {
  404. const vm = new Vue({
  405. template: `
  406. <div>
  407. <keep-alive :include="include">
  408. <component :is="view"></component>
  409. </keep-alive>
  410. </div>
  411. `,
  412. data: {
  413. view: 'one',
  414. include: 'one,two'
  415. },
  416. components
  417. }).$mount()
  418. vm.include = 'two'
  419. waitForUpdate(() => {
  420. assertHookCalls(one, [1, 1, 1, 0, 0])
  421. assertHookCalls(two, [0, 0, 0, 0, 0])
  422. vm.view = 'two'
  423. }).then(() => {
  424. assertHookCalls(one, [1, 1, 1, 0, 1])
  425. assertHookCalls(two, [1, 1, 1, 0, 0])
  426. }).then(done)
  427. })
  428. // #3882
  429. it('deeply nested keep-alive should be destroyed properly', done => {
  430. one.template = `<div><keep-alive><two></two></keep-alive></div>`
  431. one.components = { two }
  432. const vm = new Vue({
  433. template: `<div><parent v-if="ok"></parent></div>`,
  434. data: { ok: true },
  435. components: {
  436. parent: {
  437. template: `<div><keep-alive><one></one></keep-alive></div>`,
  438. components: { one }
  439. }
  440. }
  441. }).$mount()
  442. assertHookCalls(one, [1, 1, 1, 0, 0])
  443. assertHookCalls(two, [1, 1, 1, 0, 0])
  444. vm.ok = false
  445. waitForUpdate(() => {
  446. assertHookCalls(one, [1, 1, 1, 1, 1])
  447. assertHookCalls(two, [1, 1, 1, 1, 1])
  448. }).then(done)
  449. })
  450. // #4237
  451. it('should update latest props/listeners for a re-activated component', done => {
  452. const one = {
  453. props: ['prop'],
  454. template: `<div>one {{ prop }}</div>`
  455. }
  456. const two = {
  457. props: ['prop'],
  458. template: `<div>two {{ prop }}</div>`
  459. }
  460. const vm = new Vue({
  461. data: { view: 'one', n: 1 },
  462. template: `
  463. <div>
  464. <keep-alive>
  465. <component :is="view" :prop="n"></component>
  466. </keep-alive>
  467. </div>
  468. `,
  469. components: { one, two }
  470. }).$mount()
  471. expect(vm.$el.textContent).toBe('one 1')
  472. vm.n++
  473. waitForUpdate(() => {
  474. expect(vm.$el.textContent).toBe('one 2')
  475. vm.view = 'two'
  476. }).then(() => {
  477. expect(vm.$el.textContent).toBe('two 2')
  478. }).then(done)
  479. })
  480. it('max', done => {
  481. const spyA = jasmine.createSpy()
  482. const spyB = jasmine.createSpy()
  483. const spyC = jasmine.createSpy()
  484. const spyAD = jasmine.createSpy()
  485. const spyBD = jasmine.createSpy()
  486. const spyCD = jasmine.createSpy()
  487. function assertCount (calls) {
  488. expect([
  489. spyA.calls.count(),
  490. spyAD.calls.count(),
  491. spyB.calls.count(),
  492. spyBD.calls.count(),
  493. spyC.calls.count(),
  494. spyCD.calls.count()
  495. ]).toEqual(calls)
  496. }
  497. const vm = new Vue({
  498. template: `
  499. <keep-alive max="2">
  500. <component :is="n"></component>
  501. </keep-alive>
  502. `,
  503. data: {
  504. n: 'aa'
  505. },
  506. components: {
  507. aa: {
  508. template: '<div>a</div>',
  509. created: spyA,
  510. destroyed: spyAD
  511. },
  512. bb: {
  513. template: '<div>bbb</div>',
  514. created: spyB,
  515. destroyed: spyBD
  516. },
  517. cc: {
  518. template: '<div>ccc</div>',
  519. created: spyC,
  520. destroyed: spyCD
  521. }
  522. }
  523. }).$mount()
  524. assertCount([1, 0, 0, 0, 0, 0])
  525. vm.n = 'bb'
  526. waitForUpdate(() => {
  527. assertCount([1, 0, 1, 0, 0, 0])
  528. vm.n = 'cc'
  529. }).then(() => {
  530. // should prune A because max cache reached
  531. assertCount([1, 1, 1, 0, 1, 0])
  532. vm.n = 'bb'
  533. }).then(() => {
  534. // B should be reused, and made latest
  535. assertCount([1, 1, 1, 0, 1, 0])
  536. vm.n = 'aa'
  537. }).then(() => {
  538. // C should be pruned because B was used last so C is the oldest cached
  539. assertCount([2, 1, 1, 0, 1, 1])
  540. }).then(done)
  541. })
  542. it('max=1', done => {
  543. const spyA = jasmine.createSpy()
  544. const spyB = jasmine.createSpy()
  545. const spyC = jasmine.createSpy()
  546. const spyAD = jasmine.createSpy()
  547. const spyBD = jasmine.createSpy()
  548. const spyCD = jasmine.createSpy()
  549. function assertCount (calls) {
  550. expect([
  551. spyA.calls.count(),
  552. spyAD.calls.count(),
  553. spyB.calls.count(),
  554. spyBD.calls.count(),
  555. spyC.calls.count(),
  556. spyCD.calls.count()
  557. ]).toEqual(calls)
  558. }
  559. const vm = new Vue({
  560. template: `
  561. <keep-alive max="1">
  562. <component :is="n"></component>
  563. </keep-alive>
  564. `,
  565. data: {
  566. n: 'aa'
  567. },
  568. components: {
  569. aa: {
  570. template: '<div>a</div>',
  571. created: spyA,
  572. destroyed: spyAD
  573. },
  574. bb: {
  575. template: '<div>bbb</div>',
  576. created: spyB,
  577. destroyed: spyBD
  578. },
  579. cc: {
  580. template: '<div>ccc</div>',
  581. created: spyC,
  582. destroyed: spyCD
  583. }
  584. }
  585. }).$mount()
  586. assertCount([1, 0, 0, 0, 0, 0])
  587. vm.n = 'bb'
  588. waitForUpdate(() => {
  589. // should prune A because max cache reached
  590. assertCount([1, 1, 1, 0, 0, 0])
  591. vm.n = 'cc'
  592. }).then(() => {
  593. // should prune B because max cache reached
  594. assertCount([1, 1, 1, 1, 1, 0])
  595. vm.n = 'bb'
  596. }).then(() => {
  597. // B is recreated
  598. assertCount([1, 1, 2, 1, 1, 1])
  599. vm.n = 'aa'
  600. }).then(() => {
  601. // B is destroyed and A recreated
  602. assertCount([2, 1, 2, 2, 1, 1])
  603. }).then(done)
  604. })
  605. it('should warn unknown component inside', () => {
  606. new Vue({
  607. template: `<keep-alive><foo/></keep-alive>`
  608. }).$mount()
  609. expect(`Unknown custom element: <foo>`).toHaveBeenWarned()
  610. })
  611. // #6938
  612. it('should not cache anonymous component when include is specified', done => {
  613. const Foo = {
  614. name: 'foo',
  615. template: `<div>foo</div>`,
  616. created: jasmine.createSpy('foo')
  617. }
  618. const Bar = {
  619. template: `<div>bar</div>`,
  620. created: jasmine.createSpy('bar')
  621. }
  622. const Child = {
  623. functional: true,
  624. render (h, ctx) {
  625. return h(ctx.props.view ? Foo : Bar)
  626. }
  627. }
  628. const vm = new Vue({
  629. template: `
  630. <keep-alive include="foo">
  631. <child :view="view"></child>
  632. </keep-alive>
  633. `,
  634. data: {
  635. view: true
  636. },
  637. components: { Child }
  638. }).$mount()
  639. function assert (foo, bar) {
  640. expect(Foo.created.calls.count()).toBe(foo)
  641. expect(Bar.created.calls.count()).toBe(bar)
  642. }
  643. expect(vm.$el.textContent).toBe('foo')
  644. assert(1, 0)
  645. vm.view = false
  646. waitForUpdate(() => {
  647. expect(vm.$el.textContent).toBe('bar')
  648. assert(1, 1)
  649. vm.view = true
  650. }).then(() => {
  651. expect(vm.$el.textContent).toBe('foo')
  652. assert(1, 1)
  653. vm.view = false
  654. }).then(() => {
  655. expect(vm.$el.textContent).toBe('bar')
  656. assert(1, 2)
  657. }).then(done)
  658. })
  659. it('should cache anonymous components if include is not specified', done => {
  660. const Foo = {
  661. template: `<div>foo</div>`,
  662. created: jasmine.createSpy('foo')
  663. }
  664. const Bar = {
  665. template: `<div>bar</div>`,
  666. created: jasmine.createSpy('bar')
  667. }
  668. const Child = {
  669. functional: true,
  670. render (h, ctx) {
  671. return h(ctx.props.view ? Foo : Bar)
  672. }
  673. }
  674. const vm = new Vue({
  675. template: `
  676. <keep-alive>
  677. <child :view="view"></child>
  678. </keep-alive>
  679. `,
  680. data: {
  681. view: true
  682. },
  683. components: { Child }
  684. }).$mount()
  685. function assert (foo, bar) {
  686. expect(Foo.created.calls.count()).toBe(foo)
  687. expect(Bar.created.calls.count()).toBe(bar)
  688. }
  689. expect(vm.$el.textContent).toBe('foo')
  690. assert(1, 0)
  691. vm.view = false
  692. waitForUpdate(() => {
  693. expect(vm.$el.textContent).toBe('bar')
  694. assert(1, 1)
  695. vm.view = true
  696. }).then(() => {
  697. expect(vm.$el.textContent).toBe('foo')
  698. assert(1, 1)
  699. vm.view = false
  700. }).then(() => {
  701. expect(vm.$el.textContent).toBe('bar')
  702. assert(1, 1)
  703. }).then(done)
  704. })
  705. // #7105
  706. it('should not destroy active instance when pruning cache', done => {
  707. const Foo = {
  708. template: `<div>foo</div>`,
  709. destroyed: jasmine.createSpy('destroyed')
  710. }
  711. const vm = new Vue({
  712. template: `
  713. <div>
  714. <keep-alive :include="include">
  715. <foo/>
  716. </keep-alive>
  717. </div>
  718. `,
  719. data: {
  720. include: ['foo']
  721. },
  722. components: { Foo }
  723. }).$mount()
  724. // condition: a render where a previous component is reused
  725. vm.include = ['foo']
  726. waitForUpdate(() => {
  727. vm.include = ['']
  728. }).then(() => {
  729. expect(Foo.destroyed).not.toHaveBeenCalled()
  730. }).then(done)
  731. })
  732. if (!isIE9) {
  733. it('with transition-mode out-in', done => {
  734. let next
  735. const vm = new Vue({
  736. template: `<div>
  737. <transition name="test" mode="out-in" @after-leave="afterLeave">
  738. <keep-alive>
  739. <component :is="view" class="test"></component>
  740. </keep-alive>
  741. </transition>
  742. </div>`,
  743. data: {
  744. view: 'one'
  745. },
  746. components,
  747. methods: {
  748. afterLeave () {
  749. next()
  750. }
  751. }
  752. }).$mount(el)
  753. expect(vm.$el.textContent).toBe('one')
  754. assertHookCalls(one, [1, 1, 1, 0, 0])
  755. assertHookCalls(two, [0, 0, 0, 0, 0])
  756. vm.view = 'two'
  757. waitForUpdate(() => {
  758. expect(vm.$el.innerHTML).toBe(
  759. '<div class="test test-leave test-leave-active">one</div><!---->'
  760. )
  761. assertHookCalls(one, [1, 1, 1, 1, 0])
  762. assertHookCalls(two, [0, 0, 0, 0, 0])
  763. }).thenWaitFor(nextFrame).then(() => {
  764. expect(vm.$el.innerHTML).toBe(
  765. '<div class="test test-leave-active test-leave-to">one</div><!---->'
  766. )
  767. }).thenWaitFor(_next => { next = _next }).then(() => {
  768. expect(vm.$el.innerHTML).toBe('<!---->')
  769. }).thenWaitFor(nextFrame).then(() => {
  770. expect(vm.$el.innerHTML).toBe(
  771. '<div class="test test-enter test-enter-active">two</div>'
  772. )
  773. assertHookCalls(one, [1, 1, 1, 1, 0])
  774. assertHookCalls(two, [1, 1, 1, 0, 0])
  775. }).thenWaitFor(nextFrame).then(() => {
  776. expect(vm.$el.innerHTML).toBe(
  777. '<div class="test test-enter-active test-enter-to">two</div>'
  778. )
  779. }).thenWaitFor(duration + buffer).then(() => {
  780. expect(vm.$el.innerHTML).toBe(
  781. '<div class="test">two</div>'
  782. )
  783. assertHookCalls(one, [1, 1, 1, 1, 0])
  784. assertHookCalls(two, [1, 1, 1, 0, 0])
  785. }).then(() => {
  786. vm.view = 'one'
  787. }).then(() => {
  788. expect(vm.$el.innerHTML).toBe(
  789. '<div class="test test-leave test-leave-active">two</div><!---->'
  790. )
  791. assertHookCalls(one, [1, 1, 1, 1, 0])
  792. assertHookCalls(two, [1, 1, 1, 1, 0])
  793. }).thenWaitFor(nextFrame).then(() => {
  794. expect(vm.$el.innerHTML).toBe(
  795. '<div class="test test-leave-active test-leave-to">two</div><!---->'
  796. )
  797. }).thenWaitFor(_next => { next = _next }).then(() => {
  798. expect(vm.$el.innerHTML).toBe('<!---->')
  799. }).thenWaitFor(nextFrame).then(() => {
  800. expect(vm.$el.innerHTML).toBe(
  801. '<div class="test test-enter test-enter-active">one</div>'
  802. )
  803. assertHookCalls(one, [1, 1, 2, 1, 0])
  804. assertHookCalls(two, [1, 1, 1, 1, 0])
  805. }).thenWaitFor(nextFrame).then(() => {
  806. expect(vm.$el.innerHTML).toBe(
  807. '<div class="test test-enter-active test-enter-to">one</div>'
  808. )
  809. }).thenWaitFor(duration + buffer).then(() => {
  810. expect(vm.$el.innerHTML).toBe(
  811. '<div class="test">one</div>'
  812. )
  813. assertHookCalls(one, [1, 1, 2, 1, 0])
  814. assertHookCalls(two, [1, 1, 1, 1, 0])
  815. }).then(done)
  816. })
  817. it('with transition-mode out-in + include', done => {
  818. let next
  819. const vm = new Vue({
  820. template: `<div>
  821. <transition name="test" mode="out-in" @after-leave="afterLeave">
  822. <keep-alive include="one">
  823. <component :is="view" class="test"></component>
  824. </keep-alive>
  825. </transition>
  826. </div>`,
  827. data: {
  828. view: 'one'
  829. },
  830. components,
  831. methods: {
  832. afterLeave () {
  833. next()
  834. }
  835. }
  836. }).$mount(el)
  837. expect(vm.$el.textContent).toBe('one')
  838. assertHookCalls(one, [1, 1, 1, 0, 0])
  839. assertHookCalls(two, [0, 0, 0, 0, 0])
  840. vm.view = 'two'
  841. waitForUpdate(() => {
  842. expect(vm.$el.innerHTML).toBe(
  843. '<div class="test test-leave test-leave-active">one</div><!---->'
  844. )
  845. assertHookCalls(one, [1, 1, 1, 1, 0])
  846. assertHookCalls(two, [0, 0, 0, 0, 0])
  847. }).thenWaitFor(nextFrame).then(() => {
  848. expect(vm.$el.innerHTML).toBe(
  849. '<div class="test test-leave-active test-leave-to">one</div><!---->'
  850. )
  851. }).thenWaitFor(_next => { next = _next }).then(() => {
  852. expect(vm.$el.innerHTML).toBe('<!---->')
  853. }).thenWaitFor(nextFrame).then(() => {
  854. expect(vm.$el.innerHTML).toBe(
  855. '<div class="test test-enter test-enter-active">two</div>'
  856. )
  857. assertHookCalls(one, [1, 1, 1, 1, 0])
  858. assertHookCalls(two, [1, 1, 0, 0, 0])
  859. }).thenWaitFor(nextFrame).then(() => {
  860. expect(vm.$el.innerHTML).toBe(
  861. '<div class="test test-enter-active test-enter-to">two</div>'
  862. )
  863. }).thenWaitFor(duration + buffer).then(() => {
  864. expect(vm.$el.innerHTML).toBe(
  865. '<div class="test">two</div>'
  866. )
  867. assertHookCalls(one, [1, 1, 1, 1, 0])
  868. assertHookCalls(two, [1, 1, 0, 0, 0])
  869. }).then(() => {
  870. vm.view = 'one'
  871. }).then(() => {
  872. expect(vm.$el.innerHTML).toBe(
  873. '<div class="test test-leave test-leave-active">two</div><!---->'
  874. )
  875. assertHookCalls(one, [1, 1, 1, 1, 0])
  876. assertHookCalls(two, [1, 1, 0, 0, 1])
  877. }).thenWaitFor(nextFrame).then(() => {
  878. expect(vm.$el.innerHTML).toBe(
  879. '<div class="test test-leave-active test-leave-to">two</div><!---->'
  880. )
  881. }).thenWaitFor(_next => { next = _next }).then(() => {
  882. expect(vm.$el.innerHTML).toBe('<!---->')
  883. }).thenWaitFor(nextFrame).then(() => {
  884. expect(vm.$el.innerHTML).toBe(
  885. '<div class="test test-enter test-enter-active">one</div>'
  886. )
  887. assertHookCalls(one, [1, 1, 2, 1, 0])
  888. assertHookCalls(two, [1, 1, 0, 0, 1])
  889. }).thenWaitFor(nextFrame).then(() => {
  890. expect(vm.$el.innerHTML).toBe(
  891. '<div class="test test-enter-active test-enter-to">one</div>'
  892. )
  893. }).thenWaitFor(duration + buffer).then(() => {
  894. expect(vm.$el.innerHTML).toBe(
  895. '<div class="test">one</div>'
  896. )
  897. assertHookCalls(one, [1, 1, 2, 1, 0])
  898. assertHookCalls(two, [1, 1, 0, 0, 1])
  899. }).then(done)
  900. })
  901. it('with transition-mode in-out', done => {
  902. let next
  903. const vm = new Vue({
  904. template: `<div>
  905. <transition name="test" mode="in-out" @after-enter="afterEnter">
  906. <keep-alive>
  907. <component :is="view" class="test"></component>
  908. </keep-alive>
  909. </transition>
  910. </div>`,
  911. data: {
  912. view: 'one'
  913. },
  914. components,
  915. methods: {
  916. afterEnter () {
  917. next()
  918. }
  919. }
  920. }).$mount(el)
  921. expect(vm.$el.textContent).toBe('one')
  922. assertHookCalls(one, [1, 1, 1, 0, 0])
  923. assertHookCalls(two, [0, 0, 0, 0, 0])
  924. vm.view = 'two'
  925. waitForUpdate(() => {
  926. expect(vm.$el.innerHTML).toBe(
  927. '<div class="test">one</div>' +
  928. '<div class="test test-enter test-enter-active">two</div>'
  929. )
  930. assertHookCalls(one, [1, 1, 1, 1, 0])
  931. assertHookCalls(two, [1, 1, 1, 0, 0])
  932. }).thenWaitFor(nextFrame).then(() => {
  933. expect(vm.$el.innerHTML).toBe(
  934. '<div class="test">one</div>' +
  935. '<div class="test test-enter-active test-enter-to">two</div>'
  936. )
  937. }).thenWaitFor(_next => { next = _next }).then(() => {
  938. expect(vm.$el.innerHTML).toBe(
  939. '<div class="test">one</div>' +
  940. '<div class="test">two</div>'
  941. )
  942. }).then(() => {
  943. expect(vm.$el.innerHTML).toBe(
  944. '<div class="test test-leave test-leave-active">one</div>' +
  945. '<div class="test">two</div>'
  946. )
  947. }).thenWaitFor(nextFrame).then(() => {
  948. expect(vm.$el.innerHTML).toBe(
  949. '<div class="test test-leave-active test-leave-to">one</div>' +
  950. '<div class="test">two</div>'
  951. )
  952. }).thenWaitFor(duration + buffer).then(() => {
  953. expect(vm.$el.innerHTML).toBe(
  954. '<div class="test">two</div>'
  955. )
  956. assertHookCalls(one, [1, 1, 1, 1, 0])
  957. assertHookCalls(two, [1, 1, 1, 0, 0])
  958. }).then(() => {
  959. vm.view = 'one'
  960. }).then(() => {
  961. expect(vm.$el.innerHTML).toBe(
  962. '<div class="test">two</div>' +
  963. '<div class="test test-enter test-enter-active">one</div>'
  964. )
  965. assertHookCalls(one, [1, 1, 2, 1, 0])
  966. assertHookCalls(two, [1, 1, 1, 1, 0])
  967. }).thenWaitFor(nextFrame).then(() => {
  968. expect(vm.$el.innerHTML).toBe(
  969. '<div class="test">two</div>' +
  970. '<div class="test test-enter-active test-enter-to">one</div>'
  971. )
  972. }).thenWaitFor(_next => { next = _next }).then(() => {
  973. expect(vm.$el.innerHTML).toBe(
  974. '<div class="test">two</div>' +
  975. '<div class="test">one</div>'
  976. )
  977. }).then(() => {
  978. expect(vm.$el.innerHTML).toBe(
  979. '<div class="test test-leave test-leave-active">two</div>' +
  980. '<div class="test">one</div>'
  981. )
  982. }).thenWaitFor(nextFrame).then(() => {
  983. expect(vm.$el.innerHTML).toBe(
  984. '<div class="test test-leave-active test-leave-to">two</div>' +
  985. '<div class="test">one</div>'
  986. )
  987. }).thenWaitFor(duration + buffer).then(() => {
  988. expect(vm.$el.innerHTML).toBe(
  989. '<div class="test">one</div>'
  990. )
  991. assertHookCalls(one, [1, 1, 2, 1, 0])
  992. assertHookCalls(two, [1, 1, 1, 1, 0])
  993. }).then(done)
  994. })
  995. it('dynamic components, in-out with early cancel', done => {
  996. let next
  997. const vm = new Vue({
  998. template: `<div>
  999. <transition name="test" mode="in-out" @after-enter="afterEnter">
  1000. <keep-alive>
  1001. <component :is="view" class="test"></component>
  1002. </keep-alive>
  1003. </transition>
  1004. </div>`,
  1005. data: { view: 'one' },
  1006. components,
  1007. methods: {
  1008. afterEnter () {
  1009. next()
  1010. }
  1011. }
  1012. }).$mount(el)
  1013. expect(vm.$el.textContent).toBe('one')
  1014. vm.view = 'two'
  1015. waitForUpdate(() => {
  1016. expect(vm.$el.innerHTML).toBe(
  1017. '<div class="test">one</div>' +
  1018. '<div class="test test-enter test-enter-active">two</div>'
  1019. )
  1020. }).thenWaitFor(nextFrame).then(() => {
  1021. expect(vm.$el.innerHTML).toBe(
  1022. '<div class="test">one</div>' +
  1023. '<div class="test test-enter-active test-enter-to">two</div>'
  1024. )
  1025. // switch again before enter finishes,
  1026. // this cancels both enter and leave.
  1027. vm.view = 'one'
  1028. }).then(() => {
  1029. // 1. the pending leaving "one" should be removed instantly.
  1030. // 2. the entering "two" should be placed into its final state instantly.
  1031. // 3. a new "one" is created and entering
  1032. expect(vm.$el.innerHTML).toBe(
  1033. '<div class="test">two</div>' +
  1034. '<div class="test test-enter test-enter-active">one</div>'
  1035. )
  1036. }).thenWaitFor(nextFrame).then(() => {
  1037. expect(vm.$el.innerHTML).toBe(
  1038. '<div class="test">two</div>' +
  1039. '<div class="test test-enter-active test-enter-to">one</div>'
  1040. )
  1041. }).thenWaitFor(_next => { next = _next }).then(() => {
  1042. expect(vm.$el.innerHTML).toBe(
  1043. '<div class="test">two</div>' +
  1044. '<div class="test">one</div>'
  1045. )
  1046. }).then(() => {
  1047. expect(vm.$el.innerHTML).toBe(
  1048. '<div class="test test-leave test-leave-active">two</div>' +
  1049. '<div class="test">one</div>'
  1050. )
  1051. }).thenWaitFor(nextFrame).then(() => {
  1052. expect(vm.$el.innerHTML).toBe(
  1053. '<div class="test test-leave-active test-leave-to">two</div>' +
  1054. '<div class="test">one</div>'
  1055. )
  1056. }).thenWaitFor(duration + buffer).then(() => {
  1057. expect(vm.$el.innerHTML).toBe(
  1058. '<div class="test">one</div>'
  1059. )
  1060. }).then(done)
  1061. })
  1062. // #4339
  1063. it('component with inner transition', done => {
  1064. const vm = new Vue({
  1065. template: `
  1066. <div>
  1067. <keep-alive>
  1068. <component ref="test" :is="view"></component>
  1069. </keep-alive>
  1070. </div>
  1071. `,
  1072. data: { view: 'foo' },
  1073. components: {
  1074. foo: { template: '<transition><div class="test">foo</div></transition>' },
  1075. bar: { template: '<transition name="test"><div class="test">bar</div></transition>' }
  1076. }
  1077. }).$mount(el)
  1078. // should not apply transition on initial render by default
  1079. expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
  1080. vm.view = 'bar'
  1081. waitForUpdate(() => {
  1082. expect(vm.$el.innerHTML).toBe(
  1083. '<div class="test v-leave v-leave-active">foo</div>' +
  1084. '<div class="test test-enter test-enter-active">bar</div>'
  1085. )
  1086. }).thenWaitFor(nextFrame).then(() => {
  1087. expect(vm.$el.innerHTML).toBe(
  1088. '<div class="test v-leave-active v-leave-to">foo</div>' +
  1089. '<div class="test test-enter-active test-enter-to">bar</div>'
  1090. )
  1091. }).thenWaitFor(duration + buffer).then(() => {
  1092. expect(vm.$el.innerHTML).toBe(
  1093. '<div class="test">bar</div>'
  1094. )
  1095. vm.view = 'foo'
  1096. }).then(() => {
  1097. expect(vm.$el.innerHTML).toBe(
  1098. '<div class="test test-leave test-leave-active">bar</div>' +
  1099. '<div class="test v-enter v-enter-active">foo</div>'
  1100. )
  1101. }).thenWaitFor(nextFrame).then(() => {
  1102. expect(vm.$el.innerHTML).toBe(
  1103. '<div class="test test-leave-active test-leave-to">bar</div>' +
  1104. '<div class="test v-enter-active v-enter-to">foo</div>'
  1105. )
  1106. }).thenWaitFor(duration + buffer).then(() => {
  1107. expect(vm.$el.innerHTML).toBe(
  1108. '<div class="test">foo</div>'
  1109. )
  1110. }).then(done)
  1111. })
  1112. it('async components with transition-mode out-in', done => {
  1113. const barResolve = jasmine.createSpy('bar resolved')
  1114. let next
  1115. const foo = (resolve) => {
  1116. setTimeout(() => {
  1117. resolve(one)
  1118. Vue.nextTick(next)
  1119. }, duration / 2)
  1120. }
  1121. const bar = (resolve) => {
  1122. setTimeout(() => {
  1123. resolve(two)
  1124. barResolve()
  1125. }, duration / 2)
  1126. }
  1127. components = {
  1128. foo,
  1129. bar
  1130. }
  1131. const vm = new Vue({
  1132. template: `<div>
  1133. <transition name="test" mode="out-in" @after-enter="afterEnter" @after-leave="afterLeave">
  1134. <keep-alive>
  1135. <component :is="view" class="test"></component>
  1136. </keep-alive>
  1137. </transition>
  1138. </div>`,
  1139. data: {
  1140. view: 'foo'
  1141. },
  1142. components,
  1143. methods: {
  1144. afterEnter () {
  1145. next()
  1146. },
  1147. afterLeave () {
  1148. next()
  1149. }
  1150. }
  1151. }).$mount(el)
  1152. expect(vm.$el.textContent).toBe('')
  1153. next = () => {
  1154. assertHookCalls(one, [1, 1, 1, 0, 0])
  1155. assertHookCalls(two, [0, 0, 0, 0, 0])
  1156. waitForUpdate(() => {
  1157. expect(vm.$el.innerHTML).toBe(
  1158. '<div class="test test-enter test-enter-active">one</div>'
  1159. )
  1160. }).thenWaitFor(nextFrame).then(() => {
  1161. expect(vm.$el.innerHTML).toBe(
  1162. '<div class="test test-enter-active test-enter-to">one</div>'
  1163. )
  1164. }).thenWaitFor(_next => { next = _next }).then(() => {
  1165. // foo afterEnter get called
  1166. expect(vm.$el.innerHTML).toBe('<div class="test">one</div>')
  1167. vm.view = 'bar'
  1168. }).thenWaitFor(nextFrame).then(() => {
  1169. assertHookCalls(one, [1, 1, 1, 1, 0])
  1170. assertHookCalls(two, [0, 0, 0, 0, 0])
  1171. expect(vm.$el.innerHTML).toBe(
  1172. '<div class="test test-leave-active test-leave-to">one</div><!---->'
  1173. )
  1174. }).thenWaitFor(_next => { next = _next }).then(() => {
  1175. // foo afterLeave get called
  1176. // and bar has already been resolved before afterLeave get called
  1177. expect(barResolve.calls.count()).toBe(1)
  1178. expect(vm.$el.innerHTML).toBe('<!---->')
  1179. }).thenWaitFor(nextFrame).then(() => {
  1180. expect(vm.$el.innerHTML).toBe(
  1181. '<div class="test test-enter test-enter-active">two</div>'
  1182. )
  1183. assertHookCalls(one, [1, 1, 1, 1, 0])
  1184. assertHookCalls(two, [1, 1, 1, 0, 0])
  1185. }).thenWaitFor(nextFrame).then(() => {
  1186. expect(vm.$el.innerHTML).toBe(
  1187. '<div class="test test-enter-active test-enter-to">two</div>'
  1188. )
  1189. }).thenWaitFor(_next => { next = _next }).then(() => {
  1190. // bar afterEnter get called
  1191. expect(vm.$el.innerHTML).toBe('<div class="test">two</div>')
  1192. }).then(done)
  1193. }
  1194. })
  1195. // #10083
  1196. it('should not attach event handler repeatedly', done => {
  1197. const vm = new Vue({
  1198. template: `
  1199. <keep-alive>
  1200. <btn v-if="showBtn" @click.native="add" />
  1201. </keep-alive>
  1202. `,
  1203. data: { showBtn: true, n: 0 },
  1204. methods: {
  1205. add () {
  1206. this.n++
  1207. }
  1208. },
  1209. components: {
  1210. btn: { template: '<button>add 1</button>' }
  1211. }
  1212. }).$mount()
  1213. const btn = vm.$el
  1214. expect(vm.n).toBe(0)
  1215. btn.click()
  1216. expect(vm.n).toBe(1)
  1217. vm.showBtn = false
  1218. waitForUpdate(() => {
  1219. vm.showBtn = true
  1220. }).then(() => {
  1221. btn.click()
  1222. expect(vm.n).toBe(2)
  1223. }).then(done)
  1224. })
  1225. }
  1226. })