Skip to content

Commit 39fe933

Browse files
Add twoWayBinding support and writable signal test cases
Co-authored-by: timdeschryver <[email protected]>
1 parent ec079c2 commit 39fe933

File tree

3 files changed

+83
-4
lines changed

3 files changed

+83
-4
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Angular Testing Library also supports Angular's native bindings API, which provi
157157

158158
```typescript
159159
import { render, screen } from '@testing-library/angular';
160-
import { inputBinding, outputBinding } from '@angular/core';
160+
import { inputBinding, outputBinding, twoWayBinding, signal } from '@angular/core';
161161
import { CounterComponent } from './counter.component';
162162

163163
describe('Counter with Bindings API', () => {
@@ -182,6 +182,23 @@ describe('Counter with Bindings API', () => {
182182

183183
expect(clickHandler).toHaveBeenCalledWith(1);
184184
});
185+
186+
it('should handle two-way binding with signals', async () => {
187+
const counterSignal = signal(0);
188+
189+
await render(CounterComponent, {
190+
bindings: [twoWayBinding('counter', counterSignal)],
191+
});
192+
193+
expect(screen.getByText('Current Count: 0')).toBeVisible();
194+
195+
const incrementButton = screen.getByRole('button', { name: '+' });
196+
fireEvent.click(incrementButton);
197+
198+
// Two-way binding updates the external signal
199+
expect(counterSignal()).toBe(1);
200+
expect(screen.getByText('Current Count: 1')).toBeVisible();
201+
});
185202
});
186203
```
187204

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,14 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
317317
* []
318318
*
319319
* @example
320-
* import { inputBinding, outputBinding } from '@angular/core';
320+
* import { inputBinding, outputBinding, twoWayBinding } from '@angular/core';
321+
* import { signal } from '@angular/core';
321322
*
322323
* await render(AppComponent, {
323324
* bindings: [
324325
* inputBinding('value', () => 'test value'),
325-
* outputBinding('click', (event) => console.log(event))
326+
* outputBinding('click', (event) => console.log(event)),
327+
* twoWayBinding('name', signal('initial value'))
326328
* ]
327329
* })
328330
*/

projects/testing-library/tests/bindings-support.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, input, output, inputBinding, outputBinding } from '@angular/core';
1+
import { Component, input, output, inputBinding, outputBinding, twoWayBinding, signal, model } from '@angular/core';
22
import { render, screen, aliasedInput } from '../src/public_api';
33

44
describe('ATL Bindings API Support', () => {
@@ -17,6 +17,23 @@ describe('ATL Bindings API Support', () => {
1717
clicked = output<string>();
1818
}
1919

20+
@Component({
21+
selector: 'atl-two-way-test',
22+
template: `
23+
<div data-testid="name-display">{{ name() }}</div>
24+
<input data-testid="name-input" [value]="name()" (input)="name.set($any($event.target).value)" />
25+
<button data-testid="update-button" (click)="updateName()">Update</button>
26+
`,
27+
standalone: true,
28+
})
29+
class TwoWayBindingTestComponent {
30+
name = model<string>('default');
31+
32+
updateName() {
33+
this.name.set('updated from component');
34+
}
35+
}
36+
2037
it('should support inputBinding for regular inputs', async () => {
2138
await render(BindingsTestComponent, {
2239
bindings: [inputBinding('value', () => 'test-value'), inputBinding('greet', () => 'hi there')],
@@ -39,6 +56,49 @@ describe('ATL Bindings API Support', () => {
3956
expect(clickHandler).toHaveBeenCalledWith('clicked: bound-value');
4057
});
4158

59+
it('should support inputBinding with writable signal for re-rendering scenario', async () => {
60+
const valueSignal = signal('initial-value');
61+
62+
await render(BindingsTestComponent, {
63+
bindings: [inputBinding('value', valueSignal), inputBinding('greet', () => 'hi there')],
64+
});
65+
66+
expect(screen.getByTestId('value')).toHaveTextContent('initial-value');
67+
expect(screen.getByTestId('greeting')).toHaveTextContent('hi there');
68+
69+
// Update the signal and verify it reflects in the component
70+
valueSignal.set('updated-value');
71+
72+
// The binding should automatically update the component
73+
expect(await screen.findByText('updated-value')).toBeInTheDocument();
74+
});
75+
76+
it('should support twoWayBinding for model signals', async () => {
77+
const nameSignal = signal('initial name');
78+
79+
await render(TwoWayBindingTestComponent, {
80+
bindings: [twoWayBinding('name', nameSignal)],
81+
});
82+
83+
// Verify initial value
84+
expect(screen.getByTestId('name-display')).toHaveTextContent('initial name');
85+
expect(screen.getByTestId('name-input')).toHaveValue('initial name');
86+
87+
// Update from outside (signal change)
88+
nameSignal.set('updated from signal');
89+
expect(await screen.findByDisplayValue('updated from signal')).toBeInTheDocument();
90+
expect(screen.getByTestId('name-display')).toHaveTextContent('updated from signal');
91+
92+
// Update from component - let's trigger change detection after the click
93+
const updateButton = screen.getByTestId('update-button');
94+
updateButton.click();
95+
96+
// Give Angular a chance to process the update and check both the signal and display
97+
// The twoWayBinding should update the external signal
98+
expect(await screen.findByText('updated from component')).toBeInTheDocument();
99+
expect(nameSignal()).toBe('updated from component');
100+
});
101+
42102
it('should warn when mixing bindings with traditional inputs but still work', async () => {
43103
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
44104
const clickHandler = jest.fn();

0 commit comments

Comments
 (0)