Skip to content
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

chore: restructure ByTestId queries code by predicate type #608

Merged
merged 9 commits into from
Jan 13, 2021
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
173 changes: 173 additions & 0 deletions src/__tests__/byTestId.test.js
Original file line number Diff line number Diff line change
@@ -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 changes: 0 additions & 6 deletions src/__tests__/findByApi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ 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,
findAllByPlaceholderText,
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(
Expand Down Expand Up @@ -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();
Expand Down
52 changes: 0 additions & 52 deletions src/__tests__/getByApi.test.js

This file was deleted.

30 changes: 0 additions & 30 deletions src/__tests__/render.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
46 changes: 46 additions & 0 deletions src/helpers/byTestId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @flow
import { makeQueries } from './makeQueries';
import type { Queries } from './makeQueries';

const getNodeByTestId = (node, testID) => {
Copy link
Member

Choose a reason for hiding this comment

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

Consider adding explicit types

Copy link
Member

Choose a reason for hiding this comment

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

We can address that in a followup if necessary (I suspect Flow infers that, as it's internal fn)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I confirm, flow infers it
image

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,
};
23 changes: 1 addition & 22 deletions src/helpers/findByAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
import waitFor from '../waitFor';
import type { WaitForOptions } from '../waitFor';
import {
getByTestId,
getAllByTestId,
getByText,
getAllByText,
getByPlaceholderText,
getAllByPlaceholderText,
getByDisplayValue,
getAllByDisplayValue,
} from './getByAPI';
import { findAllByTestId, findByTestId } from './byTestId';
import { throwRenamedFunctionError } from './errors';

export type FindByAPI = {|
Expand Down Expand Up @@ -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
): ((
Expand Down
Loading