Skip to content

Commit 0a23805

Browse files
fix(test): fix infinite loops w/ react and @testing-library/dom (#4188)
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.
1 parent 44b8954 commit 0a23805

File tree

4 files changed

+51
-35
lines changed

4 files changed

+51
-35
lines changed

src/compiler/transformers/add-component-meta-proxy.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,27 @@ export const createComponentMetadataProxy = (compilerMeta: d.ComponentCompilerMe
4747
};
4848

4949
/**
50-
* Create a call expression for wrapping a component represented as an anonymous class in a proxy. This call expression
51-
* takes a form:
50+
* Create a call expression for wrapping a component represented as a class
51+
* expression in a proxy. This call expression takes the form:
52+
*
5253
* ```ts
5354
* PROXY_CUSTOM_ELEMENT(Clazz, Metadata);
5455
* ```
56+
*
5557
* where
56-
* - `PROXY_CUSTOM_ELEMENT` is a Stencil internal identifier that will be replaced with the name of the actual function
57-
* name at compile name
58-
* - `Clazz` is an anonymous class to be proxied
58+
* - `PROXY_CUSTOM_ELEMENT` is a Stencil internal identifier that will be
59+
* replaced with the name of the actual function name at compile name
60+
* - `Clazz` is a class expression to be proxied
5961
* - `Metadata` is the compiler metadata associated with the Stencil component
6062
*
61-
* @param compilerMeta compiler metadata associated with the component to be wrapped in a proxy
62-
* @param clazz the anonymous class to proxy
63+
* @param compilerMeta compiler metadata associated with the component to be
64+
* wrapped in a proxy
65+
* @param clazz the class expression to proxy
6366
* @returns the generated call expression
6467
*/
65-
export const createAnonymousClassMetadataProxy = (
68+
export const createClassMetadataProxy = (
6669
compilerMeta: d.ComponentCompilerMeta,
67-
clazz: ts.Expression
70+
clazz: ts.ClassExpression
6871
): ts.CallExpression => {
6972
const compactMeta: d.ComponentRuntimeMetaCompact = formatComponentRuntimeMeta(compilerMeta, true);
7073
const literalMeta = convertValueToLiteral(compactMeta);

src/compiler/transformers/component-native/proxy-custom-element-function.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ts from 'typescript';
22

33
import type * as d from '../../../declarations';
4-
import { createAnonymousClassMetadataProxy } from '../add-component-meta-proxy';
4+
import { createClassMetadataProxy } from '../add-component-meta-proxy';
55
import { addImports } from '../add-imports';
66
import { RUNTIME_APIS } from '../core-runtime-apis';
77
import { getModuleFromSourceFile } from '../transform-utils';
@@ -47,8 +47,22 @@ export const proxyCustomElement = (
4747
continue;
4848
}
4949

50+
// to narrow the type of `declaration.initializer` to `ts.ClassExpression`
51+
if (!ts.isClassExpression(declaration.initializer)) {
52+
continue;
53+
}
54+
55+
const renamedClassExpression = ts.factory.updateClassExpression(
56+
declaration.initializer,
57+
ts.getModifiers(declaration.initializer),
58+
ts.factory.createIdentifier(principalComponent.componentClassName),
59+
declaration.initializer.typeParameters,
60+
declaration.initializer.heritageClauses,
61+
declaration.initializer.members
62+
);
63+
5064
// wrap the Stencil component's class declaration in a component proxy
51-
const proxyCreationCall = createAnonymousClassMetadataProxy(principalComponent, declaration.initializer);
65+
const proxyCreationCall = createClassMetadataProxy(principalComponent, renamedClassExpression);
5266
ts.addSyntheticLeadingComment(proxyCreationCall, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false);
5367

5468
// update the component's variable declaration to use the new initializer

src/compiler/transformers/test/add-component-meta-proxy.spec.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import ts from 'typescript';
33
import { stubComponentCompilerMeta } from '../../../compiler/types/tests/ComponentCompilerMeta.stub';
44
import type * as d from '../../../declarations';
55
import * as FormatComponentRuntimeMeta from '../../../utils/format-component-runtime-meta';
6-
import { createAnonymousClassMetadataProxy } from '../add-component-meta-proxy';
6+
import { createClassMetadataProxy } from '../add-component-meta-proxy';
77
import { HTML_ELEMENT } from '../core-runtime-apis';
88
import * as TransformUtils from '../transform-utils';
99

1010
describe('add-component-meta-proxy', () => {
11-
describe('createAnonymousClassMetadataProxy()', () => {
11+
describe('createClassMetadataProxy()', () => {
1212
let classExpr: ts.ClassExpression;
1313
let htmlElementHeritageClause: ts.HeritageClause;
1414
let literalMetadata: ts.StringLiteral;
@@ -51,33 +51,33 @@ describe('add-component-meta-proxy', () => {
5151
});
5252

5353
it('returns a call expression', () => {
54-
const result: ts.CallExpression = createAnonymousClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
54+
const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
5555

5656
expect(ts.isCallExpression(result)).toBe(true);
5757
});
5858

5959
it('wraps the initializer in PROXY_CUSTOM_ELEMENT', () => {
60-
const result: ts.CallExpression = createAnonymousClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
60+
const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
6161

6262
expect((result.expression as ts.Identifier).escapedText).toBe('___stencil_proxyCustomElement');
6363
});
6464

6565
it("doesn't add any type arguments to the call", () => {
66-
const result: ts.CallExpression = createAnonymousClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
66+
const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
6767

6868
expect(result.typeArguments).toHaveLength(0);
6969
});
7070

7171
it('adds the correct arguments to the PROXY_CUSTOM_ELEMENT call', () => {
72-
const result: ts.CallExpression = createAnonymousClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
72+
const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
7373

7474
expect(result.arguments).toHaveLength(2);
7575
expect(result.arguments[0]).toBe(classExpr);
7676
expect(result.arguments[1]).toBe(literalMetadata);
7777
});
7878

7979
it('includes the heritage clause', () => {
80-
const result: ts.CallExpression = createAnonymousClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
80+
const result: ts.CallExpression = createClassMetadataProxy(stubComponentCompilerMeta(), classExpr);
8181

8282
expect(result.arguments.length).toBeGreaterThanOrEqual(1);
8383
const createdClassExpression = result.arguments[0];

src/compiler/transformers/test/proxy-custom-element-function.spec.ts

+16-17
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ describe('proxy-custom-element-function', () => {
1717
ReturnType<typeof TransformUtils.getModuleFromSourceFile>,
1818
Parameters<typeof TransformUtils.getModuleFromSourceFile>
1919
>;
20-
let createAnonymousClassMetadataProxySpy: jest.SpyInstance<
21-
ReturnType<typeof AddComponentMetaProxy.createAnonymousClassMetadataProxy>,
22-
Parameters<typeof AddComponentMetaProxy.createAnonymousClassMetadataProxy>
20+
let createClassMetadataProxySpy: jest.SpyInstance<
21+
ReturnType<typeof AddComponentMetaProxy.createClassMetadataProxy>,
22+
Parameters<typeof AddComponentMetaProxy.createClassMetadataProxy>
2323
>;
2424

2525
beforeEach(() => {
@@ -47,20 +47,19 @@ describe('proxy-custom-element-function', () => {
4747
} as d.Module;
4848
});
4949

50-
createAnonymousClassMetadataProxySpy = jest.spyOn(AddComponentMetaProxy, 'createAnonymousClassMetadataProxy');
51-
createAnonymousClassMetadataProxySpy.mockImplementation(
52-
(_compilerMeta: d.ComponentCompilerMeta, clazz: ts.Expression) =>
53-
ts.factory.createCallExpression(
54-
ts.factory.createIdentifier(PROXY_CUSTOM_ELEMENT),
55-
[],
56-
[clazz, ts.factory.createTrue()]
57-
)
50+
createClassMetadataProxySpy = jest.spyOn(AddComponentMetaProxy, 'createClassMetadataProxy');
51+
createClassMetadataProxySpy.mockImplementation((_compilerMeta: d.ComponentCompilerMeta, clazz: ts.Expression) =>
52+
ts.factory.createCallExpression(
53+
ts.factory.createIdentifier(PROXY_CUSTOM_ELEMENT),
54+
[],
55+
[clazz, ts.factory.createTrue()]
56+
)
5857
);
5958
});
6059

6160
afterEach(() => {
6261
getModuleFromSourceFileSpy.mockRestore();
63-
createAnonymousClassMetadataProxySpy.mockRestore();
62+
createClassMetadataProxySpy.mockRestore();
6463
});
6564

6665
describe('proxyCustomElement()', () => {
@@ -82,7 +81,7 @@ describe('proxy-custom-element-function', () => {
8281
const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]);
8382

8483
expect(transpiledModule.outputText).toContain(
85-
`export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true);`
84+
`export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);`
8685
);
8786
});
8887

@@ -94,7 +93,7 @@ describe('proxy-custom-element-function', () => {
9493
const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]);
9594

9695
expect(transpiledModule.outputText).toContain(
97-
`export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true);`
96+
`export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true);`
9897
);
9998
});
10099

@@ -105,18 +104,18 @@ describe('proxy-custom-element-function', () => {
105104
const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]);
106105

107106
expect(transpiledModule.outputText).toContain(
108-
`export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true), foo = 'hello world!';`
107+
`export const ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), foo = 'hello world!';`
109108
);
110109
});
111110

112111
it('wraps a class initializer properly in the middle of multiple variable declarations', () => {
113-
const code = `const foo = 'hello world!', ${componentClassName} = class extends HTMLElement {}, bar = 'goodbye?'`;
112+
const code = `const foo = 'hello world!', ${componentClassName} = class ${componentClassName} extends HTMLElement {}, bar = 'goodbye?'`;
114113

115114
const transformer = proxyCustomElement(compilerCtx, transformOpts);
116115
const transpiledModule = transpileModule(code, null, compilerCtx, [], [transformer]);
117116

118117
expect(transpiledModule.outputText).toContain(
119-
`export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class extends HTMLElement {}, true), bar = 'goodbye?';`
118+
`export const foo = 'hello world!', ${componentClassName} = /*@__PURE__*/ __stencil_proxyCustomElement(class ${componentClassName} extends HTMLElement {}, true), bar = 'goodbye?';`
120119
);
121120
});
122121
});

0 commit comments

Comments
 (0)