@djirdehh

A Full Day of Vue.js

06. Mixins, Filters, & Testing

Mixins allow us to neatly reuse shared functionality between components.

 Mixins

 Mixins

<template>
  <h1>This is Component #1</h1>
</template>

<script>
export default {
  name: "Component1",
  created() {
    console.log(`${this.options.name} has been created!`); 
  },
  ...
};
</script>

<style lang="css">
<!-- Styles-->
</style>
<template>
  <h1>This is Component #2</h1>
</template>

<script>
export default {
  name: "Component2",
  created() {
    console.log(`${this.options.name} has been created!`); 
  },
  ...
};
</script>

<style lang="css">
<!-- Styles-->
</style>

 Mixins

const createdMixin = {
  created() {
    console.log(`${this.options.name} has been created!`);
  }
};

 Mixins

<template>
  ...
</template>

<script>
export default {
  name: "ComponentName",
  mixins: [nameOfMixin]
};
</script>

 Mixins

<template>
  <h1>This is Component #1</h1>
</template>

<script>
import {createdMixin} from './mixins';

export default {
  name: "Component1",
  mixins: [createdMixin]
  ...
};
</script>

<style lang="css">
<!-- Styles-->
</style>
<template>
  <h1>This is Component #2</h1>
</template>

<script>
import {createdMixin} from './mixins';

export default {
  name: "Component2",
  mixins: [createdMixin]
  ...
};
</script>

<style lang="css">
<!-- Styles-->
</style>

 Mixins

export const helloWorldMixin = {
  data() {
    return {
      reversedGreeting: '!dlrow olleH',
    }
  },
  computed: {
    getGreeting() {
      return this.reversedGreeting.split("").reverse().join("");
    }
  }
}

export const createdMixin = {
  created() {
    console.log(`${this.options.name} has been created!`);
  }
}
mixins.js

 Mixins

<template>
  <h1>This is Component #1</h1>
  <p>{{getGreeting}}</p>
</template>

<script>
import {helloWorldMixin, createdMixin} from './mixins';

export default {
  name: "Component1",
  mixins: [helloWorldMixin, createdMixin]
  ...
};
</script>

<style lang="css">
<!-- Styles-->
</style>
<template>
  <h1>This is Component #2</h1>
  <p>{{getGreeting}}</p>
</template>

<script>
import {helloWorldMixin, createdMixin} from './mixins';

export default {
  name: "Component2",
  mixins: [helloWorldMixin, createdMixin]
  ...
};
</script>

<style lang="css">
<!-- Styles-->
</style>
Component1.vue
Component2.vue

 Mixins

What if the mixin and component have overlapping information?

 Mixins

const greetingMixin = {
  data() {
    return {
      greeting: 'Hello World!',
    }
  }
}

Vue.component('greeting-component', {
  mixins: [greetingMixin],
  data() {
    return {
      greeting: 'Good Morning!',
    }
  },
  created() {
    console.log(this.greeting); // 'Good Morning!'
  }
});

Component data takes priority

 Mixins

const greetingMixin = {
  methods: {
    greetingMethod() {
      console.log('Hello World!');
    }
  },
  components: {
    'greeting-component': ComponentA
  }
}

Vue.component('greeting-component', {
  mixins: [greetingMixin],
  methods: {
    greetingMethod() {
      console.log('Good Morning!');
    }
  },
  components: {
    'greeting-component': ComponentB
  }
});

Component options takes priority as well

 Mixins

const greetingMixin = {
  created() {
    console.log('Hello World!');
  }
}

Vue.component('greeting-component', {
  mixins: [greetingMixin],
  created() {
    console.log('Good Morning!');
  }
});

// 'Hello World!'
// 'Good Morning!'

Lifecycle hooks run in mixins first, then in components

 Mixins

Apart from lifecycle hooks; I would strongly try to avoid replicating mixin data/properties with that of components.

 Mixins

We can also define mixins globally!

Vue.mixin({ });

 Mixins

"Use global mixins sparsely and carefully, because it affects every single Vue instance created, including third party components."

Filters allow us to return formatted versions of data without making changes to the data source.

Filters

Filters are used in mustache interpolations and v-bind expressions.

Filters

{{ property | nameOfFilter }}
v-bind:src="property | nameOfFilter"

Filters

<template>
  ...
</template>

<script>
export default {
  name: "ComponentName",
  filters: {
    nameOfFilter(value) {
      return ...
    }
  }
};
</script>

Filters

filters: {
  nameOfFilter(value) {
    return ...
  }
}

Always the data value that is being filtered as the first argument.

Filters

<template>
  <div>
    {{ greeting }}
    {{ greeting | reverseString }}
  </div>
</template>

<script>
export default {
  name: "GreetingComponent",
  data() {
    return {
      greeting: 'Hello World!';
    }
  },
  filters: {
    reverseString(originalValue) {
      return originalValue.split("").reverse().join("");
    },
  }
};
</script>

<style lang="css">
<!-- Styles-->
</style>

Filters

{{ property | nameOfFilter(arg1, arg2) }}
filters: {
  nameOfFilter(value, arg1, arg2) {
    return ...
  }
}

Filters

{{ property | filter1 | filter 2 | filter 3 }}

property value is first arg

returned value of prev filter is first arg

returned value of prev filter is first arg

Filters

We can also define filters globally!

Vue.filter('name-of-filter', function() {});

Testing

Testing can help reveal bugs before they appear, instill confidence in your web application, and make it easy to onboard new developers on an existing codebase.

Testing - Basics

Unit Testing

Tests are made to parts of an application in isolation.

End to End Testing

Tests focus on whether an application has been built appropriately from start to finish.

Two main ways to test web applications

2

1

Many unit test environments/suites exist

  • Mocha: A JavaScript testing framework. Allows us to specify our test suites with describe and it blocks.

  • Chai: A testing assertion library. Provides interfaces for us to create assertions for our tests (should..., expect..., assert...).

  • Jest: A JavaScript testing framework that comes with an assertion library, mocking support, snapshot testing, etc. with minimal to no configuration/set-up.

and many more....

Testing - Basics

new Calculator()

2 + 2 | we expect 4

5 - 3 | we expect 2

4 x 4 | we expect 16

Testing - Basics

describe('Calculator', () => {
  it('sums 2 and 2 to 4', () => {
    ...
  });

  it('subtracts 5 and 3 to 2', () => {
    ...
  });
});

describe functions segment each logical unit of tests.

it functions test each expectation we'd want to assert.

Testing - Basics

describe('Calculator', () => {
  describe('sum()', () => {
    it('sums 2 and 2 to 4', () => {
      ...
    });
  });
  
  describe('subtract()', () => {
    it('subtracts 5 and 3 to 2', () => {
      ...
    });
  });
});

Testing - Basics

describe('Calculator', () => {
  it('sums 2 and 2 to 4', () => {
    const calc = new Calculator();
    expect(calc.sum(2,2)).toEqual(4);
  });

  it('subtracts 5 and 3 to 2', () => {
    const calc = new Calculator();
    expect(calc.subtract(5,3)).toEqual(2);
  });
});

Testing - Basics

expect(...).toEqual(...);

Unique functions from the testing framework/assertion library (e.g.  Jest) being used.

Testing - Basics

Testing - Basics

The Arrange, Act, Assert (AAA) pattern

describe('Calculator', () => {
  it('sums 2 and 2 to 4', () => {
    const calc = new Calculator();

    const sumResult = calc.sum(2,2);

    expect(sumResult).toEqual(4);
  });
});

Arrange

Act

Assert

Testing - Basics

We'll now look to test a basic Vue component.

list of todos

input to create new todo

element to remove certain todo

1

2

3

Simple version of TodoMVC

Testing - Vue Component

Testing - Vue Component

<template>
  <div id="app">
    <section class="todoapp">
      <header class="header">
        <h1 class="title">todos</h1>
        <input class="new-todo"
          placeholder="What needs to be done?"
          v-model="newTodo"
          @keyup.enter="addTodo">
      </header>
      <section class="main">
        <ul class="todo-list">
          <li class="todo"
             v-for="(todo, index) in todos"
             :key="index">
            <div class="view">
              <label>{{ todo }}</label>
              <button class="destroy" @click="removeTodo(todo)"></button>
            </div>
          </li>
        </ul>
      </section>
    </section>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      todos: [],
      newTodo: ''
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo !== '') {
        this.todos.push(this.newTodo);
        this.newTodo = '';
      }
    },
    removeTodo(todo) {
      this.todos.splice(this.todos.indexOf(todo), 1);
    }
  }
};
</script>
App.vue
describe('App.vue', () => {
  it('should render correct initial content', () => {
    ...
  });
});

Testing - Vue Component

Testing - Vue Component

import App from "./App";

describe("App", () => {
  it("should render correct initial content", () => {
    // ...
  });
});
App.spec.js

Testing - Vue Component

To assert the HTML content of our component - we need to have our component mounted().

Before we have our component  mounted() , we can create the component constructor.

Testing - Vue Component

import Vue from "vue";
import App from "./App";

describe("App", () => {
  it("should render correct initial content", () => {
    const Constructor = Vue.extend(App);
  });
});
App.spec.js

Testing - Vue Component

import Vue from "vue";
import App from "./App";

describe("App", () => {
  it("should render correct initial content", () => {
    const Constructor = Vue.extend(App);
    const vm = new Constructor().$mount();
  });
});
App.spec.js

Testing - Vue Component

import Vue from "vue";
import App from "./App";

describe("App", () => {
  it("should render correct initial content", () => {
    const Constructor = Vue.extend(App);
    const vm = new Constructor().$mount();

    expect(vm.$el.querySelector(".title").textContent).toBe("todos");
    expect(vm.$el.querySelector(".new-todo").placeholder).toBe(
      "What needs to be done?"
    );
  });
});
App.spec.js

Testing - Vue Component

describe('App.vue', () => {
  it('should render correct initial content', () => {
    ...
  });

  it('should set correct default data', () => {
    ...
  });
});

Testing - Vue Component

todos: [ ] ,
newTodo: ' '

import Vue from "vue";
import App from "./App";

describe("App", () => {
  it("should render correct initial content", () => {
    // ...
  });

  it("should set correct default data", () => {
    const initialData = App.data();
  });
});
App.spec.js

Testing - Vue Component

import Vue from "vue";
import App from "./App";

describe("App", () => {
  it("should render correct initial content", () => {
    // ...
  });

  it("should set correct default data", () => {
    const initialData = App.data();

    expect(initialData.todos).toEqual([]);
    expect(initialData.newTodo).toEqual("");
  });
});
App.spec.js

Testing - Vue Component

vue-test-utils?

Testing - Vue Component

Testing - vue-test-utils

vue-test-utils is a utility library that provides useful methods that help make testing easier.

Testing - vue-test-utils

We're able to mount a component by simply doing:

const wrapper = mount(Component);
import { mount } from "@vue/test-utils";

 

Testing - vue-test-utils

const vm = wrapper.vm;
const html = wrapper.html();
const button = wrapper.find('button');
  • Access the component instance:

  • Retrieve component's html:

  • Find element (wrapper):

  • Get Props:

const propValue = wrapper.props('propKey');
  • Trigger Event:

wrapper.trigger('click');
  • Set Methods:

wrapper.setMethods({});

and a lot more...

Testing - vue-test-utils

Testing - vue-test-utils

.html()

.setProps()

.setData()

.setMethods()

.vm

component

wrapper

Testing - vue-test-utils

mount()

Creates a Wrapper that contains the mounted Vue component.

shallowMount()

Creates a Wrapper that contains the mounted Vue component, but with stubbed child components.

import { mount, shallowMount } from "@vue/test-utils";
import { mount } from "@vue/test-utils";

Testing - vue-test-utils

Tests the component in isolation.

shallowMount()

It's faster.

great to use when we want to focus on a single component...

Testing - vue-test-utils

import Vue from "vue";
import App from "./App";

describe("App.vue", () => {
  it("should render correct initial content", () => {
    const Constructor = Vue.extend(App);
    const vm = new Constructor().$mount();

    expect(vm.$el.querySelector(".title").textContent).toBe("todos");
    expect(vm.$el.querySelector(".new-todo").placeholder).toBe(
      "What needs to be done?"
    );
  });

  it("should set correct default data", () => {
    const initialData = App.data();

    expect(initialData.todos).toEqual([]);
    expect(initialData.newTodo).toEqual("");
  });
});
App.spec.js

Testing - vue-test-utils

npm install @vue/test-utils --save-dev

Testing - vue-test-utils

import App from "./App";
import { shallowMount } from "@vue/test-utils";

describe("App.vue", () => {
  it("should render correct initial content", () => {
    ...
  });

  it("should set correct default data", () => {
    ...
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";
import { shallowMount } from "@vue/test-utils";

describe("App.vue", () => {
  let wrapper = shallowMount(App);

  it("should render correct initial content", () => {
    ...
  });

  it("should set correct default data", () => {
    ...
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";
import { shallowMount } from "@vue/test-utils";

describe("App.vue", () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(App);
  });

  it("should render correct initial content", () => {
    ...
  });

  it("should set correct default data", () => {
    ...
  });
});

Testing - vue-test-utils

import App from "./App";
import { shallowMount } from "@vue/test-utils";

describe("App.vue", () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(App);
  });

  it("should render correct initial content", () => {
    expect(wrapper.find(".title").text()).toBe("todos");
    expect(wrapper.find(".new-todo").element.placeholder).toBe(
      "What needs to be done?"
    );
  });

  it("should set correct default data", () => {
    ...
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";
import { shallowMount } from "@vue/test-utils";

describe("App.vue", () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallowMount(App);
  });

  it("should render correct initial content", () => {
    expect(wrapper.find(".title").text()).toBe("todos");
    expect(wrapper.find(".new-todo").element.placeholder).toBe(
      "What needs to be done?"
    );
  });

  it("should set correct default data", () => {
    expect(wrapper.vm.todos).toEqual([]);
    expect(wrapper.vm.newTodo).toEqual("");
  });
});
App.spec.js

Testing - vue-test-utils

input to create new todo

1

Testing - vue-test-utils

describe("App.vue", () => {
  it("should render correct initial content", () => {
    ...
  });

  it("should set correct default data", () => {
    ...
  });

 

  describe("the user populates the text input field", () => {
    ...
  });
});

Testing - vue-test-utils

describe("App.vue", () => {
  ...

 

  describe("the user populates the text input field", () => {
    it("should update the 'newTodo' data property", () => {
      ...
    });
  });
});

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field", () => {
    it("should update the 'newTodo' data property", () => {
      const inputField = wrapper.find(".new-todo");
    });
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field", () => {
    it("should update the 'newTodo' data property", () => {
      const inputField = wrapper.find(".new-todo");
      
      inputField.element.value = "New Todo";
      inputField.trigger("input");
    });
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field", () => {
    it("should update the 'newTodo' data property", () => {
      const inputField = wrapper.find(".new-todo");
      
      inputField.element.value = "New Todo";
      inputField.trigger("input");

      expect(wrapper.vm.newTodo).toEqual("New Todo");
    });
  });
});
App.spec.js

Testing - vue-test-utils

list of todos

2

Testing - vue-test-utils

describe("App.vue", () => {
  ...

 

  describe("the user populates the text input field and presses Enter", () => {
    it("should add a new todo to the 'todos' array", () => {
      ...
    });
  });
});

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field and presses Enter", () => {
    it("should add a new todo to the 'todos' array", () => {
      const inputField = wrapper.find(".new-todo");
    });
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field and presses Enter", () => {
    it("should add a new todo to the 'todos' array", () => {
      const inputField = wrapper.find(".new-todo");
      
      inputField.element.value = "New Todo";
      inputField.trigger("input");
      inputField.trigger("keyup.enter");
    });
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field and presses Enter", () => {
    it("should add a new todo to the 'todos' array", () => {
      const inputField = wrapper.find(".new-todo");
      
      inputField.element.value = "New Todo";
      inputField.trigger("input");
      inputField.trigger("keyup.enter");
      
      expect(wrapper.vm.todos).toEqual(["New Todo"]);
    });
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field", () => {
    it("should update the 'newTodo' data property", () => {
      const inputField = wrapper.find(".new-todo");

      inputField.element.value = "New Todo";
      inputField.trigger("input");

      expect(wrapper.vm.newTodo).toEqual("New Todo");
    });
  });

  describe("the user populates the text input field and presses Enter", () => {
    it("should add a new todo to the 'todos' array", () => {
      const inputField = wrapper.find(".new-todo");

      inputField.element.value = "New Todo";
      inputField.trigger("input");
      inputField.trigger("keyup.enter");

      expect(wrapper.vm.todos).toEqual(["New Todo"]);
    });
  });
});
App.spec.js

Testing - vue-test-utils

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field", () => {
    let inputField;

    beforeEach(() => {
      inputField = wrapper.find(".new-todo");
      inputField.element.value = "New Todo";
      inputField.trigger("input");
    });

    it("should update the 'newTodo' data property", () => {
      expect(wrapper.vm.newTodo).toEqual("New Todo");
    });

    describe("and presses Enter", () => {
      it("should add a new todo to the 'todos' array", () => {
        inputField.trigger("keyup.enter");
        expect(wrapper.vm.todos).toEqual(["New Todo"]);
      });
    });
  });
});
App.spec.js

Testing - vue-test-utils

 Mixins, Filters, & Testing - Chapter Summary!

  • Mixins allow us to neatly reuse functionality between components.

  • const nameofMixin = {} : mixins: [nameOfMixin]

  • Component data/properties take priority of identical Mixin information. Mixin lifecycle hooks run first.

  • Can define Mixins globally with Vue.mixin({}) - should be used sparingly!

  • Filters helps return formatted versions of data without making changes to the data source.

  • {{ property | nameOfFilter }} ------- filters: {nameOfFilter() {return ...}}

  • Used in mustache interpolations and v-bind expressions  - {{ property | nameOfFilter }}

  • Data value that is filtered is always the first argument of the filter function.

  • vue-test-utils is Vue's official testing utility library.

  • Mount and shallow mount components with mount() and shallowMount() functions.

  • Mounted wrapper contains useful methods to help make testing easier:

wrapper.html();

wrapper.find();

wrapper.trigger();

wrapper.setData();

wrapper.setProps();

wrapper.setMethods();

 Mixins, Filters, & Testing - Exercise 💪!

 Mixins 💪

 Mixins, Filters, & Testing - Exercise 💪!

 Mixins 💪

 Mixins, Filters, & Testing - Exercise 💪!

 Filters 💪

 Mixins, Filters, & Testing - Exercise 💪!

 Filters 💪

To modify the price

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 0
}).format(number);

To modify the circulating supply

new Intl.NumberFormat().format(number);

 Mixins, Filters, & Testing - Exercise 💪!

Testing 💪

element to remove certain todo

3

button to remove all todos

4

 Mixins, Filters, & Testing - Exercise 💪!

<template>
  <div id="app">
    <section class="todoapp">
      ...
          <li class="todo"
             v-for="(todo, key) in todos"
              :key="key">
            <div class="view">
              <label>{{ todo }}</label>
              <button class="destroy" @click="removeTodo(todo)"></button>
            </div>
          </li>
        ...
    </section>
    <footer class="footer">
      <button @click="removeAllTodos" class="clear-completed">
        Remove all
      </button>
    </footer>
  </div>
</template>

<script>
export default {
  ...
  methods: {
    ...,
    removeTodo(todo) {
      this.todos.splice(this.todos.indexOf(todo), 1);
    },
    removeAllTodos() {
      this.todos = [];
    }
  }
};
</script>
App.vue

 Mixins, Filters, & Testing - Exercise 💪!

import App from "./App";

describe("App", () => {
  // ...

  describe("the user populates the text input field", () => {
    // ...

    it("should update the 'newTodo' data property", () => {
      // ...
    });

    describe("and presses Enter", () => {
      it("should add a new todo to the 'todos' array", () => {
        // ...
      });
    });

    describe("and presses Enter and then removes the todo item", () => {
      it("should have the new todo removed from the 'todos' array", () => {
        // Trigger keyup.enter on inputField to submit todo
        // Then find the Remove icon and trigger a click
      });
    });
  });

  describe("the user clicks the 'Remove all' label", () => {
    it("should remove all todos from the 'todos' data property", () => {
      // Quickly update the todos array with wrapper.setData({})
      // Find the Remove all button and trigger a click
    });
  });
});
App.spec.js

 Mixins, Filters, & Testing - Exercise 💪!

Testing

Mixins

Filters

Solution 👀

Solution 👀

Solution 👀

A Full Day of Vue.js Workshop - 06 Mixins, Filters, and Testing

By djirdehh

A Full Day of Vue.js Workshop - 06 Mixins, Filters, and Testing

  • 475