Overview
Greetings,
I am currently in the process of updating my Angular application from version 14 to version 17. One of the tasks involved in this update is replacing the deprecated Resolver
classes with the new ResolverFn
implementation.
The challenge I'm facing is that after migrating, I am encountering difficulties injecting services without triggering the
inject() must be called from an injection context
error whenever I run test code (More details about the error can be found here).
Previous Resolver Implementation
@Injectable()
export class TimespanResolver {
private memberGranularity$: Observable<GranularityType>;
constructor(
private timespanService: TimespanService,
private coreCache: CoreCacheService
) {
this.memberGranularity$ = this.coreCache
.MemberSettings()
.pipe(
map((settings) =>
settings == null
? GranularityType.Weekly
: settings.DefaultGranularity
)
);
}
/**
* Updates the timespan when the route changes
* @param route -- route snapshot
*/
resolve({ data }: ActivatedRouteSnapshot): Promise<Timespan> {
const granularity$ = data.initGranularity
? of(data.initGranularity)
: this.memberGranularity$;
return firstValueFrom(
granularity$.pipe(
map((granularity) =>
TimespanService.GetTimespanFromGranularity(granularity)
),
tap((timespan) =>
this.timespanService.UpdateTimespan(
timespan,
data.timespanDisplayText
)
)
)
);
}
}
New Resolver Implementation
export const timespanResolver: ResolveFn<any> = (
route: ActivatedRouteSnapshot,
__: RouterStateSnapshot
) => {
const coreCache = inject(CoreCacheService); // Triggers an injection error
const timespanService = inject(TimespanService); // Triggers an injection error
let memberGranularity$: Observable<GranularityType>;
memberGranularity$ = coreCache
.MemberSettings()
.pipe(
map((settings) =>
settings == null
? GranularityType.Weekly
: settings.DefaultGranularity
)
);
const granularity$ = route.data.initGranularity
? of(route.data.initGranularity)
: memberGranularity$;
return firstValueFrom(
granularity$.pipe(
map((granularity) =>
TimespanService.GetTimespanFromGranularity(granularity)
),
tap((timespan) =>
timespanService.UpdateTimespan(
timespan,
route.data.timespanDisplayText
)
)
)
);
};
Current Test Scenario
it('should update with default granularity from user settings if one isnt specified', fakeAsync(() => {
// arrange
jest.spyOn(service, 'UpdateTimespan');
const newMoment = Date.UTC(2019, 1, 0);
const realDateNow = Date.now.bind(global.Date);
const dateNowStub = jest.fn(() => newMoment.valueOf());
global.Date.now = dateNowStub;
TimespanService._masterMomentRef = moment();
jest.spyOn(TimespanService, 'GetTimespanFromGranularity');
let snapshot: ActivatedRouteSnapshot = {
data: {}
} as ActivatedRouteSnapshot;
let stateSnap: RouterStateSnapshot = {
} as RouterStateSnapshot;
// act
timespanResolver(snapshot, stateSnap);
flush();
let res: Timespan;
service.Timespan$.subscribe(t => res = t);
flush();
let startDate = res.StartDate.year() !== res.EndDate.year() ?
res.StartDate.format('MMMM Do, YYYY')
: res.StartDate.format('MMMM Do');
let endDate = res.EndDate.month() !== res.StartDate.month() ?
res.EndDate.format('MMMM Do, YYYY')
: res.EndDate.format('Do, YYYY');
// assert
expect(service.UpdateTimespan).toHaveBeenCalledTimes(1);
expect(TimespanService.GetTimespanFromGranularity).toHaveBeenCalledWith(GranularityType.Weekly);
expect(res.Granularity).toBe(Weekly);
expect(res.DisplayDate).toMatch(`${ startDate } - ${ endDate }`);
global.Date.now = realDateNow;
TimespanService._masterMomentRef = moment();
}));
Approaches Tried So Far
- Attempting to inject within the input parameters of the lambda expression (resulted in same error):
export const timespanResolver: ResolveFn<any> = (
route: ActivatedRouteSnapshot,
__: RouterStateSnapshot,
coreCache = inject(CoreCacheService),
timespanService = inject(TimespanService)
) => {
// Code removed for brevity
}
- Introducing a class to handle the injected classes thinking constructors are considered within the injection context (resulted in same error);
export const timespanResolver: ResolveFn<any> = (
route: ActivatedRouteSnapshot,
__: RouterStateSnapshot
) => {
let services = new ResolverServices();
let coreCache = services.coreCache;
let timespanService = services.timespanService;
// Remaining code removed for brevity
};
class ResolverServices() {
public coreCache = inject(CoreCacheService);
public timespanService = inject(TimespanService);
constructor() {
}
}
- Exploring ways to obtain an Injector from the resolver to utilize runInInjection but unable to find any resources or examples.