【问题标题】:How to query by text string which contains html tags using React Testing Library?如何使用 React 测试库通过包含 html 标签的文本字符串进行查询?
【发布时间】: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 LibrarygetByText方法:

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

但这不起作用。

那么,这个问题……

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

【问题讨论】:

    标签: reactjs unit-testing jestjs react-testing-library


    【解决方案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】:

        【讨论】:

        • 这是迄今为止最简单的解决方案。在这种情况下,类似于: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”或空字符串。

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

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


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

        【讨论】:

        • 我不明白为什么这是必要的,因为根据testing-library docsgetByText 已经在寻找 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?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-23
        • 2021-02-19
        • 2013-01-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多