Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/apps/review/src/lib/hooks/useFetchChallengeInfo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { BackendPhase, ChallengeInfo } from '../models'

import { getChallengeInfoRefreshInterval } from './useFetchChallengeInfo'

jest.mock('~/apps/admin/src/lib/utils', () => ({
handleError: jest.fn(),
}), { virtual: true })

jest.mock('../services', () => ({
fetchChallengeInfoById: jest.fn(),
}))

const checkpointReviewPhase: BackendPhase = {
constraints: [],
description: 'Select checkpoint winners',
duration: 3_600,
id: 'checkpoint-review-phase',
isOpen: true,
name: 'Checkpoint Review',
phaseId: 'checkpoint-review-phase-type',
scheduledEndDate: '2026-08-01T01:00:00.000Z',
scheduledStartDate: '2026-08-01T00:00:00.000Z',
}

/**
* Build the minimum challenge data needed to exercise refresh-interval selection.
*
* @param overrides challenge fields to replace for the current test case
* @returns a complete ChallengeInfo fixture
*/
const buildChallengeInfo = (
overrides: Partial<ChallengeInfo> = {},
): ChallengeInfo => ({
checkpointWinners: [],
currentPhase: 'Checkpoint Review',
currentPhaseEndDate: '2026-08-01T01:00:00.000Z',
id: 'challenge-1',
name: 'Checkpoint challenge',
phases: [checkpointReviewPhase],
status: 'ACTIVE',
submissions: [],
track: {
id: 'track-1',
name: 'Design',
},
type: {
id: 'type-1',
name: 'Challenge',
},
typeId: 'type-1',
...overrides,
} as ChallengeInfo)

describe('getChallengeInfoRefreshInterval', () => {
it('polls while an active Checkpoint Review can still be hiding winners', () => {
expect(getChallengeInfoRefreshInterval(buildChallengeInfo()))
.toBe(10_000)
})

it('stops polling after Checkpoint Review closes', () => {
expect(getChallengeInfoRefreshInterval(buildChallengeInfo({
currentPhase: 'Submission',
phases: [{
...checkpointReviewPhase,
isOpen: false,
}],
})))
.toBe(0)
})

it('stops polling when checkpoint winners are already available', () => {
expect(getChallengeInfoRefreshInterval(buildChallengeInfo({
checkpointWinners: [{
handle: 'winner',
placement: 1,
userId: 123,
}],
})))
.toBe(0)
})

it('does not poll completed challenges or unrelated phases', () => {
expect(getChallengeInfoRefreshInterval(buildChallengeInfo({ status: 'COMPLETED' })))
.toBe(0)
expect(getChallengeInfoRefreshInterval(buildChallengeInfo({
currentPhase: 'Review',
phases: [{
...checkpointReviewPhase,
id: 'review-phase',
name: 'Review',
phaseId: 'review-phase-type',
}],
})))
.toBe(0)
})
})
31 changes: 30 additions & 1 deletion src/apps/review/src/lib/hooks/useFetchChallengeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,39 @@ import useSWR, { SWRResponse } from 'swr'

import { handleError } from '~/apps/admin/src/lib/utils'

import {
import type {
ChallengeInfo,
} from '../models'
import { fetchChallengeInfoById } from '../services'

const CHECKPOINT_REVIEW_PHASE_NAME = 'checkpoint review'
const CHECKPOINT_WINNER_REFRESH_INTERVAL_MS = 10_000

/**
* Select the challenge-info polling interval used while checkpoint winners are pending.
* The Challenge API intentionally hides checkpoint winners until Checkpoint Review closes,
* so active pages poll during that phase and stop as soon as the refreshed response changes.
*
* @param challengeInfo latest challenge response held by SWR
* @returns polling interval in milliseconds, or zero when no refresh is needed
*/
export function getChallengeInfoRefreshInterval(
challengeInfo?: ChallengeInfo,
): number {
const isActive = challengeInfo?.status?.trim()
.toUpperCase() === 'ACTIVE'
const hasCheckpointWinners = Boolean(challengeInfo?.checkpointWinners?.length)
const hasOpenCheckpointReview = challengeInfo?.phases?.some(phase => (
phase.isOpen === true
&& phase.name?.trim()
.toLowerCase() === CHECKPOINT_REVIEW_PHASE_NAME
)) ?? false

return isActive && !hasCheckpointWinners && hasOpenCheckpointReview
? CHECKPOINT_WINNER_REFRESH_INTERVAL_MS
: 0
}

export interface useFetchChallengeInfoProps {
challengeInfo: ChallengeInfo | undefined
error: Error | undefined
Expand Down Expand Up @@ -41,6 +69,7 @@ export function useFetchChallengeInfo(
{
fetcher: () => fetchChallengeInfoById(challengeId ?? ''),
isPaused: () => !challengeId,
refreshInterval: getChallengeInfoRefreshInterval,
},
)

Expand Down
Loading