Testing API
The Testing API provides comprehensive testing capabilities for running, managing, and reporting test results within the development environment.
Overview
The Testing API enables you to:
- Discover and run tests from various testing frameworks
- Create custom test providers and runners
- Display test results and coverage information
- Integrate with continuous integration systems
- Provide test debugging capabilities
- Generate test reports and analytics
- Manage test configurations and environments
Basic Usage
Test Discovery and Running
typescript
import { TraeAPI } from '@trae/api';
// Basic test runner implementation
class TestRunner {
private testController: TraeAPI.TestController;
private testData: Map<string, TraeAPI.TestItem> = new Map();
constructor() {
this.testController = TraeAPI.tests.createTestController(
'myTestRunner',
'My Test Runner'
);
this.setupTestController();
}
private setupTestController() {
// Handle test run requests
this.testController.createRunProfile(
'Run Tests',
TraeAPI.TestRunProfileKind.Run,
(request, token) => this.runTests(request, token)
);
// Handle debug run requests
this.testController.createRunProfile(
'Debug Tests',
TraeAPI.TestRunProfileKind.Debug,
(request, token) => this.debugTests(request, token)
);
// Handle coverage run requests
this.testController.createRunProfile(
'Run with Coverage',
TraeAPI.TestRunProfileKind.Coverage,
(request, token) => this.runTestsWithCoverage(request, token)
);
// Refresh tests when files change
this.testController.refreshHandler = () => this.discoverTests();
// Resolve test items
this.testController.resolveHandler = (item) => this.resolveTestItem(item);
}
// Discover tests in the workspace
async discoverTests(): Promise<void> {
console.log('Discovering tests...');
// Find test files
const testFiles = await TraeAPI.workspace.findFiles(
'**/*.{test,spec}.{js,ts}',
'**/node_modules/**'
);
// Clear existing tests
this.testController.items.replace([]);
this.testData.clear();
// Process each test file
for (const file of testFiles) {
await this.processTestFile(file);
}
console.log(`Discovered ${this.testData.size} tests`);
}
private async processTestFile(uri: TraeAPI.Uri): Promise<void> {
try {
const document = await TraeAPI.workspace.openTextDocument(uri);
const text = document.getText();
// Parse test file to find test cases
const tests = this.parseTestFile(text, uri);
if (tests.length > 0) {
// Create file-level test item
const fileItem = this.testController.createTestItem(
uri.toString(),
this.getRelativePath(uri),
uri
);
// Add individual test cases
for (const test of tests) {
const testItem = this.testController.createTestItem(
`${uri.toString()}::${test.name}`,
test.name,
uri
);
testItem.range = test.range;
testItem.canResolveChildren = false;
fileItem.children.add(testItem);
this.testData.set(testItem.id, testItem);
}
this.testController.items.add(fileItem);
this.testData.set(fileItem.id, fileItem);
}
} catch (error) {
console.error('Error processing test file:', uri.toString(), error);
}
}
private parseTestFile(content: string, uri: TraeAPI.Uri): Array<{ name: string; range: TraeAPI.Range }> {
const tests: Array<{ name: string; range: TraeAPI.Range }> = [];
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Match test patterns (Jest, Mocha, etc.)
const testMatch = line.match(/(?:test|it)\s*\(\s*['"`]([^'"`)]+)['"`]/);
if (testMatch) {
const testName = testMatch[1];
const range = new TraeAPI.Range(
new TraeAPI.Position(i, 0),
new TraeAPI.Position(i, line.length)
);
tests.push({ name: testName, range });
}
// Match describe blocks
const describeMatch = line.match(/describe\s*\(\s*['"`]([^'"`)]+)['"`]/);
if (describeMatch) {
const suiteName = describeMatch[1];
const range = new TraeAPI.Range(
new TraeAPI.Position(i, 0),
new TraeAPI.Position(i, line.length)
);
tests.push({ name: `Suite: ${suiteName}`, range });
}
}
return tests;
}
private getRelativePath(uri: TraeAPI.Uri): string {
const workspaceFolder = TraeAPI.workspace.getWorkspaceFolder(uri);
if (workspaceFolder) {
return TraeAPI.workspace.asRelativePath(uri, false);
}
return uri.fsPath;
}
// Run tests
private async runTests(
request: TraeAPI.TestRunRequest,
token: TraeAPI.CancellationToken
): Promise<void> {
const run = this.testController.createTestRun(request);
try {
const testsToRun = this.getTestsToRun(request);
for (const test of testsToRun) {
if (token.isCancellationRequested) {
break;
}
await this.runSingleTest(test, run);
}
} catch (error) {
console.error('Error running tests:', error);
} finally {
run.end();
}
}
private async runSingleTest(test: TraeAPI.TestItem, run: TraeAPI.TestRun): Promise<void> {
run.started(test);
try {
// Simulate test execution
const result = await this.executeTest(test);
if (result.success) {
run.passed(test, result.duration);
} else {
const message = new TraeAPI.TestMessage(result.error || 'Test failed');
if (result.location) {
message.location = result.location;
}
run.failed(test, message, result.duration);
}
} catch (error) {
const message = new TraeAPI.TestMessage(`Test execution error: ${error.message}`);
run.errored(test, message);
}
}
private async executeTest(test: TraeAPI.TestItem): Promise<{
success: boolean;
duration?: number;
error?: string;
location?: TraeAPI.Location;
}> {
// Mock test execution - replace with actual test runner integration
const startTime = Date.now();
// Simulate test execution time
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
const duration = Date.now() - startTime;
const success = Math.random() > 0.2; // 80% success rate for demo
if (!success) {
return {
success: false,
duration,
error: 'Assertion failed: expected true but got false',
location: test.uri ? new TraeAPI.Location(test.uri, test.range || new TraeAPI.Position(0, 0)) : undefined
};
}
return { success: true, duration };
}
private getTestsToRun(request: TraeAPI.TestRunRequest): TraeAPI.TestItem[] {
const tests: TraeAPI.TestItem[] = [];
if (request.include) {
// Run specific tests
for (const test of request.include) {
tests.push(test);
// Include children if it's a suite
test.children.forEach(child => tests.push(child));
}
} else {
// Run all tests
this.testController.items.forEach(item => {
tests.push(item);
item.children.forEach(child => tests.push(child));
});
}
// Exclude specified tests
if (request.exclude) {
const excludeIds = new Set(request.exclude.map(t => t.id));
return tests.filter(t => !excludeIds.has(t.id));
}
return tests;
}
// Debug tests
private async debugTests(
request: TraeAPI.TestRunRequest,
token: TraeAPI.CancellationToken
): Promise<void> {
const testsToDebug = this.getTestsToRun(request);
if (testsToDebug.length === 0) {
TraeAPI.window.showWarningMessage('No tests selected for debugging');
return;
}
// Create debug configuration for tests
const debugConfig: TraeAPI.DebugConfiguration = {
type: 'node',
request: 'launch',
name: 'Debug Tests',
program: '${workspaceFolder}/node_modules/.bin/jest',
args: [
'--runInBand',
'--testNamePattern',
testsToDebug.map(t => t.label).join('|')
],
console: 'integratedTerminal',
internalConsoleOptions: 'neverOpen'
};
// Start debugging session
const success = await TraeAPI.debug.startDebugging(
TraeAPI.workspace.workspaceFolders?.[0],
debugConfig
);
if (!success) {
TraeAPI.window.showErrorMessage('Failed to start debug session for tests');
}
}
// Run tests with coverage
private async runTestsWithCoverage(
request: TraeAPI.TestRunRequest,
token: TraeAPI.CancellationToken
): Promise<void> {
const run = this.testController.createTestRun(request);
try {
// Run tests and collect coverage
const testsToRun = this.getTestsToRun(request);
const coverageData = new Map<string, TraeAPI.FileCoverage>();
for (const test of testsToRun) {
if (token.isCancellationRequested) {
break;
}
const result = await this.runSingleTestWithCoverage(test, run, coverageData);
}
// Set coverage data
for (const [uri, coverage] of coverageData) {
run.addCoverage(coverage);
}
} catch (error) {
console.error('Error running tests with coverage:', error);
} finally {
run.end();
}
}
private async runSingleTestWithCoverage(
test: TraeAPI.TestItem,
run: TraeAPI.TestRun,
coverageData: Map<string, TraeAPI.FileCoverage>
): Promise<void> {
await this.runSingleTest(test, run);
// Mock coverage data collection
if (test.uri) {
const uri = test.uri.toString();
if (!coverageData.has(uri)) {
const coverage = this.generateMockCoverage(test.uri);
coverageData.set(uri, coverage);
}
}
}
private generateMockCoverage(uri: TraeAPI.Uri): TraeAPI.FileCoverage {
// Generate mock coverage data
const statementCoverage: TraeAPI.StatementCoverage[] = [];
const branchCoverage: TraeAPI.BranchCoverage[] = [];
const functionCoverage: TraeAPI.FunctionCoverage[] = [];
// Mock some coverage data
for (let i = 0; i < 50; i++) {
const range = new TraeAPI.Range(
new TraeAPI.Position(i, 0),
new TraeAPI.Position(i, 20)
);
statementCoverage.push(
new TraeAPI.StatementCoverage(
Math.random() > 0.3 ? 1 : 0, // 70% coverage
new TraeAPI.Location(uri, range)
)
);
}
return TraeAPI.FileCoverage.fromDetails(
uri,
statementCoverage,
branchCoverage,
functionCoverage
);
}
private async resolveTestItem(item: TraeAPI.TestItem | undefined): Promise<void> {
if (!item) {
// Resolve root items
await this.discoverTests();
return;
}
// Resolve children for specific item if needed
if (item.canResolveChildren) {
// Implementation for resolving children
}
}
dispose(): void {
this.testController.dispose();
}
}
// Initialize test runner
const testRunner = new TestRunner();Test Result Management
typescript
class TestResultManager {
private testResults: Map<string, TestResult> = new Map();
private testHistory: TestResult[][] = [];
private maxHistorySize = 50;
constructor() {
this.setupResultListeners();
}
private setupResultListeners() {
// Listen for test run completion
TraeAPI.tests.onDidChangeTestResults(results => {
this.processTestResults(results);
});
}
private processTestResults(results: readonly TraeAPI.TestResult[]): void {
const currentRun: TestResult[] = [];
for (const result of results) {
const testResult: TestResult = {
id: result.test.id,
name: result.test.label,
state: this.mapTestState(result.state),
duration: result.duration,
message: result.messages?.[0]?.message,
location: result.messages?.[0]?.location,
timestamp: Date.now()
};
this.testResults.set(result.test.id, testResult);
currentRun.push(testResult);
}
// Add to history
this.testHistory.unshift(currentRun);
if (this.testHistory.length > this.maxHistorySize) {
this.testHistory.pop();
}
// Generate summary
this.generateTestSummary(currentRun);
}
private mapTestState(state: TraeAPI.TestResultState): 'passed' | 'failed' | 'skipped' | 'errored' {
switch (state) {
case TraeAPI.TestResultState.Passed:
return 'passed';
case TraeAPI.TestResultState.Failed:
return 'failed';
case TraeAPI.TestResultState.Skipped:
return 'skipped';
case TraeAPI.TestResultState.Errored:
return 'errored';
default:
return 'failed';
}
}
private generateTestSummary(results: TestResult[]): void {
const summary = {
total: results.length,
passed: results.filter(r => r.state === 'passed').length,
failed: results.filter(r => r.state === 'failed').length,
skipped: results.filter(r => r.state === 'skipped').length,
errored: results.filter(r => r.state === 'errored').length,
duration: results.reduce((sum, r) => sum + (r.duration || 0), 0)
};
console.log('Test Summary:', summary);
// Show notification for failed tests
if (summary.failed > 0 || summary.errored > 0) {
TraeAPI.window.showWarningMessage(
`Tests completed: ${summary.passed} passed, ${summary.failed + summary.errored} failed`
);
} else {
TraeAPI.window.showInformationMessage(
`All ${summary.total} tests passed! ✅`
);
}
}
// Get test results
getTestResult(testId: string): TestResult | undefined {
return this.testResults.get(testId);
}
getAllTestResults(): TestResult[] {
return Array.from(this.testResults.values());
}
getTestHistory(): TestResult[][] {
return [...this.testHistory];
}
// Get test statistics
getTestStatistics(): TestStatistics {
const allResults = this.getAllTestResults();
return {
totalTests: allResults.length,
passedTests: allResults.filter(r => r.state === 'passed').length,
failedTests: allResults.filter(r => r.state === 'failed').length,
skippedTests: allResults.filter(r => r.state === 'skipped').length,
erroredTests: allResults.filter(r => r.state === 'errored').length,
averageDuration: allResults.reduce((sum, r) => sum + (r.duration || 0), 0) / allResults.length,
successRate: (allResults.filter(r => r.state === 'passed').length / allResults.length) * 100
};
}
// Export test results
exportResults(format: 'json' | 'xml' | 'html' = 'json'): string {
const results = this.getAllTestResults();
switch (format) {
case 'json':
return JSON.stringify(results, null, 2);
case 'xml':
return this.generateXMLReport(results);
case 'html':
return this.generateHTMLReport(results);
default:
return JSON.stringify(results, null, 2);
}
}
private generateXMLReport(results: TestResult[]): string {
const statistics = this.getTestStatistics();
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += `<testsuites tests="${statistics.totalTests}" failures="${statistics.failedTests}" errors="${statistics.erroredTests}" time="${statistics.averageDuration / 1000}">\n`;
const groupedResults = this.groupResultsByFile(results);
for (const [file, fileResults] of groupedResults) {
xml += ` <testsuite name="${file}" tests="${fileResults.length}">\n`;
for (const result of fileResults) {
xml += ` <testcase name="${result.name}" time="${(result.duration || 0) / 1000}">\n`;
if (result.state === 'failed' || result.state === 'errored') {
xml += ` <failure message="${this.escapeXML(result.message || 'Test failed')}"></failure>\n`;
} else if (result.state === 'skipped') {
xml += ` <skipped></skipped>\n`;
}
xml += ` </testcase>\n`;
}
xml += ` </testsuite>\n`;
}
xml += '</testsuites>';
return xml;
}
private generateHTMLReport(results: TestResult[]): string {
const statistics = this.getTestStatistics();
let html = `
<!DOCTYPE html>
<html>
<head>
<title>Test Results</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
.test-item { margin: 10px 0; padding: 10px; border-left: 4px solid #ccc; }
.passed { border-left-color: #4CAF50; background: #f1f8e9; }
.failed { border-left-color: #f44336; background: #ffebee; }
.skipped { border-left-color: #ff9800; background: #fff3e0; }
.errored { border-left-color: #9c27b0; background: #f3e5f5; }
.test-name { font-weight: bold; }
.test-duration { color: #666; font-size: 0.9em; }
.test-message { color: #d32f2f; margin-top: 5px; font-family: monospace; }
</style>
</head>
<body>
<h1>Test Results Report</h1>
<div class="summary">
<h2>Summary</h2>
<p>Total Tests: ${statistics.totalTests}</p>
<p>Passed: ${statistics.passedTests}</p>
<p>Failed: ${statistics.failedTests}</p>
<p>Skipped: ${statistics.skippedTests}</p>
<p>Errored: ${statistics.erroredTests}</p>
<p>Success Rate: ${statistics.successRate.toFixed(2)}%</p>
<p>Average Duration: ${statistics.averageDuration.toFixed(2)}ms</p>
</div>
<h2>Test Details</h2>
`;
for (const result of results) {
html += `
<div class="test-item ${result.state}">
<div class="test-name">${result.name}</div>
<div class="test-duration">Duration: ${result.duration || 0}ms</div>
${result.message ? `<div class="test-message">${this.escapeHTML(result.message)}</div>` : ''}
</div>
`;
}
html += `
</body>
</html>
`;
return html;
}
private groupResultsByFile(results: TestResult[]): Map<string, TestResult[]> {
const grouped = new Map<string, TestResult[]>();
for (const result of results) {
const file = result.location?.uri.fsPath || 'Unknown';
const existing = grouped.get(file) || [];
existing.push(result);
grouped.set(file, existing);
}
return grouped;
}
private escapeXML(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
private escapeHTML(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
}
interface TestResult {
id: string;
name: string;
state: 'passed' | 'failed' | 'skipped' | 'errored';
duration?: number;
message?: string;
location?: TraeAPI.Location;
timestamp: number;
}
interface TestStatistics {
totalTests: number;
passedTests: number;
failedTests: number;
skippedTests: number;
erroredTests: number;
averageDuration: number;
successRate: number;
}
// Initialize test result manager
const testResultManager = new TestResultManager();Test Coverage Analysis
typescript
class TestCoverageAnalyzer {
private coverageData: Map<string, FileCoverageData> = new Map();
private coverageThresholds = {
statements: 80,
branches: 75,
functions: 85,
lines: 80
};
constructor() {
this.setupCoverageListeners();
}
private setupCoverageListeners() {
// Listen for coverage data updates
TraeAPI.tests.onDidChangeTestResults(results => {
this.processCoverageData(results);
});
}
private processCoverageData(results: readonly TraeAPI.TestResult[]): void {
// Extract coverage data from test results
for (const result of results) {
if (result.coverage) {
for (const fileCoverage of result.coverage) {
this.analyzFileCoverage(fileCoverage);
}
}
}
this.generateCoverageReport();
}
private analyzFileCoverage(fileCoverage: TraeAPI.FileCoverage): void {
const uri = fileCoverage.uri.toString();
const analysis: FileCoverageData = {
uri: fileCoverage.uri,
statements: this.analyzeStatementCoverage(fileCoverage.statementCoverage),
branches: this.analyzeBranchCoverage(fileCoverage.branchCoverage),
functions: this.analyzeFunctionCoverage(fileCoverage.functionCoverage),
lines: this.analyzeLineCoverage(fileCoverage)
};
this.coverageData.set(uri, analysis);
}
private analyzeStatementCoverage(statements: readonly TraeAPI.StatementCoverage[]): CoverageMetrics {
const total = statements.length;
const covered = statements.filter(s => s.executed > 0).length;
return {
total,
covered,
percentage: total > 0 ? (covered / total) * 100 : 0,
uncoveredRanges: statements
.filter(s => s.executed === 0)
.map(s => s.location.range)
};
}
private analyzeBranchCoverage(branches: readonly TraeAPI.BranchCoverage[]): CoverageMetrics {
const total = branches.length;
const covered = branches.filter(b => b.executed > 0).length;
return {
total,
covered,
percentage: total > 0 ? (covered / total) * 100 : 0,
uncoveredRanges: branches
.filter(b => b.executed === 0)
.map(b => b.location.range)
};
}
private analyzeFunctionCoverage(functions: readonly TraeAPI.FunctionCoverage[]): CoverageMetrics {
const total = functions.length;
const covered = functions.filter(f => f.executed > 0).length;
return {
total,
covered,
percentage: total > 0 ? (covered / total) * 100 : 0,
uncoveredRanges: functions
.filter(f => f.executed === 0)
.map(f => f.location.range)
};
}
private analyzeLineCoverage(fileCoverage: TraeAPI.FileCoverage): CoverageMetrics {
// Calculate line coverage from statement coverage
const lineMap = new Map<number, boolean>();
for (const statement of fileCoverage.statementCoverage) {
const line = statement.location.range.start.line;
if (!lineMap.has(line)) {
lineMap.set(line, statement.executed > 0);
} else {
// If any statement on the line is covered, mark line as covered
lineMap.set(line, lineMap.get(line)! || statement.executed > 0);
}
}
const total = lineMap.size;
const covered = Array.from(lineMap.values()).filter(Boolean).length;
return {
total,
covered,
percentage: total > 0 ? (covered / total) * 100 : 0,
uncoveredRanges: Array.from(lineMap.entries())
.filter(([line, isCovered]) => !isCovered)
.map(([line]) => new TraeAPI.Range(
new TraeAPI.Position(line, 0),
new TraeAPI.Position(line, Number.MAX_SAFE_INTEGER)
))
};
}
private generateCoverageReport(): void {
const overallCoverage = this.calculateOverallCoverage();
console.log('Coverage Report:', {
overall: overallCoverage,
thresholds: this.coverageThresholds,
files: this.coverageData.size
});
// Check thresholds
this.checkCoverageThresholds(overallCoverage);
}
private calculateOverallCoverage(): OverallCoverage {
const files = Array.from(this.coverageData.values());
if (files.length === 0) {
return {
statements: { total: 0, covered: 0, percentage: 0 },
branches: { total: 0, covered: 0, percentage: 0 },
functions: { total: 0, covered: 0, percentage: 0 },
lines: { total: 0, covered: 0, percentage: 0 }
};
}
const totals = files.reduce(
(acc, file) => ({
statements: {
total: acc.statements.total + file.statements.total,
covered: acc.statements.covered + file.statements.covered
},
branches: {
total: acc.branches.total + file.branches.total,
covered: acc.branches.covered + file.branches.covered
},
functions: {
total: acc.functions.total + file.functions.total,
covered: acc.functions.covered + file.functions.covered
},
lines: {
total: acc.lines.total + file.lines.total,
covered: acc.lines.covered + file.lines.covered
}
}),
{
statements: { total: 0, covered: 0 },
branches: { total: 0, covered: 0 },
functions: { total: 0, covered: 0 },
lines: { total: 0, covered: 0 }
}
);
return {
statements: {
...totals.statements,
percentage: totals.statements.total > 0 ? (totals.statements.covered / totals.statements.total) * 100 : 0
},
branches: {
...totals.branches,
percentage: totals.branches.total > 0 ? (totals.branches.covered / totals.branches.total) * 100 : 0
},
functions: {
...totals.functions,
percentage: totals.functions.total > 0 ? (totals.functions.covered / totals.functions.total) * 100 : 0
},
lines: {
...totals.lines,
percentage: totals.lines.total > 0 ? (totals.lines.covered / totals.lines.total) * 100 : 0
}
};
}
private checkCoverageThresholds(coverage: OverallCoverage): void {
const failures: string[] = [];
if (coverage.statements.percentage < this.coverageThresholds.statements) {
failures.push(`Statements: ${coverage.statements.percentage.toFixed(2)}% < ${this.coverageThresholds.statements}%`);
}
if (coverage.branches.percentage < this.coverageThresholds.branches) {
failures.push(`Branches: ${coverage.branches.percentage.toFixed(2)}% < ${this.coverageThresholds.branches}%`);
}
if (coverage.functions.percentage < this.coverageThresholds.functions) {
failures.push(`Functions: ${coverage.functions.percentage.toFixed(2)}% < ${this.coverageThresholds.functions}%`);
}
if (coverage.lines.percentage < this.coverageThresholds.lines) {
failures.push(`Lines: ${coverage.lines.percentage.toFixed(2)}% < ${this.coverageThresholds.lines}%`);
}
if (failures.length > 0) {
TraeAPI.window.showWarningMessage(
`Coverage thresholds not met:\n${failures.join('\n')}`
);
} else {
TraeAPI.window.showInformationMessage('All coverage thresholds met! 🎯');
}
}
// Get coverage data
getCoverageForFile(uri: TraeAPI.Uri): FileCoverageData | undefined {
return this.coverageData.get(uri.toString());
}
getAllCoverageData(): FileCoverageData[] {
return Array.from(this.coverageData.values());
}
getOverallCoverage(): OverallCoverage {
return this.calculateOverallCoverage();
}
// Set coverage thresholds
setCoverageThresholds(thresholds: Partial<typeof this.coverageThresholds>): void {
this.coverageThresholds = { ...this.coverageThresholds, ...thresholds };
}
// Export coverage report
exportCoverageReport(format: 'json' | 'lcov' | 'html' = 'json'): string {
const coverage = this.getOverallCoverage();
const files = this.getAllCoverageData();
switch (format) {
case 'json':
return JSON.stringify({ overall: coverage, files }, null, 2);
case 'lcov':
return this.generateLCOVReport(files);
case 'html':
return this.generateHTMLCoverageReport(coverage, files);
default:
return JSON.stringify({ overall: coverage, files }, null, 2);
}
}
private generateLCOVReport(files: FileCoverageData[]): string {
let lcov = '';
for (const file of files) {
lcov += `SF:${file.uri.fsPath}\n`;
// Function coverage
lcov += `FNF:${file.functions.total}\n`;
lcov += `FNH:${file.functions.covered}\n`;
// Line coverage
lcov += `LF:${file.lines.total}\n`;
lcov += `LH:${file.lines.covered}\n`;
// Branch coverage
lcov += `BRF:${file.branches.total}\n`;
lcov += `BRH:${file.branches.covered}\n`;
lcov += 'end_of_record\n';
}
return lcov;
}
private generateHTMLCoverageReport(overall: OverallCoverage, files: FileCoverageData[]): string {
return `
<!DOCTYPE html>
<html>
<head>
<title>Coverage Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
.metric { display: inline-block; margin: 10px; padding: 10px; border-radius: 5px; min-width: 120px; text-align: center; }
.high { background: #4CAF50; color: white; }
.medium { background: #ff9800; color: white; }
.low { background: #f44336; color: white; }
.file-item { margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
.file-name { font-weight: bold; margin-bottom: 5px; }
.coverage-bar { height: 20px; background: #f0f0f0; border-radius: 10px; overflow: hidden; }
.coverage-fill { height: 100%; background: #4CAF50; transition: width 0.3s; }
</style>
</head>
<body>
<h1>Test Coverage Report</h1>
<div class="summary">
<h2>Overall Coverage</h2>
<div class="metric ${this.getCoverageClass(overall.statements.percentage)}">
<div>Statements</div>
<div>${overall.statements.percentage.toFixed(2)}%</div>
<div>${overall.statements.covered}/${overall.statements.total}</div>
</div>
<div class="metric ${this.getCoverageClass(overall.branches.percentage)}">
<div>Branches</div>
<div>${overall.branches.percentage.toFixed(2)}%</div>
<div>${overall.branches.covered}/${overall.branches.total}</div>
</div>
<div class="metric ${this.getCoverageClass(overall.functions.percentage)}">
<div>Functions</div>
<div>${overall.functions.percentage.toFixed(2)}%</div>
<div>${overall.functions.covered}/${overall.functions.total}</div>
</div>
<div class="metric ${this.getCoverageClass(overall.lines.percentage)}">
<div>Lines</div>
<div>${overall.lines.percentage.toFixed(2)}%</div>
<div>${overall.lines.covered}/${overall.lines.total}</div>
</div>
</div>
<h2>File Coverage</h2>
${files.map(file => `
<div class="file-item">
<div class="file-name">${file.uri.fsPath}</div>
<div>Statements: ${file.statements.percentage.toFixed(2)}%</div>
<div class="coverage-bar">
<div class="coverage-fill" style="width: ${file.statements.percentage}%"></div>
</div>
</div>
`).join('')}
</body>
</html>
`;
}
private getCoverageClass(percentage: number): string {
if (percentage >= 80) return 'high';
if (percentage >= 60) return 'medium';
return 'low';
}
}
interface CoverageMetrics {
total: number;
covered: number;
percentage: number;
uncoveredRanges: TraeAPI.Range[];
}
interface FileCoverageData {
uri: TraeAPI.Uri;
statements: CoverageMetrics;
branches: CoverageMetrics;
functions: CoverageMetrics;
lines: CoverageMetrics;
}
interface OverallCoverage {
statements: { total: number; covered: number; percentage: number };
branches: { total: number; covered: number; percentage: number };
functions: { total: number; covered: number; percentage: number };
lines: { total: number; covered: number; percentage: number };
}
// Initialize coverage analyzer
const coverageAnalyzer = new TestCoverageAnalyzer();API Reference
Core Interfaces
typescript
interface TestController {
readonly id: string;
readonly label: string;
readonly items: TestItemCollection;
createTestItem(id: string, label: string, uri?: Uri): TestItem;
createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun;
createRunProfile(
label: string,
kind: TestRunProfileKind,
runHandler: (request: TestRunRequest, token: CancellationToken) => void | Thenable<void>,
isDefault?: boolean
): TestRunProfile;
refreshHandler?: () => void | Thenable<void>;
resolveHandler?: (item: TestItem | undefined) => void | Thenable<void>;
dispose(): void;
}
interface TestItem {
readonly id: string;
label: string;
uri?: Uri;
children: TestItemCollection;
parent?: TestItem;
tags: readonly TestTag[];
canResolveChildren: boolean;
busy: boolean;
range?: Range;
error?: string | MarkdownString;
}
interface TestRun {
readonly name?: string;
readonly token: CancellationToken;
enqueued(test: TestItem): void;
started(test: TestItem): void;
skipped(test: TestItem): void;
failed(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void;
errored(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void;
passed(test: TestItem, duration?: number): void;
appendOutput(output: string, location?: Location, test?: TestItem): void;
addCoverage(fileCoverage: FileCoverage): void;
end(): void;
}
interface TestRunRequest {
readonly include?: readonly TestItem[];
readonly exclude?: readonly TestItem[];
readonly profile?: TestRunProfile;
}
interface FileCoverage {
readonly uri: Uri;
readonly statementCoverage: readonly StatementCoverage[];
readonly branchCoverage: readonly BranchCoverage[];
readonly functionCoverage: readonly FunctionCoverage[];
static fromDetails(
uri: Uri,
statementCoverage: readonly StatementCoverage[],
branchCoverage?: readonly BranchCoverage[],
functionCoverage?: readonly FunctionCoverage[]
): FileCoverage;
}Best Practices
- Test Discovery: Implement efficient test discovery with file watching
- Performance: Use lazy loading and caching for large test suites
- User Experience: Provide clear progress indicators and error messages
- Integration: Support multiple testing frameworks and runners
- Coverage: Implement comprehensive coverage analysis
- Reporting: Generate detailed and actionable test reports
- Debugging: Provide seamless test debugging capabilities
- CI/CD: Integrate with continuous integration systems
Related APIs
- Debugging API - For test debugging
- Commands API - For test commands
- Workspace API - For file operations
- UI API - For test UI components