@sanity/sdk-react 0.0.0-alpha.9 → 0.0.0-rc.1

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 (94) hide show
  1. package/README.md +33 -126
  2. package/dist/index.d.ts +4742 -2
  3. package/dist/index.js +1054 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +19 -43
  6. package/src/_exports/index.ts +66 -10
  7. package/src/components/Login/LoginLinks.test.tsx +90 -0
  8. package/src/components/Login/LoginLinks.tsx +58 -0
  9. package/src/components/SDKProvider.test.tsx +79 -0
  10. package/src/components/SDKProvider.tsx +42 -0
  11. package/src/components/SanityApp.test.tsx +104 -2
  12. package/src/components/SanityApp.tsx +54 -17
  13. package/src/components/auth/AuthBoundary.test.tsx +2 -2
  14. package/src/components/auth/AuthBoundary.tsx +13 -3
  15. package/src/components/auth/Login.test.tsx +1 -1
  16. package/src/components/auth/Login.tsx +11 -26
  17. package/src/components/auth/LoginCallback.tsx +4 -7
  18. package/src/components/auth/LoginError.tsx +12 -8
  19. package/src/components/auth/LoginFooter.tsx +13 -20
  20. package/src/components/auth/LoginLayout.tsx +8 -9
  21. package/src/components/auth/authTestHelpers.tsx +1 -8
  22. package/src/components/utils.ts +22 -0
  23. package/src/context/SanityInstanceContext.ts +4 -0
  24. package/src/context/SanityProvider.test.tsx +1 -1
  25. package/src/context/SanityProvider.tsx +10 -8
  26. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  27. package/src/hooks/auth/useAuthState.tsx +0 -2
  28. package/src/hooks/auth/useCurrentUser.tsx +26 -20
  29. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  30. package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
  31. package/src/hooks/client/useClient.ts +8 -30
  32. package/src/hooks/comlink/useFrameConnection.test.tsx +55 -10
  33. package/src/hooks/comlink/useFrameConnection.ts +39 -43
  34. package/src/hooks/comlink/useManageFavorite.test.ts +106 -0
  35. package/src/hooks/comlink/useManageFavorite.ts +101 -0
  36. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +77 -0
  37. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +79 -0
  38. package/src/hooks/comlink/useWindowConnection.test.ts +53 -12
  39. package/src/hooks/comlink/useWindowConnection.ts +69 -29
  40. package/src/hooks/context/useSanityInstance.test.tsx +1 -1
  41. package/src/hooks/context/useSanityInstance.ts +21 -5
  42. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +97 -0
  43. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +274 -0
  44. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +91 -0
  45. package/src/hooks/datasets/useDatasets.ts +37 -0
  46. package/src/hooks/document/useApplyActions.test.ts +5 -4
  47. package/src/hooks/document/useApplyActions.ts +55 -5
  48. package/src/hooks/document/useDocument.test.ts +2 -2
  49. package/src/hooks/document/useDocument.ts +90 -21
  50. package/src/hooks/document/useDocumentEvent.test.ts +13 -3
  51. package/src/hooks/document/useDocumentEvent.ts +36 -4
  52. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -1
  53. package/src/hooks/document/useDocumentSyncStatus.ts +26 -2
  54. package/src/hooks/document/useEditDocument.test.ts +55 -10
  55. package/src/hooks/document/useEditDocument.ts +159 -31
  56. package/src/hooks/document/usePermissions.ts +82 -0
  57. package/src/hooks/helpers/createCallbackHook.tsx +3 -2
  58. package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
  59. package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
  60. package/src/hooks/infiniteList/useInfiniteList.test.tsx +152 -0
  61. package/src/hooks/infiniteList/useInfiniteList.ts +174 -0
  62. package/src/hooks/paginatedList/usePaginatedList.test.tsx +259 -0
  63. package/src/hooks/paginatedList/usePaginatedList.ts +290 -0
  64. package/src/hooks/preview/usePreview.test.tsx +6 -6
  65. package/src/hooks/preview/usePreview.tsx +12 -9
  66. package/src/hooks/projection/useProjection.test.tsx +218 -0
  67. package/src/hooks/projection/useProjection.ts +147 -0
  68. package/src/hooks/projects/useProject.ts +45 -0
  69. package/src/hooks/projects/useProjects.ts +41 -0
  70. package/src/hooks/query/useQuery.test.tsx +188 -0
  71. package/src/hooks/query/useQuery.ts +103 -0
  72. package/src/hooks/users/useUsers.test.ts +163 -0
  73. package/src/hooks/users/useUsers.ts +107 -0
  74. package/src/utils/getEnv.ts +21 -0
  75. package/src/version.ts +8 -0
  76. package/dist/_chunks-es/context.js +0 -8
  77. package/dist/_chunks-es/context.js.map +0 -1
  78. package/dist/_chunks-es/useLogOut.js +0 -45
  79. package/dist/_chunks-es/useLogOut.js.map +0 -1
  80. package/dist/components.d.ts +0 -111
  81. package/dist/components.js +0 -153
  82. package/dist/components.js.map +0 -1
  83. package/dist/context.d.ts +0 -45
  84. package/dist/context.js +0 -5
  85. package/dist/context.js.map +0 -1
  86. package/dist/hooks.d.ts +0 -3532
  87. package/dist/hooks.js +0 -218
  88. package/dist/hooks.js.map +0 -1
  89. package/src/_exports/components.ts +0 -2
  90. package/src/_exports/context.ts +0 -2
  91. package/src/_exports/hooks.ts +0 -32
  92. package/src/hooks/client/useClient.test.tsx +0 -130
  93. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  94. package/src/hooks/documentCollection/useDocuments.ts +0 -135
@@ -1,3 +1,4 @@
1
+ import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
1
2
  import {
2
3
  type FrameMessage,
3
4
  getOrCreateChannel,
@@ -5,7 +6,7 @@ import {
5
6
  releaseChannel,
6
7
  type WindowMessage,
7
8
  } from '@sanity/sdk'
8
- import {useCallback, useEffect, useMemo} from 'react'
9
+ import {useCallback, useEffect, useRef} from 'react'
9
10
 
10
11
  import {useSanityInstance} from '../context/useSanityInstance'
11
12
 
@@ -23,7 +24,11 @@ export interface UseFrameConnectionOptions<TWindowMessage extends WindowMessage>
23
24
  name: string
24
25
  connectTo: string
25
26
  targetOrigin: string
26
- onMessage?: Record<string, FrameMessageHandler<TWindowMessage>>
27
+ onMessage?: {
28
+ [K in TWindowMessage['type']]: (data: Extract<TWindowMessage, {type: K}>['data']) => void
29
+ }
30
+ heartbeat?: boolean
31
+ onStatus?: (status: Status) => void
27
32
  }
28
33
 
29
34
  /**
@@ -45,65 +50,56 @@ export function useFrameConnection<
45
50
  TFrameMessage extends FrameMessage,
46
51
  TWindowMessage extends WindowMessage,
47
52
  >(options: UseFrameConnectionOptions<TWindowMessage>): FrameConnection<TFrameMessage> {
48
- const {onMessage, targetOrigin, name, connectTo} = options
53
+ const {onMessage, targetOrigin, name, connectTo, heartbeat, onStatus} = options
49
54
  const instance = useSanityInstance()
50
-
51
- const controller = useMemo(
52
- () => getOrCreateController(instance, targetOrigin),
53
- [instance, targetOrigin],
54
- )
55
-
56
- const channel = useMemo(
57
- () =>
58
- getOrCreateChannel(instance, {
59
- name,
60
- connectTo,
61
- }),
62
- [instance, name, connectTo],
63
- )
55
+ const controllerRef = useRef<Controller | null>(null)
56
+ const channelRef = useRef<ChannelInstance<TFrameMessage, TWindowMessage> | null>(null)
64
57
 
65
58
  useEffect(() => {
66
- if (!channel || !onMessage) return
59
+ const controller = getOrCreateController(instance, targetOrigin)
60
+ const channel = getOrCreateChannel(instance, {name, connectTo, heartbeat})
61
+ controllerRef.current = controller
62
+ channelRef.current = channel
67
63
 
68
- const unsubscribers: Array<() => void> = []
69
-
70
- Object.entries(onMessage).forEach(([type, handler]) => {
71
- const unsubscribe = channel.on(type, handler)
72
- unsubscribers.push(unsubscribe)
64
+ channel.onStatus((event) => {
65
+ onStatus?.(event.status)
73
66
  })
74
67
 
68
+ const messageUnsubscribers: Array<() => void> = []
69
+
70
+ if (onMessage) {
71
+ Object.entries(onMessage).forEach(([type, handler]) => {
72
+ const unsubscribe = channel.on(type, handler as FrameMessageHandler<TWindowMessage>)
73
+ messageUnsubscribers.push(unsubscribe)
74
+ })
75
+ }
76
+
75
77
  return () => {
76
- unsubscribers.forEach((unsub) => unsub())
78
+ // Clean up all subscriptions and stop controller/channel
79
+ messageUnsubscribers.forEach((unsub) => unsub())
80
+ releaseChannel(instance, name)
81
+ channelRef.current = null
82
+ controllerRef.current = null
77
83
  }
78
- }, [channel, onMessage])
84
+ }, [targetOrigin, name, connectTo, heartbeat, onMessage, instance, onStatus])
79
85
 
80
- const connect = useCallback(
81
- (frameWindow: Window) => {
82
- const removeTarget = controller?.addTarget(frameWindow)
83
- return () => {
84
- removeTarget?.()
85
- }
86
- },
87
- [controller],
88
- )
86
+ const connect = useCallback((frameWindow: Window) => {
87
+ const removeTarget = controllerRef.current?.addTarget(frameWindow)
88
+ return () => {
89
+ removeTarget?.()
90
+ }
91
+ }, [])
89
92
 
90
93
  const sendMessage = useCallback(
91
94
  <T extends TFrameMessage['type']>(
92
95
  type: T,
93
96
  data?: Extract<TFrameMessage, {type: T}>['data'],
94
97
  ) => {
95
- channel?.post(type, data)
98
+ channelRef.current?.post(type, data)
96
99
  },
97
- [channel],
100
+ [],
98
101
  )
99
102
 
100
- // cleanup channel on unmount
101
- useEffect(() => {
102
- return () => {
103
- releaseChannel(instance, name)
104
- }
105
- }, [name, instance])
106
-
107
103
  return {
108
104
  connect,
109
105
  sendMessage,
@@ -0,0 +1,106 @@
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 {useManageFavorite} from './useManageFavorite'
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('useManageFavorite', () => {
18
+ let node: Node<Message, Message>
19
+ let statusCallback: ((status: Status) => void) | null = null
20
+
21
+ const mockDocumentHandle = {
22
+ _id: 'mock-id',
23
+ _type: 'mock-type',
24
+ }
25
+
26
+ function createMockNode() {
27
+ return {
28
+ on: vi.fn(() => () => {}),
29
+ post: vi.fn(),
30
+ stop: vi.fn(),
31
+ onStatus: vi.fn((callback) => {
32
+ statusCallback = callback
33
+ return () => {}
34
+ }),
35
+ } as unknown as Node<Message, Message>
36
+ }
37
+
38
+ beforeEach(() => {
39
+ statusCallback = null
40
+ node = createMockNode()
41
+ vi.mocked(getOrCreateNode).mockReturnValue(node)
42
+ })
43
+
44
+ it('should initialize with default states', () => {
45
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
46
+
47
+ expect(result.current.isFavorited).toBe(false)
48
+ expect(result.current.isConnected).toBe(false)
49
+ })
50
+
51
+ it('should handle favorite action', () => {
52
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
53
+
54
+ act(() => {
55
+ result.current.favorite()
56
+ })
57
+
58
+ expect(node.post).toHaveBeenCalledWith('core/v1/events/favorite', {
59
+ documentId: 'mock-id',
60
+ documentType: 'mock-type',
61
+ eventType: 'added',
62
+ })
63
+ expect(result.current.isFavorited).toBe(true)
64
+ })
65
+
66
+ it('should handle unfavorite action', () => {
67
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
68
+
69
+ act(() => {
70
+ result.current.unfavorite()
71
+ })
72
+
73
+ expect(node.post).toHaveBeenCalledWith('core/v1/events/favorite', {
74
+ documentId: 'mock-id',
75
+ documentType: 'mock-type',
76
+ eventType: 'removed',
77
+ })
78
+ expect(result.current.isFavorited).toBe(false)
79
+ })
80
+
81
+ it('should throw error during favorite/unfavorite actions', () => {
82
+ const errorMessage = 'Failed to update favorite status'
83
+
84
+ vi.mocked(node.post).mockImplementationOnce(() => {
85
+ throw new Error(errorMessage)
86
+ })
87
+
88
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
89
+
90
+ act(() => {
91
+ expect(() => result.current.favorite()).toThrow(errorMessage)
92
+ })
93
+ })
94
+
95
+ it('should update connection status', () => {
96
+ const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
97
+
98
+ expect(result.current.isConnected).toBe(false)
99
+
100
+ act(() => {
101
+ statusCallback?.('connected')
102
+ })
103
+
104
+ expect(result.current.isConnected).toBe(true)
105
+ })
106
+ })
@@ -0,0 +1,101 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
3
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
4
+ import {useCallback, useState} from 'react'
5
+
6
+ import {useWindowConnection} from './useWindowConnection'
7
+
8
+ // should we import this whole type from the message protocol?
9
+
10
+ interface ManageFavorite {
11
+ favorite: () => void
12
+ unfavorite: () => void
13
+ isFavorited: boolean
14
+ isConnected: boolean
15
+ }
16
+
17
+ /**
18
+ * @beta
19
+ *
20
+ * ## useManageFavorite
21
+ * This hook provides functionality to add and remove documents from favorites,
22
+ * and tracks the current favorite status of the document.
23
+ * @category Core UI Communication
24
+ * @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
25
+ * @returns An object containing:
26
+ * - `favorite` - Function to add document to favorites
27
+ * - `unfavorite` - Function to remove document from favorites
28
+ * - `isFavorited` - Boolean indicating if document is currently favorited
29
+ * - `isConnected` - Boolean indicating if connection to Core UI is established
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * function MyDocumentAction(props: DocumentActionProps) {
34
+ * const {_id, _type} = props
35
+ * const {favorite, unfavorite, isFavorited, isConnected} = useManageFavorite({
36
+ * _id,
37
+ * _type
38
+ * })
39
+ *
40
+ * return (
41
+ * <Button
42
+ * disabled={!isConnected}
43
+ * onClick={() => isFavorited ? unfavorite() : favorite()}
44
+ * text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
45
+ * />
46
+ * )
47
+ * }
48
+ * ```
49
+ */
50
+ export function useManageFavorite({_id, _type}: DocumentHandle): ManageFavorite {
51
+ const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
52
+ const [status, setStatus] = useState<Status>('idle')
53
+ const {sendMessage} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
54
+ name: SDK_NODE_NAME,
55
+ connectTo: SDK_CHANNEL_NAME,
56
+ onStatus: setStatus,
57
+ })
58
+
59
+ const handleFavoriteAction = useCallback(
60
+ (action: 'added' | 'removed', setFavoriteState: boolean) => {
61
+ if (!_id || !_type) return
62
+
63
+ try {
64
+ const message: Events.FavoriteMessage = {
65
+ type: 'core/v1/events/favorite',
66
+ data: {
67
+ eventType: action,
68
+ documentId: _id,
69
+ documentType: _type,
70
+ },
71
+ }
72
+
73
+ sendMessage(message.type, message.data)
74
+ setIsFavorited(setFavoriteState)
75
+ } catch (err) {
76
+ const error = err instanceof Error ? err : new Error('Failed to update favorite status')
77
+ // eslint-disable-next-line no-console
78
+ console.error(
79
+ `Failed to ${action === 'added' ? 'favorite' : 'unfavorite'} document:`,
80
+ error,
81
+ )
82
+ throw error
83
+ }
84
+ },
85
+ [_id, _type, sendMessage],
86
+ )
87
+
88
+ const favorite = useCallback(() => handleFavoriteAction('added', true), [handleFavoriteAction])
89
+
90
+ const unfavorite = useCallback(
91
+ () => handleFavoriteAction('removed', false),
92
+ [handleFavoriteAction],
93
+ )
94
+
95
+ return {
96
+ favorite,
97
+ unfavorite,
98
+ isFavorited,
99
+ isConnected: status === 'connected',
100
+ }
101
+ }
@@ -0,0 +1,77 @@
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
+ _id: 'mock-id',
23
+ _type: 'mock-type',
24
+ }
25
+
26
+ function createMockNode() {
27
+ return {
28
+ on: vi.fn(() => () => {}),
29
+ post: vi.fn(),
30
+ stop: vi.fn(),
31
+ onStatus: vi.fn((callback) => {
32
+ statusCallback = callback
33
+ return () => {}
34
+ }),
35
+ } as unknown as Node<Message, Message>
36
+ }
37
+
38
+ beforeEach(() => {
39
+ statusCallback = null
40
+ node = createMockNode()
41
+ vi.mocked(getOrCreateNode).mockReturnValue(node)
42
+ })
43
+
44
+ it('should initialize with correct connection status', () => {
45
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
46
+
47
+ expect(result.current.isConnected).toBe(false)
48
+
49
+ act(() => {
50
+ statusCallback?.('connected')
51
+ })
52
+
53
+ expect(result.current.isConnected).toBe(true)
54
+ })
55
+
56
+ it('should send correct message when recording events', () => {
57
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
58
+
59
+ result.current.recordEvent('viewed')
60
+
61
+ expect(node.post).toHaveBeenCalledWith('core/v1/events/history', {
62
+ eventType: 'viewed',
63
+ documentId: 'mock-id',
64
+ documentType: 'mock-type',
65
+ })
66
+ })
67
+
68
+ it('should handle errors when sending messages', () => {
69
+ vi.mocked(node.post).mockImplementation(() => {
70
+ throw new Error('Failed to send message')
71
+ })
72
+
73
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
74
+
75
+ expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
76
+ })
77
+ })
@@ -0,0 +1,79 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
3
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
4
+ import {useCallback, useState} from 'react'
5
+
6
+ import {useWindowConnection} from './useWindowConnection'
7
+
8
+ interface DocumentInteractionHistory {
9
+ recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
10
+ isConnected: boolean
11
+ }
12
+
13
+ /**
14
+ * @public
15
+ * Hook for managing document interaction history in Sanity Studio.
16
+ * This hook provides functionality to record document interactions.
17
+ * @category History
18
+ * @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
19
+ * @returns An object containing:
20
+ * - `recordEvent` - Function to record document interactions
21
+ * - `isConnected` - Boolean indicating if connection to Studio is established
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * function MyDocumentAction(props: DocumentActionProps) {
26
+ * const {_id, _type} = props
27
+ * const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
28
+ * _id,
29
+ * _type
30
+ * })
31
+ *
32
+ * return (
33
+ * <Button
34
+ * disabled={!isConnected}
35
+ * onClick={() => recordEvent('viewed')}
36
+ * text={'Viewed'}
37
+ * />
38
+ * )
39
+ * }
40
+ * ```
41
+ */
42
+ export function useRecordDocumentHistoryEvent({
43
+ _id,
44
+ _type,
45
+ }: DocumentHandle): DocumentInteractionHistory {
46
+ const [status, setStatus] = useState<Status>('idle')
47
+ const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
48
+ name: SDK_NODE_NAME,
49
+ connectTo: SDK_CHANNEL_NAME,
50
+ onStatus: setStatus,
51
+ })
52
+
53
+ const recordEvent = useCallback(
54
+ (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
55
+ try {
56
+ const message: Events.HistoryMessage = {
57
+ type: 'core/v1/events/history',
58
+ data: {
59
+ eventType,
60
+ documentId: _id,
61
+ documentType: _type,
62
+ },
63
+ }
64
+
65
+ sendMessage(message.type, message.data)
66
+ } catch (error) {
67
+ // eslint-disable-next-line no-console
68
+ console.error('Failed to record history event:', error)
69
+ throw error
70
+ }
71
+ },
72
+ [_id, _type, sendMessage],
73
+ )
74
+
75
+ return {
76
+ recordEvent,
77
+ isConnected: status === 'connected',
78
+ }
79
+ }
@@ -1,8 +1,8 @@
1
- import {type Message, type Node} from '@sanity/comlink'
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
2
  import {getOrCreateNode, releaseNode} from '@sanity/sdk'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
- import {renderHook} from '../../../test/test-utils'
5
+ import {act, renderHook} from '../../../test/test-utils'
6
6
  import {useWindowConnection} from './useWindowConnection'
7
7
 
8
8
  vi.mock(import('@sanity/sdk'), async (importOriginal) => {
@@ -26,24 +26,65 @@ interface AnotherMessage {
26
26
 
27
27
  type TestMessages = TestMessage | AnotherMessage
28
28
 
29
- function createMockNode() {
30
- return {
31
- // return unsubscribe function
32
- on: vi.fn(() => () => {}),
33
- post: vi.fn(),
34
- stop: vi.fn(),
35
- } as unknown as Node<Message, Message>
36
- }
37
-
38
29
  describe('useWindowConnection', () => {
39
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
+ }
40
45
 
41
46
  beforeEach(() => {
47
+ statusCallback = null
42
48
  node = createMockNode()
43
-
44
49
  vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
45
50
  })
46
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
+
47
88
  it('should register message handlers', () => {
48
89
  const mockHandler = vi.fn()
49
90
  const mockData = {someData: 'test'}