@sanity/sdk-react 0.0.0-rc.5 → 0.0.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.
- package/README.md +5 -57
- package/dist/index.d.ts +1000 -438
- package/dist/index.js +325 -259
- package/dist/index.js.map +1 -1
- package/package.json +15 -14
- package/src/_exports/sdk-react.ts +4 -1
- package/src/components/SDKProvider.tsx +6 -1
- package/src/components/SanityApp.test.tsx +29 -47
- package/src/components/SanityApp.tsx +12 -11
- package/src/components/auth/AuthBoundary.test.tsx +177 -7
- package/src/components/auth/AuthBoundary.tsx +32 -2
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginError.tsx +9 -3
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +3 -3
- package/src/hooks/comlink/useManageFavorite.test.ts +276 -27
- package/src/hooks/comlink/useManageFavorite.ts +102 -51
- package/src/hooks/comlink/useWindowConnection.ts +3 -2
- package/src/hooks/datasets/useDatasets.test.ts +80 -0
- package/src/hooks/datasets/useDatasets.ts +2 -1
- package/src/hooks/document/useApplyDocumentActions.ts +105 -31
- package/src/hooks/document/useDocument.test.ts +41 -4
- package/src/hooks/document/useDocument.ts +198 -114
- package/src/hooks/document/useDocumentEvent.test.ts +5 -5
- package/src/hooks/document/useDocumentEvent.ts +67 -23
- package/src/hooks/document/useDocumentPermissions.ts +47 -8
- package/src/hooks/document/useDocumentSyncStatus.test.ts +12 -5
- package/src/hooks/document/useDocumentSyncStatus.ts +41 -14
- package/src/hooks/document/useEditDocument.test.ts +24 -6
- package/src/hooks/document/useEditDocument.ts +238 -133
- package/src/hooks/documents/useDocuments.test.tsx +1 -1
- package/src/hooks/documents/useDocuments.ts +153 -44
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +1 -1
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +120 -47
- package/src/hooks/projection/useProjection.ts +134 -46
- package/src/hooks/projects/useProject.test.ts +80 -0
- package/src/hooks/projects/useProjects.test.ts +77 -0
- package/src/hooks/query/useQuery.test.tsx +4 -4
- package/src/hooks/query/useQuery.ts +115 -43
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- package/src/hooks/releases/usePerspective.ts +50 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {observeOrganizationVerificationState, type OrgVerificationResult} from '@sanity/sdk'
|
|
2
|
+
import {act, renderHook, waitFor} from '@testing-library/react'
|
|
3
|
+
import {Subject} from 'rxjs'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {useVerifyOrgProjects} from './useVerifyOrgProjects'
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
11
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
12
|
+
return {
|
|
13
|
+
...original,
|
|
14
|
+
observeOrganizationVerificationState: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
vi.mock('../context/useSanityInstance')
|
|
18
|
+
|
|
19
|
+
describe('useVerifyOrgProjects', () => {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
const mockInstance = {config: {}} as any // Dummy instance
|
|
22
|
+
const mockObserve = vi.mocked(observeOrganizationVerificationState)
|
|
23
|
+
const mockUseInstance = vi.mocked(useSanityInstance)
|
|
24
|
+
const testProjectIds = ['proj-1']
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks()
|
|
28
|
+
mockUseInstance.mockReturnValue(mockInstance)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return null and not observe state if disabled', () => {
|
|
32
|
+
const {result} = renderHook(() => useVerifyOrgProjects(true, testProjectIds))
|
|
33
|
+
|
|
34
|
+
expect(result.current).toBeNull()
|
|
35
|
+
expect(mockObserve).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should return null and not observe state if projectIds is missing or empty', () => {
|
|
39
|
+
const {result: resultUndefined} = renderHook(() => useVerifyOrgProjects(false, undefined))
|
|
40
|
+
expect(resultUndefined.current).toBeNull()
|
|
41
|
+
expect(mockObserve).not.toHaveBeenCalled()
|
|
42
|
+
|
|
43
|
+
const {result: resultEmpty} = renderHook(() => useVerifyOrgProjects(false, []))
|
|
44
|
+
expect(resultEmpty.current).toBeNull()
|
|
45
|
+
expect(mockObserve).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should return null initially when not disabled and projectIds provided', () => {
|
|
49
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
50
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
51
|
+
|
|
52
|
+
const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
53
|
+
|
|
54
|
+
expect(result.current).toBeNull()
|
|
55
|
+
expect(mockObserve).toHaveBeenCalledWith(mockInstance, testProjectIds)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should return null if observable emits { error: null }', async () => {
|
|
59
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
60
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
61
|
+
|
|
62
|
+
const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
63
|
+
|
|
64
|
+
act(() => {
|
|
65
|
+
subject.next({error: null})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(result.current).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return error string if observable emits { error: string }', async () => {
|
|
74
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
75
|
+
const errorMessage = 'Org mismatch'
|
|
76
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
77
|
+
|
|
78
|
+
const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
79
|
+
|
|
80
|
+
act(() => {
|
|
81
|
+
subject.next({error: errorMessage})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(result.current).toBe(errorMessage)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should unsubscribe on unmount', () => {
|
|
90
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
91
|
+
const unsubscribeSpy = vi.spyOn(subject, 'unsubscribe')
|
|
92
|
+
mockObserve.mockReturnValue(subject)
|
|
93
|
+
|
|
94
|
+
const {unmount} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
95
|
+
|
|
96
|
+
expect(unsubscribeSpy).not.toHaveBeenCalled()
|
|
97
|
+
unmount()
|
|
98
|
+
// Note: RxJS handles the inner subscription cleanup when the source (Subject) completes or errors,
|
|
99
|
+
// but testing library unmount should trigger the useEffect cleanup which calls unsubscribe.
|
|
100
|
+
// However, the spy might be on the Subject itself, not the final Subscription object.
|
|
101
|
+
// Let's adjust to spy on the returned subscription directly if possible, or accept this limitation.
|
|
102
|
+
// For now, we assume the useEffect cleanup calls unsubscribe correctly.
|
|
103
|
+
// We can validate subscription logic more deeply if needed.
|
|
104
|
+
// For this test, let's check if the observable reference still has observers.
|
|
105
|
+
expect(subject.observed).toBe(false) // Check if observers are gone after unmount
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should clear the error if disabled becomes true', async () => {
|
|
109
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
110
|
+
const errorMessage = 'Org mismatch'
|
|
111
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
112
|
+
|
|
113
|
+
const {result, rerender} = renderHook(
|
|
114
|
+
({disabled, pIds}) => useVerifyOrgProjects(disabled, pIds),
|
|
115
|
+
{
|
|
116
|
+
initialProps: {disabled: false, pIds: testProjectIds},
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Set initial error
|
|
121
|
+
act(() => {
|
|
122
|
+
subject.next({error: errorMessage})
|
|
123
|
+
})
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(result.current).toBe(errorMessage)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Disable the hook
|
|
129
|
+
rerender({disabled: true, pIds: testProjectIds})
|
|
130
|
+
|
|
131
|
+
// Error should be cleared
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(result.current).toBeNull()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {observeOrganizationVerificationState, type OrgVerificationResult} from '@sanity/sdk'
|
|
2
|
+
import {useEffect, useState} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook that verifies the current projects belongs to the organization ID specified in the dashboard context.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
* @param disabled - When true, disables verification and skips project verification API calls
|
|
11
|
+
* @returns Error message if the project doesn't match the organization ID, or null if all match or verification isn't needed
|
|
12
|
+
* @category Projects
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function OrgVerifier() {
|
|
16
|
+
* const error = useVerifyOrgProjects()
|
|
17
|
+
*
|
|
18
|
+
* if (error) {
|
|
19
|
+
* return <div className="error">{error}</div>
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* return <div>Organization projects verified!</div>
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): string | null {
|
|
27
|
+
const instance = useSanityInstance()
|
|
28
|
+
const [error, setError] = useState<string | null>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (disabled || !projectIds || projectIds.length === 0) {
|
|
32
|
+
if (error !== null) setError(null)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds)
|
|
37
|
+
|
|
38
|
+
const subscription = verificationObservable$.subscribe((result: OrgVerificationResult) => {
|
|
39
|
+
setError(result.error)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
subscription.unsubscribe()
|
|
44
|
+
}
|
|
45
|
+
}, [instance, disabled, error, projectIds])
|
|
46
|
+
|
|
47
|
+
return error
|
|
48
|
+
}
|
|
@@ -5,11 +5,11 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A React hook that provides a client that subscribes to changes in your application,
|
|
8
|
-
* such as user authentication changes.
|
|
9
8
|
*
|
|
10
9
|
* @remarks
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* This hook is intended for advanced use cases and special API calls that the React SDK
|
|
11
|
+
* does not yet provide hooks for. We welcome you to get in touch with us to let us know
|
|
12
|
+
* your use cases for this!
|
|
13
13
|
*
|
|
14
14
|
* @category Platform
|
|
15
15
|
* @returns A Sanity client
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import {type Message, type Node, type Status} from '@sanity/comlink'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type FavoriteStatusResponse,
|
|
4
|
+
getFavoritesState,
|
|
5
|
+
getOrCreateNode,
|
|
6
|
+
resolveFavoritesState,
|
|
7
|
+
type SanityInstance,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {BehaviorSubject} from 'rxjs'
|
|
3
10
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
11
|
|
|
5
12
|
import {act, renderHook} from '../../../test/test-utils'
|
|
13
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
14
|
import {useManageFavorite} from './useManageFavorite'
|
|
7
15
|
|
|
8
16
|
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
@@ -11,12 +19,17 @@ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
|
11
19
|
...actual,
|
|
12
20
|
getOrCreateNode: vi.fn(),
|
|
13
21
|
releaseNode: vi.fn(),
|
|
22
|
+
getFavoritesState: vi.fn(),
|
|
23
|
+
resolveFavoritesState: vi.fn(),
|
|
14
24
|
}
|
|
15
25
|
})
|
|
16
26
|
|
|
27
|
+
vi.mock('../context/useSanityInstance')
|
|
28
|
+
|
|
17
29
|
describe('useManageFavorite', () => {
|
|
18
30
|
let node: Node<Message, Message>
|
|
19
31
|
let statusCallback: ((status: Status) => void) | null = null
|
|
32
|
+
let favoriteStatusSubject: BehaviorSubject<FavoriteStatusResponse>
|
|
20
33
|
|
|
21
34
|
const mockDocumentHandle = {
|
|
22
35
|
documentId: 'mock-id',
|
|
@@ -27,7 +40,7 @@ describe('useManageFavorite', () => {
|
|
|
27
40
|
function createMockNode() {
|
|
28
41
|
return {
|
|
29
42
|
on: vi.fn(() => () => {}),
|
|
30
|
-
|
|
43
|
+
fetch: vi.fn().mockImplementation(() => Promise.resolve({success: true})),
|
|
31
44
|
stop: vi.fn(),
|
|
32
45
|
onStatus: vi.fn((callback) => {
|
|
33
46
|
statusCallback = callback
|
|
@@ -38,8 +51,42 @@ describe('useManageFavorite', () => {
|
|
|
38
51
|
|
|
39
52
|
beforeEach(() => {
|
|
40
53
|
statusCallback = null
|
|
54
|
+
favoriteStatusSubject = new BehaviorSubject<FavoriteStatusResponse>({isFavorited: false})
|
|
41
55
|
node = createMockNode()
|
|
42
56
|
vi.mocked(getOrCreateNode).mockReturnValue(node)
|
|
57
|
+
|
|
58
|
+
// Mock getFavoritesState
|
|
59
|
+
vi.mocked(getFavoritesState).mockImplementation(() => ({
|
|
60
|
+
subscribe: (callback?: () => void) => {
|
|
61
|
+
if (!callback) return () => {}
|
|
62
|
+
|
|
63
|
+
const subscription = favoriteStatusSubject.subscribe(() => callback())
|
|
64
|
+
callback() // Initial call
|
|
65
|
+
return () => subscription.unsubscribe()
|
|
66
|
+
},
|
|
67
|
+
getCurrent: () => favoriteStatusSubject.getValue(),
|
|
68
|
+
observable: favoriteStatusSubject.asObservable(),
|
|
69
|
+
}))
|
|
70
|
+
|
|
71
|
+
// Mock resolveFavoritesState
|
|
72
|
+
vi.mocked(resolveFavoritesState).mockImplementation(async () => {
|
|
73
|
+
const newValue = {isFavorited: !favoriteStatusSubject.getValue().isFavorited}
|
|
74
|
+
favoriteStatusSubject.next(newValue)
|
|
75
|
+
return newValue
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Default mock for useSanityInstance
|
|
79
|
+
vi.mocked(useSanityInstance).mockReturnValue({
|
|
80
|
+
config: {
|
|
81
|
+
projectId: 'test',
|
|
82
|
+
dataset: 'test',
|
|
83
|
+
},
|
|
84
|
+
} as unknown as SanityInstance)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
favoriteStatusSubject.complete()
|
|
89
|
+
vi.clearAllMocks()
|
|
43
90
|
})
|
|
44
91
|
|
|
45
92
|
it('should initialize with default states', () => {
|
|
@@ -49,60 +96,118 @@ describe('useManageFavorite', () => {
|
|
|
49
96
|
expect(result.current.isConnected).toBe(false)
|
|
50
97
|
})
|
|
51
98
|
|
|
52
|
-
it('should handle favorite action', () => {
|
|
99
|
+
it('should handle favorite action and update state', async () => {
|
|
53
100
|
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
54
101
|
|
|
102
|
+
expect(result.current.isFavorited).toBe(false)
|
|
103
|
+
|
|
104
|
+
// Simulate connection first
|
|
55
105
|
act(() => {
|
|
56
|
-
|
|
106
|
+
statusCallback?.('connected')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await act(async () => {
|
|
110
|
+
await result.current.favorite()
|
|
57
111
|
})
|
|
58
112
|
|
|
59
|
-
expect(node.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
expect(node.fetch).toHaveBeenCalledWith(
|
|
114
|
+
'dashboard/v1/events/favorite/mutate',
|
|
115
|
+
{
|
|
116
|
+
document: {
|
|
117
|
+
id: 'mock-id',
|
|
118
|
+
type: 'mock-type',
|
|
119
|
+
resource: {
|
|
120
|
+
id: 'test.test',
|
|
121
|
+
type: 'studio',
|
|
122
|
+
},
|
|
66
123
|
},
|
|
124
|
+
eventType: 'added',
|
|
67
125
|
},
|
|
68
|
-
|
|
69
|
-
|
|
126
|
+
// empty options object (from useWindowConnection)
|
|
127
|
+
{},
|
|
128
|
+
)
|
|
129
|
+
expect(resolveFavoritesState).toHaveBeenCalled()
|
|
70
130
|
expect(result.current.isFavorited).toBe(true)
|
|
71
131
|
})
|
|
72
132
|
|
|
73
|
-
it('should handle unfavorite action', () => {
|
|
133
|
+
it('should handle unfavorite action and update state', async () => {
|
|
74
134
|
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
75
135
|
|
|
136
|
+
// Set initial state to favorited
|
|
137
|
+
await act(async () => {
|
|
138
|
+
favoriteStatusSubject.next({isFavorited: true})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(result.current.isFavorited).toBe(true)
|
|
142
|
+
|
|
143
|
+
// Simulate connection first
|
|
76
144
|
act(() => {
|
|
77
|
-
|
|
145
|
+
statusCallback?.('connected')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await act(async () => {
|
|
149
|
+
await result.current.unfavorite()
|
|
78
150
|
})
|
|
79
151
|
|
|
80
|
-
expect(node.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
152
|
+
expect(node.fetch).toHaveBeenCalledWith(
|
|
153
|
+
'dashboard/v1/events/favorite/mutate',
|
|
154
|
+
{
|
|
155
|
+
document: {
|
|
156
|
+
id: 'mock-id',
|
|
157
|
+
type: 'mock-type',
|
|
158
|
+
resource: {
|
|
159
|
+
id: 'test.test',
|
|
160
|
+
type: 'studio',
|
|
161
|
+
},
|
|
87
162
|
},
|
|
163
|
+
eventType: 'removed',
|
|
88
164
|
},
|
|
89
|
-
|
|
165
|
+
{},
|
|
166
|
+
)
|
|
167
|
+
expect(resolveFavoritesState).toHaveBeenCalled()
|
|
168
|
+
expect(result.current.isFavorited).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should not update state if favorite action fails', async () => {
|
|
172
|
+
vi.mocked(node.fetch).mockImplementationOnce(() => Promise.resolve({success: false}))
|
|
173
|
+
|
|
174
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
175
|
+
|
|
176
|
+
expect(result.current.isFavorited).toBe(false)
|
|
177
|
+
|
|
178
|
+
await act(async () => {
|
|
179
|
+
await result.current.favorite()
|
|
90
180
|
})
|
|
181
|
+
|
|
182
|
+
expect(resolveFavoritesState).not.toHaveBeenCalled()
|
|
91
183
|
expect(result.current.isFavorited).toBe(false)
|
|
92
184
|
})
|
|
93
185
|
|
|
94
|
-
it('should throw error during favorite/unfavorite actions', () => {
|
|
186
|
+
it('should throw error during favorite/unfavorite actions', async () => {
|
|
95
187
|
const errorMessage = 'Failed to update favorite status'
|
|
96
188
|
|
|
97
|
-
vi.mocked(node.
|
|
189
|
+
vi.mocked(node.fetch).mockImplementation(() => {
|
|
98
190
|
throw new Error(errorMessage)
|
|
99
191
|
})
|
|
100
192
|
|
|
101
193
|
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
102
194
|
|
|
103
|
-
act(() => {
|
|
104
|
-
|
|
195
|
+
await act(async () => {
|
|
196
|
+
statusCallback?.('connected')
|
|
105
197
|
})
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
await expect(result.current.favorite()).rejects.toThrow(errorMessage)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
expect(resolveFavoritesState).not.toHaveBeenCalled()
|
|
204
|
+
expect(result.current.isFavorited).toBe(false)
|
|
205
|
+
|
|
206
|
+
await act(async () => {
|
|
207
|
+
await expect(result.current.unfavorite()).rejects.toThrow(errorMessage)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(resolveFavoritesState).not.toHaveBeenCalled()
|
|
106
211
|
})
|
|
107
212
|
|
|
108
213
|
it('should update connection status', () => {
|
|
@@ -116,4 +221,148 @@ describe('useManageFavorite', () => {
|
|
|
116
221
|
|
|
117
222
|
expect(result.current.isConnected).toBe(true)
|
|
118
223
|
})
|
|
224
|
+
|
|
225
|
+
it('should throw error when studio resource is missing projectId or dataset', () => {
|
|
226
|
+
// Mock the Sanity instance to not have projectId or dataset
|
|
227
|
+
vi.mocked(useSanityInstance).mockReturnValue({
|
|
228
|
+
config: {
|
|
229
|
+
projectId: undefined,
|
|
230
|
+
dataset: undefined,
|
|
231
|
+
},
|
|
232
|
+
} as unknown as SanityInstance)
|
|
233
|
+
|
|
234
|
+
const mockDocumentHandleWithoutProjectId = {
|
|
235
|
+
documentId: 'mock-id',
|
|
236
|
+
documentType: 'mock-type',
|
|
237
|
+
resourceType: 'studio' as const,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expect(() => renderHook(() => useManageFavorite(mockDocumentHandleWithoutProjectId))).toThrow(
|
|
241
|
+
'projectId and dataset are required for studio resources',
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should throw error when resourceId is missing for non-studio resources', () => {
|
|
246
|
+
const mockMediaDocumentHandle = {
|
|
247
|
+
documentId: 'mock-id',
|
|
248
|
+
documentType: 'mock-type',
|
|
249
|
+
resourceType: 'media-library' as const,
|
|
250
|
+
resourceId: undefined,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
expect(() => renderHook(() => useManageFavorite(mockMediaDocumentHandle))).toThrow(
|
|
254
|
+
'resourceId is required for media-library and canvas resources',
|
|
255
|
+
)
|
|
256
|
+
})
|
|
257
|
+
|
|
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
|
+
})
|
|
279
|
+
|
|
280
|
+
// Favorite and unfavorite actions should be a no-op
|
|
281
|
+
await act(async () => {
|
|
282
|
+
await result.current.favorite()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
expect(node.fetch).not.toHaveBeenCalled()
|
|
286
|
+
|
|
287
|
+
await act(async () => {
|
|
288
|
+
await result.current.unfavorite()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
expect(node.fetch).not.toHaveBeenCalled()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should still throw non-timeout errors for suspension', async () => {
|
|
295
|
+
vi.mocked(getFavoritesState).mockImplementation(() => ({
|
|
296
|
+
subscribe: () => () => {},
|
|
297
|
+
getCurrent: () => undefined, // This will trigger the resolveFavoritesState call
|
|
298
|
+
observable: favoriteStatusSubject.asObservable(),
|
|
299
|
+
}))
|
|
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)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('should not call fetch if connection is not established', async () => {
|
|
313
|
+
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
|
+
await act(async () => {
|
|
320
|
+
await result.current.favorite()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// Fetch should not have been called due to the new status check
|
|
324
|
+
expect(node.fetch).not.toHaveBeenCalled()
|
|
325
|
+
|
|
326
|
+
// Try to unfavorite
|
|
327
|
+
await act(async () => {
|
|
328
|
+
await result.current.unfavorite()
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Fetch should still not have been called
|
|
332
|
+
expect(node.fetch).not.toHaveBeenCalled()
|
|
333
|
+
})
|
|
334
|
+
|
|
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')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
await act(async () => {
|
|
348
|
+
await result.current.favorite()
|
|
349
|
+
})
|
|
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
|
+
)
|
|
367
|
+
})
|
|
119
368
|
})
|