挂载组件与选项

时游大约 4 分钟

挂载组件与选项

​ @vue/test/utils 会模拟必要的输入(props、注入跟用户事件)以及对输出(渲染结果、触发的自定义事件)的断言来测试 Vue 组件。被挂在的组件会返回一个包裹器,包裹器会暴露很多封装、遍历和查询其内部的 Vue 组件实例的方法。 通过 mount 方法来创建包裹器:

mount 方法创建包裹器

counter.vue

<template>
	<div>
		<span class="count">{{ count }}</span>
		<button @click="increment">Increment</button>
	</div>
</template>

<script>
	export default {
		data() {
			return {
				count: 0
			};
		},
		methods: {
			increment() {
				this.count++;
			}
		}
	};
</script>

counter.test.js

import Vue from "vue";
import { mount } from "@vue/test-utils";
import CounterComponent from "@/src/views/counter.vue";

describe("测试计数组件", () => {
	// 创建包裹器
	const wrapper = mount(CounterComponent);
	// 通过wrapper.vm访问Vue实例
	const vm = wrapper.vm;

	it("打印页面结构", () => {
		console.log(wrapper.html());
		expect(wrapper.html()).toContain('<span class="count">0</span>');
	});

	it("检查元素是否存在", () => {
		expect(wrapper.find("button").exists()).toBe(true);
	});

	/* 模拟用户交互 */
	it("模拟用户点击", () => {
		// 先判断当前值
		expect(vm.count).toBe(0);
		// 获取button元素
		const button = wrapper.find("button");
		button.trigger("click"); // 模拟点击
		// 校验当前值
		expect(vm.count).toBe(1);
	});

	/* 异步操作 */
	it("异步操作", async () => {
		expect(wrapper.text()).toContain("1");
		const button = wrapper.find("button");
		await button.trigger("click");
		expect(wrapper.text()).toContain("2");
	});

	// 注意:使用nextTick时,其内部的错误可能不会被捕获,因为内部使用了promise
	it("该用例会超时", done => {
		// "Exceeded timeout of 5000 ms for a test while waiting for `done()` to be called.
		Vue.nextTick(() => {
			expect(true).toBe(false); // 测试超时
			done();
		});
	});

	// 接下来的三项测试都会如预期工作
	it("will catch the error using done", done => {
		// 方法一:将Vue全局错误处理器设置为done
		Vue.config.errorHandler = done;
		Vue.nextTick(() => {
			expect(true).toBe(false); // 测试不通过
			done();
		});
	});
	it("will catch the error using a promise", () => {
		// 方法二:将其作为返回
		return Vue.nextTick().then(function () {
			expect(true).toBe(false); // 测试不通过
		});
	});

	it("will catch the error using async/await", async () => {
		// 方法三:使用async await
		await Vue.nextTick();
		expect(true).toBe(false); // 测试不通过
	});

	it("异步链式结构:有错误识别不出来", () => {
		const button = wrapper.find("button");
		button.trigger("click").then(() => {
			expect(vm.count).toBe(66);
			button.trigger("click").then(() => {
				expect(vm.count).toBe(2);
			});
		});
	});
});

shallowMount 浅渲染

浅渲染,只加载当前组件,而不加载其子组件,其余用法与 mount 一致

import { shallowMount } from "@vue/test-utils";
import CounterComponent from "@/src/views/counter.vue";

describe("浅渲染", () => {
	const wrapper = shallowMount(CounterComponent);
	const vm = wrapper.vm;
	it("浅渲染内容获取", () => {
		expect(vm.count).toBe(2); // 测试不通过,真实值为0
		console.log(vm);
	});
});

挂载选项(options)

data:会并入现有的 data 中

const Component = {
	template: `
    <div>
      <span id="foo">{{ foo }}</span>
      <span id="bar">{{ bar }}</span>
    </div>
  `,

	data() {
		return {
			foo: "foo",
			bar: "bar"
		};
	}
};

const wrapper = mount(Component, {
	data() {
		return {
			bar: "my-override"
		};
	}
});

wrapper.find("#foo").text(); // 'foo'
wrapper.find("#bar").text(); // 'my-override'

slots:为组件提供一个 slot 内容的对象。该对象中的键名就是相应的 slot 名,键值可以是一个组件、一个组件数组、一个字符串模板或文本。

import Foo from "./Foo.vue";
import MyComponent from "./MyComponent.vue";

const bazComponent = {
	name: "baz-component",
	template: "<p>baz</p>"
};

const yourComponent = {
	props: {
		foo: {
			type: String,
			required: true
		}
	},
	render(h) {
		return h("p", this.foo);
	}
};

const wrapper = shallowMount(Component, {
	slots: {
		default: [Foo, "<my-component />", "text"],
		fooBar: Foo, // 将会匹配 `<slot name="FooBar" />`.
		foo: "<div />",
		bar: "bar",
		baz: bazComponent,
		qux: "<my-component />",
		quux: '<your-component foo="lorem"/><your-component :foo="yourProperty"/>'
	},
	stubs: {
		// 用来注册自定义组件
		"my-component": MyComponent,
		"your-component": yourComponent
	},
	mocks: {
		// 用来向渲染上下文添加 property
		yourProperty: "ipsum"
	}
});

expect(wrapper.find("div")).toBe(true);

mocks:传入 mock 数据

const $route = { path: "http://www.example-path.com" };
const wrapper = shallowMount(Component, {
	mocks: {
		$route
	}
});
expect(wrapper.vm.$route.path).toBe($route.path);

localVue:通过 createLocalVueopen in new window 创建的一个 Vue 的本地拷贝,用于挂载该组件的时候。在这份拷贝上安装插件可以防止原始的 Vue 被污染

import { createLocalVue, mount } from "@vue/test-utils";
import VueRouter from "vue-router";
import Foo from "./Foo.vue";

const localVue = createLocalVue();
localVue.use(VueRouter);

const routes = [{ path: "/foo", component: Foo }];

const router = new VueRouter({
	routes
});

const wrapper = mount(Component, {
	localVue,
	router
});
expect(wrapper.vm.$route).toBeInstanceOf(Object);

attachTo:指定一个 HTMLElement 或定位到一个 HTML 元素的 CSS 选择器字符串,组件将会被完全挂载到文档中的这个元素。

const Component = {
	template: "<div>ABC</div>"
};
let wrapper = mount(Component, {
	attachTo: "#root"
});
expect(wrapper.vm.$el.parentNode).to.not.be.null;
wrapper.destroy();

wrapper = mount(Component, {
	attachTo: document.getElementById("root")
});
expect(wrapper.vm.$el.parentNode).to.not.be.null;
wrapper.destroy();

attrs:设置组件实例的 $attrs 对象

propsData:设置 props 数据

listeners:设置组件实例的 $listeners 对象。

const Component = {
	template: "<button v-on:click=\"$emit('click')\"></button>"
};
const onClick = jest.fn();
const wrapper = mount(Component, {
	listeners: {
		click: onClick
	}
});

wrapper.trigger("click");
expect(onClick).toHaveBeenCalled();

parentComponent:用来作为被挂载组件的父级组件

import Foo from "./Foo.vue";

const wrapper = shallowMount(Component, {
	parentComponent: Foo
});
expect(wrapper.vm.$parent.$options.name).toBe("foo");

provide:为组件传递用于注入的属性。

const Component = {
	inject: ["foo"],
	template: "<div>{{this.foo()}}</div>"
};

const wrapper = shallowMount(Component, {
	provide: {
		foo() {
			return "fooValue";
		}
	}
});

expect(wrapper.text()).toBe("fooValue");

选择器

CSS 选择器

  1. 标签选择器
  2. id 选择器
  3. 类选择器
  4. 特性选择器:div.className

查找选项对象

使用 wrapper.find 进行查找

const button = wrapper.find("button.isChecked"); // 查找class为isChecked的button
button.trigger("click");

生命周期钩子

​ 在使用 mount 或 shallowMount 时,可以期望该组件响应 Vue 所有生命周期事件。但是 beforeDestroy 和 destroy 必须使用**wrapper.destroy()**才会触发。此外每个测试结束时并不会自动销毁,而是用户决定是否清理在结束前继续运行的任务,例如 setInterval 或 setTimeout.

beforeAll:在所有测试用例执行前运行

beforeEach:在每个测试用例执行前执行一次

afterEach:在每个测试用例执行后执行一次

afterAll:在所有测试用例结束后运行

Loading...