@@ -9,12 +9,12 @@ import {
9
9
signal ,
10
10
} from '@angular/core' ;
11
11
import { TestBed } from '@angular/core/testing' ;
12
- import { BehaviorSubject , finalize , pipe , Subject , tap } from 'rxjs' ;
13
- import { rxMethod } from '../src' ;
14
- import { createLocalService } from '../../spec/helpers' ;
15
- import { provideRouter } from '@angular/router' ;
16
12
import { provideLocationMocks } from '@angular/common/testing' ;
13
+ import { provideRouter } from '@angular/router' ;
17
14
import { RouterTestingHarness } from '@angular/router/testing' ;
15
+ import { BehaviorSubject , pipe , Subject , tap } from 'rxjs' ;
16
+ import { rxMethod } from '../src' ;
17
+ import { createLocalService } from '../../spec/helpers' ;
18
18
19
19
describe ( 'rxMethod' , ( ) => {
20
20
it ( 'runs with a value' , ( ) => {
@@ -239,43 +239,44 @@ describe('rxMethod', () => {
239
239
expect ( counter ( ) ) . toBe ( 4 ) ;
240
240
} ) ;
241
241
242
- it ( 'completes on manual destroy with Signals' , ( ) => {
243
- TestBed . runInInjectionContext ( ( ) => {
244
- let completed = false ;
245
- const counter = signal ( 1 ) ;
246
- const fn = rxMethod < number > ( finalize ( ( ) => ( completed = true ) ) ) ;
247
- TestBed . flushEffects ( ) ;
248
- fn ( counter ) ;
249
- fn . unsubscribe ( ) ;
250
- expect ( completed ) . toBe ( true ) ;
251
- } ) ;
252
- } ) ;
253
-
254
242
/**
255
- * This test suite verifies that the internal effect of
256
- * an RxMethod instance is executed with the correct injector
257
- * and is destroyed at the specified time.
258
- *
259
- * Since we cannot directly observe the destruction of the effect from the outside,
260
- * we test it indirectly.
243
+ * This test suite verifies that a signal or observable passed to a reactive
244
+ * method that is initialized at the ancestor injector level is tracked within
245
+ * the correct injection context and untracked at the specified time.
261
246
*
262
- * Components use the globalSignal from GlobalService and pass it
263
- * to the `log` method. If the component is destroyed but a subsequent
264
- * Signal change still increases the `globalSignalChangerCounter` ,
265
- * it indicates that the internal effect is still active.
247
+ * Components use ` globalSignal` or `globalObservable` from `GlobalService`
248
+ * and pass it to the reactive method. If the component is destroyed but
249
+ * signal or observable change still increases the corresponding counter ,
250
+ * the internal effect or subscription is still active.
266
251
*/
267
- describe ( 'Internal effect for Signal tracking ' , ( ) => {
252
+ describe ( 'with instance injector ' , ( ) => {
268
253
@Injectable ( { providedIn : 'root' } )
269
254
class GlobalService {
270
- globalSignal = signal ( 1 ) ;
255
+ readonly globalSignal = signal ( 1 ) ;
256
+ readonly globalObservable = new BehaviorSubject ( 1 ) ;
257
+
271
258
globalSignalChangeCounter = 0 ;
259
+ globalObservableChangeCounter = 0 ;
260
+
261
+ readonly signalMethod = rxMethod < number > (
262
+ tap ( ( ) => this . globalSignalChangeCounter ++ )
263
+ ) ;
264
+ readonly observableMethod = rxMethod < number > (
265
+ tap ( ( ) => this . globalObservableChangeCounter ++ )
266
+ ) ;
272
267
273
- log = rxMethod < number > ( pipe ( tap ( ( ) => this . globalSignalChangeCounter ++ ) ) ) ;
268
+ incrementSignal ( ) : void {
269
+ this . globalSignal . update ( ( value ) => value + 1 ) ;
270
+ }
271
+
272
+ incrementObservable ( ) : void {
273
+ this . globalObservable . next ( this . globalObservable . value + 1 ) ;
274
+ }
274
275
}
275
276
276
277
@Component ( {
277
- selector : ` app-storeless` ,
278
- template : `` ,
278
+ selector : ' app-without-store' ,
279
+ template : '' ,
279
280
standalone : true ,
280
281
} )
281
282
class WithoutStoreComponent { }
@@ -297,111 +298,161 @@ describe('rxMethod', () => {
297
298
return TestBed . inject ( GlobalService ) ;
298
299
}
299
300
300
- it ( 'it tracks the Signal when component is active ' , async ( ) => {
301
+ it ( 'tracks a signal until the component is destroyed ' , async ( ) => {
301
302
@Component ( {
302
303
selector : 'app-with-store' ,
303
- template : `` ,
304
+ template : '' ,
304
305
standalone : true ,
305
306
} )
306
307
class WithStoreComponent {
307
308
store = inject ( GlobalService ) ;
308
309
309
310
constructor ( ) {
310
- this . store . log ( this . store . globalSignal ) ;
311
+ this . store . signalMethod ( this . store . globalSignal ) ;
311
312
}
312
313
}
313
314
314
315
const globalService = setup ( WithStoreComponent ) ;
316
+ const harness = await RouterTestingHarness . create ( '/with-store' ) ;
315
317
316
- await RouterTestingHarness . create ( '/with-store' ) ;
317
318
expect ( globalService . globalSignalChangeCounter ) . toBe ( 1 ) ;
318
319
319
- globalService . globalSignal . update ( ( value ) => value + 1 ) ;
320
+ globalService . incrementSignal ( ) ;
320
321
TestBed . flushEffects ( ) ;
321
322
expect ( globalService . globalSignalChangeCounter ) . toBe ( 2 ) ;
322
323
323
- globalService . globalSignal . update ( ( value ) => value + 1 ) ;
324
+ globalService . incrementSignal ( ) ;
324
325
TestBed . flushEffects ( ) ;
325
326
expect ( globalService . globalSignalChangeCounter ) . toBe ( 3 ) ;
327
+
328
+ await harness . navigateByUrl ( '/without-store' ) ;
329
+ globalService . incrementSignal ( ) ;
330
+ TestBed . flushEffects ( ) ;
331
+
332
+ expect ( globalService . globalSignalChangeCounter ) . toBe ( 3 ) ;
326
333
} ) ;
327
334
328
- it ( 'destroys with component injector when rxMethod is in root and RxMethod in component ' , async ( ) => {
335
+ it ( 'tracks an observable until the component is destroyed ' , async ( ) => {
329
336
@Component ( {
330
337
selector : 'app-with-store' ,
331
- template : `` ,
338
+ template : '' ,
332
339
standalone : true ,
333
340
} )
334
341
class WithStoreComponent {
335
342
store = inject ( GlobalService ) ;
336
343
337
344
constructor ( ) {
338
- this . store . log ( this . store . globalSignal ) ;
345
+ this . store . observableMethod ( this . store . globalObservable ) ;
339
346
}
340
347
}
341
348
342
349
const globalService = setup ( WithStoreComponent ) ;
343
-
344
350
const harness = await RouterTestingHarness . create ( '/with-store' ) ;
345
351
346
- // effect is destroyed → Signal is not tracked anymore
352
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 1 ) ;
353
+
354
+ globalService . incrementObservable ( ) ;
355
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 2 ) ;
356
+
357
+ globalService . incrementObservable ( ) ;
358
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 3 ) ;
359
+
347
360
await harness . navigateByUrl ( '/without-store' ) ;
348
- globalService . globalSignal . update ( ( value ) => value + 1 ) ;
349
- TestBed . flushEffects ( ) ;
361
+ globalService . incrementObservable ( ) ;
350
362
351
- expect ( globalService . globalSignalChangeCounter ) . toBe ( 1 ) ;
363
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 3 ) ;
352
364
} ) ;
353
365
354
- it ( "falls back to rxMethod's injector when RxMethod's call is outside of injection context" , async ( ) => {
366
+ it ( 'tracks a signal until the provided injector is destroyed' , async ( ) => {
355
367
@Component ( {
356
- selector : ` app-store` ,
357
- template : `` ,
368
+ selector : ' app-with- store' ,
369
+ template : '' ,
358
370
standalone : true ,
359
371
} )
360
372
class WithStoreComponent implements OnInit {
361
373
store = inject ( GlobalService ) ;
374
+ injector = inject ( Injector ) ;
362
375
363
376
ngOnInit ( ) {
364
- this . store . log ( this . store . globalSignal ) ;
377
+ this . store . signalMethod ( this . store . globalSignal , {
378
+ injector : this . injector ,
379
+ } ) ;
365
380
}
366
381
}
367
382
368
383
const globalService = setup ( WithStoreComponent ) ;
369
-
370
384
const harness = await RouterTestingHarness . create ( '/with-store' ) ;
371
385
372
- // Signal is still tracked because RxMethod injector is used
386
+ globalService . incrementSignal ( ) ;
387
+ TestBed . flushEffects ( ) ;
388
+
389
+ expect ( globalService . globalSignalChangeCounter ) . toBe ( 2 ) ;
390
+
373
391
await harness . navigateByUrl ( '/without-store' ) ;
374
- globalService . globalSignal . update ( ( value ) => value + 1 ) ;
392
+ globalService . incrementSignal ( ) ;
375
393
TestBed . flushEffects ( ) ;
376
394
377
395
expect ( globalService . globalSignalChangeCounter ) . toBe ( 2 ) ;
378
396
} ) ;
379
397
380
- it ( 'provides the injector for RxMethod on call ' , async ( ) => {
398
+ it ( 'tracks an observable until the provided injector is destroyed ' , async ( ) => {
381
399
@Component ( {
382
- selector : ` app-store` ,
383
- template : `` ,
400
+ selector : ' app-with- store' ,
401
+ template : '' ,
384
402
standalone : true ,
385
403
} )
386
404
class WithStoreComponent implements OnInit {
387
405
store = inject ( GlobalService ) ;
388
406
injector = inject ( Injector ) ;
389
407
390
408
ngOnInit ( ) {
391
- this . store . log ( this . store . globalSignal , { injector : this . injector } ) ;
409
+ this . store . observableMethod ( this . store . globalObservable , {
410
+ injector : this . injector ,
411
+ } ) ;
392
412
}
393
413
}
394
414
395
415
const globalService = setup ( WithStoreComponent ) ;
416
+ const harness = await RouterTestingHarness . create ( '/with-store' ) ;
417
+
418
+ globalService . incrementObservable ( ) ;
419
+
420
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 2 ) ;
421
+
422
+ await harness . navigateByUrl ( '/without-store' ) ;
423
+ globalService . incrementObservable ( ) ;
424
+
425
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 2 ) ;
426
+ } ) ;
396
427
428
+ it ( 'falls back to source injector when reactive method is called is outside of injection context' , async ( ) => {
429
+ @Component ( {
430
+ selector : 'app-with-store' ,
431
+ template : '' ,
432
+ standalone : true ,
433
+ } )
434
+ class WithStoreComponent implements OnInit {
435
+ store = inject ( GlobalService ) ;
436
+
437
+ ngOnInit ( ) {
438
+ this . store . signalMethod ( this . store . globalSignal ) ;
439
+ this . store . observableMethod ( this . store . globalObservable ) ;
440
+ }
441
+ }
442
+
443
+ const globalService = setup ( WithStoreComponent ) ;
397
444
const harness = await RouterTestingHarness . create ( '/with-store' ) ;
398
445
399
- // effect is destroyed → Signal is not tracked anymore
446
+ expect ( globalService . globalSignalChangeCounter ) . toBe ( 1 ) ;
447
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 1 ) ;
448
+
400
449
await harness . navigateByUrl ( '/without-store' ) ;
401
- globalService . globalSignal . update ( ( value ) => value + 1 ) ;
450
+ globalService . incrementSignal ( ) ;
402
451
TestBed . flushEffects ( ) ;
452
+ globalService . incrementObservable ( ) ;
403
453
404
- expect ( globalService . globalSignalChangeCounter ) . toBe ( 1 ) ;
454
+ expect ( globalService . globalSignalChangeCounter ) . toBe ( 2 ) ;
455
+ expect ( globalService . globalObservableChangeCounter ) . toBe ( 2 ) ;
405
456
} ) ;
406
457
} ) ;
407
458
} ) ;
0 commit comments