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 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
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 (
<>
<div id="Michelle" />
<MyComponent name="Gord" />
</>
);
}

const wrapper = mount(<Wrapper />);
const divElement = wrapper.findWhere<'div'>(
(node) => node.is('div') && node.prop('id').startsWith('M'),
);

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

expect(divElement.prop('id')).toBe('Michelle');
expect(componentElement.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