Skip to content

Commit 0a918b2

Browse files
committed
add expectedFieldsStack and validate extra fields after traversal
1 parent b9087d7 commit 0a918b2

File tree

3 files changed

+313
-44
lines changed

3 files changed

+313
-44
lines changed

src/methods/validate-fixture-input.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function validateFixtureInput(
3838
const inlineFragmentSpreadsAst = inlineNamedFragmentSpreads(queryAST);
3939
const typeInfo = new TypeInfo(schema);
4040
const valueStack: any[][] = [[value]];
41+
const expectedFieldsStack: Set<string>[] = [new Set()]; // Initial set tracks root level fields
4142
const errors: string[] = [];
4243
visit(
4344
inlineFragmentSpreadsAst,
@@ -49,6 +50,9 @@ export function validateFixtureInput(
4950

5051
const responseKey = node.alias?.value || node.name.value;
5152

53+
// Track this field as expected in the parent's set
54+
expectedFieldsStack[expectedFieldsStack.length - 1].add(responseKey);
55+
5256
const fieldDefinition = typeInfo.getFieldDef();
5357
const fieldType = fieldDefinition?.type;
5458

@@ -129,10 +133,21 @@ export function validateFixtureInput(
129133
}
130134
}
131135

136+
// If this field has nested selections, prepare to track expected child fields
137+
if (node.selectionSet) {
138+
expectedFieldsStack.push(new Set<string>());
139+
}
140+
132141
valueStack.push(nestedValues);
133142
},
134-
leave() {
135-
valueStack.pop();
143+
leave(node) {
144+
const nestedValues = valueStack.pop()!;
145+
146+
// If this field had nested selections, check for extra fields
147+
if (node.selectionSet) {
148+
const expectedFields = expectedFieldsStack.pop()!;
149+
errors.push(...checkForExtraFields(nestedValues, expectedFields));
150+
}
136151
},
137152
},
138153
SelectionSet: {
@@ -160,6 +175,11 @@ export function validateFixtureInput(
160175
},
161176
})
162177
);
178+
179+
// The query's root SelectionSet has no parent Field node, so there's no Field.leave event to check it.
180+
// We manually perform the same check here that would happen in Field.leave for nested objects.
181+
errors.push(...checkForExtraFields(valueStack[0], expectedFieldsStack[0]));
182+
163183
return { errors };
164184
}
165185

@@ -259,3 +279,31 @@ function isValueExpectedForType(
259279
// Only expect the value for this type if its __typename matches the parent type
260280
return valueTypename === parentType.name;
261281
}
282+
283+
/**
284+
* Checks fixture objects for fields that are not present in the GraphQL query.
285+
*
286+
* @param fixtureObjects - Array of fixture objects to validate
287+
* @param expectedFields - Set of field names that are expected based on the query
288+
* @returns Array of error messages for any extra fields found (empty if valid)
289+
*
290+
* @remarks
291+
* Only validates object types - skips null values and arrays.
292+
*/
293+
function checkForExtraFields(
294+
fixtureObjects: any[],
295+
expectedFields: Set<string>
296+
): string[] {
297+
const errors: string[] = [];
298+
for (const fixtureObject of fixtureObjects) {
299+
if (typeof fixtureObject === "object" && fixtureObject !== null && !Array.isArray(fixtureObject)) {
300+
const fixtureFields = Object.keys(fixtureObject);
301+
for (const fixtureField of fixtureFields) {
302+
if (!expectedFields.has(fixtureField)) {
303+
errors.push(`Extra field "${fixtureField}" found in fixture data not in query`);
304+
}
305+
}
306+
}
307+
}
308+
return errors;
309+
}

0 commit comments

Comments
 (0)