diff --git a/tests/unit/dashboard-components.test.tsx b/tests/unit/dashboard-components.test.tsx
new file mode 100644
index 0000000..3a47beb
--- /dev/null
+++ b/tests/unit/dashboard-components.test.tsx
@@ -0,0 +1,233 @@
+/**
+ * Dashboard Components Unit Tests
+ * Tests for TopQuestionsChart and TranscriptViewer components
+ * Note: GeographicMap tests excluded due to react-leaflet/dynamic import issues in test environment
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import type { TopQuestion } from "../../lib/types";
+
+// Mock ReactMarkdown and rehype-raw for TranscriptViewer
+vi.mock("react-markdown", () => ({
+ default: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("rehype-raw", () => ({
+ default: vi.fn(),
+}));
+
+// Mock shadcn/ui components
+vi.mock("@/components/ui/card", () => ({
+ Card: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ CardHeader: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ CardTitle: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ CardContent: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/components/ui/badge", () => ({
+ Badge: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock("@/components/ui/separator", () => ({
+ Separator: (props: any) =>
,
+}));
+
+// Import components after mocks
+import TopQuestionsChart from "../../components/TopQuestionsChart";
+import TranscriptViewer from "../../components/TranscriptViewer";
+
+describe("Dashboard Components", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("TopQuestionsChart", () => {
+ const mockQuestions: TopQuestion[] = [
+ { question: "How do I reset my password?", count: 25 },
+ { question: "What are the working hours?", count: 20 },
+ { question: "How do I request vacation?", count: 15 },
+ { question: "Where is the employee handbook?", count: 10 },
+ { question: "How do I contact HR?", count: 8 },
+ ];
+
+ it("should render chart with questions data", () => {
+ render();
+
+ expect(screen.getByTestId("card")).toBeInTheDocument();
+ expect(screen.getByTestId("card-title")).toHaveTextContent("Top 5 Asked Questions");
+ expect(screen.getByText("How do I reset my password?")).toBeInTheDocument();
+ });
+
+ it("should render with custom title", () => {
+ render();
+
+ expect(screen.getByTestId("card-title")).toHaveTextContent("Custom Title");
+ });
+
+ it("should handle empty questions data", () => {
+ render();
+
+ expect(screen.getByTestId("card")).toBeInTheDocument();
+ expect(screen.getByTestId("card-title")).toHaveTextContent("Top 5 Asked Questions");
+ expect(screen.getByText("No questions data available")).toBeInTheDocument();
+ });
+
+ it("should display question counts as badges", () => {
+ render();
+
+ expect(screen.getByText("25")).toBeInTheDocument();
+ expect(screen.getByText("20")).toBeInTheDocument();
+ });
+
+ it("should show all questions with progress bars", () => {
+ render();
+
+ // All questions should be rendered
+ mockQuestions.forEach(question => {
+ expect(screen.getByText(question.question)).toBeInTheDocument();
+ expect(screen.getByText(question.count.toString())).toBeInTheDocument();
+ });
+ });
+
+ it("should calculate and display total questions", () => {
+ render();
+
+ const totalQuestions = mockQuestions.reduce((sum, q) => sum + q.count, 0);
+ expect(screen.getByText(totalQuestions.toString())).toBeInTheDocument();
+ expect(screen.getByText("Total questions analyzed")).toBeInTheDocument();
+ });
+ });
+
+ describe("TranscriptViewer", () => {
+ const mockTranscriptContent = `User: Hello, I need help with my account
+Assistant: I'd be happy to help you with your account. What specific issue are you experiencing?
+User: I can't log in to my account
+Assistant: Let me help you with that. Can you tell me what error message you're seeing?`;
+
+ const mockTranscriptUrl = "https://example.com/transcript/123";
+
+ it("should render transcript content", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Session Transcript")).toBeInTheDocument();
+ expect(screen.getByText(/Hello, I need help with my account/)).toBeInTheDocument();
+ });
+
+ it("should handle empty transcript content", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("No transcript content available.")).toBeInTheDocument();
+ });
+
+ it("should render without transcript URL", () => {
+ render(
+
+ );
+
+ // Should still render content
+ expect(screen.getByText("Session Transcript")).toBeInTheDocument();
+ expect(screen.getByText(/Hello, I need help with my account/)).toBeInTheDocument();
+ });
+
+ it("should toggle between formatted and raw view", () => {
+ render(
+
+ );
+
+ // Find the raw text toggle button
+ const rawToggleButton = screen.getByText("Raw Text");
+ expect(rawToggleButton).toBeInTheDocument();
+
+ // Click to show raw view
+ fireEvent.click(rawToggleButton);
+
+ // Should now show "Formatted" button and raw content
+ expect(screen.getByText("Formatted")).toBeInTheDocument();
+ });
+
+ it("should handle malformed transcript content gracefully", () => {
+ const malformedContent = "This is not a properly formatted transcript";
+
+ render(
+
+ );
+
+ // Should show "No transcript content available" in formatted view for malformed content
+ expect(screen.getByText("No transcript content available.")).toBeInTheDocument();
+
+ // But should show the raw content when toggled to raw view
+ const rawToggleButton = screen.getByText("Raw Text");
+ fireEvent.click(rawToggleButton);
+ expect(screen.getByText(malformedContent)).toBeInTheDocument();
+ });
+
+ it("should parse and display conversation messages", () => {
+ render(
+
+ );
+
+ // Check for message content
+ expect(screen.getByText(/Hello, I need help with my account/)).toBeInTheDocument();
+ expect(screen.getByText(/I'd be happy to help you/)).toBeInTheDocument();
+ });
+
+ it("should display transcript URL link when provided", () => {
+ render(
+
+ );
+
+ const link = screen.getByText("View Full Raw");
+ expect(link).toBeInTheDocument();
+ expect(link.closest("a")).toHaveAttribute("href", mockTranscriptUrl);
+ });
+ });
+});
\ No newline at end of file
diff --git a/vitest.config.mts b/vitest.config.mts
index b87bcb4..fbc4a50 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -5,7 +5,7 @@ import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
- environment: "node",
+ environment: "jsdom",
globals: true,
setupFiles: ["./tests/setup.ts"],
include: ["tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],