UNPKG

tdpw

Version:

CLI tool for uploading Playwright test reports to TestDino platform with TestDino storage support

325 lines 13 kB
"use strict"; /** * 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