diff --git a/web/src/layout/components/AppMain.vue b/web/src/layout/components/AppMain.vue index 552a067..7a137c0 100644 --- a/web/src/layout/components/AppMain.vue +++ b/web/src/layout/components/AppMain.vue @@ -1,10 +1,12 @@ @@ -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) diff --git a/web/tests/unit/layout/AppMain.spec.js b/web/tests/unit/layout/AppMain.spec.js new file mode 100644 index 0000000..c02b89a --- /dev/null +++ b/web/tests/unit/layout/AppMain.spec.js @@ -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() + }) +})