Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions packages/relay-test-utils/__tests__/RelayMockPayloadGenerator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2201,3 +2201,210 @@ describe("Aliased linked fields without arguments don't overwrite each other", (
);
});
});

describe('Plural linked field selected on both a concrete selection and an abstract InlineFragment at the same level (#5258)', () => {
// Regression test for facebook/relay#5258 — currently demonstrates buggy
// behavior; flip assertion when the underlying bug is fixed.
//
// Scenario from the issue: a query field returns a concrete type that
// implements an interface, and the same plural linked field is selected
// both directly (concrete path, returning the concrete element type) and
// via a fragment spread on the interface (abstract path, returning the
// abstract element type). Because the field's return type differs across
// the two paths, the Relay compiler does NOT dedupe the selections — the
// normalization AST keeps the concrete `LinkedField` plus an abstract
// `InlineFragment` that carries its own copy of the same `LinkedField`.
//
// RelayMockPayloadGenerator's `_traverseSelections` processes both
// selections sequentially. On the second pass, `_mockLink` reads the
// already-generated array out of `data[applicationName]` and passes the
// whole array as `prevData` to each new element generated by
// `generateMockList`. The result is `[[e1,e2,e3], [e1,e2,e3], ...]`
// instead of `[e1, e2, e3]`.
//
// Scope: this test only covers the array-shape symptom. The related
// `mockData[TYPENAME_KEY] = selection.type` symptom from the issue's
// "Root Cause Analysis" (point #3 — abstract type forced onto already-
// generated concrete data) is a separate symptom and is not asserted
// here.
//
// The OSS test schema does not naturally model a concrete-vs-abstract
// return-type divergence on the same field name (every plural field
// returning an interface returns the same `[Actor]` from both the
// interface and its implementers, so the compiler dedupes them — a
// schema-extension prototype with a divergent `[ConcreteEdge]` vs
// `[AbstractEdge]` interface implementation was tried and the compiler
// still folded the InlineFragment because the parent field's static
// type pinned the abstract narrowing). To mirror the issue's
// "Standalone Reproduction Script" exactly, this test hand-constructs
// a `ConcreteRequest` matching the AST shape from the issue body.
// `MockPayloadGenerator.generate` walks the AST and does not consult a
// schema, so the type names below need not exist in the test schema.
test('BUG: produces nested-array shape instead of a flat array (#5258)', () => {
const request: $FlowFixMe = {
kind: 'Request',
fragment: {
kind: 'Fragment',
name: 'RelayMockPayloadGeneratorTest5258Query',
type: 'Query',
metadata: null,
argumentDefinitions: [],
selections: [
{
kind: 'LinkedField',
alias: null,
name: 'items',
storageKey: null,
args: null,
concreteType: 'ConcreteConnection',
plural: false,
selections: [
{
kind: 'LinkedField',
alias: null,
name: 'edges',
storageKey: null,
args: null,
concreteType: 'ConcreteEdge',
plural: true,
selections: [
{
kind: 'ScalarField',
alias: null,
name: 'cursor',
args: null,
storageKey: null,
},
],
},
],
},
],
},
operation: {
kind: 'Operation',
name: 'RelayMockPayloadGeneratorTest5258Query',
argumentDefinitions: [],
selections: [
{
kind: 'LinkedField',
alias: null,
name: 'items',
storageKey: null,
args: null,
concreteType: 'ConcreteConnection',
plural: false,
selections: [
{
kind: 'LinkedField',
alias: null,
name: 'edges',
storageKey: null,
args: null,
concreteType: 'ConcreteEdge',
plural: true,
selections: [
{
kind: 'ScalarField',
alias: null,
name: '__typename',
args: null,
storageKey: null,
},
{
kind: 'ScalarField',
alias: null,
name: 'cursor',
args: null,
storageKey: null,
},
],
},
{
kind: 'InlineFragment',
type: 'AbstractConnection',
abstractKey: '__isAbstractConnection',
selections: [
{
kind: 'LinkedField',
alias: null,
name: 'edges',
storageKey: null,
args: null,
concreteType: null,
plural: true,
selections: [
{
kind: 'ScalarField',
alias: null,
name: '__typename',
args: null,
storageKey: null,
},
{
kind: 'ScalarField',
alias: null,
name: 'cursor',
args: null,
storageKey: null,
},
],
},
],
},
],
},
],
},
params: {
cacheID: 'relay-mock-payload-generator-test-5258',
id: null,
metadata: {},
name: 'RelayMockPayloadGeneratorTest5258Query',
operationKind: 'query',
text: null,
},
};
const operation = createOperationDescriptor(request, {});
const payload = RelayMockPayloadGenerator.generate(operation, {
ConcreteConnection: () => ({
edges: [
{__typename: 'ConcreteEdge', cursor: 'a'},
{__typename: 'ConcreteEdge', cursor: 'b'},
{__typename: 'ConcreteEdge', cursor: 'c'},
],
}),
});
// $FlowFixMe[unclear-type] payload.data is intentionally untyped here
const edges = (payload.data: any).items.edges;
expect(Array.isArray(edges)).toBe(true);
expect(edges).toHaveLength(3);
// BUG: each entry is the entire previous edges array (with extra
// `__typename`/`cursor` properties bolted onto the Array instance
// from the abstract InlineFragment pass) instead of a single edge
// object. The exact buggy shape is asserted with a strict structural
// match so any "improvement" (nested arrays collapsing partially,
// elements becoming single edges wrapped in an array, missing
// properties, etc.) breaks this test loudly. When the bug is fixed,
// replace the three deep assertions with the flat shape:
// expect(edges[0]).toEqual({__typename: 'ConcreteEdge', cursor: 'a'});
// expect(edges[1]).toEqual({__typename: 'ConcreteEdge', cursor: 'b'});
// expect(edges[2]).toEqual({__typename: 'ConcreteEdge', cursor: 'c'});
const expectedBuggyEdges = [
{__typename: 'ConcreteEdge', cursor: 'a'},
{__typename: 'ConcreteEdge', cursor: 'b'},
{__typename: 'ConcreteEdge', cursor: 'c'},
];
for (let i = 0; i < 3; i++) {
expect(Array.isArray(edges[i])).toBe(true);
expect(edges[i]).toHaveLength(3);
// The whole edges array gets repeated as each element.
expect(Array.from(edges[i])).toEqual(expectedBuggyEdges);
// Buggy second-pass also bolts the abstract path's own scalar
// fields onto the Array instance (taking the first generated
// edge's values).
expect(edges[i].__typename).toBe('ConcreteEdge');
expect(edges[i].cursor).toBe('a');
}
});
});
Loading