@sanity/sdk-react 2.3.1 → 2.5.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.
@@ -0,0 +1,245 @@
1
+ import {renderHook} from '@testing-library/react'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {AppProviders} from '../../../test/test-utils'
5
+ import {useWindowConnection} from '../comlink/useWindowConnection'
6
+ import {useAgentResourceContext} from './useAgentResourceContext'
7
+
8
+ vi.mock('../comlink/useWindowConnection', () => ({
9
+ useWindowConnection: vi.fn(),
10
+ }))
11
+
12
+ describe('useAgentResourceContext', () => {
13
+ let mockSendMessage = vi.fn()
14
+
15
+ beforeEach(() => {
16
+ mockSendMessage = vi.fn()
17
+ vi.mocked(useWindowConnection).mockImplementation(() => {
18
+ return {
19
+ sendMessage: mockSendMessage,
20
+ fetch: vi.fn(),
21
+ }
22
+ })
23
+ })
24
+
25
+ it('should send context update on mount', () => {
26
+ renderHook(
27
+ () =>
28
+ useAgentResourceContext({
29
+ projectId: 'test-project',
30
+ dataset: 'production',
31
+ documentId: 'doc-123',
32
+ }),
33
+ {wrapper: AppProviders},
34
+ )
35
+
36
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
37
+ projectId: 'test-project',
38
+ dataset: 'production',
39
+ documentId: 'doc-123',
40
+ })
41
+ })
42
+
43
+ it('should send context update without documentId', () => {
44
+ renderHook(
45
+ () =>
46
+ useAgentResourceContext({
47
+ projectId: 'test-project',
48
+ dataset: 'production',
49
+ }),
50
+ {wrapper: AppProviders},
51
+ )
52
+
53
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
54
+ projectId: 'test-project',
55
+ dataset: 'production',
56
+ documentId: undefined,
57
+ })
58
+ })
59
+
60
+ it('should send context update when context changes', () => {
61
+ const {rerender} = renderHook(
62
+ ({documentId}: {documentId: string}) =>
63
+ useAgentResourceContext({
64
+ projectId: 'test-project',
65
+ dataset: 'production',
66
+ documentId,
67
+ }),
68
+ {
69
+ wrapper: AppProviders,
70
+ initialProps: {documentId: 'doc-123'},
71
+ },
72
+ )
73
+
74
+ expect(mockSendMessage).toHaveBeenCalledTimes(1)
75
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
76
+ projectId: 'test-project',
77
+ dataset: 'production',
78
+ documentId: 'doc-123',
79
+ })
80
+
81
+ // Change documentId
82
+ rerender({documentId: 'doc-456'})
83
+
84
+ expect(mockSendMessage).toHaveBeenCalledTimes(2)
85
+ expect(mockSendMessage).toHaveBeenLastCalledWith('dashboard/v1/events/agent/resource/update', {
86
+ projectId: 'test-project',
87
+ dataset: 'production',
88
+ documentId: 'doc-456',
89
+ })
90
+ })
91
+
92
+ it('should not send duplicate updates for the same context', () => {
93
+ const {rerender} = renderHook(
94
+ ({documentId}: {documentId: string}) =>
95
+ useAgentResourceContext({
96
+ projectId: 'test-project',
97
+ dataset: 'production',
98
+ documentId,
99
+ }),
100
+ {
101
+ wrapper: AppProviders,
102
+ initialProps: {documentId: 'doc-123'},
103
+ },
104
+ )
105
+
106
+ expect(mockSendMessage).toHaveBeenCalledTimes(1)
107
+
108
+ // Re-render with the same documentId
109
+ rerender({documentId: 'doc-123'})
110
+
111
+ // Should still only be called once
112
+ expect(mockSendMessage).toHaveBeenCalledTimes(1)
113
+ })
114
+
115
+ it('should warn when projectId is missing', () => {
116
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
117
+
118
+ renderHook(
119
+ () =>
120
+ useAgentResourceContext({
121
+ projectId: '',
122
+ dataset: 'production',
123
+ documentId: 'doc-123',
124
+ }),
125
+ {wrapper: AppProviders},
126
+ )
127
+
128
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
129
+ '[useAgentResourceContext] projectId and dataset are required',
130
+ {projectId: '', dataset: 'production'},
131
+ )
132
+ expect(mockSendMessage).not.toHaveBeenCalled()
133
+
134
+ consoleWarnSpy.mockRestore()
135
+ })
136
+
137
+ it('should warn when dataset is missing', () => {
138
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
139
+
140
+ renderHook(
141
+ () =>
142
+ useAgentResourceContext({
143
+ projectId: 'test-project',
144
+ dataset: '',
145
+ documentId: 'doc-123',
146
+ }),
147
+ {wrapper: AppProviders},
148
+ )
149
+
150
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
151
+ '[useAgentResourceContext] projectId and dataset are required',
152
+ {projectId: 'test-project', dataset: ''},
153
+ )
154
+ expect(mockSendMessage).not.toHaveBeenCalled()
155
+
156
+ consoleWarnSpy.mockRestore()
157
+ })
158
+
159
+ it('should handle errors when sending messages', () => {
160
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
161
+ mockSendMessage.mockImplementation(() => {
162
+ throw new Error('Failed to send message')
163
+ })
164
+
165
+ // Should not throw, but should log error
166
+ expect(() =>
167
+ renderHook(
168
+ () =>
169
+ useAgentResourceContext({
170
+ projectId: 'test-project',
171
+ dataset: 'production',
172
+ documentId: 'doc-123',
173
+ }),
174
+ {wrapper: AppProviders},
175
+ ),
176
+ ).not.toThrow()
177
+
178
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
179
+ '[useAgentResourceContext] Failed to update context:',
180
+ expect.any(Error),
181
+ )
182
+
183
+ consoleErrorSpy.mockRestore()
184
+ })
185
+
186
+ it('should update context when switching between documents', () => {
187
+ const {rerender} = renderHook(
188
+ ({documentId}: {documentId: string}) =>
189
+ useAgentResourceContext({
190
+ projectId: 'test-project',
191
+ dataset: 'production',
192
+ documentId,
193
+ }),
194
+ {
195
+ wrapper: AppProviders,
196
+ initialProps: {documentId: 'doc-123'},
197
+ },
198
+ )
199
+
200
+ expect(mockSendMessage).toHaveBeenCalledTimes(1)
201
+
202
+ // Switch to document 456
203
+ rerender({documentId: 'doc-456'})
204
+ expect(mockSendMessage).toHaveBeenCalledTimes(2)
205
+
206
+ // Switch to document 789
207
+ rerender({documentId: 'doc-789'})
208
+ expect(mockSendMessage).toHaveBeenCalledTimes(3)
209
+
210
+ // Switch back to document 123
211
+ rerender({documentId: 'doc-123'})
212
+ expect(mockSendMessage).toHaveBeenCalledTimes(4)
213
+ })
214
+
215
+ it('should update context when document is cleared', () => {
216
+ const {rerender} = renderHook(
217
+ ({documentId}: {documentId: string | undefined}) =>
218
+ useAgentResourceContext({
219
+ projectId: 'test-project',
220
+ dataset: 'production',
221
+ documentId,
222
+ }),
223
+ {
224
+ wrapper: AppProviders,
225
+ initialProps: {documentId: 'doc-123' as string | undefined},
226
+ },
227
+ )
228
+
229
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
230
+ projectId: 'test-project',
231
+ dataset: 'production',
232
+ documentId: 'doc-123',
233
+ })
234
+
235
+ // Clear documentId
236
+ rerender({documentId: undefined})
237
+
238
+ expect(mockSendMessage).toHaveBeenCalledTimes(2)
239
+ expect(mockSendMessage).toHaveBeenLastCalledWith('dashboard/v1/events/agent/resource/update', {
240
+ projectId: 'test-project',
241
+ dataset: 'production',
242
+ documentId: undefined,
243
+ })
244
+ })
245
+ })
@@ -0,0 +1,106 @@
1
+ import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
+ import {type FrameMessage} from '@sanity/sdk'
3
+ import {useCallback, useEffect, useRef} from 'react'
4
+
5
+ import {useWindowConnection} from '../comlink/useWindowConnection'
6
+
7
+ /**
8
+ * @public
9
+ */
10
+ export interface AgentResourceContextOptions {
11
+ /**
12
+ * The project ID of the current context
13
+ */
14
+ projectId: string
15
+ /**
16
+ * The dataset of the current context
17
+ */
18
+ dataset: string
19
+ /**
20
+ * Optional document ID if the user is viewing/editing a specific document
21
+ */
22
+ documentId?: string
23
+ }
24
+
25
+ /**
26
+ * @public
27
+ * Hook for emitting agent resource context updates to the Dashboard.
28
+ * This allows the Agent to understand what resource the user is currently
29
+ * interacting with (e.g., which document they're editing).
30
+ *
31
+ * The hook will automatically emit the context when it changes, and also
32
+ * emit the initial context when the hook is first mounted.
33
+ *
34
+ * @category Agent
35
+ * @param options - The resource context options containing projectId, dataset, and optional documentId
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * import {useAgentResourceContext} from '@sanity/sdk-react'
40
+ *
41
+ * function MyComponent() {
42
+ * const documentId = 'my-document-id'
43
+ *
44
+ * // Automatically updates the Agent's context whenever the document changes
45
+ * useAgentResourceContext({
46
+ * projectId: 'my-project',
47
+ * dataset: 'production',
48
+ * documentId,
49
+ * })
50
+ *
51
+ * return <div>Editing document: {documentId}</div>
52
+ * }
53
+ * ```
54
+ */
55
+ export function useAgentResourceContext(options: AgentResourceContextOptions): void {
56
+ const {projectId, dataset, documentId} = options
57
+ const {sendMessage} = useWindowConnection<Events.AgentResourceUpdateMessage, FrameMessage>({
58
+ name: SDK_NODE_NAME,
59
+ connectTo: SDK_CHANNEL_NAME,
60
+ })
61
+
62
+ // Track the last sent context to avoid duplicate updates
63
+ const lastContextRef = useRef<string | null>(null)
64
+
65
+ const updateContext = useCallback(() => {
66
+ // Validate required fields
67
+ if (!projectId || !dataset) {
68
+ // eslint-disable-next-line no-console
69
+ console.warn('[useAgentResourceContext] projectId and dataset are required', {
70
+ projectId,
71
+ dataset,
72
+ })
73
+ return
74
+ }
75
+
76
+ // Create a stable key for the current context
77
+ const contextKey = `${projectId}:${dataset}:${documentId || ''}`
78
+
79
+ // Skip if context hasn't changed
80
+ if (lastContextRef.current === contextKey) {
81
+ return
82
+ }
83
+
84
+ try {
85
+ const message: Events.AgentResourceUpdateMessage = {
86
+ type: 'dashboard/v1/events/agent/resource/update',
87
+ data: {
88
+ projectId,
89
+ dataset,
90
+ documentId,
91
+ },
92
+ }
93
+
94
+ sendMessage(message.type, message.data)
95
+ lastContextRef.current = contextKey
96
+ } catch (error) {
97
+ // eslint-disable-next-line no-console
98
+ console.error('[useAgentResourceContext] Failed to update context:', error)
99
+ }
100
+ }, [projectId, dataset, documentId, sendMessage])
101
+
102
+ // Update context whenever it changes
103
+ useEffect(() => {
104
+ updateContext()
105
+ }, [updateContext])
106
+ }
@@ -0,0 +1,42 @@
1
+ import {renderHook} from '@testing-library/react'
2
+ import {describe, expect, it, vi} from 'vitest'
3
+
4
+ import {ResourceProvider} from '../../context/ResourceProvider'
5
+ import {useClient} from './useClient'
6
+
7
+ describe('useClient', () => {
8
+ const wrapper = ({children}: {children: React.ReactNode}) => (
9
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
10
+ {children}
11
+ </ResourceProvider>
12
+ )
13
+
14
+ it('should throw a helpful error when called without options', () => {
15
+ // Suppress console.error for this test since we expect an error to be thrown
16
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
17
+
18
+ expect(() => {
19
+ // @ts-expect-error Testing missing options
20
+ renderHook(() => useClient(), {wrapper})
21
+ }).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
22
+
23
+ consoleErrorSpy.mockRestore()
24
+ })
25
+
26
+ it('should throw a helpful error when called with undefined', () => {
27
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
28
+
29
+ expect(() => {
30
+ // @ts-expect-error Testing undefined options
31
+ renderHook(() => useClient(undefined), {wrapper})
32
+ }).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
33
+
34
+ consoleErrorSpy.mockRestore()
35
+ })
36
+
37
+ it('should return a client when called with valid options', () => {
38
+ const {result} = renderHook(() => useClient({apiVersion: '2024-11-12'}), {wrapper})
39
+ expect(result.current).toBeDefined()
40
+ expect(result.current.fetch).toBeDefined()
41
+ })
42
+ })
@@ -1,5 +1,4 @@
1
- import {getClientState} from '@sanity/sdk'
2
- import {identity} from 'rxjs'
1
+ import {type ClientOptions, getClientState, type SanityInstance} from '@sanity/sdk'
3
2
 
4
3
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
4
 
@@ -31,6 +30,14 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
31
30
  * @function
32
31
  */
33
32
  export const useClient = createStateSourceHook({
34
- getState: getClientState,
35
- getConfig: identity,
33
+ getState: (instance: SanityInstance, options: ClientOptions) => {
34
+ if (!options || typeof options !== 'object') {
35
+ throw new Error(
36
+ 'useClient() requires a configuration object with at least an "apiVersion" property. ' +
37
+ 'Example: useClient({ apiVersion: "2024-11-12" })',
38
+ )
39
+ }
40
+ return getClientState(instance, options)
41
+ },
42
+ getConfig: (options: ClientOptions) => options,
36
43
  })
@@ -0,0 +1,12 @@
1
+ import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
2
+ /**
3
+ * Document handle that optionally includes a source (e.g., media library source)
4
+ * or projectId and dataset for traditional dataset sources
5
+ * (but now marked optional since it's valid to just use a source)
6
+ * @beta
7
+ */
8
+ export interface DocumentHandleWithSource extends Omit<DocumentHandle, 'projectId' | 'dataset'> {
9
+ source?: DocumentSource
10
+ projectId?: string
11
+ dataset?: string
12
+ }
@@ -0,0 +1,239 @@
1
+ import {canvasSource, type DocumentHandle, mediaLibrarySource} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {type DocumentHandleWithSource} from './types'
6
+ import {useDispatchIntent} from './useDispatchIntent'
7
+
8
+ // Mock the useWindowConnection hook
9
+ const mockSendMessage = vi.fn()
10
+ vi.mock('../comlink/useWindowConnection', () => ({
11
+ useWindowConnection: vi.fn(() => ({
12
+ sendMessage: mockSendMessage,
13
+ })),
14
+ }))
15
+
16
+ describe('useDispatchIntent', () => {
17
+ const mockDocumentHandle: DocumentHandle = {
18
+ documentId: 'test-document-id',
19
+ documentType: 'test-document-type',
20
+ projectId: 'test-project-id',
21
+ dataset: 'test-dataset',
22
+ }
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks()
26
+ // Reset mock implementation to default behavior
27
+ mockSendMessage.mockImplementation(() => {})
28
+ })
29
+
30
+ it('should return dispatchIntent function', () => {
31
+ const {result} = renderHook(() =>
32
+ useDispatchIntent({action: 'edit', documentHandle: mockDocumentHandle}),
33
+ )
34
+
35
+ expect(result.current).toEqual({
36
+ dispatchIntent: expect.any(Function),
37
+ })
38
+ })
39
+
40
+ it('should throw error when neither action nor intentId is provided', () => {
41
+ const {result} = renderHook(() => useDispatchIntent({documentHandle: mockDocumentHandle}))
42
+
43
+ expect(() => result.current.dispatchIntent()).toThrow(
44
+ 'useDispatchIntent: Either `action` or `intentId` must be provided.',
45
+ )
46
+ })
47
+
48
+ it('should handle errors gracefully', () => {
49
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
50
+ mockSendMessage.mockImplementation(() => {
51
+ throw new Error('Test error')
52
+ })
53
+
54
+ const {result} = renderHook(() =>
55
+ useDispatchIntent({action: 'edit', documentHandle: mockDocumentHandle}),
56
+ )
57
+
58
+ expect(() => result.current.dispatchIntent()).toThrow('Test error')
59
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to dispatch intent:', expect.any(Error))
60
+
61
+ consoleErrorSpy.mockRestore()
62
+ })
63
+
64
+ it('should use memoized dispatchIntent function', () => {
65
+ const {result, rerender} = renderHook(({params}) => useDispatchIntent(params), {
66
+ initialProps: {params: {action: 'edit' as const, documentHandle: mockDocumentHandle}},
67
+ })
68
+
69
+ const firstDispatchIntent = result.current.dispatchIntent
70
+
71
+ // Rerender with the same params
72
+ rerender({params: {action: 'edit' as const, documentHandle: mockDocumentHandle}})
73
+
74
+ expect(result.current.dispatchIntent).toBe(firstDispatchIntent)
75
+ })
76
+
77
+ it('should create new dispatchIntent function when documentHandle changes', () => {
78
+ const {result, rerender} = renderHook(({params}) => useDispatchIntent(params), {
79
+ initialProps: {params: {action: 'edit' as const, documentHandle: mockDocumentHandle}},
80
+ })
81
+
82
+ const firstDispatchIntent = result.current.dispatchIntent
83
+
84
+ const newDocumentHandle: DocumentHandle = {
85
+ documentId: 'new-document-id',
86
+ documentType: 'new-document-type',
87
+ projectId: 'new-project-id',
88
+ dataset: 'new-dataset',
89
+ }
90
+
91
+ rerender({params: {action: 'edit' as const, documentHandle: newDocumentHandle}})
92
+
93
+ expect(result.current.dispatchIntent).not.toBe(firstDispatchIntent)
94
+ })
95
+
96
+ it('should warn if both action and intentId are provided', () => {
97
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
98
+ const {result} = renderHook(() =>
99
+ useDispatchIntent({
100
+ action: 'edit' as const,
101
+ intentId: 'custom-intent' as never, // test runtime error when both are provided
102
+ documentHandle: mockDocumentHandle,
103
+ }),
104
+ )
105
+ result.current.dispatchIntent()
106
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
107
+ 'useDispatchIntent: Both `action` and `intentId` were provided. Using `intentId` and ignoring `action`.',
108
+ )
109
+ consoleWarnSpy.mockRestore()
110
+ })
111
+
112
+ it('should send intent message with action and params when both are provided', () => {
113
+ const intentParams = {view: 'editor', tab: 'content'}
114
+ const {result} = renderHook(() =>
115
+ useDispatchIntent({
116
+ action: 'edit',
117
+ documentHandle: mockDocumentHandle,
118
+ parameters: intentParams,
119
+ }),
120
+ )
121
+
122
+ result.current.dispatchIntent()
123
+
124
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
125
+ action: 'edit',
126
+ document: {
127
+ id: 'test-document-id',
128
+ type: 'test-document-type',
129
+ },
130
+ resource: {
131
+ id: 'test-project-id.test-dataset',
132
+ },
133
+ parameters: intentParams,
134
+ })
135
+ })
136
+
137
+ it('should send intent message with intentId and params when both are provided', () => {
138
+ const intentParams = {view: 'editor', tab: 'content'}
139
+ const {result} = renderHook(() =>
140
+ useDispatchIntent({
141
+ intentId: 'custom-intent',
142
+ documentHandle: mockDocumentHandle,
143
+ parameters: intentParams,
144
+ }),
145
+ )
146
+
147
+ result.current.dispatchIntent()
148
+
149
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
150
+ intentId: 'custom-intent',
151
+ document: {
152
+ id: 'test-document-id',
153
+ type: 'test-document-type',
154
+ },
155
+ resource: {
156
+ id: 'test-project-id.test-dataset',
157
+ },
158
+ parameters: intentParams,
159
+ })
160
+ })
161
+
162
+ it('should send intent message with media library source', () => {
163
+ const mockMediaLibraryHandle: DocumentHandleWithSource = {
164
+ documentId: 'test-asset-id',
165
+ documentType: 'sanity.asset',
166
+ source: mediaLibrarySource('mlPGY7BEqt52'),
167
+ }
168
+
169
+ const {result} = renderHook(() =>
170
+ useDispatchIntent({
171
+ action: 'edit',
172
+ documentHandle: mockMediaLibraryHandle,
173
+ }),
174
+ )
175
+
176
+ result.current.dispatchIntent()
177
+
178
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
179
+ action: 'edit',
180
+ document: {
181
+ id: 'test-asset-id',
182
+ type: 'sanity.asset',
183
+ },
184
+ resource: {
185
+ id: 'mlPGY7BEqt52',
186
+ type: 'media-library',
187
+ },
188
+ })
189
+ })
190
+
191
+ it('should send intent message with canvas source', () => {
192
+ const mockCanvasHandle: DocumentHandleWithSource = {
193
+ documentId: 'test-canvas-document-id',
194
+ documentType: 'sanity.canvas.document',
195
+ source: canvasSource('canvas123'),
196
+ }
197
+
198
+ const {result} = renderHook(() =>
199
+ useDispatchIntent({
200
+ action: 'edit',
201
+ documentHandle: mockCanvasHandle,
202
+ }),
203
+ )
204
+
205
+ result.current.dispatchIntent()
206
+
207
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
208
+ action: 'edit',
209
+ document: {
210
+ id: 'test-canvas-document-id',
211
+ type: 'sanity.canvas.document',
212
+ },
213
+ resource: {
214
+ id: 'canvas123',
215
+ type: 'canvas',
216
+ },
217
+ })
218
+ })
219
+
220
+ describe('error handling', () => {
221
+ it('should throw error when neither source nor projectId/dataset is provided', () => {
222
+ const invalidHandle = {
223
+ documentId: 'test-document-id',
224
+ documentType: 'test-document-type',
225
+ }
226
+
227
+ const {result} = renderHook(() =>
228
+ useDispatchIntent({
229
+ action: 'edit',
230
+ documentHandle: invalidHandle as unknown as DocumentHandleWithSource,
231
+ }),
232
+ )
233
+
234
+ expect(() => result.current.dispatchIntent()).toThrow(
235
+ 'useDispatchIntent: Either `source` or both `projectId` and `dataset` must be provided in documentHandle.',
236
+ )
237
+ })
238
+ })
239
+ })