Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Added childComponentOverrides property to override nested child providers #332

Merged
merged 1 commit into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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