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', () => {