Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: stronger typing of inputs #473

Merged
merged 11 commits into from
Aug 3, 2024
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,44 +100,53 @@ counter.component.ts
@Component({
selector: 'app-counter',
template: `
<span>{{ hello() }}</span>
<button (click)="decrement()">-</button>
<span>Current Count: {{ counter }}</span>
<span>Current Count: {{ counter() }}</span>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
@Input() counter = 0;
counter = model(0);
hello = input('Hi', { alias: 'greeting' });

increment() {
this.counter += 1;
this.counter.set(this.counter() + 1);
}

decrement() {
this.counter -= 1;
this.counter.set(this.counter() - 1);
}
}
```

counter.component.spec.ts

```typescript
import { render, screen, fireEvent } from '@testing-library/angular';
import { render, screen, fireEvent, aliasedInput } from '@testing-library/angular';
import { CounterComponent } from './counter.component';

describe('Counter', () => {
test('should render counter', async () => {
await render(CounterComponent, { componentProperties: { counter: 5 } });

expect(screen.getByText('Current Count: 5'));
it('should render counter', async () => {
await render(CounterComponent, {
inputs: {
counter: 5,
// aliases need to be specified this way
...aliasedInput('greeting', 'Hello Alias!'),
},
});

expect(screen.getByText('Current Count: 5')).toBeVisible();
expect(screen.getByText('Hello Alias!')).toBeVisible();
});

test('should increment the counter on click', async () => {
await render(CounterComponent, { componentProperties: { counter: 5 } });
it('should increment the counter on click', async () => {
await render(CounterComponent, { inputs: { counter: 5 } });

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

expect(screen.getByText('Current Count: 6'));
expect(screen.getByText('Current Count: 6')).toBeVisible();
});
});
```
Expand Down
4 changes: 2 additions & 2 deletions apps/example-app/src/app/examples/02-input-output.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test('is possible to set input and listen for output', async () => {
const sendValue = jest.fn();

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

await render(InputOutputComponent, {
componentInputs: {
inputs: {
value: 47,
},
componentOutputs: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { render, screen, within } from '@testing-library/angular';
import { aliasedInput, render, screen, within } from '@testing-library/angular';
import { SignalInputComponent } from './22-signal-inputs.component';
import userEvent from '@testing-library/user-event';

test('works with signal inputs', async () => {
await render(SignalInputComponent, {
componentInputs: {
greeting: 'Hello',
inputs: {
...aliasedInput('greeting', 'Hello'),
name: 'world',
},
});
Expand All @@ -16,8 +16,8 @@ test('works with signal inputs', async () => {

test('works with computed', async () => {
await render(SignalInputComponent, {
componentInputs: {
greeting: 'Hello',
inputs: {
...aliasedInput('greeting', 'Hello'),
name: 'world',
},
});
Expand All @@ -28,8 +28,8 @@ test('works with computed', async () => {

test('can update signal inputs', async () => {
const { fixture } = await render(SignalInputComponent, {
componentInputs: {
greeting: 'Hello',
inputs: {
...aliasedInput('greeting', 'Hello'),
name: 'world',
},
});
Expand All @@ -51,8 +51,8 @@ test('can update signal inputs', async () => {
test('output emits a value', async () => {
const submitFn = jest.fn();
await render(SignalInputComponent, {
componentInputs: {
greeting: 'Hello',
inputs: {
...aliasedInput('greeting', 'Hello'),
name: 'world',
},
on: {
Expand All @@ -67,8 +67,8 @@ test('output emits a value', async () => {

test('model update also updates the template', async () => {
const { fixture } = await render(SignalInputComponent, {
componentInputs: {
greeting: 'Hello',
inputs: {
...aliasedInput('greeting', 'Hello'),
name: 'initial',
},
});
Expand Down Expand Up @@ -97,8 +97,8 @@ test('model update also updates the template', async () => {

test('works with signal inputs, computed values, and rerenders', async () => {
const view = await render(SignalInputComponent, {
componentInputs: {
greeting: 'Hello',
inputs: {
...aliasedInput('greeting', 'Hello'),
name: 'world',
},
});
Expand All @@ -110,8 +110,8 @@ test('works with signal inputs, computed values, and rerenders', async () => {
expect(computedValue.getByText(/hello world/i)).toBeInTheDocument();

await view.rerender({
componentInputs: {
greeting: 'bye',
inputs: {
...aliasedInput('greeting', 'bye'),
name: 'test',
},
});
Expand Down
44 changes: 42 additions & 2 deletions projects/testing-library/src/lib/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, DebugElement, OutputRef, EventEmitter } from '@angular/core';
import { Type, DebugElement, OutputRef, EventEmitter, Signal } from '@angular/core';
import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed } from '@angular/core/testing';
import { Routes } from '@angular/router';
import { BoundFunction, Queries, queries, Config as dtlConfig, PrettyDOMOptions } from '@testing-library/dom';
Expand Down Expand Up @@ -68,7 +68,7 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
rerender: (
properties?: Pick<
RenderTemplateOptions<ComponentType>,
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
'componentProperties' | 'componentInputs' | 'inputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
> & { partialUpdate?: boolean },
) => Promise<void>;
/**
Expand All @@ -78,6 +78,27 @@ export interface RenderResult<ComponentType, WrapperType = ComponentType> extend
renderDeferBlock: (deferBlockState: DeferBlockState, deferBlockIndex?: number) => Promise<void>;
}

declare const ALIASED_INPUT_BRAND: unique symbol;
export type AliasedInput<T> = T & {
[ALIASED_INPUT_BRAND]: T;
};
export type AliasedInputs = Record<string, AliasedInput<unknown>>;

export type ComponentInput<T> =
| {
[P in keyof T]?: T[P] extends Signal<infer U> ? U : T[P];
}
| AliasedInputs;

/**
* @description
* Creates an aliased input branded type with a value
*
*/
export function aliasedInput<TAlias extends string, T>(alias: TAlias, value: T): Record<TAlias, AliasedInput<T>> {
return { [alias]: value } as Record<TAlias, AliasedInput<T>>;
}

export interface RenderComponentOptions<ComponentType, Q extends Queries = typeof queries> {
/**
* @description
Expand Down Expand Up @@ -199,6 +220,7 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
* @description
* An object to set `@Input` properties of the component
*
* @deprecated use the `inputs` option instead. When you need to use aliases, use the `aliasedInput(...)` helper function.
* @default
* {}
*
Expand All @@ -210,6 +232,24 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
* })
*/
componentInputs?: Partial<ComponentType> | { [alias: string]: unknown };

/**
* @description
* An object to set `@Input` or `input()` properties of the component
*
* @default
* {}
*
* @example
* await render(AppComponent, {
* inputs: {
* counterValue: 10,
* // explicitly define aliases this way:
* ...aliasedInput('someAlias', 'someValue')
* })
*/
inputs?: ComponentInput<ComponentType>;
timdeschryver marked this conversation as resolved.
Show resolved Hide resolved

/**
* @description
* An object to set `@Output` properties of the component
Expand Down
11 changes: 7 additions & 4 deletions projects/testing-library/src/lib/testing-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function render<SutType, WrapperType = SutType>(
componentProperties = {},
componentInputs = {},
componentOutputs = {},
inputs: newInputs = {},
on = {},
componentProviders = [],
childComponentOverrides = [],
Expand Down Expand Up @@ -176,8 +177,10 @@ export async function render<SutType, WrapperType = SutType>(

let detectChanges: () => void;

const allInputs = { ...componentInputs, ...newInputs };

let renderedPropKeys = Object.keys(componentProperties);
let renderedInputKeys = Object.keys(componentInputs);
let renderedInputKeys = Object.keys(allInputs);
let renderedOutputKeys = Object.keys(componentOutputs);
let subscribedOutputs: SubscribedOutput<SutType>[] = [];

Expand Down Expand Up @@ -224,7 +227,7 @@ export async function render<SutType, WrapperType = SutType>(
return createdFixture;
};

const fixture = await renderFixture(componentProperties, componentInputs, componentOutputs, on);
const fixture = await renderFixture(componentProperties, allInputs, componentOutputs, on);

if (deferBlockStates) {
if (Array.isArray(deferBlockStates)) {
Expand All @@ -239,10 +242,10 @@ export async function render<SutType, WrapperType = SutType>(
const rerender = async (
properties?: Pick<
RenderTemplateOptions<SutType>,
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
'componentProperties' | 'componentInputs' | 'inputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
> & { partialUpdate?: boolean },
) => {
const newComponentInputs = properties?.componentInputs ?? {};
const newComponentInputs = { ...properties?.componentInputs, ...properties?.inputs };
const changesInComponentInput = update(
fixture,
renderedInputKeys,
Expand Down
4 changes: 2 additions & 2 deletions projects/testing-library/tests/integrations/ng-mocks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { NgIf } from '@angular/common';
test('sends the correct value to the child input', async () => {
const utils = await render(TargetComponent, {
imports: [MockComponent(ChildComponent)],
componentInputs: { value: 'foo' },
inputs: { value: 'foo' },
});

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

const children = utils.fixture.debugElement.queryAll(By.directive(ChildComponent));
Expand Down
Loading