From 6e951ad36366310551b128a406187862730e16f2 Mon Sep 17 00:00:00 2001 From: Josh Joseph Date: Mon, 28 Nov 2022 11:09:55 -0500 Subject: [PATCH] feat: childComponentOverrides property to override nested child providers (#332) --- projects/testing-library/src/lib/models.ts | 25 ++++++ .../src/lib/testing-library.ts | 10 ++- projects/testing-library/tests/render.spec.ts | 81 ++++++++++++++----- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 953c244..1f33192 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -186,6 +186,26 @@ export interface RenderComponentOptions[]; /** * @description * A collection of imports to override a standalone component's imports with. @@ -273,6 +293,11 @@ export interface RenderComponentOptions { + component: Type; + providers: any[]; +} + // eslint-disable-next-line @typescript-eslint/ban-types export interface RenderTemplateOptions extends RenderComponentOptions { diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 933e982..24e2085 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -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>(); @@ -55,6 +55,7 @@ export async function render( wrapper = WrapperComponent as Type, componentProperties = {}, componentProviders = [], + childComponentOverrides = [], ɵcomponentImports: componentImports, excludeComponentDeclaration = false, routes = [], @@ -85,6 +86,7 @@ export async function render( schemas: [...schemas], }); overrideComponentImports(sut, componentImports); + overrideChildComponentProviders(childComponentOverrides); await TestBed.compileComponents(); @@ -282,6 +284,12 @@ function overrideComponentImports(sut: Type | string, imports: } } +function overrideChildComponentProviders(componentOverrides: ComponentOverride[]) { + componentOverrides?.forEach(({ component, providers }) => { + TestBed.overrideComponent(component, { set: { providers } }); + }); +} + function hasOnChangesHook(componentInstance: SutType): componentInstance is SutType & OnChanges { return ( 'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function' diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 1faec70..4200bc9 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -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'; @@ -19,7 +20,7 @@ import { render, fireEvent, screen } from '../src/public_api'; `, }) -class FixtureComponent { } +class FixtureComponent {} test('creates queries and events', async () => { const view = await render(FixtureComponent); @@ -50,46 +51,84 @@ describe('standalone', () => { describe('standalone with child', () => { @Component({ - selector: 'child-fixture', + selector: 'atl-child-fixture', template: `A child fixture`, standalone: true, }) - class ChildFixture { } + class ChildFixtureComponent {} @Component({ - selector: 'child-fixture', + selector: 'atl-child-fixture', template: `A mock child fixture`, standalone: true, }) - class MockChildFixture { } + class MockChildFixtureComponent {} @Component({ - selector: 'parent-fixture', + selector: 'atl-parent-fixture', template: `

Parent fixture

-
`, +
`, 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(`
`, { - imports: [ParentFixture], - ɵcomponentImports: [MockChildFixture], + const view = render(`
`, { + 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: `{{ simpleService.value }}`, + standalone: true, + providers: [MySimpleService], + }) + class NestedChildFixtureComponent { + public constructor(public simpleService: MySimpleService) {} + } + + @Component({ + selector: 'atl-parent-fixture', + template: ``, + 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(); }); }); @@ -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, {