Skip to content

Commit 6e951ad

Browse files
authored
feat: childComponentOverrides property to override nested child providers (testing-library#332)
1 parent 02a688b commit 6e951ad

File tree

3 files changed

+94
-22
lines changed

3 files changed

+94
-22
lines changed

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

+25
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,26 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
186186
* })
187187
*/
188188
componentProviders?: any[];
189+
/**
190+
* @description
191+
* Collection of child component specified providers to override with
192+
*
193+
* @default
194+
* []
195+
*
196+
* @example
197+
* await render(AppComponent, {
198+
* childComponentOverrides: [
199+
* {
200+
* component: ChildOfAppComponent,
201+
* providers: [{ provide: MyService, useValue: { hello: 'world' } }]
202+
* }
203+
* ]
204+
* })
205+
*
206+
* @experimental
207+
*/
208+
childComponentOverrides?: ComponentOverride<any>[];
189209
/**
190210
* @description
191211
* A collection of imports to override a standalone component's imports with.
@@ -273,6 +293,11 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
273293
removeAngularAttributes?: boolean;
274294
}
275295

296+
export interface ComponentOverride<T> {
297+
component: Type<T>;
298+
providers: any[];
299+
}
300+
276301
// eslint-disable-next-line @typescript-eslint/ban-types
277302
export interface RenderTemplateOptions<WrapperType, Properties extends object = {}, Q extends Queries = typeof queries>
278303
extends RenderComponentOptions<Properties, Q> {

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
queries as dtlQueries,
2626
} from '@testing-library/dom';
2727
import type { Queries, BoundFunctions } from '@testing-library/dom';
28-
import { RenderComponentOptions, RenderTemplateOptions, RenderResult } from './models';
28+
import { RenderComponentOptions, RenderTemplateOptions, RenderResult, ComponentOverride } from './models';
2929
import { getConfig } from './config';
3030

3131
const mountedFixtures = new Set<ComponentFixture<any>>();
@@ -55,6 +55,7 @@ export async function render<SutType, WrapperType = SutType>(
5555
wrapper = WrapperComponent as Type<WrapperType>,
5656
componentProperties = {},
5757
componentProviders = [],
58+
childComponentOverrides = [],
5859
ɵcomponentImports: componentImports,
5960
excludeComponentDeclaration = false,
6061
routes = [],
@@ -85,6 +86,7 @@ export async function render<SutType, WrapperType = SutType>(
8586
schemas: [...schemas],
8687
});
8788
overrideComponentImports(sut, componentImports);
89+
overrideChildComponentProviders(childComponentOverrides);
8890

8991
await TestBed.compileComponents();
9092

@@ -282,6 +284,12 @@ function overrideComponentImports<SutType>(sut: Type<SutType> | string, imports:
282284
}
283285
}
284286

287+
function overrideChildComponentProviders(componentOverrides: ComponentOverride<any>[]) {
288+
componentOverrides?.forEach(({ component, providers }) => {
289+
TestBed.overrideComponent(component, { set: { providers } });
290+
});
291+
}
292+
285293
function hasOnChangesHook<SutType>(componentInstance: SutType): componentInstance is SutType & OnChanges {
286294
return (
287295
'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'

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

+60-21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SimpleChanges,
88
APP_INITIALIZER,
99
ApplicationInitStatus,
10+
Injectable,
1011
} from '@angular/core';
1112
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
1213
import { TestBed } from '@angular/core/testing';
@@ -19,7 +20,7 @@ import { render, fireEvent, screen } from '../src/public_api';
1920
<button>button</button>
2021
`,
2122
})
22-
class FixtureComponent { }
23+
class FixtureComponent {}
2324

2425
test('creates queries and events', async () => {
2526
const view = await render(FixtureComponent);
@@ -50,46 +51,84 @@ describe('standalone', () => {
5051

5152
describe('standalone with child', () => {
5253
@Component({
53-
selector: 'child-fixture',
54+
selector: 'atl-child-fixture',
5455
template: `<span>A child fixture</span>`,
5556
standalone: true,
5657
})
57-
class ChildFixture { }
58+
class ChildFixtureComponent {}
5859

5960
@Component({
60-
selector: 'child-fixture',
61+
selector: 'atl-child-fixture',
6162
template: `<span>A mock child fixture</span>`,
6263
standalone: true,
6364
})
64-
class MockChildFixture { }
65+
class MockChildFixtureComponent {}
6566

6667
@Component({
67-
selector: 'parent-fixture',
68+
selector: 'atl-parent-fixture',
6869
template: `<h1>Parent fixture</h1>
69-
<div><child-fixture></child-fixture></div> `,
70+
<div><atl-child-fixture></atl-child-fixture></div> `,
7071
standalone: true,
71-
imports: [ChildFixture],
72+
imports: [ChildFixtureComponent],
7273
})
73-
class ParentFixture { }
74+
class ParentFixtureComponent {}
7475

7576
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'));
77+
await render(ParentFixtureComponent);
78+
expect(screen.getByText('Parent fixture')).toBeInTheDocument();
79+
expect(screen.getByText('A child fixture')).toBeInTheDocument();
7980
});
8081

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'));
82+
it('renders the standalone component with child given ɵcomponentImports', async () => {
83+
await render(ParentFixtureComponent, { ɵcomponentImports: [MockChildFixtureComponent] });
84+
expect(screen.getByText('Parent fixture')).toBeInTheDocument();
85+
expect(screen.getByText('A mock child fixture')).toBeInTheDocument();
8586
});
8687

8788
it('rejects render of template with componentImports set', () => {
88-
const result = render(`<div><parent-fixture></parent-fixture></div>`, {
89-
imports: [ParentFixture],
90-
ɵcomponentImports: [MockChildFixture],
89+
const view = render(`<div><atl-parent-fixture></atl-parent-fixture></div>`, {
90+
imports: [ParentFixtureComponent],
91+
ɵcomponentImports: [MockChildFixtureComponent],
92+
});
93+
return expect(view).rejects.toMatchObject({ message: /Error while rendering/ });
94+
});
95+
});
96+
97+
describe('childComponentOverrides', () => {
98+
@Injectable()
99+
class MySimpleService {
100+
public value = 'real';
101+
}
102+
103+
@Component({
104+
selector: 'atl-child-fixture',
105+
template: `<span>{{ simpleService.value }}</span>`,
106+
standalone: true,
107+
providers: [MySimpleService],
108+
})
109+
class NestedChildFixtureComponent {
110+
public constructor(public simpleService: MySimpleService) {}
111+
}
112+
113+
@Component({
114+
selector: 'atl-parent-fixture',
115+
template: `<atl-child-fixture></atl-child-fixture>`,
116+
standalone: true,
117+
imports: [NestedChildFixtureComponent],
118+
})
119+
class ParentFixtureComponent {}
120+
121+
it('renders with overridden child service when specified', async () => {
122+
await render(ParentFixtureComponent, {
123+
childComponentOverrides: [
124+
{
125+
component: NestedChildFixtureComponent,
126+
providers: [{ provide: MySimpleService, useValue: { value: 'fake' } }],
127+
},
128+
],
91129
});
92-
return expect(result).rejects.toMatchObject({ message: /Error while rendering/ });
130+
131+
expect(screen.getByText('fake')).toBeInTheDocument();
93132
});
94133
});
95134

@@ -117,7 +156,7 @@ describe('animationModule', () => {
117156
@NgModule({
118157
declarations: [FixtureComponent],
119158
})
120-
class FixtureModule { }
159+
class FixtureModule {}
121160
describe('excludeComponentDeclaration', () => {
122161
it('does not throw if component is declared in an imported module', async () => {
123162
await render(FixtureComponent, {

0 commit comments

Comments
 (0)