fix(web): restore cached tab state (#187)
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<section class="app-main" :style="appMainStyle">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<keep-alive :include="cachedViews">
|
||||
<router-view :key="key" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -15,9 +17,6 @@ export default {
|
||||
cachedViews() {
|
||||
return this.$store.state.tagsView.cachedViews
|
||||
},
|
||||
key() {
|
||||
return this.$route.path
|
||||
},
|
||||
appMainStyle() {
|
||||
if (!this.$store.state.settings.fixedHeader) return {}
|
||||
const headerHeight = 50 + (this.$store.state.settings.tagsView ? 40 : 0)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { h, nextTick } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import AppMain from '@/layout/components/AppMain.vue'
|
||||
|
||||
function createStatefulPage(name, mounted) {
|
||||
return {
|
||||
name,
|
||||
data() {
|
||||
return { value: '' }
|
||||
},
|
||||
mounted,
|
||||
render() {
|
||||
return h('input', {
|
||||
'data-test': name,
|
||||
value: this.value,
|
||||
onInput: event => {
|
||||
this.value = event.target.value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function mountAppMain(routes, cachedViews, initialPath) {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes
|
||||
})
|
||||
await router.push(initialPath)
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(AppMain, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
mocks: {
|
||||
$store: {
|
||||
state: {
|
||||
tagsView: { cachedViews },
|
||||
settings: { fixedHeader: false, tagsView: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return { router, wrapper }
|
||||
}
|
||||
|
||||
async function navigate(router, path) {
|
||||
await router.push(path)
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
describe('AppMain route cache', () => {
|
||||
it('restores a named page instance after switching tabs', async() => {
|
||||
const mounted = { pageA: 0, pageB: 0 }
|
||||
const PageA = createStatefulPage('StatefulPageA', () => mounted.pageA++)
|
||||
const PageB = createStatefulPage('StatefulPageB', () => mounted.pageB++)
|
||||
const { router, wrapper } = await mountAppMain([
|
||||
{ path: '/page-a', name: 'StatefulPageA', component: PageA },
|
||||
{ path: '/page-b', name: 'StatefulPageB', component: PageB }
|
||||
], ['StatefulPageA', 'StatefulPageB'], '/page-a')
|
||||
|
||||
await wrapper.get('[data-test="StatefulPageA"]').setValue('retained state')
|
||||
await navigate(router, '/page-b')
|
||||
await navigate(router, '/page-a')
|
||||
|
||||
expect(wrapper.get('[data-test="StatefulPageA"]').element.value).toBe('retained state')
|
||||
expect(mounted).toEqual({ pageA: 1, pageB: 1 })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps separate cached instances for dynamic routes with the same name', async() => {
|
||||
let mountCount = 0
|
||||
const DictDataPage = createStatefulPage('DictDataPage', () => mountCount++)
|
||||
const { router, wrapper } = await mountAppMain([
|
||||
{ path: '/dict/data/:dictId', name: 'DictDataPage', component: DictDataPage }
|
||||
], ['DictDataPage'], '/dict/data/1')
|
||||
|
||||
await wrapper.get('[data-test="DictDataPage"]').setValue('dict 1')
|
||||
await navigate(router, '/dict/data/2')
|
||||
await wrapper.get('[data-test="DictDataPage"]').setValue('dict 2')
|
||||
await navigate(router, '/dict/data/3')
|
||||
await wrapper.get('[data-test="DictDataPage"]').setValue('dict 3')
|
||||
|
||||
await navigate(router, '/dict/data/1')
|
||||
expect(wrapper.get('[data-test="DictDataPage"]').element.value).toBe('dict 1')
|
||||
await navigate(router, '/dict/data/2')
|
||||
expect(wrapper.get('[data-test="DictDataPage"]').element.value).toBe('dict 2')
|
||||
await navigate(router, '/dict/data/3')
|
||||
expect(wrapper.get('[data-test="DictDataPage"]').element.value).toBe('dict 3')
|
||||
expect(mountCount).toBe(3)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('does not use RouterView directly inside KeepAlive or Transition', async() => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const Page = createStatefulPage('WarningCheckPage', () => {})
|
||||
const { wrapper } = await mountAppMain([
|
||||
{ path: '/warning-check', name: 'WarningCheckPage', component: Page }
|
||||
], ['WarningCheckPage'], '/warning-check')
|
||||
|
||||
const routerViewWarnings = warn.mock.calls.filter(args => args.some(arg =>
|
||||
typeof arg === 'string' && arg.includes('RouterView can no longer be used directly inside')
|
||||
))
|
||||
expect(routerViewWarnings).toHaveLength(0)
|
||||
|
||||
wrapper.unmount()
|
||||
warn.mockRestore()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user