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

Enzyme instance() method should be able to carry type of wrapped class #26039

Closed
4 tasks done
amacleay opened this issue May 25, 2018 · 15 comments
Closed
4 tasks done

Enzyme instance() method should be able to carry type of wrapped class #26039

amacleay opened this issue May 25, 2018 · 15 comments

Comments

@amacleay
Copy link
Contributor

This is an issue to track the problem meant to be addressed by #18043

If you know how to fix the issue, make a pull request instead.

Issue

Calls to shallow(..).instance() should have the type of the component they wrap.

Example

(thanks to @mohsen1 - took straight from the test PR)

class MyComponentWithMethods extends React.Component<{}, {}> {
    someMethod() {
        return 'some string';
    }
}
function test_instance_method_inferring() {
    let wrapper = shallow(<MyComponentWithMethods />);
    let instance: MyComponentWithMethods = wrapper.instance();
    let b: string = wrapper.instance().someMethod();
}

Result:

Property 'someMethod' is missing in type 'Component<any, any, any>'.

Workaround: cast the output of instance

function test_instance_method_inferring() {
    let wrapper = shallow(<MyComponentWithMethods />);
    let instance = wrapper.instance() as MyComponentWithMethods;
    let b: string = wrapper.instance().someMethod();  // works
}

Possible solution: add a third property parameter to shallow

export function shallow<P>(node: ReactElement<P>, options?: ShallowRendererProps): ShallowWrapper<P, any>;
< export function shallow<P, S>(node: ReactElement<P>, options?: ShallowRendererProps): ShallowWrapper<P, S>;
> export function shallow<P, S, C = Component<P, S>>(node: ReactElement<P>, options?: ShallowRendererProps): ShallowWrapper<P, S, C>;

This would mean that the test above would pass with the following change:

function test_instance_method_inferring() {
    let wrapper = shallow<{}, {}, MyComponentWithMethods>(<MyComponentWithMethods />);
    let instance: MyComponentWithMethods = wrapper.instance();
}

I can try to put together a PR if that seems like a sensible idea.

@mohsen1
Copy link
Contributor

mohsen1 commented May 25, 2018

I like the idea of extra generic parameter but it should all be infered so users don't have to pass all those generic parameters

@astorije
Copy link
Contributor

@mohsen1, thanks for your answer!
That makes total sense, but I'm afraid both my and @amacleay's skills (we work together) haven't reached that far yet. Do you know how we could do this?

@mohsen1
Copy link
Contributor

mohsen1 commented May 25, 2018

Generic type parameters that are generics themselves work just fine in TypeScript. I have not looked into this particular issue but you should be able to make it work.

I think you're on the right track here. Please give it another shot and let me know if that didn't work :)

@jwbay
Copy link
Contributor

jwbay commented May 25, 2018

It's not possible to get the component type from a JSX expression because the return type of all JSX expressions is currently just JSX.Element, so inference is a no-go. This may be worth raising with the TS team.

The third instance type parameter isn't great because the first two are actually on that type. One option is to drop down to one parameter, the component type, and use index types to get props and state. It would be a fairly massive breaking change, but it seems to work pretty well, and could even give you methods off of .find(Component).instance():

declare class Test extends React.Component<{ a?: number }> {
  method(): void;
}

declare function shallow<C extends React.Component = React.Component>(node: JSX.Element): ShallowWrapper<C>;

interface ShallowWrapper<C extends React.Component> {
  props(): C['props'];
  state(): C['state'];
  instance(): C;
}

const wrapper = shallow<Test>(<Test />);

wrapper.props().a; // ok, number
wrapper.instance().method; // ok, () => void;

@mohsen1
Copy link
Contributor

mohsen1 commented May 26, 2018

JSX (React.crrateElement really) is breaking the types here. The only way to solve this is to have a runtime function in typescript that accepts the component class and props and will invoke React.createElement in it with shallow wrapped in it.

@amacleay
Copy link
Contributor Author

amacleay commented Jun 6, 2018

I can take a run at @jwbay 's suggestion in a PR if that seems sensible. It makes sense to me, at least.

@mohsen1 - is that really feasible? I'm not sure how such a thing would work.

@mohsen1
Copy link
Contributor

mohsen1 commented Jun 6, 2018

I mean something like this:

function shallow (componentClass, props) {
  return enzyme.shallow(<componentClass {...props} />)
}

@mohsen1
Copy link
Contributor

mohsen1 commented Jun 22, 2018

With @Hotell generic syntax you can have a runtime wrapper function that removes the need of casting or providing generic type:

import enzyme from 'enzyme';

function shallow<P = {}, C extends React.Component<P>>(ComponentClass: typeof C, props: P) {
  return enzyme.shallow<C>(<ComponentClass {...props} />)
}

with that, this should work:

class MyComponent extends React.Component<{foo: number> {
 render() { return null }
 someMethod() { }
}

swallow(MyComponent, {foo: 42}).instance().someMethod()

@Hotell
Copy link
Contributor

Hotell commented Jun 22, 2018

that is unnecessary boilerplate @mohsen1

all you need to do from now on is in demonstrated in test https://github.com/DefinitelyTyped/DefinitelyTyped/pull/26565/files#diff-2931280905f283a37c4795c53167755cR354

class MyComponentWithMethods extends React.Component<{}, {}> {
    someMethod() {
        return 'some string';
    }
}
test('instance type by providing component type explicitly works', () => {
    const wrapper = shallow<MyComponentWithMethods>(<MyComponentWithMethods />);
    expect(wrapper.instance().someMethod()).toBe('some string');
  }
}

@mohsen1
Copy link
Contributor

mohsen1 commented Jun 22, 2018

I mean if you don't want to repeat component type in the generic you can do that runtime function :)

@joshuatTW
Copy link

joshuatTW commented Jan 2, 2019

This works well with const. Does anyone know how to do this for an implicitly typed any let?

e.g.

let component;  // typed any
beforeEach(() => {
  component = mount<MyComponent>(<MyComponent>);
});

test('my test', () => {
 //component is still typed any
});

@mohsen1
Copy link
Contributor

mohsen1 commented Jan 3, 2019

You need to declare the type of component before assigning to it.

@P0ntiuS
Copy link

P0ntiuS commented Jan 10, 2020

Can this work for functional components?

Like

function MyComponentWithMethods(): JSX.Element {
    function onButtonClick(): void {
        console.log('Button clicked!');
    }
    return (<button onClick={onButtonClick}>Click me!</button>);
}

@jfirebaugh
Copy link
Contributor

It's not possible to get the component type from a JSX expression because the return type of all JSX expressions is currently just JSX.Element, so inference is a no-go. This may be worth raising with the TS team.

Relevant TypeScript issues:

@vorlov
Copy link

vorlov commented Oct 8, 2021

@joshua

This works well with const. Does anyone know how to do this for an implicitly typed any let?

e.g.

let component;  // typed any
beforeEach(() => {
  component = mount<MyComponent>(<MyComponent>);
});

test('my test', () => {
 //component is still typed any
});

let wrapper: ShallowWrapper<Readonly<YourComponentProps> & Readonly<{ children?: React.ReactNode; }>, Readonly<YourComponentState>, YourComponent>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants