@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.31

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 (131) hide show
  1. package/README.md +6 -100
  2. package/dist/index.d.ts +2390 -2
  3. package/dist/index.js +1119 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +35 -49
  6. package/src/_exports/index.ts +2 -10
  7. package/src/_exports/sdk-react.ts +73 -0
  8. package/src/components/SDKProvider.test.tsx +103 -0
  9. package/src/components/SDKProvider.tsx +52 -0
  10. package/src/components/SanityApp.test.tsx +244 -0
  11. package/src/components/SanityApp.tsx +106 -0
  12. package/src/components/auth/AuthBoundary.test.tsx +204 -29
  13. package/src/components/auth/AuthBoundary.tsx +96 -19
  14. package/src/components/auth/ConfigurationError.ts +22 -0
  15. package/src/components/auth/LoginCallback.test.tsx +22 -24
  16. package/src/components/auth/LoginCallback.tsx +6 -16
  17. package/src/components/auth/LoginError.test.tsx +11 -18
  18. package/src/components/auth/LoginError.tsx +43 -25
  19. package/src/components/utils.ts +22 -0
  20. package/src/context/ResourceProvider.test.tsx +157 -0
  21. package/src/context/ResourceProvider.tsx +111 -0
  22. package/src/context/SanityInstanceContext.ts +4 -0
  23. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  24. package/src/hooks/auth/useAuthState.tsx +4 -5
  25. package/src/hooks/auth/useAuthToken.tsx +1 -1
  26. package/src/hooks/auth/useCurrentUser.tsx +28 -4
  27. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  28. package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
  29. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  30. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
  31. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  32. package/src/hooks/auth/useLogOut.tsx +1 -1
  33. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  34. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  35. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  36. package/src/hooks/client/useClient.ts +13 -33
  37. package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
  38. package/src/hooks/comlink/useFrameConnection.ts +107 -0
  39. package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
  40. package/src/hooks/comlink/useManageFavorite.ts +210 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
  42. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
  43. package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
  44. package/src/hooks/comlink/useWindowConnection.ts +123 -0
  45. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  46. package/src/hooks/context/useSanityInstance.ts +68 -11
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
  48. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
  50. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
  51. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  52. package/src/hooks/datasets/useDatasets.ts +52 -0
  53. package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
  54. package/src/hooks/document/useApplyDocumentActions.ts +124 -0
  55. package/src/hooks/document/useDocument.test.ts +118 -0
  56. package/src/hooks/document/useDocument.ts +212 -0
  57. package/src/hooks/document/useDocumentEvent.test.ts +62 -0
  58. package/src/hooks/document/useDocumentEvent.ts +94 -0
  59. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  60. package/src/hooks/document/useDocumentPermissions.ts +131 -0
  61. package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
  62. package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
  63. package/src/hooks/document/useEditDocument.test.ts +196 -0
  64. package/src/hooks/document/useEditDocument.ts +314 -0
  65. package/src/hooks/documents/useDocuments.test.tsx +179 -0
  66. package/src/hooks/documents/useDocuments.ts +300 -0
  67. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  68. package/src/hooks/helpers/createCallbackHook.tsx +1 -1
  69. package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
  70. package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
  71. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
  72. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
  73. package/src/hooks/preview/usePreview.test.tsx +85 -17
  74. package/src/hooks/preview/usePreview.tsx +81 -22
  75. package/src/hooks/projection/useProjection.test.tsx +283 -0
  76. package/src/hooks/projection/useProjection.ts +232 -0
  77. package/src/hooks/projects/useProject.test.ts +80 -0
  78. package/src/hooks/projects/useProject.ts +51 -0
  79. package/src/hooks/projects/useProjects.test.ts +77 -0
  80. package/src/hooks/projects/useProjects.ts +45 -0
  81. package/src/hooks/query/useQuery.test.tsx +188 -0
  82. package/src/hooks/query/useQuery.ts +193 -0
  83. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  84. package/src/hooks/releases/useActiveReleases.ts +39 -0
  85. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  86. package/src/hooks/releases/usePerspective.ts +49 -0
  87. package/src/hooks/users/useUsers.test.tsx +330 -0
  88. package/src/hooks/users/useUsers.ts +120 -0
  89. package/src/utils/getEnv.ts +21 -0
  90. package/src/version.ts +8 -0
  91. package/src/vite-env.d.ts +10 -0
  92. package/dist/_chunks-es/useLogOut.js +0 -44
  93. package/dist/_chunks-es/useLogOut.js.map +0 -1
  94. package/dist/assets/bundle-CcAyERuZ.css +0 -11
  95. package/dist/components.d.ts +0 -259
  96. package/dist/components.js +0 -301
  97. package/dist/components.js.map +0 -1
  98. package/dist/hooks.d.ts +0 -186
  99. package/dist/hooks.js +0 -81
  100. package/dist/hooks.js.map +0 -1
  101. package/src/_exports/components.ts +0 -13
  102. package/src/_exports/hooks.ts +0 -9
  103. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
  104. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  105. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
  106. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
  107. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  108. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
  109. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  110. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
  111. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  112. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
  113. package/src/components/Login/LoginLinks.test.tsx +0 -100
  114. package/src/components/Login/LoginLinks.tsx +0 -73
  115. package/src/components/auth/Login.test.tsx +0 -41
  116. package/src/components/auth/Login.tsx +0 -45
  117. package/src/components/auth/LoginFooter.test.tsx +0 -29
  118. package/src/components/auth/LoginFooter.tsx +0 -65
  119. package/src/components/auth/LoginLayout.test.tsx +0 -33
  120. package/src/components/auth/LoginLayout.tsx +0 -81
  121. package/src/components/context/SanityProvider.test.tsx +0 -25
  122. package/src/components/context/SanityProvider.tsx +0 -42
  123. package/src/css/css.config.js +0 -220
  124. package/src/css/paramour.css +0 -2347
  125. package/src/css/styles.css +0 -11
  126. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  127. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  128. package/src/hooks/auth/useLoginUrls.tsx +0 -51
  129. package/src/hooks/client/useClient.test.tsx +0 -130
  130. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  131. package/src/hooks/documentCollection/useDocuments.ts +0 -87
@@ -0,0 +1,85 @@
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
+ import {getOrCreateNode} from '@sanity/sdk'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {act, renderHook} from '../../../test/test-utils'
6
+ import {useRecordDocumentHistoryEvent} from './useRecordDocumentHistoryEvent'
7
+
8
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
9
+ const actual = await importOriginal()
10
+ return {
11
+ ...actual,
12
+ getOrCreateNode: vi.fn(),
13
+ releaseNode: vi.fn(),
14
+ }
15
+ })
16
+
17
+ describe('useRecordDocumentHistoryEvent', () => {
18
+ let node: Node<Message, Message>
19
+ let statusCallback: ((status: Status) => void) | null = null
20
+
21
+ const mockDocumentHandle = {
22
+ documentId: 'mock-id',
23
+ documentType: 'mock-type',
24
+ resourceType: 'studio' as const,
25
+ resourceId: 'mock-resource-id',
26
+ }
27
+
28
+ function createMockNode() {
29
+ return {
30
+ on: vi.fn(() => () => {}),
31
+ post: vi.fn(),
32
+ stop: vi.fn(),
33
+ onStatus: vi.fn((callback) => {
34
+ statusCallback = callback
35
+ return () => {}
36
+ }),
37
+ } as unknown as Node<Message, Message>
38
+ }
39
+
40
+ beforeEach(() => {
41
+ statusCallback = null
42
+ node = createMockNode()
43
+ vi.mocked(getOrCreateNode).mockReturnValue(node)
44
+ })
45
+
46
+ it('should initialize with correct connection status', () => {
47
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
48
+
49
+ expect(result.current.isConnected).toBe(false)
50
+
51
+ act(() => {
52
+ statusCallback?.('connected')
53
+ })
54
+
55
+ expect(result.current.isConnected).toBe(true)
56
+ })
57
+
58
+ it('should send correct message when recording events', () => {
59
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
60
+
61
+ result.current.recordEvent('viewed')
62
+
63
+ expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/history', {
64
+ eventType: 'viewed',
65
+ document: {
66
+ id: 'mock-id',
67
+ type: 'mock-type',
68
+ resource: {
69
+ id: 'mock-resource-id',
70
+ type: 'studio',
71
+ },
72
+ },
73
+ })
74
+ })
75
+
76
+ it('should handle errors when sending messages', () => {
77
+ vi.mocked(node.post).mockImplementation(() => {
78
+ throw new Error('Failed to send message')
79
+ })
80
+
81
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
82
+
83
+ expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
84
+ })
85
+ })
@@ -0,0 +1,115 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {
3
+ type CanvasResource,
4
+ type Events,
5
+ type MediaResource,
6
+ SDK_CHANNEL_NAME,
7
+ SDK_NODE_NAME,
8
+ type StudioResource,
9
+ } from '@sanity/message-protocol'
10
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
11
+ import {useCallback, useState} from 'react'
12
+
13
+ import {useWindowConnection} from './useWindowConnection'
14
+
15
+ interface DocumentInteractionHistory {
16
+ recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
17
+ isConnected: boolean
18
+ }
19
+
20
+ /**
21
+ * @internal
22
+ */
23
+ interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
24
+ resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
25
+ resourceId?: string
26
+ /**
27
+ * The name of the schema collection this document belongs to.
28
+ * Typically is the name of the workspace when used in the context of a studio.
29
+ */
30
+ schemaName?: string
31
+ }
32
+
33
+ /**
34
+ * @internal
35
+ * Hook for managing document interaction history in Sanity Studio.
36
+ * This hook provides functionality to record document interactions.
37
+ * @category History
38
+ * @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
39
+ * @returns An object containing:
40
+ * - `recordEvent` - Function to record document interactions
41
+ * - `isConnected` - Boolean indicating if connection to Studio is established
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * function MyDocumentAction(props: DocumentActionProps) {
46
+ * const {documentId, documentType, resourceType, resourceId} = props
47
+ * const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
48
+ * documentId,
49
+ * documentType,
50
+ * resourceType,
51
+ * resourceId,
52
+ * })
53
+ *
54
+ * return (
55
+ * <Button
56
+ * disabled={!isConnected}
57
+ * onClick={() => recordEvent('viewed')}
58
+ * text={'Viewed'}
59
+ * />
60
+ * )
61
+ * }
62
+ * ```
63
+ */
64
+ export function useRecordDocumentHistoryEvent({
65
+ documentId,
66
+ documentType,
67
+ resourceType,
68
+ resourceId,
69
+ schemaName,
70
+ }: UseRecordDocumentHistoryEventProps): DocumentInteractionHistory {
71
+ const [status, setStatus] = useState<Status>('idle')
72
+ const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
73
+ name: SDK_NODE_NAME,
74
+ connectTo: SDK_CHANNEL_NAME,
75
+ onStatus: setStatus,
76
+ })
77
+
78
+ if (resourceType !== 'studio' && !resourceId) {
79
+ throw new Error('resourceId is required for media-library and canvas resources')
80
+ }
81
+
82
+ const recordEvent = useCallback(
83
+ (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
84
+ try {
85
+ const message: Events.HistoryMessage = {
86
+ type: 'dashboard/v1/events/history',
87
+ data: {
88
+ eventType,
89
+ document: {
90
+ id: documentId,
91
+ type: documentType,
92
+ resource: {
93
+ id: resourceId!,
94
+ type: resourceType,
95
+ schemaName,
96
+ },
97
+ },
98
+ },
99
+ }
100
+
101
+ sendMessage(message.type, message.data)
102
+ } catch (error) {
103
+ // eslint-disable-next-line no-console
104
+ console.error('Failed to record history event:', error)
105
+ throw error
106
+ }
107
+ },
108
+ [documentId, documentType, resourceId, resourceType, sendMessage, schemaName],
109
+ )
110
+
111
+ return {
112
+ recordEvent,
113
+ isConnected: status === 'connected',
114
+ }
115
+ }
@@ -0,0 +1,135 @@
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
+ import {getOrCreateNode, releaseNode} from '@sanity/sdk'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {act, renderHook} from '../../../test/test-utils'
6
+ import {useWindowConnection} from './useWindowConnection'
7
+
8
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
9
+ const actual = await importOriginal()
10
+ return {
11
+ ...actual,
12
+ getOrCreateNode: vi.fn(),
13
+ createNode: vi.fn(),
14
+ releaseNode: vi.fn(),
15
+ }
16
+ })
17
+ interface TestMessage {
18
+ type: 'TEST_MESSAGE'
19
+ data: {someData: string}
20
+ }
21
+
22
+ interface AnotherMessage {
23
+ type: 'ANOTHER_MESSAGE'
24
+ data: {otherData: number}
25
+ }
26
+
27
+ type TestMessages = TestMessage | AnotherMessage
28
+
29
+ describe('useWindowConnection', () => {
30
+ let node: Node<Message, Message>
31
+ let statusCallback: ((status: Status) => void) | null = null
32
+
33
+ function createMockNode() {
34
+ return {
35
+ // return unsubscribe function
36
+ on: vi.fn(() => () => {}),
37
+ post: vi.fn(),
38
+ stop: vi.fn(),
39
+ onStatus: vi.fn((callback) => {
40
+ statusCallback = callback
41
+ return () => {}
42
+ }),
43
+ } as unknown as Node<Message, Message>
44
+ }
45
+
46
+ beforeEach(() => {
47
+ statusCallback = null
48
+ node = createMockNode()
49
+ vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
50
+ })
51
+
52
+ it('should call onStatus callback when status changes', () => {
53
+ const onStatusMock = vi.fn()
54
+ renderHook(() =>
55
+ useWindowConnection<TestMessages, TestMessages>({
56
+ name: 'test',
57
+ connectTo: 'window',
58
+ onStatus: onStatusMock,
59
+ }),
60
+ )
61
+
62
+ act(() => {
63
+ statusCallback?.('connected')
64
+ })
65
+ expect(onStatusMock).toHaveBeenCalledWith('connected')
66
+
67
+ act(() => {
68
+ statusCallback?.('disconnected')
69
+ })
70
+ expect(onStatusMock).toHaveBeenCalledWith('disconnected')
71
+ })
72
+
73
+ it('should not throw if onStatus is not provided', () => {
74
+ renderHook(() =>
75
+ useWindowConnection<TestMessages, TestMessages>({
76
+ name: 'test',
77
+ connectTo: 'window',
78
+ }),
79
+ )
80
+
81
+ expect(() => {
82
+ act(() => {
83
+ statusCallback?.('connected')
84
+ })
85
+ }).not.toThrow()
86
+ })
87
+
88
+ it('should register message handlers', () => {
89
+ const mockHandler = vi.fn()
90
+ const mockData = {someData: 'test'}
91
+
92
+ renderHook(() =>
93
+ useWindowConnection<TestMessages, TestMessages>({
94
+ name: 'test',
95
+ connectTo: 'window',
96
+ onMessage: {
97
+ TEST_MESSAGE: mockHandler,
98
+ ANOTHER_MESSAGE: vi.fn(),
99
+ },
100
+ }),
101
+ )
102
+
103
+ const onCallback = vi.mocked(node.on).mock.calls[0][1]
104
+ onCallback(mockData)
105
+
106
+ expect(mockHandler).toHaveBeenCalledWith(mockData)
107
+ })
108
+
109
+ it('should send messages through the node', () => {
110
+ const {result} = renderHook(() =>
111
+ useWindowConnection<TestMessages, TestMessages>({
112
+ name: 'test',
113
+ connectTo: 'window',
114
+ }),
115
+ )
116
+
117
+ result.current.sendMessage('TEST_MESSAGE', {someData: 'test'})
118
+ expect(node.post).toHaveBeenCalledWith('TEST_MESSAGE', {someData: 'test'})
119
+
120
+ result.current.sendMessage('ANOTHER_MESSAGE', {otherData: 123})
121
+ expect(node.post).toHaveBeenCalledWith('ANOTHER_MESSAGE', {otherData: 123})
122
+ })
123
+
124
+ it('should cleanup on unmount', () => {
125
+ const {unmount} = renderHook(() =>
126
+ useWindowConnection<TestMessages, TestMessages>({
127
+ name: 'test',
128
+ connectTo: 'window',
129
+ }),
130
+ )
131
+
132
+ unmount()
133
+ expect(releaseNode).toHaveBeenCalled()
134
+ })
135
+ })
@@ -0,0 +1,123 @@
1
+ import {type MessageData, type Node, type Status} from '@sanity/comlink'
2
+ import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
3
+ import {useCallback, useEffect, useRef} from 'react'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+
7
+ /**
8
+ * @internal
9
+ */
10
+ export type WindowMessageHandler<TFrameMessage extends FrameMessage> = (
11
+ event: TFrameMessage['data'],
12
+ ) => TFrameMessage['response']
13
+
14
+ /**
15
+ * @internal
16
+ */
17
+ export interface UseWindowConnectionOptions<TMessage extends FrameMessage> {
18
+ name: string
19
+ connectTo: string
20
+ onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>
21
+ onStatus?: (status: Status) => void
22
+ }
23
+
24
+ /**
25
+ * @internal
26
+ */
27
+ export interface WindowConnection<TMessage extends WindowMessage> {
28
+ sendMessage: <TType extends TMessage['type']>(
29
+ type: TType,
30
+ data?: Extract<TMessage, {type: TType}>['data'],
31
+ ) => void
32
+ fetch: <TResponse>(
33
+ type: string,
34
+ data?: MessageData,
35
+ options?: {
36
+ signal?: AbortSignal
37
+ suppressWarnings?: boolean
38
+ responseTimeout?: number
39
+ },
40
+ ) => Promise<TResponse>
41
+ }
42
+
43
+ /**
44
+ * @internal
45
+ * Hook to wrap a Comlink node in a React hook.
46
+ * Our store functionality takes care of the lifecycle of the node,
47
+ * as well as sharing a single node between invocations if they share the same name.
48
+ *
49
+ * Generally not to be used directly, but to be used as a dependency of
50
+ * Comlink-powered hooks like `useManageFavorite`.
51
+ */
52
+ export function useWindowConnection<
53
+ TWindowMessage extends WindowMessage,
54
+ TFrameMessage extends FrameMessage,
55
+ >({
56
+ name,
57
+ connectTo,
58
+ onMessage,
59
+ onStatus,
60
+ }: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
61
+ const nodeRef = useRef<Node<TWindowMessage, TFrameMessage> | null>(null)
62
+ const messageUnsubscribers = useRef<(() => void)[]>([])
63
+ const instance = useSanityInstance()
64
+
65
+ useEffect(() => {
66
+ const node = getOrCreateNode(instance, {
67
+ name,
68
+ connectTo,
69
+ }) as unknown as Node<TWindowMessage, TFrameMessage>
70
+ nodeRef.current = node
71
+
72
+ const statusUnsubscribe = node.onStatus((eventStatus) => {
73
+ onStatus?.(eventStatus)
74
+ })
75
+
76
+ if (onMessage) {
77
+ Object.entries(onMessage).forEach(([type, handler]) => {
78
+ const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
79
+ messageUnsubscribers.current.push(messageUnsubscribe)
80
+ })
81
+ }
82
+
83
+ return () => {
84
+ statusUnsubscribe()
85
+ messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())
86
+ messageUnsubscribers.current = []
87
+ releaseNode(instance, name)
88
+ nodeRef.current = null
89
+ }
90
+ }, [instance, name, connectTo, onMessage, onStatus])
91
+
92
+ const sendMessage = useCallback(
93
+ (type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {
94
+ if (!nodeRef.current) {
95
+ throw new Error('Cannot send message before connection is established')
96
+ }
97
+ nodeRef.current.post(type, data)
98
+ },
99
+ [],
100
+ )
101
+
102
+ const fetch = useCallback(
103
+ <TResponse>(
104
+ type: string,
105
+ data?: MessageData,
106
+ fetchOptions?: {
107
+ responseTimeout?: number
108
+ signal?: AbortSignal
109
+ suppressWarnings?: boolean
110
+ },
111
+ ): Promise<TResponse> => {
112
+ if (!nodeRef.current) {
113
+ throw new Error('Cannot fetch before connection is established')
114
+ }
115
+ return nodeRef.current?.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>
116
+ },
117
+ [],
118
+ )
119
+ return {
120
+ sendMessage,
121
+ fetch,
122
+ }
123
+ }
@@ -1,31 +1,173 @@
1
- import {createSanityInstance} from '@sanity/sdk'
1
+ import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
2
  import {renderHook} from '@testing-library/react'
3
- import React from 'react'
4
- import {describe, expect, it, vi} from 'vitest'
3
+ import {type ReactNode} from 'react'
4
+ import {describe, expect, it} from 'vitest'
5
5
 
6
- import {SanityProvider} from '../../components/context/SanityProvider'
6
+ import {SanityInstanceContext} from '../../context/SanityInstanceContext'
7
7
  import {useSanityInstance} from './useSanityInstance'
8
8
 
9
9
  describe('useSanityInstance', () => {
10
- const sanityInstance = createSanityInstance({projectId: 'test-project', dataset: 'production'})
10
+ function createWrapper(instance: SanityInstance | null) {
11
+ return function Wrapper({children}: {children: ReactNode}) {
12
+ return (
13
+ <SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
14
+ )
15
+ }
16
+ }
11
17
 
12
- it('returns sanity instance when used within provider', () => {
13
- const wrapper = ({children}: {children: React.ReactNode}) => (
14
- <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
18
+ it('should return the Sanity instance from context', () => {
19
+ // Create a Sanity instance
20
+ const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
21
+
22
+ // Render the hook with the wrapper that provides the context
23
+ const {result} = renderHook(() => useSanityInstance(), {
24
+ wrapper: createWrapper(instance),
25
+ })
26
+
27
+ // Check that the correct instance is returned
28
+ expect(result.current).toBe(instance)
29
+ })
30
+
31
+ it('should throw an error if no instance is found in context', () => {
32
+ // Expect the hook to throw when no instance is in context
33
+ expect(() => {
34
+ renderHook(() => useSanityInstance(), {
35
+ wrapper: createWrapper(null),
36
+ })
37
+ }).toThrow('SanityInstance context not found')
38
+ })
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)},
15
65
  )
16
66
 
17
- const {result} = renderHook(() => useSanityInstance(), {wrapper})
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'})
18
74
 
19
- expect(result.current).toBe(sanityInstance)
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')
20
87
  })
21
88
 
22
- it('throws error when used outside provider', () => {
23
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
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'}
24
92
 
93
+ // Expect the error to include the requested config details
25
94
  expect(() => {
26
- renderHook(() => useSanityInstance())
27
- }).toThrow('useSanityInstance must be called from within the SanityProvider')
95
+ renderHook(() => useSanityInstance(requestedConfig), {
96
+ wrapper: createWrapper(instance),
97
+ })
98
+ }).toThrow(JSON.stringify(requestedConfig, null, 2))
99
+ })
100
+
101
+ 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'})
106
+
107
+ // Render the hook with the child instance and no config
108
+ const {result} = renderHook(() => useSanityInstance(), {
109
+ wrapper: createWrapper(child),
110
+ })
111
+
112
+ // Should return the child instance
113
+ expect(result.current).toBe(child)
114
+ })
115
+
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'})
119
+
120
+ // Create a child instance that inherits projectId
121
+ const child = parent.createChild({dataset: 'child-ds'})
122
+
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),
126
+ })
127
+
128
+ // Should match and return the child instance
129
+ expect(result.current).toBe(child)
130
+ })
131
+
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',
137
+ })
138
+
139
+ // Should match when requesting just one property
140
+ const {result} = renderHook(() => useSanityInstance({dataset: 'test-ds'}), {
141
+ wrapper: createWrapper(instance),
142
+ })
143
+
144
+ expect(result.current).toBe(instance)
145
+ })
146
+
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'})
152
+
153
+ // Request config matching the root from the leaf
154
+ const {result} = renderHook(() => useSanityInstance({projectId: 'root', dataset: 'root-ds'}), {
155
+ wrapper: createWrapper(leaf),
156
+ })
157
+
158
+ // Should find and return the root instance
159
+ expect(result.current).toBe(root)
160
+ })
161
+
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),
169
+ })
28
170
 
29
- consoleSpy.mockRestore()
171
+ expect(result.current).toBe(rootInstance)
30
172
  })
31
173
  })