8

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.

Eliran Malka
  • 15,821
  • 6
  • 77
  • 100
Ryan Gill
  • 1,728
  • 2
  • 13
  • 27
  • @Eliran Malka, your "reformatting" made code less readable. What was the problem with 2 white spaces for a tab and comma-first array? Please revert the change, ty. – euvl Mar 13 '17 at 17:14
  • 1
    I see you've added console logs accessing the data and computed properties of the component you are testing. Are they reporting what you expect them to? It is recommended that you add comments above/next to the logs describing the actual result and the expected result as opposed to leaving them uncommented. – Wing Mar 13 '17 at 19:02
  • 1
    @wing yea, the logs are as expected. I just updated the question with the values of the logs as comments. Thanks. – Ryan Gill Mar 13 '17 at 19:28
  • Are you able to add breakpoints and inspect the scope? What does this yield? Does the computed method work as expected outside a test environment? – Wing Mar 13 '17 at 19:40
  • 1
    Seems like it works without using `Avoriaz` `let comp = new Vue(ComputedPropTest, MOCK_PROPS_DATA).$mount(); comp.crew = DATA; console.log(comp.canAdd);` – Mike Fielden Mar 13 '17 at 19:45
  • @wing not sure on adding a breakpoint but if I log out `this` inside the component's computed property, it returns an empty object `{}`. – Ryan Gill Mar 13 '17 at 19:52
  • That suggests to me that the wrong `this` is being bound to the computed getter. Do you know if the code is being run with a `'use-strict'` pragma? – Wing Mar 13 '17 at 20:43
  • @wing yes it running in strict mode but possible issues with the fat arrows? – Ryan Gill Mar 13 '17 at 20:57
  • 1
    I don't believe arrow functions are preventing `this` being bound correctly. You _could_ attempt to change some arrow functions but I doubt it'll work. Is your test code running in the browser? You're using Ava so I am assuming it isn't. My suspicion is that not running Avoriaz in the browser is causing issues: I've run a reduced test case in the browser and in a non-browser environment. The browser passes while the non-browser environment fails – although I'm still looking into what exactly is happening and why. – Wing Mar 13 '17 at 21:11
  • 1
    Update on the environment theory: I think I'm ruling it out. I think Avoriaz is influencing the binding of `this` to something that you don't want. – Wing Mar 13 '17 at 21:36
  • 1
    Hi Ryan, sorry you're having problems with avoriaz. If you make an issue on https://github.com/eddyerburgh/avoriaz/issues I'll work on a fix this weekend and notify you when it's done. If you don't have time, I'll write it up myself and get it fixed within a week :) – Edward Mar 16 '17 at 19:32

1 Answers1

5

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.

Wing
  • 8,438
  • 4
  • 37
  • 46
  • 1
    Hi, thanks for this detailed answer. I'm the author of avoriaz and am keen to fix bugs this bug for the next release – Edward Mar 16 '17 at 19:29
  • 1
    @Edd: no worries. I'm not familiar with Avoriaz or Vue's internals so this report is just my observation and comment on what I believe is happening. Hopefully it'll help you in getting this resolved. One potential path to look down is seeing what `vm.$options.computed` is for – I suspect it may not be the place that reliably gives you computed properties, but I can't confirm the core team's position on this. – Wing Mar 17 '17 at 09:35
  • Good explanation. Computed properties are not easy to test. – bmfteixeira Jul 06 '17 at 11:38