tdpw
Version:
CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support
325 lines • 13 kB
JavaScript
;
/**
* Test failure extraction for cache functionality
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheExtractor = void 0;
const parser_1 = require("./parser");
const discovery_1 = require("./discovery");
const types_1 = require("../types");
/**
* Service for extracting test failure data for caching
*/
class CacheExtractor {
workingDir;
constructor(workingDir) {
this.workingDir = workingDir;
}
/**
* Extract test failures and metadata for caching
*/
async extractFailureData() {
try {
// Step 1: Discover JSON reports using existing discovery system
const reportPaths = await this.discoverReports();
if (reportPaths.length === 0) {
return this.createEmptyResult();
}
// Step 2: Parse reports and extract failures
const failures = [];
const totalSummary = {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
duration: 0,
};
for (const reportPath of reportPaths) {
try {
const reportData = await (0, parser_1.parsePlaywrightJson)(reportPath);
// Extract failures from this report
const reportFailures = this.extractFailuresFromReport(reportData, reportPath);
failures.push(...reportFailures);
// Aggregate summary data from stats
if (reportData.stats) {
const stats = reportData.stats;
totalSummary.total +=
(stats.expected || 0) +
(stats.unexpected || 0) +
(stats.skipped || 0);
totalSummary.passed += stats.expected || 0;
totalSummary.failed += stats.unexpected || 0;
totalSummary.skipped += stats.skipped || 0;
totalSummary.duration += Math.round(stats.duration || 0);
}
}
catch (error) {
// Log but continue with other reports
console.warn(`⚠️ Failed to parse report ${reportPath}:`, error instanceof Error ? error.message : error);
continue;
}
}
return {
failures,
summary: totalSummary,
reportPaths,
hasData: failures.length > 0 || totalSummary.total > 0,
};
}
catch (error) {
throw new types_1.FileSystemError(`Failed to extract test failure data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Discover JSON reports using the existing discovery service
*/
async discoverReports() {
try {
// Use the existing ReportDiscoveryService for comprehensive scanning
const discoveryService = new discovery_1.ReportDiscoveryService(this.workingDir);
// Create minimal options for discovery (only need JSON report)
const options = {
reportDirectory: this.workingDir,
token: '',
uploadImages: false,
uploadVideos: false,
uploadHtml: false,
uploadTraces: false,
uploadFiles: false,
uploadFullJson: false,
verbose: false,
environment: 'unknown',
};
const discoveryResult = await discoveryService.discover(options);
// Return the discovered JSON report path
if (discoveryResult.jsonReport) {
return [discoveryResult.jsonReport];
}
return [];
}
catch (error) {
// If discovery fails, return empty array (no reports found)
if (error instanceof types_1.FileSystemError) {
console.debug('No JSON reports found:', error.message);
}
return [];
}
}
/**
* Extract failures from a parsed report
*/
extractFailuresFromReport(reportData, reportPath) {
const failures = [];
try {
// Handle Playwright report structure - suites contain the test information
const suites = reportData.suites || [];
if (Array.isArray(suites)) {
for (const suite of suites) {
this.extractFailuresFromSuite(suite, failures, '', undefined);
}
}
return failures;
}
catch (error) {
console.warn(`⚠️ Failed to extract failures from ${reportPath}:`, error instanceof Error ? error.message : error);
return [];
}
}
/**
* Recursively extract failures from suite structure
*/
extractFailuresFromSuite(suite, failures, parentPath, parentFile) {
try {
// Extract file from suite if available
const suiteFile = suite.file || parentFile;
// For building test path, only include non-file titles in the path
const shouldIncludeInPath = suite.title && (!suite.file || suite.title !== suite.file);
const suitePath = shouldIncludeInPath
? parentPath
? `${parentPath} > ${suite.title}`
: suite.title
: parentPath || '';
// Handle nested suites (recursive)
if (suite.suites && Array.isArray(suite.suites)) {
for (const nestedSuite of suite.suites) {
this.extractFailuresFromSuite(nestedSuite, failures, suitePath, suiteFile);
}
}
// Handle specs in this suite (Playwright report structure: suites -> specs -> tests)
if (suite.specs && Array.isArray(suite.specs)) {
for (const spec of suite.specs) {
this.extractFailuresFromSpec(spec, failures, suitePath, suiteFile);
}
}
// Handle individual tests in this suite (fallback for different report structures)
if (suite.tests && Array.isArray(suite.tests)) {
for (const test of suite.tests) {
this.extractFailuresFromTest(test, failures, suitePath, suiteFile);
}
}
}
catch (error) {
// Continue processing other suites on error
console.warn(`⚠️ Error processing suite:`, error instanceof Error ? error.message : error);
}
}
/**
* Extract failures from spec structure (intermediate level between suite and test)
*/
extractFailuresFromSpec(spec, failures, parentPath, suiteFile) {
try {
// Use spec title as the test title directly (e.g., "get started link")
const specTitle = spec.title || '';
// Check if spec is marked as failed (spec.ok === false means test failed)
// This is the reliable way to check for failures in Playwright reports
const specFailed = spec.ok === false;
// Handle individual tests in this spec
if (spec.tests && Array.isArray(spec.tests) && specFailed) {
// Only process the first test in the spec (all tests in a spec are the same test with retries)
const test = spec.tests[0];
if (test) {
const failure = this.createTestFailure(test, parentPath, suiteFile, specTitle);
if (failure) {
// Check for duplicates
const isDuplicate = failures.some(f => f.file === failure.file && f.testTitle === failure.testTitle);
if (!isDuplicate) {
failures.push(failure);
}
}
}
}
}
catch (error) {
// Continue processing other specs on error
console.warn(`⚠️ Error processing spec:`, error instanceof Error ? error.message : error);
}
}
/**
* Extract failures from individual test
*/
extractFailuresFromTest(test, failures, parentPath, suiteFile, specTitle) {
try {
// Handle direct test results - only count the test once, not each retry
if (this.isFailedTest(test)) {
const failure = this.createTestFailure(test, parentPath, suiteFile, specTitle);
if (failure) {
// Check if this failure already exists (avoid duplicates from retries)
const isDuplicate = failures.some(f => f.file === failure.file && f.testTitle === failure.testTitle);
if (!isDuplicate) {
failures.push(failure);
}
}
}
}
catch (error) {
// Continue processing other tests on error
console.warn(`⚠️ Error processing test:`, error instanceof Error ? error.message : error);
}
}
/**
* Check if a test item represents a failed test
*/
isFailedTest(item) {
return Boolean(item &&
(item.status === 'failed' ||
item.outcome === 'failed' ||
item.state === 'failed' ||
item.results?.some(r => r.status === 'failed')));
}
/**
* Create TestFailure object from test data
*/
createTestFailure(test, parentPath, suiteFile, specTitle) {
try {
// Extract file path - prioritize test file, then suite file
let filePath = test.file || test.location?.file || suiteFile || '';
// Clean up file path (remove absolute path prefixes)
if (filePath) {
// Convert to relative path if possible
const cwd = process.cwd();
if (filePath.startsWith(cwd)) {
filePath = filePath.substring(cwd.length + 1);
}
// Also remove 'tests/' prefix if present to match server expectations
if (filePath.startsWith('tests/')) {
filePath = filePath.substring(6);
}
}
// Extract test title - use spec title only (without suite path)
// This ensures Playwright's -g flag can match exact test names
const testTitle = specTitle || test.title || test.name || 'Unknown test';
// Extract error information
let error;
if (test.results && Array.isArray(test.results)) {
const failedResult = test.results.find(r => r.status === 'failed');
if (failedResult?.error) {
error = this.formatError(failedResult.error);
}
}
else if (test.error) {
error = this.formatError(test.error);
}
// Extract duration
let duration;
if (test.results && Array.isArray(test.results)) {
duration = test.results.reduce((total, r) => total + (r.duration || 0), 0);
}
else if (typeof test.duration === 'number') {
duration = test.duration;
}
// Only create failure if we have meaningful data
if (!filePath && !testTitle) {
return null;
}
return {
file: filePath || 'unknown',
testTitle: testTitle,
error,
duration,
};
}
catch (error) {
console.warn(`⚠️ Failed to create test failure object:`, error instanceof Error ? error.message : error);
return null;
}
}
/**
* Format error message for storage
*/
formatError(error) {
try {
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object') {
const errorObj = error;
return (errorObj.message ||
errorObj.name ||
JSON.stringify(error));
}
return String(error);
}
catch {
return 'Error formatting failed';
}
}
/**
* Create empty result for cases with no data
*/
createEmptyResult() {
return {
failures: [],
summary: {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
duration: 0,
},
reportPaths: [],
hasData: false,
};
}
}
exports.CacheExtractor = CacheExtractor;
//# sourceMappingURL=cache-extractor.js.map