Skip to content

Instantly share code, notes, and snippets.

@benquarmby
Last active January 10, 2024 18:20
Show Gist options
  • Save benquarmby/7510252ab701669c2eaf3c0156dd680c to your computer and use it in GitHub Desktop.
Save benquarmby/7510252ab701669c2eaf3c0156dd680c to your computer and use it in GitHub Desktop.
jest-dom-query

Proposal for jest assertions based on @testing-library/dom queries. Below is a fictitious README if it were packaged seperately.

Jest DOM Query

Unofficial @testing-library/dom assertions for Jest that complement @testing-library/jest-dom and make this advice easier to follow:

Advice: If you want to assert that something exists, make that assertion explicit.

Installation

Install this library with its peer dependency using a package manager such as yarn:

yarn add --dev @testing-library/jest-dom-query @testing-library/dom

Usage

Import the main module for side effects within a test suite. Alternatively, import it once for all suites using the setupFilesAfterEnv option.

import "@testing-library/jest-dom-query";

Pass a container element to expect and assert on the existence of child elements.

const dialog = screen.getByRole("dialog");

expect(dialog).toContainOneByRole("heading", {
    name: "Greetings"
});

For an equivalent of testing library's screen, pass document as the container.

expect(document).not.toContainAnyByRole("dialog");

Types of Assertions

Each of the eight core queries supported by @testing-library/dom have equivalent single and multiple element assertions. The arguments for each are identical to @testing-library/dom, assuming the container has already been passed to expect.

Please see the testing library documentation for more information about available options. For example: ByRole

Single Elements

The positive toContainOneBy... assertions pass when there is precisely one matching element. The negative not.toContainOneBy... assertions pass when there are zero or more than one matching elements.

Multiple Elements

The positive toContainAnyBy... assertions pass when there are one or more matching elements. The negative not.toContainAnyBy... assertions pass when there are zero matching elements.

Summary Table

Type of Query 0 Matches 1 Match >1 Matches
Single Element
toContainOneBy...
not.toContainOneBy...
Multiple Elements
toContainAnyBy...
not.toContainAnyBy...

Advanced Usage

The testing library family contains extensibility for advanced features such as adding custom queries. These are supported by this library using the createQueryMatcher named export, which is the exact tool used internally for all standard matchers.

Once custom queries have been defined (see official example), TypeScript users can use declaration merging to add new matchers to jests's expect function. For example, assuming a customQueries.ts module has already been defined:

// customQueryMatchers.d.ts
declare namespace jest {
    type CustomQueryArgs = DomQueryArgs<typeof import("./customQueries").queryAllBySample>;

    interface CustomQueryMatchers<R = any> {
        toContainAnyBySample(...args: CustomQueryArgs): R;
        toContainOneBySample(...args: CustomQueryArgs): R;
    }

    interface Matchers<R> extends CustomQueryMatchers<R> {}
}

Then use the createQueryMatcher for the real implementations:

// customMatchers.ts
import {createQueryMatcher} from "@testing-library/jest-dom-query";
import {queryAllBySample} from "./customQueries";

const customMatchers: Record<keyof jest.CustomQueryMatchers, jest.CustomMatcher> = {
    toContainAnyBySample: createQueryMatcher({
        matcherType: "any",
        query: queryAllBySample,
        queryName: "custom query"
    }),
    toContainOneBySample: createQueryMatcher({
        matcherType: "one",
        query: queryAllBySample,
        queryName: "custom query"
    })
};

expect.extend(customMatchers);

Finally, import the module for side effects in a test suite (or shared setup file) and use custom matchers to assert as needed.

// testSuite.ts
import "./customMatchers";

it("should be awesome", function () {
    // Rendering here
    expect(document).toContainOneBySample("sample1");
    expect(document).not.toContainAnyBySample("sample2");
});
import "./jest";
export declare interface QueryMatcherOptions {
matcherType: "any" | "one";
query: jest.DomQueryFunction;
queryName: string;
}
export declare function createQueryMatcher(options: QueryMatcherOptions): jest.CustomMatcher;
const {prettyDOM: prettyDom, queries} = require("@testing-library/dom");
const queryNameMap = {
["alt text"]: queries.queryAllByAltText,
["display value"]: queries.queryAllByDisplayValue,
["label text"]: queries.queryAllByLabelText,
["placeholder text"]: queries.queryAllByPlaceholderText,
["role"]: queries.queryAllByRole,
["test ID"]: queries.queryAllByTestId,
["text"]: queries.queryAllByText,
["title"]: queries.queryAllByTitle
};
function isString(value) {
return typeof value === "string";
}
function createQueryMatcher({matcherType, query, queryName}) {
return function (actual, ...args) {
const elements = query(actual, ...args);
const pass = matcherType === "one" ? elements.length === 1 : elements.length > 0;
// eslint-disable-next-line fp/no-this
const {printReceived, printExpected} = this.utils;
return {
message() {
const to = pass ? "not to" : "to";
const prettyExpected = printExpected(args[0].toString());
const prettyActual = printReceived(elements.length);
const prettyElements = elements.map((element) => prettyDom(element)).filter(isString);
const message = [
// Idiomatic jest language with jest formatters.
`Expected container ${to} contain ${matcherType} element by ${queryName} ${prettyExpected}, but found ${prettyActual}.`
];
if (prettyElements.length) {
// Idiomatic testing library language with testing library formatters.
message.push("Here are the matching elements:", ...prettyElements);
}
return message.join("\n\n");
},
pass
};
};
}
function createAnyQueryMatcher(queryName) {
return createQueryMatcher({
matcherType: "any",
query: queryNameMap[queryName],
queryName
});
}
function createOneQueryMatcher(queryName) {
return createQueryMatcher({
matcherType: "one",
query: queryNameMap[queryName],
queryName
});
}
const matchers = {
toContainAnyByAltText: createAnyQueryMatcher("alt text"),
toContainAnyByDisplayValue: createAnyQueryMatcher("display value"),
toContainAnyByLabelText: createAnyQueryMatcher("label text"),
toContainAnyByPlaceholderText: createAnyQueryMatcher("placeholder text"),
toContainAnyByRole: createAnyQueryMatcher("role"),
toContainAnyByTestId: createAnyQueryMatcher("test ID"),
toContainAnyByText: createAnyQueryMatcher("text"),
toContainAnyByTitle: createAnyQueryMatcher("title"),
toContainOneByAltText: createOneQueryMatcher("alt text"),
toContainOneByDisplayValue: createOneQueryMatcher("display value"),
toContainOneByLabelText: createOneQueryMatcher("label text"),
toContainOneByPlaceholderText: createOneQueryMatcher("placeholder text"),
toContainOneByRole: createOneQueryMatcher("role"),
toContainOneByTestId: createOneQueryMatcher("test ID"),
toContainOneByText: createOneQueryMatcher("text"),
toContainOneByTitle: createOneQueryMatcher("title")
};
expect.extend(matchers);
module.exports = {createQueryMatcher};
declare namespace jest {
interface DomQueryFunction {
(container: HTMLElement, ...args: any[]): HTMLElement[];
}
type DomQueryArgs<F extends DomQueryFunction = DomQueryFunction> = F extends (
container: HTMLElement,
...args: infer A
) => HTMLElement[]
? A
: never;
type DomQueryByBoundAttributeArgs = DomQueryArgs<import("@testing-library/dom").AllByBoundAttribute>;
type DomQueryByRoleArgs = DomQueryArgs<import("@testing-library/dom").AllByRole>;
type DomQueryByTextArgs = DomQueryArgs<import("@testing-library/dom").AllByText>;
interface DomQueryMatchers<R = any> {
toContainAnyByAltText(...args: DomQueryByBoundAttributeArgs): R;
toContainAnyByDisplayValue(...args: DomQueryByBoundAttributeArgs): R;
toContainAnyByLabelText(...args: DomQueryByTextArgs): R;
toContainAnyByPlaceholderText(...args: DomQueryByBoundAttributeArgs): R;
toContainAnyByRole(...args: DomQueryByRoleArgs): R;
toContainAnyByTestId(...args: DomQueryByBoundAttributeArgs): R;
toContainAnyByText(...args: DomQueryByTextArgs): R;
toContainAnyByTitle(...args: DomQueryByBoundAttributeArgs): R;
toContainOneByAltText(...args: DomQueryByBoundAttributeArgs): R;
toContainOneByDisplayValue(...args: DomQueryByBoundAttributeArgs): R;
toContainOneByLabelText(...args: DomQueryByTextArgs): R;
toContainOneByPlaceholderText(...args: DomQueryByBoundAttributeArgs): R;
toContainOneByRole(...args: DomQueryByRoleArgs): R;
toContainOneByTestId(...args: DomQueryByBoundAttributeArgs): R;
toContainOneByText(...args: DomQueryByTextArgs): R;
toContainOneByTitle(...args: DomQueryByBoundAttributeArgs): R;
}
interface Matchers<R> extends DomQueryMatchers<R> {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment