From b6dc6536ee93f0c57bc187cc3d720a677e3d07da Mon Sep 17 00:00:00 2001 From: Alice Pote Date: Mon, 20 Mar 2023 15:32:50 -0400 Subject: [PATCH] fix(test): fix infinite loops w/ react and @testing-library/dom 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. --- .../transformers/add-component-meta-proxy.ts | 2 +- .../proxy-custom-element-function.ts | 16 +++++++++++++++- .../test/proxy-custom-element-function.spec.ts | 10 +++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/compiler/transformers/add-component-meta-proxy.ts b/src/compiler/transformers/add-component-meta-proxy.ts index f20e51db0716..f6e3eb306a84 100644 --- a/src/compiler/transformers/add-component-meta-proxy.ts +++ b/src/compiler/transformers/add-component-meta-proxy.ts @@ -64,7 +64,7 @@ export const createComponentMetadataProxy = (compilerMeta: d.ComponentCompilerMe */ export const createAnonymousClassMetadataProxy = ( compilerMeta: d.ComponentCompilerMeta, - clazz: ts.Expression + clazz: ts.ClassExpression ): ts.CallExpression => { const compactMeta: d.ComponentRuntimeMetaCompact = formatComponentRuntimeMeta(compilerMeta, true); const literalMeta = convertValueToLiteral(compactMeta); diff --git a/src/compiler/transformers/component-native/proxy-custom-element-function.ts b/src/compiler/transformers/component-native/proxy-custom-element-function.ts index e97a187a9ec4..96fbb8876598 100644 --- a/src/compiler/transformers/component-native/proxy-custom-element-function.ts +++ b/src/compiler/transformers/component-native/proxy-custom-element-function.ts @@ -47,8 +47,22 @@ export const proxyCustomElement = ( continue; } + // to narrow the type of `declaration.initializer` to `ts.ClassExpression` + if (!ts.isClassExpression(declaration.initializer)) { + continue; + } + + const renamedClassExpression = ts.factory.updateClassExpression( + declaration.initializer, + ts.getModifiers(declaration.initializer), + ts.factory.createIdentifier(principalComponent.componentClassName), + declaration.initializer.typeParameters, + declaration.initializer.heritageClauses, + declaration.initializer.members + ); + // wrap the Stencil component's class declaration in a component proxy - const proxyCreationCall = createAnonymousClassMetadataProxy(principalComponent, declaration.initializer); + const proxyCreationCall = createAnonymousClassMetadataProxy(principalComponent, renamedClassExpression); ts.addSyntheticLeadingComment(proxyCreationCall, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false); // update the component's variable declaration to use the new initializer diff --git a/src/compiler/transformers/test/proxy-custom-element-function.spec.ts b/src/compiler/transformers/test/proxy-custom-element-function.spec.ts index 62681482a2b6..9f9f0be0c415 100644 --- a/src/compiler/transformers/test/proxy-custom-element-function.spec.ts +++ b/src/compiler/transformers/test/proxy-custom-element-function.spec.ts @@ -82,7 +82,7 @@ describe('proxy-custom-element-function', () => { const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]); expect(transpiledModule.outputText).toContain( - `export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true);` + `export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);` ); }); @@ -94,7 +94,7 @@ describe('proxy-custom-element-function', () => { const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]); expect(transpiledModule.outputText).toContain( - `export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true);` + `export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);` ); }); @@ -105,18 +105,18 @@ describe('proxy-custom-element-function', () => { const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]); expect(transpiledModule.outputText).toContain( - `export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true), foo = 'hello world!';` + `export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), foo = 'hello world!';` ); }); it('wraps a class initializer properly in the middle of multiple variable declarations', () => { - const code = `const foo = 'hello world!', ${componentClassName} = class extends HTMLElement {}, bar = 'goodbye?'`; + const code = `const foo = 'hello world!', ${componentClassName} = class ${componentClassName} extends HTMLElement {}, bar = 'goodbye?'`; const transformer = proxyCustomElement(compilerCtx, transformOpts); const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]); expect(transpiledModule.outputText).toContain( - `export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true), bar = 'goodbye?';` + `export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), bar = 'goodbye?';` ); }); });