Skip to content

Commit c12b358

Browse files
committed
refactor: stronger typing of inputs
1 parent 40fe4ea commit c12b358

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

projects/testing-library/src/lib/models.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, DebugElement, OutputRef, EventEmitter } from '@angular/core';
1+
import { Type, DebugElement, OutputRef, EventEmitter, Signal } from '@angular/core';
22
import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed } from '@angular/core/testing';
33
import { Routes } from '@angular/router';
44
import { BoundFunction, Queries, queries, Config as dtlConfig, PrettyDOMOptions } from '@testing-library/dom';
@@ -78,6 +78,29 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
7878
renderDeferBlock: (deferBlockState: DeferBlockState, deferBlockIndex?: number) => Promise<void>;
7979
}
8080

81+
declare const ALIASED_INPUT_BRAND: unique symbol;
82+
export type AliasedInput<T> = T & {
83+
[ALIASED_INPUT_BRAND]: T;
84+
};
85+
export type AliasedInputs = Record<string, AliasedInput<unknown>>;
86+
87+
export type ComponentInput<T> =
88+
| {
89+
[P in keyof T]?: T[P] extends Signal<infer U>
90+
? U // If the property is a Signal, apply Partial to the inner type
91+
: Partial<T[P]>; // Otherwise, apply Partial to the property itself
92+
}
93+
| AliasedInputs;
94+
95+
/**
96+
* @description
97+
* Creates an aliased input branded type with a value
98+
*
99+
*/
100+
export function aliasedInputWithValue<T>(value: T): AliasedInput<T> {
101+
return value as AliasedInput<T>;
102+
}
103+
81104
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {
82105
/**
83106
* @description
@@ -199,6 +222,7 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
199222
* @description
200223
* An object to set `@Input` properties of the component
201224
*
225+
* @deprecated use the `inputs` option instead. When you need to use aliases, use the `aliasedInputWithValue(...)` helper function.
202226
* @default
203227
* {}
204228
*
@@ -210,6 +234,23 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
210234
* })
211235
*/
212236
componentInputs?: Partial<ComponentType> | { [alias: string]: unknown };
237+
238+
/**
239+
* @description
240+
* An object to set `@Input` or `input()` properties of the component
241+
*
242+
* @default
243+
* {}
244+
*
245+
* @example
246+
* await render(AppComponent, {
247+
* inputs: {
248+
* counterValue: 10,
249+
* someAlias: aliasedInputWithValue('value')
250+
* }
251+
*/
252+
inputs?: ComponentInput<ComponentType>;
253+
213254
/**
214255
* @description
215256
* An object to set `@Output` properties of the component

projects/testing-library/src/lib/testing-library.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export async function render<SutType, WrapperType = SutType>(
6767
componentProperties = {},
6868
componentInputs = {},
6969
componentOutputs = {},
70+
inputs: newInputs = {},
7071
on = {},
7172
componentProviders = [],
7273
childComponentOverrides = [],
@@ -176,8 +177,10 @@ export async function render<SutType, WrapperType = SutType>(
176177

177178
let detectChanges: () => void;
178179

180+
const allInputs = { ...componentInputs, ...newInputs };
181+
179182
let renderedPropKeys = Object.keys(componentProperties);
180-
let renderedInputKeys = Object.keys(componentInputs);
183+
let renderedInputKeys = Object.keys(allInputs);
181184
let renderedOutputKeys = Object.keys(componentOutputs);
182185
let subscribedOutputs: SubscribedOutput<SutType>[] = [];
183186

@@ -224,7 +227,7 @@ export async function render<SutType, WrapperType = SutType>(
224227
return createdFixture;
225228
};
226229

227-
const fixture = await renderFixture(componentProperties, componentInputs, componentOutputs, on);
230+
const fixture = await renderFixture(componentProperties, allInputs, componentOutputs, on);
228231

229232
if (deferBlockStates) {
230233
if (Array.isArray(deferBlockStates)) {

projects/testing-library/tests/render.spec.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import {
1313
ElementRef,
1414
inject,
1515
output,
16+
input,
1617
} from '@angular/core';
1718
import { outputFromObservable } from '@angular/core/rxjs-interop';
1819
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
1920
import { TestBed } from '@angular/core/testing';
20-
import { render, fireEvent, screen, OutputRefKeysWithCallback } from '../src/public_api';
21+
import { render, fireEvent, screen, OutputRefKeysWithCallback, aliasedInputWithValue } from '../src/public_api';
2122
import { ActivatedRoute, Resolve, RouterModule } from '@angular/router';
2223
import { fromEvent, map } from 'rxjs';
2324
import { AsyncPipe, NgIf } from '@angular/common';
@@ -533,3 +534,46 @@ describe('configureTestBed', () => {
533534
expect(configureTestBedFn).toHaveBeenCalledTimes(1);
534535
});
535536
});
537+
538+
describe('inputs and signals', () => {
539+
@Component({
540+
selector: 'atl-fixture',
541+
template: `<span>{{ myName() }}</span> <span>{{ myJob() }}</span>`,
542+
})
543+
class InputComponent {
544+
myName = input('foo');
545+
546+
myJob = input('bar', { alias: 'job' });
547+
}
548+
549+
it('should set the input component', async () => {
550+
await render(InputComponent, {
551+
inputs: {
552+
myName: 'Bob',
553+
job: aliasedInputWithValue('Builder'),
554+
},
555+
});
556+
557+
expect(screen.getByText('Bob')).toBeInTheDocument();
558+
expect(screen.getByText('Builder')).toBeInTheDocument();
559+
});
560+
561+
it('should typecheck correctly', async () => {
562+
// @ts-expect-error - myName is a string
563+
await render(InputComponent, {
564+
inputs: {
565+
myName: 123,
566+
},
567+
});
568+
569+
// @ts-expect-error - job is not using aliasedInputWithValue
570+
await render(InputComponent, {
571+
inputs: {
572+
job: 'not used with aliasedInputWithValue',
573+
},
574+
});
575+
576+
// add a statement so the test succeeds
577+
expect(true).toBeTruthy();
578+
});
579+
});

0 commit comments

Comments
 (0)