Skip to content

Support querying arrays of elements #294

Closed
@sarenji

Description

Describe the feature you'd like:

I wanted to propose querying an array of containers rather than a single container. It would allow us to do queries like this:

getByText(getAllByRole('heading'), 'Page 2'));

... which is more composable. Currently you can only query one container at a time, not an array of them.

As for a semi-practical example, let's say I'm on a page with the following (convoluted) JSX:

const pages = ['Page 1', 'Page 2'];

const App = () => {
  const [title, setTitle] = useState(pages[0]);
  return (
    <main>
      <h1>{title}</h1>
      <nav aria-labelledby="inner">
        <h2 id="inner">Inner nav</h2>
        <ul>
          {pages.map((pageTitle) => (
            <li>
              <a href="#url" onClick={() => setTitle(pageTitle)}>
                {pageTitle}
              </a>
            </li>
          ))}
        </ul>
      </nav>
    </main>
  );
};

I want to check that clicking Page 2 results in the page heading changing from Page 1 to Page 2.

However, since the same text is inside the heading and the nav (and potentially more in a real app), it's hard to select the proper element. With the new API, here's how we could test for that:

test('clicking navigation link results in page contents changing', () => {
  const { getAllByRole, getByText, getByLabelText } = render(<App/>);

  // Click the Page 2 link inside the inner nav.
  // We could also just do `fireEvent.click(getByText('Page 2'))` but
  // this is an example, and the intent there is less clear anyway
  const navs = getAllByRole('navigation');
  // !!! New API !!!
  const toc = within(navs).getByLabelText('inner');
  fireEvent.click(toc.getByText('Page 2'));

  // now both the heading AND the inner nav have Page 2 in it.
  // but we only wanna verify that the heading has certain text
  // !!! New API !!!
  getByText(getAllByRole('heading'), 'Page 2'));
});

Instead of limiting a query to a single container, we can now query an array of containers. The end result looks very natural, hopefully!

Suggested implementation:

I'm happy to give this a go! I was thinking of adding this to the queryAllBy* methods:

// query-helpers.js
function createQuery(queryAllBy, container, ...args) => {
  if (Array.isArray(container)) {
    const elements = container
      // get an array of all the query results
      .map((el) => queryAllBy(el, ...args))
      // flatten the results into a single array
      .reduce((array, result) => array.concat(result), []);
    // dedupe
    return Array.from(new Set(elements));
  } else {
    return queryAllBy(container, ...args);
  }
};

// label-text.js
// I initially thought about adding this to `buildQueries` but then I saw that
// `*LabelText` does not follow the `buildQuery` pattern. So:
queryAllByLabelText = createQuery(queryAllByLabelText);

... And then add tests for it. Does that approach seem sound to you? I'd look deeper if I get the 👍 on whether the API change makes sense.

Describe alternatives you've considered:

We could implement the first query as getByRoleAndText(role, text), but it's a bit too specific, especially if we want to add more queries of that kind in the future. If dom-testing-library supports element arrays for the container, then these queries become natural to write without changing the API surface too much.

Teachability, Documentation, Adoption, Migration Strategy:

Migration and adoption should be easy since it's an addition to the query API. Documentation is a bit harder, but I imagine we can start stating that queries now can take an array of elements instead of just one element.

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions