playwright-testchimp 0.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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/api-client.d.ts +51 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +282 -0
- package/dist/api-client.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/reporter.d.ts +7 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +26 -0
- package/dist/reporter.js.map +1 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +93 -0
- package/dist/runtime.js.map +1 -0
- package/dist/testchimp-reporter.d.ts +82 -0
- package/dist/testchimp-reporter.d.ts.map +1 -0
- package/dist/testchimp-reporter.js +777 -0
- package/dist/testchimp-reporter.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +93 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +305 -0
- package/dist/utils.js.map +1 -0
- package/dist/worldstate.d.ts +15 -0
- package/dist/worldstate.d.ts.map +1 -0
- package/dist/worldstate.js +136 -0
- package/dist/worldstate.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TestChimpReporter = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
9
|
+
const api_client_1 = require("./api-client");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
/**
|
|
14
|
+
* TestChimp Playwright Reporter
|
|
15
|
+
*
|
|
16
|
+
* Reports test execution data to the TestChimp backend for
|
|
17
|
+
* coverage tracking and traceability.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // playwright.config.ts
|
|
21
|
+
* export default defineConfig({
|
|
22
|
+
* reporter: [
|
|
23
|
+
* ['playwright-testchimp/reporter', {
|
|
24
|
+
* verbose: true,
|
|
25
|
+
* reportOnlyFinalAttempt: true
|
|
26
|
+
* }]
|
|
27
|
+
* ]
|
|
28
|
+
* });
|
|
29
|
+
*/
|
|
30
|
+
class TestChimpReporter {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.apiClient = null;
|
|
33
|
+
this.batchInvocationId = '';
|
|
34
|
+
this.testsFolder = '';
|
|
35
|
+
// Track test executions (keyed by test ID + attempt, e.g. "testId_attempt_0", "testId_attempt_1").
|
|
36
|
+
// In platform mode we keep all attempts until test_end so the job detail we send includes full retryAttemptLogs.
|
|
37
|
+
this.testExecutions = new Map();
|
|
38
|
+
// Track retry counts per test (to identify final attempt)
|
|
39
|
+
this.testRetryInfo = new Map();
|
|
40
|
+
// Platform mode: manifest (test identity -> jobId), loaded once in onBegin
|
|
41
|
+
this.jobManifest = [];
|
|
42
|
+
// Flag to indicate if reporter is properly configured
|
|
43
|
+
this.isEnabled = false;
|
|
44
|
+
this.pendingOperations = [];
|
|
45
|
+
// Env wins over playwright.config reporter options so repair/platform runs can
|
|
46
|
+
// force mode (e.g. scriptservice sets TESTCHIMP_EXECUTION_MODE=repair; customer
|
|
47
|
+
// configs often hard-code executionMode: 'ci', which would otherwise hit FeatureService).
|
|
48
|
+
const envMode = (0, utils_1.getEnvVar)('TESTCHIMP_EXECUTION_MODE')?.trim();
|
|
49
|
+
const executionMode = envMode === 'repair' || envMode === 'platform' || envMode === 'ci'
|
|
50
|
+
? envMode
|
|
51
|
+
: (options.executionMode || 'ci');
|
|
52
|
+
this.options = {
|
|
53
|
+
apiKey: options.apiKey || '',
|
|
54
|
+
backendUrl: options.backendUrl || '',
|
|
55
|
+
platformBackendUrl: options.platformBackendUrl || '',
|
|
56
|
+
batchInvocationId: options.batchInvocationId || '',
|
|
57
|
+
projectId: options.projectId || '',
|
|
58
|
+
testsFolder: options.testsFolder || '',
|
|
59
|
+
release: options.release || '',
|
|
60
|
+
environment: options.environment || '',
|
|
61
|
+
reportOnlyFinalAttempt: options.reportOnlyFinalAttempt ?? true,
|
|
62
|
+
captureScreenshots: options.captureScreenshots ?? true,
|
|
63
|
+
verbose: options.verbose ?? false,
|
|
64
|
+
executionMode
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
onBegin(config, suite) {
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.batchInvocationId = (0, utils_1.getEnvVar)('TESTCHIMP_BATCH_INVOCATION_ID', this.options.batchInvocationId) || (0, utils_1.generateUUID)();
|
|
70
|
+
// Initialize configuration from env vars (env vars take precedence)
|
|
71
|
+
const apiKey = (0, utils_1.getEnvVar)('TESTCHIMP_API_KEY', this.options.apiKey);
|
|
72
|
+
const projectId = (0, utils_1.getEnvVar)('TESTCHIMP_PROJECT_ID', this.options.projectId);
|
|
73
|
+
this.testsFolder = (0, utils_1.getEnvVar)('TESTCHIMP_TESTS_FOLDER', this.options.testsFolder) || 'tests';
|
|
74
|
+
// In platform/repair mode reporter calls scriptservice (step_end/test_end or repair_* endpoints)
|
|
75
|
+
// via TESTCHIMP_PLATFORM_BACKEND_URL; TESTCHIMP_BACKEND_URL stays as featureservice for ai-wright etc.
|
|
76
|
+
const backendUrl = this.options.executionMode === 'platform' || this.options.executionMode === 'repair'
|
|
77
|
+
? (0, utils_1.getEnvVar)('TESTCHIMP_PLATFORM_BACKEND_URL', this.options.platformBackendUrl) || (0, utils_1.getEnvVar)('TESTCHIMP_BACKEND_URL', this.options.backendUrl) || 'https://featureservice.testchimp.io'
|
|
78
|
+
: (0, utils_1.getEnvVar)('TESTCHIMP_BACKEND_URL', this.options.backendUrl) || 'https://featureservice.testchimp.io';
|
|
79
|
+
// Update options with env var values for release/environment
|
|
80
|
+
this.options.release = (0, utils_1.getEnvVar)('TESTCHIMP_RELEASE', this.options.release) || '';
|
|
81
|
+
this.options.environment = (0, utils_1.getEnvVar)('TESTCHIMP_ENV', this.options.environment) || '';
|
|
82
|
+
// In repair mode we allow reporting to scriptservice localhost without an API key.
|
|
83
|
+
// (API client still requires a header value, so we pass a dummy string.)
|
|
84
|
+
if (!apiKey && this.options.executionMode !== 'repair') {
|
|
85
|
+
console.warn('[TestChimp] Missing TESTCHIMP_API_KEY. Reporting disabled.');
|
|
86
|
+
this.isEnabled = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.apiClient = new api_client_1.TestChimpApiClient(backendUrl, apiKey || 'local-repair', projectId || '', this.options.verbose);
|
|
90
|
+
this.isEnabled = true;
|
|
91
|
+
if (this.options.executionMode === 'platform') {
|
|
92
|
+
this.jobManifest = this.loadJobManifest();
|
|
93
|
+
const manifestPath = (0, utils_1.getEnvVar)('TESTCHIMP_JOB_MANIFEST_PATH') ||
|
|
94
|
+
(this.testsFolder ? path_1.default.join(this.testsFolder, '.testchimp-job-manifest.json') : '.testchimp-job-manifest.json');
|
|
95
|
+
const rootDir = this.config?.rootDir || process.cwd();
|
|
96
|
+
const resolvedPath = path_1.default.isAbsolute(manifestPath) ? manifestPath : path_1.default.join(rootDir, manifestPath);
|
|
97
|
+
const sample = this.jobManifest.length > 0
|
|
98
|
+
? ` (sample: ${JSON.stringify(this.jobManifest.slice(0, 2).map((e) => ({ fileId: e.fileId, testId: e.testId, folderPath: e.folderPath, fileName: e.fileName, suitePath: e.suitePath ?? [], testName: e.testName })))}`
|
|
99
|
+
: '';
|
|
100
|
+
console.log(`[TestChimp] Platform mode: manifest from ${resolvedPath} → ${this.jobManifest.length} entries${sample} (backend for step_end/test_end: ${backendUrl})`);
|
|
101
|
+
}
|
|
102
|
+
if (this.options.verbose) {
|
|
103
|
+
console.log(`[TestChimp] Reporter initialized. Batch ID: ${this.batchInvocationId}`);
|
|
104
|
+
console.log(`[TestChimp] Tests folder: ${this.testsFolder || '(root)'}`);
|
|
105
|
+
console.log(`[TestChimp] Execution mode: ${this.options.executionMode}`);
|
|
106
|
+
}
|
|
107
|
+
// Scan suite to understand retry configuration
|
|
108
|
+
this.scanTestRetries(suite);
|
|
109
|
+
}
|
|
110
|
+
loadJobManifest() {
|
|
111
|
+
const manifestPath = (0, utils_1.getEnvVar)('TESTCHIMP_JOB_MANIFEST_PATH') ||
|
|
112
|
+
(this.testsFolder ? path_1.default.join(this.testsFolder, '.testchimp-job-manifest.json') : '.testchimp-job-manifest.json');
|
|
113
|
+
const rootDir = this.config?.rootDir || process.cwd();
|
|
114
|
+
const resolvedPath = path_1.default.isAbsolute(manifestPath) ? manifestPath : path_1.default.join(rootDir, manifestPath);
|
|
115
|
+
try {
|
|
116
|
+
const content = fs_1.default.readFileSync(resolvedPath, 'utf8');
|
|
117
|
+
const parsed = JSON.parse(content);
|
|
118
|
+
const entries = Array.isArray(parsed) ? parsed : [];
|
|
119
|
+
if (entries.length === 0 && this.options.executionMode === 'platform') {
|
|
120
|
+
console.warn(`[TestChimp] Platform mode: manifest at ${resolvedPath} is empty or not an array (step_end/test_end will be skipped).`);
|
|
121
|
+
}
|
|
122
|
+
return entries;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
126
|
+
console.warn(`[TestChimp] Could not load job manifest from ${resolvedPath}: ${msg}`);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve jobId from the platform manifest.
|
|
132
|
+
* - No describe block: both parser and Playwright use suitePath [] → exact match.
|
|
133
|
+
* - Global describe(): parser only sees test.describe() so manifest has []; Playwright reports e.g. ["Suite"] → fallback with [].
|
|
134
|
+
*/
|
|
135
|
+
getJobFromManifest(folderPath, fileName, suitePath, testName) {
|
|
136
|
+
const resolved = (0, utils_1.resolveManifestEntryFromRuntime)(this.jobManifest, { folderPath, fileName, suitePath, testName });
|
|
137
|
+
if (!resolved?.entry?.jobId)
|
|
138
|
+
return undefined;
|
|
139
|
+
return { jobId: resolved.entry.jobId, strategy: resolved.strategy };
|
|
140
|
+
}
|
|
141
|
+
getManifestDebugCandidates(fileName, testName, limit = 3) {
|
|
142
|
+
const candidates = this.jobManifest
|
|
143
|
+
.filter((e) => e.fileName === fileName || e.testName === testName)
|
|
144
|
+
.slice(0, limit)
|
|
145
|
+
.map((e) => ({
|
|
146
|
+
folderPath: (0, utils_1.normalizeManifestFolderPath)(e.folderPath),
|
|
147
|
+
fileName: e.fileName,
|
|
148
|
+
suitePath: e.suitePath || [],
|
|
149
|
+
testName: e.testName
|
|
150
|
+
}));
|
|
151
|
+
return JSON.stringify(candidates);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build full job detail from all attempts for this test (from in-memory testExecutions).
|
|
155
|
+
* For step_end: currentAttemptIsFinal=false (current attempt in progress).
|
|
156
|
+
* For test_end: currentAttemptIsFinal=true, finalStatus/error from result.
|
|
157
|
+
* Past attempts are marked FAILED (retry implies they did not succeed).
|
|
158
|
+
*/
|
|
159
|
+
buildJobDetailFromAttempts(testId, testName, upToRetryInclusive, currentAttemptIsFinal, finalStatus, finalError) {
|
|
160
|
+
// Only prior attempts go in retryAttemptLogs; last run is in jobDetail.steps (no duplication when 1 run).
|
|
161
|
+
const retryAttemptLogs = [];
|
|
162
|
+
for (let r = 0; r < upToRetryInclusive; r++) {
|
|
163
|
+
const key = `${testId}_attempt_${r}`;
|
|
164
|
+
const exec = this.testExecutions.get(key);
|
|
165
|
+
if (!exec)
|
|
166
|
+
continue;
|
|
167
|
+
retryAttemptLogs.push({
|
|
168
|
+
retryCount: r,
|
|
169
|
+
steps: [...exec.steps],
|
|
170
|
+
status: types_1.SmartTestExecutionStatus.SMART_TEST_EXECUTION_FAILED,
|
|
171
|
+
error: undefined
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const currentExec = this.testExecutions.get(`${testId}_attempt_${upToRetryInclusive}`);
|
|
175
|
+
const steps = currentExec?.steps ?? [];
|
|
176
|
+
return {
|
|
177
|
+
testName,
|
|
178
|
+
steps,
|
|
179
|
+
status: currentAttemptIsFinal && finalStatus !== undefined ? finalStatus : types_1.SmartTestExecutionStatus.SMART_TEST_EXECUTION_IN_PROGRESS,
|
|
180
|
+
error: currentAttemptIsFinal ? finalError : undefined,
|
|
181
|
+
scenarioCoverageResults: [],
|
|
182
|
+
retryAttemptLogs
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Build current job detail for platform step_end (all attempts so far, current still in progress) */
|
|
186
|
+
buildCurrentJobDetailForPlatform(test, currentRetry, testName) {
|
|
187
|
+
return this.buildJobDetailFromAttempts(test.id, testName, currentRetry, false);
|
|
188
|
+
}
|
|
189
|
+
/** Build final job detail for platform test_end (all attempts with final status) */
|
|
190
|
+
buildFinalJobDetailForPlatform(test, result, testName, currentSteps) {
|
|
191
|
+
const status = this.mapStatus(result.status);
|
|
192
|
+
const jobDetail = this.buildJobDetailFromAttempts(test.id, testName, result.retry, true, status, result.error?.message);
|
|
193
|
+
return {
|
|
194
|
+
...jobDetail,
|
|
195
|
+
steps: [...currentSteps],
|
|
196
|
+
pwError: this.toPlaywrightError(result.error)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
onTestBegin(test, result) {
|
|
200
|
+
console.log(`[TestChimp] onTestBegin called for test: ${test.title} (retry: ${result.retry})`);
|
|
201
|
+
if (!this.isEnabled) {
|
|
202
|
+
console.log(`[TestChimp] Reporter is not enabled, skipping test start tracking for: ${test.title}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const testKey = this.getTestKey(test, result.retry);
|
|
206
|
+
this.testExecutions.set(testKey, {
|
|
207
|
+
testCase: test,
|
|
208
|
+
steps: [],
|
|
209
|
+
startedAt: Date.now(),
|
|
210
|
+
attemptNumber: result.retry + 1
|
|
211
|
+
});
|
|
212
|
+
console.log(`[TestChimp] Created execution state for test: ${test.title} (key: ${testKey})`);
|
|
213
|
+
// Update retry tracking
|
|
214
|
+
const retryKey = test.id;
|
|
215
|
+
const retryInfo = this.testRetryInfo.get(retryKey);
|
|
216
|
+
if (retryInfo) {
|
|
217
|
+
retryInfo.currentAttempt = result.retry;
|
|
218
|
+
}
|
|
219
|
+
if (this.options.verbose) {
|
|
220
|
+
console.log(`[TestChimp] Test started: ${test.title} (attempt ${result.retry + 1})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
onStepEnd(test, result, step) {
|
|
224
|
+
if (!this.isEnabled)
|
|
225
|
+
return;
|
|
226
|
+
// Repair mode: emit lightweight progress events (multiple healer reruns are grouped by run index).
|
|
227
|
+
if (this.options.executionMode === 'repair' && this.apiClient) {
|
|
228
|
+
const jobId = (0, utils_1.getEnvVar)('TESTCHIMP_REPAIR_JOB_ID', '') || '';
|
|
229
|
+
if (jobId) {
|
|
230
|
+
const runIndexRaw = (0, utils_1.getEnvVar)('TESTCHIMP_REPAIR_RUN_INDEX', '0') || '0';
|
|
231
|
+
const runIndex = Number(runIndexRaw) || 0;
|
|
232
|
+
const event = {
|
|
233
|
+
runIndex,
|
|
234
|
+
timestampMillis: Date.now(),
|
|
235
|
+
message: `[${step.category}] ${step.title}${step.error?.message ? ` (error: ${step.error.message})` : ''}`,
|
|
236
|
+
phase: 'RUNNING_HEALER',
|
|
237
|
+
rawPayloadJson: JSON.stringify({
|
|
238
|
+
testTitle: test.title,
|
|
239
|
+
retry: result.retry,
|
|
240
|
+
step: { title: step.title, category: step.category, duration: step.duration },
|
|
241
|
+
error: step.error ? { message: step.error.message, stack: step.error.stack } : undefined,
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
const p = this.apiClient.repairStepEnd(jobId, event).catch((err) => {
|
|
245
|
+
console.error(`[TestChimp] repair_step_end failed jobId=${jobId}:`, err instanceof Error ? err.message : err);
|
|
246
|
+
});
|
|
247
|
+
this.pendingOperations.push(p);
|
|
248
|
+
}
|
|
249
|
+
// Continue capturing steps locally for consistency (no-op for repair UI today).
|
|
250
|
+
}
|
|
251
|
+
const testKey = this.getTestKey(test, result.retry);
|
|
252
|
+
const execution = this.testExecutions.get(testKey);
|
|
253
|
+
if (!execution)
|
|
254
|
+
return;
|
|
255
|
+
// Log all steps when verbose is enabled (for debugging)
|
|
256
|
+
if (this.options.verbose) {
|
|
257
|
+
console.log(`[TestChimp] Step seen: "${step.title}" (category: ${step.category})`);
|
|
258
|
+
}
|
|
259
|
+
// Capture test.step (user-defined steps), expect (assertions), and pw:api (Playwright API calls)
|
|
260
|
+
// Exclude internal hooks, fixtures, and attachments
|
|
261
|
+
if (step.category !== 'test.step' && step.category !== 'expect' && step.category !== 'pw:api') {
|
|
262
|
+
if (this.options.verbose) {
|
|
263
|
+
console.log(`[TestChimp] Step filtered out: "${step.title}" (category: ${step.category})`);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const stepNumber = execution.steps.length + 1;
|
|
268
|
+
const stepId = (0, utils_1.generateStepId)(stepNumber);
|
|
269
|
+
const executionStep = {
|
|
270
|
+
stepId,
|
|
271
|
+
description: this.getStepDescription(step),
|
|
272
|
+
status: step.error
|
|
273
|
+
? types_1.StepExecutionStatus.FAILURE_STEP_EXECUTION
|
|
274
|
+
: types_1.StepExecutionStatus.SUCCESS_STEP_EXECUTION,
|
|
275
|
+
error: step.error?.message,
|
|
276
|
+
pwStepCategory: step.category,
|
|
277
|
+
durationMs: step.duration,
|
|
278
|
+
pwError: this.toPlaywrightError(step.error),
|
|
279
|
+
wasRepaired: false
|
|
280
|
+
};
|
|
281
|
+
execution.steps.push(executionStep);
|
|
282
|
+
if (this.options.verbose) {
|
|
283
|
+
console.log(`[TestChimp] Step captured: ${stepNumber} (${step.category}): "${executionStep.description}" (raw: "${step.title}") - ${executionStep.status}`);
|
|
284
|
+
}
|
|
285
|
+
// Platform mode: after each step, send full job detail to scriptservice (blind upsert)
|
|
286
|
+
if (this.options.executionMode === 'platform' && this.apiClient) {
|
|
287
|
+
const paths = (0, utils_1.derivePaths)(test, this.testsFolder, this.config.rootDir, false);
|
|
288
|
+
const resolved = this.getJobFromManifest(paths.folderPath, paths.fileName, paths.suitePath, paths.testName);
|
|
289
|
+
if (resolved?.jobId) {
|
|
290
|
+
const jobId = resolved.jobId;
|
|
291
|
+
console.log(`[TestChimp] platform/step_end resolve strategy=${resolved.strategy} jobId=${jobId} fileName="${paths.fileName}" suitePath=${JSON.stringify(paths.suitePath)} testName="${paths.testName}"`);
|
|
292
|
+
const jobDetail = this.buildCurrentJobDetailForPlatform(test, result.retry, paths.testName);
|
|
293
|
+
const p = this.apiClient.platformStepEnd(jobId, jobDetail).then(() => {
|
|
294
|
+
if (this.options.verbose) {
|
|
295
|
+
console.log(`[TestChimp] platform/step_end ok jobId=${jobId} steps=${jobDetail.steps?.length ?? 0}`);
|
|
296
|
+
}
|
|
297
|
+
}, (err) => {
|
|
298
|
+
console.error(`[TestChimp] platform/step_end failed jobId=${jobId}:`, err instanceof Error ? err.message : err);
|
|
299
|
+
});
|
|
300
|
+
this.pendingOperations.push(p);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.warn(`[TestChimp] platform/step_end skipped: no jobId in manifest for folderPath="${paths.folderPath}" normalizedFolderPath="${(0, utils_1.normalizeManifestFolderPath)(paths.folderPath)}" fileName="${paths.fileName}" suitePath=${JSON.stringify(paths.suitePath)} testName="${paths.testName}" candidates=${this.getManifestDebugCandidates(paths.fileName, paths.testName)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async onTestEnd(test, result) {
|
|
308
|
+
const p = this._onTestEndInner(test, result);
|
|
309
|
+
this.pendingOperations.push(p);
|
|
310
|
+
await p;
|
|
311
|
+
}
|
|
312
|
+
async _onTestEndInner(test, result) {
|
|
313
|
+
console.log(`[TestChimp] onTestEnd called for test: ${test.title} (status: ${result.status}, retry: ${result.retry})`);
|
|
314
|
+
if (!this.isEnabled) {
|
|
315
|
+
console.log(`[TestChimp] Reporter is not enabled, skipping report for: ${test.title}`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!this.apiClient) {
|
|
319
|
+
console.log(`[TestChimp] API client is not initialized, skipping report for: ${test.title}`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Repair mode: emit end-of-run marker and stop (no CI/platform ingest report).
|
|
323
|
+
if (this.options.executionMode === 'repair') {
|
|
324
|
+
const jobId = (0, utils_1.getEnvVar)('TESTCHIMP_REPAIR_JOB_ID', '') || '';
|
|
325
|
+
if (jobId) {
|
|
326
|
+
const runIndexRaw = (0, utils_1.getEnvVar)('TESTCHIMP_REPAIR_RUN_INDEX', '0') || '0';
|
|
327
|
+
const runIndex = Number(runIndexRaw) || 0;
|
|
328
|
+
const summary = {
|
|
329
|
+
runIndex,
|
|
330
|
+
timestampMillis: Date.now(),
|
|
331
|
+
status: result.status,
|
|
332
|
+
message: `Repair run completed with status=${result.status} retry=${result.retry}`,
|
|
333
|
+
errorMessage: result.error?.message,
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
await this.apiClient.repairTestEnd(jobId, summary);
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
console.error(`[TestChimp] repair_test_end failed jobId=${jobId}:`, e);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const testKey = this.getTestKey(test, result.retry);
|
|
345
|
+
const execution = this.testExecutions.get(testKey);
|
|
346
|
+
if (!execution) {
|
|
347
|
+
console.log(`[TestChimp] No execution state found for test: ${test.title} (key: ${testKey}), skipping report`);
|
|
348
|
+
console.log(`[TestChimp] Available execution keys: ${Array.from(this.testExecutions.keys()).join(', ')}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Check if this is the final attempt (for retry handling)
|
|
352
|
+
// If test passed, it's always the final attempt (no retries will occur)
|
|
353
|
+
// If test failed, check if we've reached max retries
|
|
354
|
+
const retryKey = test.id;
|
|
355
|
+
const retryInfo = this.testRetryInfo.get(retryKey);
|
|
356
|
+
const testPassed = result.status === 'passed';
|
|
357
|
+
const isFinalAttempt = testPassed || !retryInfo || result.retry >= retryInfo.maxRetries;
|
|
358
|
+
console.log(`[TestChimp] Test status: ${result.status}, retry: ${result.retry}, maxRetries: ${retryInfo?.maxRetries ?? 'unknown'}, isFinalAttempt: ${isFinalAttempt}`);
|
|
359
|
+
// Platform mode: we keep all retry attempts in testExecutions until test_end. Only send test_end on final attempt (with full retryAttemptLogs), then cleanup.
|
|
360
|
+
if (this.options.executionMode === 'platform') {
|
|
361
|
+
if (isFinalAttempt && this.apiClient) {
|
|
362
|
+
const paths = (0, utils_1.derivePaths)(test, this.testsFolder, this.config.rootDir, false);
|
|
363
|
+
const resolved = this.getJobFromManifest(paths.folderPath, paths.fileName, paths.suitePath, paths.testName);
|
|
364
|
+
if (resolved?.jobId) {
|
|
365
|
+
const jobId = resolved.jobId;
|
|
366
|
+
console.log(`[TestChimp] platform/test_end resolve strategy=${resolved.strategy} jobId=${jobId} fileName="${paths.fileName}" suitePath=${JSON.stringify(paths.suitePath)} testName="${paths.testName}"`);
|
|
367
|
+
if (this.options.captureScreenshots) {
|
|
368
|
+
await this.attachScreenshotsToFailingSteps(execution.steps, result.attachments);
|
|
369
|
+
}
|
|
370
|
+
const jobDetail = this.buildFinalJobDetailForPlatform(test, result, paths.testName, execution.steps);
|
|
371
|
+
const traceGcsPath = await this.uploadTraceAttachmentIfPresent(result.attachments);
|
|
372
|
+
if (traceGcsPath) {
|
|
373
|
+
jobDetail.traceGcsPath = traceGcsPath;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
await this.apiClient.platformTestEnd(jobId, jobDetail);
|
|
377
|
+
console.log(`[TestChimp] platform/test_end sent: ${test.title} jobId=${jobId} retryAttemptLogs=${jobDetail.retryAttemptLogs?.length ?? 0}`);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
console.error(`[TestChimp] platform/test_end failed for ${test.title}:`, error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
console.warn(`[TestChimp] platform/test_end skipped: no jobId in manifest for folderPath="${paths.folderPath}" normalizedFolderPath="${(0, utils_1.normalizeManifestFolderPath)(paths.folderPath)}" fileName="${paths.fileName}" suitePath=${JSON.stringify(paths.suitePath)} testName="${paths.testName}" candidates=${this.getManifestDebugCandidates(paths.fileName, paths.testName)}`);
|
|
385
|
+
}
|
|
386
|
+
// Cleanup all attempts for this test (we have attempts 0..result.retry)
|
|
387
|
+
for (let r = 0; r <= result.retry; r++) {
|
|
388
|
+
this.testExecutions.delete(this.getTestKey(test, r));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// CI mode: skip non-final attempts if configured
|
|
394
|
+
if (this.options.reportOnlyFinalAttempt && !isFinalAttempt) {
|
|
395
|
+
console.log(`[TestChimp] Skipping non-final attempt ${result.retry + 1} for: ${test.title}`);
|
|
396
|
+
this.testExecutions.delete(testKey);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Attach screenshots (CI mode) before building the report
|
|
400
|
+
if (this.options.captureScreenshots) {
|
|
401
|
+
await this.attachScreenshotsToFailingSteps(execution.steps, result.attachments);
|
|
402
|
+
}
|
|
403
|
+
// Build the report
|
|
404
|
+
const report = this.buildReport(test, result, execution);
|
|
405
|
+
const traceGcsPath = await this.uploadTraceAttachmentIfPresent(result.attachments);
|
|
406
|
+
if (traceGcsPath) {
|
|
407
|
+
report.jobDetail.traceGcsPath = traceGcsPath;
|
|
408
|
+
}
|
|
409
|
+
// Log report details
|
|
410
|
+
console.log(`[TestChimp] Preparing to send report for test: ${test.title}`);
|
|
411
|
+
console.log(`[TestChimp] Status: ${report.jobDetail.status}`);
|
|
412
|
+
console.log(`[TestChimp] Steps: ${report.jobDetail.steps.length}`);
|
|
413
|
+
const stepsWithScreenshots = report.jobDetail.steps.filter(s => s.screenshotBase64);
|
|
414
|
+
if (stepsWithScreenshots.length > 0) {
|
|
415
|
+
console.log(`[TestChimp] Steps with screenshots: ${stepsWithScreenshots.length}`);
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const response = await this.apiClient.ingestExecutionReport(report);
|
|
419
|
+
if (this.options.verbose) {
|
|
420
|
+
console.log(`[TestChimp] Reported: ${test.title} (jobId: ${response.jobId}, testFound: ${response.testFound})`);
|
|
421
|
+
if (response.scenariosPopulated && response.scenariosPopulated > 0) {
|
|
422
|
+
console.log(`[TestChimp] Auto-populated ${response.scenariosPopulated} scenario(s)`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
console.error(`[TestChimp] Failed to report test: ${test.title}`, error);
|
|
428
|
+
}
|
|
429
|
+
// Cleanup
|
|
430
|
+
this.testExecutions.delete(testKey);
|
|
431
|
+
}
|
|
432
|
+
async onEnd(result) {
|
|
433
|
+
if (this.pendingOperations.length > 0) {
|
|
434
|
+
console.log(`[TestChimp] Waiting for ${this.pendingOperations.length} pending operations to complete...`);
|
|
435
|
+
await Promise.allSettled(this.pendingOperations);
|
|
436
|
+
}
|
|
437
|
+
if (this.options.verbose) {
|
|
438
|
+
console.log(`[TestChimp] Test run completed. Status: ${result.status}`);
|
|
439
|
+
console.log(`[TestChimp] Batch invocation ID: ${this.batchInvocationId}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Private helpers
|
|
444
|
+
// ============================================================================
|
|
445
|
+
getTestKey(test, retry) {
|
|
446
|
+
return `${test.id}_attempt_${retry}`;
|
|
447
|
+
}
|
|
448
|
+
scanTestRetries(suite) {
|
|
449
|
+
const scanSuite = (s) => {
|
|
450
|
+
for (const test of s.tests) {
|
|
451
|
+
this.testRetryInfo.set(test.id, {
|
|
452
|
+
maxRetries: test.retries,
|
|
453
|
+
currentAttempt: 0
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
for (const child of s.suites) {
|
|
457
|
+
scanSuite(child);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
scanSuite(suite);
|
|
461
|
+
if (this.options.verbose) {
|
|
462
|
+
console.log(`[TestChimp] Scanned ${this.testRetryInfo.size} test(s)`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
buildReport(test, result, execution) {
|
|
466
|
+
// Derive paths from test location
|
|
467
|
+
const paths = (0, utils_1.derivePaths)(test, this.testsFolder, this.config.rootDir, this.options.verbose);
|
|
468
|
+
const branchName = (0, utils_1.getEnvVar)('TESTCHIMP_BRANCH', '') ||
|
|
469
|
+
(0, utils_1.getEnvVar)('GITHUB_HEAD_REF', '') ||
|
|
470
|
+
(0, utils_1.getEnvVar)('GITHUB_REF_NAME', '') ||
|
|
471
|
+
(0, utils_1.getEnvVar)('CI_COMMIT_REF_NAME', '') ||
|
|
472
|
+
undefined;
|
|
473
|
+
// Platform run: scriptservice sets TESTCHIMP_BRANCH_ID (our entity id) for unique test resolution; CI does not have it
|
|
474
|
+
const branchIdRaw = (0, utils_1.getEnvVar)('TESTCHIMP_BRANCH_ID', '');
|
|
475
|
+
const branchId = branchIdRaw ? parseInt(branchIdRaw, 10) : undefined;
|
|
476
|
+
const branchIdValid = branchId !== undefined && !Number.isNaN(branchId) ? branchId : undefined;
|
|
477
|
+
// Map Playwright status to SmartTestExecutionStatus
|
|
478
|
+
const status = this.mapStatus(result.status);
|
|
479
|
+
return {
|
|
480
|
+
folderPath: paths.folderPath,
|
|
481
|
+
fileName: paths.fileName,
|
|
482
|
+
suitePath: paths.suitePath,
|
|
483
|
+
testName: paths.testName,
|
|
484
|
+
release: this.options.release || undefined,
|
|
485
|
+
environment: this.options.environment || undefined,
|
|
486
|
+
batchInvocationId: this.batchInvocationId,
|
|
487
|
+
jobDetail: {
|
|
488
|
+
testName: paths.testName,
|
|
489
|
+
steps: execution.steps,
|
|
490
|
+
status,
|
|
491
|
+
error: result.error?.message,
|
|
492
|
+
pwError: this.toPlaywrightError(result.error),
|
|
493
|
+
scenarioCoverageResults: [] // Backend will populate if empty
|
|
494
|
+
},
|
|
495
|
+
startedAtMillis: execution.startedAt,
|
|
496
|
+
completedAtMillis: Date.now(),
|
|
497
|
+
branchName,
|
|
498
|
+
branchId: branchIdValid
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
mapStatus(playwrightStatus) {
|
|
502
|
+
switch (playwrightStatus) {
|
|
503
|
+
case 'passed':
|
|
504
|
+
return types_1.SmartTestExecutionStatus.SMART_TEST_EXECUTION_COMPLETED;
|
|
505
|
+
case 'failed':
|
|
506
|
+
case 'timedOut':
|
|
507
|
+
return types_1.SmartTestExecutionStatus.SMART_TEST_EXECUTION_FAILED;
|
|
508
|
+
case 'skipped':
|
|
509
|
+
case 'interrupted':
|
|
510
|
+
default:
|
|
511
|
+
return types_1.SmartTestExecutionStatus.UNKNOWN_SMART_TEST_EXECUTION_STATUS;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Attach failure screenshots: failing steps first; if none, attach to last step (test-level failures e.g. ai.act).
|
|
516
|
+
*/
|
|
517
|
+
async attachScreenshotsToFailingSteps(steps, attachments) {
|
|
518
|
+
// Log all attachments for debugging
|
|
519
|
+
console.log(`[TestChimp] Processing screenshots: ${attachments.length} total attachment(s), ${steps.length} step(s) total`);
|
|
520
|
+
if (attachments.length > 0) {
|
|
521
|
+
attachments.forEach((att, idx) => {
|
|
522
|
+
console.log(`[TestChimp] Attachment ${idx + 1}: name="${att.name}", contentType="${att.contentType}", path="${att.path || 'none'}", body=${att.body ? `present (${att.body.length} bytes)` : 'none'}`);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
// Filter for image attachments (with either path or body)
|
|
526
|
+
const screenshots = attachments.filter((a) => a.contentType?.startsWith('image/') && (a.path || a.body));
|
|
527
|
+
console.log(`[TestChimp] Found ${screenshots.length} screenshot(s) (with path or body)`);
|
|
528
|
+
if (screenshots.length === 0) {
|
|
529
|
+
console.log(`[TestChimp] No screenshots found in attachments - Playwright may not be configured to capture screenshots on failure`);
|
|
530
|
+
console.log(`[TestChimp] To enable screenshots, add 'screenshot: "only-on-failure"' to your Playwright config`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const failingWithoutScreenshot = steps.filter((s) => s.status === types_1.StepExecutionStatus.FAILURE_STEP_EXECUTION && !s.screenshotPath);
|
|
534
|
+
let targetSteps;
|
|
535
|
+
if (failingWithoutScreenshot.length > 0) {
|
|
536
|
+
targetSteps = failingWithoutScreenshot;
|
|
537
|
+
console.log(`[TestChimp] Found ${targetSteps.length} failing step(s) without screenshots; stepIds=${targetSteps
|
|
538
|
+
.map((s) => s.stepId || 'unknown')
|
|
539
|
+
.join(', ')}`);
|
|
540
|
+
}
|
|
541
|
+
else if (steps.length > 0) {
|
|
542
|
+
const last = steps[steps.length - 1];
|
|
543
|
+
if (last && !last.screenshotPath) {
|
|
544
|
+
targetSteps = [last];
|
|
545
|
+
console.log(`[TestChimp] No failing steps without screenshot; attaching failure screenshot to last step (fallback): "${last.description}" (${last.stepId})`);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
console.log(`[TestChimp] No failing steps to attach screenshots to${last?.screenshotPath ? ' (last step already has screenshot)' : ''}`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
console.log(`[TestChimp] No failing steps to attach screenshots to (no steps recorded)`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Use the last screenshot (most recent, likely from test failure)
|
|
557
|
+
const screenshot = screenshots[screenshots.length - 1];
|
|
558
|
+
let imageBuffer = null;
|
|
559
|
+
try {
|
|
560
|
+
if (screenshot.path) {
|
|
561
|
+
imageBuffer = fs_1.default.readFileSync(screenshot.path);
|
|
562
|
+
}
|
|
563
|
+
else if (screenshot.body) {
|
|
564
|
+
imageBuffer = Buffer.from(screenshot.body);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
console.error(`[TestChimp] ✗ Failed to read screenshot:`, error);
|
|
569
|
+
imageBuffer = null;
|
|
570
|
+
}
|
|
571
|
+
if (!imageBuffer) {
|
|
572
|
+
console.warn('[TestChimp] Screenshot has neither readable path nor body; skipping upload');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
console.log(`[TestChimp] Read screenshot buffer: ${imageBuffer.length} bytes`);
|
|
576
|
+
if (!this.apiClient) {
|
|
577
|
+
console.warn('[TestChimp] API client not initialized; cannot upload attachment');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
// Convert to JPEG (quality 50) to reduce size
|
|
582
|
+
const jpegBuffer = await (0, sharp_1.default)(imageBuffer)
|
|
583
|
+
.jpeg({ quality: 50 })
|
|
584
|
+
.toBuffer();
|
|
585
|
+
console.log(`[TestChimp] Converted to JPEG: ${jpegBuffer.length} bytes, calling uploadAttachment`);
|
|
586
|
+
const uploadResp = await this.apiClient.uploadAttachment(jpegBuffer, 'image/jpeg', {
|
|
587
|
+
filename: 'screenshot.jpeg'
|
|
588
|
+
});
|
|
589
|
+
const gcpPath = uploadResp.gcpPath;
|
|
590
|
+
if (!gcpPath) {
|
|
591
|
+
console.error('[TestChimp] uploadAttachment response missing gcpPath; cannot attach to steps');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
console.log(`[TestChimp] uploadAttachment succeeded: ${gcpPath}`);
|
|
595
|
+
for (let stepIdx = 0; stepIdx < targetSteps.length; stepIdx++) {
|
|
596
|
+
const step = targetSteps[stepIdx];
|
|
597
|
+
step.screenshotPath = gcpPath;
|
|
598
|
+
if (step.screenshotBase64) {
|
|
599
|
+
delete step.screenshotBase64;
|
|
600
|
+
}
|
|
601
|
+
console.log(`[TestChimp] ✓ Attached screenshot path to step ${stepIdx + 1}: "${step.description}" -> ${gcpPath}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.error('[TestChimp] ✗ Failed to upload screenshot attachment:', error);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
getTraceMaxBytes() {
|
|
609
|
+
const raw = (0, utils_1.getEnvVar)('TESTCHIMP_TRACE_MAX_BYTES', '') || '';
|
|
610
|
+
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
611
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
612
|
+
return parsed;
|
|
613
|
+
}
|
|
614
|
+
return TestChimpReporter.DEFAULT_TRACE_MAX_BYTES;
|
|
615
|
+
}
|
|
616
|
+
isTraceAttachment(attachment) {
|
|
617
|
+
const name = (attachment.name || '').toLowerCase();
|
|
618
|
+
const contentType = (attachment.contentType || '').toLowerCase();
|
|
619
|
+
const filePath = (attachment.path || '').toLowerCase();
|
|
620
|
+
return (contentType.includes('zip') ||
|
|
621
|
+
name.includes('trace') ||
|
|
622
|
+
filePath.endsWith('.zip'));
|
|
623
|
+
}
|
|
624
|
+
async uploadTraceAttachmentIfPresent(attachments) {
|
|
625
|
+
if (!this.apiClient) {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
const traceAttachment = [...attachments].reverse().find((a) => this.isTraceAttachment(a));
|
|
629
|
+
if (!traceAttachment) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
let traceBuffer = null;
|
|
633
|
+
try {
|
|
634
|
+
if (traceAttachment.path) {
|
|
635
|
+
traceBuffer = fs_1.default.readFileSync(traceAttachment.path);
|
|
636
|
+
}
|
|
637
|
+
else if (traceAttachment.body) {
|
|
638
|
+
traceBuffer = Buffer.from(traceAttachment.body);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
console.error('[TestChimp] Failed to read trace attachment:', error);
|
|
643
|
+
return undefined;
|
|
644
|
+
}
|
|
645
|
+
if (!traceBuffer || traceBuffer.length === 0) {
|
|
646
|
+
console.warn('[TestChimp] Trace attachment is empty; skipping upload');
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
const maxBytes = this.getTraceMaxBytes();
|
|
650
|
+
if (traceBuffer.length > maxBytes) {
|
|
651
|
+
console.warn(`[TestChimp] Trace attachment too large (${traceBuffer.length} bytes > ${maxBytes} bytes); skipping upload`);
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
const contentType = traceAttachment.contentType || 'application/zip';
|
|
655
|
+
const filename = traceAttachment.path
|
|
656
|
+
? path_1.default.basename(traceAttachment.path)
|
|
657
|
+
: `${(traceAttachment.name || 'trace').replace(/[^a-zA-Z0-9._-]/g, '_')}.zip`;
|
|
658
|
+
try {
|
|
659
|
+
const uploadResp = await this.apiClient.uploadAttachment(traceBuffer, contentType, {
|
|
660
|
+
filename,
|
|
661
|
+
timeoutMs: TestChimpReporter.DEFAULT_TRACE_UPLOAD_TIMEOUT_MS,
|
|
662
|
+
maxRetries: TestChimpReporter.DEFAULT_TRACE_UPLOAD_RETRIES
|
|
663
|
+
});
|
|
664
|
+
if (this.options.verbose) {
|
|
665
|
+
console.log(`[TestChimp] Trace uploaded (${traceBuffer.length} bytes): ${uploadResp.gcpPath}`);
|
|
666
|
+
}
|
|
667
|
+
return uploadResp.gcpPath;
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
// Non-blocking by design: test execution should still be reported.
|
|
671
|
+
console.error('[TestChimp] Trace upload failed (continuing without trace):', error);
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
isGenericPwApiTitle(title) {
|
|
676
|
+
return TestChimpReporter.GENERIC_PW_API_TITLES.has(title.trim().toLowerCase());
|
|
677
|
+
}
|
|
678
|
+
/** Closest ancestor test.step (user/framework intent), e.g. ai.act wrapper. */
|
|
679
|
+
getInnermostEnclosingTestStepTitle(step) {
|
|
680
|
+
let p = step.parent;
|
|
681
|
+
while (p) {
|
|
682
|
+
if (p.category === 'test.step') {
|
|
683
|
+
return p.title;
|
|
684
|
+
}
|
|
685
|
+
p = p.parent;
|
|
686
|
+
}
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
/** Single-line description: test.step / expect use title; generic pw:api uses enclosing test.step title when present. */
|
|
690
|
+
getStepDescription(step) {
|
|
691
|
+
if (step.category === 'test.step' || step.category === 'expect') {
|
|
692
|
+
return step.title;
|
|
693
|
+
}
|
|
694
|
+
if (step.category === 'pw:api' && this.isGenericPwApiTitle(step.title)) {
|
|
695
|
+
const enclosing = this.getInnermostEnclosingTestStepTitle(step);
|
|
696
|
+
if (enclosing) {
|
|
697
|
+
return enclosing;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return step.title;
|
|
701
|
+
}
|
|
702
|
+
toPlaywrightError(error, depth = 0, maxDepth = 3) {
|
|
703
|
+
if (!error || depth >= maxDepth) {
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
const mapped = {
|
|
707
|
+
message: error.message,
|
|
708
|
+
stack: error.stack,
|
|
709
|
+
snippet: error.snippet,
|
|
710
|
+
value: error.value,
|
|
711
|
+
};
|
|
712
|
+
if (error.location) {
|
|
713
|
+
mapped.location = {
|
|
714
|
+
file: error.location.file,
|
|
715
|
+
line: error.location.line,
|
|
716
|
+
column: error.location.column,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (error.cause) {
|
|
720
|
+
mapped.cause = this.toPlaywrightError(error.cause, depth + 1, maxDepth);
|
|
721
|
+
}
|
|
722
|
+
return mapped;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
exports.TestChimpReporter = TestChimpReporter;
|
|
726
|
+
TestChimpReporter.DEFAULT_TRACE_MAX_BYTES = 25 * 1024 * 1024;
|
|
727
|
+
TestChimpReporter.DEFAULT_TRACE_UPLOAD_TIMEOUT_MS = 120000;
|
|
728
|
+
TestChimpReporter.DEFAULT_TRACE_UPLOAD_RETRIES = 2;
|
|
729
|
+
/**
|
|
730
|
+
* Generic Playwright pw:api leaf titles; use innermost enclosing test.step title instead.
|
|
731
|
+
*/
|
|
732
|
+
TestChimpReporter.GENERIC_PW_API_TITLES = new Set([
|
|
733
|
+
'evaluate',
|
|
734
|
+
'screenshot',
|
|
735
|
+
'navigate',
|
|
736
|
+
'wait',
|
|
737
|
+
'click',
|
|
738
|
+
'fill',
|
|
739
|
+
'select',
|
|
740
|
+
'check',
|
|
741
|
+
'press',
|
|
742
|
+
'hover',
|
|
743
|
+
'drag',
|
|
744
|
+
'tap',
|
|
745
|
+
'type',
|
|
746
|
+
'reload',
|
|
747
|
+
'go to url',
|
|
748
|
+
'get attribute',
|
|
749
|
+
'inner text',
|
|
750
|
+
'text content',
|
|
751
|
+
'viewport',
|
|
752
|
+
'close context',
|
|
753
|
+
'close page',
|
|
754
|
+
'new page',
|
|
755
|
+
'keyboard',
|
|
756
|
+
'mouse',
|
|
757
|
+
'wait for event',
|
|
758
|
+
'wait for timeout',
|
|
759
|
+
'wait for load state',
|
|
760
|
+
'wait for selector',
|
|
761
|
+
'wait for function',
|
|
762
|
+
'focus',
|
|
763
|
+
'blur',
|
|
764
|
+
'dispatch event',
|
|
765
|
+
'emulate media',
|
|
766
|
+
'add init script',
|
|
767
|
+
'expose binding',
|
|
768
|
+
'route',
|
|
769
|
+
'unroute',
|
|
770
|
+
'set content',
|
|
771
|
+
'set extra http headers',
|
|
772
|
+
'add cookies',
|
|
773
|
+
'clear cookies',
|
|
774
|
+
'grant permissions',
|
|
775
|
+
'clear permissions'
|
|
776
|
+
]);
|
|
777
|
+
//# sourceMappingURL=testchimp-reporter.js.map
|