diff --git a/.all-contributorsrc b/.all-contributorsrc index df2ae0b..bc403e2 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -312,6 +312,16 @@ "contributions": [ "code" ] + }, + { + "login": "JJosephttg", + "name": "Josh Joseph", + "avatar_url": "https://avatars.githubusercontent.com/u/23690250?v=4", + "profile": "https://github.com/JJosephttg", + "contributions": [ + "code", + "test" + ] } ], "contributorsPerLine": 7, @@ -319,5 +329,6 @@ "projectOwner": "testing-library", "repoType": "github", "repoHost": "https://github.com", - "skipCi": true + "skipCi": true, + "commitConvention": "angular" } diff --git a/README.md b/README.md index 2d46cd9..231a0a0 100644 --- a/README.md +++ b/README.md @@ -183,48 +183,51 @@ Thanks goes to these people ([emoji key][emojis]): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Tim Deschryver

💻 📖 🚇 ⚠️

Michaël De Boey

📖

Ignacio Le Fluk

💻 ⚠️

Tamás Szabó

💻

Gregor Woiwode

💻

Toni Villena

🐛 💻 📖 ⚠️

ShPelles

📖

Miluoshi

💻 ⚠️

Nick McCurdy

📖

Srinivasan Sekar

📖

Bitcollage

📖

Emil Sundin

💻

Ombrax

💻

Rafael Santana

💻 ⚠️ 🐛

Benjamin Blackwood

📖 ⚠️

Gustavo Porto

📖

Bo Vandersteene

💻

Janek

💻 ⚠️

Gleb Irovich

💻 ⚠️

Arjen

💻 🚧

Suguru Inatomi

💻 🤔

Amit Miran

🚇

Jan-Willem Willebrands

💻

Sandro

💻 🐛

Michael Westphal

💻 ⚠️

Lukas

💻

Matan Borenkraout

🚧

mleimer

📖 ⚠️

MeIr

🐛 ⚠️

John Dengis

💻 ⚠️

Rokas Brazdžionis

💻

Mateus Duraes

💻
Tim Deschryver
Tim Deschryver

💻 📖 🚇 ⚠️
Michaël De Boey
Michaël De Boey

📖
Ignacio Le Fluk
Ignacio Le Fluk

💻 ⚠️
Tamás Szabó
Tamás Szabó

💻
Gregor Woiwode
Gregor Woiwode

💻
Toni Villena
Toni Villena

🐛 💻 📖 ⚠️
ShPelles
ShPelles

📖
Miluoshi
Miluoshi

💻 ⚠️
Nick McCurdy
Nick McCurdy

📖
Srinivasan Sekar
Srinivasan Sekar

📖
Bitcollage
Bitcollage

📖
Emil Sundin
Emil Sundin

💻
Ombrax
Ombrax

💻
Rafael Santana
Rafael Santana

💻 ⚠️ 🐛
Benjamin Blackwood
Benjamin Blackwood

📖 ⚠️
Gustavo Porto
Gustavo Porto

📖
Bo Vandersteene
Bo Vandersteene

💻
Janek
Janek

💻 ⚠️
Gleb Irovich
Gleb Irovich

💻 ⚠️
Arjen
Arjen

💻 🚧
Suguru Inatomi
Suguru Inatomi

💻 🤔
Amit Miran
Amit Miran

🚇
Jan-Willem Willebrands
Jan-Willem Willebrands

💻
Sandro
Sandro

💻 🐛
Michael Westphal
Michael Westphal

💻 ⚠️
Lukas
Lukas

💻
Matan Borenkraout
Matan Borenkraout

🚧
mleimer
mleimer

📖 ⚠️
MeIr
MeIr

🐛 ⚠️
John Dengis
John Dengis

💻 ⚠️
Rokas Brazdžionis
Rokas Brazdžionis

💻
Mateus Duraes
Mateus Duraes

💻
Josh Joseph
Josh Joseph

💻 ⚠️
diff --git a/apps/example-app/src/app/issues/issue-316.spec.ts b/apps/example-app/src/app/issues/issue-316.spec.ts new file mode 100644 index 0000000..4dd3881 --- /dev/null +++ b/apps/example-app/src/app/issues/issue-316.spec.ts @@ -0,0 +1,78 @@ +import { Component, Input } from '@angular/core' +import { render, screen } from '@testing-library/angular' + +describe('TagComponent', () => { + it('create a tag default primary', async () => { + await render(`primary`, { + declarations: [TagComponent], + }) + + expect(screen.getByText(/primary/i)).toBeInTheDocument() + expect(screen.getByRole('tag')).toHaveClass('hc-tag-primary') + }) +}) + + +@Component({ + selector: 'hc-tag', + template: ` + + + + `, + styles: [ + ` + .hc-tag { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--neutral-white); + padding: 4px 10px; + border-radius: 6px; + } + + .hc-tag-primary { + background-color: var(--primary-default); + } + + .hc-tag-success { + background-color: var(--green-default); + } + + .hc-tag-info { + background-color: var(--primary-default); + } + + .hc-tag-warning { + background-color: var(--yellow-default); + } + + .hc-tag-danger { + background-color: var(--red-default); + } + + .hc-tag-rounded { + border-radius: 10rem; + } + `, + ], +}) +export class TagComponent { + @Input() severity: 'success' | 'info' | 'warning' | 'danger' | 'primary' = 'primary' + @Input() rounded = false + @Input() ariaLabel?: string + + get classes() { + return { + ['hc-tag']: true, + [`hc-tag-${this.severity}`]: true, + ['hc-tag-rounded']: this.rounded, + } + } +} + 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, {