Skip to content
Open
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
102 changes: 102 additions & 0 deletions __tests__/components/session-hooks-panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import SessionHooksPanel from "@/app/components/session-hooks-panel";

const mocks = vi.hoisted(() => ({
searchHookActivityAction: vi.fn(),
ChevronDown: (props: any) => <svg data-testid="chevron-icon" {...props} />,
ShieldCheck: () => <span data-testid="shield-check" />,
ShieldX: () => <span data-testid="shield-x" />,
ShieldAlert: () => <span data-testid="shield-alert" />,
Shield: () => <span data-testid="shield" />,
Copy: (props: any) => <span data-testid="copy-icon" {...props} />,
Check: (props: any) => <span data-testid="check-icon" {...props} />,
ChevronLeft: () => <span data-testid="chevron-left" />,
ChevronRight: () => <span data-testid="chevron-right" />,
}));

vi.mock("@/app/actions/get-hook-activity", () => ({
searchHookActivityAction: mocks.searchHookActivityAction,
}));

vi.mock("lucide-react", () => ({
ChevronDown: mocks.ChevronDown,
ShieldCheck: mocks.ShieldCheck,
ShieldX: mocks.ShieldX,
ShieldAlert: mocks.ShieldAlert,
Shield: mocks.Shield,
Copy: mocks.Copy,
Check: mocks.Check,
}));

vi.mock("@/contexts/AutoRefreshContext", () => ({
useAutoRefresh: () => ({ intervalSec: 0 }),
}));

vi.mock("@/lib/format-duration", () => ({
formatRelativeTime: () => "just now",
}));

const mockEntry = {
timestamp: Date.now(),
eventType: "PreToolUse",
integration: "claude",
toolName: "Read",
policyName: "test-policy",
policyNames: ["test-policy"],
decision: "allow" as const,
reason: "ok",
durationMs: 42,
sessionId: "sess_123",
permissionMode: "default",
};

const defaultPayload = {
entries: [mockEntry],
totalPages: 1,
page: 1,
stats: { totalEvents: 1, denyCount: 0, topPolicy: null, topPolicyCount: 0 },
};

describe("SessionHooksPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.searchHookActivityAction.mockResolvedValue(defaultPayload);
});

it("renders expand row button with correct aria attributes", () => {
render(<SessionHooksPanel sessionId="sess_123" initialData={defaultPayload} />);
const btn = screen.getByRole("button", { name: /expand hook event details/i });
expect(btn).toBeInTheDocument();
expect(btn).toHaveAttribute("aria-expanded", "false");
});

it("toggles aria-expanded on Space key press", async () => {
const user = userEvent.setup();
render(<SessionHooksPanel sessionId="sess_123" initialData={defaultPayload} />);
const btn = screen.getByRole("button", { name: /expand hook event details/i });
btn.focus();
await user.keyboard(" ");
expect(btn).toHaveAttribute("aria-expanded", "true");
await user.keyboard(" ");
expect(btn).toHaveAttribute("aria-expanded", "false");
});

it("toggles aria-expanded on Enter key press", async () => {
const user = userEvent.setup();
render(<SessionHooksPanel sessionId="sess_123" initialData={defaultPayload} />);
const btn = screen.getByRole("button", { name: /expand hook event details/i });
btn.focus();
await user.keyboard("{Enter}");
expect(btn).toHaveAttribute("aria-expanded", "true");
});

it("toggles aria-expanded on button click", async () => {
const user = userEvent.setup();
render(<SessionHooksPanel sessionId="sess_123" initialData={defaultPayload} />);
const btn = screen.getByRole("button", { name: /expand hook event details/i });
await user.click(btn);
expect(btn).toHaveAttribute("aria-expanded", "true");
});
});
19 changes: 14 additions & 5 deletions app/components/session-hooks-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,20 @@ export default function SessionHooksPanel({ sessionId, initialData }: SessionHoo
} ${isExpanded ? "bg-muted/20" : ""}`}
>
<td className="px-4 py-2">
<ChevronDown
className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-150 ${
isExpanded ? "rotate-0" : "-rotate-90"
}`}
/>
<button
type="button"
onClick={(e) => { e.stopPropagation(); toggleRow(i); }}
aria-expanded={isExpanded}
aria-label={`${isExpanded ? "Collapse" : "Expand"} hook event details`}
className="focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm"
>
<ChevronDown
className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-150 ${
isExpanded ? "rotate-0" : "-rotate-90"
}`}
aria-hidden="true"
/>
</button>
</td>
<td className="px-3 py-2">
<DecisionBadge decision={item.decision} />
Expand Down
19 changes: 14 additions & 5 deletions app/policies/hooks-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -687,11 +687,20 @@ function ActivityTab({
} ${isExpanded ? "bg-muted/20" : ""}`}
>
<td className="px-4 py-2">
<ChevronDown
className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-150 ${
isExpanded ? "rotate-0" : "-rotate-90"
}`}
/>
<button
type="button"
onClick={(e) => { e.stopPropagation(); toggleRow(i); }}
aria-expanded={isExpanded}
aria-label={`${isExpanded ? "Collapse" : "Expand"} hook event details`}
className="focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-sm"
>
Comment thread
chhhee10 marked this conversation as resolved.
<ChevronDown
className={`h-3.5 w-3.5 text-muted-foreground transition-transform duration-150 ${
isExpanded ? "rotate-0" : "-rotate-90"
}`}
aria-hidden="true"
/>
Comment thread
chhhee10 marked this conversation as resolved.
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</td>
<td
className="px-3 py-2 text-muted-foreground whitespace-nowrap"
Expand Down