1
- import { describe , expect , test } from 'vitest'
1
+ import { describe , expect , test , afterEach } from 'vitest'
2
2
import { createViewClient , ObservableViewClient , type ViewClientConfig } from '../../src/views'
3
- import nock from 'nock'
4
3
5
4
const apiHost = 'api.sanity.url'
6
5
const apicdnHost = 'apicdn.sanity.url'
7
6
8
7
describe ( 'view client' , async ( ) => {
8
+ const isEdge = typeof EdgeRuntime === 'string'
9
+ let nock : typeof import ( 'nock' ) = ( ( ) => {
10
+ throw new Error ( 'Not supported in EdgeRuntime' )
11
+ } ) as any
12
+ if ( ! isEdge ) {
13
+ const _nock = await import ( 'nock' )
14
+ nock = _nock . default
15
+ }
16
+
17
+ afterEach ( ( ) => {
18
+ if ( ! isEdge ) {
19
+ nock . cleanAll ( )
20
+ }
21
+ } )
22
+
9
23
const defaultConfig : ViewClientConfig = {
10
24
apiHost : `https://${ apiHost } ` ,
11
25
apiCdnHost : `https://${ apicdnHost } ` ,
12
26
apiVersion : '2025-01-01' ,
13
27
}
14
28
29
+ describe ( 'createViewClient' , ( ) => {
30
+ test ( 'can create a view client' , ( ) => {
31
+ const client = createViewClient ( defaultConfig )
32
+ expect ( client ) . toBeDefined ( )
33
+ expect ( client . observable ) . toBeInstanceOf ( ObservableViewClient )
34
+ } )
35
+
36
+ test ( 'can create a view client with additional config options' , ( ) => {
37
+ const config : ViewClientConfig = {
38
+ ...defaultConfig ,
39
+ maxRetries : 5 ,
40
+ retryDelay : ( ) => 1000 , // retryDelay should be a function
41
+ timeout : 30000 ,
42
+ }
43
+ const client = createViewClient ( config )
44
+ expect ( client ) . toBeDefined ( )
45
+ expect ( client . observable ) . toBeInstanceOf ( ObservableViewClient )
46
+ } )
47
+ } )
48
+
15
49
describe ( 'promise client' , ( ) => {
16
- test ( 'uses the correct url for a view resource' , async ( ) => {
50
+ test . skipIf ( isEdge ) ( 'uses the correct url for a view resource' , async ( ) => {
17
51
const client = createViewClient ( defaultConfig )
18
52
const result = [ { _id : 'njgNkngskjg' } ]
19
53
20
- nock ( `https://${ apicdnHost } ` )
54
+ // Mock both hosts to see which one gets called
55
+ const apiMock = nock ( `https://${ apiHost } ` )
56
+ . get ( / .* / )
57
+ . reply ( 200 , { ms : 123 , result} )
58
+
59
+ const apicdnMock = nock ( `https://${ apicdnHost } ` )
21
60
. get (
22
61
`/v2025-01-01/views/vw123/query?query=*%5B_id+%3D%3D+%22view%22%5D%7B_id%7D&returnQuery=false` ,
23
62
)
@@ -28,6 +67,132 @@ describe('view client', async () => {
28
67
29
68
const res = await client . fetch ( 'vw123' , '*[_id == "view"]{_id}' , { } , { } )
30
69
expect ( res ) . toEqual ( result )
70
+
71
+ expect ( apiMock . isDone ( ) ) . toBe ( false )
72
+ expect ( apicdnMock . isDone ( ) ) . toBe ( true )
73
+ } )
74
+
75
+ test . skipIf ( isEdge ) ( 'can fetch without params' , async ( ) => {
76
+ const client = createViewClient ( defaultConfig )
77
+ const result = [ { _id : 'doc1' , title : 'Test Document' } ]
78
+
79
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 150 , result} )
80
+
81
+ const res = await client . fetch ( 'vw456' , '*[_type == "document"]' )
82
+ expect ( res ) . toEqual ( result )
83
+ } )
84
+
85
+ test . skipIf ( isEdge ) ( 'can fetch with params' , async ( ) => {
86
+ const client = createViewClient ( defaultConfig )
87
+ const result = [ { _id : 'doc1' , title : 'Specific Document' } ]
88
+ const params = { docType : 'article' }
89
+
90
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 200 , result} )
91
+
92
+ const res = await client . fetch ( 'vw789' , '*[_type == $docType]' , params )
93
+ expect ( res ) . toEqual ( result )
94
+ } )
95
+
96
+ test . skipIf ( isEdge ) ( 'can fetch with ViewQueryOptions' , async ( ) => {
97
+ const client = createViewClient ( defaultConfig )
98
+ const result = [ { _id : 'doc1' , title : 'Published Document' } ]
99
+
100
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 180 , result} )
101
+
102
+ const res = await client . fetch ( 'vw101' , '*[_type == "page"]' , { } , { perspective : 'published' } )
103
+ expect ( res ) . toEqual ( result )
104
+ } )
105
+
106
+ test . skipIf ( isEdge ) ( 'can fetch with resultSourceMap option' , async ( ) => {
107
+ const client = createViewClient ( defaultConfig )
108
+ const result = [ { _id : 'doc1' , title : 'Document with source map' } ]
109
+ const resultSourceMap = {
110
+ documents : [
111
+ {
112
+ _id : 'doc1' ,
113
+ _type : 'document' ,
114
+ } ,
115
+ ] ,
116
+ paths : [ '$[0]' ] ,
117
+ mappings : {
118
+ '$[0]' : {
119
+ source : {
120
+ document : 0 ,
121
+ path : '' ,
122
+ type : 'documentValue' ,
123
+ } ,
124
+ } ,
125
+ } ,
126
+ }
127
+
128
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 220 , result, resultSourceMap} )
129
+
130
+ const res = await client . fetch ( 'vw202' , '*[_type == "page"]' , { } , { resultSourceMap : true } )
131
+ // By default, the client returns just the result, even with resultSourceMap: true
132
+ // To get the full response including resultSourceMap, use filterResponse: false
133
+ expect ( res ) . toEqual ( result )
134
+ } )
135
+
136
+ test . skipIf ( isEdge ) ( 'can fetch with filterResponse: false to get full response' , async ( ) => {
137
+ const client = createViewClient ( defaultConfig )
138
+ const result = [ { _id : 'doc1' , title : 'Document with full response' } ]
139
+ const resultSourceMap = {
140
+ documents : [
141
+ {
142
+ _id : 'doc1' ,
143
+ _type : 'document' ,
144
+ } ,
145
+ ] ,
146
+ paths : [ '$[0]' ] ,
147
+ mappings : {
148
+ '$[0]' : {
149
+ source : {
150
+ document : 0 ,
151
+ path : '' ,
152
+ type : 'documentValue' ,
153
+ } ,
154
+ } ,
155
+ } ,
156
+ }
157
+
158
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 250 , result, resultSourceMap} )
159
+
160
+ const res = await client . fetch ( 'vw203' , '*[_type == "page"]' , { } , { resultSourceMap : true , filterResponse : false } )
161
+ // With filterResponse: false, the client returns the full response object
162
+ expect ( res ) . toEqual ( { result, resultSourceMap, ms : 250 } )
163
+ } )
164
+
165
+ test ( 'can clone client with withConfig' , ( ) => {
166
+ const client = createViewClient ( defaultConfig )
167
+ const newClient = client . withConfig ( { apiVersion : '2024-12-01' } )
168
+
169
+ expect ( client ) . not . toBe ( newClient )
170
+ expect ( newClient ) . toBeDefined ( )
171
+ expect ( newClient . observable ) . toBeInstanceOf ( ObservableViewClient )
172
+ } )
173
+
174
+ test . skipIf ( isEdge ) ( 'withConfig preserves existing configuration' , async ( ) => {
175
+ const client = createViewClient ( {
176
+ ...defaultConfig ,
177
+ timeout : 5000 ,
178
+ } )
179
+ const newClient = client . withConfig ( { apiVersion : '2024-12-01' } )
180
+ const result = [ { _id : 'test' } ]
181
+
182
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 100 , result} )
183
+
184
+ const res = await newClient . fetch ( 'vw303' , '*' )
185
+ expect ( res ) . toEqual ( result )
186
+ } )
187
+
188
+ test . skipIf ( isEdge ) ( 'always uses CDN for view queries' , async ( ) => {
189
+ const client = createViewClient ( defaultConfig )
190
+ const result = [ { _id : 'cdn-test' } ]
191
+
192
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 50 , result} )
193
+
194
+ const res = await client . fetch ( 'vw404' , '*' )
195
+ expect ( res ) . toEqual ( result )
31
196
} )
32
197
} )
33
198
@@ -37,18 +202,11 @@ describe('view client', async () => {
37
202
expect ( client . observable ) . toBeInstanceOf ( ObservableViewClient )
38
203
} )
39
204
40
- test ( 'uses the correct url for a view resource' , async ( ) => {
205
+ test . skipIf ( isEdge ) ( 'uses the correct url for a view resource' , async ( ) => {
41
206
const client = createViewClient ( defaultConfig )
42
207
const result = [ { _id : 'njgNkngskjg' } ]
43
208
44
- nock ( `https://${ apicdnHost } ` )
45
- . get (
46
- `/v2025-01-01/views/vw123/query?query=*%5B_id+%3D%3D+%22view%22%5D%7B_id%7D&returnQuery=false` ,
47
- )
48
- . reply ( 200 , {
49
- ms : 123 ,
50
- result,
51
- } )
209
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 123 , result} )
52
210
53
211
const req = client . observable . fetch ( 'vw123' , '*[_id == "view"]{_id}' , { } , { } )
54
212
await new Promise ( ( resolve ) => setTimeout ( resolve , 1 ) )
@@ -63,5 +221,165 @@ describe('view client', async () => {
63
221
} )
64
222
} )
65
223
} )
224
+
225
+ test . skipIf ( isEdge ) ( 'observable requests are lazy' , async ( ) => {
226
+ const client = createViewClient ( defaultConfig )
227
+ let didRequest = false
228
+
229
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( ( ) => {
230
+ didRequest = true
231
+ return [ 200 , { ms : 100 , result : [ ] } ]
232
+ } )
233
+
234
+ const req = client . observable . fetch ( 'vw505' , '*' )
235
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1 ) )
236
+
237
+ expect ( didRequest ) . toBe ( false )
238
+
239
+ await new Promise < void > ( ( resolve , reject ) => {
240
+ req . subscribe ( {
241
+ next : ( ) => {
242
+ expect ( didRequest ) . toBe ( true )
243
+ } ,
244
+ error : reject ,
245
+ complete : resolve ,
246
+ } )
247
+ } )
248
+ } )
249
+
250
+ test . skipIf ( isEdge ) ( 'observable requests are cold' , async ( ) => {
251
+ const client = createViewClient ( defaultConfig )
252
+ let requestCount = 0
253
+
254
+ // Mock CDN host
255
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . twice ( ) . reply ( ( ) => {
256
+ requestCount ++
257
+ return [ 200 , { ms : 100 , result : [ { _id : `doc${ requestCount } ` } ] } ]
258
+ } )
259
+
260
+ const req = client . observable . fetch ( 'vw606' , '*' )
261
+
262
+ await new Promise < void > ( ( resolve , reject ) => {
263
+ expect ( requestCount ) . toBe ( 0 )
264
+ req . subscribe ( {
265
+ next : ( ) => {
266
+ expect ( requestCount ) . toBe ( 1 )
267
+ req . subscribe ( {
268
+ next : ( ) => {
269
+ expect ( requestCount ) . toBe ( 2 )
270
+ } ,
271
+ error : reject ,
272
+ complete : resolve ,
273
+ } )
274
+ } ,
275
+ error : reject ,
276
+ } )
277
+ } )
278
+ } )
279
+
280
+ test . skipIf ( isEdge ) ( 'can fetch without params' , async ( ) => {
281
+ const client = createViewClient ( defaultConfig )
282
+ const result = [ { _id : 'obs-doc1' , title : 'Observable Document' } ]
283
+
284
+ // Mock CDN host
285
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 160 , result} )
286
+
287
+ await new Promise < void > ( ( resolve , reject ) => {
288
+ client . observable . fetch ( 'vw707' , '*[_type == "post"]' ) . subscribe ( {
289
+ next : ( res ) => {
290
+ expect ( res ) . toEqual ( result )
291
+ } ,
292
+ error : reject ,
293
+ complete : resolve ,
294
+ } )
295
+ } )
296
+ } )
297
+
298
+ test . skipIf ( isEdge ) ( 'can fetch with params' , async ( ) => {
299
+ const client = createViewClient ( defaultConfig )
300
+ const result = [ { _id : 'obs-doc2' , category : 'tech' } ]
301
+ const params = { cat : 'tech' }
302
+
303
+ // Mock CDN host
304
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 140 , result} )
305
+
306
+ await new Promise < void > ( ( resolve , reject ) => {
307
+ client . observable . fetch ( 'vw808' , '*[category == $cat]' , params ) . subscribe ( {
308
+ next : ( res ) => {
309
+ expect ( res ) . toEqual ( result )
310
+ } ,
311
+ error : reject ,
312
+ complete : resolve ,
313
+ } )
314
+ } )
315
+ } )
316
+
317
+ test . skipIf ( isEdge ) ( 'can fetch with ViewQueryOptions' , async ( ) => {
318
+ const client = createViewClient ( defaultConfig )
319
+ const result = [ { _id : 'obs-doc3' , status : 'draft' } ]
320
+
321
+ // Mock CDN host
322
+ nock ( `https://${ apicdnHost } ` )
323
+ . get ( / .* / )
324
+ . reply ( 200 , { ms : 190 , result} )
325
+
326
+ await new Promise < void > ( ( resolve , reject ) => {
327
+ client . observable . fetch ( 'vw909' , '*[_type == "article"]' , { } , { perspective : 'previewDrafts' } ) . subscribe ( {
328
+ next : ( res ) => {
329
+ expect ( res ) . toEqual ( result )
330
+ } ,
331
+ error : reject ,
332
+ complete : resolve ,
333
+ } )
334
+ } )
335
+ } )
336
+
337
+ test ( 'can clone observable client with withConfig' , ( ) => {
338
+ const client = createViewClient ( defaultConfig )
339
+ const newObservableClient = client . observable . withConfig ( { apiVersion : '2024-11-01' } )
340
+
341
+ expect ( client . observable ) . not . toBe ( newObservableClient )
342
+ expect ( newObservableClient ) . toBeInstanceOf ( ObservableViewClient )
343
+ } )
344
+
345
+ test . skipIf ( isEdge ) ( 'withConfig on observable client preserves existing configuration' , async ( ) => {
346
+ const client = createViewClient ( {
347
+ ...defaultConfig ,
348
+ timeout : 8000 ,
349
+ } )
350
+ const newObservableClient = client . observable . withConfig ( { apiVersion : '2024-11-01' } )
351
+ const result = [ { _id : 'config-test' } ]
352
+
353
+ // Mock CDN host
354
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 80 , result} )
355
+
356
+ await new Promise < void > ( ( resolve , reject ) => {
357
+ newObservableClient . fetch ( 'vw1010' , '*' ) . subscribe ( {
358
+ next : ( res ) => {
359
+ expect ( res ) . toEqual ( result )
360
+ } ,
361
+ error : reject ,
362
+ complete : resolve ,
363
+ } )
364
+ } )
365
+ } )
366
+
367
+ test . skipIf ( isEdge ) ( 'always uses CDN for observable view queries' , async ( ) => {
368
+ const client = createViewClient ( defaultConfig )
369
+ const result = [ { _id : 'obs-cdn-test' } ]
370
+
371
+ // Mock CDN host
372
+ nock ( `https://${ apicdnHost } ` ) . get ( / .* / ) . reply ( 200 , { ms : 60 , result} )
373
+
374
+ await new Promise < void > ( ( resolve , reject ) => {
375
+ client . observable . fetch ( 'vw1111' , '*' ) . subscribe ( {
376
+ next : ( res ) => {
377
+ expect ( res ) . toEqual ( result )
378
+ } ,
379
+ error : reject ,
380
+ complete : resolve ,
381
+ } )
382
+ } )
383
+ } )
66
384
} )
67
385
} )
0 commit comments