From 3b7b42210f7555299aa1422a9fe69bb8c51e8b1f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 11 Sep 2026 16:18:19 +1000 Subject: [PATCH] PM-6307: cover multiple Design checkpoint submissions --- src/apps/review/README.md | 13 ++ .../TableCheckpointSubmissions.spec.tsx | 157 +++++++++++++++--- .../src/lib/utils/reviewProgress.spec.ts | 31 ++++ 3 files changed, 180 insertions(+), 21 deletions(-) diff --git a/src/apps/review/README.md b/src/apps/review/README.md index 6cc05479b..36e9b265a 100644 --- a/src/apps/review/README.md +++ b/src/apps/review/README.md @@ -22,6 +22,19 @@ sudo yarn start - Mock data files are under src/apps/review/src/mock-datas +### Design checkpoint screening: + +- Checkpoint Screening keeps each submission returned by the Review API visible, + including an older submission from the same member when the Design submission + limit is greater than one. Each pending row links to its own screening scorecard + on desktop and mobile. +- The submissions API must return the configured review window to assigned Design + screeners and reviewers, ranking checkpoint and final submissions independently. + An `isLatest: false` submission within that window is still eligible for review. + The frontend cannot recover rows removed by the API's history-privacy filter. +- PM-6307 regression coverage uses two submissions from one member, with the older + screening still pending, plus a completed submission from another member. + ### Winners result identity: - The Winners tab loads every page from the Review API `projectResult` endpoint. diff --git a/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.spec.tsx b/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.spec.tsx index 476dbbbc0..3e81f6257 100644 --- a/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.spec.tsx +++ b/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.spec.tsx @@ -12,9 +12,44 @@ import type { ReviewAppContextModel, Screening, } from '../../models' +import { TabContentCheckpoint } from '../ChallengeDetailsContent/TabContentCheckpoint' import { TableCheckpointSubmissions } from './TableCheckpointSubmissions' +let mockScreenWidth = 1200 + +interface TestColumn { + label?: ReactNode + renderer?: (row: Screening, rows: Screening[]) => JSX.Element +} + +/** + * Renders checkpoint IDs, scores, and actions using the table's real column renderers. + * Shared by desktop/mobile table mocks to verify row visibility and scorecard navigation. + * + * @param props - Visible rows and desktop or grouped mobile column definitions. + * @returns The relevant rendered cells for each submission. + * @throws Propagates errors from the production column renderers. + */ +function mockRenderTable(props: { columns: Array, data: Screening[] }): JSX.Element { + const columns = props.columns.flat() + .filter(column => ['Submission ID', 'Screening Score', 'Review Score', 'Action'].includes(String(column.label))) + + return ( +
+ {props.data.map(row => ( +
+ {columns.map(column => ( +
+ {column.renderer?.(row, props.data)} +
+ ))} +
+ ))} +
+ ) +} + jest.mock('react-router-dom', () => ({ Link: (props: PropsWithChildren<{ className?: string, to: string }>) => ( {props.children} @@ -37,12 +72,16 @@ jest.mock('~/libs/shared', () => ({ copyTextToClipboard: () => Promise.resolve(), useWindowSize: () => ({ height: 800, - width: 1200, + width: mockScreenWidth, }), }), { virtual: true }) jest.mock('~/apps/admin/src/lib/components/common/TableMobile', () => ({ - TableMobile: () =>
Mobile table
, + TableMobile: (props: { columns: TestColumn[][], data: Screening[] }) => mockRenderTable(props), +}), { virtual: true }) + +jest.mock('~/apps/admin/src/lib', () => ({ + TableLoading: () =>
Loading
, }), { virtual: true }) jest.mock('~/apps/admin/src/lib/utils', () => ({ @@ -57,25 +96,7 @@ jest.mock('~/libs/ui', () => ({ IconSolid: { StarIcon: () => , }, - Table: (props: { - columns: Array<{ - label?: ReactNode - renderer?: (row: Screening, rows: Screening[]) => JSX.Element - }> - data: Screening[] - }) => { - const scoreColumn = props.columns.find(column => column.label === 'Review Score') - - return ( -
- {props.data.map(row => ( -
- {scoreColumn?.renderer?.(row, props.data)} -
- ))} -
- ) - }, + Table: (props: { columns: TestColumn[], data: Screening[] }) => mockRenderTable(props), Tooltip: (props: PropsWithChildren<{ content?: ReactNode triggerOn?: string @@ -100,6 +121,7 @@ jest.mock('../../contexts', () => { }) jest.mock('../../hooks', () => ({ + useRole: jest.requireActual('../../hooks/useRole').useRole, useRolePermissions: () => ({ canViewAllSubmissions: true, }), @@ -195,6 +217,7 @@ const challengeInfo = { const challengeContext = { challengeInfo, + duplicatesBySubmissionId: {}, myResources: [], myRoles: [], } as unknown as ChallengeDetailContextModel @@ -231,6 +254,10 @@ function renderCheckpointTable(): ReturnType { } describe('TableCheckpointSubmissions checkpoint winner indicator', () => { + beforeEach(() => { + mockScreenWidth = 1200 + }) + it('marks only passing rows whose member id matches a checkpoint winner', () => { renderCheckpointTable() @@ -260,3 +287,91 @@ describe('TableCheckpointSubmissions checkpoint winner indicator', () => { .toBe('./../reviews/non-winner-submission?reviewId=non-winner-review') }) }) + +describe('checkpoint screening multiple Design submissions (PM-6307)', () => { + const screenings: Screening[] = [ + { + ...winnerRow, + isLatest: true, + myReviewId: 'review-new', + myReviewResourceId: 'checkpoint-screener', + myReviewStatus: 'COMPLETED', + reviewId: 'review-new', + reviewStatus: 'COMPLETED', + submissionId: 'checkpoint-new', + type: 'CHECKPOINT_SUBMISSION', + }, + { + ...winnerRow, + isLatest: false, + myReviewId: 'review-pending', + myReviewResourceId: 'checkpoint-screener', + myReviewStatus: 'PENDING', + result: '-', + reviewId: 'review-pending', + reviewStatus: 'PENDING', + score: 'Pending', + submissionId: 'checkpoint-pending', + type: 'CHECKPOINT_SUBMISSION', + }, + { + ...nonWinnerRow, + isLatest: true, + myReviewId: 'review-other', + myReviewResourceId: 'checkpoint-screener', + myReviewStatus: 'COMPLETED', + reviewId: 'review-other', + reviewStatus: 'COMPLETED', + type: 'CHECKPOINT_SUBMISSION', + }, + ] + + it.each([1200, 640])('keeps the older pending submission actionable at width %s', width => { + mockScreenWidth = width + const context = { + ...challengeContext, + challengeId: 'challenge-id', + challengeInfo: { + ...challengeInfo, + currentPhase: 'Checkpoint Screening', + metadata: [{ + name: 'submissionLimit', + value: '{"count":"2","limit":"true","unlimited":"false"}', + }], + status: 'Active', + }, + myResources: [{ + id: 'checkpoint-screener', + memberId: 'screener-member', + roleName: 'Checkpoint Screener', + }], + myRoles: ['Checkpoint Screener'], + } as ChallengeDetailContextModel + + render( + + + + + , + ) + + screenings.forEach(row => { + expect(screen.getByRole('button', { name: row.submissionId })) + .toBeTruthy() + }) + const pendingRow = within(screen.getByTestId('checkpoint-pending')) + expect(pendingRow.getByRole('link', { name: 'Complete Screening' }) + .getAttribute('href')) + .toBe('./../reviews/checkpoint-pending?reviewId=review-pending') + expect(pendingRow.getByRole('link', { name: 'Pending' }) + .getAttribute('href')) + .toBe('./../reviews/checkpoint-pending?reviewId=review-pending') + }) +}) diff --git a/src/apps/review/src/lib/utils/reviewProgress.spec.ts b/src/apps/review/src/lib/utils/reviewProgress.spec.ts index 9bd2a92ca..3a0d3e218 100644 --- a/src/apps/review/src/lib/utils/reviewProgress.spec.ts +++ b/src/apps/review/src/lib/utils/reviewProgress.spec.ts @@ -181,6 +181,37 @@ describe('calculateReviewProgress', () => { .toBe(50) }) + it('counts an older pending Design checkpoint submission toward progress (PM-6307)', () => { + const progress = calculateReviewProgress({ + challengePhases: [createPhase('Checkpoint Screening')], + checkpointReviewRows: [], + checkpointScreeningRows: [ + createScreeningRow('checkpoint-new', 'PASS', { + isLatest: true, + memberId: 'member-one', + reviewStatus: 'COMPLETED', + }), + createScreeningRow('checkpoint-older', '-', { + isLatest: false, + memberId: 'member-one', + reviewStatus: 'PENDING', + }), + createScreeningRow('checkpoint-other', 'PASS', { + isLatest: true, + memberId: 'member-two', + reviewStatus: 'COMPLETED', + }), + ], + currentPhaseName: 'Checkpoint Screening', + isDesignChallenge: true, + reviewRows: [], + screeningRows: [], + }) + + expect(progress) + .toBe(67) + }) + it('counts each reviewer assignment in multi-screener phase progress', () => { const screeningRows: Screening[] = [ createScreeningRow('submission-one', '-', {