From 1af98389fdaa01320dd13b8e9930590978a0bf64 Mon Sep 17 00:00:00 2001 From: Fernando Nogueira Date: Sat, 30 May 2026 14:37:46 +0100 Subject: [PATCH 1/2] chore: add testing setup with vitest --- .tool-versions | 1 + bun.lockb | Bin 219803 -> 259285 bytes package.json | 12 +++- src/api.test.ts | 25 ++++++++ .../ProtectedRoute/ProtectedRoute.test.tsx | 34 +++++++++++ src/context/auth/axios.test.tsx | 55 ++++++++++++++++++ src/pages/repository/RepositoryPage.tsx | 37 ++++++++++-- src/routes.tsx | 45 +++++++++----- src/test/setup.ts | 25 ++++++++ vitest.config.ts | 12 ++++ 10 files changed, 222 insertions(+), 24 deletions(-) create mode 100644 .tool-versions create mode 100644 src/api.test.ts create mode 100644 src/components/ProtectedRoute/ProtectedRoute.test.tsx create mode 100644 src/context/auth/axios.test.tsx create mode 100644 src/test/setup.ts create mode 100644 vitest.config.ts diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..695dfec --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 24 diff --git a/bun.lockb b/bun.lockb index ea797668c7a5d9ec0977826669aa85afdb4dcf54..166595a1c4fac21ac1b505dd13afe52da7201e22 100755 GIT binary patch delta 59872 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 diff --git a/package.json b/package.json index fc599c5..2f58449 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "vite --port 3001", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" }, "dependencies": { "@ant-design/icons": "^6.0.0", @@ -26,6 +28,9 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react-swc": "^4.2.2", @@ -33,8 +38,11 @@ "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.5.0", "globals": "^17.0.0", + "happy-dom": "^20.9.0", + "jsdom": "^29.1.1", "typescript": "~6.0.0", "typescript-eslint": "^8.52.0", - "vite": "^8.0.0" + "vite": "^8.0.0", + "vitest": "^3" } } diff --git a/src/api.test.ts b/src/api.test.ts new file mode 100644 index 0000000..bc28927 --- /dev/null +++ b/src/api.test.ts @@ -0,0 +1,25 @@ +import {describe, it, expect, beforeEach} from 'vitest' +import {api} from './api' + +type RequestHandlers = Array<{ + fulfilled: (config: {headers: Record}) => {headers: Record} | Promise<{headers: Record}> +}> + +const requestHandlers = (api.interceptors.request as unknown as {handlers: RequestHandlers}).handlers + +describe('api axios instance', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('attaches Bearer token from localStorage to outgoing requests', async () => { + localStorage.setItem('authToken', 'test-jwt-token') + const config = await requestHandlers[0].fulfilled({headers: {}}) + expect(config.headers['Authorization']).toBe('Bearer test-jwt-token') + }) + + it('omits the Authorization header when no token is present', async () => { + const config = await requestHandlers[0].fulfilled({headers: {}}) + expect(config.headers['Authorization']).toBeUndefined() + }) +}) diff --git a/src/components/ProtectedRoute/ProtectedRoute.test.tsx b/src/components/ProtectedRoute/ProtectedRoute.test.tsx new file mode 100644 index 0000000..5990e5c --- /dev/null +++ b/src/components/ProtectedRoute/ProtectedRoute.test.tsx @@ -0,0 +1,34 @@ +import {describe, it, expect} from 'vitest' +import {render, screen} from '@testing-library/react' +import {MemoryRouter, Route, Routes} from 'react-router' +import {AuthStateContext} from '../../context/auth/context' +import type {AuthState} from '../../context/auth/types' +import {ProtectedRoute} from './ProtectedRoute' + +const renderWithAuth = (authState: AuthState, initialPath: string) => + render( + + + + }> + secret payload} /> + + login page} /> + + + + ) + +describe('ProtectedRoute', () => { + it('renders the child route when the user is authenticated', () => { + renderWithAuth({isAuthenticated: true, token: 'tkn'}, '/secret') + expect(screen.getByText('secret payload')).toBeInTheDocument() + expect(screen.queryByText('login page')).not.toBeInTheDocument() + }) + + it('redirects unauthenticated users to /login', () => { + renderWithAuth({isAuthenticated: false, token: undefined}, '/secret') + expect(screen.getByText('login page')).toBeInTheDocument() + expect(screen.queryByText('secret payload')).not.toBeInTheDocument() + }) +}) diff --git a/src/context/auth/axios.test.tsx b/src/context/auth/axios.test.tsx new file mode 100644 index 0000000..8827f59 --- /dev/null +++ b/src/context/auth/axios.test.tsx @@ -0,0 +1,55 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest' +import {renderHook} from '@testing-library/react' +import type {ReactNode} from 'react' +import useAxios from './axios' +import {AuthProvider} from './provider' + +describe('useAxios 401 handler', () => { + beforeEach(() => { + localStorage.clear() + }) + + const wrapper = ({children}: {children: ReactNode}) => ( + {children} + ) + + it('clears authState and redirects to /login on a 401 response', async () => { + localStorage.setItem('authState', JSON.stringify({isAuthenticated: true, token: 'tkn'})) + const assign = vi.fn() + vi.stubGlobal('location', {assign}) + + const {result} = renderHook(() => useAxios(), {wrapper}) + // `handlers` is an axios runtime internal not exposed in the public types. + const handlers = (result.current.interceptors.response as unknown as { + handlers: Array<{rejected?: (err: unknown) => unknown}> + }).handlers + const rejected = handlers[0].rejected! + + await expect(rejected({response: {status: 401}})).rejects.toBeDefined() + + expect(localStorage.getItem('authState')).toBeNull() + expect(assign).toHaveBeenCalledWith('/login') + + vi.unstubAllGlobals() + }) + + it('passes through non-401 errors without touching auth state', async () => { + localStorage.setItem('authState', JSON.stringify({isAuthenticated: true, token: 'tkn'})) + const assign = vi.fn() + vi.stubGlobal('location', {assign}) + + const {result} = renderHook(() => useAxios(), {wrapper}) + // `handlers` is an axios runtime internal not exposed in the public types. + const handlers = (result.current.interceptors.response as unknown as { + handlers: Array<{rejected?: (err: unknown) => unknown}> + }).handlers + const rejected = handlers[0].rejected! + + await expect(rejected({response: {status: 500}})).rejects.toBeDefined() + + expect(localStorage.getItem('authState')).not.toBeNull() + expect(assign).not.toHaveBeenCalled() + + vi.unstubAllGlobals() + }) +}) diff --git a/src/pages/repository/RepositoryPage.tsx b/src/pages/repository/RepositoryPage.tsx index 95b015e..0442a90 100644 --- a/src/pages/repository/RepositoryPage.tsx +++ b/src/pages/repository/RepositoryPage.tsx @@ -7,11 +7,24 @@ import {BarChartOutlined, GithubOutlined, HomeOutlined, LineChartOutlined, Reloa import {useQuery} from "@tanstack/react-query"; import {isOk} from "../../utils/axios.ts"; import useAxios from "../../context/auth/axios.ts"; -import {RepoConfigTab} from "./tabs/RepoConfigTab.tsx"; -import {RepoResultsTab} from "./tabs/RepoResultsTab.tsx"; -import {RepoTrendsTab} from "./tabs/RepoTrendsTab.tsx"; import {colors} from "../../theme/theme.ts"; +const RepoResultsTab = React.lazy(() => + import("./tabs/RepoResultsTab.tsx").then(m => ({default: m.RepoResultsTab})) +); +const RepoTrendsTab = React.lazy(() => + import("./tabs/RepoTrendsTab.tsx").then(m => ({default: m.RepoTrendsTab})) +); +const RepoConfigTab = React.lazy(() => + import("./tabs/RepoConfigTab.tsx").then(m => ({default: m.RepoConfigTab})) +); + +const TabFallback = () => ( +
+ +
+); + enum RepoPageTabs { RESULTS = 'results', TRENDS = 'trends', @@ -166,17 +179,29 @@ export const RepositoryPage: React.FC = () => { { key: RepoPageTabs.RESULTS, label: Results, - children: + children: ( + }> + + + ) }, { key: RepoPageTabs.TRENDS, label: Trends, - children: + children: ( + }> + + + ) }, { key: RepoPageTabs.CONFIGS, label: Settings, - children: + children: ( + }> + + + ) } ]} /> diff --git a/src/routes.tsx b/src/routes.tsx index 4f7804c..6a2a548 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,33 +1,46 @@ +import {lazy, Suspense} from "react"; import {Route, Routes} from "react-router"; +import {Spin} from "antd"; import {LoginPage} from "./pages/login/Login.tsx"; import {LoginSuccessPage} from "./pages/login/Success.tsx"; import {OrganizationsPage} from "./pages/organizations/OrganizationsPage.tsx"; -import RunResultPage from "./pages/run_result/RunResult.tsx"; import {RepositoriesPage} from "./pages/repositories/RepositoriesPage.tsx"; import {RepositoryPage} from "./pages/repository/RepositoryPage.tsx"; import {AppLayout} from "./components/AppLayout/AppLayout.tsx"; import {ProtectedRoute} from "./components/ProtectedRoute/ProtectedRoute.tsx"; import {RootRedirect} from "./components/RootRedirect/RootRedirect.tsx"; +// RunResultPage pulls in react-syntax-highlighter; defer it until a user +// actually navigates to a run. +const RunResultPage = lazy(() => import("./pages/run_result/RunResult.tsx")); + +const RouteFallback = () => ( +
+ +
+); + export const AppRoutes = () => { return ( - - } /> - }/> - }/> + }> + + } /> + }/> + }/> - {/* Authenticated routes with AppLayout */} - }> - }> - }/> - }/> - }/> - }/> - }/> + {/* Authenticated routes with AppLayout */} + }> + }> + }/> + }/> + }/> + }/> + }/> + - - Not Found}/> - + Not Found}/> + + ) } diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..2bf819e --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,25 @@ +import '@testing-library/jest-dom/vitest' +import {afterEach} from 'vitest' +import {cleanup} from '@testing-library/react' + +if (typeof globalThis.localStorage === 'undefined') { + class InMemoryStorage { + private store = new Map() + get length() { return this.store.size } + key(i: number) { return Array.from(this.store.keys())[i] ?? null } + getItem(k: string) { return this.store.get(k) ?? null } + setItem(k: string, v: string) { this.store.set(k, String(v)) } + removeItem(k: string) { this.store.delete(k) } + clear() { this.store.clear() } + } + const storage = new InMemoryStorage() + Object.defineProperty(globalThis, 'localStorage', {value: storage, writable: true, configurable: true}) + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'localStorage', {value: storage, writable: true, configurable: true}) + } +} + +afterEach(() => { + cleanup() + localStorage.clear() +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..80d6072 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,12 @@ +import {defineConfig} from 'vitest/config' +import react from '@vitejs/plugin-react-swc' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'happy-dom', + globals: true, + setupFiles: ['./src/test/setup.ts'], + css: false, + }, +}) From 5f65b2e9bb758d352e79b0558b13ad4ef52d482f Mon Sep 17 00:00:00 2001 From: Fernando Nogueira Date: Sat, 30 May 2026 14:53:33 +0100 Subject: [PATCH 2/2] chore: add testing setup with vitest --- .github/workflows/pr.yml | 8 ++++++++ .github/workflows/release.yml | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ae31c75..dd37084 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,6 +10,11 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: '.tool-versions' + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -21,6 +26,9 @@ jobs: - name: Lint run: bun run lint + - name: Test + run: bun run test:run + - name: Build run: bun run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a561551..755c324 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,11 @@ jobs: with: fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: '.tool-versions' + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: