alanbuchanan
alanbuchanan

Reputation: 4173

Vue component testing using Karma: 'undefined is not an object'

I am working on an app which was created with the Vue loader's webpack template.

I included testing with Karma as an option when creating the project, so it was all set up and I haven't changed any of the config.

The app is a Github user lookup which currently consists of three components; App.vue, Stats.vue and UserForm.vue. The stats and form components are children of the containing app component.

Here is App.vue:

<template>
  <div id="app">
    <user-form
      v-model="inputValue"
      @go="submit"
      :input-value="inputValue"
    ></user-form>
    <stats
      :username="username"
      :avatar="avatar"
      :fave-lang="faveLang"
      :followers="followers"
    ></stats>
  </div>
</template>

<script>
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import _ from 'lodash'
import UserForm from './components/UserForm'
import Stats from './components/Stats'

Vue.use(VueAxios, axios)

export default {
  name: 'app',

  components: {
    UserForm,
    Stats
  },

  data () {
    return {
      inputValue: '',
      username: '',
      avatar: '',
      followers: [],
      faveLang: '',
      urlBase: 'https://api.github.com/users'
    }
  },

  methods: {
    submit () {
      if (this.inputValue) {
        const api = `${this.urlBase}/${this.inputValue}`

        this.fetchUser(api)
      }
    },

    fetchUser (api) {
      Vue.axios.get(api).then((response) => {
        const { data } = response

        this.inputValue = ''
        this.username = data.login
        this.avatar = data.avatar_url

        this.fetchFollowers()
        this.fetchFaveLang()
      }).catch(error => {
        console.warn('ERROR:', error)
      })
    },

    fetchFollowers () {
      Vue.axios.get(`${this.urlBase}/${this.username}/followers`).then(followersResponse => {
        this.followers = followersResponse.data.map(follower => {
          return follower.login
        })
      })
    },

    fetchFaveLang () {
      Vue.axios.get(`${this.urlBase}/${this.username}/repos`).then(reposResponse => {
        const langs = reposResponse.data.map(repo => {
          return repo.language
        })

        // Get most commonly occurring string from array
        const faveLang = _.chain(langs).countBy().toPairs().maxBy(_.last).head().value()
        if (faveLang !== 'null') {
          this.faveLang = faveLang
        } else {
          this.faveLang = ''
        }
      })
    }
  }
}
</script>

<style lang="stylus">
body
  background-color goldenrod

</style>

Here is Stats.vue:

<template>
  <div class="container">
    <h1 class="username" v-if="username">{{username}}</h1>
    <img v-if="avatar" :src="avatar" class="avatar">
    <h2 v-if="faveLang">Favourite Language: {{faveLang}}</h2>
    <h3 v-if="followers.length > 0">Followers ({{followers.length}}):</h3>
    <ul v-if="followers.length > 0">
      <li v-for="follower in followers">
        {{follower}}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: 'stats',
    props: [
      'username',
      'avatar',
      'faveLang',
      'followers'
    ]
  }
</script>

<style lang="stylus" scoped>
h1
  font-size 44px

.avatar
  height 200px
  width 200px
  border-radius 10%

.container
  display flex
  align-items center
  flex-flow column
  font-family Comic Sans MS

</style>

And here is UserForm.vue:

<template>
  <form @submit.prevent="handleSubmit">
    <input
      class="input"
      :value="inputValue"
      @input="updateValue($event.target.value)"
      type="text"
      placeholder="Enter a GitHub username..."
    >
    <button class="button">Go!</button>
  </form>
</template>

<script>
export default {
  props: ['inputValue'],
  name: 'user-form',
  methods: {
    updateValue (value) {
      this.$emit('input', value)
    },
    handleSubmit () {
      this.$emit('go')
    }
  }
}
</script>

<style lang="stylus" scoped>

input
  width 320px

input,
button
  font-size 25px

form
  display flex
  justify-content center

</style>

I wrote a trivial test for UserForm.vue which test's the outerHTML of the <button>:

import Vue from 'vue'
import UserForm from 'src/components/UserForm'

describe('UserForm.vue', () => {
  it('should have a data-attribute in the button outerHTML', () => {
    const vm = new Vue({
      el: document.createElement('div'),
      render: (h) => h(UserForm)
    })
    expect(vm.$el.querySelector('.button').outerHTML)
      .to.include('data-v')
  })
})

This works fine; the output when running npm run unit is:

   UserForm.vue
    ✓ should have a data-attribute in the button outerHTML

However, when I tried to write a similarly simple test for Stats.vue based on the documentation, I ran into a problem.

Here is the test:

import Vue from 'vue'
import Stats from 'src/components/Stats'

// Inspect the generated HTML after a state update
it('updates the rendered message when vm.message updates', done => {
  const vm = new Vue(Stats).$mount()
  vm.username = 'foo'
  // wait a "tick" after state change before asserting DOM updates
  Vue.nextTick(() => {
    expect(vm.$el.querySelector('.username').textContent).toBe('foo')
    done()
  })
})

and here is the respective error when running npm run unit:

ERROR LOG: '[Vue warn]: Error when rendering root instance: '
  ✗ updates the rendered message when vm.message updates
        undefined is not an object (evaluating '_vm.followers.length')

I have tried the following in an attempt to get the test working:

Why is the error referring to _vm.followers.length? What is _vm with an underscore in front? How can I get around this issue to be able to successfully test my component?

(Repo with all code: https://github.com/alanbuchanan/vue-github-lookup-2)

Upvotes: 1

Views: 2278

Answers (1)

Linus Borg
Linus Borg

Reputation: 23968

Why is the error referring to _vm.followers.length? What is _vm with an underscore in front?

This piece of code is from the render function that Vue compiled your template into. _vm is a placeholder that gets inserted automatically into all Javascript expressions when vue-loader converts the template into a render function during build - it does that to provide access to the component.

When you do this in your template:

{{followers.length}}

The compiled result in the render function for this piece of code will be:

_vm.followers.length


Now, why does the error happen in the first place? Because you have defined a prop followers on your component, but don't provide any data for it - therefore, the prop's value is undefined

Solution: either you provide a default value for the prop:

// Stats.vue
props: {
  followers: { default: () => [] }, // function required to return fresh object
  // ... other props
}

Or you propvide acual values for the prop:

// in the test:
const vm = new Vue({
  ...Stats,
  propsData: {
    followers: [/* ... actual data*/]
  }
}).$mount()

Upvotes: 1

Related Questions