Skip to content

Commit 64c9889

Browse files
committed
examples
1 parent b5c9188 commit 64c9889

2 files changed

Lines changed: 155 additions & 5 deletions

File tree

web-report/src/components/Dashboard.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Endpoints} from "@/pages/Endpoints.tsx";
88
import {TestResults} from "@/pages/TestResults.tsx";
99
import {Tests} from "@/pages/Tests.tsx";
1010
import {Warnings} from "@/pages/Warnings.tsx";
11+
import {Examples} from "@/pages/Examples.tsx";
1112

1213
import {ScrollArea, ScrollBar} from "@/components/ui/scroll-area.tsx";
1314
import {useAppContext} from "@/AppProvider.tsx";
@@ -16,13 +17,26 @@ import {REVIEW_STATE} from "@/types/Review.ts";
1617

1718
export interface ITestTabs {
1819
value: string;
20+
origin: string;
1921
}
2022

23+
const MAIN_TABS = new Set(["overview", "endpoints", "examples", "tests", "warnings"]);
24+
2125
export const Dashboard: React.FC = () => {
2226
const {data, isDirty, reviews} = useAppContext();
2327

2428
const warningCount = data?.warnings?.length ?? 0;
2529

30+
const examplesCount = useMemo(() => {
31+
const set = new Set<string>();
32+
for (const tc of data?.testCases ?? []) {
33+
for (const ex of tc.namedExamples ?? []) {
34+
if (ex) set.add(ex);
35+
}
36+
}
37+
return set.size;
38+
}, [data]);
39+
2640
const reviewRatio = useMemo(() => {
2741
if (!data) return null;
2842
const total = data.testCases.length;
@@ -41,8 +55,11 @@ export const Dashboard: React.FC = () => {
4155
const [testTabs, setTestTabs] = useState<Array<ITestTabs>>([]);
4256

4357
const addTestTab = (testName: string, event: React.MouseEvent<HTMLElement>) => {
58+
const origin = MAIN_TABS.has(activeTab)
59+
? activeTab
60+
: (testTabs.find(t => t.value === activeTab)?.origin ?? "endpoints");
4461
if (!testTabs.find((t) => t.value === testName)) {
45-
setTestTabs([{value: testName}, ...testTabs]);
62+
setTestTabs([{value: testName, origin}, ...testTabs]);
4663
}
4764

4865
if (!event.ctrlKey) {
@@ -51,12 +68,15 @@ export const Dashboard: React.FC = () => {
5168
}
5269

5370
const handleCloseTestsTab = (testName: string) => {
71+
const closing = testTabs.find(t => t.value === testName);
5472
const updatedTabs = testTabs.filter((t) => t.value !== testName);
5573
setTestTabs(updatedTabs);
74+
if (activeTab !== testName) return;
5675
if (updatedTabs.length === 0) {
57-
setActiveTab("endpoints")
76+
const fallback = closing?.origin ?? "endpoints";
77+
setActiveTab(MAIN_TABS.has(fallback) ? fallback : "endpoints");
5878
} else {
59-
setActiveTab(updatedTabs[0].value)
79+
setActiveTab(updatedTabs[0].value);
6080
}
6181
}
6282

@@ -82,7 +102,7 @@ export const Dashboard: React.FC = () => {
82102
toolNameVersion={`${data.toolName}-${data.toolVersion}`}/>
83103
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
84104
<div className="flex justify-center mb-2 w-full">
85-
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[850px] h-auto p-1 bg-transparent`}>
105+
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[1000px] h-auto p-1 bg-transparent`}>
86106
<TabsTrigger
87107
value="overview"
88108
className="flex-1 sm:flex-none sm:min-w-[150px] py-3 text-xs sm:text-sm border border-gray-500 data-[state=active]:bg-blue-100 data-[state=active]:border-2 data-[state=active]:border-black data-[state=active]:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
@@ -97,6 +117,18 @@ export const Dashboard: React.FC = () => {
97117
>
98118
Endpoints
99119
</TabsTrigger>
120+
<TabsTrigger
121+
value="examples"
122+
className="flex-1 sm:flex-none sm:min-w-[150px] py-3 text-xs sm:text-sm border border-gray-500 data-[state=active]:bg-blue-100 data-[state=active]:border-2 data-[state=active]:border-black data-[state=active]:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
123+
data-testid="tab-examples"
124+
>
125+
Examples
126+
{examplesCount > 0 && (
127+
<span className="ml-2 text-xs font-mono text-gray-600" data-testid="tab-examples-count">
128+
{examplesCount}
129+
</span>
130+
)}
131+
</TabsTrigger>
100132
<TabsTrigger
101133
value="tests"
102134
className="flex-1 sm:flex-none sm:min-w-[150px] py-3 text-xs sm:text-sm border border-gray-500 data-[state=active]:bg-blue-100 data-[state=active]:border-2 data-[state=active]:border-black data-[state=active]:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
@@ -128,7 +160,7 @@ export const Dashboard: React.FC = () => {
128160

129161
<div className="flex justify-center w-full">
130162
{
131-
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[850px] h-auto p-1 bg-transparent`}>
163+
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[1000px] h-auto p-1 bg-transparent`}>
132164
<ScrollArea className="w-[130%] whitespace-nowrap py-3">
133165
{
134166
testTabs.map((test, index) => (
@@ -166,6 +198,10 @@ export const Dashboard: React.FC = () => {
166198
<Endpoints addTestTab={addTestTab}/>
167199
</TabsContent>
168200

201+
<TabsContent value="examples">
202+
<Examples addTestTab={addTestTab}/>
203+
</TabsContent>
204+
169205
<TabsContent value="tests">
170206
<Tests/>
171207
</TabsContent>

web-report/src/pages/Examples.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, {useMemo, useState} from "react";
2+
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion.tsx";
3+
import {useAppContext} from "@/AppProvider.tsx";
4+
import {ChevronRight, Code} from "lucide-react";
5+
6+
interface IProps {
7+
addTestTab: (testName: string, event: React.MouseEvent<HTMLElement>) => void;
8+
}
9+
10+
interface ExampleEntry {
11+
name: string;
12+
cases: Array<{id: string; name: string}>;
13+
}
14+
15+
export const Examples: React.FC<IProps> = ({addTestTab}) => {
16+
const {data} = useAppContext();
17+
const testCases = useMemo(() => data?.testCases ?? [], [data]);
18+
19+
const [filter, setFilter] = useState("");
20+
21+
const namedExamples = useMemo<ExampleEntry[]>(() => {
22+
const map = new Map<string, Array<{id: string; name: string}>>();
23+
for (const tc of testCases) {
24+
if (!tc.namedExamples || !tc.id) continue;
25+
const tcId = tc.id;
26+
const tcName = tc.name ?? tc.id;
27+
for (const ex of tc.namedExamples) {
28+
if (!ex) continue;
29+
const arr = map.get(ex) ?? [];
30+
if (!arr.some(existing => existing.id === tcId)) {
31+
arr.push({id: tcId, name: tcName});
32+
}
33+
map.set(ex, arr);
34+
}
35+
}
36+
const out: ExampleEntry[] = Array.from(map.entries()).map(([name, cases]) => ({name, cases}));
37+
out.sort((a, b) => a.name.localeCompare(b.name));
38+
return out;
39+
}, [testCases]);
40+
41+
const filteredExamples = useMemo(() => {
42+
const q = filter.trim().toLowerCase();
43+
if (!q) return namedExamples;
44+
return namedExamples.filter(e => e.name.toLowerCase().includes(q));
45+
}, [namedExamples, filter]);
46+
47+
return (
48+
<div className="border-2 border-black p-3 sm:p-6 rounded-none" data-testid="examples-page">
49+
<div className="flex flex-wrap items-center gap-2 mb-4">
50+
<h2 className="text-lg font-bold">Named Examples</h2>
51+
<span className="ml-2 font-mono text-xs px-2 py-1 border-2 border-black bg-white" data-testid="examples-count">
52+
{namedExamples.length}
53+
</span>
54+
</div>
55+
<p className="text-sm text-gray-700 mb-4">
56+
Unique named examples used across the generated test cases (sorted alphabetically).
57+
Click on one to see all test cases that include it.
58+
</p>
59+
60+
{namedExamples.length > 0 && (
61+
<input
62+
type="text"
63+
placeholder="Filter by name..."
64+
value={filter}
65+
onChange={e => setFilter(e.target.value)}
66+
className="border-2 border-black px-2 py-1 mb-4 w-full sm:w-80 font-mono text-sm"
67+
data-testid="examples-filter"
68+
/>
69+
)}
70+
71+
{namedExamples.length === 0 ? (
72+
<div className="border-2 border-dashed border-gray-400 bg-gray-50 p-6 text-center text-sm text-gray-600 font-mono" data-testid="examples-empty">
73+
No named examples recorded.
74+
</div>
75+
) : filteredExamples.length === 0 ? (
76+
<div className="text-gray-500 italic text-sm">No named examples match the current filter.</div>
77+
) : (
78+
<Accordion type="multiple" className="w-full">
79+
{filteredExamples.map((ex, idx) => (
80+
<AccordionItem
81+
key={ex.name}
82+
value={`example-${idx}`}
83+
className="border-2 border-black mb-4 overflow-hidden"
84+
data-testid={`example-${idx}`}
85+
>
86+
<AccordionTrigger className="bg-blue-100 px-3 sm:px-4 py-3 text-sm sm:text-lg font-bold hover:no-underline hover:bg-blue-200">
87+
<div className="flex-1 font-mono text-left break-all">{ex.name}</div>
88+
<div className="mr-4 font-mono text-sm">{ex.cases.length}</div>
89+
</AccordionTrigger>
90+
<AccordionContent className="p-3 sm:p-4">
91+
<div className="flex flex-col gap-2">
92+
{ex.cases.map((tc, j) => (
93+
<button
94+
key={`${tc.id}-${j}`}
95+
onClick={(event) => addTestTab(tc.id, event)}
96+
className="w-full flex items-center justify-between p-3 border border-gray-200 hover:bg-blue-50 hover:border-blue-300 text-left transition-colors"
97+
data-testid={`example-${idx}-test-${j}`}
98+
>
99+
<div className="flex items-center">
100+
<Code className="mr-3 text-gray-500 shrink-0" size={20}/>
101+
<span className="font-mono text-sm break-all">{tc.name}</span>
102+
</div>
103+
<ChevronRight className="text-gray-400 shrink-0" size={18}/>
104+
</button>
105+
))}
106+
</div>
107+
</AccordionContent>
108+
</AccordionItem>
109+
))}
110+
</Accordion>
111+
)}
112+
</div>
113+
);
114+
};

0 commit comments

Comments
 (0)