Files
MOPC-Portal/tests/unit/live-vote-comment.test.ts
Matt c9dc1bfabd feat(finale): deliberation jury identity resolution, rankable projects, score-revision path, session sync
- submitVote resolves the caller's JuryGroupMember participant row server-side
  (was comparing JuryGroupMember id to User id — every juror got FORBIDDEN)
- getSessionWithVotes now includes category projects so the ranking form has
  data before finalize
- liveVoting.vote accepts any finale-ordered project (revision during
  deliberation); timed window still applies to the live project
- live.sendToScreens keeps LiveVotingSession.currentProjectId/status in sync

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:15:45 +02:00

171 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Grand-finale juror voting extras:
* - optional overall comment stored with the LiveVote (and updatable)
* - getSessionForVotingByRound: the jury page only knows the roundId
* - getMyFinaleInputs: a juror's own finale votes + ceremony notes,
* resurfaced during deliberation
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createCaller } from '../setup'
import {
createTestUser,
createTestProgram,
createTestCompetition,
createTestRound,
createTestProject,
cleanupTestData,
uid,
} from '../helpers'
import { liveVotingRouter } from '@/server/routers/live-voting'
let program: any
let round: any
let emptyRound: any
let session: any
let project: any
let juror: any
let jurorCaller: ReturnType<typeof createCaller>
beforeAll(async () => {
program = await createTestProgram()
const competition = await createTestCompetition(program.id)
const juryGroup = await prisma.juryGroup.create({
data: { competitionId: competition.id, name: 'Finals Jury', slug: uid('jg') },
})
round = await createTestRound(competition.id, {
roundType: 'LIVE_FINAL',
status: 'ROUND_ACTIVE',
})
await prisma.round.update({ where: { id: round.id }, data: { juryGroupId: juryGroup.id } })
emptyRound = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', sortOrder: 1 })
project = await createTestProject(program.id, { competitionCategory: 'STARTUP' })
juror = await createTestUser('JURY_MEMBER')
await prisma.juryGroupMember.create({
data: { juryGroupId: juryGroup.id, userId: juror.id, role: 'MEMBER' },
})
session = await prisma.liveVotingSession.create({
data: {
roundId: round.id,
status: 'IN_PROGRESS',
currentProjectId: project.id,
votingMode: 'simple',
},
})
jurorCaller = createCaller(liveVotingRouter, juror)
})
afterAll(async () => {
await cleanupTestData(program.id, [juror.id])
})
describe('vote comments', () => {
it('persists an optional comment with the vote', async () => {
const vote = await jurorCaller.vote({
sessionId: session.id,
projectId: project.id,
score: 8,
comment: 'Strong pitch, weak unit economics',
})
expect(vote.comment).toBe('Strong pitch, weak unit economics')
})
it('re-voting updates the comment and keeps it when omitted', async () => {
const updated = await jurorCaller.vote({
sessionId: session.id,
projectId: project.id,
score: 9,
comment: 'Revised after Q&A',
})
expect(updated.score).toBe(9)
expect(updated.comment).toBe('Revised after Q&A')
const again = await jurorCaller.vote({
sessionId: session.id,
projectId: project.id,
score: 7,
})
expect(again.comment).toBe('Revised after Q&A') // omitted comment is not erased
})
})
describe('deliberation-time revision', () => {
it('allows voting for an ordered project that is not currently presenting', async () => {
const otherProject = await createTestProject(program.id, {
competitionCategory: 'BUSINESS_CONCEPT',
})
await prisma.round.update({
where: { id: round.id },
data: { configJson: { projectOrder: [project.id, otherProject.id] } },
})
// otherProject is NOT currentProjectId — revision path must accept it
const vote = await jurorCaller.vote({
sessionId: session.id,
projectId: otherProject.id,
score: 6,
comment: 'revised during deliberation',
})
expect(vote.projectId).toBe(otherProject.id)
})
it('still rejects projects outside the finale order', async () => {
const stranger = await createTestProject(program.id)
await expect(
jurorCaller.vote({ sessionId: session.id, projectId: stranger.id, score: 5 })
).rejects.toThrow()
})
it('allows revision while the session is PAUSED', async () => {
await prisma.liveVotingSession.update({
where: { id: session.id },
data: { status: 'PAUSED' },
})
const vote = await jurorCaller.vote({
sessionId: session.id,
projectId: project.id,
score: 10,
})
expect(vote.score).toBe(10)
await prisma.liveVotingSession.update({
where: { id: session.id },
data: { status: 'IN_PROGRESS' },
})
})
})
describe('getSessionForVotingByRound', () => {
it('resolves the session from a roundId', async () => {
const data = await jurorCaller.getSessionForVotingByRound({ roundId: round.id })
expect(data?.session.id).toBe(session.id)
expect(data?.currentProject?.id).toBe(project.id)
expect(data?.userVote?.score).toBe(10) // last revision in the PAUSED test
})
it('returns null when the round has no session (creates nothing)', async () => {
const data = await jurorCaller.getSessionForVotingByRound({ roundId: emptyRound.id })
expect(data).toBeNull()
const count = await prisma.liveVotingSession.count({ where: { roundId: emptyRound.id } })
expect(count).toBe(0)
})
})
describe('getMyFinaleInputs', () => {
it('returns the callers votes and notes for the round', async () => {
await prisma.liveNote.create({
data: { roundId: round.id, projectId: project.id, userId: juror.id, content: 'ceremony note' },
})
const inputs = await jurorCaller.getMyFinaleInputs({ roundId: round.id })
expect(inputs.session?.id).toBe(session.id)
expect(inputs.votes).toHaveLength(2) // original + deliberation-time revision
const mainVote = inputs.votes.find((v: any) => v.projectId === project.id)
expect(mainVote?.comment).toBe('Revised after Q&A')
expect(inputs.notes).toHaveLength(1)
expect(inputs.notes[0].content).toBe('ceremony note')
})
it('is empty-safe for a round without a session', async () => {
const inputs = await jurorCaller.getMyFinaleInputs({ roundId: emptyRound.id })
expect(inputs.session).toBeNull()
expect(inputs.votes).toHaveLength(0)
})
})