keepAlive.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. import { ComponentOptions } from '../src/component'
  2. import {
  3. h,
  4. TestElement,
  5. nodeOps,
  6. render,
  7. ref,
  8. KeepAlive,
  9. serializeInner,
  10. nextTick
  11. } from '@vue/runtime-test'
  12. import { KeepAliveProps } from '../src/keepAlive'
  13. describe('keep-alive', () => {
  14. let one: ComponentOptions
  15. let two: ComponentOptions
  16. let views: Record<string, ComponentOptions>
  17. let root: TestElement
  18. beforeEach(() => {
  19. root = nodeOps.createElement('div')
  20. one = {
  21. name: 'one',
  22. data: () => ({ msg: 'one' }),
  23. render() {
  24. return h('div', this.msg)
  25. },
  26. created: jest.fn(),
  27. mounted: jest.fn(),
  28. activated: jest.fn(),
  29. deactivated: jest.fn(),
  30. unmounted: jest.fn()
  31. }
  32. two = {
  33. name: 'two',
  34. data: () => ({ msg: 'two' }),
  35. render() {
  36. return h('div', this.msg)
  37. },
  38. created: jest.fn(),
  39. mounted: jest.fn(),
  40. activated: jest.fn(),
  41. deactivated: jest.fn(),
  42. unmounted: jest.fn()
  43. }
  44. views = {
  45. one,
  46. two
  47. }
  48. })
  49. function assertHookCalls(component: any, callCounts: number[]) {
  50. expect([
  51. component.created.mock.calls.length,
  52. component.mounted.mock.calls.length,
  53. component.activated.mock.calls.length,
  54. component.deactivated.mock.calls.length,
  55. component.unmounted.mock.calls.length
  56. ]).toEqual(callCounts)
  57. }
  58. test('should preserve state', async () => {
  59. const viewRef = ref('one')
  60. const instanceRef = ref<any>(null)
  61. const App = {
  62. render() {
  63. return h(KeepAlive, null, {
  64. default: () => h(views[viewRef.value], { ref: instanceRef })
  65. })
  66. }
  67. }
  68. render(h(App), root)
  69. expect(serializeInner(root)).toBe(`<div>one</div>`)
  70. instanceRef.value.msg = 'changed'
  71. await nextTick()
  72. expect(serializeInner(root)).toBe(`<div>changed</div>`)
  73. viewRef.value = 'two'
  74. await nextTick()
  75. expect(serializeInner(root)).toBe(`<div>two</div>`)
  76. viewRef.value = 'one'
  77. await nextTick()
  78. expect(serializeInner(root)).toBe(`<div>changed</div>`)
  79. })
  80. test('should call correct lifecycle hooks', async () => {
  81. const toggle = ref(true)
  82. const viewRef = ref('one')
  83. const App = {
  84. render() {
  85. return toggle.value ? h(KeepAlive, () => h(views[viewRef.value])) : null
  86. }
  87. }
  88. render(h(App), root)
  89. expect(serializeInner(root)).toBe(`<div>one</div>`)
  90. assertHookCalls(one, [1, 1, 1, 0, 0])
  91. assertHookCalls(two, [0, 0, 0, 0, 0])
  92. // toggle kept-alive component
  93. viewRef.value = 'two'
  94. await nextTick()
  95. expect(serializeInner(root)).toBe(`<div>two</div>`)
  96. assertHookCalls(one, [1, 1, 1, 1, 0])
  97. assertHookCalls(two, [1, 1, 1, 0, 0])
  98. viewRef.value = 'one'
  99. await nextTick()
  100. expect(serializeInner(root)).toBe(`<div>one</div>`)
  101. assertHookCalls(one, [1, 1, 2, 1, 0])
  102. assertHookCalls(two, [1, 1, 1, 1, 0])
  103. viewRef.value = 'two'
  104. await nextTick()
  105. expect(serializeInner(root)).toBe(`<div>two</div>`)
  106. assertHookCalls(one, [1, 1, 2, 2, 0])
  107. assertHookCalls(two, [1, 1, 2, 1, 0])
  108. // teardown keep-alive, should unmount all components including cached
  109. toggle.value = false
  110. await nextTick()
  111. expect(serializeInner(root)).toBe(`<!---->`)
  112. assertHookCalls(one, [1, 1, 2, 2, 1])
  113. assertHookCalls(two, [1, 1, 2, 2, 1])
  114. })
  115. test('should call lifecycle hooks on nested components', async () => {
  116. one.render = () => h(two)
  117. const toggle = ref(true)
  118. const App = {
  119. render() {
  120. return h(KeepAlive, () => (toggle.value ? h(one) : null))
  121. }
  122. }
  123. render(h(App), root)
  124. expect(serializeInner(root)).toBe(`<div>two</div>`)
  125. assertHookCalls(one, [1, 1, 1, 0, 0])
  126. assertHookCalls(two, [1, 1, 1, 0, 0])
  127. toggle.value = false
  128. await nextTick()
  129. expect(serializeInner(root)).toBe(`<!---->`)
  130. assertHookCalls(one, [1, 1, 1, 1, 0])
  131. assertHookCalls(two, [1, 1, 1, 1, 0])
  132. toggle.value = true
  133. await nextTick()
  134. expect(serializeInner(root)).toBe(`<div>two</div>`)
  135. assertHookCalls(one, [1, 1, 2, 1, 0])
  136. assertHookCalls(two, [1, 1, 2, 1, 0])
  137. toggle.value = false
  138. await nextTick()
  139. expect(serializeInner(root)).toBe(`<!---->`)
  140. assertHookCalls(one, [1, 1, 2, 2, 0])
  141. assertHookCalls(two, [1, 1, 2, 2, 0])
  142. })
  143. test('should call correct hooks for nested keep-alive', async () => {
  144. const toggle2 = ref(true)
  145. one.render = () => h(KeepAlive, () => (toggle2.value ? h(two) : null))
  146. const toggle1 = ref(true)
  147. const App = {
  148. render() {
  149. return h(KeepAlive, () => (toggle1.value ? h(one) : null))
  150. }
  151. }
  152. render(h(App), root)
  153. expect(serializeInner(root)).toBe(`<div>two</div>`)
  154. assertHookCalls(one, [1, 1, 1, 0, 0])
  155. assertHookCalls(two, [1, 1, 1, 0, 0])
  156. toggle1.value = false
  157. await nextTick()
  158. expect(serializeInner(root)).toBe(`<!---->`)
  159. assertHookCalls(one, [1, 1, 1, 1, 0])
  160. assertHookCalls(two, [1, 1, 1, 1, 0])
  161. toggle1.value = true
  162. await nextTick()
  163. expect(serializeInner(root)).toBe(`<div>two</div>`)
  164. assertHookCalls(one, [1, 1, 2, 1, 0])
  165. assertHookCalls(two, [1, 1, 2, 1, 0])
  166. // toggle nested instance
  167. toggle2.value = false
  168. await nextTick()
  169. expect(serializeInner(root)).toBe(`<!---->`)
  170. assertHookCalls(one, [1, 1, 2, 1, 0])
  171. assertHookCalls(two, [1, 1, 2, 2, 0])
  172. toggle2.value = true
  173. await nextTick()
  174. expect(serializeInner(root)).toBe(`<div>two</div>`)
  175. assertHookCalls(one, [1, 1, 2, 1, 0])
  176. assertHookCalls(two, [1, 1, 3, 2, 0])
  177. toggle1.value = false
  178. await nextTick()
  179. expect(serializeInner(root)).toBe(`<!---->`)
  180. assertHookCalls(one, [1, 1, 2, 2, 0])
  181. assertHookCalls(two, [1, 1, 3, 3, 0])
  182. // toggle nested instance when parent is deactivated
  183. toggle2.value = false
  184. await nextTick()
  185. expect(serializeInner(root)).toBe(`<!---->`)
  186. assertHookCalls(one, [1, 1, 2, 2, 0])
  187. assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
  188. toggle2.value = true
  189. await nextTick()
  190. expect(serializeInner(root)).toBe(`<!---->`)
  191. assertHookCalls(one, [1, 1, 2, 2, 0])
  192. assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
  193. toggle1.value = true
  194. await nextTick()
  195. expect(serializeInner(root)).toBe(`<div>two</div>`)
  196. assertHookCalls(one, [1, 1, 3, 2, 0])
  197. assertHookCalls(two, [1, 1, 4, 3, 0])
  198. toggle1.value = false
  199. toggle2.value = false
  200. await nextTick()
  201. expect(serializeInner(root)).toBe(`<!---->`)
  202. assertHookCalls(one, [1, 1, 3, 3, 0])
  203. assertHookCalls(two, [1, 1, 4, 4, 0])
  204. toggle1.value = true
  205. await nextTick()
  206. expect(serializeInner(root)).toBe(`<!---->`)
  207. assertHookCalls(one, [1, 1, 4, 3, 0])
  208. assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive
  209. })
  210. async function assertNameMatch(props: KeepAliveProps) {
  211. const outerRef = ref(true)
  212. const viewRef = ref('one')
  213. const App = {
  214. render() {
  215. return outerRef.value
  216. ? h(KeepAlive, props, () => h(views[viewRef.value]))
  217. : null
  218. }
  219. }
  220. render(h(App), root)
  221. expect(serializeInner(root)).toBe(`<div>one</div>`)
  222. assertHookCalls(one, [1, 1, 1, 0, 0])
  223. assertHookCalls(two, [0, 0, 0, 0, 0])
  224. viewRef.value = 'two'
  225. await nextTick()
  226. expect(serializeInner(root)).toBe(`<div>two</div>`)
  227. assertHookCalls(one, [1, 1, 1, 1, 0])
  228. assertHookCalls(two, [1, 1, 0, 0, 0])
  229. viewRef.value = 'one'
  230. await nextTick()
  231. expect(serializeInner(root)).toBe(`<div>one</div>`)
  232. assertHookCalls(one, [1, 1, 2, 1, 0])
  233. assertHookCalls(two, [1, 1, 0, 0, 1])
  234. viewRef.value = 'two'
  235. await nextTick()
  236. expect(serializeInner(root)).toBe(`<div>two</div>`)
  237. assertHookCalls(one, [1, 1, 2, 2, 0])
  238. assertHookCalls(two, [2, 2, 0, 0, 1])
  239. // teardown
  240. outerRef.value = false
  241. await nextTick()
  242. expect(serializeInner(root)).toBe(`<!---->`)
  243. assertHookCalls(one, [1, 1, 2, 2, 1])
  244. assertHookCalls(two, [2, 2, 0, 0, 2])
  245. }
  246. describe('props', () => {
  247. test('include (string)', async () => {
  248. await assertNameMatch({ include: 'one' })
  249. })
  250. test('include (regex)', async () => {
  251. await assertNameMatch({ include: /^one$/ })
  252. })
  253. test('include (array)', async () => {
  254. await assertNameMatch({ include: ['one'] })
  255. })
  256. test('exclude (string)', async () => {
  257. await assertNameMatch({ exclude: 'two' })
  258. })
  259. test('exclude (regex)', async () => {
  260. await assertNameMatch({ exclude: /^two$/ })
  261. })
  262. test('exclude (array)', async () => {
  263. await assertNameMatch({ exclude: ['two'] })
  264. })
  265. test('include + exclude', async () => {
  266. await assertNameMatch({ include: 'one,two', exclude: 'two' })
  267. })
  268. test('max', async () => {
  269. const spyA = jest.fn()
  270. const spyB = jest.fn()
  271. const spyC = jest.fn()
  272. const spyAD = jest.fn()
  273. const spyBD = jest.fn()
  274. const spyCD = jest.fn()
  275. function assertCount(calls: number[]) {
  276. expect([
  277. spyA.mock.calls.length,
  278. spyAD.mock.calls.length,
  279. spyB.mock.calls.length,
  280. spyBD.mock.calls.length,
  281. spyC.mock.calls.length,
  282. spyCD.mock.calls.length
  283. ]).toEqual(calls)
  284. }
  285. const viewRef = ref('a')
  286. const views: Record<string, ComponentOptions> = {
  287. a: {
  288. render: () => `one`,
  289. created: spyA,
  290. unmounted: spyAD
  291. },
  292. b: {
  293. render: () => `two`,
  294. created: spyB,
  295. unmounted: spyBD
  296. },
  297. c: {
  298. render: () => `three`,
  299. created: spyC,
  300. unmounted: spyCD
  301. }
  302. }
  303. const App = {
  304. render() {
  305. return h(KeepAlive, { max: 2 }, () => {
  306. return h(views[viewRef.value])
  307. })
  308. }
  309. }
  310. render(h(App), root)
  311. assertCount([1, 0, 0, 0, 0, 0])
  312. viewRef.value = 'b'
  313. await nextTick()
  314. assertCount([1, 0, 1, 0, 0, 0])
  315. viewRef.value = 'c'
  316. await nextTick()
  317. // should prune A because max cache reached
  318. assertCount([1, 1, 1, 0, 1, 0])
  319. viewRef.value = 'b'
  320. await nextTick()
  321. // B should be reused, and made latest
  322. assertCount([1, 1, 1, 0, 1, 0])
  323. viewRef.value = 'a'
  324. await nextTick()
  325. // C should be pruned because B was used last so C is the oldest cached
  326. assertCount([2, 1, 1, 0, 1, 1])
  327. })
  328. })
  329. describe('cache invalidation', () => {
  330. function setup() {
  331. const viewRef = ref('one')
  332. const includeRef = ref('one,two')
  333. const App = {
  334. render() {
  335. return h(
  336. KeepAlive,
  337. {
  338. include: includeRef.value
  339. },
  340. () => h(views[viewRef.value])
  341. )
  342. }
  343. }
  344. render(h(App), root)
  345. return { viewRef, includeRef }
  346. }
  347. test('on include/exclude change', async () => {
  348. const { viewRef, includeRef } = setup()
  349. viewRef.value = 'two'
  350. await nextTick()
  351. assertHookCalls(one, [1, 1, 1, 1, 0])
  352. assertHookCalls(two, [1, 1, 1, 0, 0])
  353. includeRef.value = 'two'
  354. await nextTick()
  355. assertHookCalls(one, [1, 1, 1, 1, 1])
  356. assertHookCalls(two, [1, 1, 1, 0, 0])
  357. viewRef.value = 'one'
  358. await nextTick()
  359. assertHookCalls(one, [2, 2, 1, 1, 1])
  360. assertHookCalls(two, [1, 1, 1, 1, 0])
  361. })
  362. test('on include/exclude change + view switch', async () => {
  363. const { viewRef, includeRef } = setup()
  364. viewRef.value = 'two'
  365. await nextTick()
  366. assertHookCalls(one, [1, 1, 1, 1, 0])
  367. assertHookCalls(two, [1, 1, 1, 0, 0])
  368. includeRef.value = 'one'
  369. viewRef.value = 'one'
  370. await nextTick()
  371. assertHookCalls(one, [1, 1, 2, 1, 0])
  372. // two should be pruned
  373. assertHookCalls(two, [1, 1, 1, 1, 1])
  374. })
  375. test('should not prune current active instance', async () => {
  376. const { viewRef, includeRef } = setup()
  377. includeRef.value = 'two'
  378. await nextTick()
  379. assertHookCalls(one, [1, 1, 1, 0, 0])
  380. assertHookCalls(two, [0, 0, 0, 0, 0])
  381. viewRef.value = 'two'
  382. await nextTick()
  383. assertHookCalls(one, [1, 1, 1, 0, 1])
  384. assertHookCalls(two, [1, 1, 1, 0, 0])
  385. })
  386. async function assertAnonymous(include: boolean) {
  387. const one = {
  388. name: 'one',
  389. created: jest.fn(),
  390. render: () => 'one'
  391. }
  392. const two = {
  393. // anonymous
  394. created: jest.fn(),
  395. render: () => 'two'
  396. }
  397. const views: any = { one, two }
  398. const viewRef = ref('one')
  399. const App = {
  400. render() {
  401. return h(
  402. KeepAlive,
  403. {
  404. include: include ? 'one' : undefined
  405. },
  406. () => h(views[viewRef.value])
  407. )
  408. }
  409. }
  410. render(h(App), root)
  411. function assert(oneCreateCount: number, twoCreateCount: number) {
  412. expect(one.created.mock.calls.length).toBe(oneCreateCount)
  413. expect(two.created.mock.calls.length).toBe(twoCreateCount)
  414. }
  415. assert(1, 0)
  416. viewRef.value = 'two'
  417. await nextTick()
  418. assert(1, 1)
  419. viewRef.value = 'one'
  420. await nextTick()
  421. assert(1, 1)
  422. viewRef.value = 'two'
  423. await nextTick()
  424. // two should be re-created if include is specified, since it's not matched
  425. // otherwise it should be cached.
  426. assert(1, include ? 2 : 1)
  427. }
  428. // 2.x #6938
  429. test('should not cache anonymous component when include is specified', async () => {
  430. await assertAnonymous(true)
  431. })
  432. test('should cache anonymous components if include is not specified', async () => {
  433. await assertAnonymous(false)
  434. })
  435. // 2.x #7105
  436. test('should not destroy active instance when pruning cache', async () => {
  437. const Foo = {
  438. render: () => 'foo',
  439. unmounted: jest.fn()
  440. }
  441. const includeRef = ref(['foo'])
  442. const App = {
  443. render() {
  444. return h(
  445. KeepAlive,
  446. {
  447. include: includeRef.value
  448. },
  449. () => h(Foo)
  450. )
  451. }
  452. }
  453. render(h(App), root)
  454. // condition: a render where a previous component is reused
  455. includeRef.value = ['foo', 'bar']
  456. await nextTick()
  457. includeRef.value = []
  458. await nextTick()
  459. expect(Foo.unmounted).not.toHaveBeenCalled()
  460. })
  461. })
  462. })