]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
fix(guards): skip update and leave guards of unmounted views
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 21 Jul 2020 12:32:20 +0000 (14:32 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 21 Jul 2020 12:35:27 +0000 (14:35 +0200)
__tests__/guards/beforeRouteLeave.spec.ts
__tests__/guards/beforeRouteUpdate.spec.ts
__tests__/lazyLoading.spec.ts
e2e/multi-app/index.ts
e2e/specs/multi-app.js
src/navigationGuards.ts

index 12f5c69c53498fcdb38603b9983225855f67613f..ff5618337002ad757ff45e74ac2135769e588777 100644 (file)
@@ -100,13 +100,36 @@ describe('beforeRouteLeave', () => {
     await router.push('/guard')
     expect(beforeRouteLeave).not.toHaveBeenCalled()
 
+    // simulate a mounted route component
+    router.currentRoute.value.matched[0].instances.default = {} as any
+
     await router.push('/foo')
     expect(beforeRouteLeave).toHaveBeenCalledTimes(1)
   })
 
+  it('does not call beforeRouteLeave guard if the view is not mounted', async () => {
+    const router = createRouter({ routes })
+    beforeRouteLeave.mockImplementationOnce((to, from, next) => {
+      next()
+    })
+    await router.push('/guard')
+    expect(beforeRouteLeave).not.toHaveBeenCalled()
+
+    // usually we would have to simulate a mounted route component
+    // router.currentRoute.value.matched[0].instances.default = {} as any
+
+    await router.push('/foo')
+    expect(beforeRouteLeave).not.toHaveBeenCalled()
+  })
+
   it('calls beforeRouteLeave guard on navigation between children', async () => {
     const router = createRouter({ routes })
     await router.push({ name: 'nested-path' })
+
+    // simulate a mounted route component
+    router.currentRoute.value.matched[0].instances.default = {} as any
+    router.currentRoute.value.matched[1].instances.default = {} as any
+
     resetMocks()
     await router.push({ name: 'nested-path-b' })
     expect(nested.nestedEmpty).not.toHaveBeenCalled()
@@ -147,6 +170,11 @@ describe('beforeRouteLeave', () => {
       next()
     })
 
+    // simulate a mounted route component
+    router.currentRoute.value.matched[0].instances.default = {} as any
+    router.currentRoute.value.matched[1].instances.default = {} as any
+    router.currentRoute.value.matched[2].instances.default = {} as any
+
     await router.push('/')
     expect(nested.parent).toHaveBeenCalledTimes(1)
     expect(nested.nestedNested).toHaveBeenCalledTimes(1)
index 8911ee47fb9ca852d90cf061fc2b63ba4279429c..2b539b6ebfccdf000acbecc2a31d07c4e08654f0 100644 (file)
@@ -33,10 +33,24 @@ describe('beforeRouteUpdate', () => {
     await router.push('/guard/valid')
     // not called on initial navigation
     expect(beforeRouteUpdate).not.toHaveBeenCalled()
+    // simulate a mounted route component
+    router.currentRoute.value.matched[0].instances.default = {} as any
     await router.push('/guard/other')
     expect(beforeRouteUpdate).toHaveBeenCalledTimes(1)
   })
 
+  it('does not call beforeRouteUpdate guard if the view is not mounted', async () => {
+    const router = createRouter({ routes })
+    beforeRouteUpdate.mockImplementationOnce(noGuard)
+    await router.push('/guard/valid')
+    // not called on initial navigation
+    expect(beforeRouteUpdate).not.toHaveBeenCalled()
+    // usually we would have to simulate a mounted route component
+    // router.currentRoute.value.matched[0].instances.default = {} as any
+    await router.push('/guard/other')
+    expect(beforeRouteUpdate).not.toHaveBeenCalled()
+  })
+
   it('waits before navigating', async () => {
     const [promise, resolve] = fakePromise()
     const router = createRouter({ routes })
index 429909c5d83d31d9e8d476f700a876f76ec7c40e..f28f2cc8e6169433caee31137183d66e54978d1f 100644 (file)
@@ -211,6 +211,9 @@ describe('Lazy Loading', () => {
     expect(component).toHaveBeenCalledTimes(1)
     expect(spy).toHaveBeenCalledTimes(0)
 
+    // simulate a mounted route component
+    router.currentRoute.value.matched[0].instances.default = {} as any
+
     await router.push('/')
     expect(spy).toHaveBeenCalledTimes(1)
   })
@@ -230,6 +233,9 @@ describe('Lazy Loading', () => {
     expect(component).toHaveBeenCalledTimes(1)
     expect(spy).toHaveBeenCalledTimes(0)
 
+    // simulate a mounted route component
+    router.currentRoute.value.matched[0].instances.default = {} as any
+
     await router.push('/bar')
     expect(spy).toHaveBeenCalledTimes(1)
   })
index 3a975f327abd3600ef4a95e082e35337062051f6..65a2ea7499b438b5e620118c45450a05a0dd66ea 100644 (file)
@@ -44,13 +44,14 @@ let looper = [1, 2, 3]
 const NamedViews: RouteComponent[] = looper.map(i => ({
   name: 'part-' + i,
 
-  template: `<div class="named part-${i}">Part ${i}. Updated <span class="count">{{ count }}</span></div>`,
+  template: `<div class="named" id="part-${i}">Part ${i}. Updated <span class="count">{{ count }}</span></div>`,
 
   data: () => ({ count: 0 }),
 
   beforeRouteUpdate(to, from, next) {
+    console.log('update of', i)
     // @ts-ignore
-    // this.count++
+    this.count++
     next()
   },
 }))
@@ -118,7 +119,6 @@ looper.forEach((n, i) => {
   mountBtn.addEventListener('click', () => {
     let app = (apps[i] = createApp({
       template: `
-    <div id="app-${i}">
       <ul>
         <li><router-link to="/">Home</router-link></li>
         <li><router-link to="/users/1">User 1</router-link></li>
@@ -127,7 +127,6 @@ looper.forEach((n, i) => {
 
       <router-view></router-view>
       <router-view name="part-${n}"></router-view>
-    </div>
   `,
     }))
     app.use(router)
index 97b94f2d1b8ea99824478eb75835a9ae4d29facc..5659b27575dc12dc6d761f66f35fa7283c5d5085 100644 (file)
@@ -109,16 +109,16 @@ module.exports = {
       .assert.containsText('#app-1 .home', 'Home')
       // toggle multiple times
       .click('#app-1 li:nth-child(2) a')
-      .assert.containsText('#app-1 .count', '0')
+      .assert.containsText('#part-1 .count', '0')
       .click('#app-1 li:nth-child(3) a')
-      .assert.containsText('#app-1 .count', '1')
+      .assert.containsText('#part-1 .count', '1')
       .click('#mount2')
-      .assert.containsText('#app-2 .user', 'User 2')
+      .assert.containsText('#app-2 .user', 'User')
       .click('#app-1 li:nth-child(2) a')
       // first one keeps updating
-      .assert.containsText('#app-1 .count', '2')
+      .assert.containsText('#part-1 .count', '2')
       // second app only updated once
-      .assert.containsText('#app-2 .count', '1')
+      .assert.containsText('#part-2 .count', '1')
       .click('#mount3')
   },
 }
index 53bee1046c320ae9e33002c83a8bb90358ac0cd6..74a734f7b3720eafe161b3197de4aea505eca982 100644 (file)
@@ -200,6 +200,9 @@ export function extractComponentsGuards(
         rawComponent = () => promise
       }
 
+      // skip update and leave guards if the route component is not mounted
+      if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue
+
       if (isRouteComponent(rawComponent)) {
         // __vccOpts is added by vue-class-component and contain the regular options
         let options: ComponentOptions =