如何在JavaScript框架之间共享具有相同或相似组件的JavaScript框架之间的测试图表测试,并在故事书和单元测试中使用它们。
tl; dr
可以在具有相似HTML结构和/或类似UX的JavaScript组件之间共享测试。 Storybook的play
测试(交互测试)和vitest单元测试都可以使用相同的共享测试。
为什么在JavaScript框架之间共享测试?
- 您有一个或多个共享UX的应用程序,但是使用不同的JS框架构建
- 将应用程序从一个JS框架转换为另一个JS框架
- 您的公司维护一个设计系统,该设计系统具有由不同的JS框架构建的多个组件库,但相同的UX
共享测试以及如何使用它们
保持D.R.Y.所以...挖掘part 1 - "What are shared tests",part 2 - "Sharing tests between components"和part 3 - "Sharing tests between Vitest and Storybook",以低下共享测试。
为什么这起作用?你是向导吗?
当我一直在撰写本系列时,我意识到它是对Testing Library魔术的颂歌,该魔术是为了测试UIS作为用户会体验应用程序的。该原理意味着Testing Library
直到已经渲染后才介绍给您的UI ...这意味着Testing Library
就像用户一样……只是它可以按预期工作。
Testing Library
是这里的真正MVP。
这次我们正在测试什么?
本文显示了一组共享测试,用于测试在多个JavaScript框架上重写的计数器组件。我们将使用Astro Web Framework示例中发现的组件:koude4。这些以预先反应,反应,苗条,固体J和VUE编写。这个Astro Microfrontend示例很棒,因为它演示了Astro Web框架如何同时在同一页面上运行多个JavaScript框架。
有趣的事实!这是我用来演示故事书的代码库,在DevOps for Multi-Framework Composition Storybooks series中介绍了多个框架。
什么是计数器组件?
计数器组件是一个简单的组件,它显示一个数字并具有两个按钮:一个可以增加数字,一个可以减少数字。 每个框架版本具有完全相同的UX和HTML结构,由于框架的语法而有轻微的变化。
html用于preactcounter.tsx:
<div class="counter">
<button onClick={subtract}>-</button>
<pre>{count}</pre>
<button onClick={add}>+</button>
</div>
HTML用于ReactCounter.tsx:
<div className="counter">
<button onClick={subtract}>-</button>
<pre>{count}</pre>
<button onClick={add}>+</button>
</div>
Sveltecounter.Svelte:html:
<div class="counter">
<button on:click={subtract}>-</button>
<pre>{count}</pre>
<button on:click={add}>+</button>
</div>
vuecounter.vue的html:
<div class="counter">
<button @click="subtract()">-</button>
<pre>{{ count }}</pre>
<button @click="add()">+</button>
</div>
所有计数器组件的HTML渲染html都是相同的
所有组件输出相同的html,这是伏都教的,这使得这是可能的。同样的html?相同的测试。
<div class="counter">
<button>-</button>
<pre>0</pre>
<button>+</button>
</div>
共享测试
1.获取元素
第一步?查询我们要测试的元素的DOM。
我们需要:
-
+
(plus
)按钮 -
-
(minus
)按钮 - 包含计数的
pre
元素 - 容器,这是一个带有一类
counter
的div
11
使用Testing Library
的查询,我们将通过解析渲染的HTML来创建一个对象。在这种方法中,canvasElement
是框架呈现的实时DOM。
import { within } from '@storybook/testing-library';
/**
* Extract elements from an HTMLElement
*/
export const getElements = async (canvasElement) => {
// `within` adds the testing-library query methods
const screen = within(canvasElement);
return {
screen,
canvasElement,
// `querySelector` used here to find the generic `div` container
container: await canvasElement.querySelector('.counter'),
// `queryByRole` finds each button, using `name` to search the text
minus: await screen.queryByRole('button', { name: /-/i }),
// using `queryBy` instead of `getBy` to avoid errors in `getElements`
plus: await screen.queryByRole('button', { name: /\+/i }),
// `querySelector` again as `pre` has no role and contains variable content
count: await canvasElement.querySelector('pre'),
};
}
这返回带有我们元素的对象:
{
screen: {object with testing-library query methods},
canvasElement: <the-initial-html-element,unchanged>,
container: <div class="counter">...</div>/{and query methods},
minus: <button>-</button>/{and query methods},
plus: <button>+</button>/{and query methods},
count: <pre>0</pre>/{and query methods},
}
2.测试初始渲染元素
计数器是一个没有道具的简单组件,因此此方法仅确保存在元素,并且计数器在0
开始。 ensureElements
应在交互测试之前调用,以确保组件的初始状态是测试的。
elements
arg是从getElements
返回的对象。
/**
* Ensure elements are present and have the correct attributes/content
*/
export const ensureElements = async (elements) => {
const { minus, plus, count } = elements;
// `.toBeTruthy` ensures the element exists
await expect(minus).toBeTruthy();
await expect(plus).toBeTruthy();
await expect(count).toBeTruthy();
// ensures the count starts at zero
await expect(count).toHaveTextContent('0');
}
3.测试键盘导航
对于仅键盘用户,我们需要确保按钮可集中并按预期顺序进行。我们与交互分开测试导航,以使测试方法一次集中在一种类型的用户体验上。
组件只有两个可聚焦的元素 - 是按钮。
/**
* Test keyboard interaction
*/
export const keyboardNavigation = async (elements) => {
const { minus, plus, container } = elements;
// tab within the container
await userEvent.tab({ focusTrap: container });
await expect(minus).toHaveFocus();
// `pre` is the next element, but it's not focusable
await userEvent.tab({ focusTrap: container });
await expect(plus).toHaveFocus();
}
4.测试键盘交互
为了测试按钮的相互作用,我们将在pre
中的数字添加或减去。我们无法确保将使用测试方法的顺序,这意味着elements.count
可能不是0
。此测试方法将从获得elements.count
中的任何数字开始,并使用该数字来测试预期的加法或减法结果。
/**
* Test keyboard interactions
*/
export const keyboardInteraction = async (elements) => {
const { minus, plus, count, container } = elements;
// could be any number
const initCount = parseInt(count.textContent);
// navigation unimportant here, so we'll just focus on the button
await plus.focus();
// with the `plus` button in focus, hitting `enter` should increment
await userEvent.keyboard('{enter}');
await expect(parseInt(count.textContent)).toBe(initCount + 1);
await userEvent.keyboard('{enter}');
await expect(parseInt(count.textContent)).toBe(initCount + 2);
await minus.focus();
await userEvent.keyboard('{enter}');
await expect(parseInt(count.textContent)).toBe(initCount + 1);
await userEvent.keyboard('{enter}');
await expect(parseInt(count.textContent)).toBe(initCount);
await userEvent.keyboard('{enter}');
await expect(parseInt(count.textContent)).toBe(initCount - 1);
// reset user focus to nothing
await minus.blur();
}
5.测试鼠标相互作用
与键盘交互相同,但使用鼠标单击而不是键盘事件。
/**
* Test mouse interaction
*/
export const mouseInteraction = async (elements) => {
const { minus, plus, count } = elements;
const initCount = parseInt(count.textContent);
await userEvent.click(plus);
await expect(parseInt(count.textContent)).toBe(initCount + 1);
await userEvent.click(plus);
await expect(parseInt(count.textContent)).toBe(initCount + 2);
await userEvent.click(minus);
await expect(parseInt(count.textContent)).toBe(initCount + 1);
await userEvent.click(minus);
await expect(parseInt(count.textContent)).toBe(initCount);
await userEvent.click(minus);
await expect(parseInt(count.textContent)).toBe(initCount - 1);
// reset user focus
await minus.blur();
}
使用故事书中的共享测试
现在我们有了共享的测试,我们可以在Storybook中使用它们。 (请参阅part 1)。以下是React Story文件,请参见the full set of framework-specific stories here。在Storybook UI中,测试方法被分解为step
函数,但不需要。
// ReactCounter.stories.js
import { ReactCounter } from '../../src/components/ReactCounter';
import { getElements, ensureElements, mouseInteraction, keyboardNavigation, keyboardInteraction } from '../../src/components/tests/counter.shared-spec';
export default {
title: 'React',
component: ReactCounter,
};
export const React = {
play: async ({ args, canvasElement, step }) => {
const elements = await getElements(canvasElement);
step('react tests', async () => {
await step('ensure elements', async () => {
await ensureElements(elements);
});
await step('mouse interaction', async () => {
await mouseInteraction(elements);
});
await step('keyboard navigation', async () => {
await keyboardNavigation(elements);
});
await step('keyboard interaction', async () => {
await keyboardInteraction(elements);
});
});
},
};
在vitest单元测试中使用共享测试
他们还可以进入我们的单位测试。 (请参阅part 3)。以下是React单元测试文件,请参见the full set of framework-specific unit tests here。这里的魔术在render
函数中。每个框架都有不同的框架,但是一旦vitest呈现它,共享测试就可以理解所有框架的结果输出。
注意:现在需要单独的it
函数才能进行react,这是在用户互动一起运行时遇到的一些问题。所有框架都不需要it
功能,但是由于它们具有可读性,我们会保留它们。
// Vue.spec.tsx
import { render } from '@testing-library/vue';
import { describe, it, assert, expect } from 'vitest';
import VueCounter from '@/components/VueCounter.vue';
import { getElements, ensureElements, mouseInteraction, keyboardNavigation, keyboardInteraction } from '@/components/tests/counter.shared-spec';
describe('Vue', () => {
describe('Counter', () => {
it('ensure elements', async () => {
const rendered = render(VueCounter);
const elements = await getElements(rendered.container);
await ensureElements(elements);
});
it('mouse interaction', async () => {
const rendered = render(VueCounter);
const elements = await getElements(rendered.container);
await mouseInteraction(elements);
});
it('keyboard navigation', async () => {
const rendered = render(VueCounter);
const elements = await getElements(rendered.container);
await keyboardNavigation(elements);
});
it('keyboard interaction', async () => {
const rendered = render(VueCounter);
const elements = await getElements(rendered.container);
await keyboardInteraction(elements);
});
});
})
包起来
为UI编写可共享的基于用户体验的测试,实际上是为了计划未来。
现在正在建造NewHotness¢UI框架,并在两年内将您的灵魂组成的反应组成部分将被删除,以代替闪亮的东西。这并不意味着您的UX需要更改...或设计...或测试。下次进行改头换面。
所有事物的效率,生产力将随之而来。