I encounter some issues trying to write a test.
I have a component, which only include a SignalStore
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormulaireStore } from './store/formulaire-store';
@Component({
selector: 'app-formulaire',
imports: [],
providers: [FormulaireStore],
template: `
<p>formulaire works!</p>
@if(store.context(); as ctx) {
<p>context : {{ ctx.informations }}</p>
}
`,
styleUrl: './formulaire.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormulaireComponent {
readonly store = inject(FormulaireStore);
}
import { inject } from '@angular/core';
import {
patchState,
signalStore,
withHooks,
withMethods,
withProps,
withState,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';
import { FormsService } from '../../services/form.service';
export interface FormulaireContexte {
informations: string;
}
type FormulaireState = {
context: Partial<FormulaireContexte> | undefined;
};
const initialState: FormulaireState = {
context: undefined,
};
export const FormulaireStore = signalStore(
withState<FormulaireState>(initialState),
withProps(() => ({
_formService: inject(FormsService),
})),
withMethods((store) => {
return {
_getContext: rxMethod<void>(
pipe(
switchMap(() => {
return store._formService.getContext();
}),
tap((context) => {
patchState(store, {
context,
});
})
)
),
};
}),
withHooks(({ _getContext }) => {
return {
onInit() {
_getContext();
},
};
})
);
and the service used by the SignalStore
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { FormulaireContexte } from '../formulaire/store/formulaire-store';
@Injectable({ providedIn: 'root' })
export class FormsService {
readonly #client = inject(HttpClient);
getContext(): Observable<FormulaireContexte> {
return this.#client.get<FormulaireContexte>(`/api/forms/context`);
}
}
Therefore, i'm trying to write a test to interact with the UI
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { FormulaireComponent } from './formulaire.component';
import { provideRouter } from '@angular/router';
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing';
import { RouterTestingHarness } from '@angular/router/testing';
describe('FormulaireComponent', () => {
const setup = async () => {
TestBed.configureTestingModule({
providers: [
provideRouter([
{
path: 'formulaire',
pathMatch: 'full',
component: FormulaireComponent,
},
]),
provideHttpClient(),
provideHttpClientTesting(),
],
});
const httpCtrl = TestBed.inject(HttpTestingController);
const harness = await RouterTestingHarness.create('formulaire');
harness.fixture.autoDetectChanges(true);
httpCtrl
.match((req) => Boolean(req.url.match(`/api/forms/context`)))[0]
.flush({ informations: 'mocked informations' });
httpCtrl.verify();
return harness;
};
it('should create', async () => {
const harness = await setup();
//! This line is needed to see changes
harness.detectChanges();
debugger;
expect(1).toBe(1);
});
});
but it look like i need this specific line
//! This line is needed to see changes
harness.detectChanges();
to enter in the @if condition of the template.
the strange behaviour is, if don't call httpClient ìn the service
getContext(): Observable<FormulaireContexte> {
//return this.#client.get<FormulaireContexte>(`/api/forms/context`);
return of({ informations: 'informations from of operator' });
}
It works without necessiting the
harness.detectChanges();
I don't get the behavior here, i need some advice in order to understand it
The only difference I can see is that:
this.#client.get<FormulaireContexte>(
/api/forms/context);
-> takes some time to resolve rather than resolving immediately.
of({ informations: 'informations from of operator' })
-> resolves immediately compared to the former way.
I also notice the below line, autoDetectChanges
will trigger a change detection cycle.
const harness = await RouterTestingHarness.create('formulaire');
harness.fixture.autoDetectChanges(true);
So it could just be a matter of timing. The of
is resolved before the change detection runs and the @if
condition shows the correct value. Whereas the this.#client.get<FormulaireContexte>(`/api/forms/context`);
resolves after the change detection runs, hence you need an additional fixture.detectChanges()
for the @if
to show the proper value.
To test this theory, you can make the of
observable resolve with a delay, if the behaviour is that the test cases passes without calling fixture.detectChanges()
, then my theory is wrong. Else it is correct.
getContext(): Observable<FormulaireContexte> {
//return this.#client.get<FormulaireContexte>(`/api/forms/context`);
return of({ informations: 'informations from of operator' }).pipe(delay(1000));
}