在JavaScript框架之间共享UI测试
#javascript #前端 #测试 #storybook

如何在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元素
  • 容器,这是一个带有一类counterdiv11

使用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需要更改...或设计...或测试。下次进行改头换面。

所有事物的效率,生产力将随之而来。