TestAnalytics
surface다차원 테스트 품질 분석 대시보드. Summary cards + 6-tab multi-chart (Overview/Stability/Failures/Flaky/Performance/Advanced).
컴포넌트 의존 관계
깊이
100%
기본 사용
Test Analytics
Pass Rate94.2%↑ +2.1%
Total Tests1,234↑ +12
Total Runs56→
Flaky Tests12↓ -3
Overview: test pass/fail trends chart
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
TestAnalytics 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
TestAnalytics Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
stats* | StatCardType[] | — | 요약 통계 카드 |
activeTab | TestAnalyticsTabId | — | 활성 탭 |
onTabChange | (tabId: TestAnalyticsTabId) => void | — | 탭 변경 핸들러 |
renderOverview | () => ReactNode | — | Overview 탭 렌더러 |
renderStability | () => ReactNode | — | Stability 탭 렌더러 |
renderFailures | () => ReactNode | — | Failures 탭 렌더러 |
renderFlaky | () => ReactNode | — | Flaky 탭 렌더러 |
renderPerformance | () => ReactNode | — | Performance 탭 렌더러 |
renderAdvanced | () => ReactNode | — | Advanced 탭 렌더러 |
timeRange | DateRange | — | 시간 범위 필터 |
onTimeRangeChange | (range: DateRange) => void | — | 시간 범위 변경 핸들러 |
loading | boolean | — | 로딩 상태 |
header | ReactNode | — | 커스텀 헤더 슬롯 |
actions | ReactNode | — | 액션 버튼 슬롯 |
labels | TestAnalyticsLabels | — | 커스텀 레이블 |
className | string | — | 최외곽 CSS 클래스 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add test-analyticsConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { TestAnalytics } from "@/components/surfaces/test-analytics";Registry metadata
- 설명
- 다차원 테스트 품질 분석 대시보드. Summary cards + 6-tab multi-chart (Overview/Stability/Failures/Flaky/Performance/Advanced).
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- 없음
- 태그
- dashboardanalytics
- Install notes
- 없음
포함 파일
test-analytics.tsxtest-analytics.tsx
Surface 소스 보기
test-analytics.tsx
"use client";
import * as React from "react";
import {
SurfaceLayout,
SummaryRow,
TimeRangeSelector,
cn,
type DateRange,
type StatCardType,
} from "@reopt-ai/opt-ui";
// ── Types ──
/** Type definition for `TestAnalyticsTabId`. */
export type TestAnalyticsTabId =
| "overview"
| "stability"
| "failures"
| "flaky"
| "performance"
| "advanced";
/** Labels for `TestAnalytics`. */
export interface TestAnalyticsLabels {
title?: string;
overviewTab?: string;
stabilityTab?: string;
failuresTab?: string;
flakyTab?: string;
performanceTab?: string;
advancedTab?: string;
emptyTitle?: string;
emptyDescription?: string;
refreshLabel?: string;
}
const defaultLabels: Required<TestAnalyticsLabels> = {
title: "Test Analytics",
overviewTab: "Overview",
stabilityTab: "Stability",
failuresTab: "Failures",
flakyTab: "Flaky",
performanceTab: "Performance",
advancedTab: "Advanced",
emptyTitle: "No analytics data",
emptyDescription: "Run tests to generate analytics data.",
refreshLabel: "Refresh",
};
// ── Props ──
/** Props for `TestAnalytics`. */
export interface TestAnalyticsProps {
/** Summary stat cards (pass rate, tests, runs, flaky count, etc.) */
stats: StatCardType[];
/** Active tab */
activeTab?: TestAnalyticsTabId;
/** Called when tab changes */
onTabChange?: (tabId: TestAnalyticsTabId) => void;
/** Tab content renderers */
renderOverview?: () => React.ReactNode;
renderStability?: () => React.ReactNode;
renderFailures?: () => React.ReactNode;
renderFlaky?: () => React.ReactNode;
renderPerformance?: () => React.ReactNode;
renderAdvanced?: () => React.ReactNode;
/** Time range for filtering data */
timeRange?: DateRange;
/** Called when time range changes */
onTimeRangeChange?: (range: DateRange) => void;
/** Refresh handler */
onRefresh?: () => void;
loading?: boolean;
header?: React.ReactNode;
actions?: React.ReactNode;
labels?: TestAnalyticsLabels;
className?: string;
}
// ── Component ──
/** Renders the `TestAnalytics` component. */
export function TestAnalytics({
stats,
activeTab: controlledActiveTab,
onTabChange,
renderOverview,
renderStability,
renderFailures,
renderFlaky,
renderPerformance,
renderAdvanced,
timeRange,
onTimeRangeChange,
onRefresh,
loading = false,
header,
actions,
labels: customLabels,
className,
}: TestAnalyticsProps) {
const labels = { ...defaultLabels, ...customLabels };
const sectionId = React.useId();
const [internalActiveTab, setInternalActiveTab] =
React.useState<TestAnalyticsTabId>("overview");
const activeTab = controlledActiveTab ?? internalActiveTab;
const handleTabChange = React.useCallback(
(tabId: string) => {
setInternalActiveTab(tabId as TestAnalyticsTabId);
onTabChange?.(tabId as TestAnalyticsTabId);
},
[onTabChange],
);
const isEmpty = stats.length === 0;
const tabs = React.useMemo(
() => [
{ id: "overview", label: labels.overviewTab },
{ id: "stability", label: labels.stabilityTab },
{ id: "failures", label: labels.failuresTab },
{ id: "flaky", label: labels.flakyTab },
{ id: "performance", label: labels.performanceTab },
{ id: "advanced", label: labels.advancedTab },
],
[labels],
);
const tabRenderers: Record<
TestAnalyticsTabId,
(() => React.ReactNode) | undefined
> = {
overview: renderOverview,
stability: renderStability,
failures: renderFailures,
flaky: renderFlaky,
performance: renderPerformance,
advanced: renderAdvanced,
};
return (
<SurfaceLayout loading={loading} className={className}>
{/* Header */}
<div className="flex items-center justify-between">
{header ? (
<div>{header}</div>
) : (
<h2 className="text-text-primary text-lg font-semibold">
{labels.title}
</h2>
)}
<div className="gap-element flex items-center">
<TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
{onRefresh && (
<button
type="button"
onClick={onRefresh}
className="text-text-tertiary hover:text-text-primary rounded p-1.5 text-sm transition-colors"
aria-label={labels.refreshLabel}
>
↻
</button>
)}
{actions}
</div>
</div>
{isEmpty ? (
<div
role="status"
className="flex flex-col items-center justify-center py-16 text-center"
>
<h3 className="text-text-primary text-lg font-medium">
{labels.emptyTitle}
</h3>
<p className="text-text-tertiary mt-1 text-sm">
{labels.emptyDescription}
</p>
</div>
) : (
<>
{/* Summary stat cards */}
<SummaryRow stats={stats} columns={4} />
{/* Tabbed content */}
<section aria-labelledby={`${sectionId}-tabs`}>
<span id={`${sectionId}-tabs`} className="sr-only">
{labels.title}
</span>
<div
role="tablist"
aria-label={labels.title}
className="border-border-subtle flex overflow-x-auto border-b"
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={activeTab === tab.id}
onClick={() => handleTabChange(tab.id)}
className={cn(
"px-4 py-2 text-sm font-medium transition-colors",
activeTab === tab.id
? "border-accent text-text-primary border-b-2"
: "text-text-tertiary hover:text-text-secondary",
)}
>
{tab.label}
</button>
))}
</div>
<div role="tabpanel" className="py-4">
{tabRenderers[activeTab]?.()}
</div>
</section>
</>
)}
</SurfaceLayout>
);
}
TestAnalytics.displayName = "TestAnalytics";