├── .gitignore
├── README.md
├── jest.config.js
├── package.json
├── src
├── __tests__
│ ├── by-selector-queries.test.js
│ ├── enhance-queries.test.js
│ └── matchers.test.js
├── by-selector-queries.js
├── enhance-queries.js
├── index.js
├── matchers.js
├── screen.js
└── within.js
└── test
├── setup.js
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /package-lock.json
3 | /coverage/
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # query-extensions
2 |
3 | Extensions for core @testing-library queries
4 |
5 | [Kent & Travis briefly discuss](https://youtu.be/FdO2cphSH9Y?t=772) during one
6 | of Kent's helpful office hours sessions
7 |
8 | # Install
9 |
10 | ```sh
11 | npm install query-extensions
12 |
13 | # or
14 | yarn add query-extensions
15 | ```
16 |
17 | # Motivation
18 |
19 | Here's the reason this package exists:
20 |
21 | ```js
22 | import { screen } from "query-extensions";
23 | import { fireEvent } from "@testing-library/react";
24 | // ... more imports
25 |
26 | test("the standard screen queries work nicely for the majority of cases", async () => {
27 | render();
28 |
29 | // standard queries are available
30 | // component starts in loading state
31 | const loadingEl = screen.getByText("Loading...");
32 | expect(loadingEl).toBeInTheDocument();
33 |
34 | // loads up an email input, loading disappears
35 | const emailInput = await screen.findByLabelText("Your email");
36 | expect(screen.queryByText("Loading...")).toBeNull();
37 |
38 | // fill out email and click to sign up
39 | fireEvent.change(emailInput, { target: { value: "email@example.com" } });
40 | fireEvent.click(screen.getByRole("button", { name: /sign up/i }));
41 |
42 | // success modal pops up and takes over component (hiding other content)
43 | await screen.findByRole("img", { name: "celebration" });
44 | expect(screen.queryByLabelText("Your email")).toBeNull();
45 | expect(screen.queryByRole("button", { name: /sign up/i })).toBeNull();
46 | });
47 |
48 | test("the query extensions API can help us write something more readable and maintainable", async () => {
49 | const ui = {
50 | successIcon: { filter: "role", params: ["img", { name: "celebration" }] },
51 | signUpBtn: { filter: "role", params: ["button", { name: /sign up/i }] },
52 | emailInput: { filter: "labelText", params: ["Your email"] },
53 | loading: { filter: "text", params: ["Loading..."] },
54 | };
55 |
56 | render();
57 |
58 | // component starts in loading state
59 | expect(screen.get(ui.loading)).toBeInTheDocument();
60 |
61 | // loads up an email input, loading disappears
62 | const emailInput = await screen.find(ui.emailInput);
63 | expect(screen.query(ui.loading)).toBeNull();
64 |
65 | // fill out email and click to sign up
66 | fireEvent.change(emailInput, { target: { value: "email@example.com" } });
67 | fireEvent.click(screen.get(ui.signUpBtn));
68 |
69 | // success modal pops up and takes over component (hiding other content)
70 | await screen.find(ui.successIcon);
71 | expect(screen.query(ui.emailInput)).toBeNull();
72 | expect(screen.query(ui.signUpBtn)).toBeNull();
73 | });
74 | ```
75 |
76 | TL;DR wouldn't it be nice to reuse your querying configs without coupling to a
77 | particular flavor of get/query/find?
78 |
79 | If that (contrived) example doesn't sell you outright, consider a couple of
80 | "maintenance" scenarios. What happens to each test (or a _much_ bigger, more
81 | hypothetical test suite) if:
82 |
83 | 1. A UI element goes from rendering sync to async (or vice versa)
84 | 2. A UI element has a text/markup/label change which requires a different query
85 |
86 | # Usage
87 |
88 | ## `screen`
89 |
90 | There's a handy, pre-built `screen` object available for direct use. This is
91 | probably the most common way you'll interact with `query-extensions`
92 |
93 | ```js
94 | import { screen } from 'query-extensions';
95 | import { render } from '@testing-library/react';
96 | // ... more imports
97 |
98 | test('your actual test', () => {
99 | render();
100 |
101 | // standard screen query
102 | expect(screen.queryByText('Expected text')).toBeTruthy();
103 |
104 | // equivalent _enhanced_ query!
105 | expect(screen.query({ filter: 'text', params: ['Expected text'] }).toBeTruthy();
106 | })
107 | ```
108 |
109 | ## `within`
110 |
111 | Similarly, `query-extensions` provides its own version of the `within` API which
112 | makes the extended queries available on the resulting query object.
113 |
114 | ```js
115 | import { within, screen } from "query-extensions";
116 | import { render } from "@testing-library/react";
117 | // ... more imports
118 |
119 | test("your actual test", () => {
120 | render();
121 |
122 | // standard within-scoped query
123 | expect(
124 | within(screen.getByTestId("container-id")).queryByText("Expected text")
125 | ).toBeTruthy();
126 |
127 | // equivalent _enhanced_ query! OK it's actually _longer_ but you'll have to
128 | // make your own conclusions about tradeoffs ;)
129 | const containerConfig = { filter: "testId", params: ["container-id"] };
130 | const targetConfig = { filter: "text", params: ["Expected text"] };
131 | expect(within(screen.get(containerConfig)).query(targetConfig)).toBeTruthy();
132 | });
133 | ```
134 |
135 | Scoping with `within` is also possible via the `within` property of the query
136 | descriptor object (this can nest/compose with itself as well as the top-level
137 | `within` API)
138 |
139 | ```js
140 | import { screen } from "query-extensions";
141 | import { render } from "@testing-library/react";
142 | // ... more imports
143 |
144 | test("your actual test", () => {
145 | render();
146 |
147 | // standard within-scoped query
148 | expect(
149 | within(screen.getByTestId("container-id")).queryByText("Expected text")
150 | ).toBeTruthy();
151 |
152 | // equivalent _enhanced_ query!
153 | const containerConfig = { filter: "testId", params: ["container-id"] };
154 | expect(
155 | query({
156 | filter: "text",
157 | params: ["Expected text"],
158 | within: containerConfig,
159 | })
160 | ).toBeTruthy();
161 | });
162 | ```
163 |
164 | ## `enhanceQueries`
165 |
166 | You can also enhance any query objects you like using `enhanceQueries`
167 |
168 | ```js
169 | import { render } from '@testing-library/react';
170 | import { enhanceQueries } from 'query-extensions';
171 | // ... more imports
172 |
173 | test('your actual test', () => {
174 | const queries = render();
175 |
176 | // standard query
177 | expect(queries.queryByText('Expected text')).toBeTruthy();
178 |
179 | // equivalent _enhanced_ query!
180 | const enhanced = enhanceQueries(queries);
181 | expect(enhanced.query({ filter: 'text', params: ['Expected text'] }).toBeTruthy();
182 | })
183 | ```
184 |
185 | ## `queryBySelector` (and the whole \*BySelector family)
186 |
187 | OK, you _really_ should do everything in your power to keep your tests following
188 | the [guiding principles](https://testing-library.com/docs/guiding-principles) of
189 | @testing-library
190 |
191 | _BUT_ sometimes your application code is just a bit of a mess and your tests
192 | really need to drop down and do a standard `querySelector`-style interaction.
193 |
194 | This has always been possible with a bit of manual intervention, but
195 | `query-extensions` offers a simple wrapper for API consistency.
196 |
197 | ```js
198 | import { render } from "@testing-library/react";
199 | import { screen } from "query-extensions";
200 | // ... more imports
201 |
202 | test("sometimes you just have to use a selector", async () => {
203 | const { unmount } = render();
204 |
205 | // maybe your logo is just a styled div with a background-image, I dunno
206 | const logoData = { filter: "selector", params: [".company-logo"] };
207 |
208 | const logo = screen.get(logoData);
209 | expect(logo).toHaveStyle({ backgroundImage: "/some/image.png" }); // maybe!?
210 |
211 | // the long-form query API is available as well, of course!
212 | const logo2 = screen.getBySelector(".company-logo");
213 | expect(logo2).toHaveStyle({ backgroundImage: "/some/image.png" });
214 |
215 | unmount();
216 |
217 | expect(screen.query(logoData)).toBeNull();
218 | expect(screen.queryBySelector(".company-logo")).toBeNull();
219 | });
220 | ```
221 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after `n` failures
9 | // bail: 0,
10 |
11 | // The directory where Jest should store its cached dependency information
12 | // cacheDirectory: "/private/var/folders/t0/r14q7d0s51vbmt63j6g3gv3c0000gn/T/jest_dx",
13 |
14 | // Automatically clear mock calls and instances between every test
15 | clearMocks: true,
16 |
17 | // Indicates whether the coverage information should be collected while executing the test
18 | // collectCoverage: false,
19 |
20 | // An array of glob patterns indicating a set of files for which coverage information should be collected
21 | // collectCoverageFrom: undefined,
22 |
23 | // The directory where Jest should output its coverage files
24 | coverageDirectory: "coverage",
25 |
26 | // An array of regexp pattern strings used to skip coverage collection
27 | // coveragePathIgnorePatterns: [
28 | // "/node_modules/"
29 | // ],
30 |
31 | // Indicates which provider should be used to instrument code for coverage
32 | coverageProvider: "v8",
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: undefined,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: undefined,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // Force coverage collection from ignored files using an array of glob patterns
52 | // forceCoverageMatch: [],
53 |
54 | // A path to a module which exports an async function that is triggered once before all test suites
55 | // globalSetup: undefined,
56 |
57 | // A path to a module which exports an async function that is triggered once after all test suites
58 | // globalTeardown: undefined,
59 |
60 | // A set of global variables that need to be available in all test environments
61 | // globals: {},
62 |
63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
64 | // maxWorkers: "50%",
65 |
66 | // An array of directory names to be searched recursively up from the requiring module's location
67 | // moduleDirectories: [
68 | // "node_modules"
69 | // ],
70 |
71 | // An array of file extensions your modules use
72 | // moduleFileExtensions: [
73 | // "js",
74 | // "json",
75 | // "jsx",
76 | // "ts",
77 | // "tsx",
78 | // "node"
79 | // ],
80 |
81 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
82 | // moduleNameMapper: {},
83 |
84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
85 | // modulePathIgnorePatterns: [],
86 |
87 | // Activates notifications for test results
88 | // notify: false,
89 |
90 | // An enum that specifies notification mode. Requires { notify: true }
91 | // notifyMode: "failure-change",
92 |
93 | // A preset that is used as a base for Jest's configuration
94 | // preset: undefined,
95 |
96 | // Run tests from one or more projects
97 | // projects: undefined,
98 |
99 | // Use this configuration option to add custom reporters to Jest
100 | // reporters: undefined,
101 |
102 | // Automatically reset mock state between every test
103 | // resetMocks: false,
104 |
105 | // Reset the module registry before running each individual test
106 | // resetModules: false,
107 |
108 | // A path to a custom resolver
109 | // resolver: undefined,
110 |
111 | // Automatically restore mock state between every test
112 | // restoreMocks: false,
113 |
114 | // The root directory that Jest should scan for tests and modules within
115 | // rootDir: undefined,
116 |
117 | // A list of paths to directories that Jest should use to search for files in
118 | // roots: [
119 | // ""
120 | // ],
121 |
122 | // Allows you to use a custom runner instead of Jest's default test runner
123 | // runner: "jest-runner",
124 |
125 | // The paths to modules that run some code to configure or set up the testing environment before each test
126 | // setupFiles: [],
127 |
128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
129 | setupFilesAfterEnv: ['/test/setup.js'],
130 |
131 | // The number of seconds after which a test is considered as slow and reported as such in the results.
132 | // slowTestThreshold: 5,
133 |
134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
135 | // snapshotSerializers: [],
136 |
137 | // The test environment that will be used for testing
138 | // testEnvironment: "jest-environment-jsdom",
139 |
140 | // Options that will be passed to the testEnvironment
141 | // testEnvironmentOptions: {},
142 |
143 | // Adds a location field to test results
144 | // testLocationInResults: false,
145 |
146 | // The glob patterns Jest uses to detect test files
147 | // testMatch: [
148 | // "**/__tests__/**/*.[jt]s?(x)",
149 | // "**/?(*.)+(spec|test).[tj]s?(x)"
150 | // ],
151 |
152 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
153 | // testPathIgnorePatterns: [
154 | // "/node_modules/"
155 | // ],
156 |
157 | // The regexp pattern or array of patterns that Jest uses to detect test files
158 | // testRegex: [],
159 |
160 | // This option allows the use of a custom results processor
161 | // testResultsProcessor: undefined,
162 |
163 | // This option allows use of a custom test runner
164 | // testRunner: "jasmine2",
165 |
166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
167 | // testURL: "http://localhost",
168 |
169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
170 | // timers: "real",
171 |
172 | // A map from regular expressions to paths to transformers
173 | // transform: undefined,
174 |
175 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
176 | // transformIgnorePatterns: [
177 | // "/node_modules/",
178 | // "\\.pnp\\.[^\\/]+$"
179 | // ],
180 |
181 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
182 | // unmockedModulePathPatterns: undefined,
183 |
184 | // Indicates whether each individual test should be reported during the run
185 | // verbose: undefined,
186 |
187 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
188 | // watchPathIgnorePatterns: [],
189 |
190 | // Whether to use watchman for file crawling
191 | // watchman: true,
192 | };
193 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "query-extensions",
3 | "version": "0.0.4",
4 | "description": "Extensions to the core @testing-library query API",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "test": "jest"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/tjefferson08/query-extensions.git"
12 | },
13 | "keywords": [
14 | "testing-library",
15 | "queries",
16 | "extensions",
17 | "get",
18 | "find",
19 | "query"
20 | ],
21 | "author": "Travis Jefferson",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/tjefferson08/query-extensions/issues"
25 | },
26 | "homepage": "https://github.com/tjefferson08/query-extensions#readme",
27 | "dependencies": {},
28 | "devDependencies": {
29 | "@testing-library/dom": "^7.24.2",
30 | "@testing-library/jest-dom": "^5.11.4",
31 | "jest": "^26.4.2"
32 | },
33 | "peerDependencies": {
34 | "@testing-library/dom": "^7.24.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/__tests__/by-selector-queries.test.js:
--------------------------------------------------------------------------------
1 | const { renderIntoDocument } = require("../../test/utils");
2 | const {
3 | queryBySelector,
4 | queryAllBySelector,
5 | getBySelector,
6 | getAllBySelector,
7 | findBySelector,
8 | findAllBySelector
9 | } = require("../by-selector-queries");
10 |
11 | test("should support all queries by standard css selectors", async () => {
12 | const { unmount } = renderIntoDocument(
13 | `