footer component ts file:
import { AppSignalState } from './../../app-state/app-state.service';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-footer',
imports: [CommonModule, RouterLink, RouterLinkActive],
templateUrl: './footerponent.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.nav .nav-item .active_section {
color: #257bb1;
}
`,
],
})
export class FooterComponent {
currentYear = new Date();
// signals
appSignalState = inject(AppSignalState);
serverBuildData = this.appSignalState.getStateServerBuildData();
isUserAuthenticated = this.appSignalState.isUserLoggedIn();
userMfaAuthRequired = this.appSignalState.userMfaAuthRequired();
userResetPasswordRequired = this.appSignalState.userResetPasswordRequired();
isContractAgreementRequired =
this.appSignalState.isContractAgreementRequired();
canShowLogin = computed(() => {
return (
!this.userMfaAuthRequired() &&
!this.userResetPasswordRequired() &&
!this.isContractAgreementRequired() &&
!this.isUserAuthenticated()
);
});
}
footer component ts file:
import { AppSignalState } from './../../app-state/app-state.service';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
} from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-footer',
imports: [CommonModule, RouterLink, RouterLinkActive],
templateUrl: './footerponent.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.nav .nav-item .active_section {
color: #257bb1;
}
`,
],
})
export class FooterComponent {
currentYear = new Date();
// signals
appSignalState = inject(AppSignalState);
serverBuildData = this.appSignalState.getStateServerBuildData();
isUserAuthenticated = this.appSignalState.isUserLoggedIn();
userMfaAuthRequired = this.appSignalState.userMfaAuthRequired();
userResetPasswordRequired = this.appSignalState.userResetPasswordRequired();
isContractAgreementRequired =
this.appSignalState.isContractAgreementRequired();
canShowLogin = computed(() => {
return (
!this.userMfaAuthRequired() &&
!this.userResetPasswordRequired() &&
!this.isContractAgreementRequired() &&
!this.isUserAuthenticated()
);
});
}
test file is here:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footerponent';
import { AppSignalState } from '../../app-state/app-state.service';
import { signal, WritableSignal } from '@angular/core';
import { provideRouter } from '@angular/router';
describe('FooterComponent (Standalone)', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
let stateMock: jasmine.SpyObj<AppSignalState>;
beforeEach(() => {
const mockAppState = jasmine.createSpyObj('AppSignalState', [], {
userMfaAuthRequired: signal(false) as WritableSignal<boolean>,
userResetPasswordRequired: signal(false) as WritableSignal<boolean>,
isContractAgreementRequired: signal(false) as WritableSignal<boolean>,
isUserLoggedIn: signal(false) as WritableSignal<boolean>,
getStateServerBuildData: signal({ version: '1.0.0' }) as WritableSignal<{
version: string;
}>,
});
TestBed.configureTestingModule({
imports: [FooterComponent], // Import the standalone component
providers: [
provideRouter([]),
{
provide: AppSignalState,
useValue: mockAppState,
},
],
})pileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixtureponentInstance;
stateMock = TestBed.inject(
AppSignalState
) as jasmine.SpyObj<AppSignalState>;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should use the mocked signal', () => {
expect(stateMock.userMfaAuthRequired()).toBe(false);
expect(stateMock.userResetPasswordRequired()).toBe(false);
expect(stateMock.isContractAgreementRequired()).toBe(false);
expect(stateMock.isUserLoggedIn()).toBe(false);
});
it('should initialize currentYear with the current year', () => {
expect(component.currentYear.getFullYear()).toBe(new Date().getFullYear());
});
it('should get serverBuildData from AppSignalState', () => {
const mockServerBuildData = { version: '1.0.0' };
expect(stateMock.getStateServerBuildData()).toEqual(mockServerBuildData);
});
it('should return true if all conditions are false', () => {
// stateMock.userMfaAuthRequired.and.returnValue(signal(false));
// stateMock.isContractAgreementRequired.and.returnValue(signal(false));
// stateMock.isUserLoggedIn.and.returnValue(signal(false));
// fixture.detectChanges();
expect(component.canShowLogin()).toBe(true);
});
});
service :
@Injectable({
providedIn: 'root',
})
export class AppSignalState {
private readonly stateSessionItem = 'state';
private readonly state = signal<AppStateInterface>(appInitialState());
private readonly encryptedState: boolean = false;
private readonly JWT_TOKEN_WORD = 'Bearer_';
private spinnerService = inject(NgxSpinnerService);
mfaUtilityService = inject(MfaUtilityService);
httpClient = inject(HttpClient);
isUserLoggedIn(): Signal<boolean | unknown> {
return computed(() => {
return ...
}
userMfaAuthRequired(): Signal<boolean | unknown> {
return computed(() => {
return ....
});
}
....
}
I am getting error in the canShowLogin test case : TypeError: this.userMfaAuthRequired is not a function also how to change the values of the signals and test canShowLogin with different parameters. the stateMock.userMfaAuthRequired.and.returnValue(signal(false)); not working and giving error.
thanks very much
Share edited Mar 7 at 19:56 Naren Murali 60.7k5 gold badges44 silver badges79 bronze badges asked Mar 7 at 17:57 Saeid KhodarahmiSaeid Khodarahmi 294 bronze badges 2 |1 Answer
Reset to default 1I think the computed should not be inside a callback, since the computed itself is a callback.
@Injectable({
providedIn: 'root',
})
export class AppSignalState {
private readonly stateSessionItem = 'state';
private readonly state = signal<any>(appInitialState());
private readonly encryptedState: boolean = false;
private readonly JWT_TOKEN_WORD = 'Bearer_';
isUserLoggedIn: Signal<boolean | unknown> = computed(() => {
return true;
});
userMfaAuthRequired: Signal<boolean | unknown> = computed(() => {
return true;
});
getStateServerBuildData: Signal<boolean | unknown> = computed(() => {
return true;
});
userResetPasswordRequired: Signal<boolean | unknown> = computed(() => {
return true;
});
isContractAgreementRequired: Signal<boolean | unknown> = computed(() => {
return true;
});
}
Now we can directly access the computed, using the method below.
@Component({
selector: 'my-app',
templateUrl: './appponent.html',
imports: [CommonModule, RouterLink, RouterLinkActive],
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.nav .nav-item .active_section {
color: #257bb1;
}
`,
],
})
export class FooterComponent {
currentYear = new Date();
// signals
appSignalState = inject(AppSignalState);
serverBuildData = this.appSignalState.getStateServerBuildData;
isUserAuthenticated = this.appSignalState.isUserLoggedIn;
userMfaAuthRequired = this.appSignalState.userMfaAuthRequired;
userResetPasswordRequired = this.appSignalState.userResetPasswordRequired;
isContractAgreementRequired = this.appSignalState.isContractAgreementRequired;
canShowLogin = computed(() => {
return (
!this.userMfaAuthRequired() &&
!this.userResetPasswordRequired() &&
!this.isContractAgreementRequired() &&
!this.isUserAuthenticated()
);
});
}
From the testing perspective, it is better not to use a spy here, since you are targetting reactivity, toggling the flags and expecting the computed to react to the changes.
So define a class, this class will be used to trigger the reactivity of the component.
class MockAppState {
userMfaAuthRequired = signal(false) as WritableSignal<boolean>;
userResetPasswordRequired = signal(false) as WritableSignal<boolean>;
isContractAgreementRequired = signal(false) as WritableSignal<boolean>;
isUserLoggedIn = signal(false) as WritableSignal<boolean>;
getStateServerBuildData = signal({ version: '1.0.0' }) as WritableSignal<{
version: string;
}>;
}
We then provide this class over the original service.
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FooterComponent], // Import the standalone component
providers: [
provideRouter([]),
{
provide: AppSignalState,
useClass: MockAppState,
},
],
})pileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixtureponentInstance;
stateMock = TestBed.inject(AppSignalState) as unknown as MockAppState;
});
Notice I am doing as unknown as MockAppState
, this is because we are using a signal to a property which should be a computed
. Since the code was not shared for the computed, I am doing it this way.
Now for the test cases, we simply use .set()
method to set the values. Then trigger fixture.detectChanges()
for the computed to have the latest value, finally check the result.
it('should return true if all conditions are false', () => {
stateMock.userMfaAuthRequired.set(false);
stateMock.isContractAgreementRequired.set(false);
stateMock.isUserLoggedIn.set(false);
fixture.detectChanges();
expect(component.canShowLogin()).toBe(true);
});
Full Code:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { signal, WritableSignal } from '@angular/core';
import { provideRouter } from '@angular/router';
import { AppSignalState, FooterComponent } from './appponent';
class MockAppState {
userMfaAuthRequired = signal(false) as WritableSignal<boolean>;
userResetPasswordRequired = signal(false) as WritableSignal<boolean>;
isContractAgreementRequired = signal(false) as WritableSignal<boolean>;
isUserLoggedIn = signal(false) as WritableSignal<boolean>;
getStateServerBuildData = signal({ version: '1.0.0' }) as WritableSignal<{
version: string;
}>;
}
describe('FooterComponent (Standalone)', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
let stateMock: MockAppState;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FooterComponent], // Import the standalone component
providers: [
provideRouter([]),
{
provide: AppSignalState,
useClass: MockAppState,
},
],
})pileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixtureponentInstance;
stateMock = TestBed.inject(AppSignalState) as unknown as MockAppState;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should use the mocked signal', () => {
expect(stateMock.userMfaAuthRequired()).toBe(false);
expect(stateMock.userResetPasswordRequired()).toBe(false);
expect(stateMock.isContractAgreementRequired()).toBe(false);
expect(stateMock.isUserLoggedIn()).toBe(false);
});
it('should initialize currentYear with the current year', () => {
expect(component.currentYear.getFullYear()).toBe(new Date().getFullYear());
});
it('should get serverBuildData from AppSignalState', () => {
const mockServerBuildData: any = { version: '1.0.0' };
expect(stateMock.getStateServerBuildData()).toEqual(mockServerBuildData);
});
it('should return true if all conditions are false', () => {
stateMock.userMfaAuthRequired.set(false);
stateMock.isContractAgreementRequired.set(false);
stateMock.isUserLoggedIn.set(false);
fixture.detectChanges();
expect(component.canShowLogin()).toBe(true);
});
it('canShowLogin should return false if any of the conditions are true', () => {
stateMock.userMfaAuthRequired.set(true);
stateMock.isContractAgreementRequired.set(true);
stateMock.isUserLoggedIn.set(true);
stateMock.userResetPasswordRequired = signal(false) as any;
fixture.detectChanges();
expect(component.canShowLogin()).toBe(false);
});
});
app-state.service
– Naren Murali Commented Mar 7 at 18:06