Skip to content

Commit cf1eb14

Browse files
committed
Add unit tests for operation type components
Adds component tests for QueryType, MutationType, and SubscriptionType using inline snapshots. Tests cover rendering with scalar return types, model return types (with stub type registration), parameters, and empty operation lists. Updates renderComponentToSDL utility to support skipPlaceholderQuery option for testing QueryType, while maintaining backwards compatibility with existing tests that pass context overrides directly. Also cleans up e2e and emitter tests to use consistent vitest expect() assertions with toMatchInlineSnapshot() instead of mixed strictEqual and .includes() checks.
1 parent 9639639 commit cf1eb14

4 files changed

Lines changed: 305 additions & 32 deletions

File tree

packages/graphql/test/components/component-test-utils.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,33 @@ import type { Program } from "@typespec/compiler";
55
import { GraphQLSchema } from "../../src/components/graphql-schema.js";
66
import type { GraphQLSchemaContextValue } from "../../src/context/index.js";
77

8+
export interface RenderOptions {
9+
/** Context overrides for the GraphQL schema context */
10+
contextOverrides?: Partial<GraphQLSchemaContextValue>;
11+
/** Skip the placeholder Query type (use when testing QueryType component) */
12+
skipPlaceholderQuery?: boolean;
13+
}
14+
815
/**
916
* Renders GraphQL components in isolation and returns the printed SDL.
1017
*
1118
* Wraps children in the required context providers (TspContext + GraphQLSchemaContext)
12-
* and always includes a placeholder Query type (required by graphql-js).
19+
* and includes a placeholder Query type by default (required by graphql-js).
1320
*
1421
* Tests should assert on fragments of the returned SDL, ignoring the placeholder Query.
22+
*
23+
* @param options - Either RenderOptions object or context overrides directly (for backwards compatibility)
1524
*/
1625
export function renderComponentToSDL(
1726
program: Program,
1827
children: Children,
19-
contextOverrides?: Partial<GraphQLSchemaContextValue>,
28+
options?: RenderOptions | Partial<GraphQLSchemaContextValue>,
2029
): string {
30+
// Backwards compatibility: if options doesn't have RenderOptions keys, treat it as contextOverrides
31+
const isRenderOptions = options && ("contextOverrides" in options || "skipPlaceholderQuery" in options);
32+
const { contextOverrides, skipPlaceholderQuery } = isRenderOptions
33+
? (options as RenderOptions)
34+
: { contextOverrides: options as Partial<GraphQLSchemaContextValue> | undefined, skipPlaceholderQuery: false };
2135
const contextValue: GraphQLSchemaContextValue = {
2236
classifiedTypes: {
2337
interfaces: [],
@@ -39,9 +53,11 @@ export function renderComponentToSDL(
3953
const schema = renderSchema(
4054
<GraphQLSchema program={program} contextValue={contextValue}>
4155
{children}
42-
<gql.Query>
43-
<gql.Field name="_placeholder" type={gql.Boolean} nonNull={false} />
44-
</gql.Query>
56+
{!skipPlaceholderQuery && (
57+
<gql.Query>
58+
<gql.Field name="_placeholder" type={gql.Boolean} nonNull={false} />
59+
</gql.Query>
60+
)}
4561
</GraphQLSchema>,
4662
{ namePolicy: null },
4763
);
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { t } from "@typespec/compiler/testing";
2+
import * as gql from "@alloy-js/graphql";
3+
import { describe, expect, it, beforeEach } from "vitest";
4+
import {
5+
QueryType,
6+
MutationType,
7+
SubscriptionType,
8+
} from "../../src/components/operations/index.js";
9+
import { Tester } from "../test-host.js";
10+
import { renderComponentToSDL } from "./component-test-utils.js";
11+
12+
describe("QueryType component", () => {
13+
let tester: Awaited<ReturnType<typeof Tester.createInstance>>;
14+
beforeEach(async () => {
15+
tester = await Tester.createInstance();
16+
});
17+
18+
it("renders nothing when no operations", async () => {
19+
await tester.compile(t.code`model Placeholder { id: string; }`);
20+
21+
const sdl = renderComponentToSDL(tester.program, <QueryType operations={[]} />);
22+
23+
// Should only contain the placeholder Query from test utils
24+
expect(sdl).toMatchInlineSnapshot(`
25+
"type Query {
26+
_placeholder: Boolean
27+
}"
28+
`);
29+
});
30+
31+
it("renders single query operation with scalar return type", async () => {
32+
const { getVersion } = await tester.compile(
33+
t.code`op ${t.op("getVersion")}(): string;`,
34+
);
35+
36+
const sdl = renderComponentToSDL(
37+
tester.program,
38+
<QueryType operations={[getVersion]} />,
39+
{ skipPlaceholderQuery: true },
40+
);
41+
42+
expect(sdl).toMatchInlineSnapshot(`
43+
"type Query {
44+
getVersion: String!
45+
}"
46+
`);
47+
});
48+
49+
it("renders query operation with model return type", async () => {
50+
const { getBook } = await tester.compile(
51+
t.code`
52+
model ${t.model("Book")} { id: string; title: string; }
53+
op ${t.op("getBook")}(id: string): Book;
54+
`,
55+
);
56+
57+
// Stub the Book type so buildSchema can resolve the reference
58+
const sdl = renderComponentToSDL(
59+
tester.program,
60+
<>
61+
<gql.ObjectType name="Book">
62+
<gql.Field name="id" type={gql.String} nonNull />
63+
<gql.Field name="title" type={gql.String} nonNull />
64+
</gql.ObjectType>
65+
<QueryType operations={[getBook]} />
66+
</>,
67+
{ skipPlaceholderQuery: true },
68+
);
69+
70+
expect(sdl).toMatchInlineSnapshot(`
71+
"type Book {
72+
id: String!
73+
title: String!
74+
}
75+
76+
type Query {
77+
getBook(id: String!): Book!
78+
}"
79+
`);
80+
});
81+
82+
it("renders multiple query operations", async () => {
83+
const { getCount, getName } = await tester.compile(
84+
t.code`
85+
op ${t.op("getCount")}(): int32;
86+
op ${t.op("getName")}(): string;
87+
`,
88+
);
89+
90+
const sdl = renderComponentToSDL(
91+
tester.program,
92+
<QueryType operations={[getCount, getName]} />,
93+
{ skipPlaceholderQuery: true },
94+
);
95+
96+
expect(sdl).toMatchInlineSnapshot(`
97+
"type Query {
98+
getCount: Int!
99+
getName: String!
100+
}"
101+
`);
102+
});
103+
104+
it("renders query with parameters", async () => {
105+
const { search } = await tester.compile(
106+
t.code`op ${t.op("search")}(query: string, limit?: int32): string[];`,
107+
);
108+
109+
const sdl = renderComponentToSDL(
110+
tester.program,
111+
<QueryType operations={[search]} />,
112+
{ skipPlaceholderQuery: true },
113+
);
114+
115+
expect(sdl).toMatchInlineSnapshot(`
116+
"type Query {
117+
search(query: String!, limit: Int!): [String!]!
118+
}"
119+
`);
120+
});
121+
});
122+
123+
describe("MutationType component", () => {
124+
let tester: Awaited<ReturnType<typeof Tester.createInstance>>;
125+
beforeEach(async () => {
126+
tester = await Tester.createInstance();
127+
});
128+
129+
it("renders nothing when no operations", async () => {
130+
await tester.compile(t.code`model Placeholder { id: string; }`);
131+
132+
const sdl = renderComponentToSDL(tester.program, <MutationType operations={[]} />);
133+
134+
// Should only contain the placeholder Query from test utils
135+
expect(sdl).toMatchInlineSnapshot(`
136+
"type Query {
137+
_placeholder: Boolean
138+
}"
139+
`);
140+
});
141+
142+
it("renders single mutation operation", async () => {
143+
const { deleteItem } = await tester.compile(
144+
t.code`op ${t.op("deleteItem")}(id: string): boolean;`,
145+
);
146+
147+
const sdl = renderComponentToSDL(
148+
tester.program,
149+
<MutationType operations={[deleteItem]} />,
150+
);
151+
152+
expect(sdl).toMatchInlineSnapshot(`
153+
"type Mutation {
154+
deleteItem(id: String!): Boolean!
155+
}
156+
157+
type Query {
158+
_placeholder: Boolean
159+
}"
160+
`);
161+
});
162+
163+
it("renders mutation with input parameters", async () => {
164+
const { createUser } = await tester.compile(
165+
t.code`
166+
model ${t.model("User")} { id: string; name: string; }
167+
op ${t.op("createUser")}(name: string, email: string): User;
168+
`,
169+
);
170+
171+
const sdl = renderComponentToSDL(
172+
tester.program,
173+
<>
174+
<gql.ObjectType name="User">
175+
<gql.Field name="id" type={gql.String} nonNull />
176+
<gql.Field name="name" type={gql.String} nonNull />
177+
</gql.ObjectType>
178+
<MutationType operations={[createUser]} />
179+
</>,
180+
);
181+
182+
expect(sdl).toMatchInlineSnapshot(`
183+
"type User {
184+
id: String!
185+
name: String!
186+
}
187+
188+
type Mutation {
189+
createUser(name: String!, email: String!): User!
190+
}
191+
192+
type Query {
193+
_placeholder: Boolean
194+
}"
195+
`);
196+
});
197+
});
198+
199+
describe("SubscriptionType component", () => {
200+
let tester: Awaited<ReturnType<typeof Tester.createInstance>>;
201+
beforeEach(async () => {
202+
tester = await Tester.createInstance();
203+
});
204+
205+
it("renders nothing when no operations", async () => {
206+
await tester.compile(t.code`model Placeholder { id: string; }`);
207+
208+
const sdl = renderComponentToSDL(
209+
tester.program,
210+
<SubscriptionType operations={[]} />,
211+
);
212+
213+
// Should only contain the placeholder Query from test utils
214+
expect(sdl).toMatchInlineSnapshot(`
215+
"type Query {
216+
_placeholder: Boolean
217+
}"
218+
`);
219+
});
220+
221+
it("renders single subscription operation", async () => {
222+
const { onMessage } = await tester.compile(
223+
t.code`op ${t.op("onMessage")}(): string;`,
224+
);
225+
226+
const sdl = renderComponentToSDL(
227+
tester.program,
228+
<SubscriptionType operations={[onMessage]} />,
229+
);
230+
231+
expect(sdl).toMatchInlineSnapshot(`
232+
"type Subscription {
233+
onMessage: String!
234+
}
235+
236+
type Query {
237+
_placeholder: Boolean
238+
}"
239+
`);
240+
});
241+
});

packages/graphql/test/e2e.test.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { strictEqual } from "node:assert";
2-
import { describe, it } from "vitest";
1+
import { describe, expect, it } from "vitest";
32
import { emitSingleSchemaWithDiagnostics } from "./test-host.js";
43

54
/**
@@ -24,8 +23,19 @@ describe("End-to-end", () => {
2423

2524
const result = await emitSingleSchemaWithDiagnostics(code, {});
2625
const errors = result.diagnostics.filter((d) => d.severity === "error");
27-
strictEqual(errors.length, 0, "Should have no errors for valid schema");
28-
strictEqual(result.graphQLOutput?.includes("type Book {"), true, "Should contain Book type");
29-
strictEqual(result.graphQLOutput?.includes("type Query {"), true, "Should contain Query type");
26+
27+
expect(errors).toHaveLength(0);
28+
expect(result.graphQLOutput).toMatchInlineSnapshot(`
29+
"type Book {
30+
id: String!
31+
title: String!
32+
}
33+
34+
type Query {
35+
getBook(id: String!): Book!
36+
}
37+
38+
"
39+
`);
3040
});
3141
});

0 commit comments

Comments
 (0)