This commit is contained in:
KS Jannette
2026-02-12 07:28:55 -05:00
parent 48fef7d077
commit 0800f90ff5
8 changed files with 4763 additions and 3385 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,40 @@
{ {
"name": "congruity-frontend", "name": "congruity-frontend",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview",
}, "test": "vitest run --config vitest.config.ts",
"dependencies": { "test:watch": "vitest --config vitest.config.ts"
"@tanstack/react-query": "^5.90.21", },
"react": "^19.2.0", "dependencies": {
"react-dom": "^19.2.0", "@tanstack/react-query": "^5.90.21",
"react-router-dom": "^7.13.0" "react": "^19.2.0",
}, "react-dom": "^19.2.0",
"devDependencies": { "react-router-dom": "^7.13.0"
"@eslint/js": "^9.39.1", },
"@types/node": "^24.10.1", "devDependencies": {
"@types/react": "^19.2.7", "@eslint/js": "^9.39.1",
"@types/react-dom": "^19.2.3", "@testing-library/jest-dom": "^6.9.1",
"@vitejs/plugin-react": "^5.1.1", "@testing-library/react": "^16.3.2",
"eslint": "^9.39.1", "@testing-library/user-event": "^14.6.1",
"eslint-plugin-react-hooks": "^7.0.1", "@types/node": "^24.10.1",
"eslint-plugin-react-refresh": "^0.4.24", "@types/react": "^19.2.7",
"globals": "^16.5.0", "@types/react-dom": "^19.2.3",
"typescript": "~5.9.3", "@vitejs/plugin-react": "^5.1.1",
"typescript-eslint": "^8.48.0", "eslint": "^9.39.1",
"vite": "^7.3.1" "eslint-plugin-react-hooks": "^7.0.1",
} "eslint-plugin-react-refresh": "^0.4.24",
} "globals": "^16.5.0",
"jsdom": "^28.0.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
}

View File

@@ -0,0 +1,46 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from '../App';
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchMock = vi.fn();
globalThis.fetch = fetchMock as unknown as typeof fetch;
fetchMock.mockReturnValue(
Promise.resolve({ ok: true, json: () => Promise.resolve([]) } as Response)
);
});
afterEach(() => {
vi.restoreAllMocks();
});
const renderApp = (initialRoute = '/') => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialRoute]}>
<App />
</MemoryRouter>
</QueryClientProvider>
);
};
describe('App routing', () => {
it('should render the Home page on "/"', async () => {
renderApp('/');
expect(await screen.findByText('kohngrüti')).toBeInTheDocument();
});
it('should redirect unknown routes to Home', async () => {
renderApp('/some/random/path');
expect(await screen.findByText('kohngrüti')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,27 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from '../components/button';
describe('Button', () => {
it('should render with the provided label', () => {
render(<Button onClick={() => {}} isLoading={false} label="Click Me" />);
expect(screen.getByRole('button', { name: 'Click Me' })).toBeInTheDocument();
});
it('should show "Working..." when isLoading is true', () => {
render(<Button onClick={() => {}} isLoading={true} label="Click Me" />);
expect(screen.getByRole('button', { name: 'Working...' })).toBeInTheDocument();
});
it('should call onClick when clicked', async () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick} isLoading={false} label="Click Me" />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledOnce();
});
});

View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';

View File

@@ -0,0 +1,107 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Stickies from '../components/stickies';
import { createTestWrapper } from './test-wrapper';
const MOCK_STICKIES = [
{ id: 'note_001', text: 'Login flow feels confusing', x: 193, y: 191, author: 'user_5', color: 'yellow' },
{ id: 'note_002', text: 'Export takes too long', x: 798, y: 211, author: 'user_2', color: 'green' },
];
const MOCK_CLUSTERS = [
{ label: 'Auth Issues', noteIds: ['note_001'] },
{ label: 'Export Issues', noteIds: ['note_002'] },
];
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchMock = vi.fn();
globalThis.fetch = fetchMock as unknown as typeof fetch;
});
afterEach(() => {
vi.restoreAllMocks();
});
const mockFetchOk = (data: unknown) =>
Promise.resolve({ ok: true, json: () => Promise.resolve(data) } as Response);
const mockFetchFail = () =>
Promise.resolve({ ok: false, status: 500, json: () => Promise.resolve({}) } as Response);
describe('Stickies', () => {
it('should show a loading indicator while fetching notes', () => {
// Fetch never resolves — stays in loading state
fetchMock.mockReturnValue(new Promise(() => {}));
const { Wrapper } = createTestWrapper();
render(<Stickies />, { wrapper: Wrapper });
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should render all sticky notes after fetch succeeds', async () => {
fetchMock.mockReturnValue(mockFetchOk(MOCK_STICKIES));
const { Wrapper } = createTestWrapper();
render(<Stickies />, { wrapper: Wrapper });
expect(await screen.findByText('Login flow feels confusing')).toBeInTheDocument();
expect(screen.getByText('Export takes too long')).toBeInTheDocument();
});
it('should render the cluster button', async () => {
fetchMock.mockReturnValue(mockFetchOk(MOCK_STICKIES));
const { Wrapper } = createTestWrapper();
render(<Stickies />, { wrapper: Wrapper });
expect(await screen.findByRole('button', { name: 'Group Stickies By Topic' })).toBeInTheDocument();
});
it('should show an error message when fetching notes fails', async () => {
fetchMock.mockReturnValue(mockFetchFail());
const { Wrapper } = createTestWrapper();
render(<Stickies />, { wrapper: Wrapper });
expect(await screen.findByText(/Error:/)).toBeInTheDocument();
});
it('should show cluster labels after clustering succeeds', async () => {
// First call = GET notes, second call = POST cluster
fetchMock
.mockReturnValueOnce(mockFetchOk(MOCK_STICKIES))
.mockReturnValueOnce(mockFetchOk(MOCK_CLUSTERS));
const { Wrapper } = createTestWrapper();
render(<Stickies />, { wrapper: Wrapper });
const btn = await screen.findByRole('button', { name: 'Group Stickies By Topic' });
await userEvent.click(btn);
await waitFor(() => {
expect(screen.getByText('Auth Issues')).toBeInTheDocument();
expect(screen.getByText('Export Issues')).toBeInTheDocument();
});
});
it('should still render sticky note text inside clusters', async () => {
fetchMock
.mockReturnValueOnce(mockFetchOk(MOCK_STICKIES))
.mockReturnValueOnce(mockFetchOk(MOCK_CLUSTERS));
const { Wrapper } = createTestWrapper();
render(<Stickies />, { wrapper: Wrapper });
const btn = await screen.findByRole('button', { name: 'Group Stickies By Topic' });
await userEvent.click(btn);
expect(await screen.findByText('Login flow feels confusing')).toBeInTheDocument();
expect(screen.getByText('Export takes too long')).toBeInTheDocument();
});
});

View File

@@ -5,7 +5,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"types": ["vite/client"], "types": ["vite/client", "@testing-library/jest-dom"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */

11
frontend/vitest.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/tests/setup.ts',
globals: true,
},
});