@sanity/sdk-react 2.9.0 → 2.11.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 (72) hide show
  1. package/dist/index.d.ts +338 -215
  2. package/dist/index.js +564 -342
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -14
  5. package/src/_exports/index.ts +2 -0
  6. package/src/_exports/sdk-react.ts +8 -0
  7. package/src/components/SDKProvider.test.tsx +5 -12
  8. package/src/components/SDKProvider.tsx +58 -28
  9. package/src/components/SanityApp.tsx +2 -2
  10. package/src/components/auth/AuthBoundary.tsx +8 -1
  11. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  12. package/src/components/auth/LoginError.test.tsx +191 -5
  13. package/src/components/auth/LoginError.tsx +100 -56
  14. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  15. package/src/components/errors/ChunkLoadError.tsx +56 -0
  16. package/src/components/errors/chunkReloadStorage.ts +57 -0
  17. package/src/config/handles.ts +55 -0
  18. package/src/constants.ts +5 -0
  19. package/src/context/DefaultResourceContext.ts +10 -0
  20. package/src/context/PerspectiveContext.ts +12 -0
  21. package/src/context/ResourceProvider.test.tsx +2 -2
  22. package/src/context/ResourceProvider.tsx +56 -51
  23. package/src/context/ResourcesContext.tsx +7 -0
  24. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  25. package/src/context/SanityInstanceProvider.tsx +71 -0
  26. package/src/hooks/agent/agentActions.ts +55 -38
  27. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  28. package/src/hooks/context/useResource.test.tsx +32 -0
  29. package/src/hooks/context/useResource.ts +24 -0
  30. package/src/hooks/context/useSanityInstance.test.tsx +42 -111
  31. package/src/hooks/context/useSanityInstance.ts +28 -50
  32. package/src/hooks/dashboard/useDispatchIntent.test.ts +11 -7
  33. package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
  34. package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
  35. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  36. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -17
  37. package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +46 -81
  38. package/src/hooks/document/useApplyDocumentActions.ts +33 -67
  39. package/src/hooks/document/useDocument.ts +4 -6
  40. package/src/hooks/document/useDocumentEvent.ts +8 -7
  41. package/src/hooks/document/useDocumentPermissions.test.tsx +60 -152
  42. package/src/hooks/document/useDocumentPermissions.ts +78 -55
  43. package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
  44. package/src/hooks/document/useEditDocument.test.tsx +25 -60
  45. package/src/hooks/document/useEditDocument.ts +3 -3
  46. package/src/hooks/documents/useDocuments.ts +19 -11
  47. package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
  48. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
  49. package/src/hooks/helpers/useNormalizedResourceOptions.ts +169 -0
  50. package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
  51. package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
  52. package/src/hooks/organizations/useOrganization.test.ts +65 -0
  53. package/src/hooks/organizations/useOrganization.ts +40 -0
  54. package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
  55. package/src/hooks/organizations/useOrganizations.test.ts +85 -0
  56. package/src/hooks/organizations/useOrganizations.ts +45 -0
  57. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +29 -14
  58. package/src/hooks/presence/usePresence.test.tsx +56 -9
  59. package/src/hooks/presence/usePresence.ts +16 -4
  60. package/src/hooks/preview/useDocumentPreview.tsx +8 -10
  61. package/src/hooks/projection/useDocumentProjection.ts +7 -9
  62. package/src/hooks/projects/useProject.test-d.ts +49 -0
  63. package/src/hooks/projects/useProject.ts +33 -41
  64. package/src/hooks/projects/useProjects.test-d.ts +49 -0
  65. package/src/hooks/projects/useProjects.ts +17 -23
  66. package/src/hooks/query/useQuery.ts +11 -10
  67. package/src/hooks/releases/useActiveReleases.ts +14 -14
  68. package/src/hooks/releases/usePerspective.ts +11 -16
  69. package/src/hooks/users/useUser.ts +1 -1
  70. package/src/hooks/users/useUsers.ts +1 -1
  71. package/src/context/SourcesContext.tsx +0 -7
  72. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -107
@@ -0,0 +1,100 @@
1
+ import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
2
+ import {act, render, screen} from '@testing-library/react'
3
+ import {use, useEffect} from 'react'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {SanityInstanceContext} from './SanityInstanceContext'
7
+ import {SanityInstanceProvider} from './SanityInstanceProvider'
8
+
9
+ function promiseWithResolvers<T = void>(): {
10
+ promise: Promise<T>
11
+ resolve: (t: T) => void
12
+ reject: (error: unknown) => void
13
+ } {
14
+ let resolve!: (t: T) => void
15
+ let reject!: (error: unknown) => void
16
+ const promise = new Promise<T>((res, rej) => {
17
+ resolve = res
18
+ reject = rej
19
+ })
20
+ return {resolve, reject, promise}
21
+ }
22
+
23
+ describe('SanityInstanceProvider', () => {
24
+ it('renders children', () => {
25
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
26
+
27
+ render(
28
+ <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
29
+ <div data-testid="test-child">Child Component</div>
30
+ </SanityInstanceProvider>,
31
+ )
32
+
33
+ expect(screen.getByTestId('test-child')).toBeInTheDocument()
34
+ instance.dispose()
35
+ })
36
+
37
+ it('provides the given instance via context', async () => {
38
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
39
+ const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
40
+
41
+ const CaptureInstance = () => {
42
+ const ctx = use(SanityInstanceContext)
43
+ useEffect(() => resolve(ctx), [ctx])
44
+ return null
45
+ }
46
+
47
+ render(
48
+ <SanityInstanceProvider instance={instance} fallback={null}>
49
+ <CaptureInstance />
50
+ </SanityInstanceProvider>,
51
+ )
52
+
53
+ const provided = await promise
54
+ expect(provided).toBe(instance)
55
+ instance.dispose()
56
+ })
57
+
58
+ it('shows fallback during suspense', async () => {
59
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
60
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
61
+ const {promise, resolve} = promiseWithResolvers()
62
+
63
+ function SuspendingChild(): React.ReactNode {
64
+ throw promise
65
+ }
66
+
67
+ render(
68
+ <SanityInstanceProvider
69
+ instance={instance}
70
+ fallback={<div data-testid="fallback">Loading...</div>}
71
+ >
72
+ <SuspendingChild />
73
+ </SanityInstanceProvider>,
74
+ )
75
+
76
+ expect(screen.getByTestId('fallback')).toBeInTheDocument()
77
+ act(() => {
78
+ resolve()
79
+ })
80
+ await new Promise((r) => setTimeout(r, 0))
81
+ instance.dispose()
82
+ consoleSpy.mockRestore()
83
+ })
84
+
85
+ it('does not dispose the instance on unmount', async () => {
86
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
87
+
88
+ const {unmount} = render(
89
+ <SanityInstanceProvider instance={instance} fallback={null}>
90
+ <div />
91
+ </SanityInstanceProvider>,
92
+ )
93
+
94
+ unmount()
95
+ await new Promise((r) => setTimeout(r, 0))
96
+
97
+ expect(instance.isDisposed()).toBe(false)
98
+ instance.dispose()
99
+ })
100
+ })
@@ -0,0 +1,71 @@
1
+ import {type SanityInstance} from '@sanity/sdk'
2
+ import {Suspense} from 'react'
3
+
4
+ import {SanityInstanceContext} from './SanityInstanceContext'
5
+
6
+ /**
7
+ * Props for the SanityInstanceProvider component
8
+ * @public
9
+ */
10
+ export interface SanityInstanceProviderProps {
11
+ /**
12
+ * A pre-created SanityInstance to provide to child components.
13
+ * The caller owns the instance lifecycle — SanityInstanceProvider
14
+ * will not dispose it on unmount.
15
+ */
16
+ instance: SanityInstance
17
+ /**
18
+ * React node to show while content is loading.
19
+ * Used as the fallback for the internal Suspense boundary.
20
+ */
21
+ fallback: React.ReactNode
22
+ children: React.ReactNode
23
+ }
24
+
25
+ /**
26
+ * Provides an externally-created Sanity instance to child components through React Context.
27
+ *
28
+ * @internal
29
+ *
30
+ * @remarks
31
+ * Unlike {@link ResourceProvider}, this component does not create or dispose a SanityInstance.
32
+ * The caller is responsible for creating the instance via `createSanityInstance` and disposing
33
+ * it when appropriate. This is useful when a non-React system layer (e.g. a state machine)
34
+ * owns the instance and the React tree should consume it without managing its lifecycle.
35
+ *
36
+ * All SDK hooks (`useSanityInstance`, `useDocuments`, etc.) will read from the provided instance.
37
+ *
38
+ * @example Providing a pre-created instance
39
+ * ```tsx
40
+ * import { createSanityInstance, type SanityConfig } from '@sanity/sdk'
41
+ * import { SanityInstanceProvider } from '@sanity/sdk-react'
42
+ *
43
+ * const config: SanityConfig = {
44
+ * projectId: 'my-project-id',
45
+ * dataset: 'production',
46
+ * }
47
+ *
48
+ * const instance = createSanityInstance(config)
49
+ *
50
+ * function App() {
51
+ * return (
52
+ * <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
53
+ * <MyApp />
54
+ * </SanityInstanceProvider>
55
+ * )
56
+ * }
57
+ * ```
58
+ *
59
+ * @category Components
60
+ */
61
+ export function SanityInstanceProvider({
62
+ instance,
63
+ fallback,
64
+ children,
65
+ }: SanityInstanceProviderProps): React.ReactNode {
66
+ return (
67
+ <SanityInstanceContext.Provider value={instance}>
68
+ <Suspense fallback={fallback}>{children}</Suspense>
69
+ </SanityInstanceContext.Provider>
70
+ )
71
+ }
@@ -11,11 +11,13 @@ import {
11
11
  type AgentTransformOptions,
12
12
  agentTranslate,
13
13
  type AgentTranslateOptions,
14
- type SanityInstance,
15
14
  } from '@sanity/sdk'
15
+ import {useCallback} from 'react'
16
16
  import {firstValueFrom} from 'rxjs'
17
17
 
18
- import {createCallbackHook} from '../helpers/createCallbackHook'
18
+ import {type ResourceHandle} from '../../config/handles'
19
+ import {useSanityInstance} from '../context/useSanityInstance'
20
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
19
21
 
20
22
  interface Subscription {
21
23
  unsubscribe(): void
@@ -103,10 +105,17 @@ interface Subscribable<T> {
103
105
  *
104
106
  * @category Agent Actions
105
107
  */
106
- export const useAgentGenerate: () => (options: AgentGenerateOptions) => Subscribable<unknown> =
107
- createCallbackHook(agentGenerate) as unknown as () => (
108
- options: AgentGenerateOptions,
109
- ) => Subscribable<unknown>
108
+ export function useAgentGenerate(
109
+ resourceHandle?: ResourceHandle,
110
+ ): (options: AgentGenerateOptions) => Subscribable<unknown> {
111
+ const instance = useSanityInstance()
112
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
113
+ return useCallback(
114
+ (options: AgentGenerateOptions) =>
115
+ agentGenerate(instance, options, resource) as unknown as Subscribable<unknown>,
116
+ [instance, resource],
117
+ )
118
+ }
110
119
 
111
120
  /**
112
121
  * @alpha
@@ -179,10 +188,17 @@ export const useAgentGenerate: () => (options: AgentGenerateOptions) => Subscrib
179
188
  *
180
189
  * @category Agent Actions
181
190
  */
182
- export const useAgentTransform: () => (options: AgentTransformOptions) => Subscribable<unknown> =
183
- createCallbackHook(agentTransform) as unknown as () => (
184
- options: AgentTransformOptions,
185
- ) => Subscribable<unknown>
191
+ export function useAgentTransform(
192
+ resourceHandle?: ResourceHandle,
193
+ ): (options: AgentTransformOptions) => Subscribable<unknown> {
194
+ const instance = useSanityInstance()
195
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
196
+ return useCallback(
197
+ (options: AgentTransformOptions) =>
198
+ agentTransform(instance, options, resource) as unknown as Subscribable<unknown>,
199
+ [instance, resource],
200
+ )
201
+ }
186
202
 
187
203
  /**
188
204
  * @alpha
@@ -274,20 +290,16 @@ export const useAgentTransform: () => (options: AgentTransformOptions) => Subscr
274
290
  *
275
291
  * @category Agent Actions
276
292
  */
277
- export const useAgentTranslate: () => (options: AgentTranslateOptions) => Subscribable<unknown> =
278
- createCallbackHook(agentTranslate) as unknown as () => (
279
- options: AgentTranslateOptions,
280
- ) => Subscribable<unknown>
281
-
282
- /**
283
- * @internal
284
- * Adapter to convert the agentPrompt observable to a Promise.
285
- */
286
- function promptAdapter(
287
- instance: SanityInstance,
288
- options: AgentPromptOptions,
289
- ): Promise<AgentPromptResult> {
290
- return firstValueFrom(agentPrompt(instance, options))
293
+ export function useAgentTranslate(
294
+ resourceHandle?: ResourceHandle,
295
+ ): (options: AgentTranslateOptions) => Subscribable<unknown> {
296
+ const instance = useSanityInstance()
297
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
298
+ return useCallback(
299
+ (options: AgentTranslateOptions) =>
300
+ agentTranslate(instance, options, resource) as unknown as Subscribable<unknown>,
301
+ [instance, resource],
302
+ )
291
303
  }
292
304
 
293
305
  /**
@@ -384,18 +396,15 @@ function promptAdapter(
384
396
  *
385
397
  * @category Agent Actions
386
398
  */
387
- export const useAgentPrompt: () => (options: AgentPromptOptions) => Promise<AgentPromptResult> =
388
- createCallbackHook(promptAdapter)
389
-
390
- /**
391
- * @internal
392
- * Adapter to convert the agentPatch observable to a Promise.
393
- */
394
- function patchAdapter(
395
- instance: SanityInstance,
396
- options: AgentPatchOptions,
397
- ): Promise<AgentPatchResult> {
398
- return firstValueFrom(agentPatch(instance, options))
399
+ export function useAgentPrompt(
400
+ resourceHandle?: ResourceHandle,
401
+ ): (options: AgentPromptOptions) => Promise<AgentPromptResult> {
402
+ const instance = useSanityInstance()
403
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
404
+ return useCallback(
405
+ (options: AgentPromptOptions) => firstValueFrom(agentPrompt(instance, options, resource)),
406
+ [instance, resource],
407
+ )
399
408
  }
400
409
 
401
410
  /**
@@ -547,5 +556,13 @@ function patchAdapter(
547
556
  *
548
557
  * @category Agent Actions
549
558
  */
550
- export const useAgentPatch: () => (options: AgentPatchOptions) => Promise<AgentPatchResult> =
551
- createCallbackHook(patchAdapter)
559
+ export function useAgentPatch(
560
+ resourceHandle?: ResourceHandle,
561
+ ): (options: AgentPatchOptions) => Promise<AgentPatchResult> {
562
+ const instance = useSanityInstance()
563
+ const {resource} = useNormalizedResourceOptions(resourceHandle ?? {})
564
+ return useCallback(
565
+ (options: AgentPatchOptions) => firstValueFrom(agentPatch(instance, options, resource)),
566
+ [instance, resource],
567
+ )
568
+ }
@@ -27,13 +27,20 @@ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): s
27
27
  const instance = useSanityInstance()
28
28
  const [error, setError] = useState<string | null>(null)
29
29
 
30
+ const isInactive = disabled || !projectIds || projectIds.length === 0
31
+
32
+ // Reset stale errors when verification turns off so the next activation
33
+ // doesn't briefly leak the previous result.
34
+ const [prevInactive, setPrevInactive] = useState(isInactive)
35
+ if (prevInactive !== isInactive) {
36
+ setPrevInactive(isInactive)
37
+ if (isInactive) setError(null)
38
+ }
39
+
30
40
  useEffect(() => {
31
- if (disabled || !projectIds || projectIds.length === 0) {
32
- if (error !== null) setError(null)
33
- return
34
- }
41
+ if (isInactive) return
35
42
 
36
- const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds)
43
+ const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds!)
37
44
 
38
45
  const subscription = verificationObservable$.subscribe((result: OrgVerificationResult) => {
39
46
  setError(result.error)
@@ -42,7 +49,7 @@ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): s
42
49
  return () => {
43
50
  subscription.unsubscribe()
44
51
  }
45
- }, [instance, disabled, error, projectIds])
52
+ }, [instance, isInactive, projectIds])
46
53
 
47
54
  return error
48
55
  }
@@ -0,0 +1,32 @@
1
+ import {renderHook as reactRenderHook} from '@testing-library/react'
2
+ import {type ReactNode} from 'react'
3
+ import {describe, expect, it} from 'vitest'
4
+
5
+ import {renderHook} from '../../../test/test-utils'
6
+ import {ResourceProvider} from '../../context/ResourceProvider'
7
+ import {useResource} from './useResource'
8
+
9
+ describe('useResource', () => {
10
+ it('returns the resource from the instance config when no explicit resource is set', () => {
11
+ // test-utils wraps with ResourceProvider projectId="test" dataset="test"
12
+ const {result} = renderHook(() => useResource())
13
+ expect(result.current).toEqual({projectId: 'test', dataset: 'test'})
14
+ })
15
+
16
+ it('returns the explicit resource when ResourceProvider has a resource prop', () => {
17
+ const resource = {projectId: 'explicit-project', dataset: 'explicit-dataset'}
18
+ const {result} = reactRenderHook(() => useResource(), {
19
+ wrapper: ({children}: {children: ReactNode}) => (
20
+ <ResourceProvider resource={resource} fallback={null}>
21
+ {children}
22
+ </ResourceProvider>
23
+ ),
24
+ })
25
+ expect(result.current).toEqual(resource)
26
+ })
27
+
28
+ it('returns undefined when no resource or instance config is available', () => {
29
+ const {result} = reactRenderHook(() => useResource())
30
+ expect(result.current).toBeUndefined()
31
+ })
32
+ })
@@ -0,0 +1,24 @@
1
+ import {type DocumentResource} from '@sanity/sdk'
2
+
3
+ import {useEffectiveContextResource} from '../helpers/useNormalizedResourceOptions'
4
+
5
+ /**
6
+ * Returns the currently active `DocumentResource` for the nearest resource context.
7
+ *
8
+ * Resolves in priority order:
9
+ * 1. A `resource` prop on the nearest `<ResourceProvider>`
10
+ * 2. The `projectId`/`dataset` from the current `SanityInstance` config
11
+ * 3. `undefined` when neither is available
12
+ *
13
+ * @public
14
+ * @category Platform
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * const resource = useResource()
19
+ * console.log(resource?.projectId, resource?.dataset)
20
+ * ```
21
+ */
22
+ export function useResource(): DocumentResource | undefined {
23
+ return useEffectiveContextResource()
24
+ }
@@ -1,7 +1,7 @@
1
1
  import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
2
  import {renderHook} from '@testing-library/react'
3
3
  import {type ReactNode} from 'react'
4
- import {describe, expect, it} from 'vitest'
4
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
5
5
 
6
6
  import {SanityInstanceContext} from '../../context/SanityInstanceContext'
7
7
  import {useSanityInstance} from './useSanityInstance'
@@ -37,137 +37,68 @@ describe('useSanityInstance', () => {
37
37
  }).toThrow('SanityInstance context not found')
38
38
  })
39
39
 
40
- it('should include the requested config in error message when no instance found', () => {
41
- const requestedConfig = {projectId: 'test', dataset: 'test'}
42
-
43
- // Expect the hook to throw and include the requested config in the error
44
- expect(() => {
45
- renderHook(() => useSanityInstance(requestedConfig), {
46
- wrapper: createWrapper(null),
47
- })
48
- }).toThrow(JSON.stringify(requestedConfig, null, 2))
49
- })
50
-
51
- it('should find a matching instance with provided config', () => {
52
- // Create a parent instance
53
- const parentInstance = createSanityInstance({
54
- projectId: 'parent-project',
55
- dataset: 'parent-dataset',
56
- })
57
-
58
- // Create a child instance
59
- const childInstance = parentInstance.createChild({dataset: 'child-dataset'})
60
-
61
- // Render the hook with the child instance and request the parent config
62
- const {result} = renderHook(
63
- () => useSanityInstance({projectId: 'parent-project', dataset: 'parent-dataset'}),
64
- {wrapper: createWrapper(childInstance)},
65
- )
66
-
67
- // Should match and return the parent instance
68
- expect(result.current).toBe(parentInstance)
69
- })
70
-
71
- it('should throw an error if no matching instance is found for config', () => {
72
- // Create an instance
73
- const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
74
-
75
- // Request a config that doesn't match
76
- const requestedConfig: SanityConfig = {
77
- projectId: 'non-existent',
78
- dataset: 'not-found',
79
- }
80
-
81
- // Expect the hook to throw for a non-matching config
82
- expect(() => {
83
- renderHook(() => useSanityInstance(requestedConfig), {
84
- wrapper: createWrapper(instance),
85
- })
86
- }).toThrow('Could not find a matching Sanity instance')
87
- })
88
-
89
- it('should include the requested config in error message when no matching instance', () => {
90
- const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
91
- const requestedConfig = {projectId: 'different', dataset: 'different'}
92
-
93
- // Expect the error to include the requested config details
94
- expect(() => {
95
- renderHook(() => useSanityInstance(requestedConfig), {
96
- wrapper: createWrapper(instance),
97
- })
98
- }).toThrow(JSON.stringify(requestedConfig, null, 2))
99
- })
100
-
101
40
  it('should return the current instance when no config is provided', () => {
102
- // Create a hierarchy of instances
103
- const grandparent = createSanityInstance({projectId: 'gp', dataset: 'gp-ds'})
104
- const parent = grandparent.createChild({projectId: 'p'})
105
- const child = parent.createChild({dataset: 'child-ds'})
41
+ // Create a Sanity instance
42
+ const instance = createSanityInstance({projectId: 'gp', dataset: 'gp-ds'})
106
43
 
107
- // Render the hook with the child instance and no config
44
+ // Render the hook with the wrapper that provides the context
108
45
  const {result} = renderHook(() => useSanityInstance(), {
109
- wrapper: createWrapper(child),
46
+ wrapper: createWrapper(instance),
110
47
  })
111
48
 
112
- // Should return the child instance
113
- expect(result.current).toBe(child)
49
+ // Should return the instance
50
+ expect(result.current).toBe(instance)
114
51
  })
115
52
 
116
- it('should match child instance when it satisfies the config', () => {
117
- // Create a parent instance
118
- const parent = createSanityInstance({projectId: 'parent', dataset: 'parent-ds'})
53
+ describe('deprecated config parameter', () => {
54
+ let warnSpy: ReturnType<typeof vi.spyOn>
119
55
 
120
- // Create a child instance that inherits projectId
121
- const child = parent.createChild({dataset: 'child-ds'})
56
+ beforeEach(() => {
57
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
58
+ })
122
59
 
123
- // Render the hook with the child instance and request by the child's dataset
124
- const {result} = renderHook(() => useSanityInstance({dataset: 'child-ds'}), {
125
- wrapper: createWrapper(child),
60
+ afterEach(() => {
61
+ warnSpy.mockRestore()
126
62
  })
127
63
 
128
- // Should match and return the child instance
129
- expect(result.current).toBe(child)
130
- })
64
+ it('should return the current context instance regardless of config', () => {
65
+ const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
66
+ const requestedConfig: SanityConfig = {projectId: 'test-project', dataset: 'test-dataset'}
131
67
 
132
- it('should match partial config correctly', () => {
133
- // Create an instance with multiple config values
134
- const instance = createSanityInstance({
135
- projectId: 'test-proj',
136
- dataset: 'test-ds',
68
+ const {result} = renderHook(() => useSanityInstance(requestedConfig), {
69
+ wrapper: createWrapper(instance),
70
+ })
71
+
72
+ expect(result.current).toBe(instance)
137
73
  })
138
74
 
139
- // Should match when requesting just one property
140
- const {result} = renderHook(() => useSanityInstance({dataset: 'test-ds'}), {
141
- wrapper: createWrapper(instance),
75
+ it('should throw if no instance in context even when config is provided', () => {
76
+ expect(() => {
77
+ renderHook(() => useSanityInstance({projectId: 'test'}), {
78
+ wrapper: createWrapper(null),
79
+ })
80
+ }).toThrow('SanityInstance context not found')
142
81
  })
143
82
 
144
- expect(result.current).toBe(instance)
145
- })
83
+ it('warns once when a config argument is passed', () => {
84
+ const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
85
+ const {rerender} = renderHook(() => useSanityInstance({projectId: 'test-project'}), {
86
+ wrapper: createWrapper(instance),
87
+ })
146
88
 
147
- it("should match deeper in hierarchy when current instance doesn't match", () => {
148
- // Create a three-level hierarchy
149
- const root = createSanityInstance({projectId: 'root', dataset: 'root-ds'})
150
- const middle = root.createChild({projectId: 'middle'})
151
- const leaf = middle.createChild({dataset: 'leaf-ds'})
89
+ expect(warnSpy).toHaveBeenCalledTimes(1)
90
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[useSanityInstance]'))
152
91
 
153
- // Request config matching the root from the leaf
154
- const {result} = renderHook(() => useSanityInstance({projectId: 'root', dataset: 'root-ds'}), {
155
- wrapper: createWrapper(leaf),
92
+ rerender()
93
+ rerender()
94
+ expect(warnSpy).toHaveBeenCalledTimes(1)
156
95
  })
157
96
 
158
- // Should find and return the root instance
159
- expect(result.current).toBe(root)
160
- })
97
+ it('does not warn when no config is passed', () => {
98
+ const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
99
+ renderHook(() => useSanityInstance(), {wrapper: createWrapper(instance)})
161
100
 
162
- it('should match undefined values in config', () => {
163
- // Create instance with only projectId
164
- const rootInstance = createSanityInstance({projectId: 'test'})
165
-
166
- // Match specifically looking for undefined dataset
167
- const {result} = renderHook(() => useSanityInstance({dataset: undefined}), {
168
- wrapper: createWrapper(rootInstance),
101
+ expect(warnSpy).not.toHaveBeenCalled()
169
102
  })
170
-
171
- expect(result.current).toBe(rootInstance)
172
103
  })
173
104
  })