mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:51:13 -05:00
Make test results copyable
Add Markdown formatting utilities for test results, wire up clipboard copy in IntegrationTestRunner, and export new formatters. Introduce formatters.ts, extend index.ts exports, and implement copy all / copy failed / per-test copy functionality with updated UI.
This commit is contained in:
@@ -14,8 +14,8 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { useSuperuserGuard } from '@/hooks/useSuperuserGuard';
|
||||
import { IntegrationTestRunner as TestRunner, allTestSuites, type TestResult } from '@/lib/integrationTests';
|
||||
import { Play, Square, Download, ChevronDown, CheckCircle2, XCircle, Clock, SkipForward } from 'lucide-react';
|
||||
import { IntegrationTestRunner as TestRunner, allTestSuites, type TestResult, formatResultsAsMarkdown, formatSingleTestAsMarkdown } from '@/lib/integrationTests';
|
||||
import { Play, Square, Download, ChevronDown, CheckCircle2, XCircle, Clock, SkipForward, Copy, ClipboardX } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
|
||||
@@ -105,6 +105,38 @@ export function IntegrationTestRunner() {
|
||||
toast.success('Test results exported');
|
||||
}, [runner]);
|
||||
|
||||
const copyAllResults = useCallback(async () => {
|
||||
const summary = runner.getSummary();
|
||||
const results = runner.getResults();
|
||||
|
||||
const markdown = formatResultsAsMarkdown(results, summary);
|
||||
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
toast.success('All test results copied to clipboard');
|
||||
}, [runner]);
|
||||
|
||||
const copyFailedTests = useCallback(async () => {
|
||||
const summary = runner.getSummary();
|
||||
const failedResults = runner.getResults().filter(r => r.status === 'fail');
|
||||
|
||||
if (failedResults.length === 0) {
|
||||
toast.info('No failed tests to copy');
|
||||
return;
|
||||
}
|
||||
|
||||
const markdown = formatResultsAsMarkdown(failedResults, summary, true);
|
||||
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
toast.success(`${failedResults.length} failed test(s) copied to clipboard`);
|
||||
}, [runner]);
|
||||
|
||||
const copyTestResult = useCallback(async (result: TestResult) => {
|
||||
const markdown = formatSingleTestAsMarkdown(result);
|
||||
|
||||
await navigator.clipboard.writeText(markdown);
|
||||
toast.success('Test result copied to clipboard');
|
||||
}, []);
|
||||
|
||||
// Guard is handled by the route/page, no loading state needed here
|
||||
|
||||
const summary = runner.getSummary();
|
||||
@@ -166,10 +198,22 @@ export function IntegrationTestRunner() {
|
||||
</Button>
|
||||
)}
|
||||
{results.length > 0 && !isRunning && (
|
||||
<Button onClick={exportResults} variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export Results
|
||||
</Button>
|
||||
<>
|
||||
<Button onClick={exportResults} variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export JSON
|
||||
</Button>
|
||||
<Button onClick={copyAllResults} variant="outline">
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy All
|
||||
</Button>
|
||||
{summary.failed > 0 && (
|
||||
<Button onClick={copyFailedTests} variant="outline">
|
||||
<ClipboardX className="w-4 h-4 mr-2" />
|
||||
Copy Failed ({summary.failed})
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -236,6 +280,14 @@ export function IntegrationTestRunner() {
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{result.duration}ms
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => copyTestResult(result)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
{(result.error || result.details) && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
||||
|
||||
76
src/lib/integrationTests/formatters.ts
Normal file
76
src/lib/integrationTests/formatters.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Test Result Formatters
|
||||
*
|
||||
* Utilities for formatting test results into different formats for easy sharing and debugging.
|
||||
*/
|
||||
|
||||
import type { TestResult } from './testRunner';
|
||||
|
||||
export function formatResultsAsMarkdown(
|
||||
results: TestResult[],
|
||||
summary: { total: number; passed: number; failed: number; skipped: number; totalDuration: number },
|
||||
failedOnly: boolean = false
|
||||
): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
const title = failedOnly ? 'Failed Test Results' : 'Test Results';
|
||||
|
||||
let markdown = `# ${title} - ${timestamp}\n\n`;
|
||||
|
||||
// Summary section
|
||||
markdown += `## Summary\n`;
|
||||
markdown += `✅ Passed: ${summary.passed}\n`;
|
||||
markdown += `❌ Failed: ${summary.failed}\n`;
|
||||
markdown += `⏭️ Skipped: ${summary.skipped}\n`;
|
||||
markdown += `⏱️ Duration: ${(summary.totalDuration / 1000).toFixed(2)}s\n\n`;
|
||||
|
||||
// Results by status
|
||||
if (!failedOnly && summary.failed > 0) {
|
||||
markdown += `## Failed Tests\n\n`;
|
||||
results.filter(r => r.status === 'fail').forEach(result => {
|
||||
markdown += formatTestResultMarkdown(result);
|
||||
});
|
||||
}
|
||||
|
||||
if (failedOnly) {
|
||||
results.forEach(result => {
|
||||
markdown += formatTestResultMarkdown(result);
|
||||
});
|
||||
} else {
|
||||
// Include passed tests in summary
|
||||
if (summary.passed > 0) {
|
||||
markdown += `## Passed Tests\n\n`;
|
||||
results.filter(r => r.status === 'pass').forEach(result => {
|
||||
markdown += `### ✅ ${result.name} (${result.suite})\n`;
|
||||
markdown += `**Duration:** ${result.duration}ms\n\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
export function formatSingleTestAsMarkdown(result: TestResult): string {
|
||||
return formatTestResultMarkdown(result);
|
||||
}
|
||||
|
||||
function formatTestResultMarkdown(result: TestResult): string {
|
||||
const icon = result.status === 'fail' ? '❌' : result.status === 'pass' ? '✅' : '⏭️';
|
||||
|
||||
let markdown = `### ${icon} ${result.name} (${result.suite})\n`;
|
||||
markdown += `**Duration:** ${result.duration}ms\n`;
|
||||
markdown += `**Status:** ${result.status}\n`;
|
||||
|
||||
if (result.error) {
|
||||
markdown += `**Error:** ${result.error}\n\n`;
|
||||
}
|
||||
|
||||
if (result.stack) {
|
||||
markdown += `**Stack Trace:**\n\`\`\`\n${result.stack}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
if (result.details) {
|
||||
markdown += `**Details:**\n\`\`\`json\n${JSON.stringify(result.details, null, 2)}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
@@ -6,5 +6,6 @@
|
||||
|
||||
export { IntegrationTestRunner } from './testRunner';
|
||||
export { allTestSuites } from './suites';
|
||||
export { formatResultsAsMarkdown, formatSingleTestAsMarkdown } from './formatters';
|
||||
|
||||
export type { TestResult, Test, TestSuite } from './testRunner';
|
||||
|
||||
Reference in New Issue
Block a user