diff --git a/src/queries/__tests__/predicate.test.tsx b/src/queries/__tests__/predicate.test.tsx
new file mode 100644
index 000000000..764c89aad
--- /dev/null
+++ b/src/queries/__tests__/predicate.test.tsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import { View, Text, TextInput, Button } from 'react-native';
+import { ReactTestInstance } from 'react-test-renderer';
+import { render } from '../..';
+
+test('getByPredicate returns only native elements', () => {
+ const testIdPredicate = (testID: string) => (element: ReactTestInstance) => {
+ return element.props.testID === testID;
+ };
+
+ const textInputPredicate = function matchTextInput(
+ element: ReactTestInstance
+ ) {
+ // @ts-expect-error - ReatTestInstance type is missing host element typing
+ return element.type === 'TextInput';
+ };
+
+ const { getByPredicate, getAllByPredicate } = render(
+
+ Text
+
+
+
+
+ );
+
+ expect(getByPredicate(testIdPredicate('text'))).toBeTruthy();
+ expect(getByPredicate(testIdPredicate('textInput'))).toBeTruthy();
+ expect(getByPredicate(testIdPredicate('view'))).toBeTruthy();
+ expect(getByPredicate(testIdPredicate('button'))).toBeTruthy();
+
+ expect(getAllByPredicate(testIdPredicate('text'))).toHaveLength(1);
+ expect(getAllByPredicate(testIdPredicate('textInput'))).toHaveLength(1);
+ expect(getAllByPredicate(testIdPredicate('view'))).toHaveLength(1);
+ expect(getAllByPredicate(testIdPredicate('button'))).toHaveLength(1);
+
+ expect(getByPredicate(textInputPredicate)).toBeTruthy();
+});
+
+test('getByPredicate error messages', () => {
+ function hasStylePredicate(element: ReactTestInstance) {
+ return element.props.style !== undefined;
+ }
+
+ const textInputPredicate = function textInputPredicate(
+ element: ReactTestInstance
+ ) {
+ // @ts-expect-error - ReatTestInstance type is missing host element typing
+ return element.type === 'TextInput';
+ };
+
+ const testIdPredicate = (testID: string) => (element: ReactTestInstance) => {
+ return element.props.testID === testID;
+ };
+
+ const { getByPredicate, getAllByPredicate } = render(
+
+ Text
+
+ );
+
+ expect(() => getByPredicate(hasStylePredicate))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element matching predicate: function hasStylePredicate(element) {
+ return element.props.style !== undefined;
+ }"
+ `);
+
+ expect(() => getByPredicate(textInputPredicate))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element matching predicate: function textInputPredicate(element) {
+ // @ts-expect-error - ReatTestInstance type is missing host element typing
+ return element.type === 'TextInput';
+ }"
+ `);
+
+ expect(() => getByPredicate(testIdPredicate('myComponent')))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element matching predicate: element => {
+ return element.props.testID === testID;
+ }"
+ `);
+ expect(() => getAllByPredicate(testIdPredicate('myComponent')))
+ .toThrowErrorMatchingInlineSnapshot(`
+ "Unable to find an element matching predicate: element => {
+ return element.props.testID === testID;
+ }"
+ `);
+});
diff --git a/src/queries/predicate.ts b/src/queries/predicate.ts
new file mode 100644
index 000000000..50a31522f
--- /dev/null
+++ b/src/queries/predicate.ts
@@ -0,0 +1,63 @@
+import type { ReactTestInstance } from 'react-test-renderer';
+import { isHostElement } from '../helpers/component-tree';
+import { findAll } from '../helpers/findAll';
+import { makeQueries } from './makeQueries';
+import type {
+ FindAllByQuery,
+ FindByQuery,
+ GetAllByQuery,
+ GetByQuery,
+ QueryAllByQuery,
+ QueryByQuery,
+} from './makeQueries';
+import { CommonQueryOptions } from './options';
+
+type PredicateFn = (instance: ReactTestInstance) => boolean;
+type ByPredicateQueryOptions = CommonQueryOptions;
+
+function queryAllByPredicate(instance: ReactTestInstance) {
+ return function queryAllByPredicateFn(
+ predicate: PredicateFn,
+ options?: ByPredicateQueryOptions
+ ): Array {
+ const results = findAll(
+ instance,
+ (node) => isHostElement(node) && predicate(node),
+ options
+ );
+
+ return results;
+ };
+}
+
+const getMultipleError = (predicate: PredicateFn) =>
+ `Found multiple elements matching predicate: ${predicate}`;
+
+const getMissingError = (predicate: PredicateFn) =>
+ `Unable to find an element matching predicate: ${predicate}`;
+
+const { getBy, getAllBy, queryBy, queryAllBy, findBy, findAllBy } = makeQueries(
+ queryAllByPredicate,
+ getMissingError,
+ getMultipleError
+);
+
+export type ByTestIdQueries = {
+ getByPredicate: GetByQuery;
+ getAllByPredicate: GetAllByQuery;
+ queryByPredicate: QueryByQuery;
+ queryAllByPredicate: QueryAllByQuery;
+ findByPredicate: FindByQuery;
+ findAllByPredicate: FindAllByQuery;
+};
+
+export const bindByPredicateQueries = (
+ instance: ReactTestInstance
+): ByTestIdQueries => ({
+ getByPredicate: getBy(instance),
+ getAllByPredicate: getAllBy(instance),
+ queryByPredicate: queryBy(instance),
+ queryAllByPredicate: queryAllBy(instance),
+ findByPredicate: findBy(instance),
+ findAllByPredicate: findAllBy(instance),
+});
diff --git a/src/screen.ts b/src/screen.ts
index 34a1504d5..987fb7faf 100644
--- a/src/screen.ts
+++ b/src/screen.ts
@@ -107,6 +107,12 @@ const defaultScreen: RenderResult = {
queryAllByText: notImplemented,
findByText: notImplemented,
findAllByText: notImplemented,
+ getByPredicate: notImplemented,
+ getAllByPredicate: notImplemented,
+ queryByPredicate: notImplemented,
+ queryAllByPredicate: notImplemented,
+ findByPredicate: notImplemented,
+ findAllByPredicate: notImplemented,
};
export let screen: RenderResult = defaultScreen;
diff --git a/src/within.ts b/src/within.ts
index d4d36751f..ae8587388 100644
--- a/src/within.ts
+++ b/src/within.ts
@@ -10,6 +10,7 @@ import { bindByA11yStateQueries } from './queries/a11yState';
import { bindByA11yValueQueries } from './queries/a11yValue';
import { bindUnsafeByTypeQueries } from './queries/unsafeType';
import { bindUnsafeByPropsQueries } from './queries/unsafeProps';
+import { bindByPredicateQueries } from './queries/predicate';
export function within(instance: ReactTestInstance) {
return {
@@ -22,6 +23,7 @@ export function within(instance: ReactTestInstance) {
...bindByRoleQueries(instance),
...bindByA11yStateQueries(instance),
...bindByA11yValueQueries(instance),
+ ...bindByPredicateQueries(instance),
...bindUnsafeByTypeQueries(instance),
...bindUnsafeByPropsQueries(instance),
};
diff --git a/typings/index.flow.js b/typings/index.flow.js
index 2bbf303c7..3387c796e 100644
--- a/typings/index.flow.js
+++ b/typings/index.flow.js
@@ -389,6 +389,38 @@ interface A11yAPI {
) => FindAllReturn;
}
+type PredicateFn = (node: ReactTestInstance) => boolean;
+type ByPredicateOptions = CommonQueryOptions;
+
+interface ByPredicateQueries {
+ getByPredicate: (
+ predicate: PredicateFn,
+ options?: ByPredicateOptions
+ ) => ReactTestInstance;
+ getAllByPredicate: (
+ predicate: PredicateFn,
+ options?: ByPredicateOptions
+ ) => Array;
+ queryByPredicate: (
+ predicate: PredicateFn,
+ options?: ByPredicateOptions
+ ) => ReactTestInstance | null;
+ queryAllByPredicate: (
+ predicate: PredicateFn,
+ options?: ByPredicateOptions
+ ) => Array | [];
+ findByPredicate: (
+ predicate: PredicateFn,
+ queryOptions?: ByPredicateOptions,
+ waitForOptions?: WaitForOptions
+ ) => FindReturn;
+ findAllByPredicate: (
+ predicate: PredicateFn,
+ queryOptions?: ByPredicateOptions,
+ waitForOptions?: WaitForOptions
+ ) => FindAllReturn;
+}
+
interface Thenable {
then: (resolve: () => any, reject?: () => any) => any;
}
@@ -412,6 +444,7 @@ type Queries = ByTextQueries &
ByTestIdQueries &
ByDisplayValueQueries &
ByPlaceholderTextQueries &
+ ByPredicateQueries &
UnsafeByTypeQueries &
UnsafeByPropsQueries &
A11yAPI;
diff --git a/website/docs/Queries.md b/website/docs/Queries.md
index 7ae87cbc3..b1ec65252 100644
--- a/website/docs/Queries.md
+++ b/website/docs/Queries.md
@@ -26,6 +26,7 @@ title: Queries
- [Default state for: `disabled`, `selected`, and `busy` keys](#default-state-for-disabled-selected-and-busy-keys)
- [Default state for: `checked` and `expanded` keys](#default-state-for-checked-and-expanded-keys)
- [`ByA11Value`, `ByAccessibilityValue`](#bya11value-byaccessibilityvalue)
+ - [`ByPredicate`](#bypredicate)
- [Common options](#common-options)
- [`includeHiddenElements` option](#includehiddenelements-option)
- [TextMatch](#textmatch)
@@ -391,6 +392,23 @@ const element = screen.getByA11yValue({ now: 25 });
const element2 = screen.getByA11yValue({ text: /25/ });
```
+### `ByPredicate`
+
+> getByPredicate, getAllByPredicate, queryByPredicate, queryAllByPredicate, findByPredicate, findAllByPredicate
+
+```ts
+getByPredicate(
+ predicate: (element: ReactTestInstance) => boolean,
+ options?: {
+ includeHiddenElements?: boolean;
+ }
+): ReactTestInstance;
+```
+
+Returns a host element matching a custom `predicate` function.
+
+This query type is an escape hatch and should be used with care. In most cases you using the standard queries like `getByRole` or `getByText` will lead to test more-resembling user perspective. Use this query with care in rare cases where more flexibility is needed.
+
## Common options
### `includeHiddenElements` option