]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
test: add modal sample
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 24 Mar 2020 09:10:26 +0000 (10:10 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 24 Mar 2020 15:09:43 +0000 (16:09 +0100)
e2e/modal/index.html [new file with mode: 0644]
e2e/modal/index.ts [new file with mode: 0644]
e2e/specs/modal.js [new file with mode: 0644]

diff --git a/e2e/modal/index.html b/e2e/modal/index.html
new file mode 100644 (file)
index 0000000..825586b
--- /dev/null
@@ -0,0 +1,19 @@
+<!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 Examples - Encoding</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 />
+
+    <div id="app">
+      <router-view></router-view>
+    </div>
+  </body>
+</html>
diff --git a/e2e/modal/index.ts b/e2e/modal/index.ts
new file mode 100644 (file)
index 0000000..0aaa8e5
--- /dev/null
@@ -0,0 +1,260 @@
+import { createRouter, createWebHistory, useRoute } from '../../src'
+import { RouteComponent } from '../../src/types'
+import { createApp, readonly, reactive, ref, watchEffect } from 'vue'
+
+const users = readonly([
+  { name: 'John' },
+  { name: 'Jessica' },
+  { name: 'James' },
+])
+
+const modalState = reactive({
+  showModal: false,
+  userId: 0,
+})
+
+const enum GhostNavigation {
+  none = 0,
+  restoreGhostUrl,
+  backToOriginal,
+}
+
+let paused: GhostNavigation = GhostNavigation.none
+window.addEventListener('popstate', function customPopListener(event) {
+  let { state } = event
+
+  console.log('popstate!', paused, event.state)
+  if (paused !== GhostNavigation.none) {
+    if (paused === GhostNavigation.restoreGhostUrl) {
+      webHistory.replace(state.ghostURL)
+      console.log('replaced ghost', state.ghostURL)
+      paused = GhostNavigation.backToOriginal
+      webHistory.back(false)
+    } else if (paused === GhostNavigation.backToOriginal) {
+      paused = GhostNavigation.none
+      Object.assign(modalState, state.modalState)
+      console.log('came from a ghost navigation, nothing to do')
+      // let's remove the guard from navigating away, it will be added again by afterEach when
+      // entering the url
+      historyCleaner && historyCleaner()
+      historyCleaner = undefined
+      event.stopImmediatePropagation()
+    }
+
+    return
+  }
+
+  if (!state) return
+  // we came from a navigation that only changed the url
+  // TODO: check the currentValue
+  if (state.forwardGhost) {
+    // make sure the url saved in the history stack is good
+    paused = GhostNavigation.restoreGhostUrl
+    webHistory.forward(false)
+    // we are coming from
+  } else if (
+    state.displayURL &&
+    state.originalURL === router.currentRoute.value.fullPath
+  ) {
+    webHistory.replace(state.displayURL)
+    event.stopImmediatePropagation()
+    Object.assign(modalState, state.modalState)
+    // webHistory.replace(state.originalURL)
+  }
+  // if ((state && !state.forward) || state.showModal) {
+  //   console.log('stopping it!')
+  //   // copy showModal state
+  //   modalState.showModal = !!state.showModal
+  //   // don't let the router catch this one
+  //   event.stopImmediatePropagation()
+  // }
+})
+
+const About: RouteComponent = {
+  template: `<div>
+  <h1>About</h1>
+  <p>If you came from a user modal, you should go back to it</p>
+  <button @click="back">Back</button>
+  </div>
+  `,
+  methods: {
+    back() {
+      window.history.back()
+    },
+  },
+}
+
+let historyCleaner: (() => void) | undefined
+function showUserModal(id: number) {
+  const route = router.currentRoute.value
+  // generate a new entry that is exactly like the one we are on but with an extra query
+  // so it still counts like a navigation for the router when leaving it or when pushing on top
+  const newURL = router.resolve({
+    path: route.path,
+    query: { ...route.query, __m: Math.random() },
+    hash: route.hash,
+  })
+  // the url we want to show
+  let url = router.resolve({ name: 'user', params: { id: '' + id } })
+  const displayURL = url.fullPath
+  const ghostURL = newURL.fullPath
+  const originalURL = router.currentRoute.value.fullPath
+
+  webHistory.replace(router.currentRoute.value, {
+    // save that we are going to a ghost route
+    forwardGhost: ghostURL,
+    modalState: { ...modalState },
+  })
+
+  modalState.userId = id
+  modalState.showModal = true
+
+  // push a new entry in the history stack
+  webHistory.push(newURL, {
+    // the url we need to give the router to show
+    displayURL,
+    originalURL,
+    ghostURL,
+    modalState: { ...modalState },
+  })
+
+  // TODO: this can be one single call
+
+  // display the correct url
+  webHistory.replace(displayURL)
+
+  // history.pushState({ showModal: true }, '', url)
+  historyCleaner && historyCleaner()
+  // make sure we clear what we did before leaving
+  historyCleaner = router.beforeEach((to, from, next) => {
+    console.log('restoring the url', originalURL)
+    // change the URL before leaving so that when we go back we are navigating to the right url
+    webHistory.replace(ghostURL, {
+      // save the current value of the modalState
+      modalState: { ...modalState },
+    })
+    // remove this guard
+    historyCleaner()
+    let remove = router.afterEach(() => {
+      webHistory.replace(router.currentRoute.value.fullPath, {
+        previousGhost: ghostURL,
+      })
+      remove()
+    })
+    // hide the modal after saving the state
+    modalState.showModal = false
+    // trigger the same navigation again
+    next(to.fullPath)
+  })
+}
+
+function closeUserModal() {
+  history.back()
+}
+
+const Home: RouteComponent = {
+  template: `<div>
+  <h1>Home</h1>
+  <p>Select a user</p>
+  <ul>
+    <li v-for="(user, id) in users">
+      <router-link :to="{ name: 'user', params: { id }}">{{ user.name }}</router-link>
+      - <button @click="showUserModal(id)">Details</button>
+    </li>
+  </ul>
+
+  <dialog ref="modal" id="dialog">
+    <p>
+    User #{{ modalState.userId }}
+    <br>
+    Name: {{ users[modalState.userId].name }}
+    </p>
+    <router-link to="/about">Go somewhere else</router-link>
+    <br>
+    <button @click="closeUserModal">Close</button>
+  </dialog>
+  </div>`,
+  setup() {
+    const modal = ref()
+
+    watchEffect(() => {
+      if (!modal.value) return
+
+      const show = modalState.showModal
+      console.log('show modal?', show)
+      if (show) modal.value.show()
+      else modal.value.close()
+    })
+
+    return {
+      modal,
+      showUserModal,
+      closeUserModal,
+      modalState,
+      users,
+    }
+  },
+}
+
+const UserDetails: RouteComponent = {
+  template: `<div>
+    <h1>User #{{ id }}</h1>
+    <p>
+      Name: {{ users[id].name }}
+    </p>
+    <router-link to="/">Back home</router-link>
+  </div>`,
+  props: ['id'],
+  data: () => ({ users }),
+}
+
+const webHistory = createWebHistory('/' + __dirname)
+const router = createRouter({
+  history: webHistory,
+  routes: [
+    { path: '/', component: Home },
+    { path: '/about', component: About },
+    { path: '/users/:id', props: true, name: 'user', component: UserDetails },
+  ],
+})
+
+router.beforeEach((to, from, next) => {
+  console.log('---')
+  console.log('going from', from.fullPath, 'to', to.fullPath)
+  console.log('state:', window.history.state)
+  console.log('---')
+  next()
+})
+
+router.afterEach(() => {
+  const { state } = window.history
+  console.log('afterEach', state)
+  if (state && state.displayURL) {
+    console.log('restoring', state.displayURL, 'for', state.originalURL)
+    // restore the state
+    Object.assign(modalState, state.modalState)
+    webHistory.replace(state.displayURL)
+    // history.pushState({ showModal: true }, '', url)
+    // historyCleaner && historyCleaner()
+    historyCleaner = router.beforeEach((to, from, next) => {
+      // add data to history state so it can be restored if we go back
+      webHistory.replace(state.ghostURL, {
+        modalState: { ...modalState },
+      })
+      // remove this guard
+      historyCleaner && historyCleaner()
+      // trigger the same navigation again
+      next(to.fullPath)
+    })
+  }
+})
+
+const app = createApp({
+  setup() {
+    const route = useRoute()
+    return { route }
+  },
+})
+app.use(router)
+
+window.vm = app.mount('#app')
diff --git a/e2e/specs/modal.js b/e2e/specs/modal.js
new file mode 100644 (file)
index 0000000..2e32553
--- /dev/null
@@ -0,0 +1,177 @@
+const bsStatus = require('../browserstack-send-status')
+
+const baseURL = 'http://localhost:8080/modal'
+
+module.exports = {
+  ...bsStatus(),
+
+  '@tags': ['history'],
+
+  /** @type {import('nightwatch').NightwatchTest} */
+  'changes the url'(browser) {
+    browser
+      .url(baseURL)
+      .waitForElementVisible('#app', 1000)
+      .assert.containsText('h1', 'Home')
+      .assert.not.visible('dialog')
+
+      .click('li:nth-child(2) button')
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.visible('dialog')
+      .assert.containsText('dialog', 'User #1')
+
+      .end()
+  },
+
+  /** @type {import('nightwatch').NightwatchTest} */
+  'can close and reopen the modal through history'(browser) {
+    browser
+      .url(baseURL)
+      .waitForElementVisible('#app', 1000)
+      .assert.containsText('h1', 'Home')
+
+      .click('li:nth-child(2) button')
+      .assert.visible('dialog')
+      .back()
+      .assert.not.visible('dialog')
+      .assert.urlEquals(baseURL + '/')
+      .forward()
+      .assert.visible('dialog')
+      .assert.urlEquals(baseURL + '/users/1')
+      .back()
+      .assert.not.visible('dialog')
+      .assert.urlEquals(baseURL + '/')
+      .forward()
+      .assert.visible('dialog')
+      .assert.urlEquals(baseURL + '/users/1')
+
+      .end()
+  },
+
+  /** @type {import('nightwatch').NightwatchTest} */
+  'should not keep the modal when reloading'(browser) {
+    browser
+      .url(baseURL)
+      .waitForElementVisible('#app', 1000)
+      .assert.containsText('h1', 'Home')
+
+      .click('li:nth-child(2) button')
+      .assert.visible('dialog')
+      .refresh()
+      .assert.containsText('h1', 'User #1')
+      .assert.urlEquals(baseURL + '/users/1')
+      .back()
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      // FIXME: reload
+      // .assert.not.visible('dialog')
+
+      .end()
+  },
+
+  'should not keep the modal when reloading and navigating to home again'(
+    browser
+  ) {
+    browser
+      .url(baseURL)
+      .waitForElementVisible('#app', 1000)
+      .assert.containsText('h1', 'Home')
+
+      .click('li:nth-child(2) button')
+      .assert.visible('dialog')
+      .refresh()
+      .assert.containsText('h1', 'User #1')
+      .assert.urlEquals(baseURL + '/users/1')
+      .click('#app a')
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      // FIXME: reload
+      // .assert.not.visible('dialog')
+
+      .end()
+  },
+
+  /** @type {import('nightwatch').NightwatchTest} */
+  'can pass through the modal and then back'(browser) {
+    browser
+      .url(baseURL)
+      .waitForElementVisible('#app', 1000)
+      .assert.containsText('h1', 'Home')
+      .assert.not.visible('dialog')
+
+      .click('li:nth-child(2) a')
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.containsText('h1', 'User #1')
+      .click('a')
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      .click('li:nth-child(3) a')
+      .assert.urlEquals(baseURL + '/users/2')
+      .assert.containsText('h1', 'User #2')
+      .click('a')
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      .click('li:nth-child(2) button')
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.visible('dialog')
+      .back()
+      .assert.not.visible('dialog')
+      .assert.urlEquals(baseURL + '/')
+      .back()
+      .assert.urlEquals(baseURL + '/users/2')
+      .assert.containsText('h1', 'User #2')
+      .back()
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      .back()
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.containsText('h1', 'User #1')
+      .back()
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      .forward()
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.containsText('h1', 'User #1')
+      .forward()
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      .forward()
+      .assert.urlEquals(baseURL + '/users/2')
+      .assert.containsText('h1', 'User #2')
+      .forward()
+      .assert.urlEquals(baseURL + '/')
+      .assert.containsText('h1', 'Home')
+      .assert.not.visible('dialog')
+      .assert.urlEquals(baseURL + '/')
+      .forward()
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.visible('dialog')
+
+      .end()
+  },
+
+  /** @type {import('nightwatch').NightwatchTest} */
+  'can navigate away from the modal then come back'(browser) {
+    browser
+      .url(baseURL)
+      .waitForElementVisible('#app', 1000)
+      .assert.containsText('h1', 'Home')
+
+      .click('li:nth-child(2) button')
+      .assert.visible('dialog')
+      .click('dialog a')
+      .assert.urlEquals(baseURL + '/about')
+      .assert.containsText('h1', 'About')
+      .back()
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.visible('dialog')
+      .forward()
+      .assert.urlEquals(baseURL + '/about')
+      .assert.containsText('h1', 'About')
+      .back()
+      .assert.urlEquals(baseURL + '/users/1')
+      .assert.visible('dialog')
+
+      .end()
+  },
+}