Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions __tests__/pages/utilities/har-file-viewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,75 @@ describe("HARFileViewer", () => {
const row1 = within(rows[0]).getByTestId("column-status-code");
expect(row1).toHaveTextContent("200");
});

test("should filter requests based on search query", async () => {
const user = userEvent.setup();
render(<HARFileViewer />);

// Upload a HAR file
const file = new File([JSON.stringify(mockHarData)], "test.har", {
type: "application/json",
});
const fileInput = screen.getByTestId("input");
await user.upload(fileInput, file);

// Wait for all requests to be displayed
await screen.findByText("https://example.com/api/test");
await screen.findByText("https://example.com/css/style.css");

// Find the search input
const searchInput = screen.getByPlaceholderText(
"Search in URLs, headers, requests, and responses..."
);

// Search for "api" - should only show the first request
await user.type(searchInput, "api");

// Wait for debounce (300ms) + rendering time
await new Promise((resolve) => setTimeout(resolve, 500));

// Should still see the api request
expect(
screen.getByText("https://example.com/api/test")
).toBeInTheDocument();

// Should not see the css request (it should be filtered out)
const rows = screen.queryAllByTestId("table-row");
expect(rows.length).toBe(1);
});

test("should clear search query when clear button is clicked", async () => {
const user = userEvent.setup();
render(<HARFileViewer />);

// Upload a HAR file
const file = new File([JSON.stringify(mockHarData)], "test.har", {
type: "application/json",
});
const fileInput = screen.getByTestId("input");
await user.upload(fileInput, file);

// Wait for requests to be displayed
await screen.findByText("https://example.com/api/test");

// Find and use the search input
const searchInput = screen.getByPlaceholderText(
"Search in URLs, headers, requests, and responses..."
);
await user.type(searchInput, "api");

// Wait for debounce
await new Promise((resolve) => setTimeout(resolve, 400));

// Find the clear button (it should appear when there's text)
const clearButton = screen.getByTitle("Clear search");
await user.click(clearButton);

// Search input should be empty
expect(searchInput).toHaveValue("");

// Both requests should be visible again
await screen.findByText("https://example.com/api/test");
await screen.findByText("https://example.com/css/style.css");
});
});
70 changes: 65 additions & 5 deletions components/har-waterfall/HarWaterfall.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React, { useRef, useState, useCallback, useMemo } from "react";
import { HarEntry, FilterType, getFilterType } from "../utils/har-utils";
import {
HarEntry,
FilterType,
getFilterType,
isBase64,
} from "../utils/har-utils";
import { WaterfallCanvas } from "./WaterfallCanvas";
import { WaterfallTooltip } from "./WaterfallTooltip";
import { WaterfallLegend } from "./WaterfallLegend";
Expand All @@ -11,12 +16,14 @@ interface HarWaterfallProps {
entries: HarEntry[];
activeFilter: FilterType;
className?: string;
searchQuery?: string;
}

export const HarWaterfall: React.FC<HarWaterfallProps> = ({
entries,
activeFilter,
className = "",
searchQuery = "",
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [hoveredEntry, setHoveredEntry] = useState<{
Expand All @@ -36,11 +43,64 @@ export const HarWaterfall: React.FC<HarWaterfallProps> = ({
} | null>(null);
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });

// Filter entries based on active filter
// Filter entries based on active filter and search query
const filteredEntries = useMemo(() => {
if (activeFilter === "All") return entries;
return entries.filter((entry) => getFilterType(entry) === activeFilter);
}, [entries, activeFilter]);
let result = entries;

// Apply content type filter
if (activeFilter !== "All") {
result = result.filter((entry) => getFilterType(entry) === activeFilter);
}

// Apply search filter
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter((entry) => {
// Search in URL
if (entry.request.url.toLowerCase().includes(query)) return true;

// Search in request headers
const requestHeaderMatch = entry.request.headers.some(
(header) =>
header.name.toLowerCase().includes(query) ||
header.value.toLowerCase().includes(query)
);
if (requestHeaderMatch) return true;

// Search in response headers
const responseHeaderMatch = entry.response.headers.some(
(header) =>
header.name.toLowerCase().includes(query) ||
header.value.toLowerCase().includes(query)
);
if (responseHeaderMatch) return true;

// Search in request payload
if (entry.request.postData?.text) {
if (entry.request.postData.text.toLowerCase().includes(query))
return true;
}

// Search in response content
if (entry.response.content.text) {
// For base64 content, try to decode and search
let contentToSearch = entry.response.content.text;
if (isBase64(contentToSearch)) {
try {
contentToSearch = atob(contentToSearch);
} catch (e) {
// If decode fails, search in original
}
}
if (contentToSearch.toLowerCase().includes(query)) return true;
}

return false;
});
}

return result;
}, [entries, activeFilter, searchQuery]);

// Calculate timings for all entries
const timings = useMemo(() => {
Expand Down
Loading