Skip to content

within operator for bound queries #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
71 changes: 71 additions & 0 deletions src/__tests__/within.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could merge these 2 tests, but no biggie

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 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -16,3 +17,4 @@ export { flushMicrotasksQueue };
export { render };
export { shallow };
export { waitForElement };
export { within };
12 changes: 12 additions & 0 deletions src/within.js
Original file line number Diff line number Diff line change
@@ -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 changes: 113 additions & 0 deletions typings/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
debug,
waitForElement,
act,
within,
} from '../..';

interface HasRequiredProp {
Expand Down Expand Up @@ -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 });
5 changes: 4 additions & 1 deletion typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 changes: 28 additions & 0 deletions website/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down