Skip to content

Commit

Permalink
refactor(signals): improve rxMethod tests by using child injectors (#…
Browse files Browse the repository at this point in the history
…4652)

Co-authored-by: Tim Deschryver <[email protected]>
  • Loading branch information
rainerhahnekamp and timdeschryver authored Jan 9, 2025
1 parent 7f42065 commit cf62e71
Showing 1 changed file with 55 additions and 127 deletions.
182 changes: 55 additions & 127 deletions modules/signals/rxjs-interop/spec/rx-method.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import {
Component,
createEnvironmentInjector,
EnvironmentInjector,
inject,
Injectable,
Injector,
OnInit,
runInInjectionContext,
signal,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { provideLocationMocks } from '@angular/common/testing';
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { BehaviorSubject, pipe, Subject, tap } from 'rxjs';
import { rxMethod } from '../src';
import { createLocalService } from '../../spec/helpers';
Expand Down Expand Up @@ -209,10 +203,11 @@ describe('rxMethod', () => {
* method that is initialized at the ancestor injector level is tracked within
* the correct injection context and untracked at the specified time.
*
* Components use `globalSignal` or `globalObservable` from `GlobalService`
* and pass it to the reactive method. If the component is destroyed but
* signal or observable change still increases the corresponding counter,
* the internal effect or subscription is still active.
* Different injection contexts use `globalSignal` or `globalObservable`
* from `GlobalService` and pass it to the reactive method.
* If the injector is destroyed but the signal or the observable still
* increases the corresponding counter, the internal effect or subscription
* is still active.
*/
describe('with instance injector', () => {
@Injectable({ providedIn: 'root' })
Expand All @@ -223,10 +218,10 @@ describe('rxMethod', () => {
globalSignalChangeCounter = 0;
globalObservableChangeCounter = 0;

readonly signalMethod = rxMethod<number>(
readonly trackSignal = rxMethod<number>(
tap(() => this.globalSignalChangeCounter++)
);
readonly observableMethod = rxMethod<number>(
readonly trackObservable = rxMethod<number>(
tap(() => this.globalObservableChangeCounter++)
);

Expand All @@ -239,81 +234,45 @@ describe('rxMethod', () => {
}
}

@Component({
selector: 'app-without-store',
template: '',
})
class WithoutStoreComponent {}

function setup(WithStoreComponent: new () => unknown): GlobalService {
TestBed.configureTestingModule({
providers: [
provideRouter([
{ path: 'with-store', component: WithStoreComponent },
{
path: 'without-store',
component: WithoutStoreComponent,
},
]),
provideLocationMocks(),
],
it('tracks a signal until the instanceInjector is destroyed', () => {
const instanceInjector = createEnvironmentInjector(
[],
TestBed.inject(EnvironmentInjector)
);
const globalService = TestBed.inject(GlobalService);
runInInjectionContext(instanceInjector, () => {
globalService.trackSignal(globalService.globalSignal);
});

return TestBed.inject(GlobalService);
}

it('tracks a signal until the component is destroyed', async () => {
@Component({
selector: 'app-with-store',
template: '',
})
class WithStoreComponent {
store = inject(GlobalService);

constructor() {
this.store.signalMethod(this.store.globalSignal);
}
}

const globalService = setup(WithStoreComponent);
const harness = await RouterTestingHarness.create('/with-store');

TestBed.flushEffects();
expect(globalService.globalSignalChangeCounter).toBe(1);

globalService.incrementSignal();
harness.detectChanges();
TestBed.flushEffects();
expect(globalService.globalSignalChangeCounter).toBe(2);

globalService.incrementSignal();
harness.detectChanges();
TestBed.flushEffects();
expect(globalService.globalSignalChangeCounter).toBe(3);

await harness.navigateByUrl('/without-store');
instanceInjector.destroy();
globalService.incrementSignal();
harness.detectChanges();
TestBed.flushEffects();

expect(globalService.globalSignalChangeCounter).toBe(3);
});

it('tracks an observable until the component is destroyed', async () => {
@Component({
selector: 'app-with-store',
template: '',
})
class WithStoreComponent {
store = inject(GlobalService);

constructor() {
this.store.observableMethod(this.store.globalObservable);
}
}

const globalService = setup(WithStoreComponent);
const harness = await RouterTestingHarness.create('/with-store');
it('tracks an observable until the instanceInjector is destroyed', () => {
const instanceInjector = createEnvironmentInjector(
[],
TestBed.inject(EnvironmentInjector)
);
const globalService = TestBed.inject(GlobalService);
runInInjectionContext(instanceInjector, () =>
globalService.trackObservable(globalService.globalObservable)
);

TestBed.flushEffects();
expect(globalService.globalObservableChangeCounter).toBe(1);

globalService.incrementObservable();
Expand All @@ -322,98 +281,67 @@ describe('rxMethod', () => {
globalService.incrementObservable();
expect(globalService.globalObservableChangeCounter).toBe(3);

await harness.navigateByUrl('/without-store');
instanceInjector.destroy();
globalService.incrementObservable();

expect(globalService.globalObservableChangeCounter).toBe(3);
});

it('tracks a signal until the provided injector is destroyed', async () => {
@Component({
selector: 'app-with-store',
template: '',
})
class WithStoreComponent implements OnInit {
store = inject(GlobalService);
injector = inject(Injector);

ngOnInit() {
this.store.signalMethod(this.store.globalSignal, {
injector: this.injector,
});
}
}

const globalService = setup(WithStoreComponent);
const harness = await RouterTestingHarness.create('/with-store');
it('tracks a signal until the provided injector is destroyed', () => {
const instanceInjector = createEnvironmentInjector(
[],
TestBed.inject(EnvironmentInjector)
);
const globalService = TestBed.inject(GlobalService);
globalService.trackSignal(globalService.globalSignal, {
injector: instanceInjector,
});

TestBed.flushEffects();
globalService.incrementSignal();
harness.detectChanges();
TestBed.flushEffects();

expect(globalService.globalSignalChangeCounter).toBe(2);

await harness.navigateByUrl('/without-store');
instanceInjector.destroy();
globalService.incrementSignal();
harness.detectChanges();
TestBed.flushEffects();

expect(globalService.globalSignalChangeCounter).toBe(2);
});

it('tracks an observable until the provided injector is destroyed', async () => {
@Component({
selector: 'app-with-store',
template: '',
})
class WithStoreComponent implements OnInit {
store = inject(GlobalService);
injector = inject(Injector);

ngOnInit() {
this.store.observableMethod(this.store.globalObservable, {
injector: this.injector,
});
}
}

const globalService = setup(WithStoreComponent);
const harness = await RouterTestingHarness.create('/with-store');
const instanceInjector = createEnvironmentInjector(
[],
TestBed.inject(EnvironmentInjector)
);
const globalService = TestBed.inject(GlobalService);
globalService.trackObservable(globalService.globalObservable, {
injector: instanceInjector,
});

globalService.incrementObservable();

expect(globalService.globalObservableChangeCounter).toBe(2);

await harness.navigateByUrl('/without-store');
instanceInjector.destroy();
globalService.incrementObservable();

expect(globalService.globalObservableChangeCounter).toBe(2);
});

it('falls back to source injector when reactive method is called outside of the injection context', async () => {
@Component({
selector: 'app-with-store',
template: '',
})
class WithStoreComponent implements OnInit {
store = inject(GlobalService);

ngOnInit() {
this.store.signalMethod(this.store.globalSignal);
this.store.observableMethod(this.store.globalObservable);
}
}
it('falls back to source injector when reactive method is called outside of the injection context', () => {
const globalService = TestBed.inject(GlobalService);

const globalService = setup(WithStoreComponent);
const harness = await RouterTestingHarness.create('/with-store');
globalService.trackSignal(globalService.globalSignal);
globalService.trackObservable(globalService.globalObservable);

TestBed.flushEffects();
expect(globalService.globalSignalChangeCounter).toBe(1);
expect(globalService.globalObservableChangeCounter).toBe(1);

await harness.navigateByUrl('/without-store');
globalService.incrementSignal();
TestBed.flushEffects();
globalService.incrementObservable();
TestBed.flushEffects();

expect(globalService.globalSignalChangeCounter).toBe(2);
expect(globalService.globalObservableChangeCounter).toBe(2);
Expand Down

0 comments on commit cf62e71

Please sign in to comment.