Skip to content

Commit 459b500

Browse files
authored
feat: support componentImports option for standalone components (experimental) (#307)
1 parent ed262f6 commit 459b500

File tree

3 files changed

+92
-16
lines changed

3 files changed

+92
-16
lines changed

projects/testing-library/src/lib/models.ts

+17
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,23 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
186186
* })
187187
*/
188188
componentProviders?: any[];
189+
/**
190+
* @description
191+
* A collection of imports to override a standalone component's imports with.
192+
*
193+
* @default
194+
* undefined
195+
*
196+
* @example
197+
* const component = await render(AppComponent, {
198+
* ɵcomponentImports: [
199+
* MockChildComponent
200+
* ]
201+
* })
202+
*
203+
* @experimental
204+
*/
205+
ɵcomponentImports?: (Type<any> | any[])[];
189206
/**
190207
* @description
191208
* Queries to bind. Overrides the default set from DOM Testing Library unless merged.

projects/testing-library/src/lib/testing-library.ts

+28-14
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export async function render<SutType, WrapperType = SutType>(
5555
wrapper = WrapperComponent as Type<WrapperType>,
5656
componentProperties = {},
5757
componentProviders = [],
58+
ɵcomponentImports: componentImports,
5859
excludeComponentDeclaration = false,
5960
routes = [],
6061
removeAngularAttributes = false,
@@ -83,6 +84,7 @@ export async function render<SutType, WrapperType = SutType>(
8384
providers: [...providers],
8485
schemas: [...schemas],
8586
});
87+
overrideComponentImports(sut, componentImports);
8688

8789
await TestBed.compileComponents();
8890

@@ -128,23 +130,23 @@ export async function render<SutType, WrapperType = SutType>(
128130
const [path, params] = (basePath + href).split('?');
129131
const queryParams = params
130132
? params.split('&').reduce((qp, q) => {
131-
const [key, value] = q.split('=');
132-
const currentValue = qp[key];
133-
if (typeof currentValue === 'undefined') {
134-
qp[key] = value;
135-
} else if (Array.isArray(currentValue)) {
136-
qp[key] = [...currentValue, value];
137-
} else {
138-
qp[key] = [currentValue, value];
139-
}
140-
return qp;
141-
}, {} as Record<string, string | string[]>)
133+
const [key, value] = q.split('=');
134+
const currentValue = qp[key];
135+
if (typeof currentValue === 'undefined') {
136+
qp[key] = value;
137+
} else if (Array.isArray(currentValue)) {
138+
qp[key] = [...currentValue, value];
139+
} else {
140+
qp[key] = [currentValue, value];
141+
}
142+
return qp;
143+
}, {} as Record<string, string | string[]>)
142144
: undefined;
143145

144146
const navigateOptions: NavigationExtras | undefined = queryParams
145147
? {
146-
queryParams,
147-
}
148+
queryParams,
149+
}
148150
: undefined;
149151

150152
const doNavigate = () => {
@@ -264,6 +266,18 @@ function setComponentProperties<SutType>(
264266
return fixture;
265267
}
266268

269+
function overrideComponentImports<SutType>(sut: Type<SutType> | string, imports: (Type<any> | any[])[] | undefined) {
270+
if (imports) {
271+
if (typeof sut === 'function' && ɵisStandalone(sut)) {
272+
TestBed.overrideComponent(sut, { set: { imports } });
273+
} else {
274+
throw new Error(
275+
`Error while rendering ${sut}: Cannot specify componentImports on a template or non-standalone component.`,
276+
);
277+
}
278+
}
279+
}
280+
267281
function hasOnChangesHook<SutType>(componentInstance: SutType): componentInstance is SutType & OnChanges {
268282
return (
269283
'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'
@@ -397,7 +411,7 @@ if (typeof process === 'undefined' || !process.env?.ATL_SKIP_AUTO_CLEANUP) {
397411
}
398412

399413
@Component({ selector: 'atl-wrapper-component', template: '' })
400-
class WrapperComponent {}
414+
class WrapperComponent { }
401415

402416
/**
403417
* Wrap findBy queries to poke the Angular change detection cycle

projects/testing-library/tests/render.spec.ts

+47-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { render, fireEvent, screen } from '../src/public_api';
1919
<button>button</button>
2020
`,
2121
})
22-
class FixtureComponent {}
22+
class FixtureComponent { }
2323

2424
test('creates queries and events', async () => {
2525
const view = await render(FixtureComponent);
@@ -48,6 +48,51 @@ describe('standalone', () => {
4848
});
4949
});
5050

51+
describe('standalone with child', () => {
52+
@Component({
53+
selector: 'child-fixture',
54+
template: `<span>A child fixture</span>`,
55+
standalone: true,
56+
})
57+
class ChildFixture { }
58+
59+
@Component({
60+
selector: 'child-fixture',
61+
template: `<span>A mock child fixture</span>`,
62+
standalone: true,
63+
})
64+
class MockChildFixture { }
65+
66+
@Component({
67+
selector: 'parent-fixture',
68+
template: `<h1>Parent fixture</h1>
69+
<div><child-fixture></child-fixture></div> `,
70+
standalone: true,
71+
imports: [ChildFixture],
72+
})
73+
class ParentFixture { }
74+
75+
it('renders the standalone component with child', async () => {
76+
await render(ParentFixture);
77+
expect(screen.getByText('Parent fixture'));
78+
expect(screen.getByText('A child fixture'));
79+
});
80+
81+
it('renders the standalone component with child', async () => {
82+
await render(ParentFixture, { ɵcomponentImports: [MockChildFixture] });
83+
expect(screen.getByText('Parent fixture'));
84+
expect(screen.getByText('A mock child fixture'));
85+
});
86+
87+
it('rejects render of template with componentImports set', () => {
88+
const result = render(`<div><parent-fixture></parent-fixture></div>`, {
89+
imports: [ParentFixture],
90+
ɵcomponentImports: [MockChildFixture],
91+
});
92+
return expect(result).rejects.toMatchObject({ message: /Error while rendering/ });
93+
});
94+
});
95+
5196
describe('removeAngularAttributes', () => {
5297
it('should remove angular attribute', async () => {
5398
await render(FixtureComponent, {
@@ -72,7 +117,7 @@ describe('animationModule', () => {
72117
@NgModule({
73118
declarations: [FixtureComponent],
74119
})
75-
class FixtureModule {}
120+
class FixtureModule { }
76121
describe('excludeComponentDeclaration', () => {
77122
it('does not throw if component is declared in an imported module', async () => {
78123
await render(FixtureComponent, {

0 commit comments

Comments
 (0)