Mirror: The spec-compliant minimum of client-side GraphQL.
1import { describe, it, expect } from 'vitest';
2import * as graphql16 from 'graphql16';
3
4import type { DocumentNode } from '../ast';
5import { parse } from '../parser';
6import { print, printString, printBlockString } from '../printer';
7import kitchenSinkAST from './fixtures/kitchen_sink.json';
8import { Kind, OperationTypeNode } from 'src/kind';
9
10function dedentString(string: string) {
11 const trimmedStr = string
12 .replace(/^\n*/m, '') // remove leading newline
13 .replace(/[ \t\n]*$/, ''); // remove trailing spaces and tabs
14 // fixes indentation by removing leading spaces and tabs from each line
15 let indent = '';
16 for (const char of trimmedStr) {
17 if (char !== ' ' && char !== '\t') {
18 break;
19 }
20 indent += char;
21 }
22
23 return trimmedStr.replace(RegExp('^' + indent, 'mg'), ''); // remove indent
24}
25
26function dedent(strings: readonly string[], ...values: unknown[]) {
27 let str = strings[0];
28 for (let i = 1; i < strings.length; ++i) str += values[i - 1] + strings[i]; // interpolation
29 return dedentString(str);
30}
31
32describe('printString', () => {
33 it('prints strings as expected', () => {
34 expect(printString('test')).toEqual('"test"');
35 expect(printString('\n')).toEqual('"\\n"');
36 });
37});
38
39describe('printBlockString', () => {
40 it('prints block strings as expected', () => {
41 expect(printBlockString('test')).toEqual('"""\ntest\n"""');
42 expect(printBlockString('\n')).toEqual('"""\n\n\n"""');
43 expect(printBlockString('"""')).toEqual('"""\n\\"""\n"""');
44 });
45});
46
47describe('print', () => {
48 it('prints the kitchen sink document like graphql.js does', () => {
49 const doc = print(kitchenSinkAST);
50 expect(doc).toMatchSnapshot();
51 expect(doc).toEqual(graphql16.print(kitchenSinkAST));
52 });
53
54 it('prints minimal ast', () => {
55 expect(
56 print({
57 kind: 'Field',
58 name: { kind: 'Name', value: 'foo' },
59 } as any)
60 ).toBe('foo');
61
62 expect(
63 print({
64 kind: 'Name',
65 value: 'foo',
66 } as any)
67 ).toBe('foo');
68
69 expect(
70 print({
71 kind: 'Document',
72 definitions: [],
73 } as any)
74 ).toBe('');
75 });
76
77 it('prints integers and floats', () => {
78 expect(
79 print({
80 kind: 'IntValue',
81 value: '12',
82 } as any)
83 ).toBe('12');
84
85 expect(
86 print({
87 kind: 'FloatValue',
88 value: '12e2',
89 } as any)
90 ).toBe('12e2');
91 });
92
93 it('prints lists of values', () => {
94 expect(
95 print({
96 kind: 'ListValue',
97 values: [{ kind: 'NullValue' }],
98 } as any)
99 ).toBe('[null]');
100 });
101
102 it('prints types', () => {
103 expect(
104 print({
105 kind: 'ListType',
106 type: {
107 kind: 'NonNullType',
108 type: {
109 kind: 'NamedType',
110 name: {
111 kind: 'Name',
112 value: 'Type',
113 },
114 },
115 },
116 } as any)
117 ).toBe('[Type!]');
118 });
119
120 // NOTE: The shim won't throw for invalid AST nodes
121 it('returns empty strings for invalid AST', () => {
122 const badAST = { random: 'Data' };
123 expect(print(badAST as any)).toBe('');
124 });
125
126 it('correctly prints non-query operations without name', () => {
127 const queryASTShorthanded = parse('query { id, name }');
128 expect(print(queryASTShorthanded)).toBe(dedent`
129 {
130 id
131 name
132 }
133 `);
134
135 const mutationAST = parse('mutation { id, name }');
136 expect(print(mutationAST)).toBe(dedent`
137 mutation {
138 id
139 name
140 }
141 `);
142
143 const queryASTWithArtifacts = parse('query ($foo: TestType) @testDirective { id, name }');
144 expect(print(queryASTWithArtifacts)).toBe(dedent`
145 query ($foo: TestType) @testDirective {
146 id
147 name
148 }
149 `);
150
151 const mutationASTWithArtifacts = parse('mutation ($foo: TestType) @testDirective { id, name }');
152 expect(print(mutationASTWithArtifacts)).toBe(dedent`
153 mutation ($foo: TestType) @testDirective {
154 id
155 name
156 }
157 `);
158 });
159
160 it('prints query with variable directives', () => {
161 const queryASTWithVariableDirective = parse(
162 'query ($foo: TestType = {a: 123} @testDirective(if: true) @test) { id }'
163 );
164 expect(print(queryASTWithVariableDirective)).toBe(dedent`
165 query ($foo: TestType = {a: 123} @testDirective(if: true) @test) {
166 id
167 }
168 `);
169 });
170
171 it('keeps arguments on one line if line is short (<= 80 chars)', () => {
172 const printed = print(parse('{trip(wheelchair:false arriveBy:false){dateTime}}'));
173
174 expect(printed).toBe(
175 dedent`
176 {
177 trip(wheelchair: false, arriveBy: false) {
178 dateTime
179 }
180 }
181 `
182 );
183 });
184
185 it('Handles empty array selections', () => {
186 const document: DocumentNode = {
187 kind: Kind.DOCUMENT,
188 definitions: [
189 {
190 kind: Kind.OPERATION_DEFINITION,
191 operation: OperationTypeNode.QUERY,
192 name: undefined,
193 selectionSet: {
194 kind: Kind.SELECTION_SET,
195 selections: [
196 {
197 kind: Kind.FIELD,
198 name: { kind: Kind.NAME, value: 'id' },
199 alias: undefined,
200 arguments: [],
201 directives: [],
202 selectionSet: { kind: Kind.SELECTION_SET, selections: [] },
203 },
204 ],
205 },
206 variableDefinitions: [],
207 },
208 ],
209 };
210
211 expect(print(document)).toBe(
212 dedent`
213 {
214 id
215 }
216 `
217 );
218 });
219});