@tzurp/openapi-skills 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +230 -0
  3. package/bin/openapi-skills +4 -0
  4. package/dist/cli.d.ts +37 -0
  5. package/dist/cli.js +1646 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/client-schema-builder.d.ts +35 -0
  8. package/dist/client-schema-builder.js +268 -0
  9. package/dist/client-schema-builder.js.map +1 -0
  10. package/dist/helper/base64url-json.d.ts +3 -0
  11. package/dist/helper/base64url-json.js +28 -0
  12. package/dist/helper/base64url-json.js.map +1 -0
  13. package/dist/helper/dotNotation.d.ts +3 -0
  14. package/dist/helper/dotNotation.js +34 -0
  15. package/dist/helper/dotNotation.js.map +1 -0
  16. package/dist/helper/endpoint-filter.d.ts +16 -0
  17. package/dist/helper/endpoint-filter.js +165 -0
  18. package/dist/helper/endpoint-filter.js.map +1 -0
  19. package/dist/helper/endpoint-utils.d.ts +2 -0
  20. package/dist/helper/endpoint-utils.js +10 -0
  21. package/dist/helper/endpoint-utils.js.map +1 -0
  22. package/dist/helper/error-codes.d.ts +27 -0
  23. package/dist/helper/error-codes.js +116 -0
  24. package/dist/helper/error-codes.js.map +1 -0
  25. package/dist/helper/error-formatter.d.ts +46 -0
  26. package/dist/helper/error-formatter.js +72 -0
  27. package/dist/helper/error-formatter.js.map +1 -0
  28. package/dist/helper/get-operation-filter.d.ts +17 -0
  29. package/dist/helper/get-operation-filter.js +26 -0
  30. package/dist/helper/get-operation-filter.js.map +1 -0
  31. package/dist/helper/graphql.d.ts +40 -0
  32. package/dist/helper/graphql.js +967 -0
  33. package/dist/helper/graphql.js.map +1 -0
  34. package/dist/helper/http-client.d.ts +28 -0
  35. package/dist/helper/http-client.js +90 -0
  36. package/dist/helper/http-client.js.map +1 -0
  37. package/dist/helper/json-updater.d.ts +17 -0
  38. package/dist/helper/json-updater.js +195 -0
  39. package/dist/helper/json-updater.js.map +1 -0
  40. package/dist/helper/logger.d.ts +13 -0
  41. package/dist/helper/logger.js +56 -0
  42. package/dist/helper/logger.js.map +1 -0
  43. package/dist/helper/output-size.d.ts +5 -0
  44. package/dist/helper/output-size.js +7 -0
  45. package/dist/helper/output-size.js.map +1 -0
  46. package/dist/helper/parameter-schema.d.ts +14 -0
  47. package/dist/helper/parameter-schema.js +36 -0
  48. package/dist/helper/parameter-schema.js.map +1 -0
  49. package/dist/helper/paths.d.ts +11 -0
  50. package/dist/helper/paths.js +47 -0
  51. package/dist/helper/paths.js.map +1 -0
  52. package/dist/helper/prompt-delete.d.ts +1 -0
  53. package/dist/helper/prompt-delete.js +13 -0
  54. package/dist/helper/prompt-delete.js.map +1 -0
  55. package/dist/helper/request-artifacts.d.ts +14 -0
  56. package/dist/helper/request-artifacts.js +60 -0
  57. package/dist/helper/request-artifacts.js.map +1 -0
  58. package/dist/helper/request-preparation.d.ts +23 -0
  59. package/dist/helper/request-preparation.js +85 -0
  60. package/dist/helper/request-preparation.js.map +1 -0
  61. package/dist/helper/sanitizer.d.ts +1 -0
  62. package/dist/helper/sanitizer.js +17 -0
  63. package/dist/helper/sanitizer.js.map +1 -0
  64. package/dist/helper/update-check.d.ts +1 -0
  65. package/dist/helper/update-check.js +36 -0
  66. package/dist/helper/update-check.js.map +1 -0
  67. package/dist/index.d.ts +50 -0
  68. package/dist/index.js +163 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/install-skill.d.ts +5 -0
  71. package/dist/install-skill.js +139 -0
  72. package/dist/install-skill.js.map +1 -0
  73. package/dist/parser.d.ts +12 -0
  74. package/dist/parser.js +393 -0
  75. package/dist/parser.js.map +1 -0
  76. package/dist/validate-response.d.ts +47 -0
  77. package/dist/validate-response.js +1017 -0
  78. package/dist/validate-response.js.map +1 -0
  79. package/package.json +63 -0
  80. package/skill-templates/SKILL.md +204 -0
  81. package/skill-templates/create-endpoint-test.md +413 -0
  82. package/skill-templates/http-client.md +141 -0
  83. package/skill-templates/write-client-code.md +219 -0
@@ -0,0 +1,413 @@
1
+ # API Operation Test Generation Skill
2
+
3
+ ## Purpose
4
+ Generate live integration tests for schema operations that exercise and validate the generated client code against a real API.
5
+
6
+ ## Core Principle
7
+ **All generated tests MUST call the operation exclusively through the generated client class. NO direct HTTP calls (fetch, axios, supertest, etc.). NO mocks or test doubles.**
8
+
9
+ This ensures:
10
+ - Client code is exercised in realistic scenarios
11
+ - Tests validate actual API behavior, not mocked behavior
12
+ - Tests are maintainable and match client structure
13
+
14
+ ## References (Required Dependencies)
15
+ - `/references/write-client-code.md` — Instructions and examples for generating the required client code class
16
+ - `/references/http-client.md` — for FetchClient interface specification
17
+
18
+ ---
19
+
20
+ ## File Header Requirement
21
+
22
+ Every generated test file must start with:
23
+ ```typescript
24
+ // Generated from <apiName> API schema by openapi-skills.
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Agent Workflow (Decision Tree)
30
+
31
+ **FOLLOW THIS SEQUENCE IN ORDER:**
32
+
33
+ ### Step 1: Gather Prerequisites
34
+ 1. **Ask for testing framework** (if not specified):
35
+ - Options: Jest, Vitest, Mocha, Node test runner, Playwright
36
+ - Required before proceeding
37
+ 2. **Check for client code**:
38
+ - It is mandatory to verify whether the generated client file exists in the workspace
39
+ - If the client code file is missing or incomplete, it is mandatory to generate the client code by following `/references/write-client-code.md` before creating any test
40
+ - Do NOT proceed without client code
41
+ 3. **Verify API is parsed** (`openapi-skills get-api-names`):
42
+ - Extract `apiName` from output
43
+ - If not found, offer to parse it
44
+
45
+ ### Step 2: Retrieve Operation Metadata
46
+ 1. Run `openapi-skills list --api <apiName>` with at least one filter. Valid filters include `--method`, `--path`, `--tag`, `--root-type`, `--filter`, and `--index`. Use the schema-appropriate filters; `--filter`, `--tag`, and `--index` can be combined with either schema.
47
+ 2. Extract:
48
+ - OpenAPI: `operationId`, `method`, `path`
49
+ - GraphQL: `name`, `rootType`
50
+ 3. Note: If the operation returns multiple response codes or outcomes, plan error tests for each relevant branch
51
+
52
+ ### Step 3: Optional Live Data Check
53
+ If a base URL is configured, try `openapi-skills request <operationName> --api <apiName>` once to capture real request/response examples. If the call succeeds, use those artifacts as reference when writing tests. If it fails repeatedly with 400/500 errors, ask the user whether to skip this step and continue with schema-only data, known from the client code in Step 1.
54
+ For multipart or file-upload operations, inspect the request template and make sure the test uses a real file path or file descriptor for the binary field instead of a plain string body. If you know the uploaded file, derive `fileName` and `mimeType` from that file context instead of leaving them blank.
55
+
56
+ ### Step 4: Generate Test File
57
+ 1. Ensure `/tests` directory exists (create if needed)
58
+ 2. Generate tests using rules in next sections
59
+ 3. Filename: `<operationName>.test.ts`
60
+
61
+ ---
62
+
63
+ ## Setup: FetchClient Helper (User Responsibility)
64
+
65
+ **One-time setup per project:**
66
+ 1. Copy the `FetchClient` interface from `/references/http-client.md`
67
+ 2. Save as a separate file `tests/fetch-client.ts` in your project root
68
+ 3. Agent assumes this file exists when generating tests
69
+
70
+ **Config File Reference:**
71
+ - Location: `.openapi-skills/config.json` (relative to project root)
72
+ - Contains API base URL at: `apis.<apiName>.baseUrl`
73
+ - Example:
74
+ ```json
75
+ {
76
+ "apis": {
77
+ "petstore": {
78
+ "baseUrl": "https://api.petstore.com",
79
+ "auth": { "Authorization": "Bearer token..." }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Test Types (Definitions & Requirements)
88
+
89
+ ### 1. Success Test (Happy Path)
90
+ **Definition:** Operation is called with valid input; API returns a successful response.
91
+
92
+ **Generates when:** Always (every operation has at least one success scenario)
93
+
94
+ **What to assert:**
95
+ - Response structure matches client return type (check keys/types exist)
96
+ - Response status is 2xx
97
+ - Key fields have expected values (use client types or API schema constraints)
98
+ - For upload endpoints, assert the request succeeds with a real file fixture and a 2xx response, even if the API does not echo the filename back.
99
+
100
+ **Example triggers:**
101
+ - GET operation with path `{id}` → provide valid ID, expect pet details
102
+ - POST operation → provide valid request body, expect 201 + resource ID
103
+
104
+ **Minimum assertions:**
105
+ ```typescript
106
+ const result = await petClient.getPetById(1);
107
+ expect(result).toBeDefined();
108
+ expect(result.id).toBe(1); // or typeof check if value varies
109
+ expect(result.name).toBeDefined();
110
+ ```
111
+
112
+ ### 2. Error Test (Failure Scenarios)
113
+ **Definition:** Operation is called with invalid input or encounters an error; API returns an error response.
114
+
115
+ **Generates when:** Operation defines non-2xx responses (400, 404, 500, etc.)
116
+
117
+ **How many:** One test per distinct error response type (one 404 test, one 400 test, etc.)
118
+
119
+ **What to assert:**
120
+ - Response structure matches error type from client (e.g., `FindPetError`)
121
+ - Response status code matches expected error (404, 400, etc.)
122
+ - Error message/details are present
123
+
124
+ **Example triggers:**
125
+ - GET `/pet/{id}` with invalid ID → expect 404 error response
126
+ - POST with malformed body → expect 400 error response
127
+ - API rate limit exceeded → expect 429 error response
128
+
129
+ **Minimum assertions:**
130
+ ```typescript
131
+ const result = await petClient.getPetById(999); // invalid ID
132
+ expect(result.error).toBeDefined();
133
+ expect(result.status).toBe(404);
134
+ ```
135
+
136
+ ### 3. Validation Test (Input Constraints)
137
+ **Definition:** Operation requires specific input validation per API schema (e.g., required fields, min/max values, enum constraints).
138
+
139
+ **Generates when:** API schema includes validation rules (required properties, minLength, minimum value, enum, pattern, etc.)
140
+
141
+ **What to assert:**
142
+ - Omitting required field → API returns 400 error
143
+ - Providing value outside min/max range → API returns 400 error
144
+ - Providing invalid enum value → API returns 400 error
145
+
146
+ **Example triggers:**
147
+ - POST body missing required `name` field → expect 400
148
+ - Query param `age` with value `-5` (minimum is 0) → expect 400
149
+ - Path param with non-numeric value when integer required → expect 400
150
+
151
+ **Minimum assertions:**
152
+ ```typescript
153
+ // Test: missing required field
154
+ const result = await petClient.createPet({}); // missing name
155
+ expect(result.error).toBeDefined();
156
+ expect(result.error).toContain('required'); // or similar
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Test Generation Rules
162
+
163
+ ### File Structure
164
+ - **Location:** `/tests` directory
165
+ - **Filename:** `<operationName>.test.ts`
166
+ - **First line:** `// Generated from <apiName> API schema by openapi-skills.`
167
+
168
+ ### Client Usage Requirement
169
+ - All tests MUST invoke the client's method for the operation under test
170
+ - Import the client class: `import { PetstorePetClient } from '../client/PetstorePetClient'`
171
+ - Instantiate with `FetchClient`: `const client = new PetstorePetClient(httpClient)`
172
+ - Call method: `const result = await client.findPetById(1)`
173
+ - No direct HTTP calls, mocks, or test doubles
174
+
175
+ ### FetchClient Setup in Tests
176
+ ```typescript
177
+ import fs from 'fs-extra';
178
+ import path from 'path';
179
+ import { FetchClient } from './fetch-client';
180
+ import { PetstorePetClient } from '../client/PetstorePetClient';
181
+
182
+ const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
183
+ const apiBaseUrl = config.apis.petstore.baseUrl;
184
+
185
+ const httpClient = new FetchClient(apiBaseUrl);
186
+ const petstorePetClient = new PetstorePetClient(httpClient);
187
+ ```
188
+
189
+ ### Test Count per Operation
190
+ - **Success test:** Always (1)
191
+ - **Error tests:** One per non-2xx response code defined for the operation (0 to N)
192
+ - **Validation tests:** One per input validation rule if applicable (0 to N)
193
+ - **Minimum:** 1 (success); 2-3 typical
194
+
195
+ ### Assertions Strategy
196
+ 1. **Structure assertions:** Verify response keys/properties exist
197
+ 2. **Type assertions:** Verify values match expected types (number, string, etc.)
198
+ 3. **Value assertions:** Use realistic values from API constraints or live request data
199
+ 4. **Status assertions:** Explicitly check response status code (2xx for success, specific codes for errors)
200
+
201
+ Example:
202
+ ```typescript
203
+ const result = await petClient.getPetById(1);
204
+ expect(result).toBeDefined();
205
+ expect(typeof result.id).toBe('number');
206
+ expect(result.id).toBe(1);
207
+ expect(result.name).toBeDefined();
208
+ ```
209
+
210
+ ### Error Handling
211
+ - Derive error response types from client return union (e.g., `FindPetByIdError`)
212
+ - Assert error structure matches client interface
213
+ - Include status code and error message in assertions
214
+ - Do NOT use try/catch for error tests; assert on response object
215
+
216
+ Example:
217
+ ```typescript
218
+ const result = await petClient.getPetById(999);
219
+ expect(result.error).toBeDefined();
220
+ expect(result.status).toBe(404);
221
+ expect(result.error.message).toContain('not found');
222
+ ```
223
+
224
+ ### Client Variable Naming
225
+ - Use descriptive, operation-specific names
226
+ - Format: `<ApiName><Operation>Client`
227
+ - Examples: `petstorePetClient`, `githubRepoClient`, `stripePaymentClient`
228
+ - NOT: `client`, `c`, `httpClient`, `apiClient`
229
+
230
+ ---
231
+
232
+ ## Framework Support
233
+
234
+ ### Supported Frameworks
235
+ - **Jest** — use `test()`, `expect()`
236
+ - **Vitest** — use `test()`, `expect()`
237
+ - **Mocha** — use `it()`, `expect()` with assertion library
238
+ - **Node test runner** — use `test()`, `assert()`
239
+ - **Playwright** — use `test()`, `expect()` from `@playwright/test`
240
+
241
+ ### Framework Agnostic Pattern
242
+
243
+ All frameworks must follow this structure:
244
+
245
+ ```typescript
246
+ import fs from 'fs-extra';
247
+ import path from 'path';
248
+ import { FetchClient } from './fetch-client';
249
+ import { PetstorePetClient } from '../client/PetstorePetClient';
250
+
251
+ const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
252
+ const apiBaseUrl = config.apis.petstore.baseUrl;
253
+ const httpClient = new FetchClient(apiBaseUrl);
254
+ const petstorePetClient = new PetstorePetClient(httpClient);
255
+
256
+ test('getPetById returns pet', async () => {
257
+ const result = await petstorePetClient.getPetById(1);
258
+ expect(result.id).toBe(1);
259
+ });
260
+
261
+ test('getPetById returns 404 for invalid id', async () => {
262
+ const result = await petstorePetClient.getPetById(999);
263
+ expect(result.error).toBeDefined();
264
+ });
265
+ ```
266
+
267
+ ### Playwright API Testing
268
+ When user selects Playwright:
269
+
270
+ ```typescript
271
+ import { test, expect } from '@playwright/test';
272
+ import fs from 'fs-extra';
273
+ import path from 'path';
274
+ import { FetchClient } from './fetch-client';
275
+ import { PetstorePetClient } from '../client/PetstorePetClient';
276
+
277
+ test('getPetById returns pet', async ({ request }) => {
278
+ const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
279
+ const apiBaseUrl = config.apis.petstore.baseUrl;
280
+ // Passing Playwright's request:APIRequestContext enables FetchClient.fetchWithContext()
281
+ const httpClient = new FetchClient(apiBaseUrl, request);
282
+ const petstorePetClient = new PetstorePetClient(httpClient);
283
+ const result = await petstorePetClient.getPetById(1);
284
+ expect(result.id).toBe(1);
285
+ });
286
+ ```
287
+
288
+ **Important:** Playwright tests are API-only (not browser/UI tests). Use same client-driven pattern as other frameworks.
289
+
290
+ ---
291
+
292
+ ## Error Handling & Troubleshooting
293
+
294
+ ### Common Issues
295
+
296
+ | Issue | Solution |
297
+ |-------|----------|
298
+ | **FetchClient import fails** | Verify `tests/fetch-client.ts` exists and copied from `/references/http-client.md` |
299
+ | **Config file not found** | Verify `.openapi-skills/config.json` exists in project root |
300
+ | **API base URL wrong** | Check `apis.<apiName>.baseUrl` in config.json matches live API |
301
+ | **Client class import fails** | Verify client code generated first using `/references/write-client-code.md` |
302
+ | **Tests fail at runtime** | Ensure live API is accessible; tests perform real HTTP requests |
303
+ | **Response doesn't match type** | Verify API schema is accurate; report schema issues to API maintainer |
304
+
305
+ ---
306
+
307
+ ## Examples
308
+
309
+ ### Example 1: Jest Test (GET operation)
310
+ ```typescript
311
+ // Generated from petstore API schema by openapi-skills.
312
+ import fs from 'fs-extra';
313
+ import path from 'path';
314
+ import { FetchClient } from './fetch-client';
315
+ import { PetstorePetClient } from '../client/PetstorePetClient';
316
+
317
+ const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
318
+ const apiBaseUrl = config.apis.petstore.baseUrl;
319
+
320
+ describe('PetstorePetClient', () => {
321
+ let httpClient: FetchClient;
322
+ let petstorePetClient: PetstorePetClient;
323
+
324
+ beforeEach(() => {
325
+ httpClient = new FetchClient(apiBaseUrl);
326
+ petstorePetClient = new PetstorePetClient(httpClient);
327
+ });
328
+
329
+ test('getPetById returns pet for valid id', async () => {
330
+ const result = await petstorePetClient.getPetById(1);
331
+ expect(result).toBeDefined();
332
+ expect(result.id).toBe(1);
333
+ expect(typeof result.name).toBe('string');
334
+ });
335
+
336
+ test('getPetById returns 404 for invalid id', async () => {
337
+ const result = await petstorePetClient.getPetById(999);
338
+ expect(result.error).toBeDefined();
339
+ expect(result.status).toBe(404);
340
+ });
341
+ });
342
+ ```
343
+
344
+ ### Example 2: Vitest Test (POST operation)
345
+ ```typescript
346
+ // Generated from petstore API schema by openapi-skills.
347
+ import { describe, it, expect, beforeEach } from 'vitest';
348
+ import fs from 'fs-extra';
349
+ import path from 'path';
350
+ import { FetchClient } from './fetch-client';
351
+ import { PetstorePetClient } from '../client/PetstorePetClient';
352
+
353
+ const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
354
+ const apiBaseUrl = config.apis.petstore.baseUrl;
355
+
356
+ describe('PetstorePetClient', () => {
357
+ let httpClient: FetchClient;
358
+ let petstorePetClient: PetstorePetClient;
359
+
360
+ beforeEach(() => {
361
+ httpClient = new FetchClient(apiBaseUrl);
362
+ petstorePetClient = new PetstorePetClient(httpClient);
363
+ });
364
+
365
+ it('createPet creates and returns pet', async () => {
366
+ const body = { name: 'Fluffy', status: 'available' };
367
+ const result = await petstorePetClient.createPet(body);
368
+ expect(result.id).toBeDefined();
369
+ expect(result.name).toBe('Fluffy');
370
+ });
371
+
372
+ it('createPet returns 400 for missing required name', async () => {
373
+ const body = { status: 'available' };
374
+ const result = await petstorePetClient.createPet(body);
375
+ expect(result.error).toBeDefined();
376
+ expect(result.status).toBe(400);
377
+ });
378
+ });
379
+ ```
380
+
381
+ ### Example 3: Playwright Test (API testing)
382
+ ```typescript
383
+ // Generated from petstore API schema by openapi-skills.
384
+ import { test, expect } from '@playwright/test';
385
+ import fs from 'fs-extra';
386
+ import path from 'path';
387
+ import { FetchClient } from './fetch-client';
388
+ import { PetstorePetClient } from '../client/PetstorePetClient';
389
+
390
+ const config = await fs.readJson(path.resolve('.openapi-skills/config.json'));
391
+ const apiBaseUrl = config.apis.petstore.baseUrl;
392
+
393
+ test.describe('PetstorePetClient', () => {
394
+ let httpClient: FetchClient;
395
+ let petstorePetClient: PetstorePetClient;
396
+
397
+ test.beforeEach(() => {
398
+ httpClient = new FetchClient(apiBaseUrl);
399
+ petstorePetClient = new PetstorePetClient(httpClient);
400
+ });
401
+
402
+ test('getPetById returns pet', async () => {
403
+ const result = await petstorePetClient.getPetById(1);
404
+ expect(result.id).toBe(1);
405
+ expect(result.name).toBeDefined();
406
+ });
407
+
408
+ test('getPetById returns 404 for invalid id', async () => {
409
+ const result = await petstorePetClient.getPetById(999);
410
+ expect(result.error).toBeDefined();
411
+ });
412
+ });
413
+ ```
@@ -0,0 +1,141 @@
1
+ # Shared Fetch Client Helper
2
+
3
+ Copy this file to `tests/fetch-client.ts` and instantiate `FetchClient` in your generated tests.
4
+
5
+ ```typescript
6
+ export interface HttpRequestOptionsBase {
7
+ headers?: Record<string, string>;
8
+ query?: Record<string, string | number | boolean | undefined>;
9
+ }
10
+
11
+ export interface HttpRequestWithBodyOptions extends HttpRequestOptionsBase {
12
+ body?: any;
13
+ }
14
+
15
+ export interface HttpClient {
16
+ // Methods WITHOUT body
17
+ get<T = unknown>(url: string, options?: HttpRequestOptionsBase): Promise<T>;
18
+ head<T = unknown>(url: string, options?: HttpRequestOptionsBase): Promise<T>;
19
+ options<T = unknown>(url: string, options?: HttpRequestOptionsBase): Promise<T>;
20
+ // Methods WITH body
21
+ post<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
22
+ put<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
23
+ patch<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
24
+ delete<T = unknown>(url: string, options?: HttpRequestWithBodyOptions): Promise<T>;
25
+ }
26
+
27
+ /**
28
+ * PLAYWRIGHT ADAPTATION NOTE:
29
+ *
30
+ * This FetchClient is written for Node's global `fetch()`, but it can be
31
+ * switched to Playwright's APIRequestContext by replacing the fetch call.
32
+ *
33
+ * To enable Playwright mode:
34
+ * 1. Pass a Playwright `APIRequestContext` into the constructor:
35
+ * new FetchClient(baseUrl, requestContext)
36
+ *
37
+ * 2. Replace:
38
+ * fetch(finalUrl, { method, headers, body })
39
+ * With:
40
+ * requestContext.fetch(finalUrl, {
41
+ * method,
42
+ * headers,
43
+ * data: body, // Playwright uses `data` instead of `body`
44
+ * })
45
+ *
46
+ * 3. Playwright response fields are functions:
47
+ * response.status() instead of response.status
48
+ * response.statusText() instead of response.statusText
49
+ *
50
+ * 4. The rest of the client stays the same — only the fetch call changes.
51
+ *
52
+ * This allows the same client to work in both environments:
53
+ * - Node/Vitest → uses global fetch()
54
+ * - Playwright → uses requestContext.fetch()
55
+ *
56
+ * Multipart and binary uploads should be passed through as real request bodies:
57
+ * - JSON bodies can still be plain objects
58
+ * - multipart bodies should be FormData
59
+ * - file parts should be represented as Blob/File/Buffer-backed values, not strings
60
+ * - do not force `Content-Type: application/json` when the request is multipart
61
+ */
62
+
63
+ export class FetchClient implements HttpClient {
64
+ constructor(private readonly baseUrl: string) {}
65
+
66
+ private async request<T>(
67
+ method: string,
68
+ path: string,
69
+ options: HttpRequestOptionsBase | HttpRequestWithBodyOptions = {}
70
+ ): Promise<T> {
71
+ const finalUrl = this.baseUrl.replace(/\/$/, "") + "/" + path.replace(/^\//, "");
72
+
73
+ const hasBody =
74
+ "body" in options &&
75
+ options.body !== undefined &&
76
+ method !== "GET" &&
77
+ method !== "HEAD";
78
+
79
+ const response = await fetch(finalUrl, {
80
+ method,
81
+ headers: {
82
+ ...(hasBody ? { "Content-Type": "application/json" } : {}),
83
+ ...(options.headers ?? {})
84
+ },
85
+ body: hasBody ? JSON.stringify((options as HttpRequestWithBodyOptions).body) : null
86
+ });
87
+
88
+ const text = await response.text();
89
+ let body: unknown = {};
90
+
91
+ if (text.trim().length > 0) {
92
+ try {
93
+ body = JSON.parse(text);
94
+ } catch {
95
+ body = text;
96
+ }
97
+ }
98
+
99
+ if (body !== null && typeof body === "object" && !Array.isArray(body)) {
100
+ return {
101
+ ...body,
102
+ status: response.status,
103
+ statusText: response.statusText,
104
+ } as T;
105
+ }
106
+
107
+ return {
108
+ body,
109
+ status: response.status,
110
+ statusText: response.statusText,
111
+ } as T;
112
+ }
113
+
114
+ get<T>(path: string, options?: HttpRequestOptionsBase): Promise<T> {
115
+ return this.request<T>("GET", path, options);
116
+ }
117
+
118
+ head<T>(path: string, options?: HttpRequestOptionsBase): Promise<T> {
119
+ return this.request<T>("HEAD", path, options);
120
+ }
121
+
122
+ options<T>(path: string, options?: HttpRequestOptionsBase): Promise<T> {
123
+ return this.request<T>("OPTIONS", path, options);
124
+ }
125
+
126
+ post<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
127
+ return this.request<T>("POST", path, options);
128
+ }
129
+
130
+ put<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
131
+ return this.request<T>("PUT", path, options);
132
+ }
133
+
134
+ patch<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
135
+ return this.request<T>("PATCH", path, options);
136
+ }
137
+
138
+ delete<T>(path: string, options?: HttpRequestWithBodyOptions): Promise<T> {
139
+ return this.request<T>("DELETE", path, options);
140
+ }
141
+ }