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

bug: proxyCustomElement causing infinite loops with react testing library #3434

Closed
3 tasks done
liamdebeasi opened this issue Jun 23, 2022 · 17 comments
Closed
3 tasks done
Labels
Bug: Validated This PR or Issue is verified to be a bug within Stencil

Comments

@liamdebeasi
Copy link
Contributor

Prerequisites

Stencil Version

2.17.0

Current Behavior

Rendering multiple Stencil components wrapped as React components causes an PrettyFormatPluginError error when used with @testing-library/react. This error only reproduces in a test environment.

Expected Behavior

I expect there to be no errors when running the test.

Steps to Reproduce

  1. Clone the attached repo.
  2. Use the following script to install dependencies, build projects, and pack local dependencies:
cd sample-component-library && npm i && npm run build && npm pack
cd ../sample-component-library-react && npm i && npm i ../sample-component-library/sample-component-library-0.0.1.tgz && npm run build && npm pack
cd ../sample-app && npm i && npm i ../sample-component-library-react/component-library-react-0.0.1.tgz

This script does the following things:

a) Builds and packs the base Stencil component library in sample-component-library
b) Uses the packed sample-component-library to build React wrappers for the Stencil components in sample-component-library-react.
c) Uses the packed sample-component-library-react to consume the React wrappers in a React application named sample-app.

  1. In sample-app, run npm run test. Observe that the following error is logged:
 RangeError: Invalid string length
        at Array.join (<anonymous>)

       6 |   render(<App />);
       7 |   const linkElement = screen.getByText(/learn react/i);
    >  8 |   screen.debug();
         |          ^
       9 |   expect(linkElement).toBeInTheDocument();
      10 | });
      11 |

      at printChildren (node_modules/@testing-library/dom/dist/DOMElementFilter.js:52:4)
      at Object.serialize (node_modules/@testing-library/dom/dist/DOMElementFilter.js:139:75)
      at printPlugin (node_modules/pretty-format/build/index.js:330:16)
      at printer (node_modules/pretty-format/build/index.js:379:12)
      at node_modules/@testing-library/dom/dist/DOMElementFilter.js:44:79
          at Array.map (<anonymous>)
      at printChildren (node_modules/@testing-library/dom/dist/DOMElementFilter.js:43:89)
      at Object.serialize (node_modules/@testing-library/dom/dist/DOMElementFilter.js:139:75)
      at printPlugin (node_modules/pretty-format/build/index.js:330:16)
      at printer (node_modules/pretty-format/build/index.js:379:12)
      at node_modules/@testing-library/dom/dist/DOMElementFilter.js:44:79
          at Array.map (<anonymous>)
      at printChildren (node_modules/@testing-library/dom/dist/DOMElementFilter.js:43:89)
      at Object.serialize (node_modules/@testing-library/dom/dist/DOMElementFilter.js:139:75)
      at printPlugin (node_modules/pretty-format/build/index.js:330:16)
      at printer (node_modules/pretty-format/build/index.js:379:12)
      at node_modules/@testing-library/dom/dist/DOMElementFilter.js:44:79
          at Array.map (<anonymous>)
      at printChildren (node_modules/@testing-library/dom/dist/DOMElementFilter.js:43:89)
      at Object.serialize (node_modules/@testing-library/dom/dist/DOMElementFilter.js:139:75)
      at printPlugin (node_modules/pretty-format/build/index.js:330:16)
      at Object.format (node_modules/pretty-format/build/index.js:567:16)
      at prettyDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:75:37)
      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:88:20)
      at Object.debug (node_modules/@testing-library/dom/dist/screen.js:36:167)
      at Object.<anonymous> (src/App.test.js:8:10)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
      at runJest (node_modules/@jest/core/build/runJest.js:404:19)

I did more debugging in sample-app/node_modules/sample-component-library/dist/components/my-component.js and found that the following changes "resolve" the issue. In reality, this just causes the relevant Stencil code to never get loaded, but it seems that something in the proxyCustomElement call could be causing this.

- import { proxyCustomElement, HTMLElement, h } from '@stencil/core/internal/client';
+ import { HTMLElement, h } from '@stencil/core/internal/client';

function format(first, middle, last) {
  return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
}

const myComponentCss = ":host{display:block}";

- const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
+ const MyComponent$1 = /*@__PURE__*/ class extends HTMLElement {
  constructor() {
    super();
-   this.__registerHost();
-   this.__attachShadow();
  }
  getText() {
    return format(this.first, this.middle, this.last);
  }
  render() {
    return h("div", null, "Hello, World! I'm ", this.getText());
  }
  static get style() { return myComponentCss; }
- }, [1, "my-component", {
-   "first": [1],
-   "middle": [1],
-   "last": [1]
- }]);
+ };
function defineCustomElement$1() {
  if (typeof customElements === "undefined") {
    return;
  }
  const components = ["my-component"];
  components.forEach(tagName => { switch (tagName) {
    case "my-component":
      if (!customElements.get(tagName)) {
        customElements.define(tagName, MyComponent$1);
      }
      break;
  } });
}

const MyComponent = MyComponent$1;
const defineCustomElement = defineCustomElement$1;

export { MyComponent, defineCustomElement };

Code Reproduction URL

https://github.com/vincenthongzy/stencil-repro

Additional Information

This issue was discovered by an Ionic Framework user. There is more context in ionic-team/ionic-framework#25334

There may be some cases where infinite recursion could happen in the pretty printing package (ionic-team/ionic-framework#25334 (comment)), so it could also be the case that Stencil is triggering a bug in the pretty printing package instead.

@rwaskiewicz
Copy link
Member

I've confirmed that this is a bug, although it's not clear yet the root cause is here. I don't know if this is a bug in an external library, a result of wrapping components using the react output targets, etc. Labeling as a bug to get this ingested into our backlog

@rwaskiewicz rwaskiewicz added the Bug: Validated This PR or Issue is verified to be a bug within Stencil label Jun 24, 2022
@ionitron-bot ionitron-bot bot removed the triage label Jun 24, 2022
@vincenthongzy
Copy link

vincenthongzy commented Aug 18, 2022

Hey there @rwaskiewicz, I am happy to report that the problem is due a bug in nwsapi used by jsdom. After manually updating it to the latest version, the infinite loop bug is not occurring anymore.

We can now close this bug ticket as it can be fixed by doing npm i nwsapi and confirming that it is at least v2.2.1.

@h-sakano
Copy link

h-sakano commented Sep 24, 2022

@vincenthongzy
I updated nwsapi to 2.2.2, but the problem was not resolved.
Is there any other workaround?

"@ionic/core": "6.2.8"
"@ionic/react": "6.2.8"
"@ionic/react-router": "6.2.8"
"@stencil/core": "2.18.0"
"nwsapi": "2.2.2"

@tonypatton
Copy link

Any update here? It's been quiet for a while.

I tried the nwsapi upgrade, and it did not solve the issue.

@luchsamapparat
Copy link
Contributor

An additional observation: the Invalid string length error appears when using Node 16, but not on Node 14

@vmstarchenko
Copy link

This critical issue still exists. A possible solution was suggested in the linked issue: ionic-team/ionic-framework#25334 (comment)

@vmstarchenko
Copy link

Hello, @rwaskiewicz. How do you think there are any blockers that stop this pull request from merge? According to the previous comment, it should solve the problem.

main...staff0rd:stencil:pretty-print

alicewriteswrongs added a commit that referenced this issue Mar 22, 2023
This fixes an issue (documented in #3434) where when using
`@testing-libary/dom` to test a Stencil component wrapped with the React
framework wrappers could produce an infinite loop that would cause the
tests to fail.

The issue relates to an assumption that `@testing-library/dom` makes
about the `.name` property on the constructor for a custom element. In
particular, `@testing-library/dom` expects the property to be defined
here:

https://github.com/testing-library/dom-testing-library/blob/fb069c93983bc0300a6e1c91bdec5bf9443b5286/src/DOMElementFilter.ts#L198

When building with the `dist-custom-elements` output target we create an
anonymous class expression and inline it into a call in the emitted JS
to `proxyCustomElement`, like this:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

We made a change (#3248) to fix an issue (#3191) with webpack
treeshaking where if we didn't inline an anonymous class expression like
this we would get improper tree shaking in webpack.

One consequence, however, of an _anonymous_ inline class expression is
that the `.name` property on its constructor is going to be `""`, which
fails the false-ey test in `@testing-library/dom` referenced above.

So in order to fix the issue we can simply insert a name so that the
inlined class expression is no longer anonymous, like so:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class MyComponent extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

This fixes the issue with infinite loops while testing with the React
wrapper. Additionally, using the reproduction case provided for #3191 we
can confirm that this does not cause a regression with respect the
previous fix for the webpack treeshaking issue.
alicewriteswrongs added a commit that referenced this issue Mar 22, 2023
This fixes an issue (documented in #3434) where when using
`@testing-libary/dom` to test a Stencil component wrapped with the React
framework wrappers could produce an infinite loop that would cause the
tests to fail.

The issue relates to an assumption that `@testing-library/dom` makes
about the `.name` property on the constructor for a custom element. In
particular, `@testing-library/dom` expects the property to be truthy
here:

https://github.com/testing-library/dom-testing-library/blob/fb069c93983bc0300a6e1c91bdec5bf9443b5286/src/DOMElementFilter.ts#L198

When building with the `dist-custom-elements` output target we create an
anonymous class expression and inline it into a call in the emitted JS
to `proxyCustomElement`, like this:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

We made a change (#3248) to fix an issue (#3191) with webpack
treeshaking where if we didn't inline an anonymous class expression like
this we would get improper tree shaking in webpack.

One consequence, however, of an _anonymous_ inline class expression is
that the `.name` property on its constructor is going to be `""`, which
fails the false-ey test in `@testing-library/dom` referenced above.

So in order to fix the issue we can simply insert a name so that the
inlined class expression is no longer anonymous, like so:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class MyComponent extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

This fixes the issue with infinite loops while testing with the React
wrapper. Additionally, using the reproduction case provided for #3191 we
can confirm that this does not cause a regression with respect the
previous fix for the webpack treeshaking issue.
@alicewriteswrongs
Copy link
Member

Hello all,

I just published a dev build of Stencil with a possible fix for this issue. You can install it like this:

npm install --save-dev @stencil/core@3.2.0-dev.1679495718.abfe085

it would be a tremendous help if you could try that out and report back whether that fixes the issue for you - I hope it does!

@vmstarchenko
Copy link

vmstarchenko commented Mar 22, 2023

Hello all,

I just published a dev build of Stencil with a possible fix for this issue. You can install it like this:

npm install --save-dev @stencil/core@3.2.0-dev.1679495718.abfe085

it would be a tremendous help if you could try that out and report back whether that fixes the issue for you - I hope it does!

For me, this fix did not solve the problem. But imho, I think that the problem does not seem to look like an infinite loop.

If you want to reproduce it, then it's very easy to do so. It is necessary to create an blank ionic application (from tabs template) according to the instructions from the official tutorial. After that, add the following test to the project:

test('renders without crashing', async () => {                                                                                                               
   const { baseElement, findByText } = render(<IonButton>Tab 3</IonButton>);                                                                                                                                     
   // const { baseElement, findByText } = render(<App />);                                                                                                          
   expect(baseElement).toBeDefined();                                                                                                                                            
   await findByText('Tab 5');                                                                                                                                                    
});     

In this case, the test will fail, and that is ok. However, instead of the printed html, there will be a long json text on the screen.

If you want to get an even more strange error, uncomment the line where App is used instead of IonButton. The planned result will be so big that json printing will be replaced by the problem of html dumping. In this case, the error will look similar to the infinite loop problem

alicewriteswrongs added a commit that referenced this issue Mar 23, 2023
This fixes an issue (documented in #3434) where when using
`@testing-libary/dom` to test a Stencil component wrapped with the React
framework wrappers could produce an infinite loop that would cause the
tests to fail.

The issue relates to an assumption that `@testing-library/dom` makes
about the `.name` property on the constructor for a custom element. In
particular, `@testing-library/dom` expects the property to be truthy
here:

https://github.com/testing-library/dom-testing-library/blob/fb069c93983bc0300a6e1c91bdec5bf9443b5286/src/DOMElementFilter.ts#L198

When building with the `dist-custom-elements` output target we create an
anonymous class expression and inline it into a call in the emitted JS
to `proxyCustomElement`, like this:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

We made a change (#3248) to fix an issue (#3191) with webpack
treeshaking where if we didn't inline an anonymous class expression like
this we would get improper tree shaking in webpack.

One consequence, however, of an _anonymous_ inline class expression is
that the `.name` property on its constructor is going to be `""`, which
fails the false-ey test in `@testing-library/dom` referenced above.

So in order to fix the issue we can simply insert a name so that the
inlined class expression is no longer anonymous, like so:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class MyComponent extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

This fixes the issue with infinite loops while testing with the React
wrapper. Additionally, using the reproduction case provided for #3191 we
can confirm that this does not cause a regression with respect the
previous fix for the webpack treeshaking issue.
@staff0rd
Copy link

Did not solve for me, although I'm not certain I am including it correctly.

Test that causes bad serialization looks like this:

import { IonSkeletonText } from '@ionic/react'
import { waitForIonicReact } from '@ionic/react-test-utils'
import { render } from '@testing-library/react'

describe('SkeletonForm', () => {
  it('should render a skeleton form', async () => {
    const { debug } = render(<IonSkeletonText />)
    await waitForIonicReact()
    debug()
  })
})

To integrate the above fix, I'm:

  1. npm install --save-dev @stencil/core@3.2.0-dev.1679495718.abfe085 in a local clone of @ionic/react
  2. npm run build in @ionic/react
  3. npm link @ionic/react in the project with the test

alicewriteswrongs added a commit that referenced this issue Mar 24, 2023
This fixes an issue (documented in #3434) where when using
`@testing-libary/dom` to test a Stencil component wrapped with the React
framework wrappers could produce an infinite loop that would cause the
tests to fail.

The issue relates to an assumption that `@testing-library/dom` makes
about the `.name` property on the constructor for a custom element. In
particular, `@testing-library/dom` expects the property to be truthy
here:

https://github.com/testing-library/dom-testing-library/blob/fb069c93983bc0300a6e1c91bdec5bf9443b5286/src/DOMElementFilter.ts#L198

When building with the `dist-custom-elements` output target we create an
anonymous class expression and inline it into a call in the emitted JS
to `proxyCustomElement`, like this:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

We made a change (#3248) to fix an issue (#3191) with webpack
treeshaking where if we didn't inline an anonymous class expression like
this we would get improper tree shaking in webpack.

One consequence, however, of an _anonymous_ inline class expression is
that the `.name` property on its constructor is going to be `""`, which
fails the false-ey test in `@testing-library/dom` referenced above.

So in order to fix the issue we can simply insert a name so that the
inlined class expression is no longer anonymous, like so:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class MyComponent extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

This fixes the issue with infinite loops while testing with the React
wrapper. Additionally, using the reproduction case provided for #3191 we
can confirm that this does not cause a regression with respect the
previous fix for the webpack treeshaking issue.
alicewriteswrongs added a commit that referenced this issue Mar 24, 2023
This fixes an issue (documented in #3434) where when using
`@testing-libary/dom` to test a Stencil component wrapped with the React
framework wrappers could produce an infinite loop that would cause the
tests to fail.

The issue relates to an assumption that `@testing-library/dom` makes
about the `.name` property on the constructor for a custom element. In
particular, `@testing-library/dom` expects the property to be truthy
here:

https://github.com/testing-library/dom-testing-library/blob/fb069c93983bc0300a6e1c91bdec5bf9443b5286/src/DOMElementFilter.ts#L198

When building with the `dist-custom-elements` output target we create an
anonymous class expression and inline it into a call in the emitted JS
to `proxyCustomElement`, like this:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

We made a change (#3248) to fix an issue (#3191) with webpack
treeshaking where if we didn't inline an anonymous class expression like
this we would get improper tree shaking in webpack.

One consequence, however, of an _anonymous_ inline class expression is
that the `.name` property on its constructor is going to be `""`, which
fails the false-ey test in `@testing-library/dom` referenced above.

So in order to fix the issue we can simply insert a name so that the
inlined class expression is no longer anonymous, like so:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class MyComponent extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

This fixes the issue with infinite loops while testing with the React
wrapper. Additionally, using the reproduction case provided for #3191 we
can confirm that this does not cause a regression with respect the
previous fix for the webpack treeshaking issue.
@rwaskiewicz
Copy link
Member

The fix for this issue has been included in today's v3.2.1 release of Stencil. As a result, I'm going to close this issue.

Please note that if you are encountering this issue while using the Ionic Framework, a new patch release of the Ionic Framework will need to be released that uses Stencil v3.2.1 in order to be resolved on the Framework side (see issue ionic-team/ionic-framework#25334).

@borisdiakur
Copy link

Unfortunately we are still running into this issue, even after building our component library with v3.2.1 release of Stencil.

I checked that the inlined class expression is no longer anonymous as described in PR #4188 and it looks like it should look:

const LdInputMessage$1 = /*@__PURE__*/ proxyCustomElement(class LdInputMessage extends HTMLElement {

However, the issue still occurs in a specific test case. I haven't been able to figure out the crucial difference to other test cases / components yet.

I'm attaching a link to an application on Stackblitz which contains a test that leads to the above issue, if you uncomment the last two lines of the second test: https://stackblitz.com/github/emdgroup-liquid/liquid-sandbox-react-tailwind?file=src%2FApp.test.tsx%3AL34
Here is the repo if you'd like to run it locally: https://github.com/emdgroup-liquid/liquid-sandbox-react-tailwind

It would be great, if you could have a second look at this issue @alicewriteswrongs @rwaskiewicz. 🙏

@rwaskiewicz
Copy link
Member

@borisdiakur Thanks! Can you please create a new issue with your reproduction case there for us to properly track it?

@borisdiakur
Copy link

@rwaskiewicz Yes, I will. Or maybe not. 😅 I actually managed to dig a little further in the last hour, by removing all components from the app. This resulted in a different but seemingly related issue (Maximum call stack size exceeded in picocolors used by vitest for printing error messages). The actual error message (quite huge - see screenshot) says that the testing library is not able to find the element with text that I'm looking for in the test.

Screenshot 2023-04-14 at 13 58 22

Since I'm not even using web components in the test right now, I think this issue is not related to the one documented above (although at first sight it seemed so).

TLDR: For now I think we're good and I won't need to open a new issue (at least not for Stencil). However, if I put the Web Components back in place, and run into a similar problem again, I will create a new one.

Thanks a lot 🙏

@oscargm
Copy link

oscargm commented May 29, 2023

Hello.

We've a complete design system developed using stencil 2.x and a consumer found out this issue.
I've seen this is already fixed for v3 and our next version will be released using stencil 3.x but in the meantime would be very beneficial to have the fix in v2.x.

Looking into the commit message I understand there is no breaking change, so I would appreciate it very much if this could be done.

I can provide a PR if you agree on that

alicewriteswrongs added a commit that referenced this issue May 30, 2023
This fixes an issue (documented in #3434) where when using
`@testing-libary/dom` to test a Stencil component wrapped with the React
framework wrappers could produce an infinite loop that would cause the
tests to fail.

The issue relates to an assumption that `@testing-library/dom` makes
about the `.name` property on the constructor for a custom element. In
particular, `@testing-library/dom` expects the property to be truthy
here:

https://github.com/testing-library/dom-testing-library/blob/fb069c93983bc0300a6e1c91bdec5bf9443b5286/src/DOMElementFilter.ts#L198

When building with the `dist-custom-elements` output target we create an
anonymous class expression and inline it into a call in the emitted JS
to `proxyCustomElement`, like this:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

We made a change (#3248) to fix an issue (#3191) with webpack
treeshaking where if we didn't inline an anonymous class expression like
this we would get improper tree shaking in webpack.

One consequence, however, of an _anonymous_ inline class expression is
that the `.name` property on its constructor is going to be `""`, which
fails the false-ey test in `@testing-library/dom` referenced above.

So in order to fix the issue we can simply insert a name so that the
inlined class expression is no longer anonymous, like so:

```js
const MyComponent$1 = /*@__PURE__*/ proxyCustomElement(
  class MyComponent extends HTMLElement {
    ...
  },
  [1, "my-component", {}]
);
```

This fixes the issue with infinite loops while testing with the React
wrapper. Additionally, using the reproduction case provided for #3191 we
can confirm that this does not cause a regression with respect the
previous fix for the webpack treeshaking issue.
@alicewriteswrongs
Copy link
Member

Hey @oscargm, I just opened a PR (#4435) to port the fix to v2 and published a dev build based on that change - if you have a spare minute you could try this out in your project and confirm that it fixes the bug:

npm i --save-dev @stencil/core@2.22.3-dev.1685477255.0a23805

@oscargm
Copy link

oscargm commented Jun 21, 2023

Hey @alicewriteswrongs sorry taking so much time to answer.

I tried this package and the issue is still happening, there might be other things in v3 that help fixing it.
Anyway, there is a workaround changing the way to query the elements and we'll release the new version soon using stencil v3 so this will be fixed from our side.

Thanks again for providing the test package to try this out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug: Validated This PR or Issue is verified to be a bug within Stencil
Projects
None yet
Development

No branches or pull requests