···
+
import { describe, it, expect } from 'vitest';
+
import { Kind, parse, print } from 'graphql';
+
import { visit, BREAK } from '../visitor';
+
function checkVisitorFnArgs(ast, args, isEdited = false) {
+
const [node, key, parent, path, ancestors] = args;
+
expect(node).toBeInstanceOf(Object);
+
expect(Object.values(Kind)).toContain(node.kind);
+
const isRoot = key === undefined;
+
expect(node).toEqual(ast);
+
expect(parent).toEqual(undefined);
+
expect(path).toEqual([]);
+
expect(ancestors).toEqual([]);
+
expect(typeof key).toMatch(/number|string/);
+
expect(parent).toHaveProperty([key]);
+
expect(path).toBeInstanceOf(Array);
+
expect(path[path.length - 1]).toEqual(key);
+
expect(ancestors).toBeInstanceOf(Array);
+
expect(ancestors.length).toEqual(path.length - 1);
+
for (let i = 0; i < ancestors.length; ++i) {
+
expect(ancestors[i]).toEqual(currentNode);
+
currentNode = currentNode[path[i]];
+
expect(currentNode).not.toEqual(undefined);
+
function getValue(node: any) {
+
return 'value' in node ? node.value : undefined;
+
describe('Visitor', () => {
+
it('handles empty visitor', () => {
+
const ast = parse('{ a }', { noLocation: true });
+
expect(() => visit(ast, {})).not.toThrow();
+
it('validates path argument', () => {
+
const visited: any[] = [];
+
const ast = parse('{ a }', { noLocation: true });
+
enter(_node, _key, _parent, path) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', path.slice()]);
+
leave(_node, _key, _parent, path) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', path.slice()]);
+
expect(visited).toEqual([
+
['enter', ['definitions', 0]],
+
['enter', ['definitions', 0, 'selectionSet']],
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0]],
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0]],
+
['leave', ['definitions', 0, 'selectionSet']],
+
['leave', ['definitions', 0]],
+
it('validates ancestors argument', () => {
+
const ast = parse('{ a }', { noLocation: true });
+
const visitedNodes: any[] = [];
+
enter(node, key, parent, _path, ancestors) {
+
const inArray = typeof key === 'number';
+
visitedNodes.push(parent);
+
visitedNodes.push(node);
+
const expectedAncestors = visitedNodes.slice(0, -2);
+
expect(ancestors).toEqual(expectedAncestors);
+
leave(_node, key, _parent, _path, ancestors) {
+
const expectedAncestors = visitedNodes.slice(0, -2);
+
expect(ancestors).toEqual(expectedAncestors);
+
const inArray = typeof key === 'number';
+
it('allows editing a node both on enter and on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
checkVisitorFnArgs(ast, arguments);
+
selectionSet = node.selectionSet;
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
expect(editedAST).toEqual({
+
it('allows editing the root node on enter and on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const { definitions } = ast;
+
const editedAST = visit(ast, {
+
checkVisitorFnArgs(ast, arguments);
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
expect(editedAST).toEqual({
+
it('allows for editing on enter', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
checkVisitorFnArgs(ast, arguments);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
expect(editedAST).toEqual(parse('{ a, c { a, c } }', { noLocation: true }));
+
it('allows for editing on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
expect(editedAST).toEqual(parse('{ a, c { a, c } }', { noLocation: true }));
+
it('ignores false returned on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const returnedAST = visit(ast, {
+
expect(returnedAST).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
it('visits edited node', () => {
+
let didVisitAddedField;
+
const ast = parse('{ a { x } }', { noLocation: true });
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'a') {
+
selectionSet: [addedField, node.selectionSet],
+
if (node === addedField) {
+
didVisitAddedField = true;
+
expect(didVisitAddedField).toEqual(true);
+
it('allows skipping a sub-tree', () => {
+
const visited: any[] = [];
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'OperationDefinition', undefined],
+
['leave', 'Document', undefined],
+
it('allows early exit while visiting', () => {
+
const visited: any[] = [];
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
it('allows early exit while leaving', () => {
+
const visited: any[] = [];
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'Name', 'x'],
+
it('allows a named functions visitor API', () => {
+
const visited: any[] = [];
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
expect(visited).toEqual([
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Name', 'a'],
+
['enter', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'SelectionSet', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'SelectionSet', undefined],
+
it('handles deep immutable edits correctly when using "enter"', () => {
+
const formatNode = node => {
+
!node.selectionSet.selections.some(
+
node => node.kind === Kind.FIELD && node.name.value === '__typename' && !node.alias
+
...node.selectionSet.selections,
+
const ast = parse('{ players { nodes { id } } }');
+
const expected = parse('{ players { nodes { id __typename } __typename } }');
+
const visited = visit(ast, {
+
InlineFragment: formatNode,
+
expect(print(visited)).toEqual(print(expected));