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>
This commit is contained in:
Maciej Jastrzebski
2020-05-14 12:51:19 +02:00
committed by GitHub
parent 51bd418ba7
commit 1809825313
7 changed files with 231 additions and 1 deletions
+1
View File
@@ -85,6 +85,7 @@ The [public API](https://callstack.github.io/react-native-testing-library/docs/a
- [`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.
- [`fireEvent`](https://callstack.github.io/react-native-testing-library/docs/api#fireevent) - invokes named event handler on the element.
- [`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.
- [`within`](https://callstack.github.io/react-native-testing-library/docs/api#within) - creates a queries object scoped for given element.
- [`flushMicrotasksQueue`](https://callstack.github.io/react-native-testing-library/docs/api#flushmicrotasksqueue) - waits for microtasks queue to flush.
**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.
+71
View File
@@ -0,0 +1,71 @@
// @flow
import React from 'react';
import { View, Text, TextInput } from 'react-native';
import { render, within } from '..';
test('within() exposes basic queries', () => {
const rootQueries = render(
<View>
<View accessibilityHint="first">
<Text>Same Text</Text>
<TextInput value="Same Value" placeholder="Same Placeholder" />
</View>
<View accessibilityHint="second">
<Text>Same Text</Text>
<TextInput value="Same Value" placeholder="Same Placeholder" />
</View>
</View>
);
expect(rootQueries.getAllByText('Same Text')).toHaveLength(2);
expect(rootQueries.getAllByDisplayValue('Same Value')).toHaveLength(2);
expect(rootQueries.getAllByPlaceholder('Same Placeholder')).toHaveLength(2);
const firstQueries = within(rootQueries.getByA11yHint('first'));
expect(firstQueries.getAllByText('Same Text')).toHaveLength(1);
expect(firstQueries.getByText('Same Text')).toBeTruthy();
expect(firstQueries.queryAllByText('Same Text')).toHaveLength(1);
expect(firstQueries.queryByText('Same Text')).toBeTruthy();
expect(firstQueries.getByDisplayValue('Same Value')).toBeTruthy();
expect(firstQueries.getByPlaceholder('Same Placeholder')).toBeTruthy();
const secondQueries = within(rootQueries.getByA11yHint('second'));
expect(secondQueries.getAllByText('Same Text')).toHaveLength(1);
expect(secondQueries.getByText('Same Text')).toBeTruthy();
expect(secondQueries.queryAllByText('Same Text')).toHaveLength(1);
expect(secondQueries.queryByText('Same Text')).toBeTruthy();
expect(secondQueries.getByDisplayValue('Same Value')).toBeTruthy();
expect(secondQueries.getByPlaceholder('Same Placeholder')).toBeTruthy();
});
test('within() exposes a11y queries', () => {
const rootQueries = render(
<View>
<View accessibilityHint="first">
<TextInput
value="Same Value"
accessibilityLabel="Same Label"
accessibilityHint="Same Hint"
/>
</View>
<View accessibilityHint="second">
<TextInput
value="Same Value"
accessibilityLabel="Same Label"
accessibilityHint="Same Hint"
/>
</View>
</View>
);
expect(rootQueries.getAllByA11yLabel('Same Label')).toHaveLength(2);
expect(rootQueries.getAllByA11yHint('Same Hint')).toHaveLength(2);
const firstQueries = within(rootQueries.getByA11yHint('first'));
expect(firstQueries.getByA11yLabel('Same Label')).toBeTruthy();
expect(firstQueries.getByA11yHint('Same Hint')).toBeTruthy();
const secondQueries = within(rootQueries.getByA11yHint('second'));
expect(secondQueries.getAllByA11yLabel('Same Label')).toHaveLength(1);
expect(secondQueries.getAllByA11yHint('Same Hint')).toHaveLength(1);
});
+2
View File
@@ -7,6 +7,7 @@ import flushMicrotasksQueue from './flushMicrotasksQueue';
import render from './render';
import shallow from './shallow';
import waitForElement from './waitForElement';
import within from './within';
export { act };
export { cleanup };
@@ -16,3 +17,4 @@ export { flushMicrotasksQueue };
export { render };
export { shallow };
export { waitForElement };
export { within };
+12
View File
@@ -0,0 +1,12 @@
// @flow
import { getByAPI } from './helpers/getByAPI';
import { queryByAPI } from './helpers/queryByAPI';
import a11yAPI from './helpers/a11yAPI';
export default function within(instance: ReactTestInstance) {
return {
...getByAPI(instance),
...queryByAPI(instance),
...a11yAPI(instance),
};
}
+113
View File
@@ -9,6 +9,7 @@ import {
debug,
waitForElement,
act,
within,
} from '../..';
interface HasRequiredProp {
@@ -217,3 +218,115 @@ const waitByAll: Promise<Array<ReactTestInstance>> = waitForElement<
act(() => {
render(<TestComponent />);
});
// within API
const instance: ReactTestInstance = tree.getByText('<View />');
const withinGetByText: ReactTestInstance = within(instance).getByText('Test');
const withinGetAllByText: ReactTestInstance[] = within(instance).getAllByText(
'Test'
);
const withinGetByDisplayValue: ReactTestInstance = within(
instance
).getByDisplayValue('Test');
const withinGetAllByDisplayValue: ReactTestInstance[] = within(
instance
).getAllByDisplayValue('Test');
const withinGetByPlaceholder: ReactTestInstance = within(
instance
).getByPlaceholder('Test');
const withinGetAllByPlaceholder: ReactTestInstance[] = within(
instance
).getAllByPlaceholder('Test');
const withinGetByTestId: ReactTestInstance = within(instance).getByTestId(
'Test'
);
const withinGetAllByTestId: ReactTestInstance[] = within(
instance
).getAllByTestId('Test');
const withinQueryByText: ReactTestInstance | null = within(
instance
).queryByText('Test');
const withinQueryAllByText: ReactTestInstance[] = within(
instance
).queryAllByText('Test');
const withinQueryByDisplayValue: ReactTestInstance | null = within(
instance
).queryByDisplayValue('Test');
const withinQueryAllByDisplayValue: ReactTestInstance[] = within(
instance
).queryAllByDisplayValue('Test');
const withinQueryByPlaceholder: ReactTestInstance | null = within(
instance
).queryByPlaceholder('Test');
const withinQueryAllByPlaceholder: ReactTestInstance[] = within(
instance
).queryAllByPlaceholder('Test');
const withinQueryByTestId: ReactTestInstance | null = within(
instance
).queryByTestId('Test');
const withinQueryAllByTestId: ReactTestInstance[] = within(
instance
).queryAllByTestId('Test');
const withinGetByA11yLabel: ReactTestInstance = within(instance).getByA11yLabel(
'Test'
);
const withinGetAllByA11yLabel: ReactTestInstance[] = within(
instance
).getAllByA11yLabel('Test');
const withinGetByA11yHint: ReactTestInstance = within(instance).getByA11yHint(
'Test'
);
const withinGetAllByA11yHint: ReactTestInstance[] = within(
instance
).getAllByA11yHint('Test');
const withinGetByA11yRole: ReactTestInstance = within(instance).getByA11yRole(
'button'
);
const withinGetAllByA11yRole: ReactTestInstance[] = within(
instance
).getAllByA11yRole('button');
const withinGetByA11yState: ReactTestInstance = within(
instance
).getByA11yState({ busy: true });
const withinGetAllByA11yState: ReactTestInstance[] = within(
instance
).getAllByA11yState({ busy: true });
const withinGetByA11yValue: ReactTestInstance = within(
instance
).getByA11yValue({ min: 10 });
const withinGetAllByA11yValue: ReactTestInstance[] = within(
instance
).getAllByA11yValue({ min: 10 });
const withinQueryByA11yLabel: ReactTestInstance | null = within(
instance
).queryByA11yLabel('Test');
const withinQueryAllByA11yLabel: ReactTestInstance[] = within(
instance
).queryAllByA11yLabel('Test');
const withinQueryByA11yHint: ReactTestInstance | null = within(
instance
).queryByA11yHint('Test');
const withinQueryAllByA11yHint: ReactTestInstance[] = within(
instance
).queryAllByA11yHint('Test');
const withinQueryByA11yRole: ReactTestInstance | null = within(
instance
).queryByA11yRole('button');
const withinQueryAllByA11yRole: ReactTestInstance[] = within(
instance
).queryAllByA11yRole('button');
const withinQueryByA11yState: ReactTestInstance | null = within(
instance
).queryByA11yState({ busy: true });
const withinQueryAllByA11yState: ReactTestInstance[] = within(
instance
).queryAllByA11yState({ busy: true });
const withinQueryByA11yValue: ReactTestInstance | null = within(
instance
).queryByA11yValue({ min: 10 });
const withinQueryAllByA11yValue: ReactTestInstance[] = within(
instance
).queryAllByA11yValue({ min: 10 });
+4 -1
View File
@@ -123,7 +123,9 @@ export interface RenderOptions {
createNodeMock?: (element: React.ReactElement<any>) => any;
}
export interface RenderAPI extends GetByAPI, QueryByAPI, A11yAPI {
type Queries = GetByAPI & QueryByAPI & A11yAPI;
export interface RenderAPI extends Queries {
update(nextElement: React.ReactElement<any>): void;
rerender(nextElement: React.ReactElement<any>): void;
unmount(nextElement?: React.ReactElement<any>): void;
@@ -175,3 +177,4 @@ export declare const debug: DebugAPI;
export declare const fireEvent: FireEventAPI;
export declare const waitForElement: WaitForElementFunction;
export declare const act: (callback: () => void) => Thenable;
export declare const within: (instance: ReactTestInstance) => Queries;
+28
View File
@@ -341,6 +341,34 @@ test('waiting for an Banana to be ready', async () => {
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.
## `within`
- [`Example code`](https://github.com/callstack/react-native-testing-library/blob/master/src/__tests__/within.test.js)
Defined as:
```jsx
function within(instance: ReactTestInstance): Queries
```
Perform [queries](./Queries.md) scoped to given element.
:::note
Please note that additional `render` specific operations like `update`, `unmount`, `debug`, `toJSON` are _not_ included.
:::
```jsx
const detailsScreen = within(getByA11yHint('Details Screen'));
expect(detailsScreen.getByText('Some Text')).toBeTruthy();
expect(detailsScreen.getByDisplayValue('Some Value')).toBeTruthy();
expect(detailsScreen.getByA11yLabel('Some Label')).toBeTruthy();
expect(detailsScreen.getByA11yHint('Some Label')).toBeTruthy();
```
Use cases for scoped queries include:
* queries scoped to a single item inside a FlatList containing many items
* queries scoped to a single screen in tests involving screen transitions (e.g. with react-navigation)
## `debug`
- [`Example code`](https://github.com/callstack/react-native-testing-library/blob/master/src/__tests__/debug.test.js)