@sanity/sdk-react 0.0.1 → 0.0.3

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 (31) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts +364 -287
  3. package/dist/index.js +125 -149
  4. package/dist/index.js.map +1 -1
  5. package/package.json +5 -5
  6. package/src/_exports/sdk-react.ts +10 -10
  7. package/src/hooks/comlink/useWindowConnection.test.tsx +145 -0
  8. package/src/hooks/comlink/useWindowConnection.ts +32 -30
  9. package/src/hooks/{comlink → dashboard}/useManageFavorite.test.ts +84 -134
  10. package/src/hooks/{comlink → dashboard}/useManageFavorite.ts +4 -39
  11. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +2 -73
  12. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +20 -27
  13. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +69 -0
  14. package/src/hooks/{comlink → dashboard}/useRecordDocumentHistoryEvent.ts +17 -10
  15. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +14 -85
  16. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +33 -8
  17. package/src/hooks/document/useApplyDocumentActions.ts +4 -4
  18. package/src/hooks/document/useDocument.test.ts +8 -10
  19. package/src/hooks/document/useDocument.ts +50 -33
  20. package/src/hooks/document/useDocumentEvent.ts +2 -2
  21. package/src/hooks/document/useDocumentSyncStatus.ts +1 -1
  22. package/src/hooks/document/useEditDocument.ts +15 -15
  23. package/src/hooks/documents/useDocuments.ts +5 -5
  24. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +5 -5
  25. package/src/hooks/preview/{usePreview.test.tsx → useDocumentPreview.test.tsx} +5 -5
  26. package/src/hooks/preview/{usePreview.tsx → useDocumentPreview.tsx} +11 -8
  27. package/src/hooks/projection/{useProjection.test.tsx → useDocumentProjection.test.tsx} +5 -5
  28. package/src/hooks/projection/{useProjection.ts → useDocumentProjection.ts} +22 -17
  29. package/src/hooks/query/useQuery.ts +5 -5
  30. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +0 -85
  31. package/src/hooks/comlink/useWindowConnection.test.ts +0 -135
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -46,12 +46,12 @@
46
46
  "@sanity/message-protocol": "^0.12.0",
47
47
  "@sanity/types": "^3.83.0",
48
48
  "@types/lodash-es": "^4.17.12",
49
- "groq": "3.86.2-experimental.0",
49
+ "groq": "3.88.1-typegen-experimental.0",
50
50
  "lodash-es": "^4.17.21",
51
- "react-compiler-runtime": "19.0.0-beta-ebf51a3-20250411",
51
+ "react-compiler-runtime": "19.1.0-rc.1",
52
52
  "react-error-boundary": "^5.0.0",
53
53
  "rxjs": "^7.8.2",
54
- "@sanity/sdk": "0.0.1"
54
+ "@sanity/sdk": "0.0.3"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sanity/browserslist-config": "^1.0.5",
@@ -75,9 +75,9 @@
75
75
  "vite": "^6.3.4",
76
76
  "vitest": "^3.1.2",
77
77
  "@repo/config-eslint": "0.0.0",
78
+ "@repo/config-test": "0.0.1",
78
79
  "@repo/package.bundle": "3.82.0",
79
80
  "@repo/package.config": "0.0.1",
80
- "@repo/config-test": "0.0.1",
81
81
  "@repo/tsconfig": "0.0.1"
82
82
  },
83
83
  "peerDependencies": {
@@ -20,8 +20,6 @@ export {
20
20
  useFrameConnection,
21
21
  type UseFrameConnectionOptions,
22
22
  } from '../hooks/comlink/useFrameConnection'
23
- export {useManageFavorite} from '../hooks/comlink/useManageFavorite'
24
- export {useRecordDocumentHistoryEvent} from '../hooks/comlink/useRecordDocumentHistoryEvent'
25
23
  export {
26
24
  useWindowConnection,
27
25
  type UseWindowConnectionOptions,
@@ -29,10 +27,12 @@ export {
29
27
  type WindowMessageHandler,
30
28
  } from '../hooks/comlink/useWindowConnection'
31
29
  export {useSanityInstance} from '../hooks/context/useSanityInstance'
30
+ export {useManageFavorite} from '../hooks/dashboard/useManageFavorite'
32
31
  export {
33
32
  type NavigateToStudioResult,
34
33
  useNavigateToStudioDocument,
35
34
  } from '../hooks/dashboard/useNavigateToStudioDocument'
35
+ export {useRecordDocumentHistoryEvent} from '../hooks/dashboard/useRecordDocumentHistoryEvent'
36
36
  export {useStudioWorkspacesByProjectIdDataset} from '../hooks/dashboard/useStudioWorkspacesByProjectIdDataset'
37
37
  export {useDatasets} from '../hooks/datasets/useDatasets'
38
38
  export {useApplyDocumentActions} from '../hooks/document/useApplyDocumentActions'
@@ -52,15 +52,15 @@ export {
52
52
  usePaginatedDocuments,
53
53
  } from '../hooks/paginatedDocuments/usePaginatedDocuments'
54
54
  export {
55
- usePreview,
56
- type UsePreviewOptions,
57
- type UsePreviewResults,
58
- } from '../hooks/preview/usePreview'
55
+ useDocumentPreview,
56
+ type useDocumentPreviewOptions,
57
+ type useDocumentPreviewResults,
58
+ } from '../hooks/preview/useDocumentPreview'
59
59
  export {
60
- useProjection,
61
- type UseProjectionOptions,
62
- type UseProjectionResults,
63
- } from '../hooks/projection/useProjection'
60
+ useDocumentProjection,
61
+ type useDocumentProjectionOptions,
62
+ type useDocumentProjectionResults,
63
+ } from '../hooks/projection/useDocumentProjection'
64
64
  export {useProject} from '../hooks/projects/useProject'
65
65
  export {type ProjectWithoutMembers, useProjects} from '../hooks/projects/useProjects'
66
66
  export {useQuery} from '../hooks/query/useQuery'
@@ -0,0 +1,145 @@
1
+ import {type Message, type Node} from '@sanity/comlink'
2
+ import {getNodeState, type NodeState, type StateSource} from '@sanity/sdk'
3
+ import {screen} from '@testing-library/react'
4
+ import {Suspense} from 'react'
5
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
6
+
7
+ import {render, renderHook} from '../../../test/test-utils'
8
+ import {useWindowConnection} from './useWindowConnection'
9
+
10
+ vi.mock('@sanity/sdk', async () => {
11
+ const actual = await vi.importActual('@sanity/sdk')
12
+ return {
13
+ ...actual,
14
+ getNodeState: vi.fn(),
15
+ }
16
+ })
17
+
18
+ interface TestMessage {
19
+ type: 'TEST_MESSAGE'
20
+ data: {someData: string}
21
+ }
22
+
23
+ interface AnotherMessage {
24
+ type: 'ANOTHER_MESSAGE'
25
+ data: {otherData: number}
26
+ }
27
+
28
+ type TestMessages = TestMessage | AnotherMessage
29
+
30
+ describe('useWindowConnection', () => {
31
+ let node: Node<Message, Message>
32
+ let mockStateSource: StateSource<NodeState>
33
+ let stableNodeEntry: NodeState
34
+
35
+ function createMockNode() {
36
+ return {
37
+ on: vi.fn(() => () => {}),
38
+ post: vi.fn(),
39
+ stop: vi.fn(),
40
+ } as unknown as Node<Message, Message>
41
+ }
42
+
43
+ beforeEach(() => {
44
+ node = createMockNode()
45
+ stableNodeEntry = {node, status: 'connected'}
46
+ mockStateSource = {
47
+ subscribe: vi.fn((callback) => {
48
+ callback?.(stableNodeEntry)
49
+ return () => {}
50
+ }),
51
+ getCurrent: vi.fn(() => stableNodeEntry),
52
+ observable: {subscribe: vi.fn(() => ({unsubscribe: () => {}}))},
53
+ } as unknown as StateSource<NodeState>
54
+ vi.mocked(getNodeState).mockReturnValue(mockStateSource)
55
+ })
56
+
57
+ it('should register message handlers', () => {
58
+ const mockHandler = vi.fn()
59
+ const mockData = {someData: 'test'}
60
+
61
+ renderHook(() =>
62
+ useWindowConnection<TestMessages, TestMessages>({
63
+ name: 'test',
64
+ connectTo: 'window',
65
+ onMessage: {
66
+ TEST_MESSAGE: mockHandler,
67
+ ANOTHER_MESSAGE: vi.fn(),
68
+ },
69
+ }),
70
+ )
71
+
72
+ const onCallback = vi.mocked(node.on).mock.calls[0][1]
73
+ onCallback(mockData)
74
+
75
+ expect(mockHandler).toHaveBeenCalledWith(mockData)
76
+ })
77
+
78
+ it('should send messages through the node', () => {
79
+ const {result} = renderHook(() =>
80
+ useWindowConnection<TestMessages, TestMessages>({
81
+ name: 'test',
82
+ connectTo: 'window',
83
+ }),
84
+ )
85
+
86
+ result.current.sendMessage('TEST_MESSAGE', {someData: 'test'})
87
+ expect(node.post).toHaveBeenCalledWith('TEST_MESSAGE', {someData: 'test'})
88
+
89
+ result.current.sendMessage('ANOTHER_MESSAGE', {otherData: 123})
90
+ expect(node.post).toHaveBeenCalledWith('ANOTHER_MESSAGE', {otherData: 123})
91
+ })
92
+
93
+ it('should suspend and render fallback when node state is undefined', () => {
94
+ const suspenderPromise = Promise.resolve('resolved')
95
+ const mockStateSourceWithUndefined = {
96
+ subscribe: vi.fn(),
97
+ getCurrent: vi.fn(() => undefined),
98
+ observable: {
99
+ pipe: vi.fn(() => ({
100
+ subscribe: vi.fn(() => ({unsubscribe: () => {}})),
101
+ toPromise: () => suspenderPromise,
102
+ })),
103
+ subscribe: vi.fn(() => ({unsubscribe: () => {}})),
104
+ },
105
+ } as unknown as StateSource<NodeState>
106
+
107
+ vi.mocked(getNodeState).mockReturnValue(mockStateSourceWithUndefined)
108
+
109
+ function TestComponent() {
110
+ useWindowConnection<TestMessages, TestMessages>({
111
+ name: 'test',
112
+ connectTo: 'window',
113
+ })
114
+ return <div>Loaded</div>
115
+ }
116
+
117
+ render(
118
+ <Suspense fallback={<div>Loading...</div>}>
119
+ <TestComponent />
120
+ </Suspense>,
121
+ )
122
+
123
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
124
+ })
125
+
126
+ it('should call node.fetch with correct arguments and return its result', async () => {
127
+ const mockFetch = vi.fn().mockResolvedValue('fetch-result')
128
+ node.fetch = mockFetch
129
+
130
+ const {result} = renderHook(() =>
131
+ useWindowConnection<TestMessages, TestMessages>({
132
+ name: 'test',
133
+ connectTo: 'window',
134
+ }),
135
+ )
136
+
137
+ const response = await result.current.fetch('TYPE', {foo: 'bar'}, {responseTimeout: 123})
138
+ expect(mockFetch).toHaveBeenCalledWith('TYPE', {foo: 'bar'}, {responseTimeout: 123})
139
+ expect(response).toBe('fetch-result')
140
+
141
+ const responseNoArgs = await result.current.fetch('TYPE')
142
+ expect(mockFetch).toHaveBeenCalledWith('TYPE', undefined, {})
143
+ expect(responseNoArgs).toBe('fetch-result')
144
+ })
145
+ })
@@ -1,8 +1,17 @@
1
- import {type MessageData, type Node, type Status} from '@sanity/comlink'
2
- import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
1
+ import {type MessageData, type NodeInput} from '@sanity/comlink'
2
+ import {
3
+ type FrameMessage,
4
+ getNodeState,
5
+ type NodeState,
6
+ type SanityInstance,
7
+ type StateSource,
8
+ type WindowMessage,
9
+ } from '@sanity/sdk'
3
10
  import {useCallback, useEffect, useRef} from 'react'
11
+ import {filter, firstValueFrom} from 'rxjs'
4
12
 
5
13
  import {useSanityInstance} from '../context/useSanityInstance'
14
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
6
15
 
7
16
  /**
8
17
  * @internal
@@ -18,7 +27,6 @@ export interface UseWindowConnectionOptions<TMessage extends FrameMessage> {
18
27
  name: string
19
28
  connectTo: string
20
29
  onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>
21
- onStatus?: (status: Status) => void
22
30
  }
23
31
 
24
32
  /**
@@ -40,6 +48,18 @@ export interface WindowConnection<TMessage extends WindowMessage> {
40
48
  ) => Promise<TResponse>
41
49
  }
42
50
 
51
+ const useNodeState = createStateSourceHook({
52
+ getState: getNodeState as (
53
+ instance: SanityInstance,
54
+ nodeInput: NodeInput,
55
+ ) => StateSource<NodeState>,
56
+ shouldSuspend: (instance: SanityInstance, nodeInput: NodeInput) =>
57
+ getNodeState(instance, nodeInput).getCurrent() === undefined,
58
+ suspender: (instance: SanityInstance, nodeInput: NodeInput) => {
59
+ return firstValueFrom(getNodeState(instance, nodeInput).observable.pipe(filter(Boolean)))
60
+ },
61
+ })
62
+
43
63
  /**
44
64
  * @internal
45
65
  * Hook to wrap a Comlink node in a React hook.
@@ -56,47 +76,32 @@ export function useWindowConnection<
56
76
  name,
57
77
  connectTo,
58
78
  onMessage,
59
- onStatus,
60
79
  }: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
61
- const nodeRef = useRef<Node<TWindowMessage, TFrameMessage> | null>(null)
80
+ const {node} = useNodeState({name, connectTo})
62
81
  const messageUnsubscribers = useRef<(() => void)[]>([])
63
82
  const instance = useSanityInstance()
64
83
 
65
84
  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
85
  if (onMessage) {
77
86
  Object.entries(onMessage).forEach(([type, handler]) => {
78
87
  const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
79
- messageUnsubscribers.current.push(messageUnsubscribe)
88
+ if (messageUnsubscribe) {
89
+ messageUnsubscribers.current.push(messageUnsubscribe)
90
+ }
80
91
  })
81
92
  }
82
93
 
83
94
  return () => {
84
- statusUnsubscribe()
85
95
  messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())
86
96
  messageUnsubscribers.current = []
87
- releaseNode(instance, name)
88
- nodeRef.current = null
89
97
  }
90
- }, [instance, name, connectTo, onMessage, onStatus])
98
+ }, [instance, name, onMessage, node])
91
99
 
92
100
  const sendMessage = useCallback(
93
101
  (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)
102
+ node.post(type, data)
98
103
  },
99
- [],
104
+ [node],
100
105
  )
101
106
 
102
107
  const fetch = useCallback(
@@ -109,12 +114,9 @@ export function useWindowConnection<
109
114
  suppressWarnings?: boolean
110
115
  },
111
116
  ): 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>
117
+ return node.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>
116
118
  },
117
- [],
119
+ [node],
118
120
  )
119
121
  return {
120
122
  sendMessage,
@@ -1,8 +1,7 @@
1
- import {type Message, type Node, type Status} from '@sanity/comlink'
1
+ import {type Message} from '@sanity/comlink'
2
2
  import {
3
3
  type FavoriteStatusResponse,
4
4
  getFavoritesState,
5
- getOrCreateNode,
6
5
  resolveFavoritesState,
7
6
  type SanityInstance,
8
7
  } from '@sanity/sdk'
@@ -10,6 +9,7 @@ import {BehaviorSubject} from 'rxjs'
10
9
  import {beforeEach, describe, expect, it, vi} from 'vitest'
11
10
 
12
11
  import {act, renderHook} from '../../../test/test-utils'
12
+ import {useWindowConnection, type WindowConnection} from '../comlink/useWindowConnection'
13
13
  import {useSanityInstance} from '../context/useSanityInstance'
14
14
  import {useManageFavorite} from './useManageFavorite'
15
15
 
@@ -17,8 +17,6 @@ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
17
17
  const actual = await importOriginal()
18
18
  return {
19
19
  ...actual,
20
- getOrCreateNode: vi.fn(),
21
- releaseNode: vi.fn(),
22
20
  getFavoritesState: vi.fn(),
23
21
  resolveFavoritesState: vi.fn(),
24
22
  }
@@ -26,10 +24,14 @@ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
26
24
 
27
25
  vi.mock('../context/useSanityInstance')
28
26
 
27
+ vi.mock('../comlink/useWindowConnection', () => ({
28
+ useWindowConnection: vi.fn(),
29
+ }))
30
+
29
31
  describe('useManageFavorite', () => {
30
- let node: Node<Message, Message>
31
- let statusCallback: ((status: Status) => void) | null = null
32
32
  let favoriteStatusSubject: BehaviorSubject<FavoriteStatusResponse>
33
+ let mockFetch: ReturnType<typeof vi.fn>
34
+ let mockSendMessage: ReturnType<typeof vi.fn>
33
35
 
34
36
  const mockDocumentHandle = {
35
37
  documentId: 'mock-id',
@@ -37,23 +39,8 @@ describe('useManageFavorite', () => {
37
39
  resourceType: 'studio' as const,
38
40
  }
39
41
 
40
- function createMockNode() {
41
- return {
42
- on: vi.fn(() => () => {}),
43
- fetch: vi.fn().mockImplementation(() => Promise.resolve({success: true})),
44
- stop: vi.fn(),
45
- onStatus: vi.fn((callback) => {
46
- statusCallback = callback
47
- return () => {}
48
- }),
49
- } as unknown as Node<Message, Message>
50
- }
51
-
52
42
  beforeEach(() => {
53
- statusCallback = null
54
43
  favoriteStatusSubject = new BehaviorSubject<FavoriteStatusResponse>({isFavorited: false})
55
- node = createMockNode()
56
- vi.mocked(getOrCreateNode).mockReturnValue(node)
57
44
 
58
45
  // Mock getFavoritesState
59
46
  vi.mocked(getFavoritesState).mockImplementation(() => ({
@@ -82,6 +69,17 @@ describe('useManageFavorite', () => {
82
69
  dataset: 'test',
83
70
  },
84
71
  } as unknown as SanityInstance)
72
+
73
+ // Mock useWindowConnection
74
+ mockFetch = vi.fn().mockResolvedValue({success: true})
75
+ mockSendMessage = vi.fn()
76
+ vi.mocked(useWindowConnection).mockImplementation(() => {
77
+ return {
78
+ fetch: (type: string, data?: unknown, options: unknown = {}) =>
79
+ mockFetch(type, data, options),
80
+ sendMessage: mockSendMessage,
81
+ }
82
+ })
85
83
  })
86
84
 
87
85
  afterEach(() => {
@@ -93,7 +91,6 @@ describe('useManageFavorite', () => {
93
91
  const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
94
92
 
95
93
  expect(result.current.isFavorited).toBe(false)
96
- expect(result.current.isConnected).toBe(false)
97
94
  })
98
95
 
99
96
  it('should handle favorite action and update state', async () => {
@@ -101,16 +98,11 @@ describe('useManageFavorite', () => {
101
98
 
102
99
  expect(result.current.isFavorited).toBe(false)
103
100
 
104
- // Simulate connection first
105
- act(() => {
106
- statusCallback?.('connected')
107
- })
108
-
109
101
  await act(async () => {
110
102
  await result.current.favorite()
111
103
  })
112
104
 
113
- expect(node.fetch).toHaveBeenCalledWith(
105
+ expect(mockFetch).toHaveBeenCalledWith(
114
106
  'dashboard/v1/events/favorite/mutate',
115
107
  {
116
108
  document: {
@@ -140,16 +132,11 @@ describe('useManageFavorite', () => {
140
132
 
141
133
  expect(result.current.isFavorited).toBe(true)
142
134
 
143
- // Simulate connection first
144
- act(() => {
145
- statusCallback?.('connected')
146
- })
147
-
148
135
  await act(async () => {
149
136
  await result.current.unfavorite()
150
137
  })
151
138
 
152
- expect(node.fetch).toHaveBeenCalledWith(
139
+ expect(mockFetch).toHaveBeenCalledWith(
153
140
  'dashboard/v1/events/favorite/mutate',
154
141
  {
155
142
  document: {
@@ -169,7 +156,7 @@ describe('useManageFavorite', () => {
169
156
  })
170
157
 
171
158
  it('should not update state if favorite action fails', async () => {
172
- vi.mocked(node.fetch).mockImplementationOnce(() => Promise.resolve({success: false}))
159
+ mockFetch.mockResolvedValueOnce({success: false})
173
160
 
174
161
  const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
175
162
 
@@ -186,16 +173,12 @@ describe('useManageFavorite', () => {
186
173
  it('should throw error during favorite/unfavorite actions', async () => {
187
174
  const errorMessage = 'Failed to update favorite status'
188
175
 
189
- vi.mocked(node.fetch).mockImplementation(() => {
176
+ mockFetch.mockImplementation(() => {
190
177
  throw new Error(errorMessage)
191
178
  })
192
179
 
193
180
  const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
194
181
 
195
- await act(async () => {
196
- statusCallback?.('connected')
197
- })
198
-
199
182
  await act(async () => {
200
183
  await expect(result.current.favorite()).rejects.toThrow(errorMessage)
201
184
  })
@@ -210,18 +193,6 @@ describe('useManageFavorite', () => {
210
193
  expect(resolveFavoritesState).not.toHaveBeenCalled()
211
194
  })
212
195
 
213
- it('should update connection status', () => {
214
- const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
215
-
216
- expect(result.current.isConnected).toBe(false)
217
-
218
- act(() => {
219
- statusCallback?.('connected')
220
- })
221
-
222
- expect(result.current.isConnected).toBe(true)
223
- })
224
-
225
196
  it('should throw error when studio resource is missing projectId or dataset', () => {
226
197
  // Mock the Sanity instance to not have projectId or dataset
227
198
  vi.mocked(useSanityInstance).mockReturnValue({
@@ -255,114 +226,93 @@ describe('useManageFavorite', () => {
255
226
  )
256
227
  })
257
228
 
258
- it('should handle favorites service timeout gracefully', async () => {
259
- // Mock both state functions for timeout scenario
260
- vi.mocked(getFavoritesState).mockImplementationOnce(() => ({
261
- subscribe: () => () => {},
262
- getCurrent: () => undefined, // This will trigger the resolveFavoritesState call
263
- observable: favoriteStatusSubject.asObservable(),
264
- }))
265
-
266
- vi.mocked(resolveFavoritesState).mockImplementationOnce(() => {
267
- throw new Error('Favorites service connection timeout')
268
- })
269
-
270
- const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
271
-
272
- // Should return fallback state instead of suspending
273
- expect(result.current).toEqual({
274
- favorite: expect.any(Function),
275
- unfavorite: expect.any(Function),
276
- isFavorited: false,
277
- isConnected: false,
278
- })
229
+ it('should include schemaName in payload when provided', async () => {
230
+ const mockDocumentHandleWithSchema = {
231
+ ...mockDocumentHandle,
232
+ schemaName: 'testSchema',
233
+ }
234
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandleWithSchema))
279
235
 
280
- // Favorite and unfavorite actions should be a no-op
281
236
  await act(async () => {
282
237
  await result.current.favorite()
283
238
  })
284
239
 
285
- expect(node.fetch).not.toHaveBeenCalled()
286
-
287
- await act(async () => {
288
- await result.current.unfavorite()
289
- })
290
-
291
- expect(node.fetch).not.toHaveBeenCalled()
240
+ expect(mockFetch).toHaveBeenCalledWith(
241
+ 'dashboard/v1/events/favorite/mutate',
242
+ {
243
+ document: {
244
+ id: 'mock-id',
245
+ type: 'mock-type',
246
+ resource: {
247
+ id: 'test.test',
248
+ type: 'studio',
249
+ schemaName: 'testSchema',
250
+ },
251
+ },
252
+ eventType: 'added',
253
+ },
254
+ {},
255
+ )
292
256
  })
293
257
 
294
- it('should still throw non-timeout errors for suspension', async () => {
258
+ it('should default isFavorited to false if state is undefined', () => {
259
+ // Mock getFavoritesState to return undefined for getCurrent
295
260
  vi.mocked(getFavoritesState).mockImplementation(() => ({
296
- subscribe: () => () => {},
297
- getCurrent: () => undefined, // This will trigger the resolveFavoritesState call
261
+ subscribe: (callback?: () => void) => {
262
+ if (!callback) return () => {}
263
+ callback()
264
+ return () => {}
265
+ },
266
+ getCurrent: () => undefined,
298
267
  observable: favoriteStatusSubject.asObservable(),
299
268
  }))
300
-
301
- // Mock resolveFavoritesState to throw
302
- const error = new Error('Some other error')
303
- vi.mocked(resolveFavoritesState).mockImplementation(() => {
304
- throw error
305
- })
306
-
307
- expect(() => {
308
- renderHook(() => useManageFavorite(mockDocumentHandle))
309
- }).toThrow(error)
269
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
270
+ expect(result.current.isFavorited).toBe(false)
310
271
  })
311
272
 
312
- it('should not call fetch if connection is not established', async () => {
273
+ it('should do nothing if fetch is missing', async () => {
274
+ vi.mocked(useWindowConnection).mockReturnValue({
275
+ fetch: undefined,
276
+ sendMessage: mockSendMessage,
277
+ } as unknown as WindowConnection<Message>)
313
278
  const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
314
-
315
- // Ensure connection is not established
316
- expect(result.current.isConnected).toBe(false)
317
-
318
- // Try to favorite
319
279
  await act(async () => {
320
280
  await result.current.favorite()
281
+ await result.current.unfavorite()
321
282
  })
283
+ expect(mockFetch).not.toHaveBeenCalled()
284
+ })
322
285
 
323
- // Fetch should not have been called due to the new status check
324
- expect(node.fetch).not.toHaveBeenCalled()
325
-
326
- // Try to unfavorite
286
+ it('should do nothing if documentId is missing', async () => {
287
+ const handle = {...mockDocumentHandle, documentId: undefined}
288
+ // @ts-expect-error -- no access to ManageFavorite props type
289
+ const {result} = renderHook(() => useManageFavorite(handle))
327
290
  await act(async () => {
291
+ await result.current.favorite()
328
292
  await result.current.unfavorite()
329
293
  })
330
-
331
- // Fetch should still not have been called
332
- expect(node.fetch).not.toHaveBeenCalled()
294
+ expect(mockFetch).not.toHaveBeenCalled()
333
295
  })
334
296
 
335
- it('should include schemaName in payload when provided', async () => {
336
- const mockDocumentHandleWithSchema = {
337
- ...mockDocumentHandle,
338
- schemaName: 'testSchema',
339
- }
340
- const {result} = renderHook(() => useManageFavorite(mockDocumentHandleWithSchema))
341
-
342
- // Simulate connection first
343
- act(() => {
344
- statusCallback?.('connected')
297
+ it('should do nothing if documentType is missing', async () => {
298
+ const handle = {...mockDocumentHandle, documentType: undefined}
299
+ // @ts-expect-error -- no access to ManageFavorite props type
300
+ const {result} = renderHook(() => useManageFavorite(handle))
301
+ await act(async () => {
302
+ await result.current.favorite()
303
+ await result.current.unfavorite()
345
304
  })
305
+ expect(mockFetch).not.toHaveBeenCalled()
306
+ })
346
307
 
308
+ it('should do nothing if resourceType is missing', async () => {
309
+ const handle = {...mockDocumentHandle, resourceType: undefined, resourceId: 'studio'}
310
+ // @ts-expect-error -- no access to ManageFavorite props type
311
+ const {result} = renderHook(() => useManageFavorite(handle))
347
312
  await act(async () => {
348
313
  await result.current.favorite()
314
+ await result.current.unfavorite()
349
315
  })
350
-
351
- expect(node.fetch).toHaveBeenCalledWith(
352
- 'dashboard/v1/events/favorite/mutate',
353
- {
354
- document: {
355
- id: 'mock-id',
356
- type: 'mock-type',
357
- resource: {
358
- id: 'test.test',
359
- type: 'studio',
360
- schemaName: 'testSchema', // <-- Expect schemaName here
361
- },
362
- },
363
- eventType: 'added',
364
- },
365
- {},
366
- )
316
+ expect(mockFetch).not.toHaveBeenCalled()
367
317
  })
368
318
  })