From 48629f71e9601211d9d8729810c87625c6e3d949 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:53:53 +0200 Subject: [PATCH 1/4] test: add integration test with ng-mocks (#402) --- package.json | 1 + .../tests/integrations/ng-mocks.spec.ts | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 projects/testing-library/tests/integrations/ng-mocks.spec.ts diff --git a/package.json b/package.json index 858b01a..fc7af3d 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "karma-jasmine": "5.1.0", "karma-jasmine-html-reporter": "2.0.0", "lint-staged": "^12.1.6", + "ng-mocks": "^14.11.0", "ng-packagr": "16.0.0", "nx": "16.1.4", "postcss": "^8.4.5", diff --git a/projects/testing-library/tests/integrations/ng-mocks.spec.ts b/projects/testing-library/tests/integrations/ng-mocks.spec.ts new file mode 100644 index 0000000..a3f141b --- /dev/null +++ b/projects/testing-library/tests/integrations/ng-mocks.spec.ts @@ -0,0 +1,62 @@ +import { Component, ContentChild, EventEmitter, Input, Output, TemplateRef } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +import { MockComponent } from 'ng-mocks'; +import { render } from '../../src/public_api'; + +test('sends the correct value to the child input', async () => { + const utils = await render(TargetComponent, { + imports: [MockComponent(ChildComponent)], + componentInputs: { value: 'foo' }, + }); + + const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent)); + expect(children).toHaveLength(1); + + const mockComponent = children[0].componentInstance; + expect(mockComponent.someInput).toBe('foo'); +}); + +test('sends the correct value to the child input 2', async () => { + const utils = await render(TargetComponent, { + imports: [MockComponent(ChildComponent)], + componentInputs: { value: 'bar' }, + }); + + const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent)); + expect(children).toHaveLength(1); + + const mockComponent = children[0].componentInstance; + expect(mockComponent.someInput).toBe('bar'); +}); + +@Component({ + selector: 'atl-child', + template: 'child', + standalone: true, +}) +class ChildComponent { + @ContentChild('something') + public injectedSomething: TemplateRef | undefined; + + @Input() + public someInput = ''; + + @Output() + public someOutput = new EventEmitter(); + + public childMockComponent() { + /* noop */ + } +} + +@Component({ + selector: 'atl-target-mock-component', + template: ` `, + standalone: true, + imports: [ChildComponent], +}) +class TargetComponent { + @Input() value = ''; + public trigger = (obj: any) => obj; +} From 72203cecbaa3c2abd403de1a8ef7c04868ce47f3 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:50:26 +0200 Subject: [PATCH 2/4] docs: reproduce 397 (#401) --- ...irective-overrides-component-input.spec.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts diff --git a/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts new file mode 100644 index 0000000..c2a02a8 --- /dev/null +++ b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts @@ -0,0 +1,67 @@ +import { Component, Directive, Input, OnInit } from '@angular/core'; +import { render, screen } from '../../src/public_api'; + +test('the value set in the directive constructor is overriden by the input binding', async () => { + await render(``, { + imports: [FixtureComponent, InputOverrideViaConstructorDirective], + }); + + expect(screen.getByText('set by test')).toBeInTheDocument(); +}); + +test('the value set in the directive onInit is used instead of the input binding', async () => { + await render(``, { + imports: [FixtureComponent, InputOverrideViaOnInitDirective], + }); + + expect(screen.getByText('set by directive ngOnInit')).toBeInTheDocument(); +}); + +test('the value set in the directive constructor is used instead of the input value', async () => { + await render(``, { + imports: [FixtureComponent, InputOverrideViaConstructorDirective], + }); + + expect(screen.getByText('set by directive constructor')).toBeInTheDocument(); +}); + +test('the value set in the directive ngOnInit is used instead of the input value and the directive constructor', async () => { + await render(``, { + imports: [FixtureComponent, InputOverrideViaConstructorDirective, InputOverrideViaOnInitDirective], + }); + + expect(screen.getByText('set by directive ngOnInit')).toBeInTheDocument(); +}); + +@Component({ + standalone: true, + selector: 'atl-fixture', + template: `{{ input }}`, +}) +class FixtureComponent { + @Input() public input = 'default value'; +} + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'atl-fixture', + standalone: true, +}) +class InputOverrideViaConstructorDirective { + constructor(private fixture: FixtureComponent) { + this.fixture.input = 'set by directive constructor'; + } +} + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'atl-fixture', + standalone: true, +}) +class InputOverrideViaOnInitDirective implements OnInit { + constructor(private fixture: FixtureComponent) {} + + ngOnInit(): void { + this.fixture.input = 'set by directive ngOnInit'; + } +} From b6fd475f2310ae301fbb4a49177134f8b18e8f34 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Mon, 14 Aug 2023 19:15:14 +0200 Subject: [PATCH 3/4] docs: add version compatibility (#408) Closes #388 --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index cee1977..82a515b 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ practices.

- [This solution](#this-solution) - [Example](#example) - [Installation](#installation) +- [Version compatibility](#version-compatibility) - [Guiding Principles](#guiding-principles) - [Contributors](#contributors) - [Docs](#docs) @@ -159,6 +160,15 @@ You may also be interested in installing `jest-dom` so you can use > [**Docs**](https://testing-library.com/angular) +## Version compatibility + +| Angular | Angular Testing Library | +| ------- | ----------------------- | +| 16.x | 13.x, 14.x | +| >= 15.1 | 13.x \|\| 14.x | +| < 15.1 | 11.x \|\| 12.x | +| 14.x | 11.x \|\| 12.x | + ## Guiding Principles > [The more your tests resemble the way your software is used, the more From b92a959b5049232f8cdf50647c35f0ada958a581 Mon Sep 17 00:00:00 2001 From: Jan-Willem Baart Date: Thu, 17 Aug 2023 18:48:11 +0200 Subject: [PATCH 4/4] feat: accept query params in initialRoute (#409) Closes #407 --- .../src/lib/testing-library.ts | 77 ++++++++++--------- .../tests/integrations/ng-mocks.spec.ts | 2 + projects/testing-library/tests/render.spec.ts | 28 ++++++- 3 files changed, 69 insertions(+), 38 deletions(-) diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 974402f..48af2d5 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -112,8 +112,45 @@ export async function render( const zone = safeInject(NgZone); const router = safeInject(Router); + const _navigate = async (elementOrPath: Element | string, basePath = ''): Promise => { + const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href'); + const [path, params] = (basePath + href).split('?'); + const queryParams = params + ? params.split('&').reduce((qp, q) => { + const [key, value] = q.split('='); + const currentValue = qp[key]; + if (typeof currentValue === 'undefined') { + qp[key] = value; + } else if (Array.isArray(currentValue)) { + qp[key] = [...currentValue, value]; + } else { + qp[key] = [currentValue, value]; + } + return qp; + }, {} as Record) + : undefined; - if (initialRoute) await router.navigate([initialRoute]); + const navigateOptions: NavigationExtras | undefined = queryParams + ? { + queryParams, + } + : undefined; + + const doNavigate = () => { + return navigateOptions ? router?.navigate([path], navigateOptions) : router?.navigate([path]); + }; + + let result; + + if (zone) { + await zone.run(() => (result = doNavigate())); + } else { + result = doNavigate(); + } + return result ?? false; + }; + + if (initialRoute) await _navigate(initialRoute); if (typeof router?.initialNavigation === 'function') { if (zone) { @@ -167,43 +204,9 @@ export async function render( }; const navigate = async (elementOrPath: Element | string, basePath = ''): Promise => { - const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href'); - const [path, params] = (basePath + href).split('?'); - const queryParams = params - ? params.split('&').reduce((qp, q) => { - const [key, value] = q.split('='); - const currentValue = qp[key]; - if (typeof currentValue === 'undefined') { - qp[key] = value; - } else if (Array.isArray(currentValue)) { - qp[key] = [...currentValue, value]; - } else { - qp[key] = [currentValue, value]; - } - return qp; - }, {} as Record) - : undefined; - - const navigateOptions: NavigationExtras | undefined = queryParams - ? { - queryParams, - } - : undefined; - - const doNavigate = () => { - return navigateOptions ? router?.navigate([path], navigateOptions) : router?.navigate([path]); - }; - - let result; - - if (zone) { - await zone.run(() => (result = doNavigate())); - } else { - result = doNavigate(); - } - + const result = await _navigate(elementOrPath, basePath); detectChanges(); - return result ?? false; + return result; }; return { diff --git a/projects/testing-library/tests/integrations/ng-mocks.spec.ts b/projects/testing-library/tests/integrations/ng-mocks.spec.ts index a3f141b..6358485 100644 --- a/projects/testing-library/tests/integrations/ng-mocks.spec.ts +++ b/projects/testing-library/tests/integrations/ng-mocks.spec.ts @@ -3,6 +3,7 @@ import { By } from '@angular/platform-browser'; import { MockComponent } from 'ng-mocks'; import { render } from '../../src/public_api'; +import { NgIf } from '@angular/common'; test('sends the correct value to the child input', async () => { const utils = await render(TargetComponent, { @@ -34,6 +35,7 @@ test('sends the correct value to the child input 2', async () => { selector: 'atl-child', template: 'child', standalone: true, + imports: [NgIf], }) class ChildComponent { @ContentChild('something') diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 4b546e0..f7b1927 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -14,7 +14,9 @@ import { import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TestBed } from '@angular/core/testing'; import { render, fireEvent, screen } from '../src/public_api'; -import { Resolve, RouterModule } from '@angular/router'; +import { ActivatedRoute, Resolve, RouterModule } from '@angular/router'; +import { map } from 'rxjs'; +import { AsyncPipe, NgIf } from '@angular/common'; @Component({ selector: 'atl-fixture', @@ -365,6 +367,30 @@ describe('initialRoute', () => { expect(screen.queryByText('Secondary Component')).not.toBeInTheDocument(); expect(screen.getByText('button')).toBeInTheDocument(); }); + + it('allows initially rendering a specific route with query parameters', async () => { + @Component({ + standalone: true, + selector: 'atl-query-param-fixture', + template: `

paramPresent$: {{ paramPresent$ | async }}

`, + imports: [NgIf, AsyncPipe], + }) + class QueryParamFixtureComponent { + constructor(public route: ActivatedRoute) {} + + paramPresent$ = this.route.queryParams.pipe(map((queryParams) => (queryParams?.param ? 'present' : 'missing'))); + } + + const initialRoute = 'initial-route?param=query'; + const routes = [{ path: 'initial-route', component: QueryParamFixtureComponent }]; + + await render(RouterFixtureComponent, { + initialRoute, + routes, + }); + + expect(screen.getByText(/present/i)).toBeVisible(); + }); }); describe('configureTestBed', () => {