/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { buildSchema } from "graphql"; import { describe, expect, it } from "vitest"; import { selectDottedFieldPath } from "../path-selection.js"; import { renderQuery } from "../query-builder.js"; import { createSession, type QuerySession } from "../session.js"; import { MutationContextError } from "../walker.js"; // Models a Salesforce-shaped UIAPI with: // - Value wrapper types (StringValue { value: String }) on scalar fields // - A polymorphic Owner union (User | Group) — User has Email, Group does not // - A scalar Id field that is NOT wrapped (matches UIAPI convention) // - A non-polymorphic Account parent (plain object) for the control case const schema = buildSchema(` type Query { uiapi: UIAPI! } type UIAPI { query: RecordQuery! } type RecordQuery { Account(first: Int): AccountConnection! Case(first: Int): CaseConnection! } type AccountConnection { edges: [AccountEdge!]! } type AccountEdge { node: Account! } type CaseConnection { edges: [CaseEdge!]! } type CaseEdge { node: Case! } type Account { Id: ID! Name: StringValue Industry: StringValue Owner: OwnerUnion } type Case { Id: ID! Subject: StringValue Owner: OwnerUnion Account: Account } union OwnerUnion = User | Group type User { Id: ID! Name: StringValue Email: StringValue } type Group { Id: ID! Name: StringValue } type StringValue { value: String } `); function makeSession(): QuerySession { const session = createSession("test-org", "query"); session.navigationPath = ["query"]; return session; } const accountNodePath = ["uiapi", "query", "Account", "edges", "node"]; const caseNodePath = ["uiapi", "query", "Case", "edges", "node"]; describe("path-selection", () => { describe("non-polymorphic dotted path", () => { it("selects a scalar through a plain parent relationship", () => { const session = makeSession(); selectDottedFieldPath(session, schema, caseNodePath, "Account.Name"); const query = renderQuery(session); expect(query).toMatch(/Account\b[\s\S]*Name[\s\S]*\{[\s\S]*value/); // Should NOT introduce inline fragments — Account is a plain object. expect(query).not.toMatch(/\.\.\. on /); }); it("selects Id without appending .value", () => { const session = makeSession(); selectDottedFieldPath(session, schema, accountNodePath, "Id"); const query = renderQuery(session); // Id is a scalar directly under Account; no `value` wrapper. expect(query).toMatch(/Id\b/); expect(query).not.toMatch(/Id \{[\s\S]*value/); }); it("unwraps a StringValue to its .value selection", () => { const session = makeSession(); selectDottedFieldPath(session, schema, accountNodePath, "Name"); const query = renderQuery(session); // Name is a value wrapper — must select Name { value }. expect(query).toMatch(/Name \{[\s\S]*value/); }); }); describe("polymorphic union expansion", () => { it("expands a polymorphic union segment to inline fragments", () => { const session = makeSession(); selectDottedFieldPath(session, schema, accountNodePath, "Owner.Name"); const query = renderQuery(session); // Both members of OwnerUnion (User and Group) have Name → both selected. expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on User \{[\s\S]*Name \{[\s\S]*value/); expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on Group \{[\s\S]*Name \{[\s\S]*value/); }); it("skips union members that lack the field", () => { const session = makeSession(); // Email exists on User but NOT on Group — we should select on User only, // silently skipping Group rather than failing the whole call. selectDottedFieldPath(session, schema, accountNodePath, "Owner.Email"); const query = renderQuery(session); expect(query).toMatch(/\.\.\. on User \{[\s\S]*Email \{[\s\S]*value/); expect(query).not.toMatch(/\.\.\. on Group \{[\s\S]*Email/); }); it("throws when no union member has the field", () => { const session = makeSession(); // BadField exists on neither User nor Group. Without a guard the function // would silently no-op and the caller would render `query { }`. Fail loudly. expect(() => selectDottedFieldPath(session, schema, accountNodePath, "Owner.BadField"), ).toThrow(/Field "BadField" not found on any member of union OwnerUnion/); }); it("does not collide with sibling selections", () => { const session = makeSession(); // Mixed selection: a non-polymorphic field, then a polymorphic one. The // rendered query must contain both, and the union expansion must not // disrupt the simple selection above it. selectDottedFieldPath(session, schema, accountNodePath, "Name"); selectDottedFieldPath(session, schema, accountNodePath, "Owner.Name"); const query = renderQuery(session); expect(query).toMatch(/Name \{[\s\S]*value/); expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on User/); expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on Group/); }); }); describe("mutation context — MutationContextError", () => { const mutationSchema = buildSchema(` type Query { _placeholder: Boolean } type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! } input UIAPIMutationsInput { allOrNone: Boolean } type UIAPIMutations { AccountCreate(input: AccountCreateInput!): AccountCreatePayload } input AccountCreateInput { Account: AccountCreateRepresentation! } input AccountCreateRepresentation { Name: String } type AccountCreatePayload { Record: Account } type Account { Id: ID!, Name: StringValue, Owner: User } type User { Id: ID!, Name: StringValue } type StringValue { value: String } `); const mutationRecordPath = ["uiapi", "AccountCreate", "Record"]; function makeMutationSession(): QuerySession { return createSession("test-org", "mutation"); } it("throws MutationContextError for relationship field in mutation result", () => { const session = makeMutationSession(); expect(() => selectDottedFieldPath(session, mutationSchema, mutationRecordPath, "Owner.Name"), ).toThrow(MutationContextError); }); it("MutationContextError message includes field name and guidance", () => { const session = makeMutationSession(); try { selectDottedFieldPath(session, mutationSchema, mutationRecordPath, "Owner.Name"); expect.fail("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(MutationContextError); expect((err as MutationContextError).message).toMatch(/Owner/); expect((err as MutationContextError).message).toMatch(/not available in mutation results/); } }); it("does not throw MutationContextError for scalar fields in mutation result", () => { const session = makeMutationSession(); expect(() => selectDottedFieldPath(session, mutationSchema, mutationRecordPath, "Name"), ).not.toThrow(); const query = renderQuery(session); expect(query).toMatch(/Name \{[\s\S]*value/); }); }); });