Skip to content

Commit f8a227c

Browse files
authored
Merge pull request #34 from WebFuzzing/warnings-and-examples
Warnings and examples
2 parents 09ade8c + de62e9b commit f8a227c

6 files changed

Lines changed: 289 additions & 13 deletions

File tree

web-report/src-e2e/static/report.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4868,5 +4868,13 @@
48684868
}
48694869
]
48704870
}
4871+
],
4872+
"warnings": [
4873+
{
4874+
"message": "No authentication info was provided. Unless you are testing an example API, you should setup some authentication info for different users. If this is the first time you are using EvoMaster, and you just want to get a feeling of how it works, then ignore this warning. However, to get better results, you will need setup authentication info, eventually. More info is currently available at https://github.com/WebFuzzing/EvoMaster/blob/master/docs/auth.md",
4875+
"category": "FUZZER",
4876+
"displayPriority": 1,
4877+
"additionalProperties": {}
4878+
}
48714879
]
48724880
}

web-report/src/components/Dashboard.tsx

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

1113
import {ScrollArea, ScrollBar} from "@/components/ui/scroll-area.tsx";
1214
import {useAppContext} from "@/AppProvider.tsx";
@@ -15,11 +17,26 @@ import {REVIEW_STATE} from "@/types/Review.ts";
1517

1618
export interface ITestTabs {
1719
value: string;
20+
origin: string;
1821
}
1922

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

28+
const warningCount = data?.warnings?.length ?? 0;
29+
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+
2340
const reviewRatio = useMemo(() => {
2441
if (!data) return null;
2542
const total = data.testCases.length;
@@ -37,9 +54,16 @@ export const Dashboard: React.FC = () => {
3754

3855
const [testTabs, setTestTabs] = useState<Array<ITestTabs>>([]);
3956

57+
const [openExamples, setOpenExamples] = useState<string[]>([]);
58+
const [openEndpoint, setOpenEndpoint] = useState<string>("");
59+
const [openTestFiles, setOpenTestFiles] = useState<string[]>([]);
60+
4061
const addTestTab = (testName: string, event: React.MouseEvent<HTMLElement>) => {
62+
const origin = MAIN_TABS.has(activeTab)
63+
? activeTab
64+
: (testTabs.find(t => t.value === activeTab)?.origin ?? "endpoints");
4165
if (!testTabs.find((t) => t.value === testName)) {
42-
setTestTabs([{value: testName}, ...testTabs]);
66+
setTestTabs([{value: testName, origin}, ...testTabs]);
4367
}
4468

4569
if (!event.ctrlKey) {
@@ -48,12 +72,15 @@ export const Dashboard: React.FC = () => {
4872
}
4973

5074
const handleCloseTestsTab = (testName: string) => {
75+
const closing = testTabs.find(t => t.value === testName);
5176
const updatedTabs = testTabs.filter((t) => t.value !== testName);
5277
setTestTabs(updatedTabs);
78+
if (activeTab !== testName) return;
5379
if (updatedTabs.length === 0) {
54-
setActiveTab("endpoints")
80+
const fallback = closing?.origin ?? "endpoints";
81+
setActiveTab(MAIN_TABS.has(fallback) ? fallback : "endpoints");
5582
} else {
56-
setActiveTab(updatedTabs[0].value)
83+
setActiveTab(updatedTabs[0].value);
5784
}
5885
}
5986

@@ -79,7 +106,7 @@ export const Dashboard: React.FC = () => {
79106
toolNameVersion={`${data.toolName}-${data.toolVersion}`}/>
80107
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
81108
<div className="flex justify-center mb-2 w-full">
82-
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[700px] h-auto p-1 bg-transparent`}>
109+
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[1000px] h-auto p-1 bg-transparent`}>
83110
<TabsTrigger
84111
value="overview"
85112
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)]"
@@ -94,6 +121,18 @@ export const Dashboard: React.FC = () => {
94121
>
95122
Endpoints
96123
</TabsTrigger>
124+
<TabsTrigger
125+
value="examples"
126+
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)]"
127+
data-testid="tab-examples"
128+
>
129+
Examples
130+
{examplesCount > 0 && (
131+
<span className="ml-2 text-xs font-mono text-gray-600" data-testid="tab-examples-count">
132+
{examplesCount}
133+
</span>
134+
)}
135+
</TabsTrigger>
97136
<TabsTrigger
98137
value="tests"
99138
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)]"
@@ -107,13 +146,25 @@ export const Dashboard: React.FC = () => {
107146
)}
108147
{isDirty && <span className="ml-1 text-orange-600" title="Unsaved review changes"></span>}
109148
</TabsTrigger>
149+
<TabsTrigger
150+
value="warnings"
151+
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-orange-100 data-[state=active]:border-2 data-[state=active]:border-black data-[state=active]:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
152+
data-testid="tab-warnings"
153+
>
154+
Warnings
155+
{warningCount > 0 && (
156+
<span className="ml-2 text-xs font-mono px-1.5 py-0.5 border border-orange-600 bg-orange-50 text-orange-700" data-testid="tab-warnings-count">
157+
{warningCount}
158+
</span>
159+
)}
160+
</TabsTrigger>
110161
</TabsList>
111162
</div>
112163
<div className="border-t border-black my-2"></div>
113164

114165
<div className="flex justify-center w-full">
115166
{
116-
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[700px] h-auto p-1 bg-transparent`}>
167+
<TabsList className={`flex gap-2 sm:gap-4 w-full max-w-[1000px] h-auto p-1 bg-transparent`}>
117168
<ScrollArea className="w-[130%] whitespace-nowrap py-3">
118169
{
119170
testTabs.map((test, index) => (
@@ -148,11 +199,19 @@ export const Dashboard: React.FC = () => {
148199
</TabsContent>
149200

150201
<TabsContent value="endpoints">
151-
<Endpoints addTestTab={addTestTab}/>
202+
<Endpoints addTestTab={addTestTab} openEndpoint={openEndpoint} setOpenEndpoint={setOpenEndpoint}/>
203+
</TabsContent>
204+
205+
<TabsContent value="examples">
206+
<Examples addTestTab={addTestTab} openExamples={openExamples} setOpenExamples={setOpenExamples}/>
152207
</TabsContent>
153208

154209
<TabsContent value="tests">
155-
<Tests/>
210+
<Tests openTestFiles={openTestFiles} setOpenTestFiles={setOpenTestFiles}/>
211+
</TabsContent>
212+
213+
<TabsContent value="warnings">
214+
<Warnings/>
156215
</TabsContent>
157216

158217
{

web-report/src/pages/Endpoints.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {useAppContext} from "@/AppProvider.tsx";
66

77
interface IProps {
88
addTestTab: (value: string, event: React.MouseEvent<HTMLElement>) => void;
9+
openEndpoint: string;
10+
setOpenEndpoint: (value: string) => void;
911
}
1012

11-
export const Endpoints: React.FC<IProps> = ({addTestTab}) => {
13+
export const Endpoints: React.FC<IProps> = ({addTestTab, openEndpoint, setOpenEndpoint}) => {
1214

1315
const {transformedReport, filteredEndpoints, statusFilters, setStatusFilters} = useAppContext();
1416

@@ -21,10 +23,10 @@ export const Endpoints: React.FC<IProps> = ({addTestTab}) => {
2123
<p className="text-black-400">{filteredEndpoints.length}</p> / <p className="text-red-400">{transformedReport.length}</p>
2224
</div>
2325
</div>
24-
<Accordion type="single" collapsible className="w-full">
26+
<Accordion type="single" collapsible value={openEndpoint} onValueChange={setOpenEndpoint} className="w-full">
2527
{
2628
filteredEndpoints.map((item, index) => (
27-
<EndpointAccordion data-testid="endpoint" key={index} value={`_${index}`}
29+
<EndpointAccordion data-testid="endpoint" key={index} value={item.endpoint}
2830
endpoint={item.endpoint}
2931
statusCodes={item.httpStatusCodes} faults={item.faults}
3032
addTestTab={addTestTab}/>

web-report/src/pages/Examples.tsx

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

web-report/src/pages/Tests.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ const filterButtonClass = (active: boolean) =>
1717
: "bg-white hover:bg-gray-100"
1818
}`;
1919

20-
export const Tests: React.FC = () => {
20+
interface IProps {
21+
openTestFiles: string[];
22+
setOpenTestFiles: (value: string[]) => void;
23+
}
24+
25+
export const Tests: React.FC<IProps> = ({openTestFiles, setOpenTestFiles}) => {
2126
const {
2227
data,
2328
reviews,
@@ -153,13 +158,13 @@ export const Tests: React.FC = () => {
153158
))}
154159
</div>
155160

156-
<Accordion type="multiple" className="w-full">
161+
<Accordion type="multiple" value={openTestFiles} onValueChange={setOpenTestFiles} className="w-full">
157162
{testFilePaths.map((file, idx) => {
158163
const items = grouped.get(file) ?? [];
159164
return (
160165
<AccordionItem
161166
key={file}
162-
value={`file-${idx}`}
167+
value={file}
163168
className="border-2 border-black mb-4 overflow-hidden"
164169
data-testid={`test-file-${idx}`}
165170
>

0 commit comments

Comments
 (0)