From 25f6625c4fdcacd6a4b6c1f1c732280cd0ff4ec8 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 5 Jul 2025 15:11:15 +0200 Subject: [PATCH] test: add comprehensive dashboard component tests - Configure vitest with jsdom environment for React component testing - Add comprehensive tests for TopQuestionsChart component (6 tests) - Add comprehensive tests for TranscriptViewer component (7 tests) - Mock all necessary dependencies (ReactMarkdown, shadcn/ui components) - All 13 component tests passing successfully - GeographicMap excluded due to react-leaflet test environment issues --- tests/unit/dashboard-components.test.tsx | 233 +++++++++++++++++++++++ vitest.config.mts | 2 +- 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 tests/unit/dashboard-components.test.tsx 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}"],