Search code examples
javascriptvue.jscomputed-propertiesavaavoriaz

Test computed property in Vue.js using AVA with Avoriaz


I'm trying to test a computed property of a Vue.js component using AVA and Avoriaz. I can mount the component and access the data properties fine.

When I try an access a computed property, the function doesn't seem to have scope to the data on that component.

computed: {
  canAdd() {
    return this.crew.firstName !== '' && this.crew.lastName !== '';
  }

The error I get is Error: Cannot read property 'firstName' of undefined

Test file:

import Vue from 'vue';
import { mount }
from 'avoriaz';
import test from 'ava';
import nextTick from 'p-immediate';
import ComputedPropTest from '../../../js/vue-components/computed_prop_test.vue';

Vue.config.productionTip = false;

test.only('Should handle computed properties', async(t) => {
  const MOCK_PROPS_DATA = {
      propsData: {
        forwardTo: '/crew',
        crew: {}
      }
    },
    wrapper = mount(ComputedPropTest, MOCK_PROPS_DATA),
    DATA = {
      crew: {
        firstName: 'Ryan',
        lastName: 'Gill'
      }
    };

  wrapper.setData(DATA);
  await nextTick();

  console.log('firstName: ', wrapper.data().crew.firstName); // Ryan

  console.log('isTrue: ', wrapper.computed().isTrue()); // true
  console.log('canAdd: ', wrapper.computed().canAdd()); // Errors

  t.true(wrapper.computed().isTrue());
});

Component:

<template>
  <div>
    <label for="firstName" class="usa-color-text-primary">First Name
      <i class="tooltipTextIcon fa fa-info-circle usa-color-text-gray" title="First name of crew."></i>
      <span class="required usa-additional_text usa-color-text-secondary-dark">Required</span>
    </label>
    <input id="firstName" type="text" class="requiredInput" name="firstName" v-model="crew.firstName" autofocus>
    <label for="lastName" class="usa-color-text-primary">Last Name
      <i class="tooltipTextIcon fa fa-info-circle usa-color-text-gray" title="Last name of crew."></i>
      <span class="required usa-additional_text usa-color-text-secondary-dark">Required</span>
    </label>
    <input id="lastName" type="text" class="requiredInput" name="lastName" v-model="crew.lastName" autofocus>
  </div>
</template>

<script>
  export default {
    name: 'crew-inputs',
    data() {
      return {
        crew: {
          firstName: '',
          lastName: ''
        }
      }
    },
    computed: {
      canAdd() {
        return this.crew.firstName !== '' && this.crew.lastName !== '';
      },
      isTrue() {
        return true;
      }
    }
  }
</script>

The isTrue computed property seems to work but doesn't rely on any of the data in the component.


Solution

  • Problem

    What is happening?

    After a long look and discussion, it looks like the this context of the computed getter is being set to something unexpected. As a result of the unexpected this context, this no longer refers to the Vue instance, leading to component properties being unaccessible.

    You are witnessing this with the runtime error

    Error: Cannot read property 'firstName' of undefined

    Why is this happening?

    Without a deep dive into how Avoriaz and Vue are working, we cannot know. I did attempt a deeper investigation with the following minimal, complete and verifiable example. You or others may want to take a deeper look into it.

    'use-strict';
    
    import Vue from 'vue';
    import { mount } from 'avoriaz';
    
    const FooBar = {
      template: `
        <div>{{ foobar }}</div>
      `,
    
      data() {
        return {
          foo: 'foo',
          bar: 'bar',
        };
      },
    
      computed: {
        foobar() {
          debugger;
          return `${this.foo} ${this.bar}`;
        },
      },
    };
    
    const vueMountedCt = new Vue(FooBar).$mount();
    const vueMountedVm = vueMountedCt;
    
    const avoriazMountedCt = mount(FooBar);
    const avoriazMountedVm = avoriazMountedCt.vm;
    
    /**
     * Control case, accessing component computed property in the usual way as documented by Vue.
     *
     * @see {@link https://vuejs.org/v2/guide/computed.html}
     *
     * Expectation from log: 'foobar' (the result of the computed property)
     * Actual result from log: 'foobar'
     */
    console.log(vueMountedVm.foobar);
    
    /**
     * Reproduce Avoriaz's method of accessing a Vue component's computed properties.
     * Avoriaz returns the Vue instance's `$option.computed` when calling `wrapper.computed()`.
     *
     * @see {@link https://github.com/eddyerburgh/avoriaz/blob/9882f286e7476cd51fe069946fee23dcb2c4a3e3/src/Wrapper.js#L50}
     *
     * Expectation from log: 'foobar' (the result of the computed property)
     * Actual result from log: 'undefined undefined'
     */
    console.log(vueMountedVm.$options.computed.foobar());
    
    /**
     * Access Vue component computed property via Avoriaz's documented method.
     *
     * @see {@link https://eddyerburgh.gitbooks.io/avoriaz/content/api/mount/computed.html}
     *
     * Expectation from log: 'foobar' (the result of the computed property)
     * Actual result from log: 'undefined undefined'
     */
    console.log(avoriazMountedCt.computed().foobar());
    

    Some observations:

    • Looking at the call stack of control case (case 1), you can see Vue's internals setting the this context to the Vue instance.

    Call stack of case 1. Getter function's <code>this</code> is being set to Vue instance

    • Looking at the call stack of the failing cases, the this context of the computed function is not being set.

    Call stack of failing cases. The <code>this</code> context of the computed function is not being set

    As to why this is happening – I have no idea. To understand this I think we will need to know why vm.$options.computed exists, the planned use cases from the core Vue team and if the behaviour we are experiencing is expected.

    What can I do about this?

    You can work around this by doing

    wrapper.computed().canAdd.call(wrapper.vm);
    

    It may also be recommended you open issues in Avoriaz and/or Vue.