Skip to content

Commit

Permalink
feat: childComponentOverrides property to override nested child provi…
Browse files Browse the repository at this point in the history
…ders (#332)
  • Loading branch information
JJosephttg authored Nov 28, 2022
1 parent 02a688b commit 6e951ad
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 22 deletions.
25 changes: 25 additions & 0 deletions projects/testing-library/src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,26 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
* })
*/
componentProviders?: any[];
/**
* @description
* Collection of child component specified providers to override with
*
* @default
* []
*
* @example
* await render(AppComponent, {
* childComponentOverrides: [
* {
* component: ChildOfAppComponent,
* providers: [{ provide: MyService, useValue: { hello: 'world' } }]
* }
* ]
* })
*
* @experimental
*/
childComponentOverrides?: ComponentOverride<any>[];
/**
* @description
* A collection of imports to override a standalone component's imports with.
Expand Down Expand Up @@ -273,6 +293,11 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
removeAngularAttributes?: boolean;
}

export interface ComponentOverride<T> {
component: Type<T>;
providers: any[];
}

// eslint-disable-next-line @typescript-eslint/ban-types
export interface RenderTemplateOptions<WrapperType, Properties extends object = {}, Q extends Queries = typeof queries>
extends RenderComponentOptions<Properties, Q> {
Expand Down
10 changes: 9 additions & 1 deletion projects/testing-library/src/lib/testing-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
queries as dtlQueries,
} from '@testing-library/dom';
import type { Queries, BoundFunctions } from '@testing-library/dom';
import { RenderComponentOptions, RenderTemplateOptions, RenderResult } from './models';
import { RenderComponentOptions, RenderTemplateOptions, RenderResult, ComponentOverride } from './models';
import { getConfig } from './config';

const mountedFixtures = new Set<ComponentFixture<any>>();
Expand Down Expand Up @@ -55,6 +55,7 @@ export async function render<SutType, WrapperType = SutType>(
wrapper = WrapperComponent as Type<WrapperType>,
componentProperties = {},
componentProviders = [],
childComponentOverrides = [],
ɵcomponentImports: componentImports,
excludeComponentDeclaration = false,
routes = [],
Expand Down Expand Up @@ -85,6 +86,7 @@ export async function render<SutType, WrapperType = SutType>(
schemas: [...schemas],
});
overrideComponentImports(sut, componentImports);
overrideChildComponentProviders(childComponentOverrides);

await TestBed.compileComponents();

Expand Down Expand Up @@ -282,6 +284,12 @@ function overrideComponentImports<SutType>(sut: Type<SutType> | string, imports:
}
}

function overrideChildComponentProviders(componentOverrides: ComponentOverride<any>[]) {
componentOverrides?.forEach(({ component, providers }) => {
TestBed.overrideComponent(component, { set: { providers } });
});
}

function hasOnChangesHook<SutType>(componentInstance: SutType): componentInstance is SutType & OnChanges {
return (
'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'
Expand Down
81 changes: 60 additions & 21 deletions projects/testing-library/tests/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SimpleChanges,
APP_INITIALIZER,
ApplicationInitStatus,
Injectable,
} from '@angular/core';
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed } from '@angular/core/testing';
Expand All @@ -19,7 +20,7 @@ import { render, fireEvent, screen } from '../src/public_api';
<button>button</button>
`,
})
class FixtureComponent { }
class FixtureComponent {}

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

describe('standalone with child', () => {
@Component({
selector: 'child-fixture',
selector: 'atl-child-fixture',
template: `<span>A child fixture</span>`,
standalone: true,
})
class ChildFixture { }
class ChildFixtureComponent {}

@Component({
selector: 'child-fixture',
selector: 'atl-child-fixture',
template: `<span>A mock child fixture</span>`,
standalone: true,
})
class MockChildFixture { }
class MockChildFixtureComponent {}

@Component({
selector: 'parent-fixture',
selector: 'atl-parent-fixture',
template: `<h1>Parent fixture</h1>
<div><child-fixture></child-fixture></div> `,
<div><atl-child-fixture></atl-child-fixture></div> `,
standalone: true,
imports: [ChildFixture],
imports: [ChildFixtureComponent],
})
class ParentFixture { }
class ParentFixtureComponent {}

it('renders the standalone component with child', async () => {
await render(ParentFixture);
expect(screen.getByText('Parent fixture'));
expect(screen.getByText('A child fixture'));
await render(ParentFixtureComponent);
expect(screen.getByText('Parent fixture')).toBeInTheDocument();
expect(screen.getByText('A child fixture')).toBeInTheDocument();
});

it('renders the standalone component with child', async () => {
await render(ParentFixture, { ɵcomponentImports: [MockChildFixture] });
expect(screen.getByText('Parent fixture'));
expect(screen.getByText('A mock child fixture'));
it('renders the standalone component with child given ɵcomponentImports', async () => {
await render(ParentFixtureComponent, { ɵcomponentImports: [MockChildFixtureComponent] });
expect(screen.getByText('Parent fixture')).toBeInTheDocument();
expect(screen.getByText('A mock child fixture')).toBeInTheDocument();
});

it('rejects render of template with componentImports set', () => {
const result = render(`<div><parent-fixture></parent-fixture></div>`, {
imports: [ParentFixture],
ɵcomponentImports: [MockChildFixture],
const view = render(`<div><atl-parent-fixture></atl-parent-fixture></div>`, {
imports: [ParentFixtureComponent],
ɵcomponentImports: [MockChildFixtureComponent],
});
return expect(view).rejects.toMatchObject({ message: /Error while rendering/ });
});
});

describe('childComponentOverrides', () => {
@Injectable()
class MySimpleService {
public value = 'real';
}

@Component({
selector: 'atl-child-fixture',
template: `<span>{{ simpleService.value }}</span>`,
standalone: true,
providers: [MySimpleService],
})
class NestedChildFixtureComponent {
public constructor(public simpleService: MySimpleService) {}
}

@Component({
selector: 'atl-parent-fixture',
template: `<atl-child-fixture></atl-child-fixture>`,
standalone: true,
imports: [NestedChildFixtureComponent],
})
class ParentFixtureComponent {}

it('renders with overridden child service when specified', async () => {
await render(ParentFixtureComponent, {
childComponentOverrides: [
{
component: NestedChildFixtureComponent,
providers: [{ provide: MySimpleService, useValue: { value: 'fake' } }],
},
],
});
return expect(result).rejects.toMatchObject({ message: /Error while rendering/ });

expect(screen.getByText('fake')).toBeInTheDocument();
});
});

Expand Down Expand Up @@ -117,7 +156,7 @@ describe('animationModule', () => {
@NgModule({
declarations: [FixtureComponent],
})
class FixtureModule { }
class FixtureModule {}
describe('excludeComponentDeclaration', () => {
it('does not throw if component is declared in an imported module', async () => {
await render(FixtureComponent, {
Expand Down

0 comments on commit 6e951ad

Please sign in to comment.