From c12b358b7c0699c47920f2f50cee14049981075a Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 16:44:46 +0300 Subject: [PATCH 01/11] refactor: stronger typing of inputs --- projects/testing-library/src/lib/models.ts | 43 ++++++++++++++++- .../src/lib/testing-library.ts | 7 ++- projects/testing-library/tests/render.spec.ts | 46 ++++++++++++++++++- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 3cf053a..717f1e4 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'; @@ -78,6 +78,29 @@ 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 // If the property is a Signal, apply Partial to the inner type + : Partial; // Otherwise, apply Partial to the property itself + } + | AliasedInputs; + +/** + * @description + * Creates an aliased input branded type with a value + * + */ +export function aliasedInputWithValue(value: T): AliasedInput { + return value as AliasedInput; +} + export interface RenderComponentOptions { /** * @description @@ -199,6 +222,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, + * someAlias: aliasedInputWithValue('value') + * } + */ + 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..30f3e42 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)) { diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index b73c9c7..af8a4d3 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -13,11 +13,12 @@ import { ElementRef, inject, output, + input, } 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, aliasedInputWithValue } from '../src/public_api'; import { ActivatedRoute, Resolve, RouterModule } from '@angular/router'; import { fromEvent, map } from 'rxjs'; import { AsyncPipe, NgIf } from '@angular/common'; @@ -533,3 +534,46 @@ 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', + job: aliasedInputWithValue('Builder'), + }, + }); + + expect(screen.getByText('Bob')).toBeInTheDocument(); + expect(screen.getByText('Builder')).toBeInTheDocument(); + }); + + it('should typecheck correctly', async () => { + // @ts-expect-error - myName is a string + await render(InputComponent, { + inputs: { + myName: 123, + }, + }); + + // @ts-expect-error - job is not using aliasedInputWithValue + await render(InputComponent, { + inputs: { + job: 'not used with aliasedInputWithValue', + }, + }); + + // add a statement so the test succeeds + expect(true).toBeTruthy(); + }); +}); From d76736316307d7e059cf3431c96a10f2eea4a1a4 Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:02:33 +0300 Subject: [PATCH 02/11] fixup! refactor: stronger typing of inputs --- projects/testing-library/src/lib/models.ts | 4 +-- projects/testing-library/tests/render.spec.ts | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 717f1e4..e1acd4c 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -86,9 +86,7 @@ export type AliasedInputs = Record>; export type ComponentInput = | { - [P in keyof T]?: T[P] extends Signal - ? U // If the property is a Signal, apply Partial to the inner type - : Partial; // Otherwise, apply Partial to the property itself + [P in keyof T]?: T[P] extends Signal ? U : T[P]; } | AliasedInputs; diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index af8a4d3..a00bc27 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -559,21 +559,29 @@ describe('inputs and signals', () => { }); it('should typecheck correctly', async () => { - // @ts-expect-error - myName is a string - await render(InputComponent, { - inputs: { - myName: 123, + // we only want to check the types here + // so we are purposely not calling render + + const typeTests = [ + () => { + // @ts-expect-error - myName is a string + await render(InputComponent, { + inputs: { + myName: 123, + }, + }); }, - }); - - // @ts-expect-error - job is not using aliasedInputWithValue - await render(InputComponent, { - inputs: { - job: 'not used with aliasedInputWithValue', + () => { + // @ts-expect-error - job is not using aliasedInputWithValue + await render(InputComponent, { + inputs: { + job: 'not used with aliasedInputWithValue', + }, + }); }, - }); + ]; // add a statement so the test succeeds - expect(true).toBeTruthy(); + expect(typeTests).toBeTruthy(); }); }); From d5df601234a9b4727c404024359845e1dd9f1cfc Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:19:54 +0300 Subject: [PATCH 03/11] fixup! refactor: stronger typing of inputs --- .../22-signal-inputs.component.spec.ts | 30 +++++++++---------- projects/testing-library/src/lib/models.ts | 2 +- .../src/lib/testing-library.ts | 4 +-- 3 files changed, 18 insertions(+), 18 deletions(-) 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..8a65dda 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 { aliasedInputWithValue, 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: { + greeting: aliasedInputWithValue('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: { + greeting: aliasedInputWithValue('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: { + greeting: aliasedInputWithValue('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: { + greeting: aliasedInputWithValue('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: { + greeting: aliasedInputWithValue('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: { + greeting: aliasedInputWithValue('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: { + greeting: aliasedInputWithValue('bye'), name: 'test', }, }); diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index e1acd4c..a6e0636 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -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; /** diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 30f3e42..fbe94f2 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -242,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, From ad67fc498cb65f04d64d90e37ab75a8d8644986e Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:21:10 +0300 Subject: [PATCH 04/11] test: update deprecated componentInputs -> inputs --- .../src/app/examples/02-input-output.spec.ts | 4 ++-- .../tests/integrations/ng-mocks.spec.ts | 4 ++-- projects/testing-library/tests/rerender.spec.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) 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/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/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(); From 29163087328ec91776925eea9df2995197725732 Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:29:02 +0300 Subject: [PATCH 05/11] fixup! refactor: stronger typing of inputs --- projects/testing-library/src/lib/models.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index a6e0636..3ad4320 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -244,8 +244,9 @@ export interface RenderComponentOptions; From daf716bc4cbed58065ceab6b3bc37b06929a5428 Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:34:34 +0300 Subject: [PATCH 06/11] docs: update README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 883c9dc..2bc4fb9 100644 --- a/README.md +++ b/README.md @@ -101,19 +101,19 @@ counter.component.ts selector: 'app-counter', template: ` - Current Count: {{ counter }} + Current Count: {{ counter() }} `, }) export class CounterComponent { - @Input() counter = 0; + counter = model(0); increment() { - this.counter += 1; + this.counter.set(this.counter() + 1); } decrement() { - this.counter -= 1; + this.counter.set(this.counter() + 1); } } ``` @@ -126,13 +126,13 @@ import { CounterComponent } from './counter.component'; describe('Counter', () => { test('should render counter', async () => { - await render(CounterComponent, { componentProperties: { counter: 5 } }); + await render(CounterComponent, { inputs: { counter: 5 } }); expect(screen.getByText('Current Count: 5')); }); test('should increment the counter on click', async () => { - await render(CounterComponent, { componentProperties: { counter: 5 } }); + await render(CounterComponent, { inputs: { counter: 5 } }); const incrementButton = screen.getByRole('button', { name: '+' }); fireEvent.click(incrementButton); From 5a90c4c734e3ef74634409c10317861784dd3533 Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:37:02 +0300 Subject: [PATCH 07/11] fixup! refactor: stronger typing of inputs --- projects/testing-library/tests/render.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index a00bc27..404d535 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -563,7 +563,7 @@ describe('inputs and signals', () => { // so we are purposely not calling render const typeTests = [ - () => { + async () => { // @ts-expect-error - myName is a string await render(InputComponent, { inputs: { @@ -571,7 +571,7 @@ describe('inputs and signals', () => { }, }); }, - () => { + async () => { // @ts-expect-error - job is not using aliasedInputWithValue await render(InputComponent, { inputs: { From 058386d2c015993d895f18865e805b723fce284b Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Mon, 29 Jul 2024 17:38:14 +0300 Subject: [PATCH 08/11] fixup! refactor: stronger typing of inputs --- projects/testing-library/tests/render.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 404d535..11e1520 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -563,6 +563,14 @@ describe('inputs and signals', () => { // 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, { @@ -571,6 +579,14 @@ describe('inputs and signals', () => { }, }); }, + async () => { + // OK: + await render(InputComponent, { + inputs: { + job: aliasedInputWithValue('OK'), + }, + }); + }, async () => { // @ts-expect-error - job is not using aliasedInputWithValue await render(InputComponent, { From 539d20b382307d1a68c6828f5a9a0ccf2f2874ae Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Fri, 2 Aug 2024 11:44:00 +0300 Subject: [PATCH 09/11] refactor: introduce `...aliasedInput()` --- README.md | 25 +++++--- .../22-signal-inputs.component.spec.ts | 16 ++--- projects/testing-library/src/lib/models.ts | 8 +-- projects/testing-library/tests/render.spec.ts | 58 +++++++++++++++++-- 4 files changed, 82 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 2bc4fb9..c9e304f 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,15 @@ counter.component.ts @Component({ selector: 'app-counter', template: ` + {{ hello() }} Current Count: {{ counter() }} `, }) -export class CounterComponent { +class CounterComponent { counter = model(0); + hello = input('Hi', { alias: 'greeting' }); increment() { 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, { inputs: { 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 () => { + 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/22-signal-inputs.component.spec.ts b/apps/example-app/src/app/examples/22-signal-inputs.component.spec.ts index 8a65dda..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 { aliasedInputWithValue, 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, { inputs: { - greeting: aliasedInputWithValue('Hello'), + ...aliasedInput('greeting', 'Hello'), name: 'world', }, }); @@ -17,7 +17,7 @@ test('works with signal inputs', async () => { test('works with computed', async () => { await render(SignalInputComponent, { inputs: { - greeting: aliasedInputWithValue('Hello'), + ...aliasedInput('greeting', 'Hello'), name: 'world', }, }); @@ -29,7 +29,7 @@ test('works with computed', async () => { test('can update signal inputs', async () => { const { fixture } = await render(SignalInputComponent, { inputs: { - greeting: aliasedInputWithValue('Hello'), + ...aliasedInput('greeting', 'Hello'), name: 'world', }, }); @@ -52,7 +52,7 @@ test('output emits a value', async () => { const submitFn = jest.fn(); await render(SignalInputComponent, { inputs: { - greeting: aliasedInputWithValue('Hello'), + ...aliasedInput('greeting', 'Hello'), name: 'world', }, on: { @@ -68,7 +68,7 @@ test('output emits a value', async () => { test('model update also updates the template', async () => { const { fixture } = await render(SignalInputComponent, { inputs: { - greeting: aliasedInputWithValue('Hello'), + ...aliasedInput('greeting', 'Hello'), name: 'initial', }, }); @@ -98,7 +98,7 @@ test('model update also updates the template', async () => { test('works with signal inputs, computed values, and rerenders', async () => { const view = await render(SignalInputComponent, { inputs: { - greeting: aliasedInputWithValue('Hello'), + ...aliasedInput('greeting', 'Hello'), name: 'world', }, }); @@ -111,7 +111,7 @@ test('works with signal inputs, computed values, and rerenders', async () => { await view.rerender({ inputs: { - greeting: aliasedInputWithValue('bye'), + ...aliasedInput('greeting', 'bye'), name: 'test', }, }); diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 3ad4320..8e0e57f 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -95,8 +95,8 @@ export type ComponentInput = * Creates an aliased input branded type with a value * */ -export function aliasedInputWithValue(value: T): AliasedInput { - return value as AliasedInput; +export function aliasedInput(alias: TAlias, value: T): Record> { + return { [alias]: value } as Record>; } export interface RenderComponentOptions { @@ -220,7 +220,7 @@ export interface RenderComponentOptions; diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 11e1520..2701da9 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -14,11 +14,12 @@ import { 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, aliasedInputWithValue } 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'; @@ -550,7 +551,7 @@ describe('inputs and signals', () => { await render(InputComponent, { inputs: { myName: 'Bob', - job: aliasedInputWithValue('Builder'), + ...aliasedInput('job', 'Builder'), }, }); @@ -583,15 +584,15 @@ describe('inputs and signals', () => { // OK: await render(InputComponent, { inputs: { - job: aliasedInputWithValue('OK'), + ...aliasedInput('job', 'OK'), }, }); }, async () => { - // @ts-expect-error - job is not using aliasedInputWithValue + // @ts-expect-error - job is not using aliasedInput await render(InputComponent, { inputs: { - job: 'not used with aliasedInputWithValue', + job: 'not used with aliasedInput', }, }); }, @@ -601,3 +602,50 @@ describe('inputs and signals', () => { 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(); + }); + }); +}); From ca3444a22591a71f52f2c3b57053e2e0089529f2 Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Fri, 2 Aug 2024 11:45:05 +0300 Subject: [PATCH 10/11] fixup! refactor: introduce `...aliasedInput()` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9e304f..3bcd4d3 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ counter.component.ts `, }) -class CounterComponent { +export class CounterComponent { counter = model(0); hello = input('Hi', { alias: 'greeting' }); From becbb97b5b5f0c3b58c7124f88f0b775ab8f2aed Mon Sep 17 00:00:00 2001 From: Andrei Alecu Date: Fri, 2 Aug 2024 11:51:50 +0300 Subject: [PATCH 11/11] fixup! refactor: introduce `...aliasedInput()` --- README.md | 2 +- projects/testing-library/tests/render.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3bcd4d3..22f42f6 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ export class CounterComponent { } decrement() { - this.counter.set(this.counter() + 1); + this.counter.set(this.counter() - 1); } } ``` diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts index 2701da9..59e0f75 100644 --- a/projects/testing-library/tests/render.spec.ts +++ b/projects/testing-library/tests/render.spec.ts @@ -623,7 +623,7 @@ describe('README examples', () => { } decrement() { - this.counter.set(this.counter() + 1); + this.counter.set(this.counter() - 1); } }