From 5cf43976f6c0ca04cda9b85ae884beb027cc13f4 Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Fri, 6 Mar 2026 22:26:55 +0100 Subject: [PATCH] fix(frontend): resolve lint blockers for gitea frontend-ci --- frontend/e2e/auth.spec.ts | 4 +- frontend/e2e/shifts.spec.ts | 4 +- frontend/src/app/(protected)/layout.tsx | 2 - .../src/app/(protected)/tasks/[id]/page.tsx | 2 - frontend/src/auth/auth.ts | 2 +- .../components/__tests__/auth-guard.test.tsx | 32 ++++++------- .../__tests__/club-switcher.test.tsx | 14 +++--- .../__tests__/shift-detail.test.tsx | 10 ++-- .../components/__tests__/task-detail.test.tsx | 16 +++---- .../components/__tests__/task-list.test.tsx | 4 +- frontend/src/components/auth-guard.tsx | 2 +- frontend/src/contexts/tenant-context.tsx | 47 ++++++++++++------- .../src/hooks/__tests__/useActiveClub.test.ts | 9 ++-- frontend/src/hooks/useActiveClub.ts | 46 ++++++++++-------- frontend/src/lib/__tests__/api.test.ts | 6 +-- frontend/src/test/setup.ts | 8 ++-- 16 files changed, 112 insertions(+), 96 deletions(-) diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index 87a7e2c..2fe9b51 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -15,7 +15,7 @@ import { test, expect } from '@playwright/test'; /** * Robust club selection helper with fallback locators */ -async function selectClubIfPresent(page: any) { +async function selectClubIfPresent(page: import('@playwright/test').Page) { const isOnSelectClub = page.url().includes('/select-club'); if (!isOnSelectClub) { @@ -182,7 +182,7 @@ test.describe('Authentication Flow', () => { }); }); -async function authenticateUser(page: any, email: string, password: string) { +async function authenticateUser(page: import('@playwright/test').Page, email: string, password: string) { await page.goto('/login'); await page.click('button:has-text("Sign in with Keycloak")'); diff --git a/frontend/e2e/shifts.spec.ts b/frontend/e2e/shifts.spec.ts index 2e1aa31..ea02b0b 100644 --- a/frontend/e2e/shifts.spec.ts +++ b/frontend/e2e/shifts.spec.ts @@ -11,7 +11,7 @@ import { test, expect } from '@playwright/test'; * - Visual capacity indicators (progress bar, spot counts) */ -async function selectClubIfPresent(page: any) { +async function selectClubIfPresent(page: import('@playwright/test').Page) { const isOnSelectClub = page.url().includes('/select-club'); if (!isOnSelectClub) { @@ -51,7 +51,7 @@ async function selectClubIfPresent(page: any) { } } -async function loginAs(page: any, email: string, password: string) { +async function loginAs(page: import('@playwright/test').Page, email: string, password: string) { await page.goto('/login'); await page.click('button:has-text("Sign in with Keycloak")'); diff --git a/frontend/src/app/(protected)/layout.tsx b/frontend/src/app/(protected)/layout.tsx index 10c01b4..9e117af 100644 --- a/frontend/src/app/(protected)/layout.tsx +++ b/frontend/src/app/(protected)/layout.tsx @@ -1,8 +1,6 @@ import { AuthGuard } from '@/components/auth-guard'; import { ClubSwitcher } from '@/components/club-switcher'; import Link from 'next/link'; -import { Button } from '@/components/ui/button'; -import { LogOut } from 'lucide-react'; import { SignOutButton } from '@/components/sign-out-button'; export default function ProtectedLayout({ diff --git a/frontend/src/app/(protected)/tasks/[id]/page.tsx b/frontend/src/app/(protected)/tasks/[id]/page.tsx index 8e2b078..4d429dd 100644 --- a/frontend/src/app/(protected)/tasks/[id]/page.tsx +++ b/frontend/src/app/(protected)/tasks/[id]/page.tsx @@ -2,7 +2,6 @@ import { use } from 'react'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; import { useTask, useUpdateTask } from '@/hooks/useTasks'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -25,7 +24,6 @@ const statusColors: Record = { export default function TaskDetailPage({ params }: { params: Promise<{ id: string }> }) { const resolvedParams = use(params); - const router = useRouter(); const { data: task, isLoading, error } = useTask(resolvedParams.id); const { mutate: updateTask, isPending } = useUpdateTask(); diff --git a/frontend/src/auth/auth.ts b/frontend/src/auth/auth.ts index 5a685f4..b0f6926 100644 --- a/frontend/src/auth/auth.ts +++ b/frontend/src/auth/auth.ts @@ -31,7 +31,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ async jwt({ token, account }) { if (account) { // Add clubs claim from Keycloak access token - token.clubs = (account as any).clubs || {} + token.clubs = (account as Record).clubs as Record || {} token.accessToken = account.access_token } return token diff --git a/frontend/src/components/__tests__/auth-guard.test.tsx b/frontend/src/components/__tests__/auth-guard.test.tsx index 9de0101..eec936e 100644 --- a/frontend/src/components/__tests__/auth-guard.test.tsx +++ b/frontend/src/components/__tests__/auth-guard.test.tsx @@ -22,28 +22,28 @@ describe('AuthGuard', () => { beforeEach(() => { vi.clearAllMocks(); - (useRouter as any).mockReturnValue({ push: mockPush } as any); + (useRouter as ReturnType).mockReturnValue({ push: mockPush }); }); it('renders loading state when session is loading', () => { - (useSession as any).mockReturnValue({ data: null, status: 'loading' } as any); - (useTenant as any).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null }); + (useSession as ReturnType).mockReturnValue({ data: null, status: 'loading' }); + (useTenant as ReturnType).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null }); render(
Protected
); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('redirects to /login when unauthenticated', () => { - (useSession as any).mockReturnValue({ data: null, status: 'unauthenticated' } as any); - (useTenant as any).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null }); + (useSession as ReturnType).mockReturnValue({ data: null, status: 'unauthenticated' }); + (useTenant as ReturnType).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null }); render(
Protected
); expect(mockPush).toHaveBeenCalledWith('/login'); }); it('shows Contact admin when 0 clubs', () => { - (useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any); - (useTenant as any).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null }); + (useSession as ReturnType).mockReturnValue({ data: { user: {} }, status: 'authenticated' }); + (useTenant as ReturnType).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null }); render(
Protected
); expect(screen.getByText('Contact admin to get access to a club')).toBeInTheDocument(); @@ -51,39 +51,39 @@ describe('AuthGuard', () => { it('auto-selects when 1 club and no active club', () => { const mockSetActiveClub = vi.fn(); - (useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any); - (useTenant as any).mockReturnValue({ + (useSession as ReturnType).mockReturnValue({ data: { user: {} }, status: 'authenticated' }); + (useTenant as ReturnType).mockReturnValue({ activeClubId: null, clubs: [{ id: 'club-1', name: 'Club 1' }], setActiveClub: mockSetActiveClub, userRole: null - } as any); + }); render(
Protected
); expect(mockSetActiveClub).toHaveBeenCalledWith('club-1'); }); it('redirects to /select-club when multiple clubs and no active club', () => { - (useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any); - (useTenant as any).mockReturnValue({ + (useSession as ReturnType).mockReturnValue({ data: { user: {} }, status: 'authenticated' }); + (useTenant as ReturnType).mockReturnValue({ activeClubId: null, clubs: [{ id: 'club-1', name: 'Club 1' }, { id: 'club-2', name: 'Club 2' }], setActiveClub: vi.fn(), userRole: null - } as any); + }); render(
Protected
); expect(mockPush).toHaveBeenCalledWith('/select-club'); }); it('renders children when authenticated and active club is set', () => { - (useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any); - (useTenant as any).mockReturnValue({ + (useSession as ReturnType).mockReturnValue({ data: { user: {} }, status: 'authenticated' }); + (useTenant as ReturnType).mockReturnValue({ activeClubId: 'club-1', clubs: [{ id: 'club-1', name: 'Club 1' }], setActiveClub: vi.fn(), userRole: 'admin' - } as any); + }); render(
Protected Content
); expect(screen.getByText('Protected Content')).toBeInTheDocument(); diff --git a/frontend/src/components/__tests__/club-switcher.test.tsx b/frontend/src/components/__tests__/club-switcher.test.tsx index 2ae02bf..18b0b3a 100644 --- a/frontend/src/components/__tests__/club-switcher.test.tsx +++ b/frontend/src/components/__tests__/club-switcher.test.tsx @@ -9,7 +9,7 @@ vi.mock('../../contexts/tenant-context', () => ({ vi.mock('../ui/dropdown-menu', () => ({ DropdownMenu: ({ children }: { children: React.ReactNode }) =>
{children}
, - DropdownMenuTrigger: ({ children, asChild }: { children: React.ReactNode, asChild?: boolean }) =>
{children}
, + DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, DropdownMenuContent: ({ children }: { children: React.ReactNode }) =>
{children}
, DropdownMenuItem: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) =>
{children}
, DropdownMenuLabel: ({ children }: { children: React.ReactNode }) =>
{children}
, @@ -22,19 +22,19 @@ describe('ClubSwitcher', () => { }); it('renders loading state when clubs is empty', () => { - (useTenant as any).mockReturnValue({ + (useTenant as ReturnType).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null - } as any); + }); render(); expect(screen.getByRole('button')).toHaveTextContent('Select Club'); }); it('renders current club name and sport type badge', () => { - (useTenant as any).mockReturnValue({ + (useTenant as ReturnType).mockReturnValue({ activeClubId: 'club-1', clubs: [ { id: 'club-1', name: 'Tennis Club', sportType: 'Tennis' }, @@ -42,7 +42,7 @@ describe('ClubSwitcher', () => { ], setActiveClub: vi.fn(), userRole: 'admin' - } as any); + }); render(); expect(screen.getAllByText('Tennis Club')[0]).toBeInTheDocument(); @@ -50,7 +50,7 @@ describe('ClubSwitcher', () => { it('calls setActiveClub when club is selected', () => { const mockSetActiveClub = vi.fn(); - (useTenant as any).mockReturnValue({ + (useTenant as ReturnType).mockReturnValue({ activeClubId: 'club-1', clubs: [ { id: 'club-1', name: 'Tennis Club', sportType: 'Tennis' }, @@ -58,7 +58,7 @@ describe('ClubSwitcher', () => { ], setActiveClub: mockSetActiveClub, userRole: 'admin' - } as any); + }); render(); diff --git a/frontend/src/components/__tests__/shift-detail.test.tsx b/frontend/src/components/__tests__/shift-detail.test.tsx index 9e97e00..2221312 100644 --- a/frontend/src/components/__tests__/shift-detail.test.tsx +++ b/frontend/src/components/__tests__/shift-detail.test.tsx @@ -38,12 +38,12 @@ describe('ShiftDetailPage', () => { beforeEach(() => { vi.clearAllMocks(); - (useSignUpShift as any).mockReturnValue({ mutateAsync: mockSignUp, isPending: false }); - (useCancelSignUp as any).mockReturnValue({ mutateAsync: mockCancel, isPending: false }); + (useSignUpShift as ReturnType).mockReturnValue({ mutateAsync: mockSignUp, isPending: false }); + (useCancelSignUp as ReturnType).mockReturnValue({ mutateAsync: mockCancel, isPending: false }); }); it('shows "Sign Up" button if capacity available', async () => { - (useShift as any).mockReturnValue({ + (useShift as ReturnType).mockReturnValue({ data: { id: '1', title: 'Detail Shift', @@ -69,7 +69,7 @@ describe('ShiftDetailPage', () => { }); it('shows "Cancel Sign-up" button if user is signed up', async () => { - (useShift as any).mockReturnValue({ + (useShift as ReturnType).mockReturnValue({ data: { id: '1', title: 'Detail Shift', @@ -95,7 +95,7 @@ describe('ShiftDetailPage', () => { }); it('calls sign up mutation on click', async () => { - (useShift as any).mockReturnValue({ + (useShift as ReturnType).mockReturnValue({ data: { id: '1', title: 'Detail Shift', diff --git a/frontend/src/components/__tests__/task-detail.test.tsx b/frontend/src/components/__tests__/task-detail.test.tsx index fc74fa5..1edc1cc 100644 --- a/frontend/src/components/__tests__/task-detail.test.tsx +++ b/frontend/src/components/__tests__/task-detail.test.tsx @@ -20,18 +20,18 @@ describe('TaskDetailPage', () => { const mockMutate = vi.fn(); beforeEach(() => { - (useUpdateTask as any).mockReturnValue({ + (useUpdateTask as ReturnType).mockReturnValue({ mutate: mockMutate, isPending: false, - } as any); + }); }); it('shows valid transitions for Open status', async () => { - (useTask as any).mockReturnValue({ + (useTask as ReturnType).mockReturnValue({ data: { id: '1', title: 'Task 1', status: 'Open', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' }, isLoading: false, error: null, - } as any); + }); const params = Promise.resolve({ id: '1' }); await act(async () => { @@ -44,11 +44,11 @@ describe('TaskDetailPage', () => { }); it('shows valid transitions for InProgress status', async () => { - (useTask as any).mockReturnValue({ + (useTask as ReturnType).mockReturnValue({ data: { id: '1', title: 'Task 1', status: 'InProgress', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' }, isLoading: false, error: null, - } as any); + }); const params = Promise.resolve({ id: '1' }); await act(async () => { @@ -60,11 +60,11 @@ describe('TaskDetailPage', () => { }); it('shows valid transitions for Review status (including back transition)', async () => { - (useTask as any).mockReturnValue({ + (useTask as ReturnType).mockReturnValue({ data: { id: '1', title: 'Task 1', status: 'Review', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' }, isLoading: false, error: null, - } as any); + }); const params = Promise.resolve({ id: '1' }); await act(async () => { diff --git a/frontend/src/components/__tests__/task-list.test.tsx b/frontend/src/components/__tests__/task-list.test.tsx index d168594..ae4f1d2 100644 --- a/frontend/src/components/__tests__/task-list.test.tsx +++ b/frontend/src/components/__tests__/task-list.test.tsx @@ -24,7 +24,7 @@ vi.mock('@/hooks/useTasks', () => ({ describe('TaskListPage', () => { beforeEach(() => { - (useTasks as any).mockReturnValue({ + (useTasks as ReturnType).mockReturnValue({ data: { items: [ { id: '1', title: 'Test Task 1', status: 'Open', assigneeId: null, createdAt: '2024-01-01' }, @@ -37,7 +37,7 @@ describe('TaskListPage', () => { }, isLoading: false, error: null, - } as any); + }); }); it('renders task list with 3 data rows', () => { diff --git a/frontend/src/components/auth-guard.tsx b/frontend/src/components/auth-guard.tsx index ca88c2b..f1bf441 100644 --- a/frontend/src/components/auth-guard.tsx +++ b/frontend/src/components/auth-guard.tsx @@ -6,7 +6,7 @@ import { ReactNode, useEffect } from 'react'; import { useTenant } from '../contexts/tenant-context'; export function AuthGuard({ children }: { children: ReactNode }) { - const { data: session, status } = useSession(); + const { status } = useSession(); const { activeClubId, clubs, setActiveClub, clubsLoading } = useTenant(); const router = useRouter(); diff --git a/frontend/src/contexts/tenant-context.tsx b/frontend/src/contexts/tenant-context.tsx index 94d46ae..beb3373 100644 --- a/frontend/src/contexts/tenant-context.tsx +++ b/frontend/src/contexts/tenant-context.tsx @@ -1,6 +1,6 @@ 'use client'; -import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { createContext, useContext, useEffect, useState, useMemo, ReactNode } from 'react'; import { useSession } from 'next-auth/react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -22,10 +22,31 @@ type TenantContextType = { const TenantContext = createContext(undefined); +function getInitialClubId(): string | null { + if (typeof window === 'undefined') return null; + return localStorage.getItem('activeClubId'); +} + +function determineActiveClub(clubs: Club[], currentActiveId: string | null): string | null { + if (!clubs.length) return null; + + const stored = getInitialClubId(); + if (stored && clubs.find(c => c.id === stored)) { + return stored; + } + + if (currentActiveId && clubs.find(c => c.id === currentActiveId)) { + return currentActiveId; + } + + return clubs[0].id; +} + export function TenantProvider({ children }: { children: ReactNode }) { const { data: session, status } = useSession(); - const [activeClubId, setActiveClubId] = useState(null); const queryClient = useQueryClient(); + + const [activeClubId, setActiveClubId] = useState(getInitialClubId); const { data: clubs = [], isLoading: clubsLoading, error: clubsError } = useQuery({ queryKey: ['my-clubs', session?.accessToken], @@ -43,25 +64,19 @@ export function TenantProvider({ children }: { children: ReactNode }) { retryDelay: (attemptIndex) => Math.min(1000 * Math.pow(2, attemptIndex), 10000), }); - useEffect(() => { - if (status === 'authenticated' && clubs.length > 0) { - const stored = localStorage.getItem('activeClubId'); - if (stored && clubs.find(c => c.id === stored)) { - setActiveClubId(stored); - } else if (!activeClubId) { - setActiveClubId(clubs[0].id); - } - } + const computedActiveClubId = useMemo(() => { + if (status !== 'authenticated' || !clubs.length) return activeClubId; + return determineActiveClub(clubs, activeClubId); }, [status, clubs, activeClubId]); useEffect(() => { - if (activeClubId) { - const selectedClub = clubs.find(c => c.id === activeClubId); + if (computedActiveClubId) { + const selectedClub = clubs.find(c => c.id === computedActiveClubId); if (selectedClub) { document.cookie = `X-Tenant-Id=${selectedClub.tenantId}; path=/; max-age=86400`; } } - }, [activeClubId, clubs]); + }, [computedActiveClubId, clubs]); const handleSetActiveClub = (clubId: string) => { setActiveClubId(clubId); @@ -73,10 +88,10 @@ export function TenantProvider({ children }: { children: ReactNode }) { queryClient.invalidateQueries(); }; - const userRole = activeClubId && session?.user?.clubs ? session.user.clubs[activeClubId] || null : null; + const userRole = computedActiveClubId && session?.user?.clubs ? session.user.clubs[computedActiveClubId] || null : null; return ( - + {children} ); diff --git a/frontend/src/hooks/__tests__/useActiveClub.test.ts b/frontend/src/hooks/__tests__/useActiveClub.test.ts index 13899f3..2f56939 100644 --- a/frontend/src/hooks/__tests__/useActiveClub.test.ts +++ b/frontend/src/hooks/__tests__/useActiveClub.test.ts @@ -1,7 +1,6 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useActiveClub } from '../useActiveClub'; -import type { Session } from 'next-auth'; const mockUseSession = vi.fn(); @@ -33,15 +32,15 @@ describe('useActiveClub', () => { status: 'authenticated', }); - (localStorage.getItem as any).mockImplementation((key: string) => { + (localStorage.getItem as ReturnType).mockImplementation((key: string) => { return localStorageData[key] || null; }); - (localStorage.setItem as any).mockImplementation((key: string, value: string) => { + (localStorage.setItem as ReturnType).mockImplementation((key: string, value: string) => { localStorageData[key] = value; }); - (localStorage.clear as any).mockImplementation(() => { + (localStorage.clear as ReturnType).mockImplementation(() => { localStorageData = {}; }); }); diff --git a/frontend/src/hooks/useActiveClub.ts b/frontend/src/hooks/useActiveClub.ts index 3169f94..bf3efa8 100644 --- a/frontend/src/hooks/useActiveClub.ts +++ b/frontend/src/hooks/useActiveClub.ts @@ -1,10 +1,26 @@ 'use client'; import { useSession } from 'next-auth/react'; -import { useState, useEffect } from 'react'; +import { useState, useMemo } from 'react'; const ACTIVE_CLUB_KEY = 'activeClubId'; +function getStoredClubId(): string | null { + if (typeof window === 'undefined') return null; + return localStorage.getItem(ACTIVE_CLUB_KEY); +} + +function determineActiveId(clubs: Record | undefined, currentId: string | null): string | null { + if (!clubs || Object.keys(clubs).length === 0) return null; + + const stored = getStoredClubId(); + if (stored && clubs[stored]) return stored; + + if (currentId && clubs[currentId]) return currentId; + + return Object.keys(clubs)[0]; +} + export interface ActiveClubData { activeClubId: string | null; role: string | null; @@ -14,23 +30,13 @@ export interface ActiveClubData { export function useActiveClub(): ActiveClubData { const { data: session, status } = useSession(); - const [activeClubId, setActiveClubIdState] = useState(null); - - useEffect(() => { - if (status === 'authenticated' && session?.user?.clubs) { - const clubs = session.user.clubs; - const storedClubId = localStorage.getItem(ACTIVE_CLUB_KEY); - - if (storedClubId && clubs[storedClubId]) { - setActiveClubIdState(storedClubId); - } else { - const firstClubId = Object.keys(clubs)[0]; - if (firstClubId) { - setActiveClubIdState(firstClubId); - } - } - } - }, [session, status]); + + const [activeClubId, setActiveClubIdState] = useState(getStoredClubId); + + const computedActiveId = useMemo(() => { + if (status !== 'authenticated' || !session?.user?.clubs) return activeClubId; + return determineActiveId(session.user.clubs, activeClubId); + }, [session, status, activeClubId]); const setActiveClub = (clubId: string) => { if (session?.user?.clubs && session.user.clubs[clubId]) { @@ -40,10 +46,10 @@ export function useActiveClub(): ActiveClubData { }; const clubs = session?.user?.clubs || null; - const role = activeClubId && clubs ? clubs[activeClubId] : null; + const role = computedActiveId && clubs ? clubs[computedActiveId] : null; return { - activeClubId, + activeClubId: computedActiveId, role, clubs, setActiveClub, diff --git a/frontend/src/lib/__tests__/api.test.ts b/frontend/src/lib/__tests__/api.test.ts index 166188f..00bb574 100644 --- a/frontend/src/lib/__tests__/api.test.ts +++ b/frontend/src/lib/__tests__/api.test.ts @@ -31,7 +31,7 @@ describe('apiClient', () => { configurable: true, }); - (global.fetch as any).mockResolvedValue({ + (global.fetch as ReturnType).mockResolvedValue({ ok: true, status: 200, json: async () => ({ data: 'test' }), @@ -145,7 +145,7 @@ describe('apiClient', () => { await apiClient('/api/test'); - const callHeaders = (global.fetch as any).mock.calls[0][1].headers; + const callHeaders = (global.fetch as ReturnType).mock.calls[0][1].headers; expect(callHeaders.Authorization).toBeUndefined(); }); @@ -158,7 +158,7 @@ describe('apiClient', () => { await apiClient('/api/test'); - const callHeaders = (global.fetch as any).mock.calls[0][1].headers; + const callHeaders = (global.fetch as ReturnType).mock.calls[0][1].headers; expect(callHeaders['X-Tenant-Id']).toBeUndefined(); }); }); diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts index 6755551..e51f9ab 100644 --- a/frontend/src/test/setup.ts +++ b/frontend/src/test/setup.ts @@ -6,13 +6,13 @@ const localStorageMock = { setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn(), + length: 0, + key: vi.fn(), }; -// Ensure localStorage is available on both global and globalThis -global.localStorage = localStorageMock as any; -globalThis.localStorage = localStorageMock as any; +global.localStorage = localStorageMock as unknown as Storage; +globalThis.localStorage = localStorageMock as unknown as Storage; -// Ensure document is available if jsdom hasn't set it up yet if (typeof document === 'undefined') { Object.defineProperty(globalThis, 'document', { value: {