diff --git a/README.md b/README.md
index 883c9dc..22f42f6 100644
--- a/README.md
+++ b/README.md
@@ -100,20 +100,22 @@ counter.component.ts
@Component({
selector: 'app-counter',
template: `
+ {{ hello() }}
- Current Count: {{ counter }}
+ Current Count: {{ counter() }}
`,
})
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);
}
}
```
@@ -121,23 +123,30 @@ export class CounterComponent {
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();
});
});
```
diff --git a/apps/example-app/src/app/examples/02-input-output.spec.ts b/apps/example-app/src/app/examples/02-input-output.spec.ts
index abc0066..847f6e1 100644
--- a/apps/example-app/src/app/examples/02-input-output.spec.ts
+++ b/apps/example-app/src/app/examples/02-input-output.spec.ts
@@ -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: {
@@ -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: {
diff --git a/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts b/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts
index a05ea5b..cb22ba6 100644
--- a/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts
+++ b/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts
@@ -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',
},
});
@@ -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',
},
});
@@ -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',
},
});
@@ -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: {
@@ -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',
},
});
@@ -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',
},
});
@@ -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',
},
});
diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts
index 3cf053a..8e0e57f 100644
--- a/projects/testing-library/src/lib/models.ts
+++ b/projects/testing-library/src/lib/models.ts
@@ -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';
@@ -68,7 +68,7 @@ export interface RenderResult extend
rerender: (
properties?: Pick<
RenderTemplateOptions,
- 'componentProperties' | 'componentInputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
+ 'componentProperties' | 'componentInputs' | 'inputs' | 'componentOutputs' | 'on' | 'detectChangesOnRender'
> & { partialUpdate?: boolean },
) => Promise;
/**
@@ -78,6 +78,27 @@ export interface RenderResult extend
renderDeferBlock: (deferBlockState: DeferBlockState, deferBlockIndex?: number) => Promise;
}
+declare const ALIASED_INPUT_BRAND: unique symbol;
+export type AliasedInput = T & {
+ [ALIASED_INPUT_BRAND]: T;
+};
+export type AliasedInputs = Record>;
+
+export type ComponentInput =
+ | {
+ [P in keyof T]?: T[P] extends Signal ? U : T[P];
+ }
+ | AliasedInputs;
+
+/**
+ * @description
+ * Creates an aliased input branded type with a value
+ *
+ */
+export function aliasedInput(alias: TAlias, value: T): Record> {
+ return { [alias]: value } as Record>;
+}
+
export interface RenderComponentOptions {
/**
* @description
@@ -199,6 +220,7 @@ export interface RenderComponentOptions | { [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;
+
/**
* @description
* An object to set `@Output` properties of the component
diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts
index 0ceda24..fbe94f2 100644
--- a/projects/testing-library/src/lib/testing-library.ts
+++ b/projects/testing-library/src/lib/testing-library.ts
@@ -67,6 +67,7 @@ export async function render(
componentProperties = {},
componentInputs = {},
componentOutputs = {},
+ inputs: newInputs = {},
on = {},
componentProviders = [],
childComponentOverrides = [],
@@ -176,8 +177,10 @@ export async function render(
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[] = [];
@@ -224,7 +227,7 @@ export async function render(
return createdFixture;
};
- const fixture = await renderFixture(componentProperties, componentInputs, componentOutputs, on);
+ const fixture = await renderFixture(componentProperties, allInputs, componentOutputs, on);
if (deferBlockStates) {
if (Array.isArray(deferBlockStates)) {
@@ -239,10 +242,10 @@ export async function render(
const rerender = async (
properties?: Pick<
RenderTemplateOptions,
- '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,
diff --git a/projects/testing-library/tests/integrations/ng-mocks.spec.ts b/projects/testing-library/tests/integrations/ng-mocks.spec.ts
index 6358485..8886fb3 100644
--- a/projects/testing-library/tests/integrations/ng-mocks.spec.ts
+++ b/projects/testing-library/tests/integrations/ng-mocks.spec.ts
@@ -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));
@@ -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));
diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts
index b73c9c7..59e0f75 100644
--- a/projects/testing-library/tests/render.spec.ts
+++ b/projects/testing-library/tests/render.spec.ts
@@ -13,11 +13,13 @@ import {
ElementRef,
inject,
output,
+ input,
+ model,
} from '@angular/core';
import { outputFromObservable } from '@angular/core/rxjs-interop';
import { NoopAnimationsModule, BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed } from '@angular/core/testing';
-import { render, fireEvent, screen, OutputRefKeysWithCallback } from '../src/public_api';
+import { render, fireEvent, screen, OutputRefKeysWithCallback, aliasedInput } from '../src/public_api';
import { ActivatedRoute, Resolve, RouterModule } from '@angular/router';
import { fromEvent, map } from 'rxjs';
import { AsyncPipe, NgIf } from '@angular/common';
@@ -533,3 +535,117 @@ describe('configureTestBed', () => {
expect(configureTestBedFn).toHaveBeenCalledTimes(1);
});
});
+
+describe('inputs and signals', () => {
+ @Component({
+ selector: 'atl-fixture',
+ template: `{{ myName() }} {{ myJob() }}`,
+ })
+ class InputComponent {
+ myName = input('foo');
+
+ myJob = input('bar', { alias: 'job' });
+ }
+
+ it('should set the input component', async () => {
+ await render(InputComponent, {
+ inputs: {
+ myName: 'Bob',
+ ...aliasedInput('job', 'Builder'),
+ },
+ });
+
+ expect(screen.getByText('Bob')).toBeInTheDocument();
+ expect(screen.getByText('Builder')).toBeInTheDocument();
+ });
+
+ it('should typecheck correctly', async () => {
+ // we only want to check the types here
+ // so we are purposely not calling render
+
+ const typeTests = [
+ async () => {
+ // OK:
+ await render(InputComponent, {
+ inputs: {
+ myName: 'OK',
+ },
+ });
+ },
+ async () => {
+ // @ts-expect-error - myName is a string
+ await render(InputComponent, {
+ inputs: {
+ myName: 123,
+ },
+ });
+ },
+ async () => {
+ // OK:
+ await render(InputComponent, {
+ inputs: {
+ ...aliasedInput('job', 'OK'),
+ },
+ });
+ },
+ async () => {
+ // @ts-expect-error - job is not using aliasedInput
+ await render(InputComponent, {
+ inputs: {
+ job: 'not used with aliasedInput',
+ },
+ });
+ },
+ ];
+
+ // add a statement so the test succeeds
+ expect(typeTests).toBeTruthy();
+ });
+});
+
+describe('README examples', () => {
+ describe('Counter', () => {
+ @Component({
+ selector: 'atl-counter',
+ template: `
+ {{ hello() }}
+
+ Current Count: {{ counter() }}
+
+ `,
+ })
+ class CounterComponent {
+ counter = model(0);
+ hello = input('Hi', { alias: 'greeting' });
+
+ increment() {
+ this.counter.set(this.counter() + 1);
+ }
+
+ decrement() {
+ this.counter.set(this.counter() - 1);
+ }
+ }
+
+ it('should render counter', async () => {
+ await render(CounterComponent, {
+ inputs: {
+ counter: 5,
+ ...aliasedInput('greeting', 'Hello Alias!'),
+ },
+ });
+
+ expect(screen.getByText('Current Count: 5')).toBeVisible();
+ expect(screen.getByText('Hello Alias!')).toBeVisible();
+ });
+
+ 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')).toBeVisible();
+ });
+ });
+});
diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts
index 571d642..04b8185 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -43,7 +43,7 @@ test('rerenders the component with updated inputs', async () => {
expect(screen.getByText('Sarah')).toBeInTheDocument();
const firstName = 'Mark';
- await rerender({ componentInputs: { firstName } });
+ await rerender({ inputs: { firstName } });
expect(screen.getByText(firstName)).toBeInTheDocument();
});
@@ -52,7 +52,7 @@ test('rerenders the component with updated inputs and resets other props', async
const firstName = 'Mark';
const lastName = 'Peeters';
const { rerender } = await render(FixtureComponent, {
- componentInputs: {
+ inputs: {
firstName,
lastName,
},
@@ -61,7 +61,7 @@ test('rerenders the component with updated inputs and resets other props', async
expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
const firstName2 = 'Chris';
- await rerender({ componentInputs: { firstName: firstName2 } });
+ await rerender({ inputs: { firstName: firstName2 } });
expect(screen.getByText(firstName2)).toBeInTheDocument();
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
@@ -87,7 +87,7 @@ test('rerenders the component with updated inputs and keeps other props when par
const firstName = 'Mark';
const lastName = 'Peeters';
const { rerender } = await render(FixtureComponent, {
- componentInputs: {
+ inputs: {
firstName,
lastName,
},
@@ -96,7 +96,7 @@ test('rerenders the component with updated inputs and keeps other props when par
expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
const firstName2 = 'Chris';
- await rerender({ componentInputs: { firstName: firstName2 }, partialUpdate: true });
+ await rerender({ inputs: { firstName: firstName2 }, partialUpdate: true });
expect(screen.queryByText(firstName)).not.toBeInTheDocument();
expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
@@ -181,7 +181,7 @@ test('change detection gets not called if `detectChangesOnRender` is set to fals
expect(screen.getByText('Sarah')).toBeInTheDocument();
const firstName = 'Mark';
- await rerender({ componentInputs: { firstName }, detectChangesOnRender: false });
+ await rerender({ inputs: { firstName }, detectChangesOnRender: false });
expect(screen.getByText('Sarah')).toBeInTheDocument();
expect(screen.queryByText(firstName)).not.toBeInTheDocument();