From a06eae42ee5414f6424744f83456fb9a2d2b0626 Mon Sep 17 00:00:00 2001 From: Bhavya Reddy Date: Tue, 16 Jun 2026 22:05:13 +0530 Subject: [PATCH 1/4] Add Node version config --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..53d1c14 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22 From d9e37365819ac8fb748fcadf45b1007a084c7d9c Mon Sep 17 00:00:00 2001 From: Bhavya Reddy Date: Tue, 16 Jun 2026 22:34:50 +0530 Subject: [PATCH 2/4] Fix point calculation logic --- src/app/pathway/page.tsx | 205 ++++++++++++------ .../projects/ProjectUploadModal.tsx | 40 +++- src/context/AuthContext.tsx | 10 +- src/context/__tests__/AuthContext.test.tsx | 6 +- src/lib/__tests__/point-calculation.test.ts | 55 +++++ src/lib/point-calculation.ts | 25 ++- src/lib/points.ts | 30 ++- 7 files changed, 284 insertions(+), 87 deletions(-) create mode 100644 src/lib/__tests__/point-calculation.test.ts diff --git a/src/app/pathway/page.tsx b/src/app/pathway/page.tsx index 729ef92..9a097fc 100644 --- a/src/app/pathway/page.tsx +++ b/src/app/pathway/page.tsx @@ -16,6 +16,14 @@ import { Gift, Calendar, ChartNoAxesCombined, + CheckCircle2, + GitBranch, + GitPullRequest, + Heart, + MessageSquare, + Sparkles, + Upload, + UserCheck, } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; @@ -27,6 +35,117 @@ interface LeaderboardEntry { points?: number; } +const pointEarningActivities = [ + { + label: 'Daily Login', + value: `+${POINTS.DAILY_LOGIN} (+Streak)`, + Icon: Flame, + iconClassName: 'text-orange-500', + }, + { + label: '7-Day Streak', + value: `+${POINTS.WEEKLY_STREAK_BONUS}`, + Icon: Flame, + iconClassName: 'text-red-500', + }, + { + label: 'Follow Community', + value: `+${POINTS.FOLLOW_COMMUNITY}`, + Icon: Users, + iconClassName: 'text-blue-500', + }, + { + label: 'Gain Follower', + value: `+${POINTS.FOLLOWER_GAINED}`, + Icon: UserCheck, + iconClassName: 'text-green-500', + }, + { + label: 'Earn Badge', + value: 'Dynamic', + Icon: Award, + iconClassName: 'text-purple-500', + }, + { + label: 'Project Star', + value: `+${POINTS.PROJECT_STAR}`, + Icon: Star, + iconClassName: 'text-yellow-500', + }, + { + label: 'Event Participation', + value: `+${POINTS.EVENT_PARTICIPATION}`, + Icon: Calendar, + iconClassName: 'text-pink-500', + }, + { + label: 'Hackathon Win', + value: `+${POINTS.HACKATHON_WIN}`, + Icon: Trophy, + iconClassName: 'text-yellow-600', + }, + { + label: 'Profile Completion', + value: `+${POINTS.PROFILE_COMPLETION}`, + Icon: CheckCircle2, + iconClassName: 'text-emerald-500', + }, + { + label: 'First Project Upload', + value: `+${POINTS.FIRST_PROJECT_UPLOAD}`, + Icon: Upload, + iconClassName: 'text-cyan-500', + }, + { + label: 'Repository Contribution', + value: `+${POINTS.REPOSITORY_CONTRIBUTION}`, + Icon: GitBranch, + iconClassName: 'text-sky-500', + }, + { + label: 'Pull Request Merged', + value: `+${POINTS.PULL_REQUEST_MERGED}`, + Icon: GitPullRequest, + iconClassName: 'text-violet-500', + }, + { + label: 'Issue Resolution', + value: `+${POINTS.ISSUE_RESOLUTION}`, + Icon: CheckCircle2, + iconClassName: 'text-lime-500', + }, + { + label: 'Community Post Creation', + value: `+${POINTS.COMMUNITY_POST_CREATION}`, + Icon: MessageSquare, + iconClassName: 'text-teal-500', + }, + { + label: 'Helpful Comment Received', + value: `+${POINTS.HELPFUL_COMMENT_RECEIVED}`, + Icon: Heart, + iconClassName: 'text-rose-500', + }, + { + label: 'Consecutive Weekly Activity', + value: `+${POINTS.CONSECUTIVE_WEEKLY_ACTIVITY}`, + Icon: Flame, + iconClassName: 'text-amber-500', + }, + { + label: 'Open Source Contribution', + value: `+${POINTS.OPEN_SOURCE_CONTRIBUTION}`, + Icon: GitPullRequest, + iconClassName: 'text-indigo-500', + }, + { + label: 'Mentor Recognition', + value: `+${POINTS.MENTOR_RECOGNITION}`, + Icon: Sparkles, + iconClassName: 'text-fuchsia-500', + }, +]; + export default function PathwayPage() { const { user } = useAuth(); const [leaderboard, setLeaderboard] = useState([]); @@ -416,72 +535,26 @@ export default function PathwayPage() { How to Earn Points -
-
- - Daily Login - - - +{POINTS.DAILY_LOGIN} (+Streak) - -
-
- - 7-Day Streak - - - +{POINTS.WEEKLY_STREAK_BONUS} - -
-
- - Follow Community - - - +{POINTS.FOLLOW_COMMUNITY} - -
-
- - Earn Badge - - - +{POINTS.BADGE_EARNED} - -
-
- - Gain Follower - - - +{POINTS.FOLLOWER_GAINED} - -
-
- - Project Star - - - +{POINTS.PROJECT_STAR} - -
-
- - Event - Participation - - - +{POINTS.EVENT_PARTICIPATION} - -
-
- - Hackathon Win - - - +{POINTS.HACKATHON_WIN} - -
+
+ {pointEarningActivities.map( + ({ label, value, Icon, iconClassName }) => ( +
+ + + {label} + + + {value} + +
+ ) + )}
diff --git a/src/components/projects/ProjectUploadModal.tsx b/src/components/projects/ProjectUploadModal.tsx index e6c2cae..44f67e8 100644 --- a/src/components/projects/ProjectUploadModal.tsx +++ b/src/components/projects/ProjectUploadModal.tsx @@ -19,6 +19,8 @@ import { doc, serverTimestamp, increment, + getDoc, + arrayUnion, } from 'firebase/firestore'; import { db } from '@/lib/firebase'; import { POINTS } from '@/lib/points'; @@ -220,23 +222,37 @@ export default function ProjectUploadModal({ const memberRef = doc(db, 'members', userId); const leaderboardRef = doc(db, 'leaderboard', userId); const today = new Date().toISOString().split('T')[0]; + const memberSnap = await getDoc(memberRef); + const achievements = memberSnap.exists() + ? memberSnap.data().achievements || [] + : []; + const shouldAwardFirstProject = !achievements.includes( + 'first_project_upload' + ); batch.set(rootRef, newProjectData); batch.set(subRef, newProjectData); // XP award is part of the same atomic batch — it only lands if both // project writes succeed. - batch.update(memberRef, { points: increment(POINTS.CREATE_PROJECT) }); - batch.set( - leaderboardRef, - { - uid: userId, - name: userName, - points: increment(POINTS.CREATE_PROJECT), - role: 'member', - lastActive: today, - }, - { merge: true } - ); + if (shouldAwardFirstProject) { + batch.update(memberRef, { + achievements: arrayUnion('first_project_upload'), + points: increment(POINTS.FIRST_PROJECT_UPLOAD), + }); + } + if (shouldAwardFirstProject) { + batch.set( + leaderboardRef, + { + uid: userId, + name: userName, + points: increment(POINTS.FIRST_PROJECT_UPLOAD), + role: 'member', + lastActive: today, + }, + { merge: true } + ); + } } await batch.commit(); diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 5f7b8c3..ffd603c 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -348,9 +348,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const { currentStreak } = calculateStreak(loginDates); if (shouldUpdate || userData.streak !== currentStreak) { - // Award 1 XP if streak increased (Daily Login) + // Award daily login XP plus a capped streak multiplier. if (currentStreak > (userData.streak || 0)) { - pointsDelta += POINTS.DAILY_LOGIN; + const streakMultiplier = Math.min( + Math.max(currentStreak - 1, 0), + 6 + ); + pointsDelta += + POINTS.DAILY_LOGIN + + streakMultiplier * POINTS.STREAK_BONUS_PER_DAY; // 7-Day Streak Bonus if (currentStreak % 7 === 0 && currentStreak > 0) { diff --git a/src/context/__tests__/AuthContext.test.tsx b/src/context/__tests__/AuthContext.test.tsx index 39514f0..e27c457 100644 --- a/src/context/__tests__/AuthContext.test.tsx +++ b/src/context/__tests__/AuthContext.test.tsx @@ -50,7 +50,11 @@ jest.mock('@/lib/streakUtils', () => ({ })); jest.mock('@/lib/points', () => ({ - POINTS: { DAILY_LOGIN: 1, WEEKLY_STREAK_BONUS: 20 }, + POINTS: { + DAILY_LOGIN: 1, + WEEKLY_STREAK_BONUS: 20, + STREAK_BONUS_PER_DAY: 1, + }, })); function TestHarness() { diff --git a/src/lib/__tests__/point-calculation.test.ts b/src/lib/__tests__/point-calculation.test.ts new file mode 100644 index 0000000..55869ac --- /dev/null +++ b/src/lib/__tests__/point-calculation.test.ts @@ -0,0 +1,55 @@ +import { + calculateUserPointsAndBadges, + determineBadges, + getBadgeXp, +} from '../point-calculation'; +import { BADGE_RARITY_POINTS, POINTS } from '../points'; + +describe('point calculation', () => { + it('awards badge XP based on badge rarity', () => { + expect(getBadgeXp('builder-10')).toBe(BADGE_RARITY_POINTS.legendary); + expect(getBadgeXp('builder-5')).toBe(BADGE_RARITY_POINTS.epic); + expect(getBadgeXp('social-github')).toBe(BADGE_RARITY_POINTS.rare); + }); + + it('falls back to standard badge XP for unknown badges', () => { + expect(getBadgeXp('custom-community-badge')).toBe(POINTS.BADGE_EARNED); + }); + + it('derives earned badges from profile and project activity', () => { + const badges = determineBadges( + { + uid: 'member-1', + name: 'DevPath Member', + bio: 'Building projects and helping the community.', + photoURL: '/avatar.png', + role: 'Developer', + github: 'https://github.com/member', + linkedin: 'https://linkedin.com/in/member', + instagram: 'https://instagram.com/member', + streak: 7, + }, + [{}, {}, {}] + ); + + expect(badges).toEqual( + expect.arrayContaining([ + 'profile-perfect', + 'connector-social', + 'builder-1', + 'builder-3', + 'streak-7', + ]) + ); + }); + + it('keeps the legacy shim from overwriting transactional points', () => { + const result = calculateUserPointsAndBadges( + { uid: 'member-1', streak: 7 }, + [] + ); + + expect(result.achievements).toContain('streak-7'); + expect(result.points).toBeNull(); + }); +}); diff --git a/src/lib/point-calculation.ts b/src/lib/point-calculation.ts index 4491244..94cdf09 100644 --- a/src/lib/point-calculation.ts +++ b/src/lib/point-calculation.ts @@ -1,4 +1,4 @@ -import { POINTS } from './points'; +import { BADGE_RARITY_POINTS, BadgeRarity, POINTS } from './points'; export const SOCIAL_BADGES = [ 'social-github', @@ -6,6 +6,23 @@ export const SOCIAL_BADGES = [ 'social-instagram', ]; +export const BADGE_RARITY_BY_ID: Record = { + 'profile-perfect': 'uncommon', + storyteller: 'common', + 'face-of-community': 'common', + 'local-hero': 'common', + 'connector-social': 'rare', + 'social-github': 'rare', + 'social-linkedin': 'rare', + 'social-instagram': 'rare', + 'builder-1': 'uncommon', + 'builder-3': 'rare', + 'builder-5': 'epic', + 'builder-10': 'legendary', + 'streak-7': 'rare', + 'early-adopter': 'legendary', +}; + export interface UserData { uid: string; name?: string; @@ -31,6 +48,12 @@ export interface ProjectData { * Social badges (GitHub, LinkedIn, Instagram) award more XP than standard badges. */ export function getBadgeXp(badgeId: string): number { + const rarity = BADGE_RARITY_BY_ID[badgeId]; + + if (rarity) { + return BADGE_RARITY_POINTS[rarity]; + } + return SOCIAL_BADGES.includes(badgeId) ? POINTS.SOCIAL_BADGE_EARNED : POINTS.BADGE_EARNED; diff --git a/src/lib/points.ts b/src/lib/points.ts index 2a382bb..6a2d56b 100644 --- a/src/lib/points.ts +++ b/src/lib/points.ts @@ -1,18 +1,38 @@ export const POINTS = { DAILY_LOGIN: 1, // Base login is 1 WEEKLY_STREAK_BONUS: 20, - FOLLOW_COMMUNITY: 500, + FOLLOW_COMMUNITY: 50, BADGE_EARNED: 20, // Standard badge SOCIAL_BADGE_EARNED: 50, // GitHub, LinkedIn, Instagram FOLLOWER_GAINED: 10, PROJECT_STAR: 50, - CREATE_PROJECT: 200, - CREATE_DISCUSSION: 100, - EVENT_PARTICIPATION: 500, - HACKATHON_WIN: 5000, + CREATE_PROJECT: 50, + CREATE_DISCUSSION: 10, + EVENT_PARTICIPATION: 100, + HACKATHON_WIN: 1000, STREAK_BONUS_PER_DAY: 1, + PROFILE_COMPLETION: 25, + FIRST_PROJECT_UPLOAD: 50, + REPOSITORY_CONTRIBUTION: 20, + PULL_REQUEST_MERGED: 50, + ISSUE_RESOLUTION: 30, + COMMUNITY_POST_CREATION: 10, + HELPFUL_COMMENT_RECEIVED: 5, + CONSECUTIVE_WEEKLY_ACTIVITY: 100, + OPEN_SOURCE_CONTRIBUTION: 75, + MENTOR_RECOGNITION: 200, }; +export const BADGE_RARITY_POINTS = { + common: 20, + uncommon: 25, + rare: 50, + epic: 100, + legendary: 200, +} as const; + +export type BadgeRarity = keyof typeof BADGE_RARITY_POINTS; + export const LEVELS = [ { name: 'Shishya', From 36c7ce9ea4105c8fbeba2c784734ff93a3c5e3a0 Mon Sep 17 00:00:00 2001 From: Bhavya Reddy Date: Wed, 17 Jun 2026 10:44:40 +0530 Subject: [PATCH 3/4] Fix lint issues in point calculation PR --- .../projects/ProjectUploadModal.tsx | 15 +++++--- src/context/AuthContext.tsx | 34 +++++++++---------- src/lib/point-calculation.ts | 3 +- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/components/projects/ProjectUploadModal.tsx b/src/components/projects/ProjectUploadModal.tsx index 44f67e8..bdcac6f 100644 --- a/src/components/projects/ProjectUploadModal.tsx +++ b/src/components/projects/ProjectUploadModal.tsx @@ -84,6 +84,16 @@ const POPULAR_SKILLS = [ 'Data Science', ]; +interface ProjectFormData { + id?: string; + title?: string; + description?: string; + websiteUrl?: string; + skills?: string[]; + screenshots?: string[]; + videoUrl?: string; +} + interface ProjectUploadModalProps { isOpen: boolean; onClose: () => void; @@ -91,14 +101,13 @@ interface ProjectUploadModalProps { userEmail?: string | null; userName: string; onSuccess: () => void; - initialData?: any; // Project data for editing + initialData?: ProjectFormData; } export default function ProjectUploadModal({ isOpen, onClose, userId, - userEmail, userName, onSuccess, initialData, @@ -239,8 +248,6 @@ export default function ProjectUploadModal({ achievements: arrayUnion('first_project_upload'), points: increment(POINTS.FIRST_PROJECT_UPLOAD), }); - } - if (shouldAwardFirstProject) { batch.set( leaderboardRef, { diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index ffd603c..791a8bb 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -12,11 +12,11 @@ import { onAuthStateChanged, signInWithEmailAndPassword, signOut, - User as FirebaseUser, setPersistence, browserLocalPersistence, } from 'firebase/auth'; import { doc, getDoc, setDoc } from 'firebase/firestore'; +import type { FieldValue } from 'firebase/firestore'; import { leaderboardSyncErrorEmitter } from '@/lib/leaderboard-sync-error'; interface User { @@ -63,7 +63,7 @@ interface User { followers?: number; following?: number; contributions?: number; - lastFetched?: any; + lastFetched?: unknown; recentActivity?: { id: string; type: string; @@ -88,7 +88,7 @@ interface User { badges?: string[]; sessionId?: string; docId?: string; // Actual Firestore Document ID (Email or UID) - createdAt?: any; + createdAt?: unknown; } interface AuthContextType { @@ -164,11 +164,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // This ensures their session is tracked and they are validated securely via Firestore `admins` collection. let role: 'admin' | 'member' = 'member'; - let userData: any = { + let userData: User & Record = { uid: firebaseUser.uid, email: firebaseUser.email, name: firebaseUser.displayName, photoURL: firebaseUser.photoURL, + role, showMobile: false, showLocation: true, showEmail: false, @@ -330,10 +331,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const { calculateStreak, getISTDateString } = await import('@/lib/streakUtils'); const today = getISTDateString(new Date()); - let loginDates = userData.loginDates || []; + const loginDates = userData.loginDates || []; let shouldUpdate = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const updateData: Record = {}; + const updateData: Record = {}; let pointsDelta = 0; // 1. Check if new day login @@ -372,7 +372,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { if (shouldUpdate) { const { increment } = await import('firebase/firestore'); - const firestoreUpdate: any = { ...updateData }; + const firestoreUpdate: Record = { ...updateData }; if (pointsDelta > 0) { firestoreUpdate.points = increment(pointsDelta); } @@ -439,7 +439,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { unsubscribeSnapshot.current(); } }; - }, []); + }, [firebaseReady]); const verifyAdmin = () => { setIsAdminVerified(true); @@ -504,14 +504,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Remove undefined fields const cleanData = Object.fromEntries( - Object.entries(data).filter(([_, v]) => v !== undefined) - ); + Object.entries(data).filter(([, value]) => value !== undefined) + ) as Partial; - if ((cleanData as any).points !== undefined) { + if (cleanData.points !== undefined) { console.warn( '[updateUserProfile] Ignoring `points` field. Use awardPoints(pointsDelta) instead.' ); - delete (cleanData as any).points; + delete cleanData.points; } await setDoc(doc(db, collectionName, docId), cleanData, { merge: true }); @@ -585,7 +585,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const followUser = async ( targetUserId: string, targetRole: string = 'member', - targetEmail?: string + _targetEmail?: string ) => { if (!firebaseReady || !user) return; if (user.uid === targetUserId) return; // Cannot follow self @@ -616,7 +616,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const targetUserRef = doc(db, targetCollection, targetDocId); - const updateData: any = { + const updateData: { followers: FieldValue; points?: FieldValue } = { followers: arrayUnion(user.uid), }; @@ -663,7 +663,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const unfollowUser = async ( targetUserId: string, targetRole: string = 'member', - targetEmail?: string + _targetEmail?: string ) => { if (!firebaseReady || !user) return; if (user.email === SUPER_ADMIN_EMAIL) return; // Super Admin Guard @@ -692,7 +692,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const targetUserRef = doc(db, targetCollection, targetDocId); - const updateData: any = { + const updateData: { followers: FieldValue; points?: FieldValue } = { followers: arrayRemove(user.uid), }; diff --git a/src/lib/point-calculation.ts b/src/lib/point-calculation.ts index 94cdf09..88108c2 100644 --- a/src/lib/point-calculation.ts +++ b/src/lib/point-calculation.ts @@ -1,4 +1,5 @@ -import { BADGE_RARITY_POINTS, BadgeRarity, POINTS } from './points'; +import { BADGE_RARITY_POINTS, POINTS } from './points'; +import type { BadgeRarity } from './points'; export const SOCIAL_BADGES = [ 'social-github', From 60a097d3954cbfbfa295f96ce4ea96dac4271c1f Mon Sep 17 00:00:00 2001 From: Bhavya Reddy Date: Wed, 17 Jun 2026 10:48:00 +0530 Subject: [PATCH 4/4] Fix remaining lint annotations --- src/context/AuthContext.tsx | 6 ++-- src/context/__tests__/AuthContext.test.tsx | 32 ++++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 791a8bb..d94be6e 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -584,8 +584,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const followUser = async ( targetUserId: string, - targetRole: string = 'member', - _targetEmail?: string + targetRole: string = 'member' ) => { if (!firebaseReady || !user) return; if (user.uid === targetUserId) return; // Cannot follow self @@ -662,8 +661,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const unfollowUser = async ( targetUserId: string, - targetRole: string = 'member', - _targetEmail?: string + targetRole: string = 'member' ) => { if (!firebaseReady || !user) return; if (user.email === SUPER_ADMIN_EMAIL) return; // Super Admin Guard diff --git a/src/context/__tests__/AuthContext.test.tsx b/src/context/__tests__/AuthContext.test.tsx index e27c457..fd2e5af 100644 --- a/src/context/__tests__/AuthContext.test.tsx +++ b/src/context/__tests__/AuthContext.test.tsx @@ -2,6 +2,7 @@ import '@testing-library/jest-dom'; import React from 'react'; import { render, screen, act } from '@testing-library/react'; import { AuthProvider, useAuth } from '../AuthContext'; +import * as firebaseLib from '@/lib/firebase'; const mockSignIn = jest.fn(); const mockSignOut = jest.fn(); @@ -9,8 +10,8 @@ const mockSignOut = jest.fn(); jest.mock('firebase/auth', () => ({ getAuth: () => ({}), onAuthStateChanged: jest.fn(() => jest.fn()), - signInWithEmailAndPassword: (...args: any[]) => mockSignIn(...args), - signOut: (...args: any[]) => mockSignOut(...args), + signInWithEmailAndPassword: (...args: unknown[]) => mockSignIn(...args), + signOut: (...args: unknown[]) => mockSignOut(...args), setPersistence: jest.fn(() => Promise.resolve()), browserLocalPersistence: {}, })); @@ -25,18 +26,20 @@ jest.mock('firebase/firestore', () => ({ doc: jest.fn(() => ({})), getDoc: jest.fn(() => Promise.resolve({ exists: () => false })), setDoc: jest.fn(() => Promise.resolve()), - onSnapshot: jest.fn((_: any, cb: any) => { - cb({ exists: () => false }); - return jest.fn(); - }), + onSnapshot: jest.fn( + (_ref: unknown, cb: (snapshot: { exists: () => boolean }) => void) => { + cb({ exists: () => false }); + return jest.fn(); + } + ), writeBatch: jest.fn(() => ({ commit: jest.fn(), set: jest.fn(), update: jest.fn(), })), increment: jest.fn((n: number) => n), - arrayUnion: jest.fn((...args: any[]) => args), - arrayRemove: jest.fn((...args: any[]) => args), + arrayUnion: jest.fn((...args: unknown[]) => args), + arrayRemove: jest.fn((...args: unknown[]) => args), getFirestore: jest.fn(() => ({})), })); @@ -63,8 +66,8 @@ function TestHarness() { const handleLogin = async () => { try { await login('test@test.com', 'pass123'); - } catch (e: any) { - setError(e.message); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : String(e)); } }; return ( @@ -154,8 +157,7 @@ describe('AuthContext', () => { }); it('throws readable error when Firebase is unavailable', async () => { - const firebaseLib = require('@/lib/firebase'); - firebaseLib.firebaseAvailable = false; + jest.replaceProperty(firebaseLib, 'firebaseAvailable', false); let error: string | null = null; function TestPage() { @@ -165,8 +167,8 @@ describe('AuthContext', () => { onClick={async () => { try { await login('a@b.com', 'p'); - } catch (e: any) { - error = e.message; + } catch (e: unknown) { + error = e instanceof Error ? e.message : String(e); } }} > @@ -187,7 +189,7 @@ describe('AuthContext', () => { 'Firebase is not configured. Login is unavailable in local UI-only mode.' ); - firebaseLib.firebaseAvailable = true; + jest.replaceProperty(firebaseLib, 'firebaseAvailable', true); }); }); });