@@ -11,6 +11,24 @@ import {
1111} from "graphql" ;
1212import { inlineNamedFragmentSpreads } from "../utils/inline-named-fragment-spreads.js" ;
1313
14+ function checkForExtraFields (
15+ fixtureObjects : any [ ] ,
16+ expectedFields : Set < string >
17+ ) : string [ ] {
18+ const errors : string [ ] = [ ] ;
19+ for ( const fixtureObject of fixtureObjects ) {
20+ if ( typeof fixtureObject === "object" && fixtureObject !== null && ! Array . isArray ( fixtureObject ) ) {
21+ const fixtureFields = Object . keys ( fixtureObject ) ;
22+ for ( const fixtureField of fixtureFields ) {
23+ if ( ! expectedFields . has ( fixtureField ) ) {
24+ errors . push ( `Extra field "${ fixtureField } " found in fixture data` ) ;
25+ }
26+ }
27+ }
28+ }
29+ return errors ;
30+ }
31+
1432export interface ValidateFixtureInputResult {
1533 valid : boolean ;
1634 errors : string [ ] ;
@@ -24,6 +42,7 @@ export function validateFixtureInput(
2442 const inlineFragmentSpreadsAst = inlineNamedFragmentSpreads ( queryAST ) ;
2543 const typeInfo = new TypeInfo ( schema ) ;
2644 const valueStack : any [ ] = [ [ value ] ] ;
45+ const expectedFieldsStack : Set < string > [ ] = [ new Set ( ) ] ; // Initial set tracks root level fields
2746 const errors : string [ ] = [ ] ;
2847 visit (
2948 inlineFragmentSpreadsAst ,
@@ -35,6 +54,9 @@ export function validateFixtureInput(
3554
3655 const responseKey = node . alias ?. value || node . name . value ;
3756
57+ // Track this field as expected in the parent's set
58+ expectedFieldsStack [ expectedFieldsStack . length - 1 ] . add ( responseKey ) ;
59+
3860 const fieldDefinition = typeInfo . getFieldDef ( ) ;
3961 const fieldType = fieldDefinition ?. type ;
4062
@@ -76,10 +98,21 @@ export function validateFixtureInput(
7698 }
7799 }
78100
101+ // If this field has nested selections, prepare to track expected child fields
102+ if ( node . selectionSet ) {
103+ expectedFieldsStack . push ( new Set < string > ( ) ) ;
104+ }
105+
79106 valueStack . push ( nestedValues ) ;
80107 } ,
81- leave ( ) {
82- valueStack . pop ( ) ;
108+ leave ( node ) {
109+ const nestedValues = valueStack . pop ( ) ! ;
110+
111+ // If this field had nested selections, check for extra fields
112+ if ( node . selectionSet ) {
113+ const expectedFields = expectedFieldsStack . pop ( ) ! ;
114+ errors . push ( ...checkForExtraFields ( nestedValues , expectedFields ) ) ;
115+ }
83116 } ,
84117 } ,
85118 SelectionSet : {
@@ -97,6 +130,7 @@ export function validateFixtureInput(
97130 selection . kind == Kind . INLINE_FRAGMENT
98131 ) . length ;
99132
133+ // We only need to check for __typename if there are multiple fragment spreads
100134 if ( ! hasTypename && fragmentSpreadCount > 1 ) {
101135 errors . push (
102136 `Missing __typename field for abstract type ${ getNamedType ( typeInfo . getType ( ) ) ?. name } `
@@ -107,5 +141,10 @@ export function validateFixtureInput(
107141 } ,
108142 } )
109143 ) ;
144+
145+ // The query's root SelectionSet has no parent Field node, so there's no Field.leave event to check it.
146+ // We manually perform the same check here that would happen in Field.leave for nested objects.
147+ errors . push ( ...checkForExtraFields ( valueStack [ 0 ] , expectedFieldsStack [ 0 ] ) ) ;
148+
110149 return { valid : errors . length === 0 , errors } ;
111150}
0 commit comments