diff --git a/README.md b/README.md index 6373816d4..b95b05d41 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/__tests__/within.test.js b/src/__tests__/within.test.js new file mode 100644 index 000000000..97bf5252b --- /dev/null +++ b/src/__tests__/within.test.js @@ -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( + + + Same Text + + + + Same Text + + + + ); + + 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( + + + + + + + + + ); + + 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); +}); diff --git a/src/index.js b/src/index.js index f4263745c..2e7ac9f20 100644 --- a/src/index.js +++ b/src/index.js @@ -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 }; diff --git a/src/within.js b/src/within.js new file mode 100644 index 000000000..a7dd00b40 --- /dev/null +++ b/src/within.js @@ -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), + }; +} diff --git a/typings/__tests__/index.test.tsx b/typings/__tests__/index.test.tsx index c4a5ad020..5e0db16f4 100644 --- a/typings/__tests__/index.test.tsx +++ b/typings/__tests__/index.test.tsx @@ -9,6 +9,7 @@ import { debug, waitForElement, act, + within, } from '../..'; interface HasRequiredProp { @@ -217,3 +218,115 @@ const waitByAll: Promise> = waitForElement< act(() => { render(); }); + +// within API +const instance: ReactTestInstance = tree.getByText(''); +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 }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 1c03ed410..4afc01665 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -123,7 +123,9 @@ export interface RenderOptions { createNodeMock?: (element: React.ReactElement) => any; } -export interface RenderAPI extends GetByAPI, QueryByAPI, A11yAPI { +type Queries = GetByAPI & QueryByAPI & A11yAPI; + +export interface RenderAPI extends Queries { update(nextElement: React.ReactElement): void; rerender(nextElement: React.ReactElement): void; unmount(nextElement?: React.ReactElement): 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; diff --git a/website/docs/API.md b/website/docs/API.md index 8cf47a27c..f3037d779 100644 --- a/website/docs/API.md +++ b/website/docs/API.md @@ -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)