Skip to content

Commit 1809825

Browse files
feat: add within operator for returned instance-bound queries (#306)
* Basic within implementation and tests * Added typescript types & tests * Removed duplicated import in index.d.ts * Reversed prettier run on typings * Fixed package level exports for `within` operator * Replaced getByTestId queries with getByA11yHint queries in unit tests for `within` * Added `within` section to API.md * Updated README.md regarding `within` operator * Update website/docs/API.md Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
1 parent 51bd418 commit 1809825

File tree

7 files changed

+231
-1
lines changed

7 files changed

+231
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ The [public API](https://callstack.github.io/react-native-testing-library/docs/a
8585
- [`render`](https://callstack.github.io/react-native-testing-library/docs/api#render) – deeply renders given React element and returns helpers to query the output components.
8686
- [`fireEvent`](https://callstack.github.io/react-native-testing-library/docs/api#fireevent) - invokes named event handler on the element.
8787
- [`waitForElement`](https://callstack.github.io/react-native-testing-library/docs/api#waitforelement) - waits for non-deterministic periods of time until your element appears or times out.
88+
- [`within`](https://callstack.github.io/react-native-testing-library/docs/api#within) - creates a queries object scoped for given element.
8889
- [`flushMicrotasksQueue`](https://callstack.github.io/react-native-testing-library/docs/api#flushmicrotasksqueue) - waits for microtasks queue to flush.
8990

9091
**Note to users who are more familiar with `react-testing-library`:** This API does not expose `cleanup` because it doesn't interact with the DOM. There's nothing to clean up.

src/__tests__/within.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// @flow
2+
import React from 'react';
3+
import { View, Text, TextInput } from 'react-native';
4+
import { render, within } from '..';
5+
6+
test('within() exposes basic queries', () => {
7+
const rootQueries = render(
8+
<View>
9+
<View accessibilityHint="first">
10+
<Text>Same Text</Text>
11+
<TextInput value="Same Value" placeholder="Same Placeholder" />
12+
</View>
13+
<View accessibilityHint="second">
14+
<Text>Same Text</Text>
15+
<TextInput value="Same Value" placeholder="Same Placeholder" />
16+
</View>
17+
</View>
18+
);
19+
20+
expect(rootQueries.getAllByText('Same Text')).toHaveLength(2);
21+
expect(rootQueries.getAllByDisplayValue('Same Value')).toHaveLength(2);
22+
expect(rootQueries.getAllByPlaceholder('Same Placeholder')).toHaveLength(2);
23+
24+
const firstQueries = within(rootQueries.getByA11yHint('first'));
25+
expect(firstQueries.getAllByText('Same Text')).toHaveLength(1);
26+
expect(firstQueries.getByText('Same Text')).toBeTruthy();
27+
expect(firstQueries.queryAllByText('Same Text')).toHaveLength(1);
28+
expect(firstQueries.queryByText('Same Text')).toBeTruthy();
29+
expect(firstQueries.getByDisplayValue('Same Value')).toBeTruthy();
30+
expect(firstQueries.getByPlaceholder('Same Placeholder')).toBeTruthy();
31+
32+
const secondQueries = within(rootQueries.getByA11yHint('second'));
33+
expect(secondQueries.getAllByText('Same Text')).toHaveLength(1);
34+
expect(secondQueries.getByText('Same Text')).toBeTruthy();
35+
expect(secondQueries.queryAllByText('Same Text')).toHaveLength(1);
36+
expect(secondQueries.queryByText('Same Text')).toBeTruthy();
37+
expect(secondQueries.getByDisplayValue('Same Value')).toBeTruthy();
38+
expect(secondQueries.getByPlaceholder('Same Placeholder')).toBeTruthy();
39+
});
40+
41+
test('within() exposes a11y queries', () => {
42+
const rootQueries = render(
43+
<View>
44+
<View accessibilityHint="first">
45+
<TextInput
46+
value="Same Value"
47+
accessibilityLabel="Same Label"
48+
accessibilityHint="Same Hint"
49+
/>
50+
</View>
51+
<View accessibilityHint="second">
52+
<TextInput
53+
value="Same Value"
54+
accessibilityLabel="Same Label"
55+
accessibilityHint="Same Hint"
56+
/>
57+
</View>
58+
</View>
59+
);
60+
61+
expect(rootQueries.getAllByA11yLabel('Same Label')).toHaveLength(2);
62+
expect(rootQueries.getAllByA11yHint('Same Hint')).toHaveLength(2);
63+
64+
const firstQueries = within(rootQueries.getByA11yHint('first'));
65+
expect(firstQueries.getByA11yLabel('Same Label')).toBeTruthy();
66+
expect(firstQueries.getByA11yHint('Same Hint')).toBeTruthy();
67+
68+
const secondQueries = within(rootQueries.getByA11yHint('second'));
69+
expect(secondQueries.getAllByA11yLabel('Same Label')).toHaveLength(1);
70+
expect(secondQueries.getAllByA11yHint('Same Hint')).toHaveLength(1);
71+
});

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import flushMicrotasksQueue from './flushMicrotasksQueue';
77
import render from './render';
88
import shallow from './shallow';
99
import waitForElement from './waitForElement';
10+
import within from './within';
1011

1112
export { act };
1213
export { cleanup };
@@ -16,3 +17,4 @@ export { flushMicrotasksQueue };
1617
export { render };
1718
export { shallow };
1819
export { waitForElement };
20+
export { within };

src/within.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @flow
2+
import { getByAPI } from './helpers/getByAPI';
3+
import { queryByAPI } from './helpers/queryByAPI';
4+
import a11yAPI from './helpers/a11yAPI';
5+
6+
export default function within(instance: ReactTestInstance) {
7+
return {
8+
...getByAPI(instance),
9+
...queryByAPI(instance),
10+
...a11yAPI(instance),
11+
};
12+
}

typings/__tests__/index.test.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
debug,
1010
waitForElement,
1111
act,
12+
within,
1213
} from '../..';
1314

1415
interface HasRequiredProp {
@@ -217,3 +218,115 @@ const waitByAll: Promise<Array<ReactTestInstance>> = waitForElement<
217218
act(() => {
218219
render(<TestComponent />);
219220
});
221+
222+
// within API
223+
const instance: ReactTestInstance = tree.getByText('<View />');
224+
const withinGetByText: ReactTestInstance = within(instance).getByText('Test');
225+
const withinGetAllByText: ReactTestInstance[] = within(instance).getAllByText(
226+
'Test'
227+
);
228+
const withinGetByDisplayValue: ReactTestInstance = within(
229+
instance
230+
).getByDisplayValue('Test');
231+
const withinGetAllByDisplayValue: ReactTestInstance[] = within(
232+
instance
233+
).getAllByDisplayValue('Test');
234+
const withinGetByPlaceholder: ReactTestInstance = within(
235+
instance
236+
).getByPlaceholder('Test');
237+
const withinGetAllByPlaceholder: ReactTestInstance[] = within(
238+
instance
239+
).getAllByPlaceholder('Test');
240+
const withinGetByTestId: ReactTestInstance = within(instance).getByTestId(
241+
'Test'
242+
);
243+
const withinGetAllByTestId: ReactTestInstance[] = within(
244+
instance
245+
).getAllByTestId('Test');
246+
247+
const withinQueryByText: ReactTestInstance | null = within(
248+
instance
249+
).queryByText('Test');
250+
const withinQueryAllByText: ReactTestInstance[] = within(
251+
instance
252+
).queryAllByText('Test');
253+
const withinQueryByDisplayValue: ReactTestInstance | null = within(
254+
instance
255+
).queryByDisplayValue('Test');
256+
const withinQueryAllByDisplayValue: ReactTestInstance[] = within(
257+
instance
258+
).queryAllByDisplayValue('Test');
259+
const withinQueryByPlaceholder: ReactTestInstance | null = within(
260+
instance
261+
).queryByPlaceholder('Test');
262+
const withinQueryAllByPlaceholder: ReactTestInstance[] = within(
263+
instance
264+
).queryAllByPlaceholder('Test');
265+
const withinQueryByTestId: ReactTestInstance | null = within(
266+
instance
267+
).queryByTestId('Test');
268+
const withinQueryAllByTestId: ReactTestInstance[] = within(
269+
instance
270+
).queryAllByTestId('Test');
271+
272+
const withinGetByA11yLabel: ReactTestInstance = within(instance).getByA11yLabel(
273+
'Test'
274+
);
275+
const withinGetAllByA11yLabel: ReactTestInstance[] = within(
276+
instance
277+
).getAllByA11yLabel('Test');
278+
const withinGetByA11yHint: ReactTestInstance = within(instance).getByA11yHint(
279+
'Test'
280+
);
281+
const withinGetAllByA11yHint: ReactTestInstance[] = within(
282+
instance
283+
).getAllByA11yHint('Test');
284+
const withinGetByA11yRole: ReactTestInstance = within(instance).getByA11yRole(
285+
'button'
286+
);
287+
const withinGetAllByA11yRole: ReactTestInstance[] = within(
288+
instance
289+
).getAllByA11yRole('button');
290+
const withinGetByA11yState: ReactTestInstance = within(
291+
instance
292+
).getByA11yState({ busy: true });
293+
const withinGetAllByA11yState: ReactTestInstance[] = within(
294+
instance
295+
).getAllByA11yState({ busy: true });
296+
const withinGetByA11yValue: ReactTestInstance = within(
297+
instance
298+
).getByA11yValue({ min: 10 });
299+
const withinGetAllByA11yValue: ReactTestInstance[] = within(
300+
instance
301+
).getAllByA11yValue({ min: 10 });
302+
303+
const withinQueryByA11yLabel: ReactTestInstance | null = within(
304+
instance
305+
).queryByA11yLabel('Test');
306+
const withinQueryAllByA11yLabel: ReactTestInstance[] = within(
307+
instance
308+
).queryAllByA11yLabel('Test');
309+
const withinQueryByA11yHint: ReactTestInstance | null = within(
310+
instance
311+
).queryByA11yHint('Test');
312+
const withinQueryAllByA11yHint: ReactTestInstance[] = within(
313+
instance
314+
).queryAllByA11yHint('Test');
315+
const withinQueryByA11yRole: ReactTestInstance | null = within(
316+
instance
317+
).queryByA11yRole('button');
318+
const withinQueryAllByA11yRole: ReactTestInstance[] = within(
319+
instance
320+
).queryAllByA11yRole('button');
321+
const withinQueryByA11yState: ReactTestInstance | null = within(
322+
instance
323+
).queryByA11yState({ busy: true });
324+
const withinQueryAllByA11yState: ReactTestInstance[] = within(
325+
instance
326+
).queryAllByA11yState({ busy: true });
327+
const withinQueryByA11yValue: ReactTestInstance | null = within(
328+
instance
329+
).queryByA11yValue({ min: 10 });
330+
const withinQueryAllByA11yValue: ReactTestInstance[] = within(
331+
instance
332+
).queryAllByA11yValue({ min: 10 });

typings/index.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ export interface RenderOptions {
123123
createNodeMock?: (element: React.ReactElement<any>) => any;
124124
}
125125

126-
export interface RenderAPI extends GetByAPI, QueryByAPI, A11yAPI {
126+
type Queries = GetByAPI & QueryByAPI & A11yAPI;
127+
128+
export interface RenderAPI extends Queries {
127129
update(nextElement: React.ReactElement<any>): void;
128130
rerender(nextElement: React.ReactElement<any>): void;
129131
unmount(nextElement?: React.ReactElement<any>): void;
@@ -175,3 +177,4 @@ export declare const debug: DebugAPI;
175177
export declare const fireEvent: FireEventAPI;
176178
export declare const waitForElement: WaitForElementFunction;
177179
export declare const act: (callback: () => void) => Thenable;
180+
export declare const within: (instance: ReactTestInstance) => Queries;

website/docs/API.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,34 @@ test('waiting for an Banana to be ready', async () => {
341341

342342
If you're using Jest's [Timer Mocks](https://jestjs.io/docs/en/timer-mocks#docsNav), remember not to use `async/await` syntax as it will stall your tests.
343343

344+
## `within`
345+
346+
- [`Example code`](https://github.com/callstack/react-native-testing-library/blob/master/src/__tests__/within.test.js)
347+
348+
Defined as:
349+
350+
```jsx
351+
function within(instance: ReactTestInstance): Queries
352+
```
353+
354+
Perform [queries](./Queries.md) scoped to given element.
355+
356+
:::note
357+
Please note that additional `render` specific operations like `update`, `unmount`, `debug`, `toJSON` are _not_ included.
358+
:::
359+
360+
```jsx
361+
const detailsScreen = within(getByA11yHint('Details Screen'));
362+
expect(detailsScreen.getByText('Some Text')).toBeTruthy();
363+
expect(detailsScreen.getByDisplayValue('Some Value')).toBeTruthy();
364+
expect(detailsScreen.getByA11yLabel('Some Label')).toBeTruthy();
365+
expect(detailsScreen.getByA11yHint('Some Label')).toBeTruthy();
366+
```
367+
368+
Use cases for scoped queries include:
369+
* queries scoped to a single item inside a FlatList containing many items
370+
* queries scoped to a single screen in tests involving screen transitions (e.g. with react-navigation)
371+
344372
## `debug`
345373

346374
- [`Example code`](https://github.com/callstack/react-native-testing-library/blob/master/src/__tests__/debug.test.js)

0 commit comments

Comments
 (0)