Complete Claude Code plugin with: - 9 skills (/pw:init, generate, review, fix, migrate, coverage, testrail, browserstack, report) - 3 specialized agents (test-architect, test-debugger, migration-planner) - 55 test case templates across 11 categories (auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility) - TestRail MCP server (TypeScript) — 8 tools for bidirectional sync - BrowserStack MCP server (TypeScript) — 7 tools for cross-browser testing - Smart hooks (auto-validate tests, auto-detect Playwright projects) - 6 curated reference docs (golden rules, locators, assertions, fixtures, pitfalls, flaky tests) - Leverages Claude Code built-ins (/batch, /debug, Explore subagent) - Zero-config for core features; TestRail/BrowserStack via env vars - Both TypeScript and JavaScript support throughout Co-authored-by: Leo <leo@openclaw.ai>
175 lines
5.9 KiB
Markdown
175 lines
5.9 KiB
Markdown
# GraphQL API Template
|
|
|
|
Tests query, mutation, and subscription via Playwright's request API.
|
|
|
|
## Prerequisites
|
|
- Valid auth token: `{{apiToken}}`
|
|
- GraphQL endpoint: `{{graphqlEndpoint}}`
|
|
- WebSocket endpoint for subscriptions: `{{graphqlWsEndpoint}}`
|
|
|
|
---
|
|
|
|
## TypeScript
|
|
|
|
```typescript
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
const GQL_URL = '{{graphqlEndpoint}}';
|
|
const headers = {
|
|
'Authorization': `Bearer {{apiToken}}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
async function gql(request: any, query: string, variables = {}) {
|
|
const res = await request.post(GQL_URL, { headers, data: { query, variables } });
|
|
const body = await res.json();
|
|
expect(body.errors).toBeUndefined();
|
|
return body.data;
|
|
}
|
|
|
|
test.describe('GraphQL API', () => {
|
|
// Happy path: query
|
|
test('query fetches {{entityName}} list', async ({ request }) => {
|
|
const data = await gql(request, `
|
|
query Get{{EntityName}}s($limit: Int) {
|
|
{{entityName}}s(limit: $limit) { id name createdAt }
|
|
}
|
|
`, { limit: 10 });
|
|
expect(Array.isArray(data.{{entityName}}s)).toBe(true);
|
|
expect(data.{{entityName}}s.length).toBeLessThanOrEqual(10);
|
|
});
|
|
|
|
// Happy path: query single entity
|
|
test('query fetches single {{entityName}} by id', async ({ request }) => {
|
|
const data = await gql(request, `
|
|
query Get{{EntityName}}($id: ID!) {
|
|
{{entityName}}(id: $id) { id name description }
|
|
}
|
|
`, { id: '{{existingEntityId}}' });
|
|
expect(data.{{entityName}}.id).toBe('{{existingEntityId}}');
|
|
});
|
|
|
|
// Happy path: mutation creates entity
|
|
test('mutation creates {{entityName}}', async ({ request }) => {
|
|
const data = await gql(request, `
|
|
mutation Create{{EntityName}}($input: {{EntityName}}Input!) {
|
|
create{{EntityName}}(input: $input) { id name }
|
|
}
|
|
`, { input: { name: '{{testEntityName}}', description: '{{testDescription}}' } });
|
|
expect(data.create{{EntityName}}.id).toBeTruthy();
|
|
expect(data.create{{EntityName}}.name).toBe('{{testEntityName}}');
|
|
});
|
|
|
|
// Happy path: mutation updates entity
|
|
test('mutation updates {{entityName}}', async ({ request }) => {
|
|
const data = await gql(request, `
|
|
mutation Update{{EntityName}}($id: ID!, $input: {{EntityName}}Input!) {
|
|
update{{EntityName}}(id: $id, input: $input) { id name }
|
|
}
|
|
`, { id: '{{existingEntityId}}', input: { name: '{{updatedName}}' } });
|
|
expect(data.update{{EntityName}}.name).toBe('{{updatedName}}');
|
|
});
|
|
|
|
// Happy path: mutation deletes entity
|
|
test('mutation deletes {{entityName}}', async ({ request }) => {
|
|
const data = await gql(request, `
|
|
mutation Delete{{EntityName}}($id: ID!) {
|
|
delete{{EntityName}}(id: $id) { success }
|
|
}
|
|
`, { id: '{{deletableEntityId}}' });
|
|
expect(data.delete{{EntityName}}.success).toBe(true);
|
|
});
|
|
|
|
// Error case: invalid query returns errors array
|
|
test('invalid query returns errors', async ({ request }) => {
|
|
const res = await request.post(GQL_URL, {
|
|
headers,
|
|
data: { query: '{ invalidField }' },
|
|
});
|
|
const body = await res.json();
|
|
expect(body.errors).toBeDefined();
|
|
expect(body.errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// Error case: unauthorized query
|
|
test('query without auth returns unauthorized error', async ({ request }) => {
|
|
const res = await request.post(GQL_URL, {
|
|
headers: { 'Content-Type': 'application/json' }, // No auth
|
|
data: { query: '{ {{entityName}}s { id } }' },
|
|
});
|
|
const body = await res.json();
|
|
expect(body.errors?.[0]?.extensions?.code).toMatch(/UNAUTHENTICATED|UNAUTHORIZED/);
|
|
});
|
|
|
|
// Edge case: subscription via page WebSocket
|
|
test('subscription receives real-time update', async ({ page }) => {
|
|
await page.goto('{{baseUrl}}/dashboard');
|
|
const received: any[] = [];
|
|
await page.evaluate(() => {
|
|
const ws = new WebSocket('{{graphqlWsEndpoint}}');
|
|
ws.onmessage = e => (window as any).__gqlMsg = JSON.parse(e.data);
|
|
});
|
|
// Trigger mutation to fire subscription
|
|
await page.request.post(GQL_URL, {
|
|
headers,
|
|
data: { query: 'mutation { trigger{{EntityName}}Event { id } }' },
|
|
});
|
|
const msg = await page.evaluate(() => (window as any).__gqlMsg);
|
|
expect(msg?.type).toBe('data');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## JavaScript
|
|
|
|
```javascript
|
|
const { test, expect } = require('@playwright/test');
|
|
|
|
const headers = { 'Authorization': `Bearer {{apiToken}}`, 'Content-Type': 'application/json' };
|
|
|
|
async function gql(request, query, variables = {}) {
|
|
const res = await request.post('{{graphqlEndpoint}}', { headers, data: { query, variables } });
|
|
const body = await res.json();
|
|
expect(body.errors).toBeUndefined();
|
|
return body.data;
|
|
}
|
|
|
|
test.describe('GraphQL API', () => {
|
|
test('query fetches entity list', async ({ request }) => {
|
|
const data = await gql(request, '{ {{entityName}}s { id name } }');
|
|
expect(Array.isArray(data.{{entityName}}s)).toBe(true);
|
|
});
|
|
|
|
test('mutation creates entity', async ({ request }) => {
|
|
const data = await gql(request,
|
|
'mutation($input: {{EntityName}}Input!) { create{{EntityName}}(input: $input) { id } }',
|
|
{ input: { name: '{{testEntityName}}' } }
|
|
);
|
|
expect(data.create{{EntityName}}.id).toBeTruthy();
|
|
});
|
|
|
|
test('invalid query returns errors array', async ({ request }) => {
|
|
const res = await request.post('{{graphqlEndpoint}}', {
|
|
headers,
|
|
data: { query: '{ nonExistentField }' },
|
|
});
|
|
const body = await res.json();
|
|
expect(body.errors?.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Variants
|
|
| Variant | Description |
|
|
|---------|-------------|
|
|
| List query | Returns array of entities |
|
|
| Single query | Returns entity by ID |
|
|
| Create mutation | Returns new entity with ID |
|
|
| Update mutation | Returns updated field value |
|
|
| Delete mutation | Returns success: true |
|
|
| Invalid query | errors[] defined in response |
|
|
| Unauthenticated | UNAUTHENTICATED extension code |
|
|
| Subscription | Real-time message via WebSocket |
|