feat: /judge review + scoring + leaderboard — TDD

Final screen — replaces the last WorkshopStub; App.tsx now has zero stubs:
- SubmissionList: live review queue with scored markers + selection
- AddReview: read-only render of a submitted 5-layer ADD
- ScoreForm: 0-10 rubric per layer, auto-summed total, notes
- Leaderboard: ranked teams by average judge total
- Judge page (behind name+code AccessGate): queue + review/score + leaderboard,
  refreshes on scoring and on any collective event

21 new tests; suite 140/140 green, typecheck + lint clean, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 19:31:36 -07:00
co-authored by Claude Opus 4.8
parent 6c07788fab
commit bd3250c9c7
11 changed files with 520 additions and 17 deletions
+82
View File
@@ -0,0 +1,82 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router-dom'
import type { SubmissionDTO } from '@/types'
const api = vi.hoisted(() => ({
getSubmissions: vi.fn(),
getSubmission: vi.fn(),
getLeaderboard: vi.fn(),
postScore: vi.fn(),
openCollective: vi.fn(() => () => {}),
}))
vi.mock('@/lib/api', () => api)
import { Judge } from './Judge'
const dto: SubmissionDTO = {
teamId: 'a',
teamName: 'Alpha',
code: 'KIT-01-AAA',
add: { L1: { goal: 'g' }, L2: 'two', L3: 'three', L4: 'four', L5: 'five' },
submittedAt: 'x',
}
function renderJudge() {
return render(
<MemoryRouter>
<Judge />
</MemoryRouter>,
)
}
describe('Judge', () => {
beforeEach(() => {
sessionStorage.clear()
sessionStorage.setItem('apess_judge_code', 'judge-code')
sessionStorage.setItem('apess_judge_code_name', 'Dr. D')
api.getSubmissions.mockResolvedValue([{ teamId: 'a', teamName: 'Alpha', submittedAt: 'x', scored: false }])
api.getLeaderboard.mockResolvedValue([{ teamId: 'a', teamName: 'Alpha', avgTotal: 0, scoreCount: 0 }])
api.getSubmission.mockResolvedValue(dto)
api.postScore.mockResolvedValue({ id: 1 })
})
it('lists the submission queue and leaderboard on load', async () => {
renderJudge()
await waitFor(() => expect(screen.getByTestId('submission-list')).toHaveTextContent('Alpha'))
expect(screen.getByTestId('leaderboard')).toBeInTheDocument()
})
it('loads a submission for review when selected', async () => {
const user = userEvent.setup()
renderJudge()
await waitFor(() => expect(screen.getByTestId('submission-list')).toBeInTheDocument())
await user.click(within(screen.getByTestId('submission-list')).getByText('Alpha'))
await waitFor(() => expect(screen.getByTestId('add-review')).toHaveTextContent('KIT-01-AAA'))
})
it('posts a score and refreshes', async () => {
const user = userEvent.setup()
renderJudge()
await waitFor(() => expect(screen.getByTestId('submission-list')).toBeInTheDocument())
await user.click(within(screen.getByTestId('submission-list')).getByText('Alpha'))
await waitFor(() => expect(screen.getByTestId('score-form')).toBeInTheDocument())
const perception = screen.getByLabelText(/perception/i)
await user.clear(perception)
await user.type(perception, '7')
await user.click(screen.getByRole('button', { name: /submit score/i }))
await waitFor(() => expect(api.postScore).toHaveBeenCalled())
const arg = api.postScore.mock.calls[0]
expect(arg[0]).toBe('judge-code')
expect(arg[1]).toMatchObject({ teamId: 'a', judge: 'Dr. D', total: 7 })
})
it('gates behind name + code when none stored', () => {
sessionStorage.clear()
renderJudge()
expect(screen.getByRole('button', { name: /unlock/i })).toBeInTheDocument()
})
})