如何使用 React 测试库按包含 html 标签的文本字符串进行查询?

How to query by text string which contains html tags using React Testing Library?

当前工作解决方案

使用这个 html:

<p data-testid="foo">Name: <strong>Bob</strong> <em>(special guest)</em></p>

我可以使用 React Testing Library getByTestId 方法找到 textContent:

expect(getByTestId('foo').textContent).toEqual('Name: Bob (special guest)')

有没有更好的方法?

我想简单地使用这个 html:

<p>Name: <strong>Bob</strong> <em>(special guest)</em></p>

并像这样使用 React Testing LibrarygetByText 方法:

expect(getByText('Name: Bob (special guest)')).toBeTruthy()

但这不起作用。

那么,问题……

有没有更简单的方法来使用 React 测试库来查找带标签的文本内容字符串?

更新

下面的解决方案有效,但在某些情况下,它可能 return 不止一个结果。这是正确的实现:

getByText((_, node) => {
  const hasText = node => node.textContent === "Name: Bob (special guest)";
  const nodeHasText = hasText(node);
  const childrenDontHaveText = Array.from(node.children).every(
    child => !hasText(child)
  );

  return nodeHasText && childrenDontHaveText;
});

您可以将方法传递给 getbyText:

getByText((_, node) => node.textContent === 'Name: Bob (special guest)')

您可以将代码放入辅助函数中,这样您就不必一直输入它了:

  const { getByText } = render(<App />)
  const getByTextWithMarkup = (text) =>
    getByText((_, node) => node.textContent === text)

更新 2

用了很多次,我创建了一个助手。下面是使用此助手的示例测试。

测试助手:

// withMarkup.ts
import { MatcherFunction } from '@testing-library/react'

type Query = (f: MatcherFunction) => HTMLElement

const withMarkup = (query: Query) => (text: string): HTMLElement =>
  query((content: string, node: HTMLElement) => {
    const hasText = (node: HTMLElement) => node.textContent === text
    const childrenDontHaveText = Array.from(node.children).every(
      child => !hasText(child as HTMLElement)
    )
    return hasText(node) && childrenDontHaveText
  })

export default withMarkup

测试:

// app.test.tsx
import { render } from '@testing-library/react'
import App from './App'
import withMarkup from '../test/helpers/withMarkup'

it('tests foo and bar', () => {
  const { getByText } = render(<App />)
  const getByTextWithMarkup = withMarkup(getByText)
  getByTextWithMarkup('Name: Bob (special guest)')
})

更新 1

这是创建新匹配器 getByTextWithMarkup 的示例。请注意,此函数在测试中扩展了 getByText,因此必须在此处定义它。 (当然可以更新函数以接受 getByText 作为参数。)

import { render } from "@testing-library/react";
import "jest-dom/extend-expect";

test("pass functions to matchers", () => {
  const Hello = () => (
    <div>
      Hello <span>world</span>
    </div>
  );
  const { getByText } = render(<Hello />);

  const getByTextWithMarkup = (text: string) => {
    getByText((content, node) => {
      const hasText = (node: HTMLElement) => node.textContent === text
      const childrenDontHaveText = Array.from(node.children).every(
        child => !hasText(child as HTMLElement)
      )
      return hasText(node) && childrenDontHaveText
    })
  }

  getByTextWithMarkup('Hello world')

这是 Five Things You (Probably) Didn't Know About Testing Library from Giorgio Polvara's Blog 第 4 期的可靠答案:


查询也接受函数

您可能遇到过这样的错误:

Unable to find an element with the text: Hello world. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

通常,这是因为您的 HTML 看起来像这样:

<div>Hello <span>world</span></div>

错误消息中包含解决方案:“[...] 您可以为文本匹配器提供一个函数 [...]”。

这是怎么回事?事实证明,匹配器接受字符串、正则表达式或函数。

您正在渲染的每个节点都会调用该函数。它接收两个参数:节点的内容和节点本身。您所要做的就是 return true 或 false,具体取决于节点是否是您想要的节点。

举个例子说明:

import { render } from "@testing-library/react";
import "jest-dom/extend-expect";

test("pass functions to matchers", () => {
  const Hello = () => (
    <div>
      Hello <span>world</span>
    </div>
  );
  const { getByText } = render(<Hello />);

  // These won't match
  // getByText("Hello world");
  // getByText(/Hello world/);

  getByText((content, node) => {
    const hasText = node => node.textContent === "Hello world";
    const nodeHasText = hasText(node);
    const childrenDontHaveText = Array.from(node.children).every(
      child => !hasText(child)
    );

    return nodeHasText && childrenDontHaveText;
  });
});

我们忽略了 content 参数,因为在这种情况下,它将是 "Hello"、"world" 或空字符串。

我们检查的是当前节点是否有权限textContenthasText 是一个小辅助函数来做到这一点。我声明它是为了保持清洁。

但这还不是全部。我们的 div 不是唯一包含我们要查找的文本的节点。例如,本例中的 body 具有相同的文本。为避免 return 的节点多于所需,我们确保 none 的子节点具有与其父节点相同的文本。通过这种方式,我们确保我们 returning 的节点是最小的——换句话说,该节点接近我们 DOM 树的底部。


阅读 Five Things You (Probably) Didn't Know About Testing Library

的其余部分

子串匹配,可以使用exact:

https://testing-library.com/docs/dom-testing-library/api-queries#textmatch

In this case something like: expect(getByText('Name:', { exact: false }).textContent).toEqual('Name: Bob (special guest)');

现有答案已过时。新的 *ByRole 查询支持这个:

getByRole('button', {name: 'Bob (special guest)'})

为了避免匹配多个元素,对于一些用例只返回实际本身有文本内容的元素,过滤掉不需要的父元素就好了:

expect(
  // - content: text content of current element, without text of its children
  // - element.textContent: content of current element plus its children
  screen.getByText((content, element) => {
    return content !== '' && element.textContent === 'Name: Bob (special guest)';
  })
).toBeInTheDocument();

上面需要 一些 元素的内容正在测试,因此适用于:

<div>
  <p>Name: <strong>Bob</strong> <em>(special guest)</em></p>
</div>

...但如果 <p> 没有自己的文本内容:

<div>
  <p><em>Name: </em><strong>Bob</strong><em> (special guest)</em></p>
</div>

因此,对于通用解决方案,其他答案肯定更好。

如果您在项目中使用 testing-library/jest-dom。您也可以使用 toHaveTextContent.

expect(getByTestId('foo')).toHaveTextContent('Name: Bob (special guest)')

如果您需要部分匹配,您也可以使用正则表达式搜索模式

expect(getByTestId('foo')).toHaveTextContent(/Name: Bob/)

这是 link 到 package

不确定这是否会如您所愿,但我们可以只使用 findByText 吗?

getByText('Hello World'); // full string match
getByText('llo Worl', { exact: false }); // substring match
getByText('hello world', { exact: false }); // ignore case-sensitivity

来源:https://testing-library.com/docs/react-testing-library/cheatsheet/#queries