# Components

## Testing Components

These tests require creating the component's host element in the browser `DOM`, and investigating the component class's interaction with the `DOM` as described by its template.

Angular `TestBed` facilitates this kind of testing, but in many cases, testing the component class alone, without DOM involvement can validate much of the component's behavior.

{% hint style="info" %}
For tests without `DOM` interaction you will be:

1. Creating the Component with the `new` keyword, like in Services.
2. Asseting expectations on its `public` data and methods.
   {% endhint %}

## [Component `DOM` Testing](https://angular.dev/guide/testing/components-basics#component-dom-testing)

Components are more than just a `Class`, they interact with DOM and with other components.

Only testing a component's class cannot tell if the component is going to render properly, respond to user input and gestures, or integrate with its parent and child components.

Writing these kind of tests will require `TestBed` as well as other testing helpers.

{% hint style="info" %}
When you create a Component with `CLI`, an initial test file is created.

The created `.spec` file has some boilerplate code antecipating more advanced tests.
{% endhint %}

The bare minimum code for `DOM` testing is only:

```typescript
describe("", () => {
    const component: NewComponent;
    const fixture: ComponentFixture<NewComponent>;

    beforeEach(() => {
        TestBed.configureTestingModule({ imports: [NewComponent] });
        fixture = TestBed.createComponent(NewComponent);
        component = fixture.componentInstance;
    });

    it("should create", () => {
        expect(component).toBeDefined();
    });
});
```

### `createComponent()`

This method freezes the current `TestBed` definition, closing it to further configuration.

{% hint style="danger" %}
Do not re-configure `TestBed` after calling `createComponent`.
{% endhint %}

`createComponent` returns a `ComponentFixture`.

### `ComponentFixture`

Which is a test harness for interacting with the created component and its corresponding element.

{% hint style="info" %}
Check more of it properties and methods [here](https://angular.dev/guide/testing/utility-apis#the-componentfixture).
{% endhint %}

## DOM Elements Access

### `nativeElement`

`ComponentFixture.nativeElement` has an `any` type.

Angular cannot know at compile time what kind of `HTMLElement` the `nativeElement` ir or if it event is an `HTMLElement`. *(The application might be running on a non-browser plataform, such as the server or a Web Worker)*

Knowing that it is an `HTMLElement`, you may use `querySelector` to dive deeper into the element tree.

```typescript
describe("", () => {
    const component: NewComponent;
    const fixture: ComponentFixture<NewComponent>;

    beforeEach(() => {
        TestBed.configureTestingModule({ imports: [NewComponent] });
        fixture = TestBed.createComponent(NewComponent);
        component = fixture.componentInstance;
    });

    it("should create", () => {
        expect(component).toBeDefined();
    });

    it('should have <p> with "banner works!"', () => {
        const bannerElement: HTMLElement = fixture.nativeElement;
        const p = bannerElement.querySelector("p")!;
        expect(p.textContent).toEqual("banner works!");
    });
});
```

### `DebugElement`

Angular fixture provides the component's element directly through the `fixture.nativeElement`, which is only a convenience method.

Angular real path to `nativeElement` is through `fixture.debugElement.nativeElement`.

```typescript
const bannerElement: HTMLElement = fixture.nativeElement;
```

```typescript
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
```

This is because `nativeElement` depend upon the runtime environment, but you could be running these tests on a non-browser plataform, as said before.

So Angular relies on the `DebugElement` abstraction to work safely across all supported plataforms.

Instead of creating an HTML element tree, Angular creates a `DebugElement` tree that wraps the native elements for the runtime plataform.

```typescript
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
    const bannerDe: DebugElement = fixture.debugElement;
    const bannerEl: HTMLElement = bannerDe.nativeElement;   
    const p = bannerEl.querySelector('p')!;
    expect(p.textContent).toEqual('banner works!');
});
```

{% hint style="warning" %}
Check more of `DebugElement` methods and properties [here](https://angular.dev/guide/testing/utility-apis#debugelement).
{% endhint %}

### `By.css()`

`By.css()`, is a static method to select `DebugElement` nodes with a standard CSS selector.

It also returns `DebugElement`, so you must unwrap it to `nativeElement`.

```typescript
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
    const bannerDe: DebugElement = fixture.debugElement;
    const paragraphDe = bannerDe.query(By.css('p'));
    const p: HTMLElement = paragraphDe.nativeElement;
    expect(p.textContent).toEqual('banner works!');
});
```

## [Change Detection in Tests](https://angular.dev/guide/testing/components-scenarios#component-binding)

### `detectChanges()`

`createComponent` does not bind data, meaning that if you try to test data from signals or other bindings it might not have the expected value.

This happens because `createComponent` does not trigger change detection by default.

For dealing with triggering change detection, use `fixture.detectChanges()`.

<pre class="language-typescript" data-title="banner.component.ts"><code class="lang-typescript">@Component({
    selector: 'app-banner',
    template: '&#x3C;h1>{{title()}}&#x3C;/h1>',
    styles: ['h1 { color: green; font-size: 350%}'],
})
export class BannerComponent {
<strong>    title = signal('Test Tour of Heroes');
</strong>}
</code></pre>

{% code title="banner.component.specs.ts" %}

```typescript
it('should display original title after detectChanges()', () => {
    fixture.detectChanges();
    expect(h1.textContent).toContain(component.title);
});

it('should display a different test title', () => {
    component.title = 'Test Title';
    fixture.detectChanges();
    expect(h1.textContent).toContain('Test Title');
});
```

{% endcode %}

### [`ComponentFixtureAutoDetect`](https://angular.dev/guide/testing/components-scenarios#automatic-change-detection)

A provider configured in `TestBed` to enable automatic change detection.

```typescript
import {ComponentFixtureAutoDetect} from '@angular/core/testing';

TestBed.configureTestingModule({
    providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
});

it('should display original title', () => {
    // Hooray! No `fixture.detectChanges()` needed
    expect(h1.textContent).toContain(comp.title);
});

it('should still see original title after comp.title change', async () => {
    const oldTitle = comp.title;
    const newTitle = 'Test Title';
    comp.title.set(newTitle);
    // Displayed title is old because Angular didn't yet run change detection
    expect(h1.textContent).toContain(oldTitle);
    await fixture.whenStable();
    expect(h1.textContent).toContain(newTitle);
});

it('should display updated title after detectChanges', () => {
    comp.title.set('Test Title');
    fixture.detectChanges(); // detect changes explicitly
    expect(h1.textContent).toContain(comp.title);
});
```

{% hint style="warning" %}
The second and third test reveal an important limitation.

The Angular testing environment does not run change detection synchronously when updates happen inside the test case that changed the component's `title`.&#x20;

The test must call `await fixture.whenStable` to wait for another of change detection.
{% endhint %}

## Examples

### Testing Components with `async` pipe

When testing the components, we will mock the `ApiService.getFruits()` since the API should not be reached in the test.

{% code title="fruits-async.component.ts" %}

```typescript
@Component({
    selector: 'app-fruits-async',
    imports: [AsyncPipe],
    template: `
        @for (fruit of (fruits$ | async); track $index) {
            <span data-test-id="fruit-label">{{ fruit }}</span>
        }
    `
})
export class FruitsAsyncComponent {
    private apiService = inject(ApiService);
    protected fruits$ = this.apiService.getFruits();
}
```

{% endcode %}

{% code title="fruits-async.component.specs.ts" %}

```typescript
const expectedApiFruits = ['grapes', 'strawberries'];

describe('FruitsAsyncComponent', () => {
    let component: FruitsAsyncComponent;
    let fixture: ComponentFixture<FruitsAsyncComponent>;
    let apiServiceMock: jasmine.SpyObj<ApiService>;
    
    beforeEach(async () => {
        apiServiceMock = jasmine.createSpyObj('ApiService', ['getFruits']);
        // Since 'getFruits' expect an Observable we use 'of()'
        apiServiceMock.getFruits.and.returnValue(of(expectedApiFruits));
        
        await TestBed.configureTestingModule({
            imports: [FruitsAsyncComponent],
            providers: [
                { provide: ApiService, useValue: apiServiceMock }
            ]
        }).compileComponents();
        
        apiServiceMock = TestBed.inject(ApiService) as jasmine.SpyObj<ApiService>;
        fixture = TestBed.createComponent(FruitsAsyncComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });
    
    it('should be created', () => {
        expect(component).toBeTruthy();
    });
    
    it('shoud call fruits api on Init', () => {
        fixture.detectChange();
        expect(apiServiceMock.getFruits).toHaveBeenCalled();
    });
    
    it('should render fruits from api', () => {
        fixture.detectChange();
        const spanElements = fixture.debugElement.queryAll(By.css('[data-test-id="fruit-label"]'));
        spanElements.forEach((spanElement) => {
            const hasFruit = expectedApiFruits.includes((spanElement.nativeElement as HTMLElement).textContent);
            expect(hasFruit).toBe(true);
        });
    });
});
```

{% endcode %}
