Skip to content

Commit 4240841

Browse files
committed
use grandparentType to determine if in non-selected type
1 parent 963dadf commit 4240841

File tree

2 files changed

+148
-3
lines changed

2 files changed

+148
-3
lines changed

src/methods/validate-fixture-input.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function validateFixtureInput(
6767
errors.push(`Cannot validate ${responseKey}: missing parent type information`);
6868
} else {
6969
const typenameResponseKey = typenameResponseKeyStack[typenameResponseKeyStack.length - 1];
70-
if (isValueExpectedForType(currentValue, parentType, schema, typenameResponseKey)) {
70+
if (isValueExpectedForType(currentValue, parentType, schema, typeInfo, typenameResponseKey)) {
7171
errors.push(`Missing expected fixture data for ${responseKey}`);
7272
}
7373
}
@@ -255,6 +255,7 @@ function processNestedArrays(
255255
* @param fixtureValue - The fixture value to check
256256
* @param parentType - The parent type from typeInfo
257257
* @param schema - The GraphQL schema to resolve possible types for abstract types
258+
* @param typeInfo - TypeInfo instance to check for abstract types in ancestry
258259
* @param typenameKey - The response key for the __typename field (supports aliases like `type: __typename`)
259260
* @returns True if the value is expected for the parent type, false otherwise
260261
*
@@ -263,16 +264,28 @@ function processNestedArrays(
263264
* is one of the possible types for that abstract type.
264265
* When the parent type is concrete (e.g., inside `... on ConcreteType`), only values
265266
* whose __typename matches the concrete type are expected.
267+
*
268+
* Special case: Empty objects {} are treated as valid when __typename is not selected.
269+
* This handles GraphQL responses where union/interface members that don't match any
270+
* selected inline fragments are returned as empty objects.
266271
*/
267272
function isValueExpectedForType(
268273
fixtureValue: any,
269274
parentType: GraphQLCompositeType,
270275
schema: GraphQLSchema,
276+
typeInfo: TypeInfo,
271277
typenameKey?: string
272278
): boolean {
273-
// If __typename wasn't selected in the query, we can't discriminate, so expect all values
279+
// If __typename wasn't selected in the query, we can't discriminate
274280
if (!typenameKey) {
275-
return true;
281+
// Empty objects {} are valid if the parent field returns a union/interface
282+
// Check if the grandparent type (one level back in the stack) is abstract
283+
const parentStack = (typeInfo as any)._parentTypeStack;
284+
const grandparentType = parentStack[parentStack.length - 2];
285+
if (grandparentType && isAbstractType(grandparentType) && Object.keys(fixtureValue).length === 0) {
286+
return false; // Don't expect any fields on empty objects in union/interface contexts
287+
}
288+
return true; // Otherwise, expect all values
276289
}
277290

278291
const valueTypename = fixtureValue[typenameKey];

test/methods/validate-fixture-input.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,106 @@ describe("validateFixtureInput", () => {
288288
expect(result.errors).toHaveLength(0);
289289
});
290290

291+
it("handles empty objects in union when inline fragment doesn't match", () => {
292+
const queryAST = parse(`
293+
query {
294+
data {
295+
searchResults {
296+
... on Item {
297+
id
298+
count
299+
}
300+
}
301+
}
302+
}
303+
`);
304+
305+
const fixtureInput = {
306+
data: {
307+
searchResults: [
308+
{
309+
id: "gid://test/Item/1",
310+
count: 5
311+
},
312+
{} // Empty object - represents Metadata that didn't match the Item fragment
313+
]
314+
}
315+
};
316+
317+
const result = validateFixtureInput(queryAST, schema, fixtureInput);
318+
319+
// Empty object {} is valid - GraphQL returns this for union members that don't match any fragments
320+
expect(result.errors).toHaveLength(0);
321+
});
322+
323+
it("handles empty objects in interface fragment inside union", () => {
324+
const queryAST = parse(`
325+
query {
326+
data {
327+
products {
328+
... on Purchasable {
329+
price
330+
currency
331+
}
332+
}
333+
}
334+
}
335+
`);
336+
337+
const fixtureInput = {
338+
data: {
339+
products: [
340+
{
341+
price: 1000,
342+
currency: "USD"
343+
},
344+
{} // Empty object - GiftCard that doesn't implement Purchasable
345+
]
346+
}
347+
};
348+
349+
const result = validateFixtureInput(queryAST, schema, fixtureInput);
350+
351+
// Empty object {} is valid - represents a union member that doesn't implement the interface
352+
expect(result.errors).toHaveLength(0);
353+
});
354+
355+
it("handles objects with only __typename when inline fragment doesn't match", () => {
356+
const queryAST = parse(`
357+
query {
358+
data {
359+
searchResults {
360+
__typename
361+
... on Item {
362+
id
363+
count
364+
}
365+
}
366+
}
367+
}
368+
`);
369+
370+
const fixtureInput = {
371+
data: {
372+
searchResults: [
373+
{
374+
__typename: "Item",
375+
id: "gid://test/Item/1",
376+
count: 5
377+
},
378+
{
379+
__typename: "Metadata" // Only typename, no other fields
380+
}
381+
]
382+
}
383+
};
384+
385+
const result = validateFixtureInput(queryAST, schema, fixtureInput);
386+
387+
// Object with only __typename is valid - Metadata doesn't match the Item fragment
388+
expect(result.errors).toHaveLength(0);
389+
});
390+
291391
it("handles nested inline fragments", () => {
292392
const queryAST = parse(`
293393
query {
@@ -1053,6 +1153,38 @@ describe("validateFixtureInput", () => {
10531153
expect(result.errors[0]).toBe('Cannot validate nonExistentField: missing type information');
10541154
});
10551155

1156+
it("detects empty objects in non-union context", () => {
1157+
const queryAST = parse(`
1158+
query {
1159+
data {
1160+
items {
1161+
id
1162+
count
1163+
}
1164+
}
1165+
}
1166+
`);
1167+
1168+
const fixtureInput = {
1169+
data: {
1170+
items: [
1171+
{
1172+
id: "gid://test/Item/1",
1173+
count: 5
1174+
},
1175+
{} // Empty object in non-union context - should error
1176+
]
1177+
}
1178+
};
1179+
1180+
const result = validateFixtureInput(queryAST, schema, fixtureInput);
1181+
1182+
// Empty object {} is invalid in non-union context - missing required fields
1183+
expect(result.errors).toHaveLength(2);
1184+
expect(result.errors[0]).toBe("Missing expected fixture data for id");
1185+
expect(result.errors[1]).toBe("Missing expected fixture data for count");
1186+
});
1187+
10561188
it("detects missing fields when __typename is not selected in union with inline fragments", () => {
10571189
const queryAST = parse(`
10581190
query {

0 commit comments

Comments
 (0)