playwright-api-logger 2.0.0 → 2.1.0
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.
- package/README.md +104 -73
- package/dist/ApiLogger.d.ts +40 -31
- package/dist/ApiLogger.d.ts.map +1 -1
- package/dist/ApiLogger.js +108 -83
- package/dist/ApiLogger.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +32 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/withApiLogging.d.ts +14 -3
- package/dist/withApiLogging.d.ts.map +1 -1
- package/dist/withApiLogging.js +36 -48
- package/dist/withApiLogging.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
## How It Works
|
|
22
22
|
|
|
23
23
|
```mermaid
|
|
24
|
-
flowchart
|
|
24
|
+
flowchart LR
|
|
25
25
|
subgraph T[Playwright Test]
|
|
26
26
|
F[Fixture setup]
|
|
27
27
|
C[API Call GET/POST/PUT/DELETE]
|
|
@@ -56,10 +56,10 @@ API_LOGS=false → Logging OFF (zero overhead, default)
|
|
|
56
56
|
## Features
|
|
57
57
|
|
|
58
58
|
- **One-line integration** — just wrap `request` with `withApiLogging()`, zero changes to controllers/clients
|
|
59
|
-
- **
|
|
59
|
+
- **Structured logs** — one JSON document per test with `preconditions`, `steps`, and `teardown` sections
|
|
60
|
+
- **Step descriptions** — describe what each API call does with `.describe()`
|
|
60
61
|
- **Curl Export** — copy from log, paste into terminal or import into Postman
|
|
61
62
|
- **Env Control** — `API_LOGS=true/false` (default: `false`, zero overhead when off)
|
|
62
|
-
- **Context Tracking** — setup / test / teardown phases
|
|
63
63
|
- **Token Masking** — Authorization headers are automatically masked
|
|
64
64
|
- **Form Data** — JSON, URL-encoded, and multipart/form-data support
|
|
65
65
|
- **Error Resilient** — logging never breaks your tests
|
|
@@ -79,15 +79,38 @@ import { withApiLogging } from 'playwright-api-logger';
|
|
|
79
79
|
|
|
80
80
|
export const test = base.extend({
|
|
81
81
|
apiClient: async ({ request }, use, testInfo) => {
|
|
82
|
-
|
|
83
|
-
const apiClient = new ApiClient(
|
|
82
|
+
const loggedRequest = withApiLogging(request, testInfo);
|
|
83
|
+
const apiClient = new ApiClient(loggedRequest);
|
|
84
84
|
await use(apiClient);
|
|
85
|
+
loggedRequest.__logger.finalize(
|
|
86
|
+
testInfo.status === 'passed' ? 'PASSED' : 'FAILED'
|
|
87
|
+
);
|
|
85
88
|
},
|
|
86
89
|
});
|
|
87
90
|
```
|
|
88
91
|
|
|
89
92
|
No changes to your controllers, clients, or test files.
|
|
90
93
|
|
|
94
|
+
### With preconditions and step descriptions
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
test('GET Without token (401)', async ({ apiClient, request }) => {
|
|
98
|
+
const loggedRequest = (request as any).__logger as ApiLogger;
|
|
99
|
+
|
|
100
|
+
// Mark following calls as preconditions
|
|
101
|
+
loggedRequest.startPreconditions();
|
|
102
|
+
loggedRequest.describe('Get employee ID for test');
|
|
103
|
+
const employees = await apiClient.getEmployees({ page: 1, size: 1 });
|
|
104
|
+
const employeeId = employees.items[0].id;
|
|
105
|
+
|
|
106
|
+
// Switch to test steps
|
|
107
|
+
loggedRequest.startTest();
|
|
108
|
+
loggedRequest.describe('Get children without auth token');
|
|
109
|
+
const response = await apiClient.getChildrenWithoutAuth(employeeId);
|
|
110
|
+
expect(response.status).toBe(401);
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
91
114
|
### Enable via environment variable
|
|
92
115
|
|
|
93
116
|
```bash
|
|
@@ -100,61 +123,70 @@ API_LOGS=false
|
|
|
100
123
|
API_LOGS=true npx playwright test
|
|
101
124
|
```
|
|
102
125
|
|
|
103
|
-
### Finalize with test result (optional)
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
apiClient: async ({ request }, use, testInfo) => {
|
|
107
|
-
const loggedRequest = withApiLogging(request, testInfo);
|
|
108
|
-
const apiClient = new ApiClient(loggedRequest);
|
|
109
|
-
await use(apiClient);
|
|
110
|
-
|
|
111
|
-
// Write PASSED/FAILED to log
|
|
112
|
-
loggedRequest.__logger.finalize(
|
|
113
|
-
testInfo.status === 'passed' ? 'PASSED' : 'FAILED'
|
|
114
|
-
);
|
|
115
|
-
},
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Setup / Teardown logging
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
// In beforeAll
|
|
122
|
-
const loggedRequest = withApiLogging(request, { testName: 'auth-setup', context: 'setup' });
|
|
123
|
-
|
|
124
|
-
// In afterAll
|
|
125
|
-
const loggedRequest = withApiLogging(request, { testName: 'cleanup', context: 'teardown' });
|
|
126
|
-
```
|
|
127
|
-
|
|
128
126
|
## Log Output
|
|
129
127
|
|
|
130
|
-
|
|
128
|
+
One structured JSON document per test:
|
|
131
129
|
|
|
132
130
|
```
|
|
133
131
|
logs/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
TEARDOWN_cleanup_2026-03-16T12-00-00.log
|
|
132
|
+
get-without-token-401_2026-03-16T18-33-03.log
|
|
133
|
+
create-employee_2026-03-16T18-35-10.log
|
|
137
134
|
```
|
|
138
135
|
|
|
139
|
-
|
|
136
|
+
### Example log:
|
|
140
137
|
|
|
141
138
|
```json
|
|
142
139
|
{
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"body": { "name": "John" }
|
|
140
|
+
"test": {
|
|
141
|
+
"name": "GET Without token (401)",
|
|
142
|
+
"file": "tests/api/employees/children.spec.ts",
|
|
143
|
+
"startedAt": "2026-03-16T18:33:03.654Z",
|
|
144
|
+
"finishedAt": "2026-03-16T18:33:04.300Z",
|
|
145
|
+
"duration": 646,
|
|
146
|
+
"result": "PASSED"
|
|
151
147
|
},
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
"preconditions": [
|
|
149
|
+
{
|
|
150
|
+
"step": 1,
|
|
151
|
+
"description": "Get employee ID for test",
|
|
152
|
+
"timestamp": "2026-03-16T18:33:04.174Z",
|
|
153
|
+
"request": {
|
|
154
|
+
"method": "GET",
|
|
155
|
+
"url": "https://api.example.com/employees?page=1&size=1"
|
|
156
|
+
},
|
|
157
|
+
"response": {
|
|
158
|
+
"status": 200,
|
|
159
|
+
"body": { "items": [{ "id": "abc-123" }], "total": 27 }
|
|
160
|
+
},
|
|
161
|
+
"duration": 501,
|
|
162
|
+
"curl": "curl -X GET 'https://api.example.com/employees?page=1&size=1' -H 'Accept: application/json'"
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"steps": [
|
|
166
|
+
{
|
|
167
|
+
"step": 1,
|
|
168
|
+
"description": "Get children without auth token",
|
|
169
|
+
"timestamp": "2026-03-16T18:33:04.242Z",
|
|
170
|
+
"request": {
|
|
171
|
+
"method": "GET",
|
|
172
|
+
"url": "https://api.example.com/employees/abc-123/children"
|
|
173
|
+
},
|
|
174
|
+
"response": {
|
|
175
|
+
"status": 401,
|
|
176
|
+
"body": { "detail": "Not authenticated" }
|
|
177
|
+
},
|
|
178
|
+
"duration": 67,
|
|
179
|
+
"curl": "curl -X GET 'https://api.example.com/employees/abc-123/children'"
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"teardown": [],
|
|
183
|
+
"summary": {
|
|
184
|
+
"totalRequests": 2,
|
|
185
|
+
"preconditions": 1,
|
|
186
|
+
"testSteps": 1,
|
|
187
|
+
"teardown": 0,
|
|
188
|
+
"totalDuration": 568
|
|
189
|
+
}
|
|
158
190
|
}
|
|
159
191
|
```
|
|
160
192
|
|
|
@@ -165,28 +197,22 @@ Each log entry is a JSON object:
|
|
|
165
197
|
Main integration point. Wraps `APIRequestContext` with a Proxy that logs all HTTP calls.
|
|
166
198
|
|
|
167
199
|
```typescript
|
|
168
|
-
// With TestInfo (recommended)
|
|
169
200
|
const loggedRequest = withApiLogging(request, testInfo);
|
|
170
|
-
|
|
171
|
-
// With options
|
|
172
|
-
const loggedRequest = withApiLogging(request, {
|
|
173
|
-
testName: 'my-test',
|
|
174
|
-
context: 'setup',
|
|
175
|
-
logDirectory: 'custom-logs/',
|
|
176
|
-
maskAuthTokens: true,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Access logger for finalization
|
|
180
|
-
loggedRequest.__logger.finalize('PASSED');
|
|
201
|
+
loggedRequest.__logger // access the ApiLogger instance
|
|
181
202
|
```
|
|
182
203
|
|
|
183
|
-
###
|
|
204
|
+
### `ApiLogger` — context & description
|
|
184
205
|
|
|
185
|
-
|
|
|
186
|
-
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
206
|
+
| Method | Description |
|
|
207
|
+
|--------|-------------|
|
|
208
|
+
| `describe(text)` | Set description for the **next** API call |
|
|
209
|
+
| `startPreconditions()` | Following calls → `preconditions` section |
|
|
210
|
+
| `startTest()` | Following calls → `steps` section |
|
|
211
|
+
| `startTeardown()` | Following calls → `teardown` section |
|
|
212
|
+
| `setContext(ctx)` | Set context directly (`'preconditions'` / `'test'` / `'teardown'`) |
|
|
213
|
+
| `finalize(result, info?)` | Write structured JSON document to file |
|
|
214
|
+
| `isEnabled()` | Check if logging is active |
|
|
215
|
+
| `getLogFilePath()` | Get current log file path |
|
|
190
216
|
|
|
191
217
|
### `CurlGenerator`
|
|
192
218
|
|
|
@@ -205,19 +231,24 @@ loggedRequest.__logger.finalize('PASSED');
|
|
|
205
231
|
```typescript
|
|
206
232
|
{
|
|
207
233
|
testName?: string; // Test name (default: 'unknown-test')
|
|
208
|
-
|
|
234
|
+
testFile?: string; // Test file path
|
|
235
|
+
context?: LogContext; // 'preconditions' | 'test' | 'teardown'
|
|
209
236
|
logDirectory?: string; // Custom log dir (default: 'logs/')
|
|
210
237
|
maskAuthTokens?: boolean; // Mask auth headers (default: true)
|
|
238
|
+
logger?: ApiLogger; // Share logger across phases
|
|
211
239
|
}
|
|
212
240
|
```
|
|
213
241
|
|
|
214
|
-
## Migration from v1
|
|
215
|
-
|
|
216
|
-
v1 required changes to controllers, clients, and fixtures. v2 needs only **one line**:
|
|
242
|
+
## Migration from v1 → v2
|
|
217
243
|
|
|
218
244
|
```diff
|
|
219
|
-
-
|
|
220
|
-
|
|
245
|
+
- // v1: manual logger setup in controllers and clients
|
|
246
|
+
- const logger = createApiLogger(testInfo.title);
|
|
247
|
+
- apiClient.setApiLogger(logger);
|
|
248
|
+
|
|
249
|
+
+ // v2: one line, structured logs with sections
|
|
250
|
+
+ const loggedRequest = withApiLogging(request, testInfo);
|
|
251
|
+
+ const apiClient = new ApiClient(loggedRequest);
|
|
221
252
|
```
|
|
222
253
|
|
|
223
254
|
## License
|
package/dist/ApiLogger.d.ts
CHANGED
|
@@ -1,74 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* API Logger for capturing and logging HTTP requests/responses
|
|
3
|
+
* Produces a single structured test document with preconditions, steps, and teardown sections.
|
|
4
|
+
*
|
|
3
5
|
* Features:
|
|
4
|
-
* -
|
|
6
|
+
* - Structured test document (one JSON per test)
|
|
7
|
+
* - Preconditions / Steps / Teardown sections
|
|
8
|
+
* - Step descriptions and numbering
|
|
5
9
|
* - Curl command generation for manual testing
|
|
6
10
|
* - Environment-based enable/disable via API_LOGS
|
|
7
|
-
* -
|
|
8
|
-
* - Test context tracking (setup/test/teardown)
|
|
11
|
+
* - Test context tracking and switching
|
|
9
12
|
*/
|
|
10
13
|
import { LogContext, LoggerConfig } from './types';
|
|
11
14
|
export declare class ApiLogger {
|
|
12
15
|
private enabled;
|
|
13
16
|
private testName;
|
|
14
|
-
private
|
|
17
|
+
private testFile?;
|
|
18
|
+
private currentContext;
|
|
15
19
|
private logDirectory;
|
|
16
20
|
private logFilePath;
|
|
17
21
|
private maskAuthTokens;
|
|
18
|
-
private
|
|
19
|
-
private
|
|
22
|
+
private startedAt;
|
|
23
|
+
private preconditions;
|
|
24
|
+
private steps;
|
|
25
|
+
private teardownSteps;
|
|
26
|
+
private nextDescription?;
|
|
20
27
|
constructor(config?: LoggerConfig);
|
|
21
|
-
/**
|
|
22
|
-
* Get default log directory path
|
|
23
|
-
*/
|
|
24
28
|
private getDefaultLogDirectory;
|
|
25
|
-
/**
|
|
26
|
-
* Initialize log file and directory
|
|
27
|
-
*/
|
|
28
29
|
private initializeLogFile;
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* Set description for the next API call
|
|
32
|
+
* @example
|
|
33
|
+
* logger.describe('Get employee ID for testing');
|
|
34
|
+
* await apiClient.getEmployees(); // this call gets the description
|
|
31
35
|
*/
|
|
32
|
-
|
|
36
|
+
describe(description: string): void;
|
|
33
37
|
/**
|
|
34
|
-
*
|
|
38
|
+
* Switch current context (preconditions → test → teardown)
|
|
39
|
+
* All subsequent API calls will be logged to the new context section
|
|
35
40
|
*/
|
|
36
|
-
|
|
41
|
+
setContext(context: LogContext): void;
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
43
|
+
* Shortcut: switch to preconditions context
|
|
39
44
|
*/
|
|
40
|
-
|
|
45
|
+
startPreconditions(): void;
|
|
41
46
|
/**
|
|
42
|
-
*
|
|
47
|
+
* Shortcut: switch to test steps context
|
|
43
48
|
*/
|
|
44
|
-
|
|
49
|
+
startTest(): void;
|
|
45
50
|
/**
|
|
46
|
-
*
|
|
51
|
+
* Shortcut: switch to teardown context
|
|
47
52
|
*/
|
|
48
|
-
|
|
53
|
+
startTeardown(): void;
|
|
49
54
|
/**
|
|
50
|
-
*
|
|
55
|
+
* Log an API call with request and response
|
|
51
56
|
*/
|
|
52
|
-
|
|
57
|
+
logApiCall(method: string, url: string, requestHeaders: Record<string, string | string[]> | undefined, requestBody: any, status: number, responseHeaders: Record<string, string> | undefined, responseBody: any, duration: number): void;
|
|
53
58
|
/**
|
|
54
|
-
* Get the
|
|
59
|
+
* Get the target array for current context
|
|
55
60
|
*/
|
|
56
|
-
|
|
61
|
+
private getTargetArray;
|
|
57
62
|
/**
|
|
58
|
-
*
|
|
63
|
+
* Finalize and write the structured test document to file
|
|
59
64
|
*/
|
|
60
|
-
|
|
65
|
+
finalize(result: 'PASSED' | 'FAILED' | 'SKIPPED', additionalInfo?: Record<string, any>): void;
|
|
61
66
|
/**
|
|
62
|
-
*
|
|
67
|
+
* Write structured document to file
|
|
63
68
|
*/
|
|
64
|
-
|
|
69
|
+
private writeDocument;
|
|
70
|
+
private generateTimestamp;
|
|
71
|
+
private sanitizeTestName;
|
|
72
|
+
getLogFilePath(): string | null;
|
|
73
|
+
isEnabled(): boolean;
|
|
65
74
|
}
|
|
66
75
|
/**
|
|
67
76
|
* Factory function for creating test-context loggers
|
|
68
77
|
*/
|
|
69
78
|
export declare function createApiLogger(testName: string, context?: LogContext): ApiLogger;
|
|
70
79
|
/**
|
|
71
|
-
* Factory for setup context
|
|
80
|
+
* Factory for setup/preconditions context
|
|
72
81
|
*/
|
|
73
82
|
export declare function createSetupLogger(testName: string): ApiLogger;
|
|
74
83
|
/**
|
package/dist/ApiLogger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiLogger.d.ts","sourceRoot":"","sources":["../src/ApiLogger.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ApiLogger.d.ts","sourceRoot":"","sources":["../src/ApiLogger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EACL,UAAU,EACV,YAAY,EAKb,MAAM,SAAS,CAAC;AAEjB,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,SAAS,CAAS;IAG1B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,aAAa,CAAsB;IAG3C,OAAO,CAAC,eAAe,CAAC,CAAS;gBAErB,MAAM,GAAE,YAAiB;IAerC,OAAO,CAAC,sBAAsB;IAI9B,OAAO,CAAC,iBAAiB;IAgBzB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAInC;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAIrC;;OAEG;IACH,kBAAkB,IAAI,IAAI;IAI1B;;OAEG;IACH,SAAS,IAAI,IAAI;IAIjB;;OAEG;IACH,aAAa,IAAI,IAAI;IAIrB;;OAEG;IACH,UAAU,CACR,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,SAAS,EAC7D,WAAW,EAAE,GAAG,EAChB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EACnD,YAAY,EAAE,GAAG,EACjB,QAAQ,EAAE,MAAM,GACf,IAAI;IAuDP;;OAEG;IACH,OAAO,CAAC,cAAc;IAYtB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAyC7F;;OAEG;IACH,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,gBAAgB;IASxB,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B,SAAS,IAAI,OAAO;CAGrB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,UAAmB,GAAG,SAAS,CAEzF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAE7D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAEhE"}
|
package/dist/ApiLogger.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* API Logger for capturing and logging HTTP requests/responses
|
|
4
|
+
* Produces a single structured test document with preconditions, steps, and teardown sections.
|
|
5
|
+
*
|
|
4
6
|
* Features:
|
|
5
|
-
* -
|
|
7
|
+
* - Structured test document (one JSON per test)
|
|
8
|
+
* - Preconditions / Steps / Teardown sections
|
|
9
|
+
* - Step descriptions and numbering
|
|
6
10
|
* - Curl command generation for manual testing
|
|
7
11
|
* - Environment-based enable/disable via API_LOGS
|
|
8
|
-
* -
|
|
9
|
-
* - Test context tracking (setup/test/teardown)
|
|
12
|
+
* - Test context tracking and switching
|
|
10
13
|
*/
|
|
11
14
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -22,27 +25,24 @@ const CurlGenerator_1 = require("./CurlGenerator");
|
|
|
22
25
|
class ApiLogger {
|
|
23
26
|
constructor(config = {}) {
|
|
24
27
|
this.logFilePath = null;
|
|
25
|
-
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
+
// Structured storage
|
|
29
|
+
this.preconditions = [];
|
|
30
|
+
this.steps = [];
|
|
31
|
+
this.teardownSteps = [];
|
|
28
32
|
this.enabled = process.env.API_LOGS === 'true';
|
|
29
33
|
this.testName = config.testName || 'unknown-test';
|
|
30
|
-
this.
|
|
34
|
+
this.testFile = config.testFile;
|
|
35
|
+
this.currentContext = config.context || 'test';
|
|
31
36
|
this.logDirectory = config.logDirectory || this.getDefaultLogDirectory();
|
|
32
37
|
this.maskAuthTokens = config.maskAuthTokens ?? true;
|
|
38
|
+
this.startedAt = new Date().toISOString();
|
|
33
39
|
if (this.enabled) {
|
|
34
40
|
this.initializeLogFile();
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Get default log directory path
|
|
39
|
-
*/
|
|
40
43
|
getDefaultLogDirectory() {
|
|
41
44
|
return path_1.default.join(process.cwd(), 'logs');
|
|
42
45
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Initialize log file and directory
|
|
45
|
-
*/
|
|
46
46
|
initializeLogFile() {
|
|
47
47
|
try {
|
|
48
48
|
if (!fs_1.default.existsSync(this.logDirectory)) {
|
|
@@ -50,17 +50,48 @@ class ApiLogger {
|
|
|
50
50
|
}
|
|
51
51
|
const timestamp = this.generateTimestamp();
|
|
52
52
|
const sanitizedTestName = this.sanitizeTestName(this.testName);
|
|
53
|
-
const filename = `${
|
|
53
|
+
const filename = `${sanitizedTestName}_${timestamp}.log`;
|
|
54
54
|
this.logFilePath = path_1.default.join(this.logDirectory, filename);
|
|
55
|
-
if (!fs_1.default.existsSync(this.logFilePath)) {
|
|
56
|
-
fs_1.default.writeFileSync(this.logFilePath, '');
|
|
57
|
-
}
|
|
58
55
|
}
|
|
59
56
|
catch (error) {
|
|
60
57
|
console.warn('[ApiLogger] Failed to initialize log file:', error);
|
|
61
58
|
this.logFilePath = null;
|
|
62
59
|
}
|
|
63
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Set description for the next API call
|
|
63
|
+
* @example
|
|
64
|
+
* logger.describe('Get employee ID for testing');
|
|
65
|
+
* await apiClient.getEmployees(); // this call gets the description
|
|
66
|
+
*/
|
|
67
|
+
describe(description) {
|
|
68
|
+
this.nextDescription = description;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Switch current context (preconditions → test → teardown)
|
|
72
|
+
* All subsequent API calls will be logged to the new context section
|
|
73
|
+
*/
|
|
74
|
+
setContext(context) {
|
|
75
|
+
this.currentContext = context;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Shortcut: switch to preconditions context
|
|
79
|
+
*/
|
|
80
|
+
startPreconditions() {
|
|
81
|
+
this.currentContext = 'preconditions';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Shortcut: switch to test steps context
|
|
85
|
+
*/
|
|
86
|
+
startTest() {
|
|
87
|
+
this.currentContext = 'test';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Shortcut: switch to teardown context
|
|
91
|
+
*/
|
|
92
|
+
startTeardown() {
|
|
93
|
+
this.currentContext = 'teardown';
|
|
94
|
+
}
|
|
64
95
|
/**
|
|
65
96
|
* Log an API call with request and response
|
|
66
97
|
*/
|
|
@@ -69,7 +100,6 @@ class ApiLogger {
|
|
|
69
100
|
return;
|
|
70
101
|
}
|
|
71
102
|
try {
|
|
72
|
-
// Extract content type from headers
|
|
73
103
|
let contentType;
|
|
74
104
|
if (requestHeaders) {
|
|
75
105
|
for (const [key, value] of Object.entries(requestHeaders)) {
|
|
@@ -91,75 +121,101 @@ class ApiLogger {
|
|
|
91
121
|
headers: responseHeaders,
|
|
92
122
|
body: responseBody,
|
|
93
123
|
};
|
|
94
|
-
// Generate curl command
|
|
95
124
|
const curl = CurlGenerator_1.CurlGenerator.generate(requestData, this.maskAuthTokens);
|
|
96
|
-
//
|
|
97
|
-
const
|
|
125
|
+
// Get target array and step number
|
|
126
|
+
const targetArray = this.getTargetArray();
|
|
127
|
+
const stepNumber = targetArray.length + 1;
|
|
128
|
+
const stepEntry = {
|
|
129
|
+
step: stepNumber,
|
|
130
|
+
description: this.nextDescription,
|
|
98
131
|
timestamp: new Date().toISOString(),
|
|
99
|
-
testName: this.testName,
|
|
100
|
-
context: this.context,
|
|
101
132
|
request: requestData,
|
|
102
133
|
response: responseData,
|
|
103
134
|
duration,
|
|
104
135
|
curl,
|
|
105
136
|
};
|
|
106
|
-
|
|
137
|
+
// Clear description after use
|
|
138
|
+
this.nextDescription = undefined;
|
|
139
|
+
targetArray.push(stepEntry);
|
|
107
140
|
}
|
|
108
141
|
catch (error) {
|
|
109
142
|
console.warn('[ApiLogger] Error logging API call:', error);
|
|
110
143
|
}
|
|
111
144
|
}
|
|
112
145
|
/**
|
|
113
|
-
*
|
|
146
|
+
* Get the target array for current context
|
|
114
147
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
getTargetArray() {
|
|
149
|
+
switch (this.currentContext) {
|
|
150
|
+
case 'preconditions':
|
|
151
|
+
return this.preconditions;
|
|
152
|
+
case 'teardown':
|
|
153
|
+
return this.teardownSteps;
|
|
154
|
+
case 'test':
|
|
155
|
+
default:
|
|
156
|
+
return this.steps;
|
|
118
157
|
}
|
|
119
|
-
this.currentRequest = { method, url, headers, body };
|
|
120
|
-
this.requestStartTime = Date.now();
|
|
121
158
|
}
|
|
122
159
|
/**
|
|
123
|
-
*
|
|
160
|
+
* Finalize and write the structured test document to file
|
|
124
161
|
*/
|
|
125
|
-
|
|
126
|
-
if (!this.enabled
|
|
162
|
+
finalize(result, additionalInfo) {
|
|
163
|
+
if (!this.enabled) {
|
|
127
164
|
return;
|
|
128
165
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
166
|
+
try {
|
|
167
|
+
const finishedAt = new Date().toISOString();
|
|
168
|
+
const startTime = new Date(this.startedAt).getTime();
|
|
169
|
+
const endTime = new Date(finishedAt).getTime();
|
|
170
|
+
// Calculate total API duration
|
|
171
|
+
const allSteps = [...this.preconditions, ...this.steps, ...this.teardownSteps];
|
|
172
|
+
const totalApiDuration = allSteps.reduce((sum, s) => sum + s.duration, 0);
|
|
173
|
+
const document = {
|
|
174
|
+
test: {
|
|
175
|
+
name: this.testName,
|
|
176
|
+
file: this.testFile || additionalInfo?.testFile,
|
|
177
|
+
startedAt: this.startedAt,
|
|
178
|
+
finishedAt,
|
|
179
|
+
duration: endTime - startTime,
|
|
180
|
+
result,
|
|
181
|
+
},
|
|
182
|
+
preconditions: this.preconditions,
|
|
183
|
+
steps: this.steps,
|
|
184
|
+
teardown: this.teardownSteps,
|
|
185
|
+
summary: {
|
|
186
|
+
totalRequests: allSteps.length,
|
|
187
|
+
preconditions: this.preconditions.length,
|
|
188
|
+
testSteps: this.steps.length,
|
|
189
|
+
teardown: this.teardownSteps.length,
|
|
190
|
+
totalDuration: totalApiDuration,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
this.writeDocument(document);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.warn('[ApiLogger] Error finalizing log:', error);
|
|
197
|
+
}
|
|
133
198
|
}
|
|
134
199
|
/**
|
|
135
|
-
* Write
|
|
200
|
+
* Write structured document to file
|
|
136
201
|
*/
|
|
137
|
-
|
|
202
|
+
writeDocument(document) {
|
|
138
203
|
try {
|
|
139
204
|
if (!this.logFilePath) {
|
|
140
205
|
return;
|
|
141
206
|
}
|
|
142
|
-
|
|
143
|
-
fs_1.default.appendFileSync(this.logFilePath, logLine + '\n\n');
|
|
207
|
+
fs_1.default.writeFileSync(this.logFilePath, JSON.stringify(document, null, 2) + '\n');
|
|
144
208
|
}
|
|
145
209
|
catch (error) {
|
|
146
210
|
console.warn('[ApiLogger] Failed to write log file:', error);
|
|
147
211
|
}
|
|
148
212
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Generate ISO timestamp for filenames
|
|
151
|
-
*/
|
|
152
213
|
generateTimestamp() {
|
|
153
|
-
|
|
154
|
-
return now
|
|
214
|
+
return new Date()
|
|
155
215
|
.toISOString()
|
|
156
216
|
.replace(/[:.]/g, '-')
|
|
157
|
-
.replace('T', 'T')
|
|
158
217
|
.split('.')[0];
|
|
159
218
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Sanitize test name for use in filename
|
|
162
|
-
*/
|
|
163
219
|
sanitizeTestName(name) {
|
|
164
220
|
return name
|
|
165
221
|
.toLowerCase()
|
|
@@ -168,43 +224,12 @@ class ApiLogger {
|
|
|
168
224
|
.replace(/^-|-$/g, '')
|
|
169
225
|
.substring(0, 80);
|
|
170
226
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Get the current log file path
|
|
173
|
-
*/
|
|
174
227
|
getLogFilePath() {
|
|
175
228
|
return this.logFilePath;
|
|
176
229
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Check if logger is enabled
|
|
179
|
-
*/
|
|
180
230
|
isEnabled() {
|
|
181
231
|
return this.enabled;
|
|
182
232
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Finalize logging for test completion
|
|
185
|
-
*/
|
|
186
|
-
finalize(result, additionalInfo) {
|
|
187
|
-
if (!this.enabled) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
const finalizationEntry = {
|
|
192
|
-
timestamp: new Date().toISOString(),
|
|
193
|
-
step: 'TEST_FINALIZATION',
|
|
194
|
-
testName: this.testName,
|
|
195
|
-
context: this.context,
|
|
196
|
-
result,
|
|
197
|
-
logFilePath: this.logFilePath,
|
|
198
|
-
additionalInfo,
|
|
199
|
-
};
|
|
200
|
-
if (this.logFilePath) {
|
|
201
|
-
fs_1.default.appendFileSync(this.logFilePath, JSON.stringify(finalizationEntry, null, 2) + '\n\n');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
console.warn('[ApiLogger] Error finalizing log:', error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
233
|
}
|
|
209
234
|
exports.ApiLogger = ApiLogger;
|
|
210
235
|
/**
|
|
@@ -214,10 +239,10 @@ function createApiLogger(testName, context = 'test') {
|
|
|
214
239
|
return new ApiLogger({ testName, context });
|
|
215
240
|
}
|
|
216
241
|
/**
|
|
217
|
-
* Factory for setup context
|
|
242
|
+
* Factory for setup/preconditions context
|
|
218
243
|
*/
|
|
219
244
|
function createSetupLogger(testName) {
|
|
220
|
-
return new ApiLogger({ testName, context: '
|
|
245
|
+
return new ApiLogger({ testName, context: 'preconditions' });
|
|
221
246
|
}
|
|
222
247
|
/**
|
|
223
248
|
* Factory for teardown context
|
package/dist/ApiLogger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiLogger.js","sourceRoot":"","sources":["../src/ApiLogger.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"ApiLogger.js","sourceRoot":"","sources":["../src/ApiLogger.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { ApiLogger, createApiLogger, createSetupLogger, createTeardownLogger } f
|
|
|
2
2
|
export { CurlGenerator } from './CurlGenerator';
|
|
3
3
|
export { withApiLogging } from './withApiLogging';
|
|
4
4
|
export type { ApiLoggingOptions } from './withApiLogging';
|
|
5
|
-
export type { LoggerConfig, RequestLogData, ResponseLogData, LogEntry, LogContext } from './types';
|
|
5
|
+
export type { LoggerConfig, RequestLogData, ResponseLogData, StepLogEntry, TestLogDocument, LogEntry, LogContext, } from './types';
|
|
6
6
|
export type { RequestData } from './CurlGenerator';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAClG,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAClG,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EACV,YAAY,EACZ,cAAc,EACd,eAAe,EACf,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export type LogContext = '
|
|
1
|
+
export type LogContext = 'preconditions' | 'test' | 'teardown';
|
|
2
2
|
export interface LoggerConfig {
|
|
3
3
|
testName?: string;
|
|
4
|
+
testFile?: string;
|
|
4
5
|
context?: LogContext;
|
|
5
6
|
logDirectory?: string;
|
|
6
7
|
maskAuthTokens?: boolean;
|
|
@@ -17,6 +18,36 @@ export interface ResponseLogData {
|
|
|
17
18
|
headers?: Record<string, string>;
|
|
18
19
|
body?: any;
|
|
19
20
|
}
|
|
21
|
+
export interface StepLogEntry {
|
|
22
|
+
step: number;
|
|
23
|
+
description?: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
request: RequestLogData;
|
|
26
|
+
response: ResponseLogData;
|
|
27
|
+
duration: number;
|
|
28
|
+
curl: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TestLogDocument {
|
|
31
|
+
test: {
|
|
32
|
+
name: string;
|
|
33
|
+
file?: string;
|
|
34
|
+
startedAt: string;
|
|
35
|
+
finishedAt?: string;
|
|
36
|
+
duration?: number;
|
|
37
|
+
result?: 'PASSED' | 'FAILED' | 'SKIPPED';
|
|
38
|
+
};
|
|
39
|
+
preconditions: StepLogEntry[];
|
|
40
|
+
steps: StepLogEntry[];
|
|
41
|
+
teardown: StepLogEntry[];
|
|
42
|
+
summary: {
|
|
43
|
+
totalRequests: number;
|
|
44
|
+
preconditions: number;
|
|
45
|
+
testSteps: number;
|
|
46
|
+
teardown: number;
|
|
47
|
+
totalDuration: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** @deprecated Use StepLogEntry instead */
|
|
20
51
|
export interface LogEntry {
|
|
21
52
|
timestamp: string;
|
|
22
53
|
testName: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;KAC1C,CAAC;IACF,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,EAAE;QACP,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED,2CAA2C;AAC3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;IACpB,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd"}
|
package/dist/withApiLogging.d.ts
CHANGED
|
@@ -8,12 +8,16 @@ import { LogContext } from './types';
|
|
|
8
8
|
export interface ApiLoggingOptions {
|
|
9
9
|
/** Test name for log filename (auto-detected from testInfo if provided) */
|
|
10
10
|
testName?: string;
|
|
11
|
-
/**
|
|
11
|
+
/** Test file path (auto-detected from testInfo if provided) */
|
|
12
|
+
testFile?: string;
|
|
13
|
+
/** Log context: 'preconditions' | 'test' | 'teardown' (default: 'test') */
|
|
12
14
|
context?: LogContext;
|
|
13
15
|
/** Custom log directory (default: 'logs/') */
|
|
14
16
|
logDirectory?: string;
|
|
15
17
|
/** Mask Authorization headers (default: true) */
|
|
16
18
|
maskAuthTokens?: boolean;
|
|
19
|
+
/** Existing logger to share across setup/test/teardown phases */
|
|
20
|
+
logger?: ApiLogger;
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Wrap Playwright's APIRequestContext with automatic logging.
|
|
@@ -29,8 +33,15 @@ export interface ApiLoggingOptions {
|
|
|
29
33
|
* const apiClient = new ApiClient(loggedRequest);
|
|
30
34
|
*
|
|
31
35
|
* @example
|
|
32
|
-
* // With
|
|
33
|
-
* const loggedRequest = withApiLogging(request,
|
|
36
|
+
* // With preconditions and test steps:
|
|
37
|
+
* const loggedRequest = withApiLogging(request, testInfo);
|
|
38
|
+
* loggedRequest.__logger.startPreconditions();
|
|
39
|
+
* loggedRequest.__logger.describe('Get employee for test');
|
|
40
|
+
* await apiClient.getEmployees();
|
|
41
|
+
* loggedRequest.__logger.startTest();
|
|
42
|
+
* loggedRequest.__logger.describe('Try to access without token');
|
|
43
|
+
* await apiClient.getWithoutAuth();
|
|
44
|
+
* loggedRequest.__logger.finalize('PASSED');
|
|
34
45
|
*/
|
|
35
46
|
export declare function withApiLogging(request: APIRequestContext, testInfoOrOptions?: TestInfo | ApiLoggingOptions): APIRequestContext & {
|
|
36
47
|
__logger: ApiLogger;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withApiLogging.d.ts","sourceRoot":"","sources":["../src/withApiLogging.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAe,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAgB,MAAM,SAAS,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,
|
|
1
|
+
{"version":3,"file":"withApiLogging.d.ts","sourceRoot":"","sources":["../src/withApiLogging.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAe,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAgB,MAAM,SAAS,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,iBAAiB,EAC1B,iBAAiB,CAAC,EAAE,QAAQ,GAAG,iBAAiB,GAC/C,iBAAiB,GAAG;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAmG7C"}
|
package/dist/withApiLogging.js
CHANGED
|
@@ -20,30 +20,48 @@ const ApiLogger_1 = require("./ApiLogger");
|
|
|
20
20
|
* const apiClient = new ApiClient(loggedRequest);
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
23
|
-
* // With
|
|
24
|
-
* const loggedRequest = withApiLogging(request,
|
|
23
|
+
* // With preconditions and test steps:
|
|
24
|
+
* const loggedRequest = withApiLogging(request, testInfo);
|
|
25
|
+
* loggedRequest.__logger.startPreconditions();
|
|
26
|
+
* loggedRequest.__logger.describe('Get employee for test');
|
|
27
|
+
* await apiClient.getEmployees();
|
|
28
|
+
* loggedRequest.__logger.startTest();
|
|
29
|
+
* loggedRequest.__logger.describe('Try to access without token');
|
|
30
|
+
* await apiClient.getWithoutAuth();
|
|
31
|
+
* loggedRequest.__logger.finalize('PASSED');
|
|
25
32
|
*/
|
|
26
33
|
function withApiLogging(request, testInfoOrOptions) {
|
|
27
|
-
// Resolve options
|
|
28
34
|
let options;
|
|
29
35
|
if (testInfoOrOptions && 'title' in testInfoOrOptions) {
|
|
30
|
-
|
|
36
|
+
const testInfo = testInfoOrOptions;
|
|
31
37
|
options = {
|
|
32
|
-
testName:
|
|
38
|
+
testName: testInfo.title,
|
|
39
|
+
testFile: testInfo.file,
|
|
33
40
|
context: 'test',
|
|
34
41
|
};
|
|
35
42
|
}
|
|
36
43
|
else {
|
|
37
44
|
options = testInfoOrOptions || {};
|
|
38
45
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// Use shared logger or create new
|
|
47
|
+
let logger;
|
|
48
|
+
if (options.logger) {
|
|
49
|
+
logger = options.logger;
|
|
50
|
+
if (options.context) {
|
|
51
|
+
logger.setContext(options.context);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const config = {
|
|
56
|
+
testName: options.testName,
|
|
57
|
+
testFile: options.testFile,
|
|
58
|
+
context: options.context || 'test',
|
|
59
|
+
logDirectory: options.logDirectory,
|
|
60
|
+
maskAuthTokens: options.maskAuthTokens,
|
|
61
|
+
};
|
|
62
|
+
logger = new ApiLogger_1.ApiLogger(config);
|
|
63
|
+
}
|
|
64
|
+
// If logging is disabled, return original request with logger ref
|
|
47
65
|
if (!logger.isEnabled()) {
|
|
48
66
|
return Object.assign(request, { __logger: logger });
|
|
49
67
|
}
|
|
@@ -51,22 +69,18 @@ function withApiLogging(request, testInfoOrOptions) {
|
|
|
51
69
|
const proxy = new Proxy(request, {
|
|
52
70
|
get(target, prop, receiver) {
|
|
53
71
|
const propName = typeof prop === 'string' ? prop : '';
|
|
54
|
-
// Expose logger reference
|
|
55
72
|
if (propName === '__logger') {
|
|
56
73
|
return logger;
|
|
57
74
|
}
|
|
58
75
|
if (HTTP_METHODS.includes(propName)) {
|
|
59
|
-
return async (url,
|
|
60
|
-
const method = propName === 'fetch' ? (
|
|
76
|
+
return async (url, reqOptions) => {
|
|
77
|
+
const method = propName === 'fetch' ? (reqOptions?.method || 'GET') : propName;
|
|
61
78
|
const startTime = Date.now();
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const requestBody = extractBody(options);
|
|
65
|
-
const contentType = extractContentType(options, requestHeaders);
|
|
79
|
+
const requestHeaders = reqOptions?.headers;
|
|
80
|
+
const requestBody = extractBody(reqOptions);
|
|
66
81
|
try {
|
|
67
|
-
const response = await target[propName](url,
|
|
82
|
+
const response = await target[propName](url, reqOptions);
|
|
68
83
|
const duration = Date.now() - startTime;
|
|
69
|
-
// Parse response body safely
|
|
70
84
|
const responseBody = await safeParseResponseBody(response);
|
|
71
85
|
const responseHeaders = extractResponseHeaders(response);
|
|
72
86
|
logger.logApiCall(method.toUpperCase(), response.url(), requestHeaders, requestBody, response.status(), responseHeaders, responseBody, duration);
|
|
@@ -84,9 +98,6 @@ function withApiLogging(request, testInfoOrOptions) {
|
|
|
84
98
|
});
|
|
85
99
|
return Object.assign(proxy, { __logger: logger });
|
|
86
100
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Extract body from Playwright request options
|
|
89
|
-
*/
|
|
90
101
|
function extractBody(options) {
|
|
91
102
|
if (!options)
|
|
92
103
|
return undefined;
|
|
@@ -98,26 +109,6 @@ function extractBody(options) {
|
|
|
98
109
|
return options.multipart;
|
|
99
110
|
return undefined;
|
|
100
111
|
}
|
|
101
|
-
/**
|
|
102
|
-
* Detect content type from options
|
|
103
|
-
*/
|
|
104
|
-
function extractContentType(options, headers) {
|
|
105
|
-
if (options?.form)
|
|
106
|
-
return 'application/x-www-form-urlencoded';
|
|
107
|
-
if (options?.multipart)
|
|
108
|
-
return 'multipart/form-data';
|
|
109
|
-
if (headers) {
|
|
110
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
111
|
-
if (key.toLowerCase() === 'content-type') {
|
|
112
|
-
return Array.isArray(value) ? value[0] : String(value);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Safely parse response body (try JSON, fallback to text)
|
|
120
|
-
*/
|
|
121
112
|
async function safeParseResponseBody(response) {
|
|
122
113
|
try {
|
|
123
114
|
return await response.json();
|
|
@@ -131,9 +122,6 @@ async function safeParseResponseBody(response) {
|
|
|
131
122
|
}
|
|
132
123
|
}
|
|
133
124
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Extract headers from APIResponse
|
|
136
|
-
*/
|
|
137
125
|
function extractResponseHeaders(response) {
|
|
138
126
|
try {
|
|
139
127
|
return response.headers();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withApiLogging.js","sourceRoot":"","sources":["../src/withApiLogging.ts"],"names":[],"mappings":";AAAA;;;GAGG;;
|
|
1
|
+
{"version":3,"file":"withApiLogging.js","sourceRoot":"","sources":["../src/withApiLogging.ts"],"names":[],"mappings":"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"}
|