···
1
+
import { describe, it, expect } from 'vitest';
2
+
import { Kind, parse, print } from 'graphql';
3
+
import { visit, BREAK } from '../visitor';
5
+
function checkVisitorFnArgs(ast, args, isEdited = false) {
6
+
const [node, key, parent, path, ancestors] = args;
8
+
expect(node).toBeInstanceOf(Object);
9
+
expect(Object.values(Kind)).toContain(node.kind);
11
+
const isRoot = key === undefined;
14
+
expect(node).toEqual(ast);
16
+
expect(parent).toEqual(undefined);
17
+
expect(path).toEqual([]);
18
+
expect(ancestors).toEqual([]);
22
+
expect(typeof key).toMatch(/number|string/);
24
+
expect(parent).toHaveProperty([key]);
26
+
expect(path).toBeInstanceOf(Array);
27
+
expect(path[path.length - 1]).toEqual(key);
29
+
expect(ancestors).toBeInstanceOf(Array);
30
+
expect(ancestors.length).toEqual(path.length - 1);
33
+
let currentNode = ast;
34
+
for (let i = 0; i < ancestors.length; ++i) {
35
+
expect(ancestors[i]).toEqual(currentNode);
37
+
currentNode = currentNode[path[i]];
38
+
expect(currentNode).not.toEqual(undefined);
43
+
function getValue(node: any) {
44
+
return 'value' in node ? node.value : undefined;
47
+
describe('Visitor', () => {
48
+
it('handles empty visitor', () => {
49
+
const ast = parse('{ a }', { noLocation: true });
50
+
expect(() => visit(ast, {})).not.toThrow();
53
+
it('validates path argument', () => {
54
+
const visited: any[] = [];
56
+
const ast = parse('{ a }', { noLocation: true });
59
+
enter(_node, _key, _parent, path) {
60
+
checkVisitorFnArgs(ast, arguments);
61
+
visited.push(['enter', path.slice()]);
63
+
leave(_node, _key, _parent, path) {
64
+
checkVisitorFnArgs(ast, arguments);
65
+
visited.push(['leave', path.slice()]);
69
+
expect(visited).toEqual([
71
+
['enter', ['definitions', 0]],
72
+
['enter', ['definitions', 0, 'selectionSet']],
73
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0]],
74
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
75
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
76
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0]],
77
+
['leave', ['definitions', 0, 'selectionSet']],
78
+
['leave', ['definitions', 0]],
83
+
it('validates ancestors argument', () => {
84
+
const ast = parse('{ a }', { noLocation: true });
85
+
const visitedNodes: any[] = [];
88
+
enter(node, key, parent, _path, ancestors) {
89
+
const inArray = typeof key === 'number';
91
+
visitedNodes.push(parent);
93
+
visitedNodes.push(node);
95
+
const expectedAncestors = visitedNodes.slice(0, -2);
96
+
expect(ancestors).toEqual(expectedAncestors);
98
+
leave(_node, key, _parent, _path, ancestors) {
99
+
const expectedAncestors = visitedNodes.slice(0, -2);
100
+
expect(ancestors).toEqual(expectedAncestors);
102
+
const inArray = typeof key === 'number';
104
+
visitedNodes.pop();
106
+
visitedNodes.pop();
111
+
it('allows editing a node both on enter and on leave', () => {
112
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
116
+
const editedAST = visit(ast, {
117
+
OperationDefinition: {
119
+
checkVisitorFnArgs(ast, arguments);
120
+
selectionSet = node.selectionSet;
124
+
kind: 'SelectionSet',
131
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
141
+
expect(editedAST).toEqual({
145
+
...ast.definitions[0],
153
+
it('allows editing the root node on enter and on leave', () => {
154
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
156
+
const { definitions } = ast;
158
+
const editedAST = visit(ast, {
161
+
checkVisitorFnArgs(ast, arguments);
169
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
179
+
expect(editedAST).toEqual({
186
+
it('allows for editing on enter', () => {
187
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
188
+
const editedAST = visit(ast, {
190
+
checkVisitorFnArgs(ast, arguments);
191
+
if (node.kind === 'Field' && node.name.value === 'b') {
197
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
199
+
expect(editedAST).toEqual(parse('{ a, c { a, c } }', { noLocation: true }));
202
+
it('allows for editing on leave', () => {
203
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
204
+
const editedAST = visit(ast, {
206
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
207
+
if (node.kind === 'Field' && node.name.value === 'b') {
213
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
215
+
expect(editedAST).toEqual(parse('{ a, c { a, c } }', { noLocation: true }));
218
+
it('ignores false returned on leave', () => {
219
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
220
+
const returnedAST = visit(ast, {
226
+
expect(returnedAST).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
229
+
it('visits edited node', () => {
230
+
const addedField = {
234
+
value: '__typename',
238
+
let didVisitAddedField;
240
+
const ast = parse('{ a { x } }', { noLocation: true });
243
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
244
+
if (node.kind === 'Field' && node.name.value === 'a') {
247
+
selectionSet: [addedField, node.selectionSet],
250
+
if (node === addedField) {
251
+
didVisitAddedField = true;
256
+
expect(didVisitAddedField).toEqual(true);
259
+
it('allows skipping a sub-tree', () => {
260
+
const visited: any[] = [];
262
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
265
+
checkVisitorFnArgs(ast, arguments);
266
+
visited.push(['enter', node.kind, getValue(node)]);
267
+
if (node.kind === 'Field' && node.name.value === 'b') {
273
+
checkVisitorFnArgs(ast, arguments);
274
+
visited.push(['leave', node.kind, getValue(node)]);
278
+
expect(visited).toEqual([
279
+
['enter', 'Document', undefined],
280
+
['enter', 'OperationDefinition', undefined],
281
+
['enter', 'SelectionSet', undefined],
282
+
['enter', 'Field', undefined],
283
+
['enter', 'Name', 'a'],
284
+
['leave', 'Name', 'a'],
285
+
['leave', 'Field', undefined],
286
+
['enter', 'Field', undefined],
287
+
['enter', 'Field', undefined],
288
+
['enter', 'Name', 'c'],
289
+
['leave', 'Name', 'c'],
290
+
['leave', 'Field', undefined],
291
+
['leave', 'SelectionSet', undefined],
292
+
['leave', 'OperationDefinition', undefined],
293
+
['leave', 'Document', undefined],
297
+
it('allows early exit while visiting', () => {
298
+
const visited: any[] = [];
300
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
303
+
checkVisitorFnArgs(ast, arguments);
304
+
visited.push(['enter', node.kind, getValue(node)]);
305
+
if (node.kind === 'Name' && node.value === 'x') {
310
+
checkVisitorFnArgs(ast, arguments);
311
+
visited.push(['leave', node.kind, getValue(node)]);
315
+
expect(visited).toEqual([
316
+
['enter', 'Document', undefined],
317
+
['enter', 'OperationDefinition', undefined],
318
+
['enter', 'SelectionSet', undefined],
319
+
['enter', 'Field', undefined],
320
+
['enter', 'Name', 'a'],
321
+
['leave', 'Name', 'a'],
322
+
['leave', 'Field', undefined],
323
+
['enter', 'Field', undefined],
324
+
['enter', 'Name', 'b'],
325
+
['leave', 'Name', 'b'],
326
+
['enter', 'SelectionSet', undefined],
327
+
['enter', 'Field', undefined],
328
+
['enter', 'Name', 'x'],
332
+
it('allows early exit while leaving', () => {
333
+
const visited: any[] = [];
335
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
338
+
checkVisitorFnArgs(ast, arguments);
339
+
visited.push(['enter', node.kind, getValue(node)]);
343
+
checkVisitorFnArgs(ast, arguments);
344
+
visited.push(['leave', node.kind, getValue(node)]);
345
+
if (node.kind === 'Name' && node.value === 'x') {
351
+
expect(visited).toEqual([
352
+
['enter', 'Document', undefined],
353
+
['enter', 'OperationDefinition', undefined],
354
+
['enter', 'SelectionSet', undefined],
355
+
['enter', 'Field', undefined],
356
+
['enter', 'Name', 'a'],
357
+
['leave', 'Name', 'a'],
358
+
['leave', 'Field', undefined],
359
+
['enter', 'Field', undefined],
360
+
['enter', 'Name', 'b'],
361
+
['leave', 'Name', 'b'],
362
+
['enter', 'SelectionSet', undefined],
363
+
['enter', 'Field', undefined],
364
+
['enter', 'Name', 'x'],
365
+
['leave', 'Name', 'x'],
369
+
it('allows a named functions visitor API', () => {
370
+
const visited: any[] = [];
372
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
375
+
checkVisitorFnArgs(ast, arguments);
376
+
visited.push(['enter', node.kind, getValue(node)]);
380
+
checkVisitorFnArgs(ast, arguments);
381
+
visited.push(['enter', node.kind, getValue(node)]);
384
+
checkVisitorFnArgs(ast, arguments);
385
+
visited.push(['leave', node.kind, getValue(node)]);
390
+
expect(visited).toEqual([
391
+
['enter', 'SelectionSet', undefined],
392
+
['enter', 'Name', 'a'],
393
+
['enter', 'Name', 'b'],
394
+
['enter', 'SelectionSet', undefined],
395
+
['enter', 'Name', 'x'],
396
+
['leave', 'SelectionSet', undefined],
397
+
['enter', 'Name', 'c'],
398
+
['leave', 'SelectionSet', undefined],
402
+
it('handles deep immutable edits correctly when using "enter"', () => {
403
+
const formatNode = node => {
405
+
node.selectionSet &&
406
+
!node.selectionSet.selections.some(
407
+
node => node.kind === Kind.FIELD && node.name.value === '__typename' && !node.alias
413
+
...node.selectionSet,
415
+
...node.selectionSet.selections,
420
+
value: '__typename',
428
+
const ast = parse('{ players { nodes { id } } }');
429
+
const expected = parse('{ players { nodes { id __typename } __typename } }');
430
+
const visited = visit(ast, {
432
+
InlineFragment: formatNode,
435
+
expect(print(visited)).toEqual(print(expected));