feat(user): context-aware default dashboard (§D.1)
user.getDefaultDashboard returns the highest-priority role for which the user has actionable work right now — pending eval in active round, active mentoring assignment, applicant project in active round, etc. — falling back to static priority order if nothing is actionable. src/app/page.tsx now reads roles[] (multi-role array) instead of just the primary role, fixing the bug where mentor+juror users always landed on their primary role's dashboard. Uses static priority for simplicity in the server component; the context-aware procedure remains available for client surfaces. Tests cover six cases: super-admin, juror with active eval, juror+observer fallback, mentor+juror in mentoring round, both-active-priority-tiebreak, observer-only. Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
This commit is contained in:
150
tests/unit/default-dashboard.test.ts
Normal file
150
tests/unit/default-dashboard.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { prisma, createCaller } from '../setup'
|
||||
import {
|
||||
createTestUser,
|
||||
createTestProgram,
|
||||
createTestProject,
|
||||
createTestCompetition,
|
||||
createTestRound,
|
||||
createTestProjectRoundState,
|
||||
cleanupTestData,
|
||||
uid,
|
||||
} from '../helpers'
|
||||
import { userRouter } from '../../src/server/routers/user'
|
||||
import type { UserRole } from '@prisma/client'
|
||||
|
||||
async function createUserWithRoles(primaryRole: UserRole, rolesArray: UserRole[]) {
|
||||
const id = uid('user')
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
id,
|
||||
email: `${id}@test.local`,
|
||||
name: `Test ${primaryRole}`,
|
||||
role: primaryRole,
|
||||
roles: rolesArray,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('user.getDefaultDashboard', () => {
|
||||
const programIds: string[] = []
|
||||
const userIds: string[] = []
|
||||
|
||||
afterAll(async () => {
|
||||
for (const programId of programIds) {
|
||||
await prisma.mentorAssignment.deleteMany({ where: { project: { programId } } })
|
||||
await cleanupTestData(programId, [])
|
||||
}
|
||||
if (userIds.length > 0) {
|
||||
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
|
||||
}
|
||||
})
|
||||
|
||||
it('returns /admin for SUPER_ADMIN unconditionally', async () => {
|
||||
const u = await createUserWithRoles('SUPER_ADMIN', ['SUPER_ADMIN'])
|
||||
userIds.push(u.id)
|
||||
const caller = createCaller(userRouter, { id: u.id, email: u.email, role: 'SUPER_ADMIN' })
|
||||
const result = await caller.getDefaultDashboard()
|
||||
expect(result.path).toBe('/admin')
|
||||
expect(result.role).toBe('SUPER_ADMIN')
|
||||
})
|
||||
|
||||
it('returns /jury for a juror with a pending assignment in an active round', async () => {
|
||||
const u = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER', 'OBSERVER'])
|
||||
userIds.push(u.id)
|
||||
const program = await createTestProgram({ name: `default-juror-active-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const competition = await createTestCompetition(program.id, { status: 'ACTIVE' })
|
||||
const round = await createTestRound(competition.id, {
|
||||
roundType: 'EVALUATION',
|
||||
status: 'ROUND_ACTIVE',
|
||||
})
|
||||
const project = await createTestProject(program.id)
|
||||
await createTestProjectRoundState(project.id, round.id)
|
||||
await prisma.assignment.create({
|
||||
data: { userId: u.id, projectId: project.id, roundId: round.id, method: 'MANUAL' },
|
||||
})
|
||||
const caller = createCaller(userRouter, { id: u.id, email: u.email, role: 'JURY_MEMBER' })
|
||||
const result = await caller.getDefaultDashboard()
|
||||
expect(result.path).toBe('/jury')
|
||||
})
|
||||
|
||||
it('falls back to /jury (static priority) when juror+observer has no active work', async () => {
|
||||
const u = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER', 'OBSERVER'])
|
||||
userIds.push(u.id)
|
||||
const caller = createCaller(userRouter, { id: u.id, email: u.email, role: 'JURY_MEMBER' })
|
||||
const result = await caller.getDefaultDashboard()
|
||||
expect(result.path).toBe('/jury')
|
||||
expect(result.reason).toBe('static-fallback')
|
||||
})
|
||||
|
||||
it('returns /mentor for a mentor+juror with active mentoring but no jury work', async () => {
|
||||
const u = await createUserWithRoles('MENTOR', ['MENTOR', 'JURY_MEMBER'])
|
||||
userIds.push(u.id)
|
||||
const program = await createTestProgram({ name: `default-mentor-active-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const competition = await createTestCompetition(program.id, { status: 'ACTIVE' })
|
||||
const round = await createTestRound(competition.id, {
|
||||
roundType: 'MENTORING',
|
||||
status: 'ROUND_ACTIVE',
|
||||
})
|
||||
const project = await createTestProject(program.id)
|
||||
await createTestProjectRoundState(project.id, round.id)
|
||||
await prisma.mentorAssignment.create({
|
||||
data: {
|
||||
projectId: project.id,
|
||||
mentorId: u.id,
|
||||
method: 'MANUAL',
|
||||
workspaceEnabled: true,
|
||||
},
|
||||
})
|
||||
const caller = createCaller(userRouter, { id: u.id, email: u.email, role: 'MENTOR' })
|
||||
const result = await caller.getDefaultDashboard()
|
||||
expect(result.path).toBe('/mentor')
|
||||
})
|
||||
|
||||
it('returns /jury when both jury and mentor work are active (jury wins on priority)', async () => {
|
||||
const u = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER', 'MENTOR'])
|
||||
userIds.push(u.id)
|
||||
const program = await createTestProgram({ name: `default-both-active-${uid()}` })
|
||||
programIds.push(program.id)
|
||||
const competition = await createTestCompetition(program.id, { status: 'ACTIVE' })
|
||||
const evalRound = await createTestRound(competition.id, {
|
||||
roundType: 'EVALUATION',
|
||||
status: 'ROUND_ACTIVE',
|
||||
sortOrder: 0,
|
||||
})
|
||||
const mentRound = await createTestRound(competition.id, {
|
||||
roundType: 'MENTORING',
|
||||
status: 'ROUND_ACTIVE',
|
||||
sortOrder: 1,
|
||||
})
|
||||
const projectE = await createTestProject(program.id)
|
||||
const projectM = await createTestProject(program.id)
|
||||
await createTestProjectRoundState(projectE.id, evalRound.id)
|
||||
await createTestProjectRoundState(projectM.id, mentRound.id)
|
||||
await prisma.assignment.create({
|
||||
data: { userId: u.id, projectId: projectE.id, roundId: evalRound.id, method: 'MANUAL' },
|
||||
})
|
||||
await prisma.mentorAssignment.create({
|
||||
data: {
|
||||
projectId: projectM.id,
|
||||
mentorId: u.id,
|
||||
method: 'MANUAL',
|
||||
workspaceEnabled: true,
|
||||
},
|
||||
})
|
||||
const caller = createCaller(userRouter, { id: u.id, email: u.email, role: 'JURY_MEMBER' })
|
||||
const result = await caller.getDefaultDashboard()
|
||||
expect(result.path).toBe('/jury')
|
||||
})
|
||||
|
||||
it('returns /observer for an observer-only user (static fallback)', async () => {
|
||||
const u = await createUserWithRoles('OBSERVER', ['OBSERVER'])
|
||||
userIds.push(u.id)
|
||||
const caller = createCaller(userRouter, { id: u.id, email: u.email, role: 'OBSERVER' })
|
||||
const result = await caller.getDefaultDashboard()
|
||||
expect(result.path).toBe('/observer')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user