Skip to content

Commit

Permalink
feat(core): Add ability to pass inputs to TestBed.createComponent
Browse files Browse the repository at this point in the history
This helps mitigate angular#57858 by allowing testers to pass inputs that will
be set _within_ the `NgZone.run` call that wraps component
initialization. This will ensure that inputs are set prior to the
automatic `ApplicationRef.tick`.

resolves angular#57856
  • Loading branch information
atscott committed Sep 17, 2024
1 parent 8e01734 commit 586c99d
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 6 deletions.
2 changes: 2 additions & 0 deletions adev/src/content/guide/testing/utility-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ These are rarely needed.
## The `ComponentFixture`

The `TestBed.createComponent<T>` creates an instance of the component `T` and returns a strongly typed `ComponentFixture` for that component.
You can also pass inputs to the `createComponent` method to ensure initial input values are set up correctly, for example
`TestBed.createComponent(ExampleComponent, {inputs: {'name': 'Angular'}})`.

The `ComponentFixture` properties and methods provide access to the component, its DOM representation, and aspects of its Angular environment.

Expand Down
2 changes: 1 addition & 1 deletion goldens/public-api/core/testing/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export interface TestBed {
// (undocumented)
configureTestingModule(moduleDef: TestModuleMetadata): TestBed;
// (undocumented)
createComponent<T>(component: Type<T>): ComponentFixture<T>;
createComponent<T>(component: Type<T>, options?: TestBedCreateComponentOptions): ComponentFixture<T>;
// (undocumented)
execute(tokens: any[], fn: Function, context?: any): any;
flushEffects(): void;
Expand Down
45 changes: 40 additions & 5 deletions packages/core/testing/src/test_bed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ export interface TestBedStatic extends TestBed {
new (...args: any[]): TestBed;
}

/**
* Additional options for configuring the component fixture on creation.
*
* @publicApi
* @see TestBed#createComponent
*/
export interface TestBedCreateComponentOptions {
/**
* The initial inputs for the component.
*
* These key/value pairs will be used with `ComponentRef#setInput` after the
* test component is created and before the fixture is returned from `createComponent`.
*
* Specifying inputs at creation is useful in conjunction with `ComponentFixtureAutoDetect`
* to ensure that the inputs are set before change detection runs on the component.
*
* @see ComponentRef#setInput
* @see ComponentFixture#componentRef
*/
inputs?: {[templateName: string]: unknown};
}

/**
* @publicApi
*/
Expand Down Expand Up @@ -160,7 +182,10 @@ export interface TestBed {

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

createComponent<T>(component: Type<T>): ComponentFixture<T>;
createComponent<T>(
component: Type<T>,
options?: TestBedCreateComponentOptions,
): ComponentFixture<T>;

/**
* Execute any pending effects.
Expand Down Expand Up @@ -398,8 +423,11 @@ export class TestBedImpl implements TestBed {
return TestBedImpl.INSTANCE.runInInjectionContext(fn);
}

static createComponent<T>(component: Type<T>): ComponentFixture<T> {
return TestBedImpl.INSTANCE.createComponent(component);
static createComponent<T>(
component: Type<T>,
options?: TestBedCreateComponentOptions,
): ComponentFixture<T> {
return TestBedImpl.INSTANCE.createComponent(component, options);
}

static resetTestingModule(): TestBed {
Expand Down Expand Up @@ -668,7 +696,10 @@ export class TestBedImpl implements TestBed {
return this.overrideComponent(component, {set: {template, templateUrl: null!}});
}

createComponent<T>(type: Type<T>): ComponentFixture<T> {
createComponent<T>(
type: Type<T>,
{inputs = {}}: TestBedCreateComponentOptions = {inputs: {}},
): ComponentFixture<T> {
const testComponentRenderer = this.inject(TestComponentRenderer);
const rootElId = `root${_nextRootElementId++}`;
testComponentRenderer.insertRootElement(rootElId);
Expand All @@ -694,7 +725,11 @@ export class TestBedImpl implements TestBed {
`#${rootElId}`,
this.testModuleRef,
) as ComponentRef<T>;
return this.runInInjectionContext(() => new ComponentFixture(componentRef));
const fixture = this.runInInjectionContext(() => new ComponentFixture(componentRef));
for (const key of Object.keys(inputs)) {
fixture.componentRef.setInput(key, inputs[key]);
}
return fixture;
};
const noNgZone = this.inject(ComponentFixtureNoNgZone, false);
const ngZone = noNgZone ? null : this.inject(NgZone, null);
Expand Down
8 changes: 8 additions & 0 deletions packages/core/testing/src/test_bed_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export class TestComponentRenderer {

/**
* @publicApi
*
* When true, ensures components created with `TestBed#createComponent` are attached to the application.
* This more closely matches production behavior for the vast majority of components in Angular.
* When using `ComponentFixtureAutoDetect`, consider also defining the initial inputs in the
* options of `TestBed#createComponent` to ensure they are set prior to the first change detection
* of the component.
*
* @see TestBedCreateComponentOptions
*/
export const ComponentFixtureAutoDetect = new InjectionToken<boolean>('ComponentFixtureAutoDetect');

Expand Down

0 comments on commit 586c99d

Please sign in to comment.