Skip to content

Commit f05507f

Browse files
CopilotLayZeeDK
andcommitted
Add comprehensive documentation and demo tests for TestingRouterStore
Co-authored-by: LayZeeDK <[email protected]>
1 parent acb1d29 commit f05507f

File tree

2 files changed

+345
-0
lines changed

2 files changed

+345
-0
lines changed

packages/router-component-store/README.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,150 @@ export type StrictRouteParams = {
208208
readonly [key: string]: string | undefined;
209209
};
210210
```
211+
212+
## Testing
213+
214+
Router Component Store provides testing utilities to make it easy to test components and services that depend on `RouterStore`.
215+
216+
### TestingRouterStore
217+
218+
`TestingRouterStore` is a testing implementation of the `RouterStore` interface that uses stubbed observables. This allows you to easily control router state in your tests without needing to set up complex routing configurations.
219+
220+
#### Basic usage
221+
222+
```typescript
223+
import { TestBed } from '@angular/core/testing';
224+
import { provideTestingRouterStore, TestingRouterStore } from '@ngworker/router-component-store';
225+
226+
describe('HeroDetailComponent', () => {
227+
let routerStore: TestingRouterStore;
228+
229+
beforeEach(() => {
230+
TestBed.configureTestingModule({
231+
imports: [HeroDetailComponent],
232+
providers: [provideTestingRouterStore()],
233+
});
234+
235+
routerStore = TestBed.inject(RouterStore) as TestingRouterStore;
236+
});
237+
238+
it('should display hero ID from route param', () => {
239+
const fixture = TestBed.createComponent(HeroDetailComponent);
240+
241+
// Set route parameter
242+
routerStore.setRouteParam('id', '123');
243+
fixture.detectChanges();
244+
245+
expect(fixture.nativeElement.textContent).toContain('Hero: 123');
246+
});
247+
});
248+
```
249+
250+
#### Testing different router states
251+
252+
```typescript
253+
it('should handle various router states', () => {
254+
// Set URL
255+
routerStore.setUrl('/heroes/456?search=batman#details');
256+
257+
// Set individual parameters
258+
routerStore.setRouteParam('id', '456');
259+
routerStore.setQueryParam('search', 'batman');
260+
routerStore.setFragment('details');
261+
262+
// Set route data
263+
routerStore.setRouteDataParam('title', 'Hero Details');
264+
routerStore.setRouteDataParam('breadcrumbs', ['Home', 'Heroes']);
265+
266+
// Or set multiple values at once
267+
routerStore.setRouteParams({ id: '456', type: 'superhero' });
268+
routerStore.setQueryParams({ search: 'batman', page: '1' });
269+
routerStore.setRouteData({ title: 'Hero Details', allowEdit: true });
270+
271+
fixture.detectChanges();
272+
273+
// Your assertions here...
274+
});
275+
```
276+
277+
#### Testing with services
278+
279+
```typescript
280+
class HeroService {
281+
private routerStore = inject(RouterStore);
282+
283+
currentHeroId$ = this.routerStore.selectRouteParam('id');
284+
searchQuery$ = this.routerStore.selectQueryParam('q');
285+
}
286+
287+
describe('HeroService', () => {
288+
let service: HeroService;
289+
let routerStore: TestingRouterStore;
290+
291+
beforeEach(() => {
292+
TestBed.configureTestingModule({
293+
providers: [HeroService, provideTestingRouterStore()],
294+
});
295+
296+
service = TestBed.inject(HeroService);
297+
routerStore = TestBed.inject(RouterStore) as TestingRouterStore;
298+
});
299+
300+
it('should emit current hero ID', (done) => {
301+
service.currentHeroId$.subscribe(id => {
302+
expect(id).toBe('789');
303+
done();
304+
});
305+
306+
routerStore.setRouteParam('id', '789');
307+
});
308+
});
309+
```
310+
311+
#### Available testing methods
312+
313+
| Method | Description |
314+
| --- | --- |
315+
| `setUrl(url: string)` | Set the current URL |
316+
| `setFragment(fragment: string \| null)` | Set the URL fragment |
317+
| `setTitle(title: string \| undefined)` | Set the resolved route title |
318+
| `setRouteParam(param: string, value: string \| undefined)` | Set a single route parameter |
319+
| `setRouteParams(params: StrictRouteParams)` | Set all route parameters |
320+
| `setQueryParam(param: string, value: string \| readonly string[] \| undefined)` | Set a single query parameter |
321+
| `setQueryParams(params: StrictQueryParams)` | Set all query parameters |
322+
| `setRouteDataParam(key: string, value: unknown)` | Set a single route data value |
323+
| `setRouteData(data: StrictRouteData)` | Set all route data |
324+
| `setCurrentRoute(route: MinimalActivatedRouteSnapshot)` | Set the complete current route |
325+
| `reset()` | Reset all values to their defaults |
326+
327+
#### Integration with RouterTestingModule
328+
329+
While `TestingRouterStore` is great for isolated unit tests, you might sometimes want to test the full routing behavior. You can still use `RouterTestingModule` with the actual `RouterStore` implementations:
330+
331+
```typescript
332+
import { provideGlobalRouterStore } from '@ngworker/router-component-store';
333+
import { RouterTestingModule } from '@angular/router/testing';
334+
335+
describe('Full routing integration', () => {
336+
beforeEach(() => {
337+
TestBed.configureTestingModule({
338+
imports: [
339+
RouterTestingModule.withRoutes([
340+
{ path: 'heroes/:id', component: HeroDetailComponent }
341+
])
342+
],
343+
providers: [provideGlobalRouterStore()],
344+
});
345+
});
346+
347+
it('should work with actual navigation', async () => {
348+
const router = TestBed.inject(Router);
349+
const routerStore = TestBed.inject(RouterStore);
350+
351+
await router.navigate(['/heroes', '123']);
352+
353+
const heroId = await firstValueFrom(routerStore.selectRouteParam('id'));
354+
expect(heroId).toBe('123');
355+
});
356+
});
357+
```
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { AsyncPipe, NgIf } from '@angular/common';
2+
import { Component, inject } from '@angular/core';
3+
import { TestBed } from '@angular/core/testing';
4+
import { firstValueFrom } from 'rxjs';
5+
import { RouterStore } from '../router-store';
6+
import { provideTestingRouterStore, TestingRouterStore } from '../testing';
7+
8+
// Example component that uses RouterStore
9+
@Component({
10+
standalone: true,
11+
imports: [AsyncPipe, NgIf],
12+
selector: 'demo-hero-detail',
13+
template: `
14+
<h1 [textContent]="title$ | async"></h1>
15+
<div class="hero-info">
16+
<p class="hero-id">Hero ID: <span [textContent]="heroId$ | async"></span></p>
17+
<p class="search">Search: <span [textContent]="searchQuery$ | async"></span></p>
18+
<p class="url">URL: <span [textContent]="url$ | async"></span></p>
19+
<p class="allow-edit">Allow Edit: <span [textContent]="(allowEdit$ | async) ? 'Yes' : 'No'"></span></p>
20+
</div>
21+
<div class="breadcrumbs" *ngIf="breadcrumbs$ | async as breadcrumbs">
22+
Breadcrumbs: {{ breadcrumbs.join(' > ') }}
23+
</div>
24+
`,
25+
})
26+
class DemoHeroDetailComponent {
27+
private routerStore = inject(RouterStore);
28+
29+
title$ = this.routerStore.title$;
30+
heroId$ = this.routerStore.selectRouteParam('id');
31+
searchQuery$ = this.routerStore.selectQueryParam('search');
32+
url$ = this.routerStore.url$;
33+
allowEdit$ = this.routerStore.selectRouteDataParam('allowEdit');
34+
breadcrumbs$ = this.routerStore.selectRouteDataParam('breadcrumbs');
35+
}
36+
37+
// Example service that uses RouterStore
38+
class DemoHeroService {
39+
private routerStore = inject(RouterStore);
40+
41+
currentHeroId$ = this.routerStore.selectRouteParam('id');
42+
isEditMode$ = this.routerStore.selectQueryParam('edit');
43+
44+
async getCurrentHeroId(): Promise<string | undefined> {
45+
return firstValueFrom(this.currentHeroId$);
46+
}
47+
}
48+
49+
describe('TestingRouterStore Demo', () => {
50+
describe('Component Integration', () => {
51+
let routerStore: TestingRouterStore;
52+
53+
beforeEach(() => {
54+
TestBed.configureTestingModule({
55+
imports: [DemoHeroDetailComponent],
56+
providers: [provideTestingRouterStore()],
57+
});
58+
59+
routerStore = TestBed.inject(RouterStore) as TestingRouterStore;
60+
});
61+
62+
it('should demonstrate complete router state control', async () => {
63+
const fixture = TestBed.createComponent(DemoHeroDetailComponent);
64+
65+
// Set up a complex router state
66+
routerStore.setUrl('/heroes/123?search=batman&edit=true#details');
67+
routerStore.setTitle('Batman Details');
68+
routerStore.setRouteParam('id', '123');
69+
routerStore.setQueryParams({ search: 'batman', edit: 'true' });
70+
routerStore.setRouteData({
71+
allowEdit: true,
72+
breadcrumbs: ['Home', 'Heroes', 'Batman'],
73+
});
74+
75+
fixture.detectChanges();
76+
await fixture.whenStable();
77+
78+
const compiled = fixture.nativeElement;
79+
80+
// Verify the component displays all the router state correctly
81+
expect(compiled.querySelector('h1')?.textContent?.trim()).toBe('Batman Details');
82+
expect(compiled.querySelector('.hero-id span')?.textContent?.trim()).toBe('123');
83+
expect(compiled.querySelector('.search span')?.textContent?.trim()).toBe('batman');
84+
expect(compiled.querySelector('.url span')?.textContent?.trim()).toBe('/heroes/123?search=batman&edit=true#details');
85+
expect(compiled.querySelector('.allow-edit span')?.textContent?.trim()).toBe('Yes');
86+
expect(compiled.textContent).toContain('Breadcrumbs: Home > Heroes > Batman');
87+
});
88+
89+
it('should handle state changes dynamically', async () => {
90+
const fixture = TestBed.createComponent(DemoHeroDetailComponent);
91+
92+
// Start with default state
93+
fixture.detectChanges();
94+
expect(fixture.nativeElement.querySelector('.hero-id span')?.textContent).toBeFalsy();
95+
96+
// Update hero ID
97+
routerStore.setRouteParam('id', '456');
98+
fixture.detectChanges();
99+
expect(fixture.nativeElement.querySelector('.hero-id span')?.textContent?.trim()).toBe('456');
100+
101+
// Reset and verify defaults are restored
102+
routerStore.reset();
103+
fixture.detectChanges();
104+
await fixture.whenStable();
105+
expect(fixture.nativeElement.querySelector('.hero-id span')?.textContent).toBeFalsy();
106+
});
107+
});
108+
109+
describe('Service Integration', () => {
110+
let service: DemoHeroService;
111+
let routerStore: TestingRouterStore;
112+
113+
beforeEach(() => {
114+
TestBed.configureTestingModule({
115+
providers: [DemoHeroService, provideTestingRouterStore()],
116+
});
117+
118+
service = TestBed.inject(DemoHeroService);
119+
routerStore = TestBed.inject(RouterStore) as TestingRouterStore;
120+
});
121+
122+
it('should work with services that depend on RouterStore', async () => {
123+
// Initially no hero ID
124+
expect(await service.getCurrentHeroId()).toBeUndefined();
125+
126+
// Set hero ID and verify service picks it up
127+
routerStore.setRouteParam('id', '789');
128+
expect(await service.getCurrentHeroId()).toBe('789');
129+
});
130+
131+
it('should handle observable streams', (done) => {
132+
let emissionCount = 0;
133+
const expectedValues = [undefined, 'first', 'second'];
134+
135+
service.currentHeroId$.subscribe(id => {
136+
expect(id).toBe(expectedValues[emissionCount]);
137+
emissionCount++;
138+
139+
if (emissionCount === 3) {
140+
done();
141+
}
142+
});
143+
144+
// Trigger emissions
145+
routerStore.setRouteParam('id', 'first');
146+
routerStore.setRouteParam('id', 'second');
147+
});
148+
});
149+
150+
describe('Complex Scenarios', () => {
151+
let routerStore: TestingRouterStore;
152+
153+
beforeEach(() => {
154+
TestBed.configureTestingModule({
155+
providers: [provideTestingRouterStore()],
156+
});
157+
158+
routerStore = TestBed.inject(RouterStore) as TestingRouterStore;
159+
});
160+
161+
it('should handle array query parameters', async () => {
162+
routerStore.setQueryParam('tags', ['action', 'superhero', 'dc']);
163+
164+
const tags = await firstValueFrom(routerStore.selectQueryParam('tags'));
165+
expect(tags).toEqual(['action', 'superhero', 'dc']);
166+
});
167+
168+
it('should handle complex route data', async () => {
169+
const complexData = {
170+
permissions: ['read', 'write'],
171+
metadata: { version: '1.0', author: 'test' },
172+
settings: { theme: 'dark', language: 'en' },
173+
};
174+
175+
routerStore.setRouteDataParam('config', complexData);
176+
177+
const config = await firstValueFrom(routerStore.selectRouteDataParam('config'));
178+
expect(config).toEqual(complexData);
179+
});
180+
181+
it('should preserve independent parameter updates', async () => {
182+
// Set initial state
183+
routerStore.setRouteParams({ id: '1', type: 'hero' });
184+
routerStore.setQueryParams({ search: 'batman', page: '1' });
185+
186+
// Update individual parameters
187+
routerStore.setRouteParam('id', '2');
188+
routerStore.setQueryParam('search', 'superman');
189+
190+
// Verify other parameters are preserved
191+
const routeParams = await firstValueFrom(routerStore.routeParams$);
192+
const queryParams = await firstValueFrom(routerStore.queryParams$);
193+
194+
expect(routeParams).toEqual({ id: '2', type: 'hero' });
195+
expect(queryParams).toEqual({ search: 'superman', page: '1' });
196+
});
197+
});
198+
});

0 commit comments

Comments
 (0)