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 2 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<MyComponent>(
(node) => node.is(MyComponent) && node.prop('name').startsWith('M'),
);

const startsWithG = wrapper.findWhere<MyComponent>(
Copy link
Contributor

Choose a reason for hiding this comment

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

I am actually a little surprise its wrapper.findWhere<MyComponent> and not wrapper.findWhere<type of MyComponent>, the new version of TS got smarter again?

Copy link
Member Author

Choose a reason for hiding this comment

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

Excellent spot, the typeof totally needs to be there

Copy link
Contributor

Choose a reason for hiding this comment

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

lol it would be so nice if typescript is smart enough in the future to know it should use the typeof in generic

(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