@sanity/sdk-react 2.7.0 → 2.9.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/dist/index.d.ts +144 -25
- package/dist/index.js +248 -139
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SanityApp.tsx +1 -0
- package/src/components/auth/AuthBoundary.test.tsx +3 -0
- package/src/components/auth/LoginError.test.tsx +5 -0
- package/src/components/auth/LoginError.tsx +22 -1
- package/src/context/ResourceProvider.test.tsx +7 -1
- package/src/context/ResourceProvider.tsx +6 -0
- package/src/context/SDKStudioContext.ts +6 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +2 -0
- package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
- package/src/hooks/dashboard/useWindowTitle.ts +112 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
- package/src/hooks/document/useApplyDocumentActions.ts +99 -3
- package/src/hooks/document/useDocument.ts +22 -6
- package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
- package/src/hooks/document/useDocumentEvent.ts +10 -3
- package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
- package/src/hooks/document/useDocumentPermissions.ts +22 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
- package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
- package/src/hooks/document/useEditDocument.ts +34 -8
- package/src/hooks/documents/useDocuments.ts +2 -0
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +50 -28
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -0
- package/src/hooks/presence/usePresence.ts +2 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
- package/src/hooks/preview/useDocumentPreview.tsx +39 -55
- package/src/hooks/projection/useDocumentProjection.ts +2 -0
- package/src/hooks/query/useQuery.ts +2 -0
- package/src/hooks/releases/useActiveReleases.ts +32 -13
- package/src/hooks/releases/usePerspective.ts +26 -14
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.ts +2 -0
|
@@ -10,6 +10,8 @@ import {type SanityDocument} from 'groq'
|
|
|
10
10
|
import {useCallback} from 'react'
|
|
11
11
|
|
|
12
12
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
13
|
+
import {useNormalizedSourceOptions} from '../helpers/useNormalizedSourceOptions'
|
|
14
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
13
15
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
14
16
|
|
|
15
17
|
const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
|
|
@@ -258,24 +260,48 @@ export function useEditDocument<TData>(
|
|
|
258
260
|
* }
|
|
259
261
|
*
|
|
260
262
|
* ```
|
|
263
|
+
*
|
|
264
|
+
* @example Edit a document in a release
|
|
265
|
+
* ```tsx
|
|
266
|
+
* import {useEditDocument} from '@sanity/sdk-react'
|
|
267
|
+
*
|
|
268
|
+
* function EditArticleInRelease({documentId}: {documentId: string}) {
|
|
269
|
+
* // Use the document's plain ID — not `versions.<releaseName>.<id>`.
|
|
270
|
+
* // The document must already exist in the release (added via `createDocument` first).
|
|
271
|
+
* const editArticle = useEditDocument({
|
|
272
|
+
* documentId,
|
|
273
|
+
* documentType: 'article',
|
|
274
|
+
* perspective: {releaseName: 'summer-drop'},
|
|
275
|
+
* })
|
|
276
|
+
*
|
|
277
|
+
* return (
|
|
278
|
+
* <button onClick={() => editArticle(prev => ({...prev, title: 'Updated for release'}))}>
|
|
279
|
+
* Edit in Release
|
|
280
|
+
* </button>
|
|
281
|
+
* )
|
|
282
|
+
* }
|
|
283
|
+
* ```
|
|
261
284
|
*/
|
|
262
285
|
export function useEditDocument({
|
|
263
286
|
path,
|
|
264
287
|
...doc
|
|
265
288
|
}: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
|
|
266
289
|
const instance = useSanityInstance(doc)
|
|
290
|
+
trackHookUsage(instance, 'useEditDocument')
|
|
291
|
+
const normalizedDoc = useNormalizedSourceOptions(doc)
|
|
292
|
+
|
|
267
293
|
const apply = useApplyDocumentActions()
|
|
268
294
|
const isDocumentReady = useCallback(
|
|
269
|
-
() => getDocumentState(instance,
|
|
270
|
-
[instance,
|
|
295
|
+
() => getDocumentState(instance, normalizedDoc).getCurrent() !== undefined,
|
|
296
|
+
[instance, normalizedDoc],
|
|
271
297
|
)
|
|
272
|
-
if (!isDocumentReady()) throw resolveDocument(instance,
|
|
298
|
+
if (!isDocumentReady()) throw resolveDocument(instance, normalizedDoc)
|
|
273
299
|
|
|
274
300
|
return (updater: Updater<unknown>) => {
|
|
275
301
|
const currentPath = path
|
|
276
302
|
|
|
277
303
|
if (currentPath) {
|
|
278
|
-
const stateWithOptions = getDocumentState(instance, {...
|
|
304
|
+
const stateWithOptions = getDocumentState(instance, {...normalizedDoc, path})
|
|
279
305
|
const currentValue = stateWithOptions.getCurrent()
|
|
280
306
|
|
|
281
307
|
const nextValue =
|
|
@@ -283,10 +309,10 @@ export function useEditDocument({
|
|
|
283
309
|
? (updater as (prev: typeof currentValue) => typeof currentValue)(currentValue)
|
|
284
310
|
: updater
|
|
285
311
|
|
|
286
|
-
return apply(editDocument(
|
|
312
|
+
return apply(editDocument(normalizedDoc, {set: {[currentPath]: nextValue}}))
|
|
287
313
|
}
|
|
288
314
|
|
|
289
|
-
const fullDocState = getDocumentState(instance, {...
|
|
315
|
+
const fullDocState = getDocumentState(instance, {...normalizedDoc, path})
|
|
290
316
|
const current = fullDocState.getCurrent() as object | null | undefined
|
|
291
317
|
const nextValue =
|
|
292
318
|
typeof updater === 'function'
|
|
@@ -308,8 +334,8 @@ export function useEditDocument({
|
|
|
308
334
|
)
|
|
309
335
|
.map((key) =>
|
|
310
336
|
key in nextValue
|
|
311
|
-
? editDocument(
|
|
312
|
-
: editDocument(
|
|
337
|
+
? editDocument(normalizedDoc, {set: {[key]: (nextValue as Record<string, unknown>)[key]}})
|
|
338
|
+
: editDocument(normalizedDoc, {unset: [key]}),
|
|
313
339
|
)
|
|
314
340
|
|
|
315
341
|
return apply(editActions)
|
|
@@ -9,6 +9,7 @@ import {pick} from 'lodash-es'
|
|
|
9
9
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
10
10
|
|
|
11
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
12
13
|
import {useQuery} from '../query/useQuery'
|
|
13
14
|
|
|
14
15
|
const DEFAULT_BATCH_SIZE = 25
|
|
@@ -207,6 +208,7 @@ export function useDocuments<
|
|
|
207
208
|
TDataset,
|
|
208
209
|
TProjectId
|
|
209
210
|
> {
|
|
211
|
+
useTrackHookUsage('useDocuments')
|
|
210
212
|
const instance = useSanityInstance(options)
|
|
211
213
|
const [limit, setLimit] = useState(batchSize)
|
|
212
214
|
const documentTypes = useMemo(
|
|
@@ -23,45 +23,32 @@ export type WithSourceNameSupport<T extends {source?: DocumentSource}> = T & {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* clean separation between React and core layers.
|
|
26
|
+
* Pure function that normalizes options by resolving `sourceName` to a `DocumentSource`
|
|
27
|
+
* using the provided sources map. Use this when options are only available at call time
|
|
28
|
+
* (e.g. inside a callback) and you cannot call the {@link useNormalizedSourceOptions} hook.
|
|
30
29
|
*
|
|
31
30
|
* @typeParam T - The options type (must include optional source field)
|
|
32
|
-
* @param options -
|
|
31
|
+
* @param options - Options that may include `sourceName` and/or `source`
|
|
32
|
+
* @param sources - Map of source names to DocumentSource (e.g. from SourcesContext)
|
|
33
33
|
* @returns Normalized options with `sourceName` removed and `source` resolved
|
|
34
|
-
*
|
|
35
|
-
* @remarks
|
|
36
|
-
* Resolution priority:
|
|
37
|
-
* 1. If `sourceName` is provided, resolves it via `SourcesContext` and uses that
|
|
38
|
-
* 2. Otherwise, uses the inline `source` if provided
|
|
39
|
-
* 3. If neither is provided, returns options without a source field
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```tsx
|
|
43
|
-
* function useQuery(options: WithSourceNameSupport<QueryOptions>) {
|
|
44
|
-
* const instance = useSanityInstance(options)
|
|
45
|
-
* const normalized = useNormalizedOptions(options)
|
|
46
|
-
* // normalized now has source but never sourceName
|
|
47
|
-
* const queryKey = getQueryKey(normalized)
|
|
48
|
-
* }
|
|
49
|
-
* ```
|
|
50
|
-
*
|
|
51
|
-
* @beta
|
|
34
|
+
* @internal
|
|
52
35
|
*/
|
|
53
|
-
export function
|
|
54
|
-
T
|
|
55
|
-
|
|
36
|
+
export function normalizeSourceOptions<T extends {source?: DocumentSource; sourceName?: string}>(
|
|
37
|
+
options: T,
|
|
38
|
+
sources: Record<string, DocumentSource>,
|
|
39
|
+
): Omit<T, 'sourceName'> {
|
|
56
40
|
const {sourceName, ...rest} = options
|
|
41
|
+
|
|
42
|
+
if (!sourceName && !options.source) {
|
|
43
|
+
return options
|
|
44
|
+
}
|
|
45
|
+
|
|
57
46
|
if (sourceName && Object.hasOwn(options, 'source')) {
|
|
58
47
|
throw new Error(
|
|
59
48
|
`Source name ${JSON.stringify(sourceName)} and source ${JSON.stringify(options.source)} cannot be used together.`,
|
|
60
49
|
)
|
|
61
50
|
}
|
|
62
51
|
|
|
63
|
-
// Resolve sourceName to source via context
|
|
64
|
-
const sources = useContext(SourcesContext)
|
|
65
52
|
let resolvedSource: DocumentSource | undefined
|
|
66
53
|
|
|
67
54
|
if (options.source) {
|
|
@@ -83,3 +70,38 @@ export function useNormalizedSourceOptions<
|
|
|
83
70
|
source: resolvedSource,
|
|
84
71
|
}
|
|
85
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalizes hook options by resolving `sourceName` to a `DocumentSource`.
|
|
76
|
+
* This hook ensures that options passed to core layer functions only contain
|
|
77
|
+
* `source` (never `sourceName`), preventing duplicate cache keys and maintaining
|
|
78
|
+
* clean separation between React and core layers.
|
|
79
|
+
*
|
|
80
|
+
* @typeParam T - The options type (must include optional source field)
|
|
81
|
+
* @param options - Hook options that may include `sourceName` and/or `source`
|
|
82
|
+
* @returns Normalized options with `sourceName` removed and `source` resolved
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Resolution priority:
|
|
86
|
+
* 1. If `sourceName` is provided, resolves it via `SourcesContext` and uses that
|
|
87
|
+
* 2. Otherwise, uses the inline `source` if provided
|
|
88
|
+
* 3. If neither is provided, returns options without a source field
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* function useQuery(options: WithSourceNameSupport<QueryOptions>) {
|
|
93
|
+
* const instance = useSanityInstance(options)
|
|
94
|
+
* const normalized = useNormalizedOptions(options)
|
|
95
|
+
* // normalized now has source but never sourceName
|
|
96
|
+
* const queryKey = getQueryKey(normalized)
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @beta
|
|
101
|
+
*/
|
|
102
|
+
export function useNormalizedSourceOptions<
|
|
103
|
+
T extends {source?: DocumentSource; sourceName?: string},
|
|
104
|
+
>(options: T): Omit<T, 'sourceName'> {
|
|
105
|
+
const sources = useContext(SourcesContext)
|
|
106
|
+
return normalizeSourceOptions(options, sources)
|
|
107
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {trackHookMounted} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useRef} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tracks the first usage of a named hook per SDK session.
|
|
9
|
+
* If the telemetry manager hasn't initialized yet, the hook
|
|
10
|
+
* name is buffered and flushed when it becomes available.
|
|
11
|
+
*
|
|
12
|
+
* Uses a ref to ensure the tracking call only happens once per
|
|
13
|
+
* component mount, avoiding repeated WeakMap lookups on re-renders.
|
|
14
|
+
*
|
|
15
|
+
* Call at the top of any public hook whose adoption we want to measure.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function useTrackHookUsage(hookName: string): void {
|
|
20
|
+
const instance = useSanityInstance()
|
|
21
|
+
const tracked = useRef(false)
|
|
22
|
+
if (!tracked.current) {
|
|
23
|
+
tracked.current = true
|
|
24
|
+
trackHookMounted(instance, hookName)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Non-hook variant for tracking hook usage when an instance is already
|
|
30
|
+
* available (avoids an extra `useSanityInstance` call in hooks that
|
|
31
|
+
* already have the instance).
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export function trackHookUsage(instance: SanityInstance, hookName: string): void {
|
|
36
|
+
trackHookMounted(instance, hookName)
|
|
37
|
+
}
|
|
@@ -4,6 +4,7 @@ import {pick} from 'lodash-es'
|
|
|
4
4
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
5
5
|
|
|
6
6
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
8
|
import {useQuery} from '../query/useQuery'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -238,6 +239,7 @@ export function usePaginatedDocuments<
|
|
|
238
239
|
TDataset,
|
|
239
240
|
TProjectId
|
|
240
241
|
> {
|
|
242
|
+
useTrackHookUsage('usePaginatedDocuments')
|
|
241
243
|
const instance = useSanityInstance(options)
|
|
242
244
|
const [pageIndex, setPageIndex] = useState(0)
|
|
243
245
|
const key = JSON.stringify({filter, search, params, orderings, pageSize})
|
|
@@ -2,6 +2,7 @@ import {getPresence, type UserPresence} from '@sanity/sdk'
|
|
|
2
2
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* A hook for subscribing to presence information for the current project.
|
|
@@ -11,6 +12,7 @@ export function usePresence(): {
|
|
|
11
12
|
locations: UserPresence[]
|
|
12
13
|
} {
|
|
13
14
|
const sanityInstance = useSanityInstance()
|
|
15
|
+
trackHookUsage(sanityInstance, 'usePresence')
|
|
14
16
|
const source = useMemo(() => getPresence(sanityInstance), [sanityInstance])
|
|
15
17
|
const subscribe = useCallback((callback: () => void) => source.subscribe(callback), [source])
|
|
16
18
|
const locations = useSyncExternalStore(
|
|
@@ -1,233 +1,124 @@
|
|
|
1
|
-
import {type DocumentHandle,
|
|
2
|
-
import {
|
|
3
|
-
import {useRef} from 'react'
|
|
4
|
-
import {type Mock} from 'vitest'
|
|
1
|
+
import {type DocumentHandle, type PreviewQueryResult} from '@sanity/sdk'
|
|
2
|
+
import {beforeEach, describe, expect, test, vi} from 'vitest'
|
|
5
3
|
|
|
6
|
-
import {
|
|
4
|
+
import {render, renderHook, screen} from '../../../test/test-utils'
|
|
5
|
+
import {useDocumentProjection} from '../projection/useDocumentProjection'
|
|
7
6
|
import {useDocumentPreview} from './useDocumentPreview'
|
|
8
7
|
|
|
9
|
-
// Mock
|
|
10
|
-
|
|
11
|
-
let intersectionObserverCallback: (entries: IntersectionObserverEntry[]) => void
|
|
12
|
-
|
|
13
|
-
beforeAll(() => {
|
|
14
|
-
vi.stubGlobal(
|
|
15
|
-
'IntersectionObserver',
|
|
16
|
-
class {
|
|
17
|
-
constructor(callback: (entries: IntersectionObserverEntry[]) => void) {
|
|
18
|
-
intersectionObserverCallback = callback
|
|
19
|
-
mockIntersectionObserver(callback)
|
|
20
|
-
}
|
|
21
|
-
observe = vi.fn()
|
|
22
|
-
disconnect = vi.fn()
|
|
23
|
-
},
|
|
24
|
-
)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// Mock the preview store
|
|
28
|
-
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
29
|
-
const actual = await importOriginal<typeof import('@sanity/sdk')>()
|
|
30
|
-
const getCurrent = vi.fn()
|
|
31
|
-
const subscribe = vi.fn()
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
...actual,
|
|
35
|
-
resolvePreview: vi.fn(),
|
|
36
|
-
getPreviewState: vi.fn().mockReturnValue({getCurrent, subscribe}),
|
|
37
|
-
}
|
|
38
|
-
})
|
|
8
|
+
// Mock useDocumentProjection since useDocumentPreview now uses it internally
|
|
9
|
+
vi.mock('../projection/useDocumentProjection')
|
|
39
10
|
|
|
40
11
|
const mockDocument: DocumentHandle = {
|
|
41
12
|
documentId: 'doc1',
|
|
42
13
|
documentType: 'exampleType',
|
|
43
14
|
}
|
|
44
15
|
|
|
45
|
-
function TestComponent(docHandle: DocumentHandle) {
|
|
46
|
-
const ref = useRef(null)
|
|
47
|
-
const {data, isPending} = useDocumentPreview({...docHandle, ref})
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div ref={ref}>
|
|
51
|
-
<h1>{data?.title}</h1>
|
|
52
|
-
<p>{data?.subtitle}</p>
|
|
53
|
-
{isPending && <div>Pending...</div>}
|
|
54
|
-
</div>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
16
|
describe('useDocumentPreview', () => {
|
|
59
|
-
let getCurrent: Mock
|
|
60
|
-
let subscribe: Mock
|
|
61
|
-
|
|
62
17
|
beforeEach(() => {
|
|
63
|
-
|
|
64
|
-
getCurrent = getPreviewState().getCurrent as Mock
|
|
65
|
-
// @ts-expect-error mock does not need param
|
|
66
|
-
subscribe = getPreviewState().subscribe as Mock
|
|
67
|
-
|
|
68
|
-
// Reset all mocks between tests
|
|
69
|
-
getCurrent.mockReset()
|
|
70
|
-
subscribe.mockReset()
|
|
71
|
-
mockIntersectionObserver.mockReset()
|
|
18
|
+
vi.clearAllMocks()
|
|
72
19
|
})
|
|
73
20
|
|
|
74
|
-
test('
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
render(
|
|
84
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
85
|
-
<TestComponent {...mockDocument} />
|
|
86
|
-
</ResourceProvider>,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
// Initially, element is not intersecting
|
|
90
|
-
expect(screen.getByText('Initial Title')).toBeInTheDocument()
|
|
91
|
-
expect(subscribe).not.toHaveBeenCalled()
|
|
92
|
-
|
|
93
|
-
// Simulate element becoming visible
|
|
94
|
-
await act(async () => {
|
|
95
|
-
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
// After element becomes visible, events subscription should be active
|
|
99
|
-
expect(subscribe).toHaveBeenCalled()
|
|
100
|
-
expect(eventsUnsubscribe).not.toHaveBeenCalled()
|
|
21
|
+
test('transforms projection result to preview format', () => {
|
|
22
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
23
|
+
_id: 'doc1',
|
|
24
|
+
_type: 'exampleType',
|
|
25
|
+
_updatedAt: '2024-01-01',
|
|
26
|
+
titleCandidates: {title: 'Test Title'},
|
|
27
|
+
subtitleCandidates: {description: 'Test Description'},
|
|
28
|
+
media: null,
|
|
29
|
+
}
|
|
101
30
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
31
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
32
|
+
data: mockProjectionResult,
|
|
33
|
+
isPending: false,
|
|
105
34
|
})
|
|
106
35
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(
|
|
36
|
+
const {result} = renderHook(() => useDocumentPreview(mockDocument))
|
|
37
|
+
const {data, isPending} = result.current
|
|
38
|
+
expect(data.title).toBe('Test Title')
|
|
39
|
+
expect(data.subtitle).toBe('Test Description')
|
|
40
|
+
expect(isPending).toBe(false)
|
|
110
41
|
})
|
|
111
42
|
|
|
112
|
-
test
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
43
|
+
test('handles pending state', () => {
|
|
44
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
45
|
+
_id: 'doc1',
|
|
46
|
+
_type: 'exampleType',
|
|
47
|
+
_updatedAt: '2024-01-01',
|
|
48
|
+
titleCandidates: {title: 'Loading Title'},
|
|
49
|
+
subtitleCandidates: {},
|
|
118
50
|
media: null,
|
|
119
|
-
}
|
|
120
|
-
;(resolvePreview as Mock).mockReturnValueOnce(resolvePromise)
|
|
51
|
+
}
|
|
121
52
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return vi.fn()
|
|
53
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
54
|
+
data: mockProjectionResult,
|
|
55
|
+
isPending: true,
|
|
126
56
|
})
|
|
127
57
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
58
|
+
function TestComponent() {
|
|
59
|
+
const {data, isPending} = useDocumentPreview(mockDocument)
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<h1>{data.title}</h1>
|
|
63
|
+
{isPending && <div>Pending...</div>}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
133
67
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Simulate element becoming visible
|
|
137
|
-
await act(async () => {
|
|
138
|
-
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
139
|
-
await resolvePromise
|
|
140
|
-
getCurrent.mockReturnValue({
|
|
141
|
-
data: {title: 'Resolved Title', subtitle: 'Resolved Subtitle'},
|
|
142
|
-
isPending: false,
|
|
143
|
-
})
|
|
144
|
-
subscriber?.()
|
|
145
|
-
})
|
|
68
|
+
render(<TestComponent />)
|
|
146
69
|
|
|
147
|
-
expect(screen.getByText('
|
|
148
|
-
expect(screen.getByText('
|
|
70
|
+
expect(screen.getByText('Loading Title')).toBeInTheDocument()
|
|
71
|
+
expect(screen.getByText('Pending...')).toBeInTheDocument()
|
|
149
72
|
})
|
|
150
73
|
|
|
151
|
-
test('
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
74
|
+
test('uses fallback title when no candidates exist', () => {
|
|
75
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
76
|
+
_id: 'doc1',
|
|
77
|
+
_type: 'article',
|
|
78
|
+
_updatedAt: '2024-01-01',
|
|
79
|
+
titleCandidates: {},
|
|
80
|
+
subtitleCandidates: {},
|
|
81
|
+
media: null,
|
|
82
|
+
}
|
|
156
83
|
|
|
157
|
-
|
|
158
|
-
data:
|
|
84
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
85
|
+
data: mockProjectionResult,
|
|
159
86
|
isPending: false,
|
|
160
87
|
})
|
|
161
|
-
subscribe.mockImplementation(() => vi.fn())
|
|
162
88
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
</ResourceProvider>,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
expect(screen.getByText('Fallback Title')).toBeInTheDocument()
|
|
170
|
-
|
|
171
|
-
// Restore IntersectionObserver
|
|
172
|
-
window.IntersectionObserver = originalIntersectionObserver
|
|
89
|
+
const {result} = renderHook(() => useDocumentPreview(mockDocument))
|
|
90
|
+
const {data} = result.current
|
|
91
|
+
expect(data.title).toBe('article: doc1')
|
|
173
92
|
})
|
|
174
93
|
|
|
175
|
-
test('
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
function NoRefComponent(docHandle: DocumentHandle) {
|
|
184
|
-
const {data} = useDocumentPreview(docHandle) // No ref provided
|
|
185
|
-
return (
|
|
186
|
-
<div>
|
|
187
|
-
<h1>{data?.title}</h1>
|
|
188
|
-
<p>{data?.subtitle}</p>
|
|
189
|
-
</div>
|
|
190
|
-
)
|
|
94
|
+
test('passes ref to useDocumentProjection', () => {
|
|
95
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
96
|
+
_id: 'doc1',
|
|
97
|
+
_type: 'exampleType',
|
|
98
|
+
_updatedAt: '2024-01-01',
|
|
99
|
+
titleCandidates: {title: 'Title'},
|
|
100
|
+
subtitleCandidates: {},
|
|
101
|
+
media: null,
|
|
191
102
|
}
|
|
192
103
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<NoRefComponent {...mockDocument} />
|
|
196
|
-
</ResourceProvider>,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
// Should subscribe immediately without waiting for intersection
|
|
200
|
-
expect(subscribe).toHaveBeenCalled()
|
|
201
|
-
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('it subscribes immediately when ref.current is not an HTML element', async () => {
|
|
205
|
-
getCurrent.mockReturnValue({
|
|
206
|
-
data: {title: 'Title', subtitle: 'Subtitle'},
|
|
104
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
105
|
+
data: mockProjectionResult,
|
|
207
106
|
isPending: false,
|
|
208
107
|
})
|
|
209
|
-
const eventsUnsubscribe = vi.fn()
|
|
210
|
-
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
211
|
-
|
|
212
|
-
function NonHtmlRefComponent(docHandle: DocumentHandle) {
|
|
213
|
-
const ref = useRef({}) // ref.current is not an HTML element
|
|
214
|
-
const {data} = useDocumentPreview({...docHandle, ref})
|
|
215
|
-
return (
|
|
216
|
-
<div>
|
|
217
|
-
<h1>{data?.title}</h1>
|
|
218
|
-
<p>{data?.subtitle}</p>
|
|
219
|
-
</div>
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
108
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
109
|
+
const ref = {current: null}
|
|
110
|
+
const {result} = renderHook(() => useDocumentPreview({...mockDocument, ref}))
|
|
111
|
+
const {data} = result.current
|
|
112
|
+
expect(data.title).toBe('Title')
|
|
113
|
+
|
|
114
|
+
// Verify useDocumentProjection was called with the ref and preview projection
|
|
115
|
+
expect(useDocumentProjection).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
documentId: 'doc1',
|
|
118
|
+
documentType: 'exampleType',
|
|
119
|
+
ref: ref,
|
|
120
|
+
projection: expect.any(String),
|
|
121
|
+
}),
|
|
227
122
|
)
|
|
228
|
-
|
|
229
|
-
// Should subscribe immediately without waiting for intersection
|
|
230
|
-
expect(subscribe).toHaveBeenCalled()
|
|
231
|
-
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
232
123
|
})
|
|
233
124
|
})
|