Ava: [recipe] Add Vue.js recipe

Created on 17 Feb 2017  Â·  21Comments  Â·  Source: avajs/ava

Note: this is not an issue but a reminder for me to add a recipe for Vue file support.

Add a Vue.js recipe for Vue file transpiling. The new vue-node (https://github.com/knpwrs/vue-node) works great as a solution to get Ava and Vue.js working along side each other. Little configuration needed other than using prexisting webpack configs.

I have it fully working together, although it is quite slow. I believe there is extra precompilation happening, caching transpiled code in vue-node should solve this issue.

I'm also looking into checking coverage reports with Vue.js files, since webpack is driving the transpiling it should work (fingers crossed).

Once I have completed my investigation I will add a recipe.ill keep this issue up to date. May be a couple of weeks, due to finishing a work project.

Most helpful comment

Will add recipe over next couple of days, should have enough free time to complete.

Require.extension.hooks is probably the best solution, most extensible and fastest. vue-node is much slower as each component requeires Webpack to do alot of work. Which adds approx 3-4s to each component import.

Extension hooks seems to have least impact, and with caching techniques this could be much faster to an already fast solution. Less configuration required aswell. Also mapping of code coverage is very accurate and not yet seen any issues.

All 21 comments

Thanks @blake-newman!

Meanwhile, @knpwrs has written an article that is very well: http://knpw.rs/blog/testing-vue-in-node

@forresst Thanks for this!

Hey, guys. @jackmellis commented on my post with his own approach which is rather interesting. He points to a forum post he made as well as two modules he made: require-extension-hooks and require-extension-hooks-vue. I think the implications of either approach should be discussed and the vue community should settle on one. This issue may or may not be the place to discuss that, but I would happily deprecate my package if people like @jackmellis' approach better, or keep it around if there are valid use cases for either approach.

https://github.com/jackmellis/require-extension-hooks-vue/pull/1

This adds more support for compiling of templates which should a) improve overall speeds and b) support es2015 features in templates

I've been playing with ava this last week with vue. It seems to work fine with require-extension-hooks - although loading babel-core in every process is incredibly slow - I found myself just writing a small script that just replaces all import/export statements instead of using the full force of babel in a node environment.

One issue I've come across is that ava will often hang when an assertion fails on a reactive property. I assume it's to do with trying to build a fairly complicated vue-embroiled stack trace?

Example:

// where vm.dirty === false

// This test causes ava to hang until the timeout has elapsed (and even then sometimes it keeps hanging)
test('...', t => {
  t.true(vm.dirty);
});

// This test works fine
test('...', t => {
  var dirty = vm.dirty;
  t.true(dirty); // dirty == false
});

I could make it work using vue-node.
Its working great with istanbul for coverage using nyc:
https://github.com/marcosmoura/vue-boilerplate

Nice. I've taken the exact test code and done the same with require-extension-hooks instead of webpack. Goes from 6.8s to 2.4s on my machine...

https://github.com/jackmellis/avoriaz-ava-example

Will add recipe over next couple of days, should have enough free time to complete.

Require.extension.hooks is probably the best solution, most extensible and fastest. vue-node is much slower as each component requeires Webpack to do alot of work. Which adds approx 3-4s to each component import.

Extension hooks seems to have least impact, and with caching techniques this could be much faster to an already fast solution. Less configuration required aswell. Also mapping of code coverage is very accurate and not yet seen any issues.

Hi, How about mocking dependencies in vue component in this approach?

Ideally any dependencies you need to mock should be provided via props. When that's the case, mocking is trivial.

@knpwrs Thank you for a quick answer to my question. I'm new in Vue.js world and I don't fully understand how I should test Vue components (everything in a single file).

Yesterday I created a simple setup with ava using guides from this issue.
Before I found this issue I was looking on Vue.js docs to see how recommended approach looks like, but I only found information about mocha + karma + webpack setup. Then I found this page https://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html

The solution suggested in this issue doesn't use vue-loader so I can't mock ES6 modules, right? That's why I asked about other ways to easily mock ES6 modules. There is no constructor dependency mechanism or IoC container in place.

Here are some files with my approach:

For now, I just created some method to simply return an instance, so I can easily mock it in tests here. I'm curious about how can I use props to pass ES6 module dependency. Isn't props designed to pass some variables from the layout?

I had one additional problem. I couldn't test mounted component with transition

<template>
  <transition appear name="slideFromBottom">

I guess it's a limitation of the approach suggested in this issue, right?

This might be a discussion for the vue forum. I haven't tried it myself and I'm not sure his well they'll play together but I'm assuming require-extension-hooks should work with rewire so that would give you a simple way to mock your required modules.

As for transitions; I'm not sure if anyone else has had issues like this? You could always try registering a mock transition component?

Abd now shameless plugging: You can do component dependency injection with vue-inject and quick quick component instances with mocked properties and props with vuenit :)

I'm going to back up @jackmellis and say that it's probably better for a vue forum, but if you wanted to shoot me at email at [email protected] we could discuss it at length.

Keep in mind that my recommendations are coming from general knowledge of component-based architecture (mostly React). This is by no means the only way to do things, just what I've found to work best for me. Essentially the pattern I would recommend is to pass all external state as props. In your case that would mean passing the accepted state as a prop, and emitting an "accepted" event when the user has accepted the policy. The root (entry-point) of your application, save for any other state-management patterns (vuex, etc), should be responsible for getting state from external resources (cookies, local storage, whatever else).

Consider a component tree where application state is passed unidirectionally from the root component to child components. By having your cookie law component materialize its own state from cookies, you effectively have multiple component tree roots. This isn't necessarily _wrong_, in fact in some cases it's what would be recommended (see the concept of container vs presentational components in React/Redux), but I would encourage keeping such patterns to a minimum. The idea is to be able to spin up your components in any environment without having to rely on the existence of any particular API in the environment.

So now you have a generic component which is easy to test, but now you may be asking how to test the entire application. At this point such a task should be handled by integration/e2e testing, which is a whole different beast. You may also be thinking that it's appropriate to have the cookie law component read state from cookies since that is the component's only purpose, but you should also keep in mind that there isn't actually a "cookie law", just a "user tracking law" (please note, I am not a lawyer, this is not legal advice). By constructing your component in a way where the accepted state is passed as a prop, your component more reflects the reality of the law, regardless of where the accepted state is actually stored.

I realize this has been long and rambly, so TL;DR: pass the accepted state as a prop, emit an accepted event when the user accepts. IMHO, realizing that this is not the only, or necessarily the most correct way to do things in all situations, the component should be constructed in such a way that it is not tracking the accepted state, but relying on the application to provide and manage accepted state.

@knpwrs Thank you for such detailed answer :) I agree with your point of view, but in this particular application, I only need some small components that don't form any bigger component. So it feels a little overengineered in this particular case. I definitely take a closer look at this approach when I start writing some bigger components.

@jackmellis Do you have any example code how to use vue-loader with require-extension-hooks? I'm quite new on this topic and I'm not even frontend developer :) Any example would be nice, so I could try to figure out how it works.

Looks promising

@blake-newman using the PR you opened as a base I tried adding this to my current vue app and I'm running into an issue no matter how I have it setup.


Error logs

➜  wvvw.me git:(master) ✗ yarn ava test/*.spec.js
yarn ava v0.23.3
$ "/Users/xo/code/wvvw.me/node_modules/.bin/ava" test/post.spec.js
You are running Vue in development mode.
Make sure to turn on production mode when deploying for production.
See more tips at https://vuejs.org/guide/deployment.html
[Vue warn]: Vue is a constructor and should be called with the `new` keyword 
/Users/xo/code/wvvw.me/node_modules/vue/dist/vue.runtime.common.js:3414
  this._init(options);
       ^

TypeError: this._init is not a function
    at Array.Vue$2 (/Users/xo/code/wvvw.me/node_modules/vue/dist/vue.runtime.common.js:3414:8)
    at hook (/Users/xo/code/wvvw.me/node_modules/require-extension-hooks/hook.js:15:26)
    at Object.require.extensions.(anonymous function) [as .vue] (/Users/xo/code/wvvw.me/node_modules/ava/lib/process-adapter.js:100:4)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.require (module.js:498:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/Users/xo/code/wvvw.me/test/post.spec.js:4:1)
    at Module._compile (module.js:571:32)
    at extensions.(anonymous function) (/Users/xo/code/wvvw.me/node_modules/require-precompiled/index.js:13:11)
    at Object.require.extensions.(anonymous function) [as .js] (/Users/xo/code/wvvw.me/node_modules/ava/lib/process-adapter.js:100:4)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.require (module.js:498:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/Users/xo/code/wvvw.me/node_modules/ava/lib/test-worker.js:49:1)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:423:7)
    at startup (bootstrap_node.js:147:9)
    at bootstrap_node.js:538:3

  1 exception

  ✖ test/post.spec.js exited with a non-zero exit code: 1

error Command failed with exit code 1.


post.spec.js

import Vue from 'vue';
import test from 'ava';

import Post from '../src/components/Post.vue';

test(t => {
    const N = Vue.extend(Post);
    const vm = new N({
        propsData: {
            post: {
                title: 'Test Post',
                content: 'This is a test post.',
                tags: ['test', 'post', 'example']
            }
        }
    });
    Vue.nextTick(() => {
        t.is(vm.$el.textContent, 'This is a test post.');
    });
});


./src/components/Post.vue

<template>
    <div v-bind:class="['post', 'styled', (post.published ? '' : 'unpublished')]">
        <template v-if="post.published || (!post.published && user)">
            <h1 class="title">{{post.title}}</h1>
            <div class="content" v-html="marked(post.content)"></div>
            <span class="meta">
                <a v-bind:href="post.permalink">{{new Date(post.date).toDateString()}}</a>
                <span class="owner">by <a v-bind:href="'/user/' + owner.id">{{owner.name}}</a></span>
            </span>
        </template>
        <template v-else>á Žá Ž
            <p class="content">This post hasn't been published yet.</p>
        </template>á Žá Žá Ž
    </div>
</template>

<script>
import Vue from 'vue';
export default Vue.extend({
    name: 'post',
    props: {
        post: {
            type: Object
        },
        user: {
            type: Object
        }
    },
    computed: {
        owner() {
            var vm = this;
            var isAnonymous = 'author' in vm.post;
            return {
                name: isAnonymous ? vm.post.author.username : 'anonymous',
                id: isAnonymous ? vm.post.author._id : 'anonymous'
            };
        }
    }
})
</script>

Here's what I got working. One thing to keep in mind is you can't use Vue.extend(Component); in your testing if you imported Vue anywhere in any of your components as it'll throw an error.

https://github.com/OmgImAlexis/wvvw.me/compare/7d6371062468e6ba617a69dbf44190b93f6ab963...master

@OmgImAlexis I've published require-extension-hooks-vue 0.2.2 which may or may not fix your issue...🤞

Since most people use vue-router or something similar would you be up for adding details on how to use it to https://github.com/avajs/ava/pull/1361? I noticed I can test components fine but trying to test my app.vue spits out errors because my routes field is missing.

The code below allows vue-router to work properly.

import Vue from 'vue';
import VueRouter from 'vue-router';
import test from 'ava';
import App from '../src/app.vue';
import HomeComponent from '../src/components/home.vue';

test.only('App should render', t => {
    Vue.use(VueRouter);
    const router = new VueRouter({
        routes: [{
            name: 'home',
            path: '/',
            component: HomeComponent
        }]
    });
    const vm = new Vue({
        router,
        render: h => h(App)
    }).$mount();
    const tree = {$el: vm.$el.outerHTML};
    t.snapshot(tree);
});

Edit: Although this is working nyc isn't returning anything in the coverage report.

➜  vue git:(feature/add-default-vue-route) ✗ yarn coverage
yarn coverage v0.23.4
$ nyc ava 

  1 failed

  App should render
  /Users/xo/code/Medusa/vue/test/app.spec.js:21

   20:     const tree = {$el: vm.$el.outerHTML};
   21:     t.snapshot(tree);                    
   22: });                                      

  Did not match snapshot

  Difference:

      Object {
    -   "$el": "<div id="app"><div>Home</div></div>",
    +   "$el": "<div id="app"><div>
    +         This is the homepage.
    +     </div></div>",
      }

  Test.fn (test/app.spec.js:21:7)
  processEmit [as emit] (node_modules/nyc/node_modules/signal-exit/index.js:155:32)
  processEmit [as emit] (node_modules/nyc/node_modules/signal-exit/index.js:155:32)

----------|----------|----------|----------|----------|----------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files |  Unknown |  Unknown |  Unknown |  Unknown |                |
----------|----------|----------|----------|----------|----------------|
error Command failed with exit code 1.
Was this page helpful?
0 / 5 - 0 ratings