Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/graphql-testing.md
1# GraphQL Testing23## Table of Contents451. [Patterns](#patterns)62. [Anti-Patterns](#anti-patterns)73. [Troubleshooting](#troubleshooting)89> **When to use**: Testing GraphQL APIs — queries, mutations, variables, and error handling.1011## Patterns1213### Basic Query with Variables1415All GraphQL requests go through `POST` to a single endpoint. Send `query`, `variables`, and optionally `operationName` in the JSON body.1617```typescript18import { test, expect } from "@playwright/test";1920const GQL_ENDPOINT = "/graphql";2122test("query with variables", async ({ request }) => {23const resp = await request.post(GQL_ENDPOINT, {24data: {25query: `26query FetchItem($id: ID!) {27item(id: $id) {28id29title30price31reviews { id rating }32}33}34`,35variables: { id: "101" },36},37});3839expect(resp.ok()).toBeTruthy();40const { data, errors } = await resp.json();4142// GraphQL returns 200 even on errors — always check both43expect(errors).toBeUndefined();44expect(data.item).toMatchObject({45id: "101",46title: expect.any(String),47price: expect.any(Number),48});49expect(data.item.reviews).toEqual(50expect.arrayContaining([51expect.objectContaining({52id: expect.any(String),53rating: expect.any(Number),54}),55])56);57});58```5960### Mutations6162```typescript63import { test, expect } from "@playwright/test";6465const GQL_ENDPOINT = "/graphql";6667test("mutation creates resource", async ({ request }) => {68const resp = await request.post(GQL_ENDPOINT, {69data: {70query: `71mutation AddItem($input: ItemInput!) {72addItem(input: $input) {73id74title75status76}77}78`,79variables: {80input: {81title: "New Widget",82price: 15.0,83status: "DRAFT",84},85},86},87});8889const { data, errors } = await resp.json();90expect(errors).toBeUndefined();91expect(data.addItem).toMatchObject({92id: expect.any(String),93title: "New Widget",94status: "DRAFT",95});96});97```9899### Validation Errors100101```typescript102import { test, expect } from "@playwright/test";103104const GQL_ENDPOINT = "/graphql";105106test("handles validation errors", async ({ request }) => {107const resp = await request.post(GQL_ENDPOINT, {108data: {109query: `110mutation AddItem($input: ItemInput!) {111addItem(input: $input) { id }112}113`,114variables: { input: { title: "" } },115},116});117118const { data, errors } = await resp.json();119expect(errors).toBeDefined();120expect(errors.length).toBeGreaterThan(0);121expect(errors[0].message).toContain("title");122expect(errors[0].extensions?.code).toBe("BAD_USER_INPUT");123});124```125126### Authorization Errors127128```typescript129import { test, expect } from "@playwright/test";130131const GQL_ENDPOINT = "/graphql";132133test("handles authorization errors", async ({ request }) => {134const resp = await request.post(GQL_ENDPOINT, {135data: {136query: `137query AdminDashboard {138adminMetrics { revenue activeUsers }139}140`,141},142});143144const { data, errors } = await resp.json();145expect(errors).toBeDefined();146expect(errors[0].extensions?.code).toBe("UNAUTHORIZED");147expect(data?.adminMetrics).toBeNull();148});149```150151### Authenticated GraphQL Fixture152153```typescript154// fixtures/graphql-fixtures.ts155import { test as base, expect, APIRequestContext } from "@playwright/test";156157type GraphQLFixtures = {158gqlClient: APIRequestContext;159adminGqlClient: APIRequestContext;160};161162export const test = base.extend<GraphQLFixtures>({163gqlClient: async ({ playwright }, use) => {164const ctx = await playwright.request.newContext({165baseURL: "https://api.myapp.io",166extraHTTPHeaders: {167Authorization: `Bearer ${process.env.API_TOKEN}`,168"Content-Type": "application/json",169},170});171await use(ctx);172await ctx.dispose();173},174175adminGqlClient: async ({ playwright }, use) => {176const loginCtx = await playwright.request.newContext({177baseURL: "https://api.myapp.io",178});179const loginResp = await loginCtx.post("/graphql", {180data: {181query: `182mutation Login($email: String!, $password: String!) {183login(email: $email, password: $password) { token }184}185`,186variables: {187email: process.env.ADMIN_EMAIL,188password: process.env.ADMIN_PASSWORD,189},190},191});192const { data } = await loginResp.json();193194if (!data?.login?.token) {195throw new Error(`Admin login failed: status ${loginResp.status()}, response: ${JSON.stringify(data)}`);196}197198await loginCtx.dispose();199200const ctx = await playwright.request.newContext({201baseURL: "https://api.myapp.io",202extraHTTPHeaders: {203Authorization: `Bearer ${data.login.token}`,204"Content-Type": "application/json",205},206});207await use(ctx);208await ctx.dispose();209},210});211212export { expect };213```214215### GraphQL Helper Function216217```typescript218// utils/graphql.ts219import { APIRequestContext, expect } from "@playwright/test";220221export async function gqlQuery<T = any>(222request: APIRequestContext,223query: string,224variables?: Record<string, any>225): Promise<{ data: T; errors?: any[] }> {226const resp = await request.post("/graphql", {227data: { query, variables },228});229expect(resp.ok()).toBeTruthy();230return resp.json();231}232233export async function gqlMutation<T = any>(234request: APIRequestContext,235mutation: string,236variables?: Record<string, any>237): Promise<{ data: T; errors?: any[] }> {238return gqlQuery<T>(request, mutation, variables);239}240```241242```typescript243// tests/api/items.spec.ts244import { test, expect } from "@playwright/test";245import { gqlQuery, gqlMutation } from "../../utils/graphql";246247test("fetch and update item", async ({ request }) => {248const { data: fetchData } = await gqlQuery(249request,250`query GetItem($id: ID!) { item(id: $id) { id title } }`,251{ id: "101" }252);253expect(fetchData.item.title).toBeDefined();254255const { data: updateData, errors } = await gqlMutation(256request,257`mutation UpdateItem($id: ID!, $title: String!) {258updateItem(id: $id, title: $title) { id title }259}`,260{ id: "101", title: "Updated Title" }261);262expect(errors).toBeUndefined();263expect(updateData.updateItem.title).toBe("Updated Title");264});265```266267## Anti-Patterns268269| Don't Do This | Problem | Do This Instead |270| --- | --- | --- |271| Check only `response.ok()` | GraphQL returns 200 even on errors — `errors` array is the real signal | Always check both `data` and `errors` in the response body |272| Ignore `errors` array | Validation and auth errors appear in `errors`, not HTTP status | Destructure and assert: `expect(errors).toBeUndefined()` |273| Hardcode query strings inline everywhere | Duplicated queries are hard to maintain | Extract queries to constants or use a helper function |274| Skip variable validation | Invalid variables cause cryptic server errors | Validate input shape before sending |275276## Troubleshooting277278### GraphQL returns 200 but data is null279280**Cause**: GraphQL servers return HTTP 200 even when the query has errors. The actual error is in the `errors` array.281282**Fix**: Always destructure and check both `data` and `errors`.283284```typescript285const { data, errors } = await resp.json();286if (errors) {287console.error("GraphQL errors:", JSON.stringify(errors, null, 2));288}289expect(errors).toBeUndefined();290expect(data.item).toBeDefined();291```292293### "Cannot query field X on type Y"294295**Cause**: The field doesn't exist in the schema, or you're querying the wrong type.296297**Fix**: Verify the schema. Use introspection or check your GraphQL IDE for available fields.298299```typescript300// Introspection query to debug schema301const { data } = await request.post("/graphql", {302data: {303query: `{ __type(name: "Item") { fields { name type { name } } } }`,304},305});306console.log(data.__type.fields);307```308309### Variables not being applied310311**Cause**: Variable names in the query don't match the `variables` object keys, or types don't match.312313**Fix**: Ensure variable names match exactly (case-sensitive) and types align with the schema.314315```typescript316// Wrong: variable name mismatch317const resp = await request.post("/graphql", {318data: {319query: `query GetItem($itemId: ID!) { item(id: $itemId) { id } }`,320variables: { id: "101" }, // Should be { itemId: "101" }321},322});323324// Correct325const resp = await request.post("/graphql", {326data: {327query: `query GetItem($itemId: ID!) { item(id: $itemId) { id } }`,328variables: { itemId: "101" },329},330});331```332