Skip to content

Commit 2e0c98b

Browse files
crisbetopkozlowski-opensource
authored andcommitted
feat(core): support bindings in TestBed (#62040)
Adds support for passing in `Binding` objects into `TestBed.createComponent`. This makes it easier to test components by avoiding the need to create a wrapper component. Furthermore, it keeps the behavior consistent between tests and the actual app. For example, given a custom checkbox that looks like this: ```typescript @component({ selector: 'my-checkbox', template: '...', host: {'[class.checked]': 'isChecked()'} }) export class MyCheckbox { isChecked = input(false); } ``` A test for the `isChecked` input would look something like this: ```typescript it('should toggle the checked class', () => { @component({ imports: [MyCheckbox], template: '<my-checkbox [isChecked]="isChecked"/>', }) class Wrapper { isChecked = false; } const fixture = TestBed.createComponent(Wrapper); const checkbox = fixture.nativeElement.querySelector('my-checkbox'); fixture.detectChanges(); expect(checkbox.classList).not.toContain('checked'); fixture.componentInstance.isChecked = true; fixture.detectChanges(); expect(checkbox.classList).toContain('checked'); }); ``` Whereas with the new API, the test would look like this: ```typescript it('should toggle the checked class', () => { const isChecked = signal(false); const fixture = TestBed.createComponent(MyCheckbox, { bindings: [inputBinding('isChecked', isChecked)] }); const checkbox = fixture.nativeElement.querySelector('my-checkbox'); fixture.detectChanges(); expect(checkbox.classList).not.toContain('checked'); isChecked.set(true); fixture.detectChanges(); expect(checkbox.classList).toContain('checked'); }); ``` PR Close #62040
1 parent 89efd27 commit 2e0c98b

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

goldens/public-api/core/testing/index.api.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface TestBed {
114114
// (undocumented)
115115
configureTestingModule(moduleDef: TestModuleMetadata): TestBed;
116116
// (undocumented)
117-
createComponent<T>(component: Type<T>): ComponentFixture<T>;
117+
createComponent<T>(component: Type<T>, options?: TestComponentOptions): ComponentFixture<T>;
118118
// (undocumented)
119119
execute(tokens: any[], fn: Function, context?: any): any;
120120
// @deprecated
@@ -177,6 +177,11 @@ export interface TestBedStatic extends TestBed {
177177
new (...args: any[]): TestBed;
178178
}
179179

180+
// @public
181+
export interface TestComponentOptions {
182+
bindings?: Binding[];
183+
}
184+
180185
// @public
181186
export class TestComponentRenderer {
182187
// (undocumented)

packages/core/test/test_bed_spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ import {
4242
DOCUMENT,
4343
signal,
4444
provideZonelessChangeDetection,
45+
inputBinding,
46+
Output,
47+
EventEmitter,
48+
outputBinding,
49+
twoWayBinding,
4550
} from '../src/core';
4651
import {DeferBlockBehavior} from '../testing';
4752
import {TestBed, TestBedImpl} from '../testing/src/test_bed';
@@ -1215,6 +1220,73 @@ describe('TestBed', () => {
12151220
});
12161221
});
12171222

1223+
describe('bindings', () => {
1224+
it('should be able to bind to inputs', () => {
1225+
@Component({template: ''})
1226+
class TestComp {
1227+
@Input() value = 0;
1228+
}
1229+
1230+
const value = signal(1);
1231+
const fixture = TestBed.createComponent(TestComp, {
1232+
bindings: [inputBinding('value', value)],
1233+
});
1234+
fixture.detectChanges();
1235+
expect(fixture.componentInstance.value).toBe(1);
1236+
1237+
value.set(2);
1238+
fixture.detectChanges();
1239+
expect(fixture.componentInstance.value).toBe(2);
1240+
});
1241+
1242+
it('should be able to bind to outputs', () => {
1243+
let count = 0;
1244+
1245+
@Component({template: '<button (click)="event.emit()">Click me</button>'})
1246+
class TestComp {
1247+
@Output() event = new EventEmitter<void>();
1248+
}
1249+
1250+
const fixture = TestBed.createComponent(TestComp, {
1251+
bindings: [outputBinding('event', () => count++)],
1252+
});
1253+
fixture.detectChanges();
1254+
const button = fixture.nativeElement.querySelector('button');
1255+
expect(count).toBe(0);
1256+
1257+
button.click();
1258+
fixture.detectChanges();
1259+
expect(count).toBe(1);
1260+
});
1261+
1262+
it('should be able to bind two-way bindings', () => {
1263+
@Component({template: 'Value: {{value}}'})
1264+
class TestComp {
1265+
@Input() value = '';
1266+
@Output() valueChange = new EventEmitter<string>();
1267+
}
1268+
1269+
const value = signal('initial');
1270+
const fixture = TestBed.createComponent(TestComp, {
1271+
bindings: [twoWayBinding('value', value)],
1272+
});
1273+
fixture.detectChanges();
1274+
expect(value()).toBe('initial');
1275+
expect(fixture.nativeElement.textContent).toBe('Value: initial');
1276+
1277+
value.set('1');
1278+
fixture.detectChanges();
1279+
expect(value()).toBe('1');
1280+
expect(fixture.nativeElement.textContent).toBe('Value: 1');
1281+
1282+
fixture.componentInstance.value = '2';
1283+
fixture.componentInstance.valueChange.emit('2');
1284+
fixture.detectChanges();
1285+
expect(value()).toBe('2');
1286+
expect(fixture.nativeElement.textContent).toBe('Value: 2');
1287+
});
1288+
});
1289+
12181290
it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.overrideProvider)', () => {
12191291
const serviceOverride = {
12201292
get() {

packages/core/testing/src/test_bed.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import {
1414
ApplicationRef,
15+
Binding,
1516
Component,
1617
ɵRender3ComponentFactory as ComponentFactory,
1718
ComponentRef,
@@ -63,6 +64,16 @@ export interface TestBedStatic extends TestBed {
6364
new (...args: any[]): TestBed;
6465
}
6566

67+
/**
68+
* Options that can be configured for a test component.
69+
*
70+
* @publicApi
71+
*/
72+
export interface TestComponentOptions {
73+
/** Bindings to apply to the test component. */
74+
bindings?: Binding[];
75+
}
76+
6677
/**
6778
* @publicApi
6879
*/
@@ -149,7 +160,7 @@ export interface TestBed {
149160

150161
overrideTemplateUsingTestingModule(component: Type<any>, template: string): TestBed;
151162

152-
createComponent<T>(component: Type<T>): ComponentFixture<T>;
163+
createComponent<T>(component: Type<T>, options?: TestComponentOptions): ComponentFixture<T>;
153164

154165
/**
155166
* Execute any pending effects.
@@ -377,8 +388,11 @@ export class TestBedImpl implements TestBed {
377388
return TestBedImpl.INSTANCE.runInInjectionContext(fn);
378389
}
379390

380-
static createComponent<T>(component: Type<T>): ComponentFixture<T> {
381-
return TestBedImpl.INSTANCE.createComponent(component);
391+
static createComponent<T>(
392+
component: Type<T>,
393+
options?: TestComponentOptions,
394+
): ComponentFixture<T> {
395+
return TestBedImpl.INSTANCE.createComponent(component, options);
382396
}
383397

384398
static resetTestingModule(): TestBed {
@@ -630,7 +644,7 @@ export class TestBedImpl implements TestBed {
630644
return this.overrideComponent(component, {set: {template, templateUrl: null!}});
631645
}
632646

633-
createComponent<T>(type: Type<T>): ComponentFixture<T> {
647+
createComponent<T>(type: Type<T>, options?: TestComponentOptions): ComponentFixture<T> {
634648
const testComponentRenderer = this.inject(TestComponentRenderer);
635649
const rootElId = `root${_nextRootElementId++}`;
636650
testComponentRenderer.insertRootElement(rootElId);
@@ -655,6 +669,8 @@ export class TestBedImpl implements TestBed {
655669
[],
656670
`#${rootElId}`,
657671
this.testModuleRef,
672+
undefined,
673+
options?.bindings,
658674
) as ComponentRef<T>;
659675
return this.runInInjectionContext(() => new ComponentFixture(componentRef));
660676
};

packages/core/testing/src/testing.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export {
2929
inject,
3030
InjectSetupWrapper,
3131
withModule,
32+
TestComponentOptions,
3233
} from './test_bed';
3334
export {
3435
TestComponentRenderer,

0 commit comments

Comments
 (0)