refactor: remove old arcade guard system
Remove deprecated arcade guard hooks and components: - useArcadeGuard.ts - useArcadeRedirect.ts - ArcadeGuardedPage.tsx - Related tests These have been replaced by the new room-based system with proper moderation, invitations, and real-time updates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,312 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import * as nextNavigation from 'next/navigation'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as arcadeGuard from '@/hooks/useArcadeGuard'
|
||||
import * as roomData from '@/hooks/useRoomData'
|
||||
import * as viewerId from '@/hooks/useViewerId'
|
||||
|
||||
// Mock Next.js navigation
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: vi.fn(),
|
||||
usePathname: vi.fn(),
|
||||
useParams: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock hooks
|
||||
vi.mock('@/hooks/useArcadeGuard')
|
||||
vi.mock('@/hooks/useRoomData')
|
||||
vi.mock('@/hooks/useViewerId')
|
||||
vi.mock('@/hooks/useUserPlayers', () => ({
|
||||
useUserPlayers: () => ({ data: [], isLoading: false }),
|
||||
useCreatePlayer: () => ({ mutate: vi.fn() }),
|
||||
useUpdatePlayer: () => ({ mutate: vi.fn() }),
|
||||
useDeletePlayer: () => ({ mutate: vi.fn() }),
|
||||
}))
|
||||
vi.mock('@/hooks/useArcadeSocket', () => ({
|
||||
useArcadeSocket: () => ({
|
||||
connected: false,
|
||||
joinSession: vi.fn(),
|
||||
socket: null,
|
||||
sendMove: vi.fn(),
|
||||
exitSession: vi.fn(),
|
||||
pingSession: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock styled-system
|
||||
vi.mock('../../../../styled-system/css', () => ({
|
||||
css: () => '',
|
||||
}))
|
||||
|
||||
// Mock components
|
||||
vi.mock('@/components/PageWithNav', () => ({
|
||||
PageWithNav: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
// Import pages after mocks
|
||||
import RoomBrowserPage from '../page'
|
||||
|
||||
describe('Room Navigation with Active Sessions', () => {
|
||||
const mockRouter = {
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
back: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(nextNavigation, 'useRouter').mockReturnValue(mockRouter as any)
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
|
||||
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
|
||||
data: 'test-user',
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
error: null,
|
||||
} as any)
|
||||
global.fetch = vi.fn()
|
||||
})
|
||||
|
||||
describe('RoomBrowserPage', () => {
|
||||
it('should render room browser without redirecting when user has active game session', async () => {
|
||||
// User has an active game session
|
||||
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
|
||||
hasActiveSession: true,
|
||||
loading: false,
|
||||
activeSession: {
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
},
|
||||
})
|
||||
|
||||
// User is in a room
|
||||
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
|
||||
roomData: {
|
||||
id: 'room-1',
|
||||
name: 'Test Room',
|
||||
code: 'ABC123',
|
||||
gameName: 'matching',
|
||||
members: [],
|
||||
memberPlayers: {},
|
||||
},
|
||||
isLoading: false,
|
||||
isInRoom: true,
|
||||
notifyRoomOfPlayerUpdate: vi.fn(),
|
||||
})
|
||||
|
||||
// Mock rooms API
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
rooms: [
|
||||
{
|
||||
id: 'room-1',
|
||||
code: 'ABC123',
|
||||
name: 'Test Room',
|
||||
gameName: 'matching',
|
||||
status: 'lobby',
|
||||
createdAt: new Date(),
|
||||
creatorName: 'Test User',
|
||||
isLocked: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
render(<RoomBrowserPage />)
|
||||
|
||||
// Should render the page
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('🎮 Multiplayer Rooms')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should NOT redirect to /arcade/room
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should NOT redirect when PageWithNav uses arcade guard with enabled=false', async () => {
|
||||
// Simulate PageWithNav calling useArcadeGuard with enabled=false
|
||||
const arcadeGuardSpy = vi.spyOn(arcadeGuard, 'useArcadeGuard')
|
||||
|
||||
// User has an active game session
|
||||
arcadeGuardSpy.mockReturnValue({
|
||||
hasActiveSession: true,
|
||||
loading: false,
|
||||
activeSession: {
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
},
|
||||
})
|
||||
|
||||
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
|
||||
roomData: null,
|
||||
isLoading: false,
|
||||
isInRoom: false,
|
||||
notifyRoomOfPlayerUpdate: vi.fn(),
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ rooms: [] }),
|
||||
})
|
||||
|
||||
render(<RoomBrowserPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('🎮 Multiplayer Rooms')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// PageWithNav should have called useArcadeGuard with enabled=false
|
||||
// This is tested in PageWithNav's own tests, but we verify no redirect happened
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow navigation to room detail even with active session', async () => {
|
||||
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
|
||||
hasActiveSession: true,
|
||||
loading: false,
|
||||
activeSession: {
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
},
|
||||
})
|
||||
|
||||
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
|
||||
roomData: null,
|
||||
isLoading: false,
|
||||
isInRoom: false,
|
||||
notifyRoomOfPlayerUpdate: vi.fn(),
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
rooms: [
|
||||
{
|
||||
id: 'room-1',
|
||||
code: 'ABC123',
|
||||
name: 'Test Room',
|
||||
gameName: 'matching',
|
||||
status: 'lobby',
|
||||
createdAt: new Date(),
|
||||
creatorName: 'Test User',
|
||||
isLocked: false,
|
||||
isMember: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
render(<RoomBrowserPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Room')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Click on the room card
|
||||
const roomCard = screen.getByText('Test Room').parentElement
|
||||
roomCard?.click()
|
||||
|
||||
// Should navigate to room detail, not to /arcade/room
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/arcade-rooms/room-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Room navigation edge cases', () => {
|
||||
it('should handle rapid navigation between room pages without redirect loops', async () => {
|
||||
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
|
||||
hasActiveSession: true,
|
||||
loading: false,
|
||||
activeSession: {
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
},
|
||||
})
|
||||
|
||||
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
|
||||
roomData: null,
|
||||
isLoading: false,
|
||||
isInRoom: false,
|
||||
notifyRoomOfPlayerUpdate: vi.fn(),
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ rooms: [] }),
|
||||
})
|
||||
|
||||
const { rerender } = render(<RoomBrowserPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('🎮 Multiplayer Rooms')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Simulate pathname changes (navigating between room pages)
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms/room-1')
|
||||
rerender(<RoomBrowserPage />)
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
|
||||
rerender(<RoomBrowserPage />)
|
||||
|
||||
// Should never redirect to game page
|
||||
expect(mockRouter.push).not.toHaveBeenCalledWith('/arcade/room')
|
||||
})
|
||||
|
||||
it('should allow user to leave room and browse other rooms during active game', async () => {
|
||||
// User is in a room with an active game
|
||||
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
|
||||
hasActiveSession: true,
|
||||
loading: false,
|
||||
activeSession: {
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
},
|
||||
})
|
||||
|
||||
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
|
||||
roomData: {
|
||||
id: 'room-1',
|
||||
name: 'Current Room',
|
||||
code: 'ABC123',
|
||||
gameName: 'matching',
|
||||
members: [],
|
||||
memberPlayers: {},
|
||||
},
|
||||
isLoading: false,
|
||||
isInRoom: true,
|
||||
notifyRoomOfPlayerUpdate: vi.fn(),
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
rooms: [
|
||||
{
|
||||
id: 'room-1',
|
||||
name: 'Current Room',
|
||||
code: 'ABC123',
|
||||
gameName: 'matching',
|
||||
status: 'playing',
|
||||
isMember: true,
|
||||
},
|
||||
{
|
||||
id: 'room-2',
|
||||
name: 'Other Room',
|
||||
code: 'DEF456',
|
||||
gameName: 'memory-quiz',
|
||||
status: 'lobby',
|
||||
isMember: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
render(<RoomBrowserPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Current Room')).toBeInTheDocument()
|
||||
expect(screen.getByText('Other Room')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should be able to view both rooms without redirect
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,43 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useArcadeGuard } from '@/hooks/useArcadeGuard'
|
||||
|
||||
export interface ArcadeGuardedPageProps {
|
||||
children: ReactNode
|
||||
/**
|
||||
* Loading component to show while checking for active session
|
||||
*/
|
||||
loadingComponent?: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper component that applies the arcade session guard
|
||||
*
|
||||
* This component:
|
||||
* - Checks for active arcade sessions
|
||||
* - Redirects to active session if user navigates to a different game
|
||||
* - Shows loading state while checking
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* export default function MatchingPage() {
|
||||
* return (
|
||||
* <ArcadeGuardedPage>
|
||||
* <MemoryPairsProvider>
|
||||
* <MemoryPairsGame />
|
||||
* </MemoryPairsProvider>
|
||||
* </ArcadeGuardedPage>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function ArcadeGuardedPage({ children, loadingComponent }: ArcadeGuardedPageProps) {
|
||||
const { loading } = useArcadeGuard()
|
||||
|
||||
if (loading && loadingComponent) {
|
||||
return <>{loadingComponent}</>
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import * as nextNavigation from 'next/navigation'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useArcadeGuard } from '../useArcadeGuard'
|
||||
import * as arcadeSocket from '../useArcadeSocket'
|
||||
import * as viewerId from '../useViewerId'
|
||||
|
||||
// Mock Next.js navigation
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: vi.fn(),
|
||||
usePathname: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock useArcadeSocket
|
||||
vi.mock('../useArcadeSocket', () => ({
|
||||
useArcadeSocket: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock useViewerId
|
||||
vi.mock('../useViewerId', () => ({
|
||||
useViewerId: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('useArcadeGuard', () => {
|
||||
const mockRouter = {
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
back: vi.fn(),
|
||||
}
|
||||
|
||||
const mockUseArcadeSocket = {
|
||||
connected: true,
|
||||
joinSession: vi.fn(),
|
||||
socket: null,
|
||||
sendMove: vi.fn(),
|
||||
exitSession: vi.fn(),
|
||||
pingSession: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(nextNavigation, 'useRouter').mockReturnValue(mockRouter as any)
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockReturnValue(mockUseArcadeSocket)
|
||||
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
|
||||
data: 'test-user',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any)
|
||||
global.fetch = vi.fn()
|
||||
})
|
||||
|
||||
it('should initialize with loading state', () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
expect(result.current.hasActiveSession).toBe(false)
|
||||
expect(result.current.activeSession).toBe(null)
|
||||
})
|
||||
|
||||
it('should fetch active session on mount', async () => {
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/matching',
|
||||
currentGame: 'matching',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/arcade-session?userId=test-user')
|
||||
expect(result.current.hasActiveSession).toBe(true)
|
||||
expect(result.current.activeSession).toEqual({
|
||||
gameUrl: '/arcade/matching',
|
||||
currentGame: 'matching',
|
||||
})
|
||||
})
|
||||
|
||||
it('should redirect to active session if on different page', async () => {
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/memory-quiz',
|
||||
currentGame: 'memory-quiz',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/arcade/memory-quiz')
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT redirect if already on active session page', async () => {
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/matching',
|
||||
currentGame: 'matching',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle no active session (404)', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.hasActiveSession).toBe(false)
|
||||
expect(result.current.activeSession).toBe(null)
|
||||
})
|
||||
|
||||
it('should call onRedirect callback when redirecting', async () => {
|
||||
const onRedirect = vi.fn()
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/memory-quiz',
|
||||
currentGame: 'memory-quiz',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
|
||||
renderHook(() => useArcadeGuard({ onRedirect }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onRedirect).toHaveBeenCalledWith('/arcade/memory-quiz')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not fetch session when disabled', () => {
|
||||
renderHook(() => useArcadeGuard({ enabled: false }))
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch session when viewerId is null', () => {
|
||||
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any)
|
||||
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should join WebSocket room when connected', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseArcadeSocket.joinSession).toHaveBeenCalledWith('test-user')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle session-state event from WebSocket', async () => {
|
||||
let onSessionStateCallback: ((data: any) => void) | null = null
|
||||
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
|
||||
onSessionStateCallback = events?.onSessionState || null
|
||||
return mockUseArcadeSocket
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
// Simulate session-state event from WebSocket
|
||||
onSessionStateCallback?.({
|
||||
gameUrl: '/arcade/complement-race',
|
||||
currentGame: 'complement-race',
|
||||
gameState: {},
|
||||
activePlayers: [1],
|
||||
version: 1,
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasActiveSession).toBe(true)
|
||||
expect(result.current.activeSession).toEqual({
|
||||
gameUrl: '/arcade/complement-race',
|
||||
currentGame: 'complement-race',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle session-ended event from WebSocket', async () => {
|
||||
let onSessionEndedCallback: (() => void) | null = null
|
||||
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
|
||||
onSessionEndedCallback = events?.onSessionEnded || null
|
||||
return mockUseArcadeSocket
|
||||
})
|
||||
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/matching',
|
||||
currentGame: 'matching',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasActiveSession).toBe(true)
|
||||
})
|
||||
|
||||
// Simulate session-ended event
|
||||
onSessionEndedCallback?.()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasActiveSession).toBe(false)
|
||||
expect(result.current.activeSession).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
;(global.fetch as any).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
// Should not crash, just set loading to false
|
||||
expect(result.current.hasActiveSession).toBe(false)
|
||||
})
|
||||
|
||||
describe('enabled flag behavior', () => {
|
||||
it('should NOT redirect from HTTP check when enabled=false', async () => {
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/memory-quiz',
|
||||
currentGame: 'memory-quiz',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
|
||||
|
||||
renderHook(() => useArcadeGuard({ enabled: false }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Should NOT redirect
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should NOT redirect from WebSocket when enabled=false', async () => {
|
||||
let onSessionStateCallback: ((data: any) => void) | null = null
|
||||
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
|
||||
onSessionStateCallback = events?.onSessionState || null
|
||||
return mockUseArcadeSocket
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard({ enabled: false }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
// Simulate session-state event from WebSocket
|
||||
onSessionStateCallback?.({
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
gameState: {},
|
||||
activePlayers: [1],
|
||||
version: 1,
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
// Should track the session
|
||||
expect(result.current.hasActiveSession).toBe(true)
|
||||
expect(result.current.activeSession).toEqual({
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
})
|
||||
})
|
||||
|
||||
// But should NOT redirect since enabled=false
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should STILL redirect from WebSocket when enabled=true', async () => {
|
||||
let onSessionStateCallback: ((data: any) => void) | null = null
|
||||
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
|
||||
onSessionStateCallback = events?.onSessionState || null
|
||||
return mockUseArcadeSocket
|
||||
})
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
})
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
|
||||
|
||||
renderHook(() => useArcadeGuard({ enabled: true }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseArcadeSocket.joinSession).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Simulate session-state event from WebSocket
|
||||
onSessionStateCallback?.({
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
gameState: {},
|
||||
activePlayers: [1],
|
||||
version: 1,
|
||||
})
|
||||
|
||||
// Should redirect when enabled=true
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/arcade/room')
|
||||
})
|
||||
})
|
||||
|
||||
it('should track session state even when enabled=false', async () => {
|
||||
const mockSession = {
|
||||
session: {
|
||||
gameUrl: '/arcade/room',
|
||||
currentGame: 'matching',
|
||||
gameState: {},
|
||||
},
|
||||
}
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useArcadeGuard({ enabled: false }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
})
|
||||
|
||||
// Should still provide session info even without redirects
|
||||
expect(result.current.hasActiveSession).toBe(false) // No fetch happened
|
||||
expect(result.current.activeSession).toBe(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,168 +0,0 @@
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ArcadeSessionResponse } from '@/app/api/arcade-session/types'
|
||||
import { useArcadeSocket } from './useArcadeSocket'
|
||||
import { useViewerId } from './useViewerId'
|
||||
|
||||
export interface UseArcadeGuardOptions {
|
||||
/**
|
||||
* Whether to enable the guard
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean
|
||||
|
||||
/**
|
||||
* Callback when redirecting to active session
|
||||
*/
|
||||
onRedirect?: (gameUrl: string) => void
|
||||
}
|
||||
|
||||
export interface UseArcadeGuardReturn {
|
||||
/**
|
||||
* Whether there's an active arcade session
|
||||
*/
|
||||
hasActiveSession: boolean
|
||||
|
||||
/**
|
||||
* Whether currently loading session state
|
||||
*/
|
||||
loading: boolean
|
||||
|
||||
/**
|
||||
* Active session details if exists
|
||||
*/
|
||||
activeSession: {
|
||||
gameUrl: string
|
||||
currentGame: string
|
||||
} | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for guarding arcade navigation and auto-resuming sessions
|
||||
*
|
||||
* Automatically redirects users to their active arcade session if they
|
||||
* navigate to a different page while a session is active.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // In arcade game pages:
|
||||
* const { hasActiveSession, loading } = useArcadeGuard()
|
||||
*
|
||||
* if (loading) return <LoadingSpinner />
|
||||
* ```
|
||||
*/
|
||||
export function useArcadeGuard(options: UseArcadeGuardOptions = {}): UseArcadeGuardReturn {
|
||||
const { enabled = true, onRedirect } = options
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { data: userId, isLoading: isLoadingUserId } = useViewerId()
|
||||
|
||||
const [hasActiveSession, setHasActiveSession] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeSession, setActiveSession] = useState<{
|
||||
gameUrl: string
|
||||
currentGame: string
|
||||
} | null>(null)
|
||||
|
||||
// WebSocket connection to listen for session changes
|
||||
const { connected, joinSession } = useArcadeSocket({
|
||||
onSessionState: (data) => {
|
||||
setHasActiveSession(true)
|
||||
setActiveSession({
|
||||
gameUrl: data.gameUrl,
|
||||
currentGame: data.currentGame,
|
||||
})
|
||||
|
||||
// Redirect if we're not already on the active game page (only if enabled)
|
||||
const isAlreadyAtTarget = pathname === data.gameUrl
|
||||
if (enabled && !isAlreadyAtTarget) {
|
||||
console.log('[ArcadeGuard] Redirecting to active session:', data.gameUrl)
|
||||
onRedirect?.(data.gameUrl)
|
||||
router.push(data.gameUrl)
|
||||
} else if (isAlreadyAtTarget) {
|
||||
console.log('[ArcadeGuard] Already at target URL, no redirect needed')
|
||||
}
|
||||
},
|
||||
|
||||
onNoActiveSession: () => {
|
||||
setHasActiveSession(false)
|
||||
setActiveSession(null)
|
||||
setLoading(false)
|
||||
},
|
||||
|
||||
onSessionEnded: () => {
|
||||
console.log('[ArcadeGuard] Session ended, clearing active session')
|
||||
setHasActiveSession(false)
|
||||
setActiveSession(null)
|
||||
},
|
||||
})
|
||||
|
||||
// Check for active session on mount and when userId changes
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (isLoadingUserId) {
|
||||
setLoading(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const checkSession = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch(`/api/arcade-session?userId=${userId}`)
|
||||
|
||||
if (response.ok) {
|
||||
const data: ArcadeSessionResponse = await response.json()
|
||||
const session = data.session // API wraps response in { session: {...} }
|
||||
|
||||
setHasActiveSession(true)
|
||||
setActiveSession({
|
||||
gameUrl: session.gameUrl,
|
||||
currentGame: session.currentGame,
|
||||
})
|
||||
|
||||
// Redirect if we're not already on the active game page (only if enabled)
|
||||
const isAlreadyAtTarget = pathname === session.gameUrl
|
||||
if (enabled && !isAlreadyAtTarget) {
|
||||
console.log('[ArcadeGuard] Redirecting to active session:', session.gameUrl)
|
||||
onRedirect?.(session.gameUrl)
|
||||
router.push(session.gameUrl)
|
||||
} else if (isAlreadyAtTarget) {
|
||||
console.log('[ArcadeGuard] Already at target URL, no redirect needed')
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
// No active session
|
||||
setHasActiveSession(false)
|
||||
setActiveSession(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ArcadeGuard] Failed to check session:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkSession()
|
||||
}, [userId, enabled, pathname, router, onRedirect, isLoadingUserId])
|
||||
|
||||
// Join WebSocket room when connected
|
||||
useEffect(() => {
|
||||
if (connected && userId) {
|
||||
joinSession(userId)
|
||||
}
|
||||
}, [connected, userId, joinSession])
|
||||
|
||||
return {
|
||||
hasActiveSession,
|
||||
loading,
|
||||
activeSession,
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useArcadeSocket } from './useArcadeSocket'
|
||||
import { useViewerId } from './useViewerId'
|
||||
|
||||
export interface UseArcadeRedirectOptions {
|
||||
/**
|
||||
* The current game this page represents (e.g., 'matching', 'memory-quiz')
|
||||
* If null, this is the arcade lobby
|
||||
*/
|
||||
currentGame?: string | null
|
||||
}
|
||||
|
||||
export interface UseArcadeRedirectReturn {
|
||||
/**
|
||||
* Whether we're checking for an active session
|
||||
*/
|
||||
isChecking: boolean
|
||||
|
||||
/**
|
||||
* Whether user has an active session
|
||||
*/
|
||||
hasActiveSession: boolean
|
||||
|
||||
/**
|
||||
* The URL of the active game (if any)
|
||||
*/
|
||||
activeGameUrl: string | null
|
||||
|
||||
/**
|
||||
* Whether players can be modified (only true in arcade lobby with no active session)
|
||||
*/
|
||||
canModifyPlayers: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to handle arcade session redirects
|
||||
*
|
||||
* - If on /arcade and user has active session → redirect to that game
|
||||
* - If on a game page and user has different active session → redirect to that game
|
||||
* - If on a game page that matches active session → allow (locked in)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // In /arcade page
|
||||
* const { canModifyPlayers } = useArcadeRedirect({ currentGame: null })
|
||||
*
|
||||
* // In /arcade/matching page
|
||||
* const { canModifyPlayers } = useArcadeRedirect({ currentGame: 'matching' })
|
||||
* ```
|
||||
*/
|
||||
export function useArcadeRedirect(options: UseArcadeRedirectOptions = {}): UseArcadeRedirectReturn {
|
||||
const { currentGame } = options
|
||||
const router = useRouter()
|
||||
const _pathname = usePathname()
|
||||
const { data: viewerId } = useViewerId()
|
||||
|
||||
const [isChecking, setIsChecking] = useState(true)
|
||||
const [hasActiveSession, setHasActiveSession] = useState(false)
|
||||
const [activeGameUrl, setActiveGameUrl] = useState<string | null>(null)
|
||||
const [_activeGameName, setActiveGameName] = useState<string | null>(null)
|
||||
|
||||
const { connected, joinSession } = useArcadeSocket({
|
||||
onSessionState: (data) => {
|
||||
console.log('[ArcadeRedirect] Got session state:', data)
|
||||
setIsChecking(false)
|
||||
setHasActiveSession(true)
|
||||
setActiveGameUrl(data.gameUrl)
|
||||
setActiveGameName(data.currentGame)
|
||||
|
||||
// Determine if we need to redirect
|
||||
const isArcadeLobby = currentGame === null || currentGame === undefined
|
||||
const isWrongGame = currentGame && currentGame !== data.currentGame
|
||||
const isAlreadyAtTarget = _pathname === data.gameUrl
|
||||
|
||||
if ((isArcadeLobby || isWrongGame) && !isAlreadyAtTarget) {
|
||||
console.log('[ArcadeRedirect] Redirecting to active game:', data.gameUrl)
|
||||
router.push(data.gameUrl)
|
||||
} else if (isAlreadyAtTarget) {
|
||||
console.log('[ArcadeRedirect] Already at target URL, no redirect needed')
|
||||
}
|
||||
},
|
||||
|
||||
onNoActiveSession: () => {
|
||||
console.log('[ArcadeRedirect] No active session')
|
||||
setIsChecking(false)
|
||||
setHasActiveSession(false)
|
||||
setActiveGameUrl(null)
|
||||
setActiveGameName(null)
|
||||
|
||||
// No redirect needed - user can navigate to any game page to start a new session
|
||||
// Only redirect when they have an active session for a different game
|
||||
},
|
||||
|
||||
onSessionEnded: () => {
|
||||
console.log('[ArcadeRedirect] Session ended')
|
||||
setHasActiveSession(false)
|
||||
setActiveGameUrl(null)
|
||||
setActiveGameName(null)
|
||||
},
|
||||
})
|
||||
|
||||
// Check for active session when connected
|
||||
useEffect(() => {
|
||||
if (connected && viewerId) {
|
||||
console.log('[ArcadeRedirect] Checking for active session')
|
||||
setIsChecking(true)
|
||||
joinSession(viewerId)
|
||||
}
|
||||
}, [connected, viewerId, joinSession])
|
||||
|
||||
// Can modify players whenever there's no active session
|
||||
// (applies to arcade lobby and game setup pages)
|
||||
const canModifyPlayers = !hasActiveSession && !isChecking
|
||||
|
||||
return {
|
||||
isChecking,
|
||||
hasActiveSession,
|
||||
activeGameUrl,
|
||||
canModifyPlayers,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user