UNPKG

pulse-dashboard

Version:

A Next.js Dashboard application for real-time monitoring and historical analysis of Playwright test executions, based on playwright-pulse-report. This component provides the UI for visualizing Playwright test results and can be run as a standalone CLI too

323 lines (322 loc) 16.7 kB
'use client'; import { useTestData } from '@/hooks/useTestData'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Skeleton } from '@/components/ui/skeleton'; import { ListX, Terminal, Info, CheckCircle, ChevronRight, SearchSlash } from 'lucide-react'; import { useMemo } from 'react'; import Link from 'next/link'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { ansiToHtml } from '@/lib/utils'; const CATEGORIES_CONFIG = [ { name: 'Timeout Errors', keywords: ['timeout', 'exceeded'], description: "Tests that failed due to exceeding a specified time limit for an operation." }, { name: 'Locator/Selector Errors', keywords: ['locator', 'selector', 'getByRole', 'getByText', 'getByLabel', 'getByPlaceholder', 'element not found', 'no element found'], description: "Failures related to finding or interacting with UI elements on the page." }, { name: 'Assertion Errors', keywords: ['expect(', 'expected', 'assertion failed'], description: "Tests where a specific condition or value did not meet the expected criteria." }, { name: 'Strict Mode Violations', keywords: ['strict mode violation'], description: "Failures caused by Playwright's strict mode, often when a locator resolves to multiple elements." }, { name: 'Navigation Errors', keywords: ['navigation failed', 'page.goto', 'frame.goto'], description: "Errors that occurred during page navigation actions." }, ]; const OTHER_ERRORS_CATEGORY = 'Other Errors'; // This utility is now only used for the categorization logic, not for display function stripAnsiCodesForLogic(str) { if (!str) return ''; return str.replace(/\u001b\[[0-9;]*[mGKH]/g, ''); } function formatTestName(fullName) { if (!fullName) return 'Unknown Test'; const parts = fullName.split(" > "); return parts[parts.length - 1] || fullName; } export function FailureCategorizationView() { const { currentRun, loadingCurrent, errorCurrent } = useTestData(); const categorizedFailures = useMemo(() => { if (!currentRun?.results) return { categories: [], totalFailed: 0 }; // Get all failed tests const allFailedTests = currentRun.results.filter((test) => test.status === 'failed' || test.status === 'timedOut'); // Deduplicate by test name - each unique test name should appear once const uniqueFailedTestsMap = new Map(); allFailedTests.forEach((test) => { if (!uniqueFailedTestsMap.has(test.name)) { uniqueFailedTestsMap.set(test.name, test); } }); const failedTests = Array.from(uniqueFailedTestsMap.values()); const categoriesMap = new Map(); failedTests.forEach((test) => { // For categorization logic, use stripped and lowercased error message const logicalErrorMessage = stripAnsiCodesForLogic(test.errorMessage || 'Unknown error').toLowerCase(); let assignedCategory = false; for (const category of CATEGORIES_CONFIG) { if (category.keywords.some(keyword => logicalErrorMessage.includes(keyword.toLowerCase()))) { if (!categoriesMap.has(category.name)) { categoriesMap.set(category.name, { tests: [], exampleErrorMessages: [] }); } categoriesMap.get(category.name).tests.push(test); // Store original error message for display if (categoriesMap.get(category.name).exampleErrorMessages.length < 3) { categoriesMap.get(category.name).exampleErrorMessages.push(test.errorMessage); } assignedCategory = true; break; } } if (!assignedCategory) { if (!categoriesMap.has(OTHER_ERRORS_CATEGORY)) { categoriesMap.set(OTHER_ERRORS_CATEGORY, { tests: [], exampleErrorMessages: [] }); } categoriesMap.get(OTHER_ERRORS_CATEGORY).tests.push(test); // Store original error message for display if (categoriesMap.get(OTHER_ERRORS_CATEGORY).exampleErrorMessages.length < 3) { categoriesMap.get(OTHER_ERRORS_CATEGORY).exampleErrorMessages.push(test.errorMessage); } } }); const result = []; categoriesMap.forEach((data, categoryName) => { result.push({ categoryName, count: data.tests.length, tests: data.tests, exampleErrorMessages: data.exampleErrorMessages, }); }); result.sort((a, b) => b.count - a.count); // Sort by count descending return { categories: result, totalFailed: failedTests.length, // Use total count of all failed tests }; }, [currentRun]); if (loadingCurrent) { return (<div className="space-y-6 p-6 lg:p-8 bg-muted/20 backdrop-blur-md border border-border/40 rounded-[2.5rem] shadow-inner"> {[...Array(3)].map((_, i) => (<Card key={i} className="shadow-lg"> <CardHeader> <Skeleton className="h-7 w-1/2 mb-2"/> <Skeleton className="h-4 w-3/4"/> </CardHeader> <CardContent> <Skeleton className="h-10 w-full"/> <Skeleton className="h-10 w-full mt-2"/> </CardContent> </Card>))} </div>); } if (errorCurrent) { return (<Alert variant="destructive" className="shadow-md"> <Terminal className="h-4 w-4"/> <AlertTitle>Error Fetching Data</AlertTitle> <AlertDescription>{errorCurrent}</AlertDescription> </Alert>); } if (!currentRun || !currentRun.results) { return (<Alert className="shadow-md"> <Info className="h-4 w-4"/> <AlertTitle>No Data Available</AlertTitle> <AlertDescription> The current run report ('playwright-pulse-report.json') could not be loaded or is empty. </AlertDescription> </Alert>); } const totalFailures = categorizedFailures.totalFailed; if (totalFailures === 0) { return (<Alert variant="default" className="shadow-md border-green-500 bg-green-50 dark:bg-green-900/30"> <CheckCircle className="h-5 w-5 text-green-600"/> <AlertTitle className="text-green-700 dark:text-green-400">No Failures Found!</AlertTitle> <AlertDescription className="text-green-600 dark:text-green-300"> Excellent! There are no failed or timed out tests in the current run report. </AlertDescription> </Alert>); } if (categorizedFailures.categories.length === 0 && totalFailures > 0) { return (<Alert className="shadow-md"> <SearchSlash className="h-4 w-4"/> <AlertTitle>Failures Present, But Not Categorized</AlertTitle> <AlertDescription> There are {totalFailures} failures in the current report, but they did not match any predefined categories. They might be listed under "Other Errors" if that category appears. </AlertDescription> </Alert>); } return (<div className="space-y-6 p-6 lg:p-8 bg-muted/20 backdrop-blur-md border border-border/40 rounded-[2.5rem] shadow-inner"> {/* Summary Stats */} <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> <Card className="shadow-lg hover:shadow-2xl transition-all duration-300 rounded-xl border-0 bg-gradient-to-br from-destructive/10 via-card to-card/95"> <CardContent className="p-6"> <div className="flex items-center justify-between"> <div> <p className="text-sm font-medium text-muted-foreground"> Total Failures </p> <p className="text-3xl font-bold text-destructive mt-2"> {totalFailures} </p> </div> <ListX className="h-12 w-12 text-destructive opacity-20"/> </div> </CardContent> </Card> <Card className="shadow-lg hover:shadow-2xl transition-all duration-300 rounded-xl border-0 bg-gradient-to-br from-primary/10 via-card to-card/95"> <CardContent className="p-6"> <div className="flex items-center justify-between"> <div> <p className="text-sm font-medium text-muted-foreground"> Categories </p> <p className="text-3xl font-bold text-primary mt-2"> {categorizedFailures.categories.length} </p> </div> <Terminal className="h-12 w-12 text-primary opacity-20"/> </div> </CardContent> </Card> <Card className="shadow-lg hover:shadow-2xl transition-all duration-300 rounded-xl border-0 bg-gradient-to-br from-accent/10 via-card to-card/95"> <CardContent className="p-6"> <div className="flex items-center justify-between"> <div> <p className="text-sm font-medium text-muted-foreground"> Most Common </p> <p className="text-lg font-bold text-foreground mt-2 truncate"> {categorizedFailures.categories[0]?.categoryName || "N/A"} </p> <p className="text-xs text-muted-foreground"> {categorizedFailures.categories[0]?.count || 0} tests </p> </div> <Info className="h-12 w-12 text-accent opacity-20"/> </div> </CardContent> </Card> </div> {/* Categorized Failures */} {categorizedFailures.categories.map((group, index) => { const categoryConfig = CATEGORIES_CONFIG.find((c) => c.name === group.categoryName); const firstExampleError = group.exampleErrorMessages[0]; const exampleErrorHtml = firstExampleError ? ansiToHtml(firstExampleError.substring(0, 150)) : ""; const showEllipsis = firstExampleError && firstExampleError.length > 150; return (<Card key={group.categoryName} className="shadow-lg hover:shadow-2xl transition-all duration-300 rounded-xl border-0 bg-gradient-to-br from-card via-card to-card/95 group"> <CardHeader className="border-b border-border/50 bg-muted/20"> <div className="flex items-center justify-between"> <div className="flex items-center gap-3"> <div className="h-12 w-12 rounded-full bg-gradient-to-br from-destructive/20 to-destructive/10 flex items-center justify-center"> <ListX className="h-6 w-6 text-destructive"/> </div> <div> <CardTitle className="text-xl font-semibold text-foreground group-hover:text-primary transition-colors flex items-center gap-2"> {group.categoryName} <Badge variant="destructive" className="text-xs font-semibold"> {group.count} </Badge> </CardTitle> {categoryConfig?.description && (<CardDescription className="text-xs mt-1"> {categoryConfig.description} </CardDescription>)} </div> </div> <div className="text-right"> <p className="text-xs text-muted-foreground"> Category #{index + 1} </p> <p className="text-sm font-semibold text-foreground"> {((group.count / totalFailures) * 100).toFixed(1)}% </p> </div> </div> {group.categoryName === OTHER_ERRORS_CATEGORY && firstExampleError && (<div className="mt-3 p-3 rounded-2xl bg-muted/50 border border-border/30"> <p className="text-xs font-medium text-muted-foreground mb-1"> Example error snippet: </p> <p className="text-xs text-foreground font-mono"> <span dangerouslySetInnerHTML={{ __html: exampleErrorHtml }}/> {showEllipsis ? "..." : ""} </p> </div>)} </CardHeader> <CardContent className="pt-4"> {group.tests.length > 0 ? (<Accordion type="multiple" className="w-full space-y-3"> {group.tests.map((test, testIndex) => (<AccordionItem value={`${group.categoryName}-${test.id}-${testIndex}`} key={`${group.categoryName}-${test.id}-${testIndex}`} className="border border-border/50 rounded-2xl bg-card hover:bg-muted/30 transition-all duration-200 hover:shadow-md overflow-hidden"> <AccordionTrigger className="px-4 py-3 text-left hover:no-underline group/trigger"> <div className="flex items-center gap-3 flex-1 min-w-0"> <div className="h-2 w-2 rounded-full bg-destructive flex-shrink-0"/> <div className="flex-1 min-w-0"> <p className="font-medium text-sm text-foreground group-hover/trigger:text-primary transition-colors truncate" title={formatTestName(test.name)}> {formatTestName(test.name)} </p> <p className="text-xs text-muted-foreground mt-0.5 truncate" title={test.suiteName}> Suite: {test.suiteName || "N/A"} </p> </div> </div> </AccordionTrigger> <AccordionContent className="px-4 pb-4 pt-2 bg-muted/10"> <div className="space-y-3"> <Link href={`/test/${test.id}`} className="inline-flex items-center gap-1 text-xs text-primary hover:underline font-medium"> <ChevronRight className="h-3 w-3"/> View full test details </Link> <div> <div className="flex items-center gap-2 mb-2"> <Terminal className="h-3 w-3 text-destructive"/> <h5 className="text-xs font-semibold text-foreground"> Error Message: </h5> </div> <ScrollArea className="max-h-40 w-full rounded-2xl border border-border/30"> <pre className="text-xs whitespace-pre-wrap break-words font-mono bg-muted/50 p-3"> <span dangerouslySetInnerHTML={{ __html: ansiToHtml(test.errorMessage || "No error message captured."), }}/> </pre> </ScrollArea> </div> </div> </AccordionContent> </AccordionItem>))} </Accordion>) : (<div className="text-center py-8"> <SearchSlash className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50"/> <p className="text-sm text-muted-foreground"> No tests found in this category. </p> </div>)} </CardContent> </Card>); })} {categorizedFailures.categories.length === 0 && (<Alert className="shadow-lg rounded-xl border-0 bg-gradient-to-br from-muted/50 to-card"> <SearchSlash className="h-5 w-5"/> <AlertTitle className="text-lg">No Failures Categorized</AlertTitle> <AlertDescription> Could not categorize any failures based on the current rules. </AlertDescription> </Alert>)} </div>); } //# sourceMappingURL=FailureCategorizationView.jsx.map