chore: restructure ByTestId queries code by predicate type (#608)

* refactor: add makeQueries functions

* refactor: use new query builders on byTestId

* rebase and rearrange to a bit

* add fixme

* refactor: make single function makeQueries

* doc: add TODO to do before end of pr

* fix: fix flow issue

* parametrize Queries

* chore: delete duplicated test

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
This commit is contained in:
MattAgn
2021-01-13 12:28:47 +01:00
committed by GitHub
parent 199de62c74
commit d71a7d8afc
9 changed files with 336 additions and 179 deletions
+173
View File
@@ -0,0 +1,173 @@
// @flow
import React from 'react';
import { View, Text, TextInput, TouchableOpacity, Button } from 'react-native';
import { render } from '..';
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
const INPUT_FRESHNESS = 'Custom Freshie';
const INPUT_CHEF = 'I inspected freshie';
class MyButton extends React.Component<any> {
render() {
return (
<TouchableOpacity onPress={this.props.onPress}>
<Text>{this.props.children}</Text>
</TouchableOpacity>
);
}
}
class Banana extends React.Component<any, any> {
state = {
fresh: false,
};
componentDidUpdate() {
if (this.props.onUpdate) {
this.props.onUpdate();
}
}
componentWillUnmount() {
if (this.props.onUnmount) {
this.props.onUnmount();
}
}
changeFresh = () => {
this.setState((state) => ({
fresh: !state.fresh,
}));
};
render() {
const test = 0;
return (
<View>
<Text>Is the banana fresh?</Text>
<Text testID="bananaFresh">
{this.state.fresh ? 'fresh' : 'not fresh'}
</Text>
<TextInput
testID="bananaCustomFreshness"
placeholder={PLACEHOLDER_FRESHNESS}
value={INPUT_FRESHNESS}
/>
<TextInput
testID="bananaChef"
placeholder={PLACEHOLDER_CHEF}
value={INPUT_CHEF}
/>
<MyButton onPress={this.changeFresh} type="primary">
Change freshness!
</MyButton>
<Text testID="duplicateText">First Text</Text>
<Text testID="duplicateText">Second Text</Text>
<Text>{test}</Text>
</View>
);
}
}
const MyComponent = () => {
return <Text>My Component</Text>;
};
test('getByTestId returns only native elements', () => {
const { getByTestId, getAllByTestId } = render(
<View>
<Text testID="text">Text</Text>
<TextInput testID="textInput" />
<View testID="view" />
<Button testID="button" title="Button" onPress={jest.fn()} />
<MyComponent testID="myComponent" />
</View>
);
expect(getByTestId('text')).toBeTruthy();
expect(getByTestId('textInput')).toBeTruthy();
expect(getByTestId('view')).toBeTruthy();
expect(getByTestId('button')).toBeTruthy();
expect(getAllByTestId('text')).toHaveLength(1);
expect(getAllByTestId('textInput')).toHaveLength(1);
expect(getAllByTestId('view')).toHaveLength(1);
expect(getAllByTestId('button')).toHaveLength(1);
expect(() => getByTestId('myComponent')).toThrowError(
'Unable to find an element with testID: myComponent'
);
expect(() => getAllByTestId('myComponent')).toThrowError(
'Unable to find an element with testID: myComponent'
);
});
test('supports a regex matcher', () => {
const { getByTestId, getAllByTestId } = render(
<View>
<Text testID="text">Text</Text>
<TextInput testID="textInput" />
<View testID="view" />
<Button testID="button" title="Button" onPress={jest.fn()} />
<MyComponent testID="myComponent" />
</View>
);
expect(getByTestId(/view/)).toBeTruthy();
expect(getAllByTestId(/text/)).toHaveLength(2);
});
test('getByTestId, queryByTestId', () => {
const { getByTestId, queryByTestId } = render(<Banana />);
const component = getByTestId('bananaFresh');
expect(component.props.children).toBe('not fresh');
expect(() => getByTestId('InExistent')).toThrow(
'Unable to find an element with testID: InExistent'
);
expect(getByTestId('bananaFresh')).toBe(component);
expect(queryByTestId('InExistent')).toBeNull();
});
test('getAllByTestId, queryAllByTestId', () => {
const { getAllByTestId, queryAllByTestId } = render(<Banana />);
const textElements = getAllByTestId('duplicateText');
expect(textElements.length).toBe(2);
expect(textElements[0].props.children).toBe('First Text');
expect(textElements[1].props.children).toBe('Second Text');
expect(() => getAllByTestId('nonExistentTestId')).toThrow(
'Unable to find an element with testID: nonExistentTestId'
);
const queriedTextElements = queryAllByTestId('duplicateText');
expect(queriedTextElements.length).toBe(2);
expect(queriedTextElements[0]).toBe(textElements[0]);
expect(queriedTextElements[1]).toBe(textElements[1]);
expect(queryAllByTestId('nonExistentTestId')).toHaveLength(0);
});
test('findByTestId and findAllByTestId work asynchronously', async () => {
const options = { timeout: 10 }; // Short timeout so that this test runs quickly
const { rerender, findByTestId, findAllByTestId } = render(<View />);
await expect(findByTestId('aTestId', options)).rejects.toBeTruthy();
await expect(findAllByTestId('aTestId', options)).rejects.toBeTruthy();
setTimeout(
() =>
rerender(
<View testID="aTestId">
<Text>Some Text</Text>
<TextInput placeholder="Placeholder Text" />
<TextInput value="Display Value" />
</View>
),
20
);
await expect(findByTestId('aTestId')).resolves.toBeTruthy();
await expect(findAllByTestId('aTestId')).resolves.toHaveLength(1);
}, 20000);
-6
View File
@@ -7,8 +7,6 @@ test('findBy queries work asynchronously', async () => {
const options = { timeout: 10 }; // Short timeout so that this test runs quickly
const {
rerender,
findByTestId,
findAllByTestId,
findByText,
findAllByText,
findByPlaceholderText,
@@ -16,8 +14,6 @@ test('findBy queries work asynchronously', async () => {
findByDisplayValue,
findAllByDisplayValue,
} = render(<View />);
await expect(findByTestId('aTestId', options)).rejects.toBeTruthy();
await expect(findAllByTestId('aTestId', options)).rejects.toBeTruthy();
await expect(findByText('Some Text', options)).rejects.toBeTruthy();
await expect(findAllByText('Some Text', options)).rejects.toBeTruthy();
await expect(
@@ -45,8 +41,6 @@ test('findBy queries work asynchronously', async () => {
20
);
await expect(findByTestId('aTestId')).resolves.toBeTruthy();
await expect(findAllByTestId('aTestId')).resolves.toHaveLength(1);
await expect(findByText('Some Text')).resolves.toBeTruthy();
await expect(findAllByText('Some Text')).resolves.toHaveLength(1);
await expect(findByPlaceholderText('Placeholder Text')).resolves.toBeTruthy();
-52
View File
@@ -1,52 +0,0 @@
// @flow
import * as React from 'react';
import { View, Text, TextInput, Button } from 'react-native';
import { render } from '..';
const MyComponent = () => {
return <Text>My Component</Text>;
};
test('getByTestId returns only native elements', () => {
const { getByTestId, getAllByTestId } = render(
<View>
<Text testID="text">Text</Text>
<TextInput testID="textInput" />
<View testID="view" />
<Button testID="button" title="Button" onPress={jest.fn()} />
<MyComponent testID="myComponent" />
</View>
);
expect(getByTestId('text')).toBeTruthy();
expect(getByTestId('textInput')).toBeTruthy();
expect(getByTestId('view')).toBeTruthy();
expect(getByTestId('button')).toBeTruthy();
expect(getAllByTestId('text')).toHaveLength(1);
expect(getAllByTestId('textInput')).toHaveLength(1);
expect(getAllByTestId('view')).toHaveLength(1);
expect(getAllByTestId('button')).toHaveLength(1);
expect(() => getByTestId('myComponent')).toThrowError(
'No instances found with testID: myComponent'
);
expect(() => getAllByTestId('myComponent')).toThrowError(
'No instances found with testID: myComponent'
);
});
test('supports a regex matcher', () => {
const { getByTestId, getAllByTestId } = render(
<View>
<Text testID="text">Text</Text>
<TextInput testID="textInput" />
<View testID="view" />
<Button testID="button" title="Button" onPress={jest.fn()} />
<MyComponent testID="myComponent" />
</View>
);
expect(getByTestId(/view/)).toBeTruthy();
expect(getAllByTestId(/text/)).toHaveLength(2);
});
-30
View File
@@ -79,36 +79,6 @@ class Banana extends React.Component<any, any> {
}
}
test('getByTestId, queryByTestId', () => {
const { getByTestId, queryByTestId } = render(<Banana />);
const component = getByTestId('bananaFresh');
expect(component.props.children).toBe('not fresh');
expect(() => getByTestId('InExistent')).toThrow('No instances found');
expect(getByTestId('bananaFresh')).toBe(component);
expect(queryByTestId('InExistent')).toBeNull();
});
test('getAllByTestId, queryAllByTestId', () => {
const { getAllByTestId, queryAllByTestId } = render(<Banana />);
const textElements = getAllByTestId('duplicateText');
expect(textElements.length).toBe(2);
expect(textElements[0].props.children).toBe('First Text');
expect(textElements[1].props.children).toBe('Second Text');
expect(() => getAllByTestId('nonExistentTestId')).toThrow(
'No instances found'
);
const queriedTextElements = queryAllByTestId('duplicateText');
expect(queriedTextElements.length).toBe(2);
expect(queriedTextElements[0]).toBe(textElements[0]);
expect(queriedTextElements[1]).toBe(textElements[1]);
expect(queryAllByTestId('nonExistentTestId')).toHaveLength(0);
});
test('UNSAFE_getAllByType, UNSAFE_queryAllByType', () => {
const { UNSAFE_getAllByType, UNSAFE_queryAllByType } = render(<Banana />);
const [text, status, button] = UNSAFE_getAllByType(Text);
+46
View File
@@ -0,0 +1,46 @@
// @flow
import { makeQueries } from './makeQueries';
import type { Queries } from './makeQueries';
const getNodeByTestId = (node, testID) => {
return typeof testID === 'string'
? testID === node.props.testID
: testID.test(node.props.testID);
};
const queryAllByTestId = (
instance: ReactTestInstance
): ((testId: string | RegExp) => Array<ReactTestInstance>) =>
function getAllByTestIdFn(testId) {
const results = instance
.findAll((node) => getNodeByTestId(node, testId))
.filter((element) => typeof element.type === 'string');
return results;
};
const getMultipleError = (testId) =>
`Found multiple elements with testID: ${String(testId)}`;
const getMissingError = (testId) =>
`Unable to find an element with testID: ${String(testId)}`;
const {
getBy: getByTestId,
getAllBy: getAllByTestId,
queryBy: queryByTestId,
findBy: findByTestId,
findAllBy: findAllByTestId,
}: Queries<string | RegExp> = makeQueries(
queryAllByTestId,
getMissingError,
getMultipleError
);
export {
getByTestId,
getAllByTestId,
queryByTestId,
findByTestId,
findAllByTestId,
queryAllByTestId,
};
+1 -22
View File
@@ -2,8 +2,6 @@
import waitFor from '../waitFor';
import type { WaitForOptions } from '../waitFor';
import {
getByTestId,
getAllByTestId,
getByText,
getAllByText,
getByPlaceholderText,
@@ -11,6 +9,7 @@ import {
getByDisplayValue,
getAllByDisplayValue,
} from './getByAPI';
import { findAllByTestId, findByTestId } from './byTestId';
import { throwRenamedFunctionError } from './errors';
export type FindByAPI = {|
@@ -57,26 +56,6 @@ const makeFindQuery = <Text, Result>(
waitForOptions: WaitForOptions
): Promise<Result> => waitFor(() => getQuery(instance)(text), waitForOptions);
export const findByTestId = (
instance: ReactTestInstance
): ((
testId: string | RegExp,
waitForOptions?: WaitForOptions
) => Promise<ReactTestInstance>) => (
testId: string | RegExp,
waitForOptions: WaitForOptions = {}
) => makeFindQuery(instance, getByTestId, testId, waitForOptions);
export const findAllByTestId = (
instance: ReactTestInstance
): ((
testId: string | RegExp,
waitForOptions?: WaitForOptions
) => Promise<Array<ReactTestInstance>>) => (
testId: string | RegExp,
waitForOptions: WaitForOptions = {}
) => makeFindQuery(instance, getAllByTestId, testId, waitForOptions);
export const findByText = (
instance: ReactTestInstance
): ((
+1 -44
View File
@@ -8,6 +8,7 @@ import {
throwRemovedFunctionError,
throwRenamedFunctionError,
} from './errors';
import { getAllByTestId, getByTestId } from './byTestId';
export type GetByAPI = {|
getByText: (text: string | RegExp) => ReactTestInstance,
@@ -117,12 +118,6 @@ const getTextInputNodeByDisplayValue = (node, value) => {
}
};
const getNodeByTestId = (node, testID) => {
return typeof testID === 'string'
? testID === node.props.testID
: testID.test(node.props.testID);
};
export const getByText = (
instance: ReactTestInstance
): ((text: string | RegExp) => ReactTestInstance) =>
@@ -169,27 +164,6 @@ export const getByDisplayValue = (
}
};
export const getByTestId = (
instance: ReactTestInstance
): ((testID: string | RegExp) => ReactTestInstance) =>
function getByTestIdFn(testID: string | RegExp) {
try {
const results = getAllByTestId(instance)(testID);
if (results.length === 1) {
return results[0];
} else {
throw new ErrorWithStack(
` Expected 1 but found ${
results.length
} instances with testID: ${String(testID)}`,
getByTestIdFn
);
}
} catch (error) {
throw new ErrorWithStack(prepareErrorMessage(error), getByTestIdFn);
}
};
export const getAllByText = (
instance: ReactTestInstance
): ((text: string | RegExp) => Array<ReactTestInstance>) =>
@@ -236,23 +210,6 @@ export const getAllByDisplayValue = (
return results;
};
export const getAllByTestId = (
instance: ReactTestInstance
): ((testID: string | RegExp) => Array<ReactTestInstance>) =>
function getAllByTestIdFn(testID: string | RegExp): ReactTestInstance[] {
const results = instance
.findAll((node) => getNodeByTestId(node, testID))
.filter((element) => typeof element.type === 'string');
if (results.length === 0) {
throw new ErrorWithStack(
`No instances found with testID: ${String(testID)}`,
getAllByTestIdFn
);
}
return results;
};
export const UNSAFE_getByType = (
instance: ReactTestInstance
): ((type: React.ComponentType<any>) => ReactTestInstance) =>
+114
View File
@@ -0,0 +1,114 @@
// @flow
import waitFor from '../waitFor';
import type { WaitForOptions } from '../waitFor';
import { ErrorWithStack } from './errors';
type QueryFunction<ArgType, ReturnType> = (
instance: ReactTestInstance
) => (args: ArgType) => ReturnType;
type FindQueryFunction<ArgType, ReturnType> = (
instance: ReactTestInstance
) => (args: ArgType, waitForOptions?: WaitForOptions) => Promise<ReturnType>;
type QueryAllByQuery<QueryArg> = QueryFunction<
QueryArg,
Array<ReactTestInstance>
>;
type QueryByQuery<QueryArg> = QueryFunction<QueryArg, null | ReactTestInstance>;
type GetAllByQuery<QueryArg> = QueryFunction<
QueryArg,
Array<ReactTestInstance>
>;
type GetByQuery<QueryArg> = QueryFunction<QueryArg, ReactTestInstance>;
type FindAllByQuery<QueryArg> = FindQueryFunction<
QueryArg,
Array<ReactTestInstance>
>;
type FindByQuery<QueryArg> = FindQueryFunction<QueryArg, ReactTestInstance>;
export type Queries<QueryArg> = {
getBy: GetByQuery<QueryArg>,
getAllBy: GetAllByQuery<QueryArg>,
queryBy: QueryByQuery<QueryArg>,
findBy: FindByQuery<QueryArg>,
findAllBy: FindAllByQuery<QueryArg>,
};
export function makeQueries<QueryArg>(
queryAllByQuery: QueryAllByQuery<QueryArg>,
getMissingError: (args: QueryArg) => string,
getMultipleError: (args: QueryArg) => string
): Queries<QueryArg> {
function getAllByQuery(instance: ReactTestInstance) {
return function getAllFn(args: QueryArg) {
const results = queryAllByQuery(instance)(args);
if (results.length === 0) {
throw new ErrorWithStack(getMissingError(args), getAllFn);
}
return results;
};
}
function queryByQuery(instance: ReactTestInstance) {
return function singleQueryFn(args: QueryArg) {
const results = queryAllByQuery(instance)(args);
if (results.length > 1) {
throw new ErrorWithStack(getMultipleError(args), singleQueryFn);
}
if (results.length === 0) {
return null;
}
return results[0];
};
}
function getByQuery(instance: ReactTestInstance) {
return function getFn(args: QueryArg) {
const results = queryAllByQuery(instance)(args);
if (results.length > 1) {
throw new ErrorWithStack(getMultipleError(args), getFn);
}
if (results.length === 0) {
throw new ErrorWithStack(getMissingError(args), getFn);
}
return results[0];
};
}
function findAllByQuery(instance: ReactTestInstance) {
return function findAllFn(
args: QueryArg,
waitForOptions?: WaitForOptions = {}
) {
return waitFor(() => getAllByQuery(instance)(args), waitForOptions);
};
}
function findByQuery(instance: ReactTestInstance) {
return function findFn(
args: QueryArg,
waitForOptions?: WaitForOptions = {}
) {
return waitFor(() => getByQuery(instance)(args), waitForOptions);
};
}
return {
getBy: getByQuery,
getAllBy: getAllByQuery,
queryBy: queryByQuery,
findBy: findByQuery,
findAllBy: findAllByQuery,
};
}
+1 -25
View File
@@ -1,11 +1,9 @@
// @flow
import * as React from 'react';
import {
getByTestId,
getByText,
getByPlaceholderText,
getByDisplayValue,
getAllByTestId,
getAllByText,
getAllByPlaceholderText,
getAllByDisplayValue,
@@ -14,6 +12,7 @@ import {
UNSAFE_getAllByType,
UNSAFE_getAllByProps,
} from './getByAPI';
import { queryByTestId, queryAllByTestId } from './byTestId';
import {
createQueryByError,
throwRemovedFunctionError,
@@ -89,17 +88,6 @@ export const queryByDisplayValue = (
}
};
export const queryByTestId = (
instance: ReactTestInstance
): ((testID: string | RegExp) => ReactTestInstance | null) =>
function queryByTestIdFn(testID: string | RegExp) {
try {
return getByTestId(instance)(testID);
} catch (error) {
return createQueryByError(error, queryByTestIdFn);
}
};
export const queryAllByText = (
instance: ReactTestInstance
): ((text: string | RegExp) => Array<ReactTestInstance>) => (
@@ -136,18 +124,6 @@ export const queryAllByDisplayValue = (
}
};
export const queryAllByTestId = (
instance: ReactTestInstance
): ((testID: string | RegExp) => Array<ReactTestInstance>) => (
testID: string | RegExp
) => {
try {
return getAllByTestId(instance)(testID);
} catch (error) {
return [];
}
};
export const UNSAFE_queryByType = (
instance: ReactTestInstance
): ((type: React.ComponentType<any>) => ReactTestInstance | null) =>