From b17c713ab7fa3bb608fb1429f8a2e80e5f70d10f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:27:46 +0000 Subject: [PATCH 1/5] Initial plan From acb1d29348f5231bb56f5a4514bccdc20f98b21c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:39:03 +0000 Subject: [PATCH 2/5] Implement TestingRouterStore and provideTestingRouterStore Co-authored-by: LayZeeDK <6364586+LayZeeDK@users.noreply.github.com> --- packages/router-component-store/src/index.ts | 2 + .../src/lib/testing/index.ts | 2 + .../provide-testing-router-store.spec.ts | 32 ++ .../testing/provide-testing-router-store.ts | 98 +++++ .../lib/testing/testing-router-store.spec.ts | 413 ++++++++++++++++++ .../src/lib/testing/testing-router-store.ts | 215 +++++++++ 6 files changed, 762 insertions(+) create mode 100644 packages/router-component-store/src/lib/testing/index.ts create mode 100644 packages/router-component-store/src/lib/testing/provide-testing-router-store.spec.ts create mode 100644 packages/router-component-store/src/lib/testing/provide-testing-router-store.ts create mode 100644 packages/router-component-store/src/lib/testing/testing-router-store.spec.ts create mode 100644 packages/router-component-store/src/lib/testing/testing-router-store.ts diff --git a/packages/router-component-store/src/index.ts b/packages/router-component-store/src/index.ts index f4f202a9..139780b1 100644 --- a/packages/router-component-store/src/index.ts +++ b/packages/router-component-store/src/index.ts @@ -9,3 +9,5 @@ export * from './lib/router-store'; export * from './lib/strict-query-params'; export * from './lib/strict-route-data'; export * from './lib/strict-route-params'; +// Testing utilities +export * from './lib/testing'; diff --git a/packages/router-component-store/src/lib/testing/index.ts b/packages/router-component-store/src/lib/testing/index.ts new file mode 100644 index 00000000..65ef0f49 --- /dev/null +++ b/packages/router-component-store/src/lib/testing/index.ts @@ -0,0 +1,2 @@ +export * from './provide-testing-router-store'; +export * from './testing-router-store'; \ No newline at end of file diff --git a/packages/router-component-store/src/lib/testing/provide-testing-router-store.spec.ts b/packages/router-component-store/src/lib/testing/provide-testing-router-store.spec.ts new file mode 100644 index 00000000..86eed18f --- /dev/null +++ b/packages/router-component-store/src/lib/testing/provide-testing-router-store.spec.ts @@ -0,0 +1,32 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterStore } from '../router-store'; +import { provideTestingRouterStore } from './provide-testing-router-store'; +import { TestingRouterStore } from './testing-router-store'; + +describe('provideTestingRouterStore', () => { + it('should provide TestingRouterStore for RouterStore token', () => { + TestBed.configureTestingModule({ + providers: [provideTestingRouterStore()], + }); + + const routerStore = TestBed.inject(RouterStore); + + expect(routerStore).toBeInstanceOf(TestingRouterStore); + }); + + it('should allow casting to TestingRouterStore for access to testing methods', () => { + TestBed.configureTestingModule({ + providers: [provideTestingRouterStore()], + }); + + const routerStore = TestBed.inject(RouterStore); + const testingRouterStore = routerStore as TestingRouterStore; + + // Should have access to testing methods + expect(typeof testingRouterStore.setUrl).toBe('function'); + expect(typeof testingRouterStore.setRouteParam).toBe('function'); + expect(typeof testingRouterStore.setQueryParam).toBe('function'); + expect(typeof testingRouterStore.setRouteDataParam).toBe('function'); + expect(typeof testingRouterStore.reset).toBe('function'); + }); +}); \ No newline at end of file diff --git a/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts b/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts new file mode 100644 index 00000000..23685899 --- /dev/null +++ b/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts @@ -0,0 +1,98 @@ +import { ClassProvider, Provider } from '@angular/core'; +import { RouterStore } from '../router-store'; +import { TestingRouterStore } from './testing-router-store'; + +/** + * Provide a testing version of `RouterStore` that uses stubbed observables + * for easy test setup and improved developer experience. + * + * This provider replaces the `RouterStore` dependency injection token with + * `TestingRouterStore`, allowing you to easily control router state in your tests. + * + * @returns The providers required for a testing router store. + * + * @example + * ```typescript + * // Basic usage in TestBed + * TestBed.configureTestingModule({ + * providers: [provideTestingRouterStore()], + * }); + * + * const routerStore = TestBed.inject(RouterStore) as TestingRouterStore; + * routerStore.setUrl('/test/123'); + * routerStore.setRouteParam('id', '123'); + * ``` + * + * @example + * ```typescript + * // Usage with component testing + * @Component({ + * template: '
Hero ID:
+Search:
+URL:
+Allow Edit:
+Route param: {{ routeParam$ | async }}
', +}) +class ExampleComponent { + private routerStore = inject(RouterStore); + routeParam$ = this.routerStore.selectRouteParam('id'); +} + +describe('injectTestingRouterStore - Usage Examples', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideTestingRouterStore()], + }); + }); + + it('demonstrates the old way vs the new way', () => { + // OLD WAY: Manual casting required + const oldWay = TestBed.inject(RouterStore) as TestingRouterStore; + oldWay.setRouteParam('id', 'old-way'); + + // NEW WAY: No casting needed with injection helper + TestBed.runInInjectionContext(() => { + const newWay = injectTestingRouterStore(); + newWay.setRouteParam('id', 'new-way'); + }); + + // Both approaches work, but the new way provides better type safety + expect(oldWay).toBeInstanceOf(TestingRouterStore); + expect(() => { + TestBed.runInInjectionContext(() => { + const newWay = injectTestingRouterStore(); + expect(newWay).toBeInstanceOf(TestingRouterStore); + }); + }).not.toThrow(); + }); + + it('provides immediate access to testing methods', () => { + TestBed.runInInjectionContext(() => { + const routerStore = injectTestingRouterStore(); + + // Direct access to all testing methods without casting + routerStore.setUrl('/test/path'); + routerStore.setRouteParam('id', '123'); + routerStore.setQueryParam('search', 'test'); + routerStore.setFragment('section'); + routerStore.setTitle('Test Page'); + + // TypeScript IntelliSense will work perfectly + expect(typeof routerStore.reset).toBe('function'); + }); + }); +}); \ No newline at end of file diff --git a/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts b/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts index 23685899..f2b79741 100644 --- a/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts +++ b/packages/router-component-store/src/lib/testing/provide-testing-router-store.ts @@ -18,9 +18,15 @@ import { TestingRouterStore } from './testing-router-store'; * providers: [provideTestingRouterStore()], * }); * + * // Option 1: Manual casting * const routerStore = TestBed.inject(RouterStore) as TestingRouterStore; * routerStore.setUrl('/test/123'); * routerStore.setRouteParam('id', '123'); + * + * // Option 2: Using injection helper (recommended) + * const routerStore = injectTestingRouterStore(); + * routerStore.setUrl('/test/123'); + * routerStore.setRouteParam('id', '123'); * ``` * * @example diff --git a/packages/router-component-store/src/lib/testing/testing-router-store-demo.spec.ts b/packages/router-component-store/src/lib/testing/testing-router-store-demo.spec.ts index ed2b4f4e..6abc72fb 100644 --- a/packages/router-component-store/src/lib/testing/testing-router-store-demo.spec.ts +++ b/packages/router-component-store/src/lib/testing/testing-router-store-demo.spec.ts @@ -9,7 +9,7 @@ import { provideTestingRouterStore, TestingRouterStore } from '../testing'; @Component({ standalone: true, imports: [AsyncPipe, NgIf], - selector: 'demo-hero-detail', + selector: 'ngw-demo-hero-detail', template: `Hero: {{ heroId$ | async }}
', + providers: [provideTestingRouterStore()], // Local provider +}) +class HeroComponent { + private routerStore = inject(RouterStore); + heroId$ = this.routerStore.selectRouteParam('id'); +} + +// In your test +const fixture = TestBed.createComponent(ParentComponent); + +// Inject from the specific component's injector +const routerStore = injectTestingRouterStore({ + component: HeroComponent, + fixture, + options: { host: true } // Optional injection options +}); + +routerStore.setRouteParam('id', '123'); +fixture.detectChanges(); +``` + **Note:** The `injectTestingRouterStore()` function should only be used when `provideTestingRouterStore()` is provided in the testing module. It must be called within an injection context (e.g., inside `TestBed.runInInjectionContext()` or within a component/service constructor). #### Available testing methods diff --git a/packages/router-component-store/src/lib/testing/inject-testing-router-store.example.spec.ts b/packages/router-component-store/src/lib/testing/inject-testing-router-store.example.spec.ts index d49bb12a..b4a2f42b 100644 --- a/packages/router-component-store/src/lib/testing/inject-testing-router-store.example.spec.ts +++ b/packages/router-component-store/src/lib/testing/inject-testing-router-store.example.spec.ts @@ -1,19 +1,7 @@ -import { Component, inject } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { RouterStore } from '../router-store'; import { injectTestingRouterStore, provideTestingRouterStore, TestingRouterStore } from '../testing'; -// Example showing the improvement in developer experience -@Component({ - standalone: true, - selector: 'ngw-example-usage', - template: 'Route param: {{ routeParam$ | async }}
', -}) -class ExampleComponent { - private routerStore = inject(RouterStore); - routeParam$ = this.routerStore.selectRouteParam('id'); -} - describe('injectTestingRouterStore - Usage Examples', () => { beforeEach(() => { TestBed.configureTestingModule({ diff --git a/packages/router-component-store/src/lib/testing/testing-router-store.spec.ts b/packages/router-component-store/src/lib/testing/testing-router-store.spec.ts index 9b2e93c5..a4cd9db3 100644 --- a/packages/router-component-store/src/lib/testing/testing-router-store.spec.ts +++ b/packages/router-component-store/src/lib/testing/testing-router-store.spec.ts @@ -488,4 +488,223 @@ describe('injectTestingRouterStore', () => { // the injection works by checking the component renders properly expect(fixture.nativeElement.querySelector('div')).toBeTruthy(); }); +}); + +describe('injectTestingRouterStore - Enhanced Options', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideTestingRouterStore()], + }); + }); + + describe('injection options', () => { + it('should support optional injection', () => { + TestBed.runInInjectionContext(() => { + // With optional: false (default behavior) + const routerStore = injectTestingRouterStore(); + expect(routerStore).toBeInstanceOf(TestingRouterStore); + + // With optional: true - should still work since TestingRouterStore is provided + const optionalStore = injectTestingRouterStore({ optional: true }); + expect(optionalStore).toBeInstanceOf(TestingRouterStore); + }); + }); + + it('should support host injection option', () => { + TestBed.runInInjectionContext(() => { + const routerStore = injectTestingRouterStore({ host: true }); + expect(routerStore).toBeInstanceOf(TestingRouterStore); + expect(typeof routerStore.setRouteParam).toBe('function'); + }); + }); + + it('should support self injection option', () => { + TestBed.runInInjectionContext(() => { + const routerStore = injectTestingRouterStore({ self: true }); + expect(routerStore).toBeInstanceOf(TestingRouterStore); + expect(typeof routerStore.setUrl).toBe('function'); + }); + }); + + it('should support skipSelf injection option', () => { + TestBed.runInInjectionContext(() => { + const routerStore = injectTestingRouterStore({ skipSelf: false }); + expect(routerStore).toBeInstanceOf(TestingRouterStore); + expect(typeof routerStore.reset).toBe('function'); + }); + }); + + it('should support combined injection options', () => { + TestBed.runInInjectionContext(() => { + const routerStore = injectTestingRouterStore({ + optional: false, + host: true, + self: false, + }); + expect(routerStore).toBeInstanceOf(TestingRouterStore); + }); + }); + }); + + describe('component injector support', () => { + @Component({ + standalone: true, + selector: 'ngw-test-with-local', + template: 'Test Component
', + providers: [provideTestingRouterStore()], // Local provider + }) + class TestComponentWithLocalStoreComponent {} + + @Component({ + standalone: true, + template: 'Different Component
', + }) + class DifferentComponent {} + + TestBed.configureTestingModule({ + imports: [DifferentComponent], + }); + + const fixture = TestBed.createComponent(DifferentComponent); + + expect(() => { + injectTestingRouterStore({ + component: TestComponentWithLocalStoreComponent, // This component is not in the fixture + fixture, + }); + }).toThrow('Component TestComponentWithLocalStoreComponent not found in fixture'); + }); + }); +}); + +describe('injectTestingRouterStore - Real-world Usage', () => { + @Component({ + standalone: true, + selector: 'ngw-hero-detail', + template: ` +Search: {{ search$ | async }}
+