1
+ /**
2
+ * @jest -environment jsdom
3
+ */
4
+ import 'isomorphic-fetch'
5
+ import { FetchRequest } from '../src/fetch_request'
6
+ import { FetchResponse } from '../src/fetch_response'
7
+
8
+ jest . mock ( '../src/lib/utils' , ( ) => {
9
+ const originalModule = jest . requireActual ( '../src/lib/utils' )
10
+ return {
11
+ __esModule : true ,
12
+ ...originalModule ,
13
+ getCookie : jest . fn ( ) . mockReturnValue ( 'mock-csrf-token' ) ,
14
+ metaContent : jest . fn ( )
15
+ }
16
+ } )
17
+
18
+ describe ( 'perform' , ( ) => {
19
+ test ( 'request is performed with 200' , async ( ) => {
20
+ const mockResponse = new Response ( "success!" , { status : 200 } )
21
+ window . fetch = jest . fn ( ) . mockResolvedValue ( mockResponse )
22
+
23
+ const testRequest = new FetchRequest ( "get" , "localhost" )
24
+ const testResponse = await testRequest . perform ( )
25
+
26
+ expect ( window . fetch ) . toHaveBeenCalledTimes ( 1 )
27
+ expect ( window . fetch ) . toHaveBeenCalledWith ( "localhost" , testRequest . fetchOptions )
28
+ expect ( testResponse ) . toStrictEqual ( new FetchResponse ( mockResponse ) )
29
+ } )
30
+
31
+ test ( 'request is performed with 401' , async ( ) => {
32
+ const mockResponse = new Response ( undefined , { status : 401 , headers : { 'WWW-Authenticate' : 'https://localhost/login' } } )
33
+ window . fetch = jest . fn ( ) . mockResolvedValue ( mockResponse )
34
+
35
+ delete window . location
36
+ window . location = new URL ( 'https://www.example.com' )
37
+ expect ( window . location . href ) . toBe ( 'https://www.example.com/' )
38
+
39
+ const testRequest = new FetchRequest ( "get" , "https://localhost" )
40
+ expect ( testRequest . perform ( ) ) . rejects . toBe ( 'https://localhost/login' )
41
+
42
+ testRequest . perform ( ) . catch ( ( ) => {
43
+ expect ( window . location . href ) . toBe ( 'https://localhost/login' )
44
+ } )
45
+ } )
46
+
47
+ test ( 'turbo stream request automatically calls renderTurboStream' , async ( ) => {
48
+ const mockResponse = new Response ( '' , { status : 200 , headers : { 'Content-Type' : 'text/vnd.turbo-stream.html' } } )
49
+ window . fetch = jest . fn ( ) . mockResolvedValue ( mockResponse )
50
+ jest . spyOn ( FetchResponse . prototype , "ok" , "get" ) . mockReturnValue ( true )
51
+ jest . spyOn ( FetchResponse . prototype , "isTurboStream" , "get" ) . mockReturnValue ( true )
52
+ const renderSpy = jest . spyOn ( FetchResponse . prototype , "renderTurboStream" ) . mockImplementation ( )
53
+
54
+ const testRequest = new FetchRequest ( "get" , "localhost" )
55
+ await testRequest . perform ( )
56
+
57
+ expect ( renderSpy ) . toHaveBeenCalledTimes ( 1 )
58
+ } )
59
+ } )
60
+
61
+ test ( 'treat method name case-insensitive' , async ( ) => {
62
+ const methodNames = [ "gEt" , "GeT" , "get" , "GET" ]
63
+ for ( const methodName of methodNames ) {
64
+ const testRequest = new FetchRequest ( methodName , "localhost" )
65
+ expect ( testRequest . fetchOptions . method ) . toBe ( "GET" )
66
+ }
67
+ } )
68
+
69
+ describe ( 'header handling' , ( ) => {
70
+ const defaultHeaders = {
71
+ 'X-Requested-With' : 'XMLHttpRequest' ,
72
+ 'X-CSRF-Token' : 'mock-csrf-token' ,
73
+ 'Accept' : 'text/html, application/xhtml+xml'
74
+ }
75
+ describe ( 'responseKind' , ( ) => {
76
+ test ( 'none' , async ( ) => {
77
+ const defaultRequest = new FetchRequest ( "get" , "localhost" )
78
+ expect ( defaultRequest . fetchOptions . headers )
79
+ . toStrictEqual ( defaultHeaders )
80
+ } )
81
+ test ( 'html' , async ( ) => {
82
+ const htmlRequest = new FetchRequest ( "get" , "localhost" , { responseKind : 'html' } )
83
+ expect ( htmlRequest . fetchOptions . headers )
84
+ . toStrictEqual ( defaultHeaders )
85
+ } )
86
+ test ( 'json' , async ( ) => {
87
+ const jsonRequest = new FetchRequest ( "get" , "localhost" , { responseKind : 'json' } )
88
+ expect ( jsonRequest . fetchOptions . headers )
89
+ . toStrictEqual ( { ...defaultHeaders , 'Accept' : 'application/json' } )
90
+ } )
91
+ test ( 'turbo-stream' , async ( ) => {
92
+ const turboRequest = new FetchRequest ( "get" , "localhost" , { responseKind : 'turbo-stream' } )
93
+ expect ( turboRequest . fetchOptions . headers )
94
+ . toStrictEqual ( { ...defaultHeaders , 'Accept' : 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml' } )
95
+ } )
96
+ test ( 'invalid' , async ( ) => {
97
+ const invalidResponseKindRequest = new FetchRequest ( "get" , "localhost" , { responseKind : 'exotic' } )
98
+ expect ( invalidResponseKindRequest . fetchOptions . headers )
99
+ . toStrictEqual ( { ...defaultHeaders , 'Accept' : '*/*' } )
100
+ } )
101
+ } )
102
+
103
+ describe ( 'contentType' , ( ) => {
104
+ test ( 'is added to headers' , ( ) => {
105
+ const customRequest = new FetchRequest ( "get" , "localhost/test.json" , { contentType : 'any/thing' } )
106
+ expect ( customRequest . fetchOptions . headers )
107
+ . toStrictEqual ( { ...defaultHeaders , "Content-Type" : 'any/thing' } )
108
+ } )
109
+ test ( 'is not set by formData' , ( ) => {
110
+ const formData = new FormData ( )
111
+ formData . append ( "this" , "value" )
112
+ const formDataRequest = new FetchRequest ( "get" , "localhost" , { body : formData } )
113
+ expect ( formDataRequest . fetchOptions . headers )
114
+ . toStrictEqual ( defaultHeaders )
115
+ } )
116
+ test ( 'is set by file body' , ( ) => {
117
+ const file = new File ( [ "contenxt" ] , "file.txt" , { type : "text/plain" } )
118
+ const fileRequest = new FetchRequest ( "get" , "localhost" , { body : file } )
119
+ expect ( fileRequest . fetchOptions . headers )
120
+ . toStrictEqual ( { ...defaultHeaders , "Content-Type" : "text/plain" } )
121
+ } )
122
+ test ( 'is set by json body' , ( ) => {
123
+ const jsonRequest = new FetchRequest ( "get" , "localhost" , { body : { some : "json" } } )
124
+ expect ( jsonRequest . fetchOptions . headers )
125
+ . toStrictEqual ( { ...defaultHeaders , "Content-Type" : "application/json" } )
126
+ } )
127
+ } )
128
+
129
+ test ( 'additional headers are appended' , ( ) => {
130
+ const request = new FetchRequest ( "get" , "localhost" , { contentType : "application/json" , headers : { custom : "Header" } } )
131
+ expect ( request . fetchOptions . headers )
132
+ . toStrictEqual ( { ...defaultHeaders , custom : "Header" , "Content-Type" : "application/json" } )
133
+ request . addHeader ( "test" , "header" )
134
+ expect ( request . fetchOptions . headers )
135
+ . toStrictEqual ( { ...defaultHeaders , custom : "Header" , "Content-Type" : "application/json" , "test" : "header" } )
136
+ } )
137
+
138
+ test ( 'headers win over contentType' , ( ) => {
139
+ const request = new FetchRequest ( "get" , "localhost" , { contentType : "application/json" , headers : { "Content-Type" : "this/overwrites" } } )
140
+ expect ( request . fetchOptions . headers )
141
+ . toStrictEqual ( { ...defaultHeaders , "Content-Type" : "this/overwrites" } )
142
+ } )
143
+
144
+ test ( 'serializes JSON to String' , ( ) => {
145
+ const jsonBody = { some : "json" }
146
+ let request
147
+ request = new FetchRequest ( "get" , "localhost" , { body : jsonBody , contentType : "application/json" } )
148
+ expect ( request . fetchOptions . body ) . toBe ( JSON . stringify ( jsonBody ) )
149
+
150
+ request = new FetchRequest ( "get" , "localhost" , { body : jsonBody } )
151
+ expect ( request . fetchOptions . body ) . toBe ( JSON . stringify ( jsonBody ) )
152
+ } )
153
+
154
+ test ( 'not serializes JSON with explicit different contentType' , ( ) => {
155
+ const jsonBody = { some : "json" }
156
+ const request = new FetchRequest ( "get" , "localhost" , { body : jsonBody , contentType : "not/json" } )
157
+ expect ( request . fetchOptions . body ) . toBe ( jsonBody )
158
+ } )
159
+
160
+ test ( 'set redirect' , ( ) => {
161
+ let request
162
+ const redirectTypes = [ "follow" , "error" , "manual" ]
163
+ for ( const redirect of redirectTypes ) {
164
+ request = new FetchRequest ( "get" , "localhost" , { redirect } )
165
+ expect ( request . fetchOptions . redirect ) . toBe ( redirect )
166
+ }
167
+
168
+ request = new FetchRequest ( "get" , "localhost" )
169
+ expect ( request . fetchOptions . redirect ) . toBe ( "follow" )
170
+
171
+ // maybe check for valid values and default to follow?
172
+ // https://developer.mozilla.org/en-US/docs/Web/API/Request/redirect
173
+ request = new FetchRequest ( "get" , "localhost" , { redirect : "nonsense" } )
174
+ expect ( request . fetchOptions . redirect ) . toBe ( "nonsense" )
175
+ } )
176
+
177
+ test ( 'sets signal' , ( ) => {
178
+ let request
179
+ request = new FetchRequest ( "get" , "localhost" )
180
+ expect ( request . fetchOptions . signal ) . toBe ( undefined )
181
+
182
+ request = new FetchRequest ( "get" , "localhost" , { signal : "signal" } )
183
+ expect ( request . fetchOptions . signal ) . toBe ( "signal" )
184
+ } )
185
+
186
+ test ( 'has fixed credentials setting which cannot be changed' , ( ) => {
187
+ let request
188
+ request = new FetchRequest ( "get" , "localhost" )
189
+ expect ( request . fetchOptions . credentials ) . toBe ( 'same-origin' )
190
+
191
+ // has no effect
192
+ request = new FetchRequest ( "get" , "localhost" , { credentials : "omit" } )
193
+ expect ( request . fetchOptions . credentials ) . toBe ( 'same-origin' )
194
+ } )
195
+ } )
196
+
197
+ describe ( 'query params are parsed' , ( ) => {
198
+ test ( 'anchors are rejected' , ( ) => {
199
+ const testRequest = new FetchRequest ( "post" , "localhost/test?a=1&b=2#anchor" , { query : { c : 3 } } )
200
+ expect ( testRequest . url ) . toBe ( "localhost/test?a=1&b=2&c=3" )
201
+ // const brokenRequest = new FetchRequest("post", "localhost/test#anchor", { query: { a: 1, b: 2, c: 3 } })
202
+ // expect(brokenRequest.url).toBe("localhost/test?a=1&b=2&c=3")
203
+ } )
204
+ test ( 'url and options are merged' , ( ) => {
205
+ const urlAndOptionRequest = new FetchRequest ( "post" , "localhost/test?a=1&b=2" , { query : { c : 3 } } )
206
+ expect ( urlAndOptionRequest . url ) . toBe ( "localhost/test?a=1&b=2&c=3" )
207
+ } )
208
+ test ( 'only url' , ( ) => {
209
+ const urlRequest = new FetchRequest ( "post" , "localhost/test?a=1&b=2" )
210
+ expect ( urlRequest . url ) . toBe ( "localhost/test?a=1&b=2" )
211
+ } )
212
+ test ( 'only options' , ( ) => {
213
+ const optionRequest = new FetchRequest ( "post" , "localhost/test" , { query : { c : 3 } } )
214
+ expect ( optionRequest . url ) . toBe ( "localhost/test?c=3" )
215
+ } )
216
+ test ( 'options accept formData' , ( ) => {
217
+ const formData = new FormData ( )
218
+ formData . append ( "a" , 1 )
219
+
220
+ const urlAndOptionRequest = new FetchRequest ( "post" , "localhost/test" , { query : formData } )
221
+ expect ( urlAndOptionRequest . url ) . toBe ( "localhost/test?a=1" )
222
+ } )
223
+ test ( 'options accept urlSearchParams' , ( ) => {
224
+ const urlSearchParams = new URLSearchParams ( )
225
+ urlSearchParams . append ( "a" , 1 )
226
+
227
+ const urlAndOptionRequest = new FetchRequest ( "post" , "localhost/test" , { query : urlSearchParams } )
228
+ expect ( urlAndOptionRequest . url ) . toBe ( "localhost/test?a=1" )
229
+ } )
230
+ test ( 'handles empty query' , ( ) => {
231
+ const emptyQueryRequest = new FetchRequest ( "get" , "localhost/test" )
232
+ expect ( emptyQueryRequest . url ) . toBe ( "localhost/test" )
233
+ } )
234
+ } )
0 commit comments