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
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { SubmissionList } from './SubmissionList'
import type { SubmissionSummary } from '@/types'
const items: SubmissionSummary[] = [
{ teamId: 'a', teamName: 'Alpha', submittedAt: 'x', scored: false },
{ teamId: 'b', teamName: 'Bravo', submittedAt: 'y', scored: true },
]
describe('SubmissionList', () => {
it('shows an empty state with no items', () => {
render(<SubmissionList items={[]} onSelect={() => {}} />)
expect(screen.getByText(/no submissions/i)).toBeInTheDocument()
})
it('renders rows, marks scored, and fires onSelect', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
render(<SubmissionList items={items} selectedId="a" onSelect={onSelect} />)
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText(/scored/i)).toBeInTheDocument()
expect(screen.getByRole('button', { current: true })).toHaveTextContent('Alpha')
await user.click(screen.getByText('Bravo'))
expect(onSelect).toHaveBeenCalledWith('b')
})
})