@sanity/sdk-react 0.0.0-alpha.1 → 0.0.0-alpha.11

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 (87) hide show
  1. package/README.md +163 -0
  2. package/dist/_chunks-es/context.js +8 -0
  3. package/dist/_chunks-es/context.js.map +1 -0
  4. package/dist/_chunks-es/useLogOut.js +29 -20
  5. package/dist/_chunks-es/useLogOut.js.map +1 -1
  6. package/dist/components.d.ts +24 -149
  7. package/dist/components.js +33 -153
  8. package/dist/components.js.map +1 -1
  9. package/dist/context.d.ts +45 -0
  10. package/dist/context.js +5 -0
  11. package/dist/context.js.map +1 -0
  12. package/dist/hooks.d.ts +3401 -12
  13. package/dist/hooks.js +210 -15
  14. package/dist/hooks.js.map +1 -1
  15. package/package.json +42 -32
  16. package/src/_exports/components.ts +2 -12
  17. package/src/_exports/context.ts +2 -0
  18. package/src/_exports/hooks.ts +25 -0
  19. package/src/components/Login/LoginLinks.test.tsx +7 -16
  20. package/src/components/Login/LoginLinks.tsx +14 -29
  21. package/src/components/SanityApp.test.tsx +54 -0
  22. package/src/components/SanityApp.tsx +53 -0
  23. package/src/components/auth/AuthBoundary.test.tsx +17 -21
  24. package/src/components/auth/AuthBoundary.tsx +14 -7
  25. package/src/components/auth/Login.test.tsx +2 -16
  26. package/src/components/auth/Login.tsx +11 -30
  27. package/src/components/auth/LoginCallback.test.tsx +2 -17
  28. package/src/components/auth/LoginCallback.tsx +5 -10
  29. package/src/components/auth/LoginError.test.tsx +2 -17
  30. package/src/components/auth/LoginError.tsx +11 -16
  31. package/src/components/auth/LoginFooter.test.tsx +2 -16
  32. package/src/components/auth/LoginFooter.tsx +8 -24
  33. package/src/components/auth/LoginLayout.test.tsx +2 -16
  34. package/src/components/auth/LoginLayout.tsx +8 -38
  35. package/src/components/auth/authTestHelpers.tsx +11 -0
  36. package/src/{components/context → context}/SanityProvider.test.tsx +1 -1
  37. package/src/{components/context → context}/SanityProvider.tsx +12 -6
  38. package/src/hooks/auth/useAuthState.test.tsx +10 -100
  39. package/src/hooks/auth/useAuthState.tsx +5 -10
  40. package/src/hooks/auth/useAuthToken.test.tsx +10 -88
  41. package/src/hooks/auth/useAuthToken.tsx +4 -10
  42. package/src/hooks/auth/useCurrentUser.test.tsx +10 -44
  43. package/src/hooks/auth/useCurrentUser.tsx +22 -22
  44. package/src/hooks/auth/useHandleCallback.test.tsx +10 -19
  45. package/src/hooks/auth/useHandleCallback.tsx +4 -9
  46. package/src/hooks/auth/useLogOut.test.tsx +11 -62
  47. package/src/hooks/auth/useLogOut.tsx +4 -9
  48. package/src/hooks/auth/useLoginUrls.test.tsx +47 -40
  49. package/src/hooks/auth/useLoginUrls.tsx +7 -6
  50. package/src/hooks/client/useClient.test.tsx +1 -1
  51. package/src/hooks/client/useClient.ts +3 -3
  52. package/src/hooks/comlink/useFrameConnection.test.tsx +122 -0
  53. package/src/hooks/comlink/useFrameConnection.ts +111 -0
  54. package/src/hooks/comlink/useWindowConnection.test.ts +94 -0
  55. package/src/hooks/comlink/useWindowConnection.ts +82 -0
  56. package/src/hooks/context/useSanityInstance.test.tsx +1 -1
  57. package/src/hooks/context/useSanityInstance.ts +4 -4
  58. package/src/hooks/document/useApplyActions.test.ts +24 -0
  59. package/src/hooks/document/useApplyActions.ts +24 -0
  60. package/src/hooks/document/useDocument.test.ts +81 -0
  61. package/src/hooks/document/useDocument.ts +38 -0
  62. package/src/hooks/document/useDocumentEvent.test.ts +53 -0
  63. package/src/hooks/document/useDocumentEvent.ts +22 -0
  64. package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
  65. package/src/hooks/document/useDocumentSyncStatus.ts +6 -0
  66. package/src/hooks/document/useEditDocument.test.ts +172 -0
  67. package/src/hooks/document/useEditDocument.ts +80 -0
  68. package/src/hooks/documentCollection/useDocuments.test.ts +130 -0
  69. package/src/hooks/documentCollection/useDocuments.ts +135 -0
  70. package/src/hooks/helpers/createCallbackHook.test.tsx +106 -0
  71. package/src/hooks/helpers/createCallbackHook.tsx +15 -0
  72. package/src/hooks/helpers/createStateSourceHook.test.tsx +130 -0
  73. package/src/hooks/helpers/createStateSourceHook.tsx +21 -0
  74. package/src/hooks/preview/usePreview.test.tsx +175 -0
  75. package/src/hooks/preview/usePreview.tsx +120 -0
  76. package/src/vite-env.d.ts +10 -0
  77. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -95
  78. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  79. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -23
  80. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -95
  81. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  82. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -15
  83. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  84. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -34
  85. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  86. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -115
  87. package/src/hooks/Documents/.keep +0 -0
@@ -0,0 +1,135 @@
1
+ import {createDocumentListStore, type DocumentHandle, type DocumentListOptions} from '@sanity/sdk'
2
+ import {useCallback, useEffect, useState, useSyncExternalStore} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ export interface DocumentCollection {
10
+ /** Retrieve more documents matching the provided options */
11
+ loadMore: () => void
12
+ /** The retrieved document handles of the documents matching the provided options */
13
+ results: DocumentHandle[]
14
+ /** Whether a retrieval of documents is in flight */
15
+ isPending: boolean
16
+ /** Whether more documents exist that match the provided options than have been retrieved */
17
+ hasMore: boolean
18
+ /** The total number of documents in the collection */
19
+ count: number
20
+ }
21
+
22
+ type DocumentListStore = ReturnType<typeof createDocumentListStore>
23
+ type DocumentListState = ReturnType<DocumentListStore['getState']>['getCurrent']
24
+ const STABLE_EMPTY = {
25
+ results: [],
26
+ isPending: false,
27
+ hasMore: false,
28
+ count: 0,
29
+ }
30
+
31
+ /**
32
+ * @public
33
+ *
34
+ * The `useDocuments` hook retrieves and provides access to a live collection of documents, optionally filtered, sorted, and matched to a given Content Lake perspective.
35
+ * Because the returned document collection is live, the results will update in real time until the component invoking the hook is unmounted.
36
+ *
37
+ * @param options - Options for narrowing and sorting the document collection
38
+ * @returns The collection of documents matching the provided options (if any), as well as properties describing the collection and a function to load more.
39
+ *
40
+ * @example Retrieving all documents of type 'movie'
41
+ * ```
42
+ * const { results, isPending } = useDocuments({ filter: '_type == "movie"' })
43
+ *
44
+ * return (
45
+ * <div>
46
+ * <h1>Movies</h1>
47
+ * {results && (
48
+ * <ul>
49
+ * {results.map(movie => (<li key={movie._id}>…</li>))}
50
+ * </ul>
51
+ * )}
52
+ * {isPending && <div>Loading movies…</div>}
53
+ * </div>
54
+ * )
55
+ * ```
56
+ *
57
+ * @example Retrieving all movies released since 1980, sorted by director’s last name
58
+ * ```
59
+ * const { results } = useDocuments({
60
+ * filter: '_type == "movie" && releaseDate >= "1980-01-01"',
61
+ * sort: [
62
+ * {
63
+ * // Expand the `director` reference field with the dereferencing operator `->`
64
+ * field: 'director->lastName',
65
+ * sort: 'asc',
66
+ * },
67
+ * ],
68
+ * })
69
+ *
70
+ * return (
71
+ * <div>
72
+ * <h1>Movies released since 1980</h1>
73
+ * {results && (
74
+ * <ol>
75
+ * {results.map(movie => (<li key={movie._id}>…</li>))}
76
+ * </ol>
77
+ * )}
78
+ * </div>
79
+ * )
80
+ * ```
81
+ */
82
+ export function useDocuments(options: DocumentListOptions = {}): DocumentCollection {
83
+ const instance = useSanityInstance()
84
+
85
+ // NOTE: useState is used because it guaranteed to return a stable reference
86
+ // across renders
87
+ const [ref] = useState<{
88
+ storeInstance: DocumentListStore | null
89
+ getCurrent: DocumentListState
90
+ initialOptions: DocumentListOptions
91
+ }>(() => ({
92
+ storeInstance: null,
93
+ getCurrent: () => STABLE_EMPTY,
94
+ initialOptions: options,
95
+ }))
96
+
97
+ // serialize options to ensure it only calls `setOptions` when the values
98
+ // themselves changes (in cases where devs put config inline)
99
+ const serializedOptions = JSON.stringify(options)
100
+ useEffect(() => {
101
+ ref.storeInstance?.setOptions(JSON.parse(serializedOptions))
102
+ }, [ref, serializedOptions])
103
+
104
+ const subscribe = useCallback(
105
+ (onStoreChanged: () => void) => {
106
+ // to match the lifecycle of `useSyncExternalState`, we create the store
107
+ // instance after subscribe and mutate the ref to connect everything
108
+ ref.storeInstance = createDocumentListStore(instance)
109
+ ref.storeInstance.setOptions(ref.initialOptions)
110
+ const state = ref.storeInstance.getState()
111
+ ref.getCurrent = state.getCurrent
112
+ const unsubscribe = state.subscribe(onStoreChanged)
113
+
114
+ return () => {
115
+ // unsubscribe to clean up the state subscriptions
116
+ unsubscribe()
117
+ // dispose of the instance
118
+ ref.storeInstance?.dispose()
119
+ }
120
+ },
121
+ [instance, ref],
122
+ )
123
+
124
+ const getSnapshot = useCallback(() => {
125
+ return ref.getCurrent()
126
+ }, [ref])
127
+
128
+ const state = useSyncExternalStore(subscribe, getSnapshot)
129
+
130
+ const loadMore = useCallback(() => {
131
+ ref.storeInstance?.loadMore()
132
+ }, [ref])
133
+
134
+ return {loadMore, ...state}
135
+ }
@@ -0,0 +1,106 @@
1
+ import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {describe, expect, it, vi} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {createCallbackHook} from './createCallbackHook'
7
+
8
+ // Mock the useSanityInstance hook
9
+ vi.mock('../context/useSanityInstance', () => ({
10
+ useSanityInstance: vi.fn(),
11
+ }))
12
+
13
+ describe('createCallbackHook', () => {
14
+ // Reset all mocks before each test
15
+ beforeEach(() => {
16
+ vi.clearAllMocks()
17
+ })
18
+
19
+ it('should create a hook that provides a memoized callback', () => {
20
+ // Create a mock Sanity instance
21
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
22
+
23
+ // Mock the useSanityInstance to return our mock instance
24
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
25
+
26
+ // Create a test callback function
27
+ const testCallback = (instance: object, param1: string, param2: number) => {
28
+ return `${param1}-${param2}-${instance ? 'valid' : 'invalid'}`
29
+ }
30
+
31
+ // Create our hook using the utility
32
+ const useTestHook = createCallbackHook(testCallback)
33
+
34
+ // Render the hook
35
+ const {result, rerender} = renderHook(() => useTestHook())
36
+
37
+ // Test the callback with parameters
38
+ const result1 = result.current('test', 123)
39
+ expect(result1).toBe('test-123-valid')
40
+
41
+ // Rerender and ensure the callback reference remains stable
42
+ rerender()
43
+ const result2 = result.current('test', 123)
44
+ expect(result2).toBe('test-123-valid')
45
+
46
+ // Verify the hook is memoizing the callback
47
+ expect(result.current).toBe(result.current)
48
+ })
49
+
50
+ it('should create new callback when instance changes', () => {
51
+ // Create two different mock instances
52
+ const mockInstance1 = createSanityInstance({projectId: 'p1', dataset: 'd'})
53
+ const mockInstance2 = createSanityInstance({projectId: 'p2', dataset: 'd'})
54
+
55
+ vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance1)
56
+
57
+ // Create a test callback
58
+ const testCallback = (instance: SanityInstance) => instance.identity.projectId
59
+
60
+ // Create and render our hook
61
+ const useTestHook = createCallbackHook(testCallback)
62
+ const {result, rerender} = renderHook(() => useTestHook())
63
+
64
+ // Store the first callback reference
65
+ const firstCallback = result.current
66
+
67
+ // Change the instance
68
+ vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance2)
69
+ rerender()
70
+
71
+ // Verify the callback reference changed
72
+ expect(result.current).not.toBe(firstCallback)
73
+
74
+ // Verify the callbacks return different results
75
+ expect(firstCallback()).toBe('p1')
76
+ expect(result.current()).toBe('p2')
77
+ })
78
+
79
+ it('should handle callbacks with multiple parameters', () => {
80
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
81
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
82
+
83
+ // Create a callback with multiple parameters
84
+ const testCallback = (
85
+ instance: SanityInstance,
86
+ path: string,
87
+ method: string,
88
+ data: object,
89
+ ) => ({
90
+ url: `${instance.identity.projectId}${path}`,
91
+ method,
92
+ data,
93
+ })
94
+
95
+ const useTestHook = createCallbackHook(testCallback)
96
+ const {result} = renderHook(() => useTestHook())
97
+
98
+ const response = result.current('/users', 'POST', {name: 'Test User'})
99
+
100
+ expect(response).toEqual({
101
+ url: 'p/users',
102
+ method: 'POST',
103
+ data: {name: 'Test User'},
104
+ })
105
+ })
106
+ })
@@ -0,0 +1,15 @@
1
+ import {type SanityInstance} from '@sanity/sdk'
2
+ import {useCallback} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ export function createCallbackHook<TParams extends unknown[], TReturn>(
7
+ callback: (instance: SanityInstance, ...params: TParams) => TReturn,
8
+ ): () => (...params: TParams) => TReturn {
9
+ function useHook() {
10
+ const instance = useSanityInstance()
11
+ return useCallback((...params: TParams) => callback(instance, ...params), [instance])
12
+ }
13
+
14
+ return useHook
15
+ }
@@ -0,0 +1,130 @@
1
+ import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {throwError} from 'rxjs'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {useSanityInstance} from '../context/useSanityInstance'
7
+ import {createStateSourceHook} from './createStateSourceHook'
8
+
9
+ // Mock the useSanityInstance hook
10
+ vi.mock('../context/useSanityInstance', () => ({
11
+ useSanityInstance: vi.fn(),
12
+ }))
13
+
14
+ describe('createStateSourceHook', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks()
17
+ })
18
+
19
+ it('should create a hook that provides access to state source', () => {
20
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
21
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
22
+
23
+ const mockState = {count: 0}
24
+ const mockSubscribe = vi.fn()
25
+ const mockGetCurrent = vi.fn(() => mockState)
26
+
27
+ const stateSourceFactory = vi.fn().mockReturnValue({
28
+ subscribe: mockSubscribe,
29
+ getCurrent: mockGetCurrent,
30
+ observable: throwError(() => new Error('unexpected usage of observable')),
31
+ })
32
+
33
+ const useTestHook = createStateSourceHook(stateSourceFactory)
34
+ const {result} = renderHook(() => useTestHook(0))
35
+
36
+ expect(stateSourceFactory).toHaveBeenCalledWith(mockInstance, 0)
37
+ expect(mockGetCurrent).toHaveBeenCalled()
38
+ expect(result.current).toBe(mockState)
39
+ })
40
+
41
+ it('should recreate state source when params change', () => {
42
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
43
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
44
+
45
+ const subscribe = vi.fn()
46
+ const stateSourceFactory = vi.fn((_instance: SanityInstance, count: number) => ({
47
+ subscribe,
48
+ getCurrent: () => count,
49
+ observable: throwError(() => new Error('unexpected usage of observable')),
50
+ }))
51
+
52
+ const useTestHook = createStateSourceHook(stateSourceFactory)
53
+ const {result, rerender} = renderHook(({count}) => useTestHook(count), {
54
+ initialProps: {count: 0},
55
+ })
56
+
57
+ expect(result.current).toEqual(0)
58
+ expect(stateSourceFactory).toHaveBeenCalledWith(mockInstance, 0)
59
+
60
+ rerender({count: 1})
61
+
62
+ expect(result.current).toEqual(1)
63
+ expect(stateSourceFactory).toHaveBeenCalledWith(mockInstance, 1)
64
+ expect(stateSourceFactory).toHaveBeenCalledTimes(2)
65
+ })
66
+
67
+ it('should recreate state source when instance changes', () => {
68
+ const mockInstance1 = createSanityInstance({projectId: 'p1', dataset: 'd'})
69
+ const mockInstance2 = createSanityInstance({projectId: 'p2', dataset: 'd'})
70
+
71
+ vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance1)
72
+
73
+ const stateSourceFactory = vi.fn((instance: SanityInstance) => ({
74
+ subscribe: vi.fn(),
75
+ getCurrent: () => instance.identity.projectId,
76
+ observable: throwError(() => new Error('unexpected usage of observable')),
77
+ }))
78
+
79
+ const useTestHook = createStateSourceHook(stateSourceFactory)
80
+ const {result, rerender} = renderHook(() => useTestHook())
81
+
82
+ expect(result.current).toEqual('p1')
83
+
84
+ vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance2)
85
+ rerender()
86
+
87
+ expect(result.current).toEqual('p2')
88
+ expect(stateSourceFactory).toHaveBeenCalledTimes(2)
89
+ })
90
+
91
+ it('should handle subscription functionality', () => {
92
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
93
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
94
+
95
+ const mockSubscribe = vi.fn()
96
+ const mockGetCurrent = vi.fn().mockReturnValue({value: 'initial'})
97
+
98
+ const stateSourceFactory = vi.fn(() => ({
99
+ subscribe: mockSubscribe,
100
+ getCurrent: mockGetCurrent,
101
+ observable: throwError(() => new Error('unexpected usage of observable')),
102
+ }))
103
+
104
+ const useTestHook = createStateSourceHook(stateSourceFactory)
105
+ renderHook(() => useTestHook())
106
+
107
+ expect(mockSubscribe).toHaveBeenCalled()
108
+ const subscriber = mockSubscribe.mock.calls[0][0]
109
+ expect(typeof subscriber).toBe('function')
110
+ })
111
+
112
+ it('should handle multiple parameters', () => {
113
+ const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
114
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
115
+
116
+ const stateSourceFactory = vi.fn(
117
+ (_instance: SanityInstance, param1: string, param2: number) => ({
118
+ subscribe: vi.fn(),
119
+ getCurrent: () => `${param1}_${param2}`,
120
+ observable: throwError(() => new Error('unexpected usage of observable')),
121
+ }),
122
+ )
123
+
124
+ const useTestHook = createStateSourceHook(stateSourceFactory)
125
+ const {result} = renderHook(() => useTestHook('test', 123))
126
+
127
+ expect(result.current).toEqual('test_123')
128
+ expect(stateSourceFactory).toHaveBeenCalledWith(mockInstance, 'test', 123)
129
+ })
130
+ })
@@ -0,0 +1,21 @@
1
+ import {type SanityInstance, type StateSource} from '@sanity/sdk'
2
+ import {useMemo, useSyncExternalStore} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ export function createStateSourceHook<TParams extends unknown[], TState>(
7
+ stateSourceFactory: (instance: SanityInstance, ...params: TParams) => StateSource<TState>,
8
+ ): (...params: TParams) => TState {
9
+ function useHook(...params: TParams) {
10
+ const instance = useSanityInstance()
11
+ const {subscribe, getCurrent} = useMemo(
12
+ () => stateSourceFactory(instance, ...params),
13
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
+ [instance, ...params],
15
+ )
16
+
17
+ return useSyncExternalStore(subscribe, getCurrent)
18
+ }
19
+
20
+ return useHook
21
+ }
@@ -0,0 +1,175 @@
1
+ import {type DocumentHandle, getPreviewState, type PreviewValue, resolvePreview} from '@sanity/sdk'
2
+ import {act, render, screen} from '@testing-library/react'
3
+ import {Suspense, useRef} from 'react'
4
+ import {type Mock} from 'vitest'
5
+
6
+ import {usePreview} from './usePreview'
7
+
8
+ // Mock IntersectionObserver
9
+ const mockIntersectionObserver = vi.fn()
10
+ let intersectionObserverCallback: (entries: IntersectionObserverEntry[]) => void
11
+
12
+ beforeAll(() => {
13
+ vi.stubGlobal(
14
+ 'IntersectionObserver',
15
+ class {
16
+ constructor(callback: (entries: IntersectionObserverEntry[]) => void) {
17
+ intersectionObserverCallback = callback
18
+ mockIntersectionObserver(callback)
19
+ }
20
+ observe = vi.fn()
21
+ disconnect = vi.fn()
22
+ },
23
+ )
24
+ })
25
+
26
+ // Mock the preview store
27
+ vi.mock('@sanity/sdk', () => {
28
+ const getCurrent = vi.fn()
29
+ const subscribe = vi.fn()
30
+
31
+ return {
32
+ resolvePreview: vi.fn(),
33
+ getPreviewState: vi.fn().mockReturnValue({getCurrent, subscribe}),
34
+ }
35
+ })
36
+
37
+ vi.mock('../context/useSanityInstance', () => ({
38
+ useSanityInstance: () => ({}),
39
+ }))
40
+
41
+ const mockDocument: DocumentHandle = {
42
+ _id: 'doc1',
43
+ _type: 'exampleType',
44
+ }
45
+
46
+ function TestComponent({document}: {document: DocumentHandle}) {
47
+ const ref = useRef(null)
48
+ const {results, isPending} = usePreview({document, ref})
49
+
50
+ return (
51
+ <div ref={ref}>
52
+ <h1>{results?.title}</h1>
53
+ <p>{results?.subtitle}</p>
54
+ {isPending && <div>Pending...</div>}
55
+ </div>
56
+ )
57
+ }
58
+
59
+ describe('usePreview', () => {
60
+ let getCurrent: Mock
61
+ let subscribe: Mock
62
+
63
+ beforeEach(() => {
64
+ // @ts-expect-error mock does not need param
65
+ getCurrent = getPreviewState().getCurrent as Mock
66
+ // @ts-expect-error mock does not need param
67
+ subscribe = getPreviewState().subscribe as Mock
68
+
69
+ // Reset all mocks between tests
70
+ getCurrent.mockReset()
71
+ subscribe.mockReset()
72
+ mockIntersectionObserver.mockReset()
73
+ })
74
+
75
+ test('it only subscribes when element is visible', async () => {
76
+ // Setup initial state
77
+ getCurrent.mockReturnValue({
78
+ results: {title: 'Initial Title', subtitle: 'Initial Subtitle'},
79
+ isPending: false,
80
+ })
81
+ const eventsUnsubscribe = vi.fn()
82
+ subscribe.mockImplementation(() => eventsUnsubscribe)
83
+
84
+ render(
85
+ <Suspense fallback={<div>Loading...</div>}>
86
+ <TestComponent document={mockDocument} />
87
+ </Suspense>,
88
+ )
89
+
90
+ // Initially, element is not intersecting
91
+ expect(screen.getByText('Initial Title')).toBeInTheDocument()
92
+ expect(subscribe).not.toHaveBeenCalled()
93
+
94
+ // Simulate element becoming visible
95
+ await act(async () => {
96
+ intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
97
+ })
98
+
99
+ // After element becomes visible, events subscription should be active
100
+ expect(subscribe).toHaveBeenCalled()
101
+ expect(eventsUnsubscribe).not.toHaveBeenCalled()
102
+
103
+ // Simulate element becoming hidden
104
+ await act(async () => {
105
+ intersectionObserverCallback([{isIntersecting: false} as IntersectionObserverEntry])
106
+ })
107
+
108
+ // When hidden, should maintain last known state
109
+ expect(screen.getByText('Initial Title')).toBeInTheDocument()
110
+ expect(eventsUnsubscribe).toHaveBeenCalled()
111
+ })
112
+
113
+ test.skip('it suspends and resolves data when element becomes visible', async () => {
114
+ // Initial setup with pending state
115
+ getCurrent.mockReturnValueOnce([null, true])
116
+ const resolvePromise = Promise.resolve<PreviewValue>({
117
+ title: 'Resolved Title',
118
+ subtitle: 'Resolved Subtitle',
119
+ media: null,
120
+ })
121
+ ;(resolvePreview as Mock).mockReturnValueOnce(resolvePromise)
122
+
123
+ let subscriber: () => void
124
+ subscribe.mockImplementation((sub: () => void) => {
125
+ subscriber = sub
126
+ return vi.fn()
127
+ })
128
+
129
+ render(
130
+ <Suspense fallback={<div>Loading...</div>}>
131
+ <TestComponent document={mockDocument} />
132
+ </Suspense>,
133
+ )
134
+
135
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
136
+
137
+ // Simulate element becoming visible
138
+ await act(async () => {
139
+ intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
140
+ await resolvePromise
141
+ getCurrent.mockReturnValue({
142
+ results: {title: 'Resolved Title', subtitle: 'Resolved Subtitle'},
143
+ isPending: false,
144
+ })
145
+ subscriber?.()
146
+ })
147
+
148
+ expect(screen.getByText('Resolved Title')).toBeInTheDocument()
149
+ expect(screen.getByText('Resolved Subtitle')).toBeInTheDocument()
150
+ })
151
+
152
+ test('it handles environments without IntersectionObserver', async () => {
153
+ // Temporarily remove IntersectionObserver
154
+ const originalIntersectionObserver = window.IntersectionObserver
155
+ // @ts-expect-error - Intentionally removing IntersectionObserver
156
+ delete window.IntersectionObserver
157
+
158
+ getCurrent.mockReturnValue({
159
+ results: {title: 'Fallback Title', subtitle: 'Fallback Subtitle'},
160
+ isPending: false,
161
+ })
162
+ subscribe.mockImplementation(() => vi.fn())
163
+
164
+ render(
165
+ <Suspense fallback={<div>Loading...</div>}>
166
+ <TestComponent document={mockDocument} />
167
+ </Suspense>,
168
+ )
169
+
170
+ expect(screen.getByText('Fallback Title')).toBeInTheDocument()
171
+
172
+ // Restore IntersectionObserver
173
+ window.IntersectionObserver = originalIntersectionObserver
174
+ })
175
+ })