@alternative-path/testlens-playwright-reporter 0.4.7 → 0.4.9
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 +83 -0
- package/index.d.ts +7 -0
- package/index.js +199 -37
- package/index.ts +1750 -1587
- package/package.json +75 -74
package/README.md
CHANGED
|
@@ -126,6 +126,88 @@ The reporter automatically reads these environment variables (no config changes
|
|
|
126
126
|
- **API Key**: `TESTLENS_API_KEY` (also checks: `testlens_api_key`, `TESTLENS_KEY`, `testlensApiKey`, `PLAYWRIGHT_API_KEY`, `PW_API_KEY`)
|
|
127
127
|
- **Build Name**: `testlensBuildName` (also checks: `TESTLENS_BUILD_NAME`, `BUILDNAME`, `BUILD_NAME`)
|
|
128
128
|
- **Build Tag**: `testlensBuildTag` (also checks: `TESTLENS_BUILD_TAG`, `BUILDTAG`, `BUILD_TAG`)
|
|
129
|
+
- **Execution ID** (one run per pipeline): see [One test run per pipeline execution](#one-test-run-per-pipeline-execution) below.
|
|
130
|
+
|
|
131
|
+
### One test run per pipeline execution
|
|
132
|
+
|
|
133
|
+
If your pipeline runs `npx playwright test` in **multiple steps**, you get one TestLens run per step. To group all steps into **one** run, set **`TESTLENS_EXECUTION_ID`** to your pipeline’s run identifier (e.g. build number or run ID) in every step. Use one of the three methods below. For per-CI examples (Bitbucket, GitHub, GitLab, Azure DevOps, Jenkins), see **[PIPELINE_EXECUTION_ID.md](./PIPELINE_EXECUTION_ID.md)**.
|
|
134
|
+
|
|
135
|
+
#### 1. Environment variable (recommended in CI)
|
|
136
|
+
|
|
137
|
+
Set one of these env vars **before** each Playwright step. The reporter uses the **first one it finds** (in this order):
|
|
138
|
+
|
|
139
|
+
| Env var | When to use |
|
|
140
|
+
|---------|--------------|
|
|
141
|
+
| `TESTLENS_EXECUTION_ID` | Any pipeline; set this to your build/run ID |
|
|
142
|
+
| `TestlensExecutionId`, `TestLensExecutionId`, `testlensexecutionid` | Alternative spellings |
|
|
143
|
+
| `BITBUCKET_BUILD_UUID` | Bitbucket Pipelines (auto-set; no config needed) |
|
|
144
|
+
| `GITHUB_RUN_ID` | GitHub Actions (auto-set) |
|
|
145
|
+
| `CI_PIPELINE_ID`, `CI_JOB_ID` | GitLab CI |
|
|
146
|
+
| `BUILD_BUILDID`, `SYSTEM_JOBID` | Azure DevOps |
|
|
147
|
+
| `BUILD_ID`, `BUILD_NUMBER` | Jenkins |
|
|
148
|
+
| `CIRCLE_WORKFLOW_ID`, `CIRCLE_BUILD_NUM` | CircleCI |
|
|
149
|
+
|
|
150
|
+
**Command examples:**
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Linux / macOS
|
|
154
|
+
export TESTLENS_EXECUTION_ID="${BITBUCKET_BUILD_UUID}"
|
|
155
|
+
npx playwright test
|
|
156
|
+
|
|
157
|
+
# Or inline
|
|
158
|
+
TESTLENS_EXECUTION_ID="my-build-123" npx playwright test
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
# Windows PowerShell
|
|
163
|
+
$env:TESTLENS_EXECUTION_ID = $env:BITBUCKET_BUILD_UUID
|
|
164
|
+
npx playwright test
|
|
165
|
+
|
|
166
|
+
# Or inline
|
|
167
|
+
$env:TESTLENS_EXECUTION_ID="my-build-123"; npx playwright test
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```cmd
|
|
171
|
+
REM Windows CMD
|
|
172
|
+
set TESTLENS_EXECUTION_ID=my-build-123 && npx playwright test
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### 2. Config option `executionId`
|
|
176
|
+
|
|
177
|
+
In your Playwright config, set the top-level reporter option:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// playwright.config.ts
|
|
181
|
+
reporter: [
|
|
182
|
+
['@alternative-path/testlens-playwright-reporter', {
|
|
183
|
+
apiKey: process.env.TESTLENS_API_KEY,
|
|
184
|
+
executionId: process.env.BITBUCKET_BUILD_UUID, // or any string
|
|
185
|
+
}]
|
|
186
|
+
],
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### 3. Config option `customMetadata`
|
|
190
|
+
|
|
191
|
+
You can pass the execution ID inside `customMetadata` (as `executionId` or `TESTLENS_EXECUTION_ID`):
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// playwright.config.ts
|
|
195
|
+
reporter: [
|
|
196
|
+
['@alternative-path/testlens-playwright-reporter', {
|
|
197
|
+
apiKey: process.env.TESTLENS_API_KEY,
|
|
198
|
+
customMetadata: {
|
|
199
|
+
executionId: process.env.BITBUCKET_BUILD_UUID,
|
|
200
|
+
// or: TESTLENS_EXECUTION_ID: process.env.BITBUCKET_BUILD_UUID,
|
|
201
|
+
testlensBuildName: 'Build123',
|
|
202
|
+
testlensBuildTag: 'smoke',
|
|
203
|
+
},
|
|
204
|
+
}]
|
|
205
|
+
],
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### Recommendation
|
|
209
|
+
|
|
210
|
+
Prefer a **UUID or globally unique ID** (e.g. `BITBUCKET_BUILD_UUID`, `GITHUB_RUN_ID`) so different repos or pipelines never share the same run. Build numbers like `BUILD_NUMBER` reset per repo and can collide. See **[PIPELINE_EXECUTION_ID.md](./PIPELINE_EXECUTION_ID.md)** for per-CI examples.
|
|
129
211
|
|
|
130
212
|
### Notes
|
|
131
213
|
|
|
@@ -140,6 +222,7 @@ The reporter automatically reads these environment variables (no config changes
|
|
|
140
222
|
| Option | Type | Default | Description |
|
|
141
223
|
|--------|------|---------|-------------|
|
|
142
224
|
| `apiKey` | `string` | **Required** | Your TestLens API key |
|
|
225
|
+
| `executionId` | `string` | — | Optional. When set (or use env `TESTLENS_EXECUTION_ID`, any auto-detected CI run ID, or `customMetadata.executionId` / `customMetadata.TESTLENS_EXECUTION_ID`), used as run ID so multiple pipeline steps share one run. See [PIPELINE_EXECUTION_ID.md](./PIPELINE_EXECUTION_ID.md). |
|
|
143
226
|
|
|
144
227
|
## Artifacts
|
|
145
228
|
|
package/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface TestLensReporterConfig {
|
|
|
28
28
|
ignoreSslErrors?: boolean;
|
|
29
29
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
30
30
|
customMetadata?: Record<string, string | string[]>;
|
|
31
|
+
/** Execution ID for one run per pipeline (e.g. from TESTLENS_EXECUTION_ID or CI build UUID). When set, used as runId so multiple steps share one run. */
|
|
32
|
+
executionId?: string;
|
|
31
33
|
}
|
|
32
34
|
export interface TestLensReporterOptions {
|
|
33
35
|
/** TestLens API endpoint URL */
|
|
@@ -58,6 +60,8 @@ export interface TestLensReporterOptions {
|
|
|
58
60
|
ignoreSslErrors?: boolean;
|
|
59
61
|
/** Custom metadata from CLI arguments (automatically parsed from --key=value arguments) */
|
|
60
62
|
customMetadata?: Record<string, string | string[]>;
|
|
63
|
+
/** Execution ID for one run per pipeline (e.g. from TESTLENS_EXECUTION_ID or CI build UUID). When set, used as runId so multiple steps share one run. */
|
|
64
|
+
executionId?: string;
|
|
61
65
|
}
|
|
62
66
|
export interface GitInfo {
|
|
63
67
|
branch: string;
|
|
@@ -99,6 +103,7 @@ export interface RunMetadata {
|
|
|
99
103
|
passedTests?: number;
|
|
100
104
|
failedTests?: number;
|
|
101
105
|
skippedTests?: number;
|
|
106
|
+
timedOutTests?: number;
|
|
102
107
|
status?: string;
|
|
103
108
|
testlensBuildName?: string;
|
|
104
109
|
customMetadata?: Record<string, string | string[]>;
|
|
@@ -156,11 +161,13 @@ export declare class TestLensReporter implements Reporter {
|
|
|
156
161
|
private axiosInstance;
|
|
157
162
|
private runId;
|
|
158
163
|
private runMetadata;
|
|
164
|
+
private usedExecutionId;
|
|
159
165
|
private specMap;
|
|
160
166
|
private testMap;
|
|
161
167
|
private runCreationFailed;
|
|
162
168
|
private cliArgs;
|
|
163
169
|
private pendingUploads;
|
|
170
|
+
private traceNetworkRows;
|
|
164
171
|
private artifactStats;
|
|
165
172
|
private artifactsSeen;
|
|
166
173
|
/**
|
package/index.js
CHANGED
|
@@ -91,6 +91,23 @@ class TestLensReporter {
|
|
|
91
91
|
// Support both TestLens-specific names (recommended) and common CI names
|
|
92
92
|
'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
|
|
93
93
|
'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
|
|
94
|
+
// Execution ID for one run per pipeline (checked in order; prefer TESTLENS_EXECUTION_ID, then CI-specific UUIDs)
|
|
95
|
+
'executionId': [
|
|
96
|
+
'TESTLENS_EXECUTION_ID',
|
|
97
|
+
'TestlensExecutionId',
|
|
98
|
+
'TestLensExecutionId',
|
|
99
|
+
'testlensexecutionid',
|
|
100
|
+
'BITBUCKET_BUILD_UUID',
|
|
101
|
+
'GITHUB_RUN_ID',
|
|
102
|
+
'CI_PIPELINE_ID',
|
|
103
|
+
'CI_JOB_ID',
|
|
104
|
+
'BUILD_BUILDID',
|
|
105
|
+
'SYSTEM_JOBID',
|
|
106
|
+
'BUILD_ID',
|
|
107
|
+
'BUILD_NUMBER',
|
|
108
|
+
'CIRCLE_WORKFLOW_ID',
|
|
109
|
+
'CIRCLE_BUILD_NUM'
|
|
110
|
+
],
|
|
94
111
|
'environment': ['ENVIRONMENT', 'ENV', 'NODE_ENV', 'DEPLOYMENT_ENV'],
|
|
95
112
|
'branch': ['BRANCH', 'GIT_BRANCH', 'CI_COMMIT_BRANCH', 'GITHUB_REF_NAME'],
|
|
96
113
|
'team': ['TEAM', 'TEAM_NAME'],
|
|
@@ -118,9 +135,11 @@ class TestLensReporter {
|
|
|
118
135
|
return customArgs;
|
|
119
136
|
}
|
|
120
137
|
constructor(options) {
|
|
138
|
+
this.usedExecutionId = false; // True when runId came from executionId (multi-step); backend should aggregate runEnd
|
|
121
139
|
this.runCreationFailed = false; // Track if run creation failed due to limits
|
|
122
140
|
this.cliArgs = {}; // Store CLI args separately
|
|
123
141
|
this.pendingUploads = new Set(); // Track pending artifact uploads
|
|
142
|
+
this.traceNetworkRows = []; // Network requests/responses from trace zip for current test
|
|
124
143
|
this.artifactStats = {
|
|
125
144
|
uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
126
145
|
skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
|
|
@@ -156,7 +175,8 @@ class TestLensReporter {
|
|
|
156
175
|
timeout: options.timeout || 60000,
|
|
157
176
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
158
177
|
ignoreSslErrors: options.ignoreSslErrors,
|
|
159
|
-
customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
|
|
178
|
+
customMetadata: { ...options.customMetadata, ...customArgs }, // Config metadata first, then CLI args override
|
|
179
|
+
executionId: options.executionId
|
|
160
180
|
};
|
|
161
181
|
if (!this.config.apiKey) {
|
|
162
182
|
throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config or set one of these environment variables: TESTLENS_API_KEY, TESTLENS_KEY, PLAYWRIGHT_API_KEY, PW_API_KEY, API_KEY, or APIKEY.');
|
|
@@ -222,7 +242,17 @@ class TestLensReporter {
|
|
|
222
242
|
}
|
|
223
243
|
return Promise.reject(error);
|
|
224
244
|
});
|
|
225
|
-
this.
|
|
245
|
+
const executionIdFromCustomMetadata = this.config.customMetadata?.executionId ?? this.config.customMetadata?.TESTLENS_EXECUTION_ID;
|
|
246
|
+
const executionIdFromCustomMetadataStr = Array.isArray(executionIdFromCustomMetadata)
|
|
247
|
+
? executionIdFromCustomMetadata[0]
|
|
248
|
+
: executionIdFromCustomMetadata;
|
|
249
|
+
const executionIdRaw = options.executionId ??
|
|
250
|
+
process.env.TESTLENS_EXECUTION_ID ??
|
|
251
|
+
customArgs.executionId ??
|
|
252
|
+
executionIdFromCustomMetadataStr;
|
|
253
|
+
const executionId = typeof executionIdRaw === 'string' && executionIdRaw.trim() ? String(executionIdRaw).trim() : undefined;
|
|
254
|
+
this.runId = executionId || (0, crypto_1.randomUUID)();
|
|
255
|
+
this.usedExecutionId = !!executionId;
|
|
226
256
|
this.runMetadata = this.initializeRunMetadata();
|
|
227
257
|
this.specMap = new Map();
|
|
228
258
|
this.testMap = new Map();
|
|
@@ -406,7 +436,9 @@ class TestLensReporter {
|
|
|
406
436
|
}
|
|
407
437
|
}
|
|
408
438
|
async onTestEnd(test, result) {
|
|
439
|
+
this.traceNetworkRows = []; // Reset at start of each test
|
|
409
440
|
const testId = this.getTestId(test);
|
|
441
|
+
let testCaseId = '';
|
|
410
442
|
let testData = this.testMap.get(testId);
|
|
411
443
|
logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
|
|
412
444
|
if (result.attachments && result.attachments.length > 0) {
|
|
@@ -577,10 +609,15 @@ class TestLensReporter {
|
|
|
577
609
|
timestamp: new Date().toISOString(),
|
|
578
610
|
test: testData
|
|
579
611
|
});
|
|
612
|
+
testCaseId = testEndResponse?.testCaseId;
|
|
580
613
|
// Handle artifacts (test case is now guaranteed to be in database)
|
|
581
614
|
if (this.config.enableArtifacts) {
|
|
582
|
-
// Pass test case DB ID if available for faster lookups
|
|
583
|
-
|
|
615
|
+
// Pass test case DB ID if available for faster lookups; pass status/endTime so backend
|
|
616
|
+
// can fix up test case status if testEnd failed but artifact request succeeds
|
|
617
|
+
await this.processArtifacts(testId, result, testEndResponse?.testCaseId, {
|
|
618
|
+
status: testData.status,
|
|
619
|
+
endTime: testData.endTime
|
|
620
|
+
});
|
|
584
621
|
}
|
|
585
622
|
else if (result.attachments && result.attachments.length > 0) {
|
|
586
623
|
for (const attachment of result.attachments) {
|
|
@@ -650,7 +687,7 @@ class TestLensReporter {
|
|
|
650
687
|
spec: specData
|
|
651
688
|
});
|
|
652
689
|
// Send spec code blocks to API
|
|
653
|
-
await this.sendSpecCodeBlocks(specPath);
|
|
690
|
+
await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId);
|
|
654
691
|
}
|
|
655
692
|
}
|
|
656
693
|
}
|
|
@@ -705,7 +742,8 @@ class TestLensReporter {
|
|
|
705
742
|
failedTests, // Already includes timedOut tests (normalized to 'failed')
|
|
706
743
|
skippedTests,
|
|
707
744
|
timedOutTests, // For informational purposes
|
|
708
|
-
status: normalizedRunStatus
|
|
745
|
+
status: normalizedRunStatus,
|
|
746
|
+
aggregationMode: this.usedExecutionId ? 'append' : 'replace'
|
|
709
747
|
}
|
|
710
748
|
});
|
|
711
749
|
// Show Build Name if provided, otherwise show Run ID
|
|
@@ -815,7 +853,7 @@ class TestLensReporter {
|
|
|
815
853
|
// Don't throw error to avoid breaking test execution
|
|
816
854
|
}
|
|
817
855
|
}
|
|
818
|
-
async processArtifacts(testId, result, testCaseDbId) {
|
|
856
|
+
async processArtifacts(testId, result, testCaseDbId, testEndPayload) {
|
|
819
857
|
// Skip artifact processing if run creation failed
|
|
820
858
|
if (this.runCreationFailed) {
|
|
821
859
|
return;
|
|
@@ -839,6 +877,81 @@ class TestLensReporter {
|
|
|
839
877
|
this.bumpArtifactStat('skipped', artifactType);
|
|
840
878
|
continue;
|
|
841
879
|
}
|
|
880
|
+
// Trace zip (default from Playwright): extract network-like data and print to console
|
|
881
|
+
const isTraceZip = artifactType === 'trace' && attachment.path && (attachment.path.endsWith('.zip') || attachment.contentType === 'application/zip');
|
|
882
|
+
if (isTraceZip) {
|
|
883
|
+
try {
|
|
884
|
+
const traceStart = Date.now();
|
|
885
|
+
const AdmZip = require('adm-zip');
|
|
886
|
+
const zip = new AdmZip(attachment.path);
|
|
887
|
+
const entries = zip.getEntries();
|
|
888
|
+
const networkRows = [];
|
|
889
|
+
for (const e of entries) {
|
|
890
|
+
if (e.isDirectory || !e.getData)
|
|
891
|
+
continue;
|
|
892
|
+
const name = (e.entryName || '').toLowerCase();
|
|
893
|
+
const isNetworkFile = name.endsWith('.network') || name.includes('.network') || name === 'network';
|
|
894
|
+
if (!isNetworkFile)
|
|
895
|
+
continue;
|
|
896
|
+
const data = e.getData();
|
|
897
|
+
if (!data)
|
|
898
|
+
continue;
|
|
899
|
+
const text = data.toString('utf8');
|
|
900
|
+
if (!text || text.length < 2)
|
|
901
|
+
continue;
|
|
902
|
+
const isApplicationJson = (o) => {
|
|
903
|
+
if (!o || typeof o !== 'object')
|
|
904
|
+
return false;
|
|
905
|
+
const res = o.snapshot?.response ?? o.response;
|
|
906
|
+
if (!res)
|
|
907
|
+
return false;
|
|
908
|
+
const mime = res.content?.mimeType;
|
|
909
|
+
if (typeof mime === 'string') {
|
|
910
|
+
const type = mime.split(';')[0].trim().toLowerCase();
|
|
911
|
+
return type === 'application/json';
|
|
912
|
+
}
|
|
913
|
+
const headers = res.headers;
|
|
914
|
+
if (Array.isArray(headers)) {
|
|
915
|
+
const ct = headers.find((h) => (h.name || '').toLowerCase() === 'content-type');
|
|
916
|
+
const val = ct?.value;
|
|
917
|
+
if (typeof val === 'string') {
|
|
918
|
+
const type = val.split(';')[0].trim().toLowerCase();
|
|
919
|
+
return type === 'application/json';
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return false;
|
|
923
|
+
};
|
|
924
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
925
|
+
for (const line of lines) {
|
|
926
|
+
try {
|
|
927
|
+
const obj = JSON.parse(line);
|
|
928
|
+
if (obj && typeof obj === 'object' && isApplicationJson(obj))
|
|
929
|
+
networkRows.push(obj);
|
|
930
|
+
}
|
|
931
|
+
catch (_) { }
|
|
932
|
+
}
|
|
933
|
+
if (networkRows.length === 0 && lines.length > 0) {
|
|
934
|
+
try {
|
|
935
|
+
const arr = JSON.parse(text);
|
|
936
|
+
if (Array.isArray(arr))
|
|
937
|
+
networkRows.push(...arr.filter((x) => x != null && typeof x === 'object' && isApplicationJson(x)));
|
|
938
|
+
}
|
|
939
|
+
catch (_) { }
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const durationMs = Date.now() - traceStart;
|
|
943
|
+
if (networkRows.length > 0) {
|
|
944
|
+
this.traceNetworkRows = networkRows;
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
this.traceNetworkRows = [];
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
catch (e) {
|
|
951
|
+
logger.warn('[TRACE] Could not read trace zip: ' + (e && e.message));
|
|
952
|
+
this.traceNetworkRows = [];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
842
955
|
try {
|
|
843
956
|
// Determine proper filename with extension
|
|
844
957
|
// Playwright attachment.name often doesn't have extension, so we need to derive it
|
|
@@ -894,7 +1007,12 @@ class TestLensReporter {
|
|
|
894
1007
|
fileSize: this.getFileSize(attachment.path),
|
|
895
1008
|
storageType: 's3',
|
|
896
1009
|
s3Key: s3Data.key,
|
|
897
|
-
s3Url: s3Data.url
|
|
1010
|
+
s3Url: s3Data.url,
|
|
1011
|
+
// So backend can fix test case status if testEnd failed but artifact succeeded
|
|
1012
|
+
...(testEndPayload && {
|
|
1013
|
+
testStatus: testEndPayload.status,
|
|
1014
|
+
testEndTime: testEndPayload.endTime
|
|
1015
|
+
})
|
|
898
1016
|
};
|
|
899
1017
|
// Send artifact data to API
|
|
900
1018
|
await this.sendToApi({
|
|
@@ -928,12 +1046,12 @@ class TestLensReporter {
|
|
|
928
1046
|
}
|
|
929
1047
|
}
|
|
930
1048
|
}
|
|
931
|
-
async sendSpecCodeBlocks(specPath) {
|
|
1049
|
+
async sendSpecCodeBlocks(specPath, testName, errors, runId, test_id, testCaseId) {
|
|
932
1050
|
try {
|
|
933
1051
|
// Extract code blocks using built-in parser
|
|
934
1052
|
const testBlocks = this.extractTestBlocks(specPath);
|
|
935
1053
|
// Transform blocks to match backend API expectations
|
|
936
|
-
const codeBlocks = testBlocks.map(block => ({
|
|
1054
|
+
const codeBlocks = testBlocks.filter(block => block.name === testName).map(block => ({
|
|
937
1055
|
type: block.type, // 'test' or 'describe'
|
|
938
1056
|
name: block.name, // test/describe name
|
|
939
1057
|
content: block.content, // full code content
|
|
@@ -951,7 +1069,12 @@ class TestLensReporter {
|
|
|
951
1069
|
await this.axiosInstance.post(specEndpoint, {
|
|
952
1070
|
filePath: path.relative(process.cwd(), specPath),
|
|
953
1071
|
codeBlocks,
|
|
954
|
-
|
|
1072
|
+
errors,
|
|
1073
|
+
traceNetworkRows: this.traceNetworkRows,
|
|
1074
|
+
testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, ''),
|
|
1075
|
+
runId,
|
|
1076
|
+
test_id,
|
|
1077
|
+
testCaseId
|
|
955
1078
|
});
|
|
956
1079
|
logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
|
|
957
1080
|
}
|
|
@@ -970,53 +1093,94 @@ class TestLensReporter {
|
|
|
970
1093
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
971
1094
|
const blocks = [];
|
|
972
1095
|
const lines = content.split('\n');
|
|
973
|
-
|
|
974
|
-
|
|
1096
|
+
// Use a stack to track nested describe blocks with their brace depths
|
|
1097
|
+
const describeStack = [];
|
|
1098
|
+
let globalBraceCount = 0;
|
|
975
1099
|
let inBlock = false;
|
|
976
1100
|
let blockStart = -1;
|
|
1101
|
+
let blockBraceCount = 0;
|
|
977
1102
|
let blockType = 'test';
|
|
978
1103
|
let blockName = '';
|
|
1104
|
+
let blockDescribe = undefined;
|
|
979
1105
|
for (let i = 0; i < lines.length; i++) {
|
|
980
1106
|
const line = lines[i];
|
|
981
1107
|
const trimmedLine = line.trim();
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1108
|
+
if (!inBlock) {
|
|
1109
|
+
// Check for describe blocks: describe(), test.describe(), test.describe.serial(), etc.
|
|
1110
|
+
const describeMatch = trimmedLine.match(/(?:test\.)?describe(?:\.(?:serial|parallel|only|skip|fixme))?\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
1111
|
+
if (describeMatch) {
|
|
1112
|
+
// Count braces on this line to find the opening brace
|
|
1113
|
+
let lineOpenBraces = 0;
|
|
1114
|
+
for (const char of line) {
|
|
1115
|
+
if (char === '{') {
|
|
1116
|
+
globalBraceCount++;
|
|
1117
|
+
lineOpenBraces++;
|
|
1118
|
+
}
|
|
1119
|
+
if (char === '}')
|
|
1120
|
+
globalBraceCount--;
|
|
1121
|
+
}
|
|
1122
|
+
// Push describe onto stack with the current brace depth
|
|
1123
|
+
describeStack.push({ name: describeMatch[1], braceDepth: globalBraceCount });
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
// Check for test blocks: test(), test.only(), test.skip(), test.fixme(), it(), it.only(), etc.
|
|
1127
|
+
const testMatch = trimmedLine.match(/(?:test|it)(?:\.(?:only|skip|fixme|slow))?\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
1128
|
+
if (testMatch) {
|
|
1129
|
+
blockType = 'test';
|
|
1130
|
+
blockName = testMatch[1];
|
|
1131
|
+
blockStart = i;
|
|
1132
|
+
blockBraceCount = 0;
|
|
1133
|
+
// Capture the current innermost describe name
|
|
1134
|
+
blockDescribe = describeStack.length > 0 ? describeStack[describeStack.length - 1].name : undefined;
|
|
1135
|
+
inBlock = true;
|
|
1136
|
+
}
|
|
995
1137
|
}
|
|
996
|
-
// Count braces
|
|
1138
|
+
// Count braces
|
|
997
1139
|
if (inBlock) {
|
|
998
1140
|
for (const char of line) {
|
|
999
|
-
if (char === '{')
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1141
|
+
if (char === '{') {
|
|
1142
|
+
blockBraceCount++;
|
|
1143
|
+
globalBraceCount++;
|
|
1144
|
+
}
|
|
1145
|
+
if (char === '}') {
|
|
1146
|
+
blockBraceCount--;
|
|
1147
|
+
globalBraceCount--;
|
|
1148
|
+
}
|
|
1149
|
+
if (blockBraceCount === 0 && blockStart !== -1 && i > blockStart) {
|
|
1150
|
+
// End of test block found
|
|
1005
1151
|
const blockContent = lines.slice(blockStart, i + 1).join('\n');
|
|
1006
1152
|
blocks.push({
|
|
1007
1153
|
type: blockType,
|
|
1008
1154
|
name: blockName,
|
|
1009
1155
|
content: blockContent,
|
|
1010
|
-
describe:
|
|
1156
|
+
describe: blockDescribe,
|
|
1011
1157
|
startLine: blockStart + 1,
|
|
1012
1158
|
endLine: i + 1
|
|
1013
1159
|
});
|
|
1014
1160
|
inBlock = false;
|
|
1015
1161
|
blockStart = -1;
|
|
1162
|
+
// Pop any describe blocks that have closed
|
|
1163
|
+
while (describeStack.length > 0 && globalBraceCount < describeStack[describeStack.length - 1].braceDepth) {
|
|
1164
|
+
describeStack.pop();
|
|
1165
|
+
}
|
|
1016
1166
|
break;
|
|
1017
1167
|
}
|
|
1018
1168
|
}
|
|
1019
1169
|
}
|
|
1170
|
+
else {
|
|
1171
|
+
// Track braces outside of test blocks (for describe open/close)
|
|
1172
|
+
for (const char of line) {
|
|
1173
|
+
if (char === '{')
|
|
1174
|
+
globalBraceCount++;
|
|
1175
|
+
if (char === '}') {
|
|
1176
|
+
globalBraceCount--;
|
|
1177
|
+
// Pop any describe blocks that have closed
|
|
1178
|
+
while (describeStack.length > 0 && globalBraceCount < describeStack[describeStack.length - 1].braceDepth) {
|
|
1179
|
+
describeStack.pop();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1020
1184
|
}
|
|
1021
1185
|
return blocks;
|
|
1022
1186
|
}
|
|
@@ -1127,7 +1291,6 @@ class TestLensReporter {
|
|
|
1127
1291
|
// Step 1: Request pre-signed URL from server
|
|
1128
1292
|
const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
|
|
1129
1293
|
const requestBody = {
|
|
1130
|
-
apiKey: this.config.apiKey,
|
|
1131
1294
|
testRunId: this.runId,
|
|
1132
1295
|
testId: testId,
|
|
1133
1296
|
fileName: fileName,
|
|
@@ -1169,7 +1332,6 @@ class TestLensReporter {
|
|
|
1169
1332
|
// Step 3: Confirm upload with server to save metadata
|
|
1170
1333
|
const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
|
|
1171
1334
|
const confirmBody = {
|
|
1172
|
-
apiKey: this.config.apiKey,
|
|
1173
1335
|
testRunId: this.runId,
|
|
1174
1336
|
testId: testId,
|
|
1175
1337
|
s3Key: s3Key,
|
|
@@ -1261,7 +1423,7 @@ class TestLensReporter {
|
|
|
1261
1423
|
// Try different ways to access getType method
|
|
1262
1424
|
const getType = mime.getType || mime.default?.getType;
|
|
1263
1425
|
if (typeof getType === 'function') {
|
|
1264
|
-
const mimeType = getType.call(mime, ext) || getType.call(mime.default, ext);
|
|
1426
|
+
const mimeType = getType.call(mime, ext) || (mime.default ? getType.call(mime.default, ext) : null);
|
|
1265
1427
|
return mimeType || 'application/octet-stream';
|
|
1266
1428
|
}
|
|
1267
1429
|
}
|