@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.
Files changed (5) hide show
  1. package/README.md +83 -0
  2. package/index.d.ts +7 -0
  3. package/index.js +199 -37
  4. package/index.ts +1750 -1587
  5. 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.runId = (0, crypto_1.randomUUID)();
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
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId);
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
- testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
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
- let currentDescribe = null;
974
- let braceCount = 0;
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
- // Check for describe blocks
983
- const describeMatch = trimmedLine.match(/describe\s*\(\s*['"`]([^'"`]+)['"`]/);
984
- if (describeMatch) {
985
- currentDescribe = describeMatch[1];
986
- }
987
- // Check for test blocks
988
- const testMatch = trimmedLine.match(/test\s*\(\s*['"`]([^'"`]+)['"`]/);
989
- if (testMatch && !inBlock) {
990
- blockType = 'test';
991
- blockName = testMatch[1];
992
- blockStart = i;
993
- braceCount = 0;
994
- inBlock = true;
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 when in a block
1138
+ // Count braces
997
1139
  if (inBlock) {
998
1140
  for (const char of line) {
999
- if (char === '{')
1000
- braceCount++;
1001
- if (char === '}')
1002
- braceCount--;
1003
- if (braceCount === 0 && blockStart !== -1 && i > blockStart) {
1004
- // End of block found
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: currentDescribe || undefined,
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
  }