Chapter 10. Testing Vue Router

This chapter covers

  • Writing unit tests for components that use Vue Router properties
  • Writing unit tests for the RouterLink component
  • Using Vue Router properties in a Vuex store

Vue Router is the official client-side routing library. If you add client-side routing to a Vue application, you’ll use Vue Router. Therefore, you should learn how to test applications that use Vue Router.

To learn Vue Router testing techniques, you’ll work on the Hacker News application. So far, the Hacker News application renders a single feed. You’ll refactor the app to support multiple feeds and add pagination, so that users can navigate between pages of feed items.

Definition

Pagination is adding paged content. A Google search page is paginated; you can click through many different pages of results (although you probably never do!).

In chapter 9, you added a Vue Router setup to the application. The router matches any of the Hacker News feeds—/top, /new, /show, /ask, and /jobs—and adds the values to the $route.params object as type. The router also matches an optional page parameter. For example, the path /top/2 will match with a $route.params object of

{ type: 'top', page: '2' }

You can use these values to render different feeds and different pages of items. To add these features, you’ll learn how to test Vue Router instance properties, how to test RouterLink components, and how to access Vue Router properties in a store.

By the end of the chapter, you’ll have added six item feeds and pagination to the Hacker News app. The first Vue Router features you’ll learn to test are router properties.

Note

You’re following on from the app you made in chapter 9. If you don’t have the app, you can check out the chapter-10 Git branch following the instructions in appendix A.

10.1. Testing router properties

Vue Router adds two instance properties when you install it on Vue: the $route property and the $router property. These properties should come with a giant warning sign, because they can cause a lot of problems in tests. $route and $router are added as read-only properties to the Vue instance. It’s impossible to overwrite their values after they’ve been added. I’ll show you how to avoid this problem in the final part of this section.

The $route property contains information about the currently matched route, which includes any router parameters from dynamic segments. In the Hacker News app, you’ll use the dynamic parameters to fetch different list types. If the path is /top, you’ll fetch items for the top list; if the path is /new, you’ll fetch items for the new list.

The other Router property is $router, which is the router instance that you pass to Vue in the entry file. The $router instance includes methods for controlling Vue Router. For example, it has a replace method you can use to update the current view without causing a page refresh.

The first property you’ll write tests for is the $route property, to render different feed types and to render the current page.

10.1.1. Testing the $route property

If a component uses the $route instance property, then the property becomes a dependency of the component. When you test components with dependencies, you need to mock the dependencies to prevent errors.

The technique for mocking Vue Router instance properties is the same as for testing other instance properties. You can use the Vue Test Utils mocks mounting option to add it as an instance property in tests, as shown in the next listing.

Listing 10.1. Mocking a $route property
test('renders id param', () => {
  const wrapper = shallowMount(TestComponent, {
    mocks: {                                     1
      $route: {
        params: {
          id: 123
        }
      }
    }
  })
  expect(wrapper.text()).toContain('123')        2
})

  • 1 Adds the mock $route instance property
  • 2 Asserts that the component renders the id param as text

In the Hacker News app, you will use the $route.params properties to fetch items for the current type and to render the current page value.

You’ll add tests for the following specs:

  • ItemList dispatches fetchListData with $route.params.type.
  • ItemList renders page 1/5 when the page parameter is 1 and maxPage is 5.
  • ItemList renders page 2/5 when the page parameter is 2 and maxPage is 5.

The first test you write will be a refactor of an existing test to dispatch the $route.params.type value in the fetchListData action. Currently, in the ItemList component, you dispatch a fetchListData action with a type of top when the component mounts. The ItemList always fetches items for the top list.

You’ll refactor this test and the component to use the $route.params.type value instead, so that it fetches a different list depending on the URL. Remember, a URL of /new/2 will have a $route.params object of { type: ’new’, page: ’2’ }.

In the test, you can add $route as an instance property using the mocks option and then assert that dispatch was called with the correct arguments. Open src/views/__tests__/ItemList.spec.js, and find the dispatches fetchListData with top test. Replace the test with the following code.

Listing 10.2. Passing props into a component
test('dispatches fetchListData with $route.params.type', async () => {
  expect.assertions(1)
  const store = createStore()                                             1
  store.dispatch = jest.fn(() => Promise.resolve())                       2

  const type = 'a type'
  const mocks = {                                                         3
    $route: {
      params: {
        type
      }
    }
  }
  createWrapper({ store, mocks })
  await flushPromises()
  expect(store.dispatch).toHaveBeenCalledWith('fetchListData', { type }) 4
})

  • 1 Creates a store using the factory function
  • 2 Replaces the store dispatch method with a mock function
  • 3 Mocks the $route.params.type value
  • 4 Asserts that dispatch was called with the correct arguments

Now refactor the dispatch call in src/views/ItemList.vue to use the $route.params .type prop value, as follows:

this.$store.dispatch('fetchListData', {
  type: this.$route.params.type
})

Check that the test passes by running npm run test:unit. Oh no, failing tests! The test you just wrote is actually passing, but all the other tests are now failing! The problem is, all the existing tests don’t have $route.params as an instance property, so they create an error when ItemList tries to access $route.params.type. For these failing tests, you need to add $route as an instance property so that the code doesn’t throw an error when it tries to access $route.params.type.

Methods for patching $route and $router

Remember the leaky bucket analogy? A component that accesses properties that are injected by Vue Router has lots of holes to patch. You can use two techniques to add $route and $router to components in tests.

First, you can install Vue Router using a localVue. This technique is useful if you’re testing a component that accesses properties and methods on $route and $router, but you don’t need to control the values of $route and $router in the tests. Remember, installing Vue Router sets $route and $router as read-only properties, so you can’t control their values in the test.

To control the data included in the $route and $router object, you need to use the mocks mounting option. The mocks mounting option makes properties available inside each mounted component.

This is where factory functions start to shine! Instead of editing the existing tests, you need to edit only the factory function to add a new default option. You add a $route object to the mocks object that the createWrapper factory function uses.

Open src/views/__tests__/ItemList.spec.js, and in the createWrapper function in the mocks object, add a $route object. Set a default type value, as shown next, because the ItemList will always have a type parameter in production; but leave page undefined because it can sometimes be undefined in production:

$route: {
  params: { type: 'top' }
},

Now run the tests with npm run test:unit. Fantastic—you filled in the leaky bucket, and all the tests are passing!

With that small change, you’ve added support for multiple lists! Open the dev server and take a look: npm run serve. If you click the links in the header, the app will render different lists—magic!

You’re only a few pages in, and you’ve already added support for different lists. The rest of the chapter will be spent adding pagination.

Because the app receives an unknown number of items from the API and will display 20 items per page, the app should indicate how many pages (of 20 items each) exist. The app will render the current page and the max page in ItemList. For example, if you were on page 2 and there were 21 pages worth of items in the store, the app would render the text “2/21.” You can access the current page on the $route .params object, and you can get the maximum page value from the maxPage getter that you wrote in chapter 7.

There are two tests you should write to test that you’re rendering the correct page information. One test will set $route.params.page and the maxPage getter. The other test handles cases where the $route.params object is undefined.

First you’ll add a test for when $route.params.page is undefined. The page parameter is undefined if the current path doesn’t include a page segment, so if the path is /top, the page parameter would be undefined. In this case, you should default to the first page of items.

In the test, you’ll create a store with a maxPage getter that returns 5 and then assert that the component text includes “1/5.” Add the test from the next code listing to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 10.3. Creating a store with a maxPage getter
  test('renders 1/5 when on page 1 of 5', () => {
  const store = createStore({
    getters: {
      maxPage: () => 5                         1
    }
  })
  const wrapper = createWrapper({ store })
  expect(wrapper.text()).toContain('1/5')      2
})

  • 1 Sets the maxPage getter to return 5
  • 2 Asserts that ItemList renders “1/5”

The second test will check that you use the $route.params.page property to display the correct page if it exists. To check this, you’ll mock the $route.params object. Add the next code to src/views/__tests__/ItemList.spec.js.

Listing 10.4. Mocking $route.params
test('renders 2/5 when on page 2 of 5', () => {
  const store = createStore({
    getters: {
      maxPage: () => 5
    }
  })
  const mocks = {
    $route: {
      params: {
        page: '2'
      }
    }
  }
  const wrapper = createWrapper({ mocks, store })
  expect(wrapper.text()).toContain('2/5')
})

Make sure the tests fail: npm run test:unit. Now make the tests pass by rendering the correct page/max page values in the ItemList. Open src/views/ItemList.vue, and add the next code to the <template> block.

Listing 10.5. Using the $route.params value in the template
<span>
  {{$route.params.page || 1}}/{{$store.getters.maxPage}}       1
</span>

  • 1 Displays page/maxPage using route params and maxPage; defaults to 1 if there is no page param

Run the tests with npm run test:unit. Great—the tests are passing.

You’re making the assumption that the page parameter is always valid. Making those kind of assumptions is dangerous, especially because the user can control the page param by changing the URL. You should add some code to handle invalid values and redirect to a valid page if the page value is invalid. To do that, you’ll learn how to test the $router property.

10.1.2. Testing the $router property

$router is the router instance, and it contains helper methods to control the routing programmatically. $router is an instance property, so you can control the $router value in tests by using the Vue Test Utils mocks mounting option.

In your app, you’re using the page URL to render the current page. So, when a user lands on /top/5, and there are 40 pages of items, they’ll see that they are on page 5 out of 40 (5/40).

But what happens if the user lands on page /top/500 when there are only 10 pages of items? Right now, the ItemList would render 500/10. That’s going to look pretty buggy to the user. Instead of leaving them on a buggy page, you should redirect them to the first page of items.

To send users to a different page, you can use the $router.replace method. $router.replace replaces the current URL and updates the RouterLink. It’s like a redirect in your code.

You’ll add a test that checks that the component calls replace with the first page of the current list the user is on if the page parameter is larger than the max page. Add the code that follows to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 10.6. Testing a router.replace call
test('calls $router.replace when the page parameter is greater than the max page count', async () => {
  expect.assertions(1)
  const store = createStore({
    getters: {
      maxPage: () => 5
    }
  })

  const mocks = {                                                1
    $route: {
      params: {
        page: '1000'
      }
    },
    $router: {
      replace: jest.fn()
    }
  }
  createWrapper({ mocks, store })
  await flushPromises()
  expect(mocks.$router.replace).toHaveBeenCalledWith('/top/1')   2
})

  • 1 Create mocks to pass to createWrapper
  • 2 Assert $router.replace was called with the correct arguments

Check that the test fails with an assertion error: npm run test:unit. Now you need to update the component to perform the redirect. You’ll add the $router.replace call inside the loadItems method, after the initial call to fetchListData. If you call it before fetchListData has completed, you won’t have the correct data in the store to calculate how many pages there are.

Open src/views/ItemList.vue, and replace the loadItems method with the following code.

Listing 10.7. Calling $router.replace
loadItems () {
  this.$bar.start()
  this.$store.dispatch('fetchListData', {
    type: this.$route.params.type
  }).then(() => {
    if (this.$route.params.page > this.$store.getters.maxPage) {
      this.$router.replace(`/${this.$route.params.type }/1`)
      return
    }
    this.$bar.finish()
  })
  .catch(() => {
    this.$bar.fail()
  })
}

Now the unit tests will pass again: npm run test:unit. You could write other tests here to check that different page parameters are handled. For example, what would happen if the path was /top/abcd, or /top/–123? The tests would be similar to the test that you just wrote, so I won’t show you how to write them here.

Congratulations, you’ve successfully added tests for logic that uses the $route and $router properties—the most notorious instance properties to write tests for. They are notorious because they have a common gotcha that catches a lot of developers out.

10.1.3. Avoiding common gotchas

The $route and $router properties are added as read-only properties to the Vue constructor prototype when Vue Router is installed. Whatever you do, there’s no way to overwrite Vue Router properties after they’ve been added to the Vue prototype. I’ve seen many people stung by this—it’s probably the most common issue people come to me with!

Remember in the last chapter when I talked about how the base Vue constructor is like a master copy? You shouldn’t write on the master copy, because everything that’s copied from it will include that writing. Installing Vue Router on the base constructor is like taking a permanent marker and scribbling all over the master copy. The Vue Router properties are never coming off, and you can’t overwrite them no matter what you do.

I’ve already spoken about why you should use a localVue constructor and avoid installing on the base constructor. This is especially important for Vue Router. Always use a localVue to install Vue Router in tests. You must make sure that no file in your test suite imports a file that calls Vue.use with Vue Router. It’s easy to accidentally import a file that includes Vue.use. Even if you don’t run a module, if the module is imported, then the code inside it will be evaluated. You can see an example in figure 10.1.

Figure 10.1. Unintentionally calling Vue.use when importing a module

I hope that you now share my wariness of Vue Router in tests. Now that you know to avoid installing Vue Router on the Vue base constructor, it’s time to learn how to test the RouterLink component.

10.2. Testing the RouterLink component

RouterLink components add Vue Router–friendly links to navigate between views. If you use logic to render RouterLink components, then you should write tests for them.

To learn how to test RouterLink components, you’re going to add pagination links to the Hacker News app. The app will render a RouterLink component that links to the previous page or the next page. For example, if you’re on /top/3, you would render one RouterLink component that links to /top/2, and one that links to /top/4 (figure 10.2). If there are no previous pages (or no more pages) to navigate to, you’ll render an <a> tag without an href (figure 10.3).

Figure 10.2. Pagination on the third page

Figure 10.3. Pagination on the first page

You can write the following four tests for ItemList to check that the pagination links are rendered correctly:

  • ItemList renders a RouterLink to the previous page if one exists.
  • ItemList renders an a tag without an href if there are no previous pages.
  • ItemList renders a RouterLink to the next page if one exists.
  • ItemList renders an a tag without an href if there are no next pages.

To create a RouterLink component that links to another view, you pass the RouterLink a to prop with a path like so:

<router-link to="/top/2">top</router-link>

To test that you render a RouterLink to another page, you need to assert that a RouterLink component receives the correct to prop. Remember, you can test component props with the wrapper find method. find uses a selector to get a matching node wrapper from the rendered output, as shown in the next listing.

Listing 10.8. Testing that a ChildComponent receives a prop
import { shallowMount } from '@vue/test-utils'
import ChildComponent from './ChildComponent.vue'
import ParentComponent from './ParentComponent.vue'

test('renders Child', () => {
  const wrapper = shallowMount(ParentComponent)
  expect(wrapper.find(ChildComponent).props().to).toBe('/path')       1
})

  • 1 Asserts that ChildComponent receives a to prop of /path

The problem is, Vue Router does not export the RouterLink or RouterView components, so you can’t use the RouterLink as a selector. The solution is to control the component that is rendered as a RouterLink and use the controlled component as a selector instead. You can control the rendered component with Vue Test Utils. When a parent Vue component renders a child component, Vue attempts to resolve the child component on the parent component instance. With the Vue Test Utils stubs option, you can override this process. For example, you could set the component to resolve all RouterLink components as <div> elements, as shown in the following listing.

Listing 10.9. Stubbing RouterLink in a test
const wrapper = shallowMount(TestComponent, {
  stubs: {
    RouterLink: 'div'         1
  }
})

  • 1 Sets all RouterLink components to render as <div> elements
Tip

You can use the component name, a camelCase version of the name, or a PascalCase (capitalized) version of the name in the stubs mounting option. So router-link, routerLink, and RouterLink would all stub the RouterLink component.

Vue Test Utils exports a RouterLinkStub component that behaves like a RouterLink component. You can stub all RouterLink components to resolve as the RouterLinkStub component and use RouterLinkStub as a selector, as shown next.

Listing 10.10. Using a RouterLinkStub
import { shallowMount, RouterLinkStub } from '@vue/test-utils'
import ParentComponent from './ParentComponent.vue'

test('renders RouterLink', () => {
  const wrapper = shallowMount(ParentComponent, {
    stubs: {
      RouterLink: RouterLinkStub            1
    }
  })
  expect(wrapper.find(RouterLinkStub).props().to).toBe('/path')
})

  • 1 Stubs the RouterLink component with a RouterLinkStub component
Note

You can read more about the stubs mounting option in the vue-test-utils docs at https://vue-test-utils.vuejs.org/api/options.html#stubs.

You’ll use this stubbing technique to test that you render a RouterLink with the correct props. In your ItemList test file, add RouterLink to the stubs option in the createWrapper factory function. In src/views/__tests__/ItemList.spec.js, import RouterLinkStub from Vue Test Utils as shown next:

import { shallowMount, createLocalVue, RouterLinkStub } from '@vue/test-utils'

Next, in the createWrapper factory function, add a stubs property to the defaultMountingOptions object to stub all RouterLink components with the RouterLinkStub:

stubs: {
  RouterLink: RouterLinkStub
}

Now you can use the RouterLinkStub component as a selector to find rendered RouterLink components. The first test to write checks that ItemList renders a RouterLink with the previous page if one exists. In the test, you should find a RouterLinkStub and check that it has the correct to prop and text.

Add the code from the next listing to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 10.11. Using RouterLinkStub to find a component
test('renders a RouterLink with the previous page if one exists', () => {
  const mocks = {                                                         1
    $route: {
      params: { page: '2' }
    }
  }
  const wrapper = createWrapper({ mocks })

  expect(wrapper.find(RouterLinkStub).props().to).toBe('/top/1')          2
  expect(wrapper.find(RouterLinkStub).text()).toBe('< prev')
})

  • 1 Creates mocks to pass to the createWrapper factory function
  • 2 Finds the stubbed router-link with a RouterLinkStub selector

You can write another test to make sure the ItemList component renders a RouterLink for the next page. To do this, you’ll need use the createStore factory function to create a store that has enough items to generate a next page. Add the two tests from the following listing to your test suite.

Listing 10.12. Using RouterLinkStub as a selector
test('renders a RouterLink with the next page if one exists', () => {
  const store = createStore({
    getters: {
      maxPage: () => 3
    }
  })
  const mocks = {
    $route: {
      params: { page: '1' }
    }
  }
  const wrapper = createWrapper({ store, mocks })
  expect(wrapper.find(RouterLinkStub).props().to).toBe('/top/2')
  expect(wrapper.find(RouterLinkStub).text()).toBe('more >')
})

test('renders a RouterLink with the next page when no page param exists', () => {
  const store = createStore({
    getters: {
      maxPage: () => 3
    }
  })
  const wrapper = createWrapper({ store
})
  expect(wrapper.find(RouterLinkStub).props().to).toBe('/top/2')
  expect(wrapper.find(RouterLinkStub).text()).toBe('more >')
})

Before you add the code to the ItemList component, you should write the tests for when a previous page or next page doesn’t exist.

When there isn’t a previous page, the ItemList should render an <a> tag without an href, which you’ll style to appear disabled. You’ll do the same thing if there isn’t a next page. Add the tests from the following code to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 10.13. Testing that router-link is rendered
test('renders an <a> element without an href if there are no previous pages',
     () => {
  const wrapper = createWrapper()

  expect(wrapper.find('a').attributes().href).toBe(undefined)       1
  expect(wrapper.find('a').text()).toBe('< prev')                   2
})

test('renders an <a> element without an href if there are no next pages', ()
     => {
  const store = createStore({
    getters: {
      maxPage: () => 1                                              3
    }
  })
  const wrapper = createWrapper({ store })

  expect(wrapper.findAll('a').at(1).attributes().href).toBe(undefined)
  expect(wrapper.findAll('a').at(1).text()).toBe('more >')
})

  • 1 Asserts that the <a> element doesn’t have an href
  • 2 Asserts that the <a> element contains correct text
  • 3 Sets maxPage to 1, so there isn’t a next page to link to

Now make the test pass by rendering RouterLink components in the ItemList template. Add the code from the next listing to src/views/ItemList.vue in the <template> block. Warning: this code block is ugly.

Listing 10.14. Using router-link in the template
<router-link
  v-if="$route.params.page > 1"
  :to="'/' + $route.params.type + '/' + ($route.params.page - 1)">      1
  &lt; prev                                                             2
</router-link>
<a v-else>&lt; prev</a>                                                 3
<span>{{ $route.params.page || 1 }}/{{ $store.getters.maxPage }}</span>
<router-link
  v-if="($route.params.page || 1) < $store.getters.maxPage"             4
  :to="'/' +  $route.params.type + '/' + ((Number($route.params.page) || 1) + 1)">
    more &gt;
</router-link>
<a v-else>more &gt;</a>

  • 1 Creates a link to the previous page using type and page params
  • 2 Escapes the < character, to avoid potential HTML parse errors
  • 3 Renders an <a> element if no previous page exists
  • 4 Constructs a link for the next page if one exists

Run the tests and watch them pass. As a side note: the code in the template is just bad. There’s a lot of repetition, and it’s difficult to read—this is a prime candidate for refactoring. The reason I had you add it is because it’s simple and it makes the test pass. Now that the test passes, you’re free to refactor the component. Refactoring is easy when you have unit tests. I won’t show you how to refactor in this chapter, but you can refactor yourself and compare to a refactored version in the chapter-11 branch.

Rendering Vue Router components in tests

Vue Router components are registered as global components when you install Vue Router. They are available for a component to render only if Vue Router was installed on a Vue constructor before the Vue instance was instantiated. If you mount a component that renders a RouterLink or RouterView component without either stubbing them or installing Vue Router on a localVue constructor, you will get warnings in your test output.

I recommend that you stub these components with the stubs mounting option in a wrapper factory function rather than installing Vue Router on a localVue constructor, so that you can overwrite Vue Router properties if required.

Now you’ve rendered pagination links and the current page in the ItemList component. There’s one problem, though—the paginated pages don’t change the content that’s rendered. To change the items that are rendered, you need to edit the displayItems getter to use the route page parameter. You can do that by syncing Vuex and Vue Router.

10.3. Using Vuex with Vue Router

It can be useful to use Vue Router properties in a Vuex store. You can use the library vuex-router-sync to synchronize Vuex and Vue Router to make the route object available in the store.

In the Hacker News app, you’re going to use the current page number, defined in route.params, to display the correct items and add pagination. You’ll do that by updating the displayItems Vuex getter to use the current page parameter from the route object.

10.3.1. Adding the route to the store

You need to add the route to the store, so that you can access the $route.params object in a store getter. You can use the vuex-router-sync library to add the route object for you.

The first step is to install the package as a dependency. Run the following install command:

npm install --save vuex-router-sync

Add the following import statement to src/main.js:

import { sync } from 'vuex-router-sync'

In src/main.js, after you create the store and router instances, call sync to sync them together as follows:

sync(store, router)

Now the store will include a route object, which has the same value as $route in component instances.

10.3.2. Using router parameters in the store

To add pagination, you need to display a range of items depending on the current page parameter. For example, if you’re on page 2, you should display the items from 20–40. If you’re on page 10, you’ll display the items from 200–220. This way, the user will feel like they’re navigating between pages.

In your app, you use the displayItems getter to return the items to render. Right now, displayItems return the first 20 items from state.items. To achieve pagination, you’ll update the getter to use the route.params.page value to calculate which items to return.

You need to add a new test for the displayItems getter to check that you return the items from index 20 to index 40 if the route page parameter is 2. In the test, you’ll create an array of numbers to use as mock items and then create a mock state object. You call the displayItems getter with the mock state and check that the getter returns the correct items. Add the code from the next listing to src/store/__tests__/getters.spec.js, inside the describe block.

Listing 10.15. Testing a getter that uses the route object
test('displayItems returns items 20-40 if page is 2', () => {
  const items = Array(40).fill().map((v, i) => i)              1
  const result = getters.displayItems({                        2
    items,
    route: {
      params: {
        page: '2'                                              3
      }
    }
  })
  const expectedResult = items.slice(20, 40)
  expect(result).toEqual(expectedResult)                       4
})

  • 1 Creates an array of 40 items. Each item will be a number of the item index, so the array will be 0, 1, 2, 3 up to 39.
  • 2 Calls displayItems with a mock state
  • 3 Sets the mock page parameter to 2
  • 4 Asserts that displayItems returns items with numbers from 19 to 39

You’ll add another test case to make sure the getter returns the remaining items if there isn’t a full page of items remaining. Add the code from the next listing to the describe block in src/store/__tests__/getters.spec.js.

Listing 10.16. Testing a getter that uses the route object
test('displayItems returns remaining items if there are insufficient
 items', () => {
  const numberArray = Array(21).fill().map((v, i) => i)    1
  const store = {
    items: numberArray,
    route: {
      params: {
        page: '2'
      }
    }
  }
  const result = getters.displayItems(store)
  expect(result).toHaveLength(1)
  expect(result[0]).toEqual(numberArray[20])               2
})

  • 1 Creates an array of 21 items. Each item will be a number of the item index, so the array will be 0, 1, 2, 3 up to 20.
  • 2 Asserts that the item is the last item in the items array

To make the test pass, you need to update the displayItems getter. Open src/store/getters.js, and replace the displayItems getter with the code shown in listing 10.17. This code uses the OR operator to default to a value of 1 for page. If Number (state.route.params.page) returns a number, the expression will be evaluated as truthy and will return the number. If Number(state.route.params.page) returns undefined, the expression will evaluate to false and return the value 1.

Note

If this usage of the OR operator confuses you, check out the guide on MDN. It explains how logical operators return values—http://mng.bz/zMMg.

Listing 10.17. Using the route params inside a Vuex getter
displayItems (state) {
  const page = Number(state.route.params.page) || 1        1
  const start = (page - 1) * 20                            2
  const end = page * 20                                    3

  return state.items.slice(start, end)                     4
},

  • 1 Casts the state.route.params.page value to a number; defaults to 1 if the page value is undefined
  • 2 Calculates where the array should be sliced from
  • 3 Calculates what position in the array the last item should be
  • 4 Returns an array containing the correct items

Now run your tests. Great—the new test passed, but another unit tests is failing with ugly type errors.

The problem is that previous tests that call the displayItems getter don’t have a route object in the state. That means there’s a type error when displayItems tries to access route.params. To fix this, you need to update previous tests to include a route object that contains an empty params object.

Open src/store/__tests__/getters.spec.js. You need to update the test displayItems returns the first 20 items from the list matching state .displayItems. In the state object, add a route object that with an empty params object as follows:

const state = {
  // ..
  route: {
    params: {}
  }
}

Now run the tests. You still have a failing test: the test in src/store/__tests__/store -config.spec.js is creating an error. Leaky buckets everywhere! It’s the same problem—route isn’t defined. In this test, though, you aren’t mocking the state, so you need to sync the router and store in the test with vuex-router-sync.

You’ll create a router instance and call the vuex-router-sync sync method before you create the store instance. Open src/store/__tests__/store-config.spec.js, and import sync from vuex-router-sync as follows:

import Router from 'vue-router'
import { sync } from 'vuex-router-sync'
import routerConfig from '../../router/router-config'

Add the code from the following listing to src/store/__tests__/store-config.spec.js. The purpose is to create an initial state just like the initial state you create in the app entry file.

Listing 10.18. Syncing Vuex and Vue Router on a localVue
localVue.use(Vuex)
localVue.use(Router)                         1
const store = new Vuex.Store(storeConfig)
const router = new Router(routerConfig)      2
sync(store, router)                          3

  • 1 Installs Vue Router on the localVue constructor
  • 2 Creates a new router instance
  • 3 Syncs the store and the router, so that the store contains a router object in its state

Now you’ve set up the displayItems getter to handle pages. That means the app will support pagination. Run the development server—npm run serve—and then open the development server and take a look.

The app is looking good now. It renders different feeds, and it has pagination to navigate between pages of feed items. In the next chapter, you’ll learn how to write and test mixins and filters to add the finishing touches to the UI.

Summary

  • The $route and $router properties cause difficult-to-debug problems when they are installed on the base Vue class.
  • RouterLink components can be tested using the Vue Test Utils stubs mounting option.
  • You can sync Vuex and Vue Router using the vuex-router-sync library.

Exercises

1

Write a test to check that the following component calls injectedMethod with the $route.path Vue instance value:

// TestComponent.vue
<script>
export default {
  beforeMount() {
    this.injectedMethod(this.$route.path)
  }
}
</script>

2

What library can you use if you want to use the current $route values inside a Vuex store?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.42.196