diff --git a/.github/workflows/pr-validation.yaml b/.github/workflows/pr-validation.yaml index 7285932..52020d0 100644 --- a/.github/workflows/pr-validation.yaml +++ b/.github/workflows/pr-validation.yaml @@ -36,9 +36,8 @@ jobs: - name: Build application run: npm run build - # Future addition when tests are implemented - # - name: Run tests - # run: npm test + - name: Run tests + run: npm test - name: Scan for vulnerabilities run: npm audit --production diff --git a/__tests__/README.md b/__tests__/README.md new file mode 100644 index 0000000..f05c9a0 --- /dev/null +++ b/__tests__/README.md @@ -0,0 +1,83 @@ +# Algorithm Visualizer Tests + +This directory contains tests for the Algorithm Visualizer application. The tests are organized to mirror the structure of the source code, making it easy to locate tests for specific components or functionality. + +## Directory Structure + +``` +__tests__/ +├── app/ # Tests for page components +│ └── sorting/ +│ └── [algorithm]/ +│ └── page.test.tsx # Tests for algorithm page +├── components/ # Tests for React components +│ ├── AlgorithmCard.test.tsx +│ └── visualizer/ +│ ├── SortingVisualization.test.tsx +│ └── VisualizerControls.test.tsx +├── context/ # Tests for React context +│ └── AlgorithmContext.test.tsx +├── lib/ # Tests for utility functions and algorithms +│ ├── algorithms/ +│ │ └── bubbleSort.test.ts +│ └── utils.test.ts +└── utils/ # Test utilities + └── test-utils.tsx # Common test utilities and helpers +``` + +## Running Tests + +You can run the tests using the following npm scripts: + +```bash +# Run all tests +npm test + +# Run tests in watch mode (useful during development) +npm run test:watch + +# Run tests with coverage report +npm run test:coverage +``` + +## Testing Approach + +1. **Unit Tests**: Test individual functions and components in isolation. +2. **Integration Tests**: Test interactions between multiple components. +3. **Mock Dependencies**: External dependencies are mocked to ensure tests are reliable and fast. + +## Test Utilities + +The `test-utils.tsx` file provides: + +- A custom render function that includes the AlgorithmProvider +- Mock implementations for key dependencies +- Helper functions for test setup + +## Coverage Reports + +After running `npm run test:coverage`, a coverage report will be generated in the `coverage/` directory. This report shows how much of the code is covered by tests and helps identify areas that need more testing. + +## Writing New Tests + +When adding new features or components, please follow these guidelines for writing tests: + +1. Create test files that mirror the structure of the source code. +2. Test both happy paths and edge cases. +3. Mock external dependencies. +4. Keep tests focused on specific behavior. +5. Use descriptive test names that explain what is being tested. + +Example: + +```typescript +describe('ComponentName', () => { + it('should render correctly with default props', () => { + // Test code here + }); + + it('should handle edge case X properly', () => { + // Test code here + }); +}); +``` \ No newline at end of file diff --git a/__tests__/app/sorting/[algorithm]/page.test.tsx b/__tests__/app/sorting/[algorithm]/page.test.tsx new file mode 100644 index 0000000..3949c7e --- /dev/null +++ b/__tests__/app/sorting/[algorithm]/page.test.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import AlgorithmPage from "@/app/sorting/[algorithm]/page"; +import { AlgorithmProvider } from "@/context/AlgorithmContext"; +import { getAlgorithmByName } from "@/lib/algorithms"; + +// Mock the next/navigation module +jest.mock("next/navigation", () => ({ + useParams: jest.fn(() => ({ algorithm: "bubbleSort" })), + notFound: jest.fn(), +})); + +// Mock the algorithms module +jest.mock("@/lib/algorithms", () => ({ + getAlgorithmByName: jest.fn().mockReturnValue(() => ({ + steps: [ + { array: [5, 3, 8], comparing: [], swapped: false, completed: [] }, + { array: [3, 5, 8], comparing: [], swapped: false, completed: [2] }, + ], + name: "Bubble Sort", + key: "bubbleSort", + category: "sorting", + description: "A simple sorting algorithm", + timeComplexity: "O(n²)", + spaceComplexity: "O(1)", + reference: "https://en.wikipedia.org/wiki/Bubble_sort", + pseudoCode: ["procedure bubbleSort(A: list of sortable items)"], + })), + availableAlgorithms: [ + { + name: "Bubble Sort", + key: "bubbleSort", + category: "sorting", + description: "A simple sorting algorithm", + difficulty: "easy", + }, + ], +})); + +// Mock the PageLayout component +jest.mock("@/components/layout/PageLayout", () => { + return ({ children, title, subtitle }: any) => ( +
+

{title}

+

{subtitle}

+
{children}
+
+ ); +}); + +// Mock the AlgorithmVisualizer component +jest.mock("@/components/visualizer/AlgorithmVisualizer", () => { + return () => ( +
Algorithm Visualizer Mock
+ ); +}); + +describe("AlgorithmPage", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should render the algorithm page with correct title and description", () => { + render( + + + + ); + + expect(screen.getByTestId("page-title")).toHaveTextContent("Bubble Sort"); + expect(screen.getByTestId("page-subtitle")).toHaveTextContent( + "A simple sorting algorithm" + ); + }); + + it("should render the AlgorithmVisualizer component", () => { + render( + + + + ); + + expect(screen.getByTestId("algorithm-visualizer")).toBeInTheDocument(); + }); + + it("should render algorithm not found message for invalid algorithm", () => { + // Mock the useParams to return an invalid algorithm + require("next/navigation").useParams.mockReturnValue({ + algorithm: "invalidAlgorithm", + }); + + // Mock the getAlgorithmByName to return null for invalid algorithm + require("@/lib/algorithms").getAlgorithmByName.mockReturnValue(null); + + // Mock the availableAlgorithms to not include the invalid algorithm + require("@/lib/algorithms").availableAlgorithms = []; + + render( + + + + ); + + // Use queryAllByText to handle multiple matches and check the first occurrence + const notFoundElements = screen.queryAllByText("Algorithm Not Found"); + expect(notFoundElements.length).toBeGreaterThan(0); + expect(notFoundElements[0]).toBeInTheDocument(); + }); + + it("should call getAlgorithmByName with the correct algorithm key", () => { + // Reset the useParams mock to return bubbleSort + require("next/navigation").useParams.mockReturnValue({ + algorithm: "bubbleSort", + }); + + // Reset algorithm mock data + require("@/lib/algorithms").availableAlgorithms = [ + { + name: "Bubble Sort", + key: "bubbleSort", + category: "sorting", + description: "A simple sorting algorithm", + difficulty: "easy", + }, + ]; + + // Clear previous calls + getAlgorithmByName.mockClear(); + + render( + + + + ); + + // Check that getAlgorithmByName was called with the right algorithm key + expect(getAlgorithmByName).toHaveBeenCalledWith("bubbleSort"); + }); +}); diff --git a/__tests__/components/AlgorithmCard.test.tsx b/__tests__/components/AlgorithmCard.test.tsx new file mode 100644 index 0000000..44a3b2e --- /dev/null +++ b/__tests__/components/AlgorithmCard.test.tsx @@ -0,0 +1,58 @@ +// __tests__/components/AlgorithmCard.test.tsx +import React from "react"; +import { render, screen } from "@testing-library/react"; +import AlgorithmCard from "@/components/AlgorithmCard"; +import { AlgorithmInfo } from "@/lib/types"; + +// Mock the Next.js Link component +jest.mock("next/link", () => { + return ({ children, href }: { children: React.ReactNode; href: string }) => { + return {children}; + }; +}); + +describe("AlgorithmCard", () => { + const mockAlgorithm: AlgorithmInfo = { + name: "Bubble Sort", + key: "bubbleSort", + category: "sorting", + description: + "A simple sorting algorithm that repeatedly steps through the list.", + difficulty: "easy", + }; + + it("should render the algorithm name", () => { + render(); + expect(screen.getByText("Bubble Sort")).toBeInTheDocument(); + }); + + it("should render the algorithm description", () => { + render(); + expect( + screen.getByText( + "A simple sorting algorithm that repeatedly steps through the list." + ) + ).toBeInTheDocument(); + }); + + it("should contain a link to the algorithm visualization page", () => { + render(); + const visualizeLink = screen.getByText("Visualize"); + expect(visualizeLink.closest("a")).toHaveAttribute( + "href", + "/sorting/bubbleSort" + ); + }); + + it("should contain a link to the difficulty page", () => { + render(); + const difficultyLink = screen.getByText("easy"); + expect(difficultyLink.closest("a")).toHaveAttribute("href", "/easy"); + }); + + it("should contain a link to the category page", () => { + render(); + const categoryLink = screen.getByText("sorting"); + expect(categoryLink.closest("a")).toHaveAttribute("href", "/sorting"); + }); +}); diff --git a/__tests__/components/visualizer/SortingVisualization.test.tsx b/__tests__/components/visualizer/SortingVisualization.test.tsx new file mode 100644 index 0000000..f7e4655 --- /dev/null +++ b/__tests__/components/visualizer/SortingVisualization.test.tsx @@ -0,0 +1,126 @@ +// __tests__/components/visualizer/SortingVisualization.test.tsx +import React from "react"; +import { render, screen } from "@testing-library/react"; +import SortingVisualization from "@/components/visualizer/SortingVisualization"; +import { SortingStep } from "@/lib/types"; + +describe("SortingVisualization", () => { + const createMockStep = ( + array: number[] = [1, 2, 3], + comparing: number[] = [], + swapped: boolean = false, + completed: number[] = [] + ): SortingStep => ({ + array, + comparing, + swapped, + completed, + }); + + it("should render the array elements", () => { + const mockStep = createMockStep([5, 10, 15]); + render(); + + expect(screen.getByText("5")).toBeInTheDocument(); + expect(screen.getByText("10")).toBeInTheDocument(); + expect(screen.getByText("15")).toBeInTheDocument(); + }); + + it("should apply different colors based on element state", () => { + // Create a step with one element comparing, one completed, and one normal + const mockStep = createMockStep( + [5, 10, 15], + [1], // comparing index 1 + false, + [2] // completed index 2 + ); + + const { container } = render( + + ); + + // Get all the bar elements + const bars = container.querySelectorAll(".bar-chart"); + expect(bars.length).toBe(3); + + // Check class names for color styles + expect(bars[0]).toHaveClass("bg-blue-400"); // Normal bar + expect(bars[1]).toHaveClass("bg-yellow-400"); // Comparing bar + expect(bars[2]).toHaveClass("bg-green-400"); // Completed bar + }); + + it("should apply the swapped color when comparing and swapped is true", () => { + // Create a step with elements being compared and swapped + const mockStep = createMockStep( + [5, 10, 15], + [0, 1], // comparing indices 0 and 1 + true, // swapped is true + [] + ); + + const { container } = render( + + ); + + // Get the bar elements + const bars = container.querySelectorAll(".bar-chart"); + + // Both comparing bars should have the swapped color + expect(bars[0]).toHaveClass("bg-red-400"); + expect(bars[1]).toHaveClass("bg-red-400"); + }); + + it("should render bars with heights proportional to their values", () => { + const mockStep = createMockStep([5, 10, 15]); + const { container } = render( + + ); + + // Get all the bar elements + const bars = container.querySelectorAll(".bar-chart"); + + // Check that heights are proportional to their values + // We need to be a bit flexible with the exact percentages as rounding can occur + const firstBarHeight = parseFloat(bars[0].style.height); + const secondBarHeight = parseFloat(bars[1].style.height); + const thirdBarHeight = parseFloat(bars[2].style.height); + + // Verify proper scaling (allow small margin for rounding) + expect(firstBarHeight).toBeGreaterThanOrEqual(32); + expect(firstBarHeight).toBeLessThanOrEqual(34); + + expect(secondBarHeight).toBeGreaterThanOrEqual(66); + expect(secondBarHeight).toBeLessThanOrEqual(68); + + expect(thirdBarHeight).toBeGreaterThanOrEqual(99); + expect(thirdBarHeight).toBeLessThanOrEqual(101); + + // Verify relative heights (proportions should be maintained) + expect(secondBarHeight).toBeCloseTo(firstBarHeight * 2, 0); + expect(thirdBarHeight).toBeCloseTo(firstBarHeight * 3, 0); + }); + + it("should adjust bar widths based on the number of elements", () => { + // Test with a small array + const smallStep = createMockStep([1, 2, 3]); + const { container: smallContainer } = render( + + ); + + // Test with a larger array + const largeStep = createMockStep([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + const { container: largeContainer } = render( + + ); + + // Get the bars from both renders + const smallBars = smallContainer.querySelectorAll(".bar-chart"); + const largeBars = largeContainer.querySelectorAll(".bar-chart"); + + // The width of bars in the small array should be wider than in the large array + const smallBarWidth = parseFloat(smallBars[0].style.width); + const largeBarWidth = parseFloat(largeBars[0].style.width); + + expect(smallBarWidth).toBeGreaterThan(largeBarWidth); + }); +}); diff --git a/__tests__/components/visualizer/VisualizerControls.test.tsx b/__tests__/components/visualizer/VisualizerControls.test.tsx new file mode 100644 index 0000000..6ea2173 --- /dev/null +++ b/__tests__/components/visualizer/VisualizerControls.test.tsx @@ -0,0 +1,159 @@ +// __tests__/components/visualizer/VisualizerControls.test.tsx +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import VisualizerControls from "@/components/visualizer/VisualizerControls"; +import { AlgorithmProvider, useAlgorithm } from "@/context/AlgorithmContext"; + +// Mock the algorithms module +jest.mock("@/lib/algorithms", () => ({ + getAlgorithmByName: jest.fn().mockReturnValue(() => ({ + steps: [ + { array: [1, 2, 3], comparing: [], swapped: false, completed: [] }, + { array: [1, 2, 3], comparing: [0, 1], swapped: false, completed: [] }, + { array: [1, 2, 3], comparing: [], swapped: false, completed: [0] }, + ], + name: "Test Algorithm", + key: "testAlgorithm", + category: "sorting", + description: "Test description", + timeComplexity: "O(n)", + spaceComplexity: "O(1)", + reference: "test-reference", + pseudoCode: ["test pseudocode"], + })), +})); + +// Mock utility functions +jest.mock("@/lib/utils", () => ({ + saveState: jest.fn(), + loadState: jest.fn().mockReturnValue(null), + generateRandomArray: jest.fn().mockReturnValue([4, 2, 1]), +})); + +// Wrapper component to provide context and access props +const TestWrapper = ({ + onGenerateNewArray, +}: { + onGenerateNewArray: () => void; +}) => { + const { state } = useAlgorithm(); + + return ( + + ); +}; + +describe("VisualizerControls", () => { + // Create a mock for the generateNewArray function + const mockGenerateNewArray = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should render control buttons", () => { + render( + + + + ); + + // Check that basic control buttons are rendered + expect(screen.getByText("Play")).toBeInTheDocument(); + expect(screen.getByText("Reset")).toBeInTheDocument(); + expect(screen.getByText("New Array")).toBeInTheDocument(); + }); + + it("should update state when play button is clicked", () => { + render( + + + + ); + + // Click the play button + fireEvent.click(screen.getByText("Play")); + + // The play button should be replaced with a pause button + expect(screen.getByText("Pause")).toBeInTheDocument(); + expect(screen.queryByText("Play")).not.toBeInTheDocument(); + }); + + it("should call onGenerateNewArray when New Array button is clicked", () => { + render( + + + + ); + + // Click the New Array button + fireEvent.click(screen.getByText("New Array")); + + // Check that the callback was called + expect(mockGenerateNewArray).toHaveBeenCalledTimes(1); + }); + + it("should display the correct step information", async () => { + render( + + + + ); + + // Wait for initialization + const stepText = await screen.findByText(/Step 1 of/); + expect(stepText).toBeInTheDocument(); + }); + + it("should update step when progress slider is changed", () => { + render( + + + + ); + + // Get the progress slider and change its value + const slider = screen.getByLabelText("Progress"); + fireEvent.change(slider, { target: { value: 1 } }); + + // Check that step information has updated (step 2 of 3) + expect(screen.getByText(/Step 2 of/)).toBeInTheDocument(); + }); + + it("should disable next button when on the last step", async () => { + render( + + + + ); + + // Move to the last step + const slider = screen.getByLabelText("Progress"); + fireEvent.change(slider, { target: { value: 2 } }); + + // Get the next button and check that it's disabled + const nextButton = screen.getByLabelText("Next"); + expect(nextButton).toBeDisabled(); + }); + + it("should disable prev button when on the first step", async () => { + render( + + + + ); + + // Should already be on the first step + const prevButton = screen.getByLabelText("Previous"); + expect(prevButton).toBeDisabled(); + + // Move to a later step and check that the button is enabled + const slider = screen.getByLabelText("Progress"); + fireEvent.change(slider, { target: { value: 1 } }); + expect(prevButton).not.toBeDisabled(); + }); +}); diff --git a/__tests__/context/AlgorithmContext.test.tsx b/__tests__/context/AlgorithmContext.test.tsx new file mode 100644 index 0000000..b04bf67 --- /dev/null +++ b/__tests__/context/AlgorithmContext.test.tsx @@ -0,0 +1,342 @@ +// __tests__/context/AlgorithmContext.test.tsx +import React from "react"; +import { render, screen, waitFor, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { AlgorithmProvider, useAlgorithm } from "@/context/AlgorithmContext"; +import { getAlgorithmByName } from "@/lib/algorithms"; + +// Mock the window.localStorage before importing context +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: jest.fn((key: string) => store[key] || null), + setItem: jest.fn((key: string, value: string) => { + store[key] = value; + }), + clear: jest.fn(() => { + store = {}; + }), + removeItem: jest.fn((key: string) => { + delete store[key]; + }), + }; +})(); + +// Replace the window.localStorage object with our mock +Object.defineProperty(window, "localStorage", { + value: localStorageMock, + writable: true, +}); + +// Mock the algorithms module +jest.mock("@/lib/algorithms", () => ({ + getAlgorithmByName: jest.fn().mockReturnValue(() => ({ + steps: [ + { array: [1, 2, 3], comparing: [], swapped: false, completed: [] }, + { array: [1, 2, 3], comparing: [0, 1], swapped: false, completed: [] }, + { array: [1, 2, 3], comparing: [], swapped: false, completed: [0] }, + ], + name: "Test Algorithm", + key: "testAlgorithm", + category: "sorting", + description: "Test description", + timeComplexity: "O(n)", + spaceComplexity: "O(1)", + reference: "test-reference", + pseudoCode: ["test pseudocode"], + })), +})); + +// Mock utility functions +jest.mock("@/lib/utils", () => { + const originalModule = jest.requireActual("@/lib/utils"); + + return { + ...originalModule, + saveState: jest.fn(), + loadState: jest.fn().mockReturnValue({ + data: [5, 3, 8], + speed: 3, + algorithm: "testAlgorithm", + }), + generateRandomArray: jest.fn().mockReturnValue([4, 2, 1]), + }; +}); + +// Test component that uses the algorithm context +const TestComponent = () => { + const { state, dispatch } = useAlgorithm(); + + return ( +
+
{state.currentStep}
+
{state.algorithm}
+
{state.speed}
+
+ {state.isPlaying ? "playing" : "paused"} +
+
{state.data?.join(",") || "No data"}
+
+ {state.visualizationData ? "Has Viz Data" : "No Viz Data"} +
+ + + + + + + + + + + + +
+ ); +}; + +describe("AlgorithmContext", () => { + // Reset timers and mocks between tests + beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + it("should initialize with the correct values from localStorage", async () => { + const user = userEvent.setup({ delay: null }); + + render( + + + + ); + + // Wait for initial state to be loaded + await waitFor(() => { + expect(screen.getByTestId("algorithm")).toHaveTextContent( + "testAlgorithm" + ); + }); + + // Check all state values + expect(screen.getByTestId("speed")).toHaveTextContent("3"); + expect(screen.getByTestId("data")).toHaveTextContent("5,3,8"); + + // Verify visualization data is generated during initialization + await waitFor(() => { + expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); + }); + }); + + it("should update step when next button is clicked", async () => { + const user = userEvent.setup({ delay: null }); + + render( + + + + ); + + // Wait for context to be initialized + await waitFor(() => { + expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); + }); + + // Get initial step + expect(screen.getByTestId("current-step")).toHaveTextContent("0"); + + // Click the next button to increment the step + await user.click(screen.getByTestId("next-btn")); + + // Wait for the state update to be processed + await waitFor(() => { + expect(screen.getByTestId("current-step")).toHaveTextContent("1"); + }); + }); + + it("should toggle playing state", async () => { + const user = userEvent.setup({ delay: null }); + + render( + + + + ); + + // Wait for context to be initialized + await waitFor(() => { + expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); + }); + + // Initially paused + expect(screen.getByTestId("is-playing")).toHaveTextContent("paused"); + + // Click play + await user.click(screen.getByTestId("play-btn")); + + // Wait for state update + await waitFor(() => { + expect(screen.getByTestId("is-playing")).toHaveTextContent("playing"); + }); + + // Click pause + await user.click(screen.getByTestId("pause-btn")); + + // Wait for state update + await waitFor(() => { + expect(screen.getByTestId("is-playing")).toHaveTextContent("paused"); + }); + }); + + it("should reset to step 0", async () => { + const user = userEvent.setup({ delay: null }); + + render( + + + + ); + + // Wait for context to be initialized + await waitFor(() => { + expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); + }); + + // Move to step 1 + await user.click(screen.getByTestId("next-btn")); + + // Wait for the state update to be processed + await waitFor(() => { + expect(screen.getByTestId("current-step")).toHaveTextContent("1"); + }); + + // Reset to step 0 + await user.click(screen.getByTestId("reset-btn")); + + // Wait for the state update to be processed + await waitFor(() => { + expect(screen.getByTestId("current-step")).toHaveTextContent("0"); + }); + }); + + it("should automatically advance steps when playing", async () => { + const user = userEvent.setup({ delay: null }); + + render( + + + + ); + + // Wait for context to be initialized + await waitFor(() => { + expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); + }); + + // Start playing + await user.click(screen.getByTestId("play-btn")); + + // Wait for state update + await waitFor(() => { + expect(screen.getByTestId("is-playing")).toHaveTextContent("playing"); + }); + + // Fast-forward time to trigger the interval + act(() => { + jest.advanceTimersByTime(1000); // Advance by 1 second + }); + + // Check that the step has advanced + await waitFor(() => { + expect(screen.getByTestId("current-step")).toHaveTextContent("1"); + }); + }); + + it("should generate new random data", async () => { + const user = userEvent.setup({ delay: null }); + + render( + + + + ); + + // Wait for context to be initialized + await waitFor(() => { + expect(screen.getByTestId("viz-data")).toHaveTextContent("Has Viz Data"); + expect(screen.getByTestId("data")).toHaveTextContent("5,3,8"); + }); + + // Reset mocks to track new calls + jest.clearAllMocks(); + + // Generate new data + await user.click(screen.getByTestId("new-data-btn")); + + // Wait for state to update + await waitFor(() => { + expect(screen.getByTestId("data")).toHaveTextContent("4,2,1"); + }); + }); +}); diff --git a/__tests__/lib/algorithms/bubbleSort.test.ts b/__tests__/lib/algorithms/bubbleSort.test.ts new file mode 100644 index 0000000..784e079 --- /dev/null +++ b/__tests__/lib/algorithms/bubbleSort.test.ts @@ -0,0 +1,82 @@ +// __tests__/lib/algorithms/bubbleSort.test.ts +import { bubbleSort } from "@/lib/algorithms/bubbleSort"; + +describe("Bubble Sort Algorithm", () => { + it("should return the correct visualization structure", () => { + const testArray = [5, 3, 8, 4, 2]; + const result = bubbleSort(testArray); + + // Check basic structure + expect(result).toHaveProperty("steps"); + expect(result).toHaveProperty("name", "Bubble Sort"); + expect(result).toHaveProperty("key", "bubbleSort"); + expect(result).toHaveProperty("category", "sorting"); + expect(result).toHaveProperty("timeComplexity", "O(n²)"); + expect(result).toHaveProperty("spaceComplexity", "O(1)"); + expect(result).toHaveProperty("pseudoCode"); + expect(Array.isArray(result.pseudoCode)).toBe(true); + }); + + it("should preserve the original array (not mutate it)", () => { + const testArray = [5, 3, 8, 4, 2]; + const originalArray = [...testArray]; + + bubbleSort(testArray); + + expect(testArray).toEqual(originalArray); + }); + + it("should correctly sort the array by the final step", () => { + const testArray = [5, 3, 8, 4, 2]; + const sortedArray = [...testArray].sort((a, b) => a - b); + + const result = bubbleSort(testArray); + const finalStepArray = result.steps[result.steps.length - 1].array; + + expect(finalStepArray).toEqual(sortedArray); + }); + + it("should handle arrays of length 1", () => { + const testArray = [1]; + const result = bubbleSort(testArray); + + expect(result.steps.length).toBeGreaterThan(0); + expect(result.steps[0].array).toEqual([1]); + expect(result.steps[result.steps.length - 1].array).toEqual([1]); + }); + + it("should handle empty arrays", () => { + const testArray: number[] = []; + const result = bubbleSort(testArray); + + expect(result.steps.length).toBeGreaterThan(0); + expect(result.steps[0].array).toEqual([]); + }); + + it("should handle already sorted arrays", () => { + const testArray = [1, 2, 3, 4, 5]; + const result = bubbleSort(testArray); + + // Check that the final state is the same as the initial sorted array + expect(result.steps[result.steps.length - 1].array).toEqual(testArray); + }); + + it("should mark elements as completed in the correct order", () => { + const testArray = [5, 3, 8, 4, 2]; + const result = bubbleSort(testArray); + + // In bubble sort, elements are completed from the end + // Check that the last step has all elements marked as completed + const finalStep = result.steps[result.steps.length - 1]; + expect(finalStep.completed.length).toBeGreaterThan(0); + + // The number of elements marked as completed should increase through the steps + let prevCompletedCount = -1; + for (const step of result.steps) { + if (step.completed.length > prevCompletedCount) { + prevCompletedCount = step.completed.length; + } + expect(step.completed.length).toBeGreaterThanOrEqual(prevCompletedCount); + } + }); +}); diff --git a/__tests__/lib/utils.test.ts b/__tests__/lib/utils.test.ts new file mode 100644 index 0000000..e2dc826 --- /dev/null +++ b/__tests__/lib/utils.test.ts @@ -0,0 +1,80 @@ +import { + generateRandomArray, + getAlgorithmLabel, + getDifficulty, +} from "@/lib/utils"; + +// We need to mock localStorage before importing the module +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: jest.fn((key: string) => store[key] || null), + setItem: jest.fn((key: string, value: string) => { + store[key] = value; + }), + clear: jest.fn(() => { + store = {}; + }), + removeItem: jest.fn((key: string) => { + delete store[key]; + }), + }; +})(); + +// Replace the window.localStorage object with our mock +Object.defineProperty(window, "localStorage", { + value: localStorageMock, + writable: true, +}); + +describe("Utils Functions", () => { + // Reset mocks before each test + beforeEach(() => { + jest.clearAllMocks(); + localStorageMock.clear(); + }); + + describe("generateRandomArray", () => { + it("should generate a random array with the specified length", () => { + const array = generateRandomArray(10, 50, 5); + + expect(array.length).toBe(5); + array.forEach((value) => { + expect(value).toBeGreaterThanOrEqual(10); + expect(value).toBeLessThanOrEqual(50); + }); + }); + + it("should generate different arrays on subsequent calls", () => { + // Note: There's a small chance this test could fail if the random arrays happen to be identical + const array1 = generateRandomArray(1, 1000, 10); + const array2 = generateRandomArray(1, 1000, 10); + + expect(array1).not.toEqual(array2); + }); + }); + + describe("getAlgorithmLabel", () => { + it("should return the correct label for known algorithms", () => { + expect(getAlgorithmLabel("bubbleSort")).toBe("Bubble Sort"); + expect(getAlgorithmLabel("quickSort")).toBe("Quick Sort"); + expect(getAlgorithmLabel("mergeSort")).toBe("Merge Sort"); + }); + + it("should return the algorithm key for unknown algorithms", () => { + expect(getAlgorithmLabel("unknownAlgorithm")).toBe("unknownAlgorithm"); + }); + }); + + describe("getDifficulty", () => { + it("should return the correct difficulty for known algorithms", () => { + expect(getDifficulty("bubbleSort")).toBe("Easy"); + expect(getDifficulty("mergeSort")).toBe("Medium"); + expect(getDifficulty("heapSort")).toBe("Hard"); + }); + + it('should return "Unknown" for unknown algorithms', () => { + expect(getDifficulty("unknownAlgorithm")).toBe("Unknown"); + }); + }); +}); diff --git a/__tests__/utils/test-utils.tsx b/__tests__/utils/test-utils.tsx new file mode 100644 index 0000000..b7e4a15 --- /dev/null +++ b/__tests__/utils/test-utils.tsx @@ -0,0 +1,35 @@ +// __tests__/utils/test-utils.tsx +import React, { ReactElement } from "react"; +import { render, RenderOptions } from "@testing-library/react"; +import { AlgorithmProvider } from "@/context/AlgorithmContext"; + +// Custom render function that includes the AlgorithmProvider +const customRender = ( + ui: ReactElement, + options?: Omit +) => { + return render(ui, { + wrapper: ({ children }) => ( + {children} + ), + ...options, + }); +}; + +// Re-export everything from testing-library +export * from "@testing-library/react"; + +// Override the render method +export { customRender as render }; + +// Mock for window.fs.readFile +export const mockReadFile = (content: string | ArrayBuffer) => { + window.fs = { + readFile: jest.fn().mockResolvedValue(content), + }; +}; + +// Helper to generate random test arrays +export const generateTestArray = (length: number) => { + return Array.from({ length }, (_, i) => i + 1); +}; diff --git a/app/[difficulty]/page.tsx b/app/[difficulty]/page.tsx index e8443c9..73517cd 100644 --- a/app/[difficulty]/page.tsx +++ b/app/[difficulty]/page.tsx @@ -46,7 +46,7 @@ export default async function DifficultyPage(props: DifficultyParams) { No algorithms found with {difficulty} difficulty.

- We're working on adding more algorithms soon! + We're working on adding more algorithms soon!

)} diff --git a/components/AlgorithmCard.tsx b/components/AlgorithmCard.tsx index b2af3e6..09bc4b0 100644 --- a/components/AlgorithmCard.tsx +++ b/components/AlgorithmCard.tsx @@ -8,20 +8,13 @@ interface AlgorithmCardProps { export default function AlgorithmCard({ algorithm }: AlgorithmCardProps) { const { name, key, category, description, difficulty } = algorithm; - // Determine badge color based on difficulty - const badgeClass = { - easy: "badge-easy", - medium: "badge-medium", - hard: "badge-hard", - }[difficulty]; - return (

{name}

{difficulty} diff --git a/components/SortingVisualization.tsx b/components/SortingVisualization.tsx index fc872c2..da18df6 100644 --- a/components/SortingVisualization.tsx +++ b/components/SortingVisualization.tsx @@ -12,27 +12,28 @@ export default function SortingVisualization({ const { array, comparing, swapped, completed } = step; return ( -
+
{array.map((value, index) => { const height = (value / maxValue) * 100; - let backgroundColor = "bg-blue-500"; + // Determine the bar color based on its state + let barColor = "bg-blue-400"; if (completed.includes(index)) { - backgroundColor = "bg-green-500"; + barColor = "bg-green-400"; } else if (comparing.includes(index)) { - backgroundColor = swapped ? "bg-red-500" : "bg-yellow-500"; + barColor = swapped ? "bg-red-400" : "bg-yellow-400"; } return (
-
{value}
+
{value}
); })} diff --git a/components/seo/JsonLd.tsx b/components/seo/JsonLd.tsx index ba128f9..729698e 100644 --- a/components/seo/JsonLd.tsx +++ b/components/seo/JsonLd.tsx @@ -1,6 +1,44 @@ import React from "react"; import { AlgorithmVisualization } from "@/lib/types"; +interface BaseJsonLd { + "@context": string; + "@type": string; + name: string; + description: string; + url: string; + author: { + "@type": string; + name: string; + url: string; + }; + publisher: { + "@type": string; + name: string; + url: string; + }; + datePublished: string; + dateModified: string; + inLanguage: string; +} + +// Algorithm-specific JSON-LD properties +interface AlgorithmJsonLd extends BaseJsonLd { + "@type": "Algorithm"; + programmingLanguage: string; + codeSampleType: string; + runtimePlatform: string; + algorithmCategory: string; + timeComplexity: string; + spaceComplexity: string; + educationalUse: string; + citation?: string; +} + +// Discriminated union of possible JSON-LD types +type SchemaJsonLd = BaseJsonLd | AlgorithmJsonLd; + +// Component props type JsonLdProps = { algorithmData?: AlgorithmVisualization; url: string; @@ -17,7 +55,7 @@ export default function JsonLd({ description, }: JsonLdProps) { // Base WebPage or WebApplication JSON-LD - let jsonLd: any = { + let jsonLd: SchemaJsonLd = { "@context": "https://schema.org", "@type": type, name: name || "Algorithm Visualizer", @@ -44,6 +82,7 @@ export default function JsonLd({ if (algorithmData && type === "Algorithm") { jsonLd = { ...jsonLd, + "@type": "Algorithm", // explicitly set the type name: algorithmData.name, description: algorithmData.description, programmingLanguage: "JavaScript/TypeScript", @@ -54,7 +93,7 @@ export default function JsonLd({ spaceComplexity: algorithmData.spaceComplexity, educationalUse: "Teaching/Learning", citation: algorithmData.reference, - }; + } as AlgorithmJsonLd; } return ( diff --git a/components/visualizer/AlgorithmInfo.tsx b/components/visualizer/AlgorithmInfo.tsx index cf52e85..f743ac9 100644 --- a/components/visualizer/AlgorithmInfo.tsx +++ b/components/visualizer/AlgorithmInfo.tsx @@ -54,6 +54,8 @@ export default function AlgorithmInfo({ algorithm }: AlgorithmInfoProps) { Wikipedia diff --git a/components/visualizer/AlgorithmVisualizer.tsx b/components/visualizer/AlgorithmVisualizer.tsx index 136978b..adfe981 100644 --- a/components/visualizer/AlgorithmVisualizer.tsx +++ b/components/visualizer/AlgorithmVisualizer.tsx @@ -1,6 +1,6 @@ "use client"; -import SortingVisualization from "../SortingVisualization"; +import SortingVisualization from "./SortingVisualization"; import VisualizerControls from "./VisualizerControls"; import AlgorithmInfo from "./AlgorithmInfo"; import AlgorithmPseudocode from "./AlgorithmPseudocode"; diff --git a/components/visualizer/VisualizerControls.tsx b/components/visualizer/VisualizerControls.tsx index 826d1bb..afcd3b6 100644 --- a/components/visualizer/VisualizerControls.tsx +++ b/components/visualizer/VisualizerControls.tsx @@ -35,6 +35,7 @@ export default function VisualizerControls({ onClick={handlePrev} disabled={currentStep <= 0 || isPlaying} className="btn btn-primary disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed" + aria-label="Previous" > = totalSteps - 1 || isPlaying} className="btn btn-primary disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed" + aria-label="Next" >
-