@furystack/shades 14.0.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/esm/component-factory.spec.d.ts.map +1 -1
  3. package/esm/component-factory.spec.js +2 -2
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/nested-hooks.js +2 -2
  6. package/esm/components/nested-hooks.js.map +1 -1
  7. package/esm/components/nested-hooks.spec.js +14 -14
  8. package/esm/components/nested-hooks.spec.js.map +1 -1
  9. package/esm/components/nested-navigate.js +1 -1
  10. package/esm/components/nested-navigate.js.map +1 -1
  11. package/esm/components/nested-navigate.spec.js +16 -16
  12. package/esm/components/nested-navigate.spec.js.map +1 -1
  13. package/esm/components/nested-replace.js +1 -1
  14. package/esm/components/nested-replace.js.map +1 -1
  15. package/esm/components/nested-replace.spec.js +6 -6
  16. package/esm/components/nested-replace.spec.js.map +1 -1
  17. package/esm/components/nested-route-link.js +1 -1
  18. package/esm/components/nested-route-link.js.map +1 -1
  19. package/esm/components/nested-route-link.spec.js +8 -8
  20. package/esm/components/nested-route-link.spec.js.map +1 -1
  21. package/esm/components/nested-router.js +3 -3
  22. package/esm/components/nested-router.js.map +1 -1
  23. package/esm/components/nested-router.spec.js +24 -24
  24. package/esm/components/nested-router.spec.js.map +1 -1
  25. package/esm/services/location-service.d.ts +39 -42
  26. package/esm/services/location-service.d.ts.map +1 -1
  27. package/esm/services/location-service.js +115 -134
  28. package/esm/services/location-service.js.map +1 -1
  29. package/esm/services/location-service.spec.js +32 -30
  30. package/esm/services/location-service.spec.js.map +1 -1
  31. package/esm/services/route-match-service.d.ts +3 -2
  32. package/esm/services/route-match-service.d.ts.map +1 -1
  33. package/esm/services/route-match-service.js +13 -22
  34. package/esm/services/route-match-service.js.map +1 -1
  35. package/esm/services/route-match-service.spec.js +40 -51
  36. package/esm/services/route-match-service.spec.js.map +1 -1
  37. package/esm/services/route-meta-utils.spec.js +3 -3
  38. package/esm/services/route-meta-utils.spec.js.map +1 -1
  39. package/esm/services/screen-service.d.ts +13 -72
  40. package/esm/services/screen-service.d.ts.map +1 -1
  41. package/esm/services/screen-service.js +45 -137
  42. package/esm/services/screen-service.js.map +1 -1
  43. package/esm/services/screen-service.spec.js +19 -18
  44. package/esm/services/screen-service.spec.js.map +1 -1
  45. package/esm/services/spatial-navigation-service.d.ts +17 -39
  46. package/esm/services/spatial-navigation-service.d.ts.map +1 -1
  47. package/esm/services/spatial-navigation-service.js +280 -313
  48. package/esm/services/spatial-navigation-service.js.map +1 -1
  49. package/esm/services/spatial-navigation-service.spec.js +163 -158
  50. package/esm/services/spatial-navigation-service.spec.js.map +1 -1
  51. package/esm/shade.d.ts +1 -1
  52. package/esm/shade.d.ts.map +1 -1
  53. package/esm/shade.js +10 -7
  54. package/esm/shade.js.map +1 -1
  55. package/package.json +5 -4
  56. package/src/component-factory.spec.tsx +3 -2
  57. package/src/components/nested-hooks.spec.ts +14 -14
  58. package/src/components/nested-hooks.ts +2 -2
  59. package/src/components/nested-navigate.spec.ts +17 -16
  60. package/src/components/nested-navigate.ts +1 -1
  61. package/src/components/nested-replace.spec.ts +7 -6
  62. package/src/components/nested-replace.ts +1 -1
  63. package/src/components/nested-route-link.spec.tsx +8 -8
  64. package/src/components/nested-route-link.tsx +1 -1
  65. package/src/components/nested-router.spec.tsx +24 -24
  66. package/src/components/nested-router.tsx +3 -3
  67. package/src/services/location-service.spec.ts +35 -31
  68. package/src/services/location-service.tsx +156 -122
  69. package/src/services/route-match-service.spec.ts +41 -32
  70. package/src/services/route-match-service.ts +17 -8
  71. package/src/services/route-meta-utils.spec.ts +3 -3
  72. package/src/services/screen-service.spec.ts +19 -18
  73. package/src/services/screen-service.ts +65 -110
  74. package/src/services/spatial-navigation-service.spec.ts +165 -160
  75. package/src/services/spatial-navigation-service.ts +299 -287
  76. package/src/shade.ts +11 -8
@@ -1,4 +1,5 @@
1
- import { Injector } from '@furystack/inject'
1
+ import type { Injector } from '@furystack/inject'
2
+ import { createInjector } from '@furystack/inject'
2
3
  import { usingAsync } from '@furystack/utils'
3
4
  import { describe, expect, expectTypeOf, it, vi } from 'vitest'
4
5
  import { LocationService } from '../services/location-service.js'
@@ -10,8 +11,8 @@ type TestRoute = Pick<NestedRoute<unknown, any, any>, 'component'>
10
11
 
11
12
  describe('nestedNavigate', () => {
12
13
  it('Should navigate to a simple path', async () => {
13
- await usingAsync(new Injector(), async (injector) => {
14
- const locationService = injector.getInstance(LocationService)
14
+ await usingAsync(createInjector(), async (injector) => {
15
+ const locationService = injector.get(LocationService)
15
16
  const spy = vi.spyOn(locationService, 'navigate')
16
17
 
17
18
  nestedNavigate(injector, { path: '/buttons' })
@@ -21,8 +22,8 @@ describe('nestedNavigate', () => {
21
22
  })
22
23
 
23
24
  it('Should compile params into the path', async () => {
24
- await usingAsync(new Injector(), async (injector) => {
25
- const locationService = injector.getInstance(LocationService)
25
+ await usingAsync(createInjector(), async (injector) => {
26
+ const locationService = injector.get(LocationService)
26
27
  const spy = vi.spyOn(locationService, 'navigate')
27
28
 
28
29
  nestedNavigate(injector, { path: '/users/:id', params: { id: '42' } })
@@ -32,8 +33,8 @@ describe('nestedNavigate', () => {
32
33
  })
33
34
 
34
35
  it('Should compile multiple params into the path', async () => {
35
- await usingAsync(new Injector(), async (injector) => {
36
- const locationService = injector.getInstance(LocationService)
36
+ await usingAsync(createInjector(), async (injector) => {
37
+ const locationService = injector.get(LocationService)
37
38
  const spy = vi.spyOn(locationService, 'navigate')
38
39
 
39
40
  nestedNavigate(injector, {
@@ -46,8 +47,8 @@ describe('nestedNavigate', () => {
46
47
  })
47
48
 
48
49
  it('Should append serialized query string when provided', async () => {
49
- await usingAsync(new Injector(), async (injector) => {
50
- const locationService = injector.getInstance(LocationService)
50
+ await usingAsync(createInjector(), async (injector) => {
51
+ const locationService = injector.get(LocationService)
51
52
  const spy = vi.spyOn(locationService, 'navigate')
52
53
 
53
54
  nestedNavigate(injector, { path: '/buttons', query: { page: 2 } })
@@ -62,8 +63,8 @@ describe('nestedNavigate', () => {
62
63
  })
63
64
 
64
65
  it('Should omit the query string when no keys are provided', async () => {
65
- await usingAsync(new Injector(), async (injector) => {
66
- const locationService = injector.getInstance(LocationService)
66
+ await usingAsync(createInjector(), async (injector) => {
67
+ const locationService = injector.get(LocationService)
67
68
  const spy = vi.spyOn(locationService, 'navigate')
68
69
 
69
70
  nestedNavigate(injector, { path: '/buttons', query: {} })
@@ -73,8 +74,8 @@ describe('nestedNavigate', () => {
73
74
  })
74
75
 
75
76
  it('Should append the hash segment when provided', async () => {
76
- await usingAsync(new Injector(), async (injector) => {
77
- const locationService = injector.getInstance(LocationService)
77
+ await usingAsync(createInjector(), async (injector) => {
78
+ const locationService = injector.get(LocationService)
78
79
  const spy = vi.spyOn(locationService, 'navigate')
79
80
 
80
81
  nestedNavigate(injector, { path: '/buttons', hash: 'overview' })
@@ -84,8 +85,8 @@ describe('nestedNavigate', () => {
84
85
  })
85
86
 
86
87
  it('Should combine params, query and hash in the correct order', async () => {
87
- await usingAsync(new Injector(), async (injector) => {
88
- const locationService = injector.getInstance(LocationService)
88
+ await usingAsync(createInjector(), async (injector) => {
89
+ const locationService = injector.get(LocationService)
89
90
  const spy = vi.spyOn(locationService, 'navigate')
90
91
 
91
92
  nestedNavigate(injector, {
@@ -197,7 +198,7 @@ describe('Type utilities', () => {
197
198
 
198
199
  const appNavigate = createNestedNavigate<Routes>()
199
200
  // @ts-expect-error -- '/nonexistent' is not a valid route path
200
- appNavigate(new Injector(), { path: '/nonexistent' })
201
+ appNavigate(createInjector(), { path: '/nonexistent' })
201
202
  })
202
203
 
203
204
  it('Should accept routes with typed match parameters (NestedRoute<T>)', () => {
@@ -71,7 +71,7 @@ export const buildNestedNavigateUrl = (args: NestedNavigateArgs): string => {
71
71
  * @param args - The navigation arguments
72
72
  */
73
73
  export const nestedNavigate = (injector: Injector, args: NestedNavigateArgs): void => {
74
- injector.getInstance(LocationService).navigate(buildNestedNavigateUrl(args))
74
+ injector.get(LocationService).navigate(buildNestedNavigateUrl(args))
75
75
  }
76
76
 
77
77
  /**
@@ -1,4 +1,5 @@
1
- import { Injector } from '@furystack/inject'
1
+ import type { Injector } from '@furystack/inject'
2
+ import { createInjector } from '@furystack/inject'
2
3
  import { usingAsync } from '@furystack/utils'
3
4
  import { describe, expect, expectTypeOf, it, vi } from 'vitest'
4
5
  import { LocationService } from '../services/location-service.js'
@@ -9,8 +10,8 @@ type TestRoute = Pick<NestedRoute<unknown, any, any>, 'component'>
9
10
 
10
11
  describe('nestedReplace', () => {
11
12
  it('Should replace the current history entry with a simple path', async () => {
12
- await usingAsync(new Injector(), async (injector) => {
13
- const locationService = injector.getInstance(LocationService)
13
+ await usingAsync(createInjector(), async (injector) => {
14
+ const locationService = injector.get(LocationService)
14
15
  const navigateSpy = vi.spyOn(locationService, 'navigate')
15
16
  const replaceSpy = vi.spyOn(locationService, 'replace')
16
17
 
@@ -22,8 +23,8 @@ describe('nestedReplace', () => {
22
23
  })
23
24
 
24
25
  it('Should compile params, query and hash for a replace call', async () => {
25
- await usingAsync(new Injector(), async (injector) => {
26
- const locationService = injector.getInstance(LocationService)
26
+ await usingAsync(createInjector(), async (injector) => {
27
+ const locationService = injector.get(LocationService)
27
28
  const replaceSpy = vi.spyOn(locationService, 'replace')
28
29
 
29
30
  nestedReplace(injector, {
@@ -69,7 +70,7 @@ describe('createNestedReplace', () => {
69
70
 
70
71
  const appReplace = createNestedReplace<Routes>()
71
72
  // @ts-expect-error -- '/nonexistent' is not a valid route path
72
- appReplace(new Injector(), { path: '/nonexistent' })
73
+ appReplace(createInjector(), { path: '/nonexistent' })
73
74
  })
74
75
 
75
76
  it('Should enforce a declared required query shape', () => {
@@ -18,7 +18,7 @@ import type { NestedRoute } from './nested-router.js'
18
18
  * @param args - The navigation arguments
19
19
  */
20
20
  export const nestedReplace = (injector: Injector, args: NestedNavigateArgs): void => {
21
- injector.getInstance(LocationService).replace(buildNestedNavigateUrl(args))
21
+ injector.get(LocationService).replace(buildNestedNavigateUrl(args))
22
22
  }
23
23
 
24
24
  /**
@@ -1,4 +1,4 @@
1
- import { Injector } from '@furystack/inject'
1
+ import { createInjector } from '@furystack/inject'
2
2
  import { usingAsync } from '@furystack/utils'
3
3
  import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
4
4
  import { initializeShadeRoot } from '../initialize.js'
@@ -24,7 +24,7 @@ describe('NestedRouteLink', () => {
24
24
  })
25
25
 
26
26
  it('Should render a link with the correct href', async () => {
27
- await usingAsync(new Injector(), async (injector) => {
27
+ await usingAsync(createInjector(), async (injector) => {
28
28
  const rootElement = document.getElementById('root') as HTMLDivElement
29
29
 
30
30
  initializeShadeRoot({
@@ -44,11 +44,11 @@ describe('NestedRouteLink', () => {
44
44
  })
45
45
 
46
46
  it('Should trigger SPA navigation on click', async () => {
47
- await usingAsync(new Injector(), async (injector) => {
47
+ await usingAsync(createInjector(), async (injector) => {
48
48
  const rootElement = document.getElementById('root') as HTMLDivElement
49
49
  const onRouteChange = vi.fn()
50
50
 
51
- injector.getInstance(LocationService).onLocationPathChanged.subscribe(onRouteChange)
51
+ injector.get(LocationService).onLocationPathChanged.subscribe(onRouteChange)
52
52
 
53
53
  initializeShadeRoot({
54
54
  injector,
@@ -68,7 +68,7 @@ describe('NestedRouteLink', () => {
68
68
  })
69
69
 
70
70
  it('Should compile route params in the href', async () => {
71
- await usingAsync(new Injector(), async (injector) => {
71
+ await usingAsync(createInjector(), async (injector) => {
72
72
  const rootElement = document.getElementById('root') as HTMLDivElement
73
73
 
74
74
  initializeShadeRoot({
@@ -88,7 +88,7 @@ describe('NestedRouteLink', () => {
88
88
  })
89
89
 
90
90
  it('Should compile route params with multiple segments', async () => {
91
- await usingAsync(new Injector(), async (injector) => {
91
+ await usingAsync(createInjector(), async (injector) => {
92
92
  const rootElement = document.getElementById('root') as HTMLDivElement
93
93
 
94
94
  initializeShadeRoot({
@@ -108,7 +108,7 @@ describe('NestedRouteLink', () => {
108
108
  })
109
109
 
110
110
  it('Should append a serialized query string to the rendered href', async () => {
111
- await usingAsync(new Injector(), async (injector) => {
111
+ await usingAsync(createInjector(), async (injector) => {
112
112
  const rootElement = document.getElementById('root') as HTMLDivElement
113
113
 
114
114
  initializeShadeRoot({
@@ -128,7 +128,7 @@ describe('NestedRouteLink', () => {
128
128
  })
129
129
 
130
130
  it('Should append the hash segment to the rendered href', async () => {
131
- await usingAsync(new Injector(), async (injector) => {
131
+ await usingAsync(createInjector(), async (injector) => {
132
132
  const rootElement = document.getElementById('root') as HTMLDivElement
133
133
 
134
134
  initializeShadeRoot({
@@ -73,7 +73,7 @@ const _NestedRouteLink = Shade<NestedRouteLinkProps>({
73
73
  ev.preventDefault()
74
74
  // eslint-disable-next-line furystack/prefer-location-service -- This IS the SPA link component; it must call pushState directly.
75
75
  history.pushState('', props.title || '', resolvedUrl)
76
- injector.getInstance(LocationService).updateState()
76
+ injector.get(LocationService).updateState()
77
77
  },
78
78
  })
79
79
  return <>{children}</>
@@ -1,4 +1,4 @@
1
- import { Injector } from '@furystack/inject'
1
+ import { createInjector } from '@furystack/inject'
2
2
  import { serializeValue } from '@furystack/rest'
3
3
  import { sleepAsync, usingAsync } from '@furystack/utils'
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -438,7 +438,7 @@ describe('NestedRouter lifecycle hooks', () => {
438
438
  callOrder.push('leave-other')
439
439
  })
440
440
 
441
- await usingAsync(new Injector(), async (injector) => {
441
+ await usingAsync(createInjector(), async (injector) => {
442
442
  const rootElement = document.getElementById('root') as HTMLDivElement
443
443
 
444
444
  initializeShadeRoot({
@@ -556,7 +556,7 @@ describe('NestedRouter query / hash re-render', () => {
556
556
  const onLeave = vi.fn(async () => {})
557
557
  const componentFn = vi.fn(({ hash }: { hash: string | undefined }) => <div id="content">hash={hash ?? 'none'}</div>)
558
558
 
559
- await usingAsync(new Injector(), async (injector) => {
559
+ await usingAsync(createInjector(), async (injector) => {
560
560
  const rootElement = document.getElementById('root') as HTMLDivElement
561
561
 
562
562
  initializeShadeRoot({
@@ -581,7 +581,7 @@ describe('NestedRouter query / hash re-render', () => {
581
581
  expect(onVisit).toBeCalledTimes(1)
582
582
  expect(onLeave).not.toBeCalled()
583
583
 
584
- const locationService = injector.getInstance(LocationService)
584
+ const locationService = injector.get(LocationService)
585
585
  locationService.navigate('/tabs#a')
586
586
  await flushUpdates()
587
587
  await flushUpdates()
@@ -609,7 +609,7 @@ describe('NestedRouter query / hash re-render', () => {
609
609
  <div id="content">page={query?.page ?? 'none'}</div>
610
610
  ))
611
611
 
612
- await usingAsync(new Injector(), async (injector) => {
612
+ await usingAsync(createInjector(), async (injector) => {
613
613
  const rootElement = document.getElementById('root') as HTMLDivElement
614
614
 
615
615
  initializeShadeRoot({
@@ -633,7 +633,7 @@ describe('NestedRouter query / hash re-render', () => {
633
633
  expect(document.getElementById('content')?.innerHTML).toBe('page=none')
634
634
  expect(onVisit).toBeCalledTimes(1)
635
635
 
636
- const locationService = injector.getInstance(LocationService)
636
+ const locationService = injector.get(LocationService)
637
637
  locationService.navigate(`/list?page=${serializeValue(2)}`)
638
638
  await flushUpdates()
639
639
  await flushUpdates()
@@ -662,7 +662,7 @@ describe('NestedRouter query / hash re-render', () => {
662
662
  </div>
663
663
  ))
664
664
 
665
- await usingAsync(new Injector(), async (injector) => {
665
+ await usingAsync(createInjector(), async (injector) => {
666
666
  const rootElement = document.getElementById('root') as HTMLDivElement
667
667
 
668
668
  initializeShadeRoot({
@@ -686,7 +686,7 @@ describe('NestedRouter query / hash re-render', () => {
686
686
  expect(document.getElementById('content')?.innerHTML).toBe('a')
687
687
  bComponent.mockClear()
688
688
 
689
- injector.getInstance(LocationService).navigate(`/b?page=${serializeValue(7)}#x`)
689
+ injector.get(LocationService).navigate(`/b?page=${serializeValue(7)}#x`)
690
690
  await flushUpdates()
691
691
  await flushUpdates()
692
692
 
@@ -731,7 +731,7 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
731
731
  callOrder.push('leave-c')
732
732
  })
733
733
 
734
- await usingAsync(new Injector(), async (injector) => {
734
+ await usingAsync(createInjector(), async (injector) => {
735
735
  const rootElement = document.getElementById('root') as HTMLDivElement
736
736
 
737
737
  initializeShadeRoot({
@@ -813,7 +813,7 @@ describe('NestedRouter lifecycle element scope', () => {
813
813
  const visitElements: Array<{ route: string; element: JSX.Element }> = []
814
814
  const leaveElements: Array<{ route: string; element: JSX.Element }> = []
815
815
 
816
- await usingAsync(new Injector(), async (injector) => {
816
+ await usingAsync(createInjector(), async (injector) => {
817
817
  const rootElement = document.getElementById('root') as HTMLDivElement
818
818
 
819
819
  initializeShadeRoot({
@@ -906,7 +906,7 @@ describe('NestedRouter flat routes', () => {
906
906
  it('should render and navigate between flat (non-nested) Record routes', async () => {
907
907
  history.pushState(null, '', '/')
908
908
 
909
- await usingAsync(new Injector(), async (injector) => {
909
+ await usingAsync(createInjector(), async (injector) => {
910
910
  const rootElement = document.getElementById('root') as HTMLDivElement
911
911
 
912
912
  initializeShadeRoot({
@@ -967,7 +967,7 @@ describe('NestedRouter outlet composition', () => {
967
967
  it('should compose parent layout wrapping child content via outlet', async () => {
968
968
  history.pushState(null, '', '/dashboard/settings')
969
969
 
970
- await usingAsync(new Injector(), async (injector) => {
970
+ await usingAsync(createInjector(), async (injector) => {
971
971
  const rootElement = document.getElementById('root') as HTMLDivElement
972
972
 
973
973
  initializeShadeRoot({
@@ -1012,7 +1012,7 @@ describe('NestedRouter outlet composition', () => {
1012
1012
  it('should render parent with fallback when navigating to parent URL without a child match', async () => {
1013
1013
  history.pushState(null, '', '/dashboard')
1014
1014
 
1015
- await usingAsync(new Injector(), async (injector) => {
1015
+ await usingAsync(createInjector(), async (injector) => {
1016
1016
  const rootElement = document.getElementById('root') as HTMLDivElement
1017
1017
 
1018
1018
  initializeShadeRoot({
@@ -1065,7 +1065,7 @@ describe('NestedRouter route param changes', () => {
1065
1065
  callOrder.push('leave-user')
1066
1066
  })
1067
1067
 
1068
- await usingAsync(new Injector(), async (injector) => {
1068
+ await usingAsync(createInjector(), async (injector) => {
1069
1069
  const rootElement = document.getElementById('root') as HTMLDivElement
1070
1070
 
1071
1071
  initializeShadeRoot({
@@ -1141,7 +1141,7 @@ describe('NestedRouter route param changes', () => {
1141
1141
  const onVisitDash = vi.fn()
1142
1142
  const onLeaveDash = vi.fn()
1143
1143
 
1144
- await usingAsync(new Injector(), async (injector) => {
1144
+ await usingAsync(createInjector(), async (injector) => {
1145
1145
  const rootElement = document.getElementById('root') as HTMLDivElement
1146
1146
 
1147
1147
  initializeShadeRoot({
@@ -1214,9 +1214,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
1214
1214
  it('should update RouteMatchService with the current match chain on navigation', async () => {
1215
1215
  history.pushState(null, '', '/parent/child-a')
1216
1216
 
1217
- await usingAsync(new Injector(), async (injector) => {
1217
+ await usingAsync(createInjector(), async (injector) => {
1218
1218
  const rootElement = document.getElementById('root') as HTMLDivElement
1219
- const routeMatchService = injector.getInstance(RouteMatchService)
1219
+ const routeMatchService = injector.get(RouteMatchService)
1220
1220
 
1221
1221
  const parentRoute: NestedRoute = {
1222
1222
  meta: { title: 'Parent' },
@@ -1278,9 +1278,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
1278
1278
  it('should expose match params through RouteMatchService', async () => {
1279
1279
  history.pushState(null, '', '/users/42')
1280
1280
 
1281
- await usingAsync(new Injector(), async (injector) => {
1281
+ await usingAsync(createInjector(), async (injector) => {
1282
1282
  const rootElement = document.getElementById('root') as HTMLDivElement
1283
- const routeMatchService = injector.getInstance(RouteMatchService)
1283
+ const routeMatchService = injector.get(RouteMatchService)
1284
1284
 
1285
1285
  initializeShadeRoot({
1286
1286
  injector,
@@ -1378,7 +1378,7 @@ describe('NestedRouter view transitions', () => {
1378
1378
  it('should call startViewTransition when viewTransition is enabled', async () => {
1379
1379
  history.pushState(null, '', '/')
1380
1380
 
1381
- await usingAsync(new Injector(), async (injector) => {
1381
+ await usingAsync(createInjector(), async (injector) => {
1382
1382
  const rootElement = document.getElementById('root') as HTMLDivElement
1383
1383
 
1384
1384
  initializeShadeRoot({
@@ -1414,7 +1414,7 @@ describe('NestedRouter view transitions', () => {
1414
1414
  it('should not call startViewTransition when viewTransition is not set', async () => {
1415
1415
  history.pushState(null, '', '/')
1416
1416
 
1417
- await usingAsync(new Injector(), async (injector) => {
1417
+ await usingAsync(createInjector(), async (injector) => {
1418
1418
  const rootElement = document.getElementById('root') as HTMLDivElement
1419
1419
 
1420
1420
  initializeShadeRoot({
@@ -1449,7 +1449,7 @@ describe('NestedRouter view transitions', () => {
1449
1449
  it('should pass types to startViewTransition when configured', async () => {
1450
1450
  history.pushState(null, '', '/')
1451
1451
 
1452
- await usingAsync(new Injector(), async (injector) => {
1452
+ await usingAsync(createInjector(), async (injector) => {
1453
1453
  const rootElement = document.getElementById('root') as HTMLDivElement
1454
1454
 
1455
1455
  initializeShadeRoot({
@@ -1484,7 +1484,7 @@ describe('NestedRouter view transitions', () => {
1484
1484
  it('should respect per-route viewTransition: false override', async () => {
1485
1485
  history.pushState(null, '', '/')
1486
1486
 
1487
- await usingAsync(new Injector(), async (injector) => {
1487
+ await usingAsync(createInjector(), async (injector) => {
1488
1488
  const rootElement = document.getElementById('root') as HTMLDivElement
1489
1489
 
1490
1490
  initializeShadeRoot({
@@ -1525,7 +1525,7 @@ describe('NestedRouter view transitions', () => {
1525
1525
 
1526
1526
  history.pushState(null, '', '/')
1527
1527
 
1528
- await usingAsync(new Injector(), async (injector) => {
1528
+ await usingAsync(createInjector(), async (injector) => {
1529
1529
  const rootElement = document.getElementById('root') as HTMLDivElement
1530
1530
 
1531
1531
  initializeShadeRoot({
@@ -386,7 +386,7 @@ export const NestedRouter = Shade<NestedRouterProps>({
386
386
  chainElements: [],
387
387
  })
388
388
 
389
- const locationService = injector.getInstance(LocationService)
389
+ const locationService = injector.get(LocationService)
390
390
 
391
391
  const updateUrl = async (currentUrl: string) => {
392
392
  const [lastState] = useState<NestedRouterState>('routerState', state)
@@ -426,7 +426,7 @@ export const NestedRouter = Shade<NestedRouterProps>({
426
426
 
427
427
  const applyUpdate = () => {
428
428
  setState({ matchChain: newChain, jsx: newResult.jsx, chainElements: newResult.chainElements })
429
- injector.getInstance(RouteMatchService).currentMatchChain.setValue(newChain)
429
+ injector.get(RouteMatchService).currentMatchChain.setValue(newChain)
430
430
  }
431
431
 
432
432
  const vtConfig = resolveViewTransition(options.props.viewTransition, newChain)
@@ -451,7 +451,7 @@ export const NestedRouter = Shade<NestedRouterProps>({
451
451
  jsx: options.props.notFound || <div />,
452
452
  chainElements: [],
453
453
  })
454
- injector.getInstance(RouteMatchService).currentMatchChain.setValue([])
454
+ injector.get(RouteMatchService).currentMatchChain.setValue([])
455
455
  }
456
456
 
457
457
  await maybeViewTransition(options.props.viewTransition, applyNotFound)
@@ -1,4 +1,4 @@
1
- import { Injector } from '@furystack/inject'
1
+ import { createInjector } from '@furystack/inject'
2
2
  import { deserializeQueryString, serializeToQueryString, serializeValue } from '@furystack/rest'
3
3
  import { usingAsync } from '@furystack/utils'
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -13,16 +13,17 @@ describe('LocationService', () => {
13
13
  })
14
14
 
15
15
  it('Shuld be constructed', async () => {
16
- await usingAsync(new Injector(), async (i) => {
17
- const s = i.getInstance(LocationService)
18
- expect(s).toBeInstanceOf(LocationService)
16
+ await usingAsync(createInjector(), async (i) => {
17
+ const s = i.get(LocationService)
18
+ expect(s).toBeDefined()
19
+ expect(typeof s.navigate).toBe('function')
19
20
  })
20
21
  })
21
22
 
22
23
  it('Shuld update state on events', async () => {
23
- await usingAsync(new Injector(), async (i) => {
24
+ await usingAsync(createInjector(), async (i) => {
24
25
  const onLocaionChanged = vi.fn()
25
- const s = i.getInstance(LocationService)
26
+ const s = i.get(LocationService)
26
27
  s.onLocationPathChanged.subscribe(onLocaionChanged)
27
28
  expect(onLocaionChanged).toBeCalledTimes(0)
28
29
  history.pushState(null, '', '/loc1')
@@ -39,9 +40,9 @@ describe('LocationService', () => {
39
40
  })
40
41
 
41
42
  it('Should update location path when navigate is called', async () => {
42
- await usingAsync(new Injector(), async (i) => {
43
+ await usingAsync(createInjector(), async (i) => {
43
44
  const onLocationChanged = vi.fn()
44
- const s = i.getInstance(LocationService)
45
+ const s = i.get(LocationService)
45
46
  s.onLocationPathChanged.subscribe(onLocationChanged)
46
47
  s.navigate('/dashboard')
47
48
  expect(s.onLocationPathChanged.getValue()).toBe('/dashboard')
@@ -51,8 +52,8 @@ describe('LocationService', () => {
51
52
 
52
53
  describe('replace', () => {
53
54
  it('Should update the observable path without pushing a new history entry', async () => {
54
- await usingAsync(new Injector(), async (i) => {
55
- const s = i.getInstance(LocationService)
55
+ await usingAsync(createInjector(), async (i) => {
56
+ const s = i.get(LocationService)
56
57
  const lengthBefore = history.length
57
58
  s.replace('/replaced')
58
59
  expect(s.onLocationPathChanged.getValue()).toBe('/replaced')
@@ -61,8 +62,8 @@ describe('LocationService', () => {
61
62
  })
62
63
 
63
64
  it('Should call history.replaceState rather than pushState', async () => {
64
- await usingAsync(new Injector(), async (i) => {
65
- const s = i.getInstance(LocationService)
65
+ await usingAsync(createInjector(), async (i) => {
66
+ const s = i.get(LocationService)
66
67
  const pushSpy = vi.spyOn(history, 'pushState')
67
68
  const replaceSpy = vi.spyOn(history, 'replaceState')
68
69
 
@@ -78,8 +79,8 @@ describe('LocationService', () => {
78
79
  })
79
80
 
80
81
  it('Should notify path subscribers after replace', async () => {
81
- await usingAsync(new Injector(), async (i) => {
82
- const s = i.getInstance(LocationService)
82
+ await usingAsync(createInjector(), async (i) => {
83
+ const s = i.get(LocationService)
83
84
  const onLocationChanged = vi.fn()
84
85
  s.onLocationPathChanged.subscribe(onLocationChanged)
85
86
  s.replace('/notify')
@@ -90,8 +91,8 @@ describe('LocationService', () => {
90
91
 
91
92
  describe('useSearchParam', () => {
92
93
  it('Should create observables lazily', async () => {
93
- await usingAsync(new Injector(), async (i) => {
94
- const service = i.getInstance(LocationService)
94
+ await usingAsync(createInjector(), async (i) => {
95
+ const service = i.get(LocationService)
95
96
  const observables = service.searchParamObservables
96
97
 
97
98
  const testSearchParam = service.useSearchParam('test', null)
@@ -110,16 +111,16 @@ describe('LocationService', () => {
110
111
  })
111
112
 
112
113
  it('Should return the default value, if not present in the query string', async () => {
113
- await usingAsync(new Injector(), async (i) => {
114
- const service = i.getInstance(LocationService)
114
+ await usingAsync(createInjector(), async (i) => {
115
+ const service = i.get(LocationService)
115
116
  const testSearchParam = service.useSearchParam('test', { value: 'foo' })
116
117
  expect(testSearchParam.getValue()).toEqual({ value: 'foo' })
117
118
  })
118
119
  })
119
120
 
120
121
  it('Should return the value from the query string', async () => {
121
- await usingAsync(new Injector(), async (i) => {
122
- const service = i.getInstance(LocationService)
122
+ await usingAsync(createInjector(), async (i) => {
123
+ const service = i.get(LocationService)
123
124
  history.pushState(null, '', `/loc1?test=${serializeValue(1)}`)
124
125
  const testSearchParam = service.useSearchParam('test', 123)
125
126
  expect(testSearchParam.getValue()).toBe(1)
@@ -127,8 +128,8 @@ describe('LocationService', () => {
127
128
  })
128
129
 
129
130
  it('should update the observable value on push / replace states', async () => {
130
- await usingAsync(new Injector(), async (i) => {
131
- const service = i.getInstance(LocationService)
131
+ await usingAsync(createInjector(), async (i) => {
132
+ const service = i.get(LocationService)
132
133
  history.pushState(null, '', `/loc1?test=${serializeValue(1)}`)
133
134
  const testSearchParam = service.useSearchParam('test', 234)
134
135
  expect(testSearchParam.getValue()).toBe(1)
@@ -138,8 +139,8 @@ describe('LocationService', () => {
138
139
  })
139
140
 
140
141
  it('Should update the URL based on search value change', async () => {
141
- await usingAsync(new Injector(), async (i) => {
142
- const service = i.getInstance(LocationService)
142
+ await usingAsync(createInjector(), async (i) => {
143
+ const service = i.get(LocationService)
143
144
  history.pushState(null, '', `/loc1?test=${serializeValue('2')}`)
144
145
  const testSearchParam = service.useSearchParam('test', '')
145
146
  testSearchParam.setValue('2')
@@ -147,25 +148,28 @@ describe('LocationService', () => {
147
148
  })
148
149
  })
149
150
 
150
- it('Should throw an error when trying to use a custom serializer after LocationService has been instantiated', async () => {
151
- await usingAsync(new Injector(), async (i) => {
151
+ it('Should throw when called after LocationService has been resolved', async () => {
152
+ await usingAsync(createInjector(), async (i) => {
152
153
  const customSerializer = vi.fn((value: any) => serializeToQueryString(value))
153
154
  const customDeserializer = vi.fn((value: string) => deserializeQueryString(value))
154
- i.getInstance(LocationService)
155
- expect(() => useCustomSearchStateSerializer(i, customSerializer, customDeserializer)).toThrowError(
156
- 'useCustomSearchStateSerializer must be called before the LocationService is instantiated',
155
+
156
+ // Eagerly resolve once so the service patches history / adds listeners.
157
+ i.get(LocationService)
158
+
159
+ expect(() => useCustomSearchStateSerializer(i, customSerializer, customDeserializer)).toThrow(
160
+ /before LocationService is resolved/,
157
161
  )
158
162
  })
159
163
  })
160
164
 
161
165
  it('Should use custom serializer and deserializer', async () => {
162
- await usingAsync(new Injector(), async (i) => {
166
+ await usingAsync(createInjector(), async (i) => {
163
167
  const customSerializer = vi.fn((value: any) => serializeToQueryString(value))
164
168
  const customDeserializer = vi.fn((value: string) => deserializeQueryString(value))
165
169
 
166
170
  useCustomSearchStateSerializer(i, customSerializer, customDeserializer)
167
171
 
168
- const locationService = i.getInstance(LocationService)
172
+ const locationService = i.get(LocationService)
169
173
  const testSearchParam = locationService.useSearchParam('test', { value: 'foo' })
170
174
 
171
175
  testSearchParam.setValue({ value: 'bar' })