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

Support wrappingComponent & wrappingComponentProps #157

Merged

Conversation

kevinweber
Copy link
Contributor

@kevinweber kevinweber commented Feb 17, 2022

Support for wrappingComponent has been requested in #99 and this PR adds it for components rendered using the shallow and mount Enzyme utils.

How was this tested?

  1. I added tests to the project and all are passing.
  2. I verified that the changes in this PR solve the issue for my organization, allowing us to use wrappingComponent and wrappingComponentProps for both shallow and mount components.

Note for my future self: this is how I verified the changes locally…

  • Make sure yalc is installed (as recommended in the contributing guide): yarn global add yalc
  • Run yalc publish in the (forked) project locally.
  • Run this in the other project where the dependency should be tested: yalc add enzyme-adapter-preact-pure. This updates the package.json and lock file. Then run yarn install.
  • After making a change, run yalc publish --push and the other project will use the latest code

Tips for faster development:

  • Auto-run tests on file change: nodemon --watch "test/**" --watch "src/**" --ext "ts,tsx" --exec yarn test
  • Auto-publish on file change: nodemon --watch "src/**" --ext "ts" --exec "yalc publish --push --changed"

@robertknight
Copy link
Member

Thanks for the contribution. I'm happy to take a look in detail when this is out of draft. Please let me know if you run into any roadblocks along the way.

@kevinweber
Copy link
Contributor Author

kevinweber commented Feb 18, 2022

@robertknight Great, thank you!
I actually have a pretty big question: How to make contexts work? …for both shallow and mount rendering?
Even though my current solution works for some cases where the wrapper is fairly dumb, it doesn't work for those who provide context. Below example gives me the following error:

could not find react-redux context value; please ensure the component is wrapped in a <Provider>

export const ProviderWithRouter = ({ children }: { children: ReactNode }) => (
  <Provider store={store}>
    <MemoryRouter>{children}</MemoryRouter>
  </Provider>
);

expect(
  mount(<Sidebar match={matched} location={location} history={history} />, {
    wrappingComponent: ProviderWithRouter,
  }),
).toHaveLength(1);

@kevinweber kevinweber marked this pull request as ready for review February 18, 2022 16:53
@kevinweber
Copy link
Contributor Author

kevinweber commented Feb 18, 2022

@robertknight Alighty, it is done! Please take a look. I'm quite happy with the solution so far and it works with context providers. I sampled this change with several tests in my org and nearly all of them pass. Some tests don't pass because they do weird/deprecated stuff and those tests need to be refactored anyway.

Copy link
Member

@robertknight robertknight left a comment

Choose a reason for hiding this comment

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

I had a few small queries and comments. Generally this looks good though. Please let me know once you've had a chance to look through and make any changes, and then I'll get this shipped ASAP.

src/Adapter.ts Outdated
* Using those props complicates a potential future migration to a different testing library.
* Instead, wrap a component like this:
* ```
* shallow(<Wrapper><Component/></Wrapper>)
Copy link
Member

Choose a reason for hiding this comment

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

Would tests then use wrapper.dive() to get at the wrapped component?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. I verified that you need to add an additional .dive().

Before:

    const wrapperProps = {
      wrappingComponent: MyTestContext.Provider,
      wrappingComponentProps: { value: { isMyTest: true } },
    };

    const wrapper: ShallowWrapper<{ isMyTest: boolean }> = shallow(
       <MyTitle>sup</MyTitle>,
      wrapperProps,
    )
      .dive()
      .dive();

After:

    const wrapper: ShallowWrapper<{ isMyTest: boolean }> = shallow(
      <MyTestContext.Provider value={true}>
        <MyTitle>sup</MyTitle>
      </MyTestContext.Provider>,
      wrapperProps,
    )
      .dive()
      .dive()
      .dive();

options?: ShallowRendererProps
) => {
return {
RootFinder: RootFinder,
Copy link
Member

Choose a reason for hiding this comment

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

To check my understanding, RootFinder is essentially a marker to allow location of the wrapped component in the rendered tree?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I understand the Enzyme source code correctly, it's purpose is to properly pass down context, see here https://github.com/enzymejs/enzyme/blob/57baba5ceaccec7a3ada40d7559f5bf71289cbe7/packages/enzyme/src/ShallowWrapper.js#L356 and here https://github.com/enzymejs/enzyme/blob/57baba5ceaccec7a3ada40d7559f5bf71289cbe7/packages/enzyme/src/ShallowWrapper.js#L322. I tried what happens if I don't insert RootFinder into wrapWithWrappingComponent and it didn't work.

@@ -0,0 +1,7 @@
import { Component } from 'preact';

export default class RootFinder extends Component {
Copy link
Member

Choose a reason for hiding this comment

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

It would be useful to have a brief comment explaining what RootFinder is used for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated


let nodeWithValidChildren = node;

if (typeof nodeWithValidChildren.props.children === 'string') {
Copy link
Member

Choose a reason for hiding this comment

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

To summarize, this is normalizing VNodes like { type: Widget, props: { children: 'test' }, ... } to { type: Widget, props: { children: ['test'] } }?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

if (typeof nodeWithValidChildren.props.children === 'string') {
// This prevents an error when `.dive()` is used:
// `TypeError: ShallowWrapper::dive() can only be called on components`
nodeWithValidChildren = Object.assign({}, nodeWithValidChildren);
Copy link
Member

Choose a reason for hiding this comment

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

Object.assign does a shallow clone so nodeWithValidChildren.props refers to the same object as node.props. You can use cloneElement to clone the node in order to modify the props.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

.instance;
const expectedInstance = renderedTree[0].instance;

assert.equal(resultInstance.toString(), expectedInstance.toString());
Copy link
Member

Choose a reason for hiding this comment

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

Is the purpose of these asserts just to check that the rendered DOM structure is the same? If so comparing the outerHTML of the two instances with a single assert may be simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perfect 👍

Copy link
Contributor Author

@kevinweber kevinweber left a comment

Choose a reason for hiding this comment

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

@robertknight Thanks for the review! Very helpful. I followed up on your feedback. Additionally, I upgraded the Enzyme dependency to 3.11.0 because it allows diving into shallow components with context: enzymejs/enzyme#1966. Then I added three integration tests, one for mount and two for shallow.

One note: The change in this PR doesn't result in 100% matching behavior with the React adapter when wrappingComponent is used, in particular when it comes to shallow rendering. This might need follow-up enhancements to this library. So far, for the few cases where I found a shallow test to pass with React's but not Preact's adapter, a good solution was to migrate the test to using mount instead of shallow.

options?: ShallowRendererProps
) => {
return {
RootFinder: RootFinder,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I understand the Enzyme source code correctly, it's purpose is to properly pass down context, see here https://github.com/enzymejs/enzyme/blob/57baba5ceaccec7a3ada40d7559f5bf71289cbe7/packages/enzyme/src/ShallowWrapper.js#L356 and here https://github.com/enzymejs/enzyme/blob/57baba5ceaccec7a3ada40d7559f5bf71289cbe7/packages/enzyme/src/ShallowWrapper.js#L322. I tried what happens if I don't insert RootFinder into wrapWithWrappingComponent and it didn't work.

src/Adapter.ts Outdated
* Using those props complicates a potential future migration to a different testing library.
* Instead, wrap a component like this:
* ```
* shallow(<Wrapper><Component/></Wrapper>)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. I verified that you need to add an additional .dive().

Before:

    const wrapperProps = {
      wrappingComponent: MyTestContext.Provider,
      wrappingComponentProps: { value: { isMyTest: true } },
    };

    const wrapper: ShallowWrapper<{ isMyTest: boolean }> = shallow(
       <MyTitle>sup</MyTitle>,
      wrapperProps,
    )
      .dive()
      .dive();

After:

    const wrapper: ShallowWrapper<{ isMyTest: boolean }> = shallow(
      <MyTestContext.Provider value={true}>
        <MyTitle>sup</MyTitle>
      </MyTestContext.Provider>,
      wrapperProps,
    )
      .dive()
      .dive()
      .dive();

@@ -0,0 +1,7 @@
import { Component } from 'preact';

export default class RootFinder extends Component {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated


let nodeWithValidChildren = node;

if (typeof nodeWithValidChildren.props.children === 'string') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

if (typeof nodeWithValidChildren.props.children === 'string') {
// This prevents an error when `.dive()` is used:
// `TypeError: ShallowWrapper::dive() can only be called on components`
nodeWithValidChildren = Object.assign({}, nodeWithValidChildren);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

.instance;
const expectedInstance = renderedTree[0].instance;

assert.equal(resultInstance.toString(), expectedInstance.toString());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perfect 👍

@robertknight
Copy link
Member

So far, for the few cases where I found a shallow test to pass with React's but not Preact's adapter, a good solution was to migrate the test to using mount instead of shallow.

For new tests and when modernizing existing tests I would recommend using mount rendering, using module mocking or dependency injection if you want to stub out rendered components. I have a blog post with some notes on that.

Copy link
Member

@robertknight robertknight left a comment

Choose a reason for hiding this comment

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

Looks good to me. Thank-you for the contribution, including the extensive tests.

@robertknight robertknight merged commit 4d51be8 into preactjs:master Feb 24, 2022
@robertknight
Copy link
Member

Published as v3.4.0.

);
});

it('passes wrappingComponentProps to wrappingComponent', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: A bit late because this PR is already merged but: I just realized that this test is basically a duplicate of the previous test. I used to run different testing logic in here but after some refactor I hadn't realized that it has become a redundant test. I'd suggest removing it in a future update.

Copy link
Member

Choose a reason for hiding this comment

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

Would you mind submitting a small follow-up PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done: #158

@kevinweber kevinweber mentioned this pull request Feb 24, 2022
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

Successfully merging this pull request may close these issues.

2 participants