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

Posted

技术标签:

【中文标题】如何使用 React 测试库通过包含 html 标签的文本字符串进行查询?【英文标题】:How to query by text string which contains html tags using React Testing Library? 【发布时间】:2019-08-25 20:26:10 【问题描述】:

当前工作解决方案

使用这个 html

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

我可以使用React Testing LibrarygetByTestId方法找到textContent

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

有没有更好的方法?

我想简单地使用这个html:

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

并像这样使用React Testing Library的getByText方法:

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

但这不起作用。

那么,这个问题……

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

【问题讨论】:

【参考方案1】:

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

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

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

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

这是package的链接

【讨论】:

【参考方案2】:

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

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>

...但如果&lt;p&gt; 没有自己的文本内容,则不会:

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

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

【讨论】:

【参考方案3】:

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

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

【讨论】:

在这种没有“按钮”的情况下如何工作? @jarthur - 使用可访问性 DOM 检查您的目标元素以确定其角色。 我想知道在 OP 的上下文中,没有明显的作用。除非p 有默认角色? @jarthur -

具有段落的作用。然而,奇怪的是 getByRole 忽略了段落。因此,您需要使用 getByRole 当前支持的不同包装元素,例如标题或区域。

@CoryHouse - 如果没有具有可访问角色的元素并且只有这样的元素怎么办: [AL] 阿尔巴尼亚 [DZ] 阿尔及利亚 如何通过文本查询第一个元素?【参考方案4】:

对于子串匹配,可以使用exact:

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

【讨论】:

这是迄今为止最简单的解决方案。在这种情况下,类似于:expect(getByText('Name:', exact: false ).textContent).toEqual('Name: Bob (special guest)');【参考方案5】:

更新 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 的第 4 个来自Giorgio Polvara's Blog 的可靠答案:


查询也接受函数

你可能见过这样的错误:

找不到带有以下文字的元素:Hello world。 这可能是因为文本被多个元素分解。 在这种情况下,您可以为您的文本提供一个功能 matcher 让你的 matcher 更加灵活。

通常,这是因为您的 HTML 如下所示:

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

解决方案包含在错误消息中:“[...] you can provide a function for your text matcher [...]”。

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

函数会为您渲染的每个节点调用。它接收两个参数:节点的内容和节点本身。您所要做的就是根据节点是否是您想要的节点返回 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”或空字符串。

我们检查的是当前节点是否拥有正确的textContent。 hasText 是一个小助手功能。我宣布它是为了保持清洁。

这还不是全部。我们的div 不是我们要查找的文本的唯一节点。例如,body 在这种情况下具有相同的文本。为了避免返回比需要更多的节点,我们确保没有一个子节点与其父节点具有相同的文本。通过这种方式,我们确保我们返回的节点是最小的——换句话说,那个节点靠近我们的 DOM 树的底部。


阅读Five Things You (Probably) Didn't Know About Testing Library的其余部分

【讨论】:

我不明白为什么这是必要的,因为根据testing-library docs,getByText 已经在寻找 textContent,因此getByText("Hello World") 应该可以工作,对(虽然它似乎不是出于某种原因)? 那是因为getByText 正在使用getNodeText 助手,它正在寻找每个文本节点textContent 属性。在您的情况下,作为&lt;p&gt; 直接子级的唯一文本节点是Name: 和` `。我不确定为什么 RTL 决定不以递归方式查找作为子节点的子节点的文本节点。也许是出于性能原因,但事实就是如此。也许@kentcdodds 可以对此提供更多见解 考虑一下 RTL 不会寻找孩子的孩子,因为否则这个 getAllByText(&lt;div&gt;&lt;div&gt;Hello&lt;/div&gt;&lt;/div&gt;, 'Hello') 会返回两个结果。有道理 不错的答案。我还必须捕获getByText 抛出的异常并用text 重新抛出另一条消息,因为使用自定义匹配器时它不包含在错误消息中。我认为在@testing-library 中默认包含这个助手会很棒。 @PaoloMoretti - ?? 您能否发布您描述的解决方案作为此问题的另一个答案?【参考方案6】:

更新

以下解决方案有效,但在某些情况下,它可能会返回多个结果。这是正确的实现:

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)

【讨论】:

这个解决方案可以在简单的场景中工作,但是如果它产生错误“找到多个带有文本的元素:(_, node) => node.textContent === 'Name: Bob (special guest )'",然后尝试另一个答案的解决方案,它也检查子节点。 同意,解决方案其实取自我的博客:D 感谢您对 Giorgio 的洞察。当我发现在新测试中需要这些解决方案时,我不断地回到这些答案。 :) 有没有办法修改这个想法以使用 cypress-testing-library?

以上是关于如何使用 React 测试库通过包含 html 标签的文本字符串进行查询?的主要内容,如果未能解决你的问题,请参考以下文章

React 测试库如何使用 waitFor

如何使用 React 测试库从选择列表中选择一个选项

React 测试库:测试样式(特别是背景图片)

如何在 React 测试库中获取 Material-UI 密码输入

如何使用 Enzyme 测试 React 组件属性的样式

如何在我的组件库中使用 React 钩子?