Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3095737

Browse files
feat: add strongly typed inputs (testing-library#473)
Closes testing-library#464 Closes testing-library#474
1 parent 40fe4ea commit 3095737

File tree

8 files changed

+212
-44
lines changed

8 files changed

+212
-44
lines changed

‎README.md‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,44 +100,53 @@ counter.component.ts
100100
@Component({
101101
selector: 'app-counter',
102102
template: `
103+
<span>{{ hello() }}</span>
103104
<button (click)="decrement()">-</button>
104-
<span>Current Count: {{ counter }}</span>
105+
<span>Current Count: {{ counter() }}</span>
105106
<button (click)="increment()">+</button>
106107
`,
107108
})
108109
export class CounterComponent {
109-
@Input() counter = 0;
110+
counter = model(0);
111+
hello = input('Hi', { alias: 'greeting' });
110112

111113
increment() {
112-
this.counter+=1;
114+
this.counter.set(this.counter() +1);
113115
}
114116

115117
decrement() {
116-
this.counter-=1;
118+
this.counter.set(this.counter() -1);
117119
}
118120
}
119121
```
120122

121123
counter.component.spec.ts
122124

123125
```typescript
124-
import { render, screen, fireEvent } from '@testing-library/angular';
126+
import { render, screen, fireEvent, aliasedInput } from '@testing-library/angular';
125127
import { CounterComponent } from './counter.component';
126128

127129
describe('Counter', () => {
128-
test('should render counter', async () => {
129-
await render(CounterComponent, { componentProperties: { counter: 5 } });
130-
131-
expect(screen.getByText('Current Count: 5'));
130+
it('should render counter', async () => {
131+
await render(CounterComponent, {
132+
inputs: {
133+
counter: 5,
134+
// aliases need to be specified this way
135+
...aliasedInput('greeting', 'Hello Alias!'),
136+
},
137+
});
138+
139+
expect(screen.getByText('Current Count: 5')).toBeVisible();
140+
expect(screen.getByText('Hello Alias!')).toBeVisible();
132141
});
133142

134-
test('should increment the counter on click', async () => {
135-
await render(CounterComponent, { componentProperties: { counter: 5 } });
143+
it('should increment the counter on click', async () => {
144+
await render(CounterComponent, { inputs: { counter: 5 } });
136145

137146
const incrementButton = screen.getByRole('button', { name: '+' });
138147
fireEvent.click(incrementButton);
139148

140-
expect(screen.getByText('Current Count: 6'));
149+
expect(screen.getByText('Current Count: 6')).toBeVisible();
141150
});
142151
});
143152
```

‎apps/example-app/src/app/examples/02-input-output.spec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test('is possible to set input and listen for output', async () => {
88
const sendValue = jest.fn();
99

1010
await render(InputOutputComponent, {
11-
componentInputs: {
11+
inputs: {
1212
value: 47,
1313
},
1414
on: {
@@ -64,7 +64,7 @@ test('is possible to set input and listen for output (deprecated)', async () =>
6464
const sendValue = jest.fn();
6565

6666
await render(InputOutputComponent, {
67-
componentInputs: {
67+
inputs: {
6868
value: 47,
6969
},
7070
componentOutputs: {

‎apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { render, screen, within } from '@testing-library/angular';
1+
import { aliasedInput,render, screen, within } from '@testing-library/angular';
22
import { SignalInputComponent } from './22-signal-inputs.component';
33
import userEvent from '@testing-library/user-event';
44

55
test('works with signal inputs', async () => {
66
await render(SignalInputComponent, {
7-
componentInputs: {
8-
greeting: 'Hello',
7+
inputs: {
8+
...aliasedInput('greeting', 'Hello'),
99
name: 'world',
1010
},
1111
});
@@ -16,8 +16,8 @@ test('works with signal inputs', async () => {
1616

1717
test('works with computed', async () => {
1818
await render(SignalInputComponent, {
19-
componentInputs: {
20-
greeting: 'Hello',
19+
inputs: {
20+
...aliasedInput('greeting', 'Hello'),
2121
name: 'world',
2222
},
2323
});
@@ -28,8 +28,8 @@ test('works with computed', async () => {
2828

2929
test('can update signal inputs', async () => {
3030
const { fixture } = await render(SignalInputComponent, {
31-
componentInputs: {
32-
greeting: 'Hello',
31+
inputs: {
32+
...aliasedInput('greeting', 'Hello'),
3333
name: 'world',
3434
},
3535
});
@@ -51,8 +51,8 @@ test('can update signal inputs', async () => {
5151
test('output emits a value', async () => {
5252
const submitFn = jest.fn();
5353
await render(SignalInputComponent, {
54-
componentInputs: {
55-
greeting: 'Hello',
54+
inputs: {
55+
...aliasedInput('greeting', 'Hello'),
5656
name: 'world',
5757
},
5858
on: {
@@ -67,8 +67,8 @@ test('output emits a value', async () => {
6767

6868
test('model update also updates the template', async () => {
6969
const { fixture } = await render(SignalInputComponent, {
70-
componentInputs: {
71-
greeting: 'Hello',
70+
inputs: {
71+
...aliasedInput('greeting', 'Hello'),
7272
name: 'initial',
7373
},
7474
});
@@ -97,8 +97,8 @@ test('model update also updates the template', async () => {
9797

9898
test('works with signal inputs, computed values, and rerenders', async () => {
9999
const view = await render(SignalInputComponent, {
100-
componentInputs: {
101-
greeting: 'Hello',
100+
inputs: {
101+
...aliasedInput('greeting', 'Hello'),
102102
name: 'world',
103103
},
104104
});
@@ -110,8 +110,8 @@ test('works with signal inputs, computed values, and rerenders', async () => {
110110
expect(computedValue.getByText(/helloworld/i)).toBeInTheDocument();
111111

112112
await view.rerender({
113-
componentInputs: {
114-
greeting: 'bye',
113+
inputs: {
114+
...aliasedInput('greeting', 'bye'),
115115
name: 'test',
116116
},
117117
});

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

Lines changed: 42 additions & 2 deletions
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';
@@ -68,7 +68,7 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
6868
rerender: (
6969
properties?: Pick<
7070
RenderTemplateOptions<ComponentType>,
71-
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
71+
'componentProperties' | 'componentInputs' | 'inputs'|'componentOutputs' | 'on' | 'detectChangesOnRender'
7272
> & { partialUpdate?: boolean },
7373
) => Promise<void>;
7474
/**
@@ -78,6 +78,27 @@ 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> ? U : T[P];
90+
}
91+
| AliasedInputs;
92+
93+
/**
94+
* @description
95+
* Creates an aliased input branded type with a value
96+
*
97+
*/
98+
export function aliasedInput<TAlias extends string, T>(alias: TAlias, value: T): Record<TAlias, AliasedInput<T>> {
99+
return { [alias]: value } as Record<TAlias, AliasedInput<T>>;
100+
}
101+
81102
export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {
82103
/**
83104
* @description
@@ -199,6 +220,7 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
199220
* @description
200221
* An object to set `@Input` properties of the component
201222
*
223+
* @deprecated use the `inputs` option instead. When you need to use aliases, use the `aliasedInput(...)` helper function.
202224
* @default
203225
* {}
204226
*
@@ -210,6 +232,24 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
210232
* })
211233
*/
212234
componentInputs?: Partial<ComponentType> | { [alias: string]: unknown };
235+
236+
/**
237+
* @description
238+
* An object to set `@Input` or `input()` properties of the component
239+
*
240+
* @default
241+
* {}
242+
*
243+
* @example
244+
* await render(AppComponent, {
245+
* inputs: {
246+
* counterValue: 10,
247+
* // explicitly define aliases this way:
248+
* ...aliasedInput('someAlias', 'someValue')
249+
* })
250+
*/
251+
inputs?: ComponentInput<ComponentType>;
252+
213253
/**
214254
* @description
215255
* An object to set `@Output` properties of the component

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

Lines changed: 7 additions & 4 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)) {
@@ -239,10 +242,10 @@ export async function render<SutType, WrapperType = SutType>(
239242
const rerender = async (
240243
properties?: Pick<
241244
RenderTemplateOptions<SutType>,
242-
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
245+
'componentProperties' | 'componentInputs' | 'inputs'|'componentOutputs' | 'on' | 'detectChangesOnRender'
243246
> & { partialUpdate?: boolean },
244247
) => {
245-
const newComponentInputs = properties?.componentInputs??{};
248+
const newComponentInputs = { ...properties?.componentInputs, ...properties?.inputs};
246249
const changesInComponentInput = update(
247250
fixture,
248251
renderedInputKeys,

‎projects/testing-library/tests/integrations/ng-mocks.spec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { NgIf } from '@angular/common';
88
test('sends the correct value to the child input', async () => {
99
const utils = await render(TargetComponent, {
1010
imports: [MockComponent(ChildComponent)],
11-
componentInputs: { value: 'foo' },
11+
inputs: { value: 'foo' },
1212
});
1313

1414
const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
@@ -21,7 +21,7 @@ test('sends the correct value to the child input', async () => {
2121
test('sends the correct value to the child input 2', async () => {
2222
const utils = await render(TargetComponent, {
2323
imports: [MockComponent(ChildComponent)],
24-
componentInputs: { value: 'bar' },
24+
inputs: { value: 'bar' },
2525
});
2626

2727
const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /