@@ -13,6 +13,34 @@ import {
1313} from "graphql" ;
1414import { inlineNamedFragmentSpreads } from "../utils/inline-named-fragment-spreads.js" ;
1515
16+ /**
17+ * Checks fixture objects for fields that are not present in the GraphQL query.
18+ *
19+ * @param fixtureObjects - Array of fixture objects to validate
20+ * @param expectedFields - Set of field names that are expected based on the query
21+ * @returns Array of error messages for any extra fields found (empty if valid)
22+ *
23+ * @remarks
24+ * Only validates object types - skips null values and arrays.
25+ */
26+ function checkForExtraFields (
27+ fixtureObjects : any [ ] ,
28+ expectedFields : Set < string >
29+ ) : string [ ] {
30+ const errors : string [ ] = [ ] ;
31+ for ( const fixtureObject of fixtureObjects ) {
32+ if ( typeof fixtureObject === "object" && fixtureObject !== null && ! Array . isArray ( fixtureObject ) ) {
33+ const fixtureFields = Object . keys ( fixtureObject ) ;
34+ for ( const fixtureField of fixtureFields ) {
35+ if ( ! expectedFields . has ( fixtureField ) ) {
36+ errors . push ( `Extra field "${ fixtureField } " found in fixture data not in query` ) ;
37+ }
38+ }
39+ }
40+ }
41+ return errors ;
42+ }
43+
1644export interface ValidateFixtureInputResult {
1745 errors : string [ ] ;
1846}
@@ -37,6 +65,7 @@ export function validateFixtureInput(
3765 const inlineFragmentSpreadsAst = inlineNamedFragmentSpreads ( queryAST ) ;
3866 const typeInfo = new TypeInfo ( schema ) ;
3967 const valueStack : any [ ] [ ] = [ [ value ] ] ;
68+ const expectedFieldsStack : Set < string > [ ] = [ new Set ( ) ] ; // Initial set tracks root level fields
4069 const errors : string [ ] = [ ] ;
4170 visit (
4271 inlineFragmentSpreadsAst ,
@@ -48,6 +77,9 @@ export function validateFixtureInput(
4877
4978 const responseKey = node . alias ?. value || node . name . value ;
5079
80+ // Track this field as expected in the parent's set
81+ expectedFieldsStack [ expectedFieldsStack . length - 1 ] . add ( responseKey ) ;
82+
5183 const fieldDefinition = typeInfo . getFieldDef ( ) ;
5284 const fieldType = fieldDefinition ?. type ;
5385
@@ -124,10 +156,21 @@ export function validateFixtureInput(
124156 }
125157 }
126158
159+ // If this field has nested selections, prepare to track expected child fields
160+ if ( node . selectionSet ) {
161+ expectedFieldsStack . push ( new Set < string > ( ) ) ;
162+ }
163+
127164 valueStack . push ( nestedValues ) ;
128165 } ,
129- leave ( ) {
130- valueStack . pop ( ) ;
166+ leave ( node ) {
167+ const nestedValues = valueStack . pop ( ) ! ;
168+
169+ // If this field had nested selections, check for extra fields
170+ if ( node . selectionSet ) {
171+ const expectedFields = expectedFieldsStack . pop ( ) ! ;
172+ errors . push ( ...checkForExtraFields ( nestedValues , expectedFields ) ) ;
173+ }
131174 } ,
132175 } ,
133176 SelectionSet : {
@@ -155,6 +198,11 @@ export function validateFixtureInput(
155198 } ,
156199 } )
157200 ) ;
201+
202+ // The query's root SelectionSet has no parent Field node, so there's no Field.leave event to check it.
203+ // We manually perform the same check here that would happen in Field.leave for nested objects.
204+ errors . push ( ...checkForExtraFields ( valueStack [ 0 ] , expectedFieldsStack [ 0 ] ) ) ;
205+
158206 return { errors } ;
159207}
160208
0 commit comments