]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
fix(view): correctly reuse instance guards (#795)
authorEduardo San Martin Morote <posva@users.noreply.github.com>
Mon, 1 Mar 2021 09:33:01 +0000 (10:33 +0100)
committerGitHub <noreply@github.com>
Mon, 1 Mar 2021 09:33:01 +0000 (10:33 +0100)
e2e/guards-instances/index.ts
e2e/specs/suspense.js [new file with mode: 0644]
e2e/suspense/index.html [new file with mode: 0644]
e2e/suspense/index.ts [new file with mode: 0644]
src/RouterView.ts

index b2e1f7194c5792f51ecdefbbb9af3e59cbe4afa0..f8bf36e84b678baee43f5ab36fb7eda25b5b7e0e 100644 (file)
@@ -94,6 +94,7 @@ function createTestComponent(key: string) {
 }
 
 const Foo = createTestComponent('Foo')
+const Bar = createTestComponent('Bar')
 const One = createTestComponent('One')
 const Two = createTestComponent('Two')
 const Aux = createTestComponent('Aux')
@@ -111,6 +112,11 @@ const router = createRouter({
       path: '/f/:id',
       component: Foo,
     },
+    // TODO: test that the onBeforeRouteUpdate isn't kept
+    {
+      path: '/b/:id',
+      component: Bar,
+    },
     {
       path: '/named-one',
       components: {
diff --git a/e2e/specs/suspense.js b/e2e/specs/suspense.js
new file mode 100644 (file)
index 0000000..cee36ef
--- /dev/null
@@ -0,0 +1,39 @@
+const bsStatus = require('../browserstack-send-status')
+
+module.exports = {
+  ...bsStatus(),
+
+  '@tags': [],
+
+  /** @type {import('nightwatch').NightwatchTest} */
+  'suspense with guards': function (browser) {
+    browser
+      .url('http://localhost:8080/suspense/foo')
+      .waitForElementPresent('#app > *', 1000)
+
+    browser
+      .click('li:nth-child(2) a')
+      .waitForElementPresent('#Foo', 1000)
+      .click('li:nth-child(4) a')
+      .click('li:nth-child(3) a')
+      .waitForElementPresent('#FooAsync', 1000)
+      .click('li:nth-child(4) a')
+      .click('li:nth-child(2) a')
+      .waitForElementPresent('#Foo', 1000)
+      .click('li:nth-child(4) a')
+      .click('li:nth-child(1) a')
+      .expect.element('#logs')
+      .text.to.equal(
+        [
+          `Foo: setup:update /foo - /foo?n=1`,
+          `Foo: setup:leave /foo?n=1 - /foo-async`,
+          `FooAsync: setup:update /foo-async - /foo-async?n=1`,
+          `FooAsync: setup:leave /foo-async?n=1 - /foo`,
+          `Foo: setup:update /foo - /foo?n=1`,
+          `Foo: setup:leave /foo?n=1 - /`,
+        ].join('\n')
+      )
+
+    browser.end()
+  },
+}
diff --git a/e2e/suspense/index.html b/e2e/suspense/index.html
new file mode 100644 (file)
index 0000000..757fb4b
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
+    <title>Vue Router e2e tests - Suspense</title>
+    <!-- TODO: replace with local imports for promises and anything else needed -->
+    <script src="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces2015"></script>
+  </head>
+  <body>
+    <a href="/">&lt;&lt; Back to Homepage</a>
+    <hr />
+
+    <main id="app"></main>
+  </body>
+</html>
diff --git a/e2e/suspense/index.ts b/e2e/suspense/index.ts
new file mode 100644 (file)
index 0000000..9ec46a7
--- /dev/null
@@ -0,0 +1,110 @@
+import {
+  createRouter,
+  createWebHistory,
+  onBeforeRouteUpdate,
+  onBeforeRouteLeave,
+} from '../../src'
+import { createApp, ref, reactive, defineComponent } from 'vue'
+
+const Home = defineComponent({
+  template: `
+    <div>
+      <h2>Home</h2>
+    </div>
+  `,
+})
+
+const logs = ref<string[]>([])
+
+const state = reactive({
+  enter: 0,
+  update: 0,
+  leave: 0,
+})
+
+const delay = (t: number) => new Promise(r => setTimeout(r, t))
+
+/**
+ * creates a component that logs the guards
+ * @param name
+ */
+function createTestComponent(key: string, isAsync = false) {
+  return defineComponent({
+    name: key,
+    template: `<div id="${key}">${key}</div>`,
+
+    setup() {
+      onBeforeRouteUpdate((to, from) => {
+        logs.value.push(
+          `${key}: setup:update ${from.fullPath} - ${to.fullPath}`
+        )
+      })
+      onBeforeRouteLeave((to, from) => {
+        logs.value.push(`${key}: setup:leave ${from.fullPath} - ${to.fullPath}`)
+      })
+
+      return isAsync ? delay(100).then(() => ({})) : {}
+    },
+  })
+}
+
+const Foo = createTestComponent('Foo')
+const FooAsync = createTestComponent('FooAsync', true)
+
+const webHistory = createWebHistory('/' + __dirname)
+const router = createRouter({
+  history: webHistory,
+  routes: [
+    { path: '/', component: Home },
+    {
+      path: '/foo',
+      component: Foo,
+    },
+    {
+      path: '/foo-async',
+      component: FooAsync,
+    },
+  ],
+})
+
+const app = createApp({
+  template: `
+    <div id="app">
+      <h1>Suspense</h1>
+
+      <pre>
+route: {{ $route.fullPath }}
+enters: {{ state.enter }}
+updates: {{ state.update }}
+leaves: {{ state.leave }}
+      </pre>
+
+      <pre id="logs">{{ logs.join('\\n') }}</pre>
+
+      <button id="resetLogs" @click="logs = []">Reset Logs</button>
+
+      <ul>
+        <li><router-link to="/">/</router-link></li>
+        <li><router-link to="/foo">/foo</router-link></li>
+        <li><router-link to="/foo-async">/foo-async</router-link></li>
+        <li><router-link id="update-query" :to="{ query: { n: (Number($route.query.n) || 0) + 1 }}" v-slot="{ route }">{{ route.fullPath }}</router-link></li>
+      </ul>
+
+      <router-view v-slot="{ Component }" >
+        <Suspense>
+          <component :is="Component" class="view" />
+        </Suspense>
+      </router-view>
+    </div>
+  `,
+  setup() {
+    return { state, logs }
+  },
+})
+
+app.use(router)
+
+// @ts-ignore
+window.r = router
+
+app.mount('#app')
index 883476a006ee3b35dc4e30c48f2d7de0440059c6..8d4ca3f30d29ea1c7f26b88b8c13d8662e7ee270 100644 (file)
@@ -73,10 +73,18 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
           // instances when navigating to a new route
           to.instances[name] = instance
           // the component instance is reused for a different route or name so
-          // we copy any saved update or leave guards
+          // we copy any saved update or leave guards. With async setup, the
+          // mounting component will mount before the matchedRoute changes,
+          // making instance === oldInstance, so we check if guards have been
+          // added before. This works because we remove guards when
+          // unmounting/deactivating components
           if (from && from !== to && instance && instance === oldInstance) {
-            to.leaveGuards = from.leaveGuards
-            to.updateGuards = from.updateGuards
+            if (!to.leaveGuards.size) {
+              to.leaveGuards = from.leaveGuards
+            }
+            if (!to.updateGuards.size) {
+              to.updateGuards = from.updateGuards
+            }
           }
         }