1// @vitest-environment jsdom
2
3import { expect, describe, it } from 'vitest';
4import { Kind } from '@0no-co/graphql.web';
5import { makeOperation } from '../utils/operation';
6import { queryOperation, mutationOperation } from '../test-utils';
7import { makeFetchBody, makeFetchURL, makeFetchOptions } from './fetchOptions';
8
9describe('makeFetchBody', () => {
10 it('creates a fetch body', () => {
11 const body = makeFetchBody(queryOperation);
12 expect(body).toMatchInlineSnapshot(`
13 {
14 "documentId": undefined,
15 "extensions": undefined,
16 "operationName": "getUser",
17 "query": "query getUser($name: String) {
18 user(name: $name) {
19 id
20 firstName
21 lastName
22 }
23 }",
24 "variables": {
25 "name": "Clara",
26 },
27 }
28 `);
29 });
30
31 it('omits the query property when APQ is set', () => {
32 const apqOperation = makeOperation(queryOperation.kind, queryOperation);
33
34 apqOperation.extensions = {
35 ...apqOperation.extensions,
36 persistedQuery: {
37 version: 1,
38 sha256Hash: '[test]',
39 },
40 };
41
42 expect(makeFetchBody(apqOperation).query).toBe(undefined);
43
44 apqOperation.extensions.persistedQuery!.miss = true;
45 expect(makeFetchBody(apqOperation).query).not.toBe(undefined);
46 });
47
48 it('omits the query property when query is a persisted document', () => {
49 // A persisted documents is one that carries a `documentId` property and
50 // has no definitions
51 const persistedOperation = makeOperation(queryOperation.kind, {
52 ...queryOperation,
53 query: {
54 kind: Kind.DOCUMENT,
55 definitions: [],
56 documentId: 'TestDocumentId',
57 },
58 });
59
60 expect(makeFetchBody(persistedOperation).query).toBe(undefined);
61 expect(makeFetchBody(persistedOperation).documentId).toBe('TestDocumentId');
62 });
63});
64
65describe('makeFetchURL', () => {
66 it('returns the URL by default', () => {
67 const body = makeFetchBody(queryOperation);
68 expect(makeFetchURL(queryOperation, body)).toBe(
69 'http://localhost:3000/graphql'
70 );
71 });
72
73 it('returns the URL by default when only a path is provided', () => {
74 const operation = makeOperation(queryOperation.kind, queryOperation, {
75 url: '/graphql',
76 });
77 const body = makeFetchBody(operation);
78 expect(makeFetchURL(operation, body)).toBe('/graphql');
79 });
80
81 it('returns a query parameter URL when GET is preferred', () => {
82 const operation = makeOperation(queryOperation.kind, queryOperation, {
83 ...queryOperation.context,
84 preferGetMethod: true,
85 });
86
87 const body = makeFetchBody(operation);
88 expect(makeFetchURL(operation, body)).toMatchInlineSnapshot(
89 '"http://localhost:3000/graphql?query=query+getUser%28%24name%3A+String%29+%7B%0A++user%28name%3A+%24name%29+%7B%0A++++id%0A++++firstName%0A++++lastName%0A++%7D%0A%7D&operationName=getUser&variables=%7B%22name%22%3A%22Clara%22%7D"'
90 );
91 });
92
93 it('returns a query parameter URL when GET is preferred and only a path is provided', () => {
94 const operation = makeOperation(queryOperation.kind, queryOperation, {
95 url: '/graphql',
96 preferGetMethod: true,
97 });
98
99 const body = makeFetchBody(operation);
100 expect(makeFetchURL(operation, body)).toMatchInlineSnapshot(
101 '"/graphql?query=query+getUser%28%24name%3A+String%29+%7B%0A++user%28name%3A+%24name%29+%7B%0A++++id%0A++++firstName%0A++++lastName%0A++%7D%0A%7D&operationName=getUser&variables=%7B%22name%22%3A%22Clara%22%7D"'
102 );
103 });
104
105 it('returns the URL without query parameters when it exceeds given length', () => {
106 const operation = makeOperation(queryOperation.kind, queryOperation, {
107 ...queryOperation.context,
108 preferGetMethod: true,
109 });
110
111 operation.variables = {
112 ...operation.variables,
113 test: 'x'.repeat(2048),
114 };
115
116 const body = makeFetchBody(operation);
117 expect(makeFetchURL(operation, body)).toBe('http://localhost:3000/graphql');
118 // Resets the `preferGetMethod` field
119 expect(operation.context.preferGetMethod).toBe(false);
120 });
121
122 it('returns the URL without query parameters for mutations', () => {
123 const operation = makeOperation(mutationOperation.kind, mutationOperation, {
124 ...mutationOperation.context,
125 preferGetMethod: true,
126 });
127
128 const body = makeFetchBody(operation);
129 expect(makeFetchURL(operation, body)).toBe('http://localhost:3000/graphql');
130 });
131});
132
133describe('makeFetchOptions', () => {
134 it('creates a JSON request by default', () => {
135 const body = makeFetchBody(queryOperation);
136 expect(makeFetchOptions(queryOperation, body)).toMatchInlineSnapshot(`
137 {
138 "body": "{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}",
139 "headers": {
140 "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed",
141 "content-type": "application/json",
142 },
143 "method": "POST",
144 }
145 `);
146 });
147
148 it('handles the Headers object', () => {
149 const headers = new Headers();
150 headers.append('x-test', 'true');
151 const operation = makeOperation(queryOperation.kind, queryOperation, {
152 ...queryOperation.context,
153 fetchOptions: {
154 headers,
155 },
156 });
157 const body = makeFetchBody(operation);
158
159 expect(makeFetchOptions(operation, body)).toMatchInlineSnapshot(`
160 {
161 "body": "{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}",
162 "headers": {
163 "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed",
164 "content-type": "application/json",
165 "x-test": "true",
166 },
167 "method": "POST",
168 }
169 `);
170 });
171
172 it('handles an Array headers init', () => {
173 const operation = makeOperation(queryOperation.kind, queryOperation, {
174 ...queryOperation.context,
175 fetchOptions: {
176 headers: [['x-test', 'true']],
177 },
178 });
179 const body = makeFetchBody(operation);
180
181 expect(makeFetchOptions(operation, body)).toMatchInlineSnapshot(`
182 {
183 "body": "{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}",
184 "headers": {
185 "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed",
186 "content-type": "application/json",
187 "x-test": "true",
188 },
189 "method": "POST",
190 }
191 `);
192 });
193
194 it('creates a GET request when preferred for query operations', () => {
195 const operation = makeOperation(queryOperation.kind, queryOperation, {
196 ...queryOperation.context,
197 preferGetMethod: 'force',
198 });
199
200 const body = makeFetchBody(operation);
201 expect(makeFetchOptions(operation, body)).toMatchInlineSnapshot(`
202 {
203 "body": undefined,
204 "headers": {
205 "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed",
206 },
207 "method": "GET",
208 }
209 `);
210 });
211
212 it('creates a POST multipart request when a file is detected', () => {
213 const operation = makeOperation(mutationOperation.kind, mutationOperation);
214 operation.variables = {
215 ...operation.variables,
216 file: new Blob(),
217 };
218
219 const body = makeFetchBody(operation);
220 const options = makeFetchOptions(operation, body);
221 expect(options).toMatchInlineSnapshot(`
222 {
223 "body": FormData {},
224 "headers": {
225 "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed",
226 },
227 "method": "POST",
228 }
229 `);
230
231 expect(options.body).toBeInstanceOf(FormData);
232 const form = options.body as FormData;
233
234 expect(JSON.parse(form.get('operations') as string)).toEqual({
235 ...body,
236 variables: {
237 ...body.variables,
238 file: null,
239 },
240 });
241
242 expect(form.get('map')).toMatchInlineSnapshot(`"{"0":["variables.file"]}"`);
243 expect(form.get('0')).toBeInstanceOf(Blob);
244 });
245});