Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Add generics to findWhere and findWhereAll in react-testing #1999

Merged
merged 4 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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 packages/react-testing/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

### Changed

- Added ability to specify a generic when calling `findWhere` and `findWhereAll` [[#1999](https://github.com/Shopify/quilt/pull/1999)]
- Updated build tooling, types are now compiled with TypeScript 4.3. [[#1997](https://github.com/Shopify/quilt/pull/1997)]

## 3.2.2 - 2021-08-04
Expand Down
37 changes: 34 additions & 3 deletions packages/react-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,11 +468,42 @@ expect(wrapper.find(MyComponent, {name: 'Gord'})!.props).toMatchObject({

Like `find()`, but returns all matches as an array.

##### <a name="findWhere"></a> `findWhere(predicate: (element: Element<unknown>) => boolean): Element<unknown> | null`
##### <a name="findWhere"></a> `findWhere<Type = unknown>(predicate: (element: Element<unknown>) => boolean): Element<PropsForComponent<Type>> | null`

Finds the first descendant component matching the passed function. The function is called with each `Element` from [`descendants`](#descendants) until a match is found. If no match is found, `null` is returned.

##### <a name="findAllWhere"></a> `findAllWhere(predicate: (element: Element<unknown>) => boolean): Element<unknown>[]`
`findWhere` accepts an optional generic argument that can be used to specify the type of the returned element. This argument is either a string or a React component, the same as the first argument on `.find`. If the generic argument is omited then the returned element will have unknown props and thus calling `.props` and `.trigger` on it will cause type errors as those functions won't know what props are valid on your element:

```tsx
function MyComponent({name}: {name: string}) {
return <div>Hello, {name}!</div>;
}

function Wrapper() {
return (
<>
<MyComponent name="Michelle" />
Copy link
Contributor

Choose a reason for hiding this comment

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

😅

Copy link
Member Author

Choose a reason for hiding this comment

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

The find docs reference you and Gord, I'm just being consistent :p

<MyComponent name="Gord" />
</>
);
}

const wrapper = mount(<Wrapper />);
const startsWithM = wrapper.findWhere<typeof MyComponent>(
(node) => node.is(MyComponent) && node.prop('name').startsWith('M'),
);

const startsWithG = wrapper.findWhere<typeof MyComponent>(
(node) => node.is(MyComponent) && node.prop('name').startsWith('G'),
);

expect(startsWithM.prop('name')).toBe('Michelle');
expect(startsWithG.prop('name')).toBe('Gord');
```

````

##### <a name="findAllWhere"></a> `findAllWhere<Type = unknown>(predicate: (element: Element<unknown>) => boolean): Element<PropsForComponent<Type>>[]`

Like `findWhere`, but returns all matches as an array.

Expand Down Expand Up @@ -505,7 +536,7 @@ function Wrapper() {
const wrapper = mount(<Wrapper />);
wrapper.find(MyComponent)!.trigger('onClick', 'some-id');
expect(wrapper.find('div')!.text()).toContain('some-id');
```
````

##### <a name="triggerKeypath"></a> `triggerKeypath<T>(keypath: string, ...args: any[]): T`

Expand Down
18 changes: 12 additions & 6 deletions packages/react-testing/src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
FunctionKeys,
DeepPartialArguments,
PropsFor,
UnknowablePropsFor,
DebugOptions,
} from './types';

Expand Down Expand Up @@ -178,14 +179,19 @@ export class Element<Props> implements Node<Props> {
) as Element<PropsFor<Type>>[];
}

findWhere(predicate: Predicate): Element<unknown> | null {
return (
this.elementDescendants.find((element) => predicate(element)) || null
);
findWhere<Type extends React.ComponentType<any> | string | unknown = unknown>(
predicate: Predicate,
): Element<UnknowablePropsFor<Type>> | null {
return (this.elementDescendants.find((element) => predicate(element)) ||
null) as Element<UnknowablePropsFor<Type>> | null;
}

findAllWhere(predicate: Predicate): Element<unknown>[] {
return this.elementDescendants.filter((element) => predicate(element));
findAllWhere<
Type extends React.ComponentType<any> | string | unknown = unknown
>(predicate: Predicate): Element<UnknowablePropsFor<Type>>[] {
return this.elementDescendants.filter((element) =>
predicate(element),
) as Element<UnknowablePropsFor<Type>>[];
}

trigger<K extends FunctionKeys<Props>>(
Expand Down
12 changes: 8 additions & 4 deletions packages/react-testing/src/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,16 @@ export class Root<Props> implements Node<Props> {
return this.withRoot((root) => root.findAll(type, props));
}

findWhere(predicate: Predicate) {
return this.withRoot((root) => root.findWhere(predicate));
findWhere<Type extends React.ComponentType<any> | string | unknown = unknown>(
predicate: Predicate,
) {
return this.withRoot((root) => root.findWhere<Type>(predicate));
}

findAllWhere(predicate: Predicate) {
return this.withRoot((root) => root.findAllWhere(predicate));
findAllWhere<
Type extends React.ComponentType<any> | string | unknown = unknown
>(predicate: Predicate) {
return this.withRoot((root) => root.findAllWhere<Type>(predicate));
}

trigger<K extends FunctionKeys<Props>>(
Expand Down
4 changes: 4 additions & 0 deletions packages/react-testing/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export type PropsFor<
? React.ComponentPropsWithoutRef<T>
: never;

export type UnknowablePropsFor<
T extends string | React.ComponentType<any> | unknown
> = T extends string | React.ComponentType<any> ? PropsFor<T> : unknown;

export type FunctionKeys<T> = {
[K in keyof T]-?: NonNullable<T[K]> extends (...args: any[]) => any
? K
Expand Down