diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx new file mode 100644 index 000000000..f0d58c0b4 --- /dev/null +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.spec.tsx @@ -0,0 +1,238 @@ +/* eslint-disable global-require, import/no-extraneous-dependencies */ +/* eslint-disable @typescript-eslint/no-var-requires, ordered-imports/ordered-imports */ +import { + fireEvent, + render, + screen, +} from '@testing-library/react' + +import type { + Challenge, + Submission, +} from '../../../../../lib/models' + +import { SubmissionsSection } from './SubmissionsSection' + +const mockUseDownloadAllSubmissions = jest.fn() +const mockUseDownloadSubmission = jest.fn() +const mockUseFetchSubmissions = jest.fn() +const mockFetchMembersByUserIds = jest.fn() +const mockIsMarathonMatchChallenge = jest.fn() + +jest.mock('~/libs/ui', () => ({ + Button: (props: { + disabled?: boolean + label: string + onClick?: () => void + }): JSX.Element => ( + + ), +}), { + virtual: true, +}) + +jest.mock('../../../../../lib/assets/icons/lock.svg', () => ({ + ReactComponent: (): JSX.Element => , +})) + +jest.mock('../../../../../lib/components', () => ({ + ArtifactsModal: (): JSX.Element =>
Artifacts modal
, + Pagination: (props: { total: number }): JSX.Element => ( +
{props.total}
+ ), + SubmissionRunnerLogsModal: (): JSX.Element =>
Runner logs modal
, + SubmissionsTable: (props: { submissions: Submission[] }): JSX.Element => ( +
+ {props.submissions.map(submission => ( +
+ {submission.id} + {submission.memberHandle} +
+ ))} +
+ ), +})) + +jest.mock('../../../../../lib/constants', () => ({ + PAGE_SIZE: 10, +})) + +jest.mock('../../../../../lib/contexts', () => { + const React = require('react') as typeof import('react') + + return { + WorkAppContext: React.createContext({ + isAdmin: false, + isAnonymous: false, + isCopilot: false, + isManager: false, + isReadOnly: false, + loginUserInfo: undefined, + userRoles: [], + }), + } +}) + +jest.mock('../../../../../lib/hooks', () => ({ + useDownloadAllSubmissions: (): unknown => mockUseDownloadAllSubmissions(), + useDownloadSubmission: (): unknown => mockUseDownloadSubmission(), + useFetchSubmissions: (...args: unknown[]): unknown => mockUseFetchSubmissions(...args), +})) + +jest.mock('../../../../../lib/services', () => ({ + fetchMembersByUserIds: (...args: unknown[]): unknown => mockFetchMembersByUserIds(...args), +})) + +jest.mock('../../../../../lib/utils', () => ({ + canDownloadSubmissions: (): boolean => false, + canViewMarathonMatchRunnerLogs: (): boolean => false, + getSubmissionFinalScore: (): number => 0, + getSubmissionInitialScore: (): number => 0, + getSubmissionProvisionalScore: (): number => 0, + getSubmissionSystemScore: (): number => 0, + getSubmissionTestProgress: ( + submission: { + reviewSummation?: Array<{ + metadata?: { + testProcess?: string + testType?: string + } + }> + }, + ): { process?: string } => { + const metadata = submission.reviewSummation?.[0]?.metadata + + return { + process: metadata?.testProcess ?? metadata?.testType, + } + }, + isMarathonMatchChallenge: (...args: unknown[]): unknown => mockIsMarathonMatchChallenge(...args), + showErrorToast: jest.fn(), +})) + +jest.mock('./SubmissionsSection.module.scss', () => new Proxy({}, { + get: (_target, property) => String(property), +})) + +const baseChallenge: Challenge = { + id: 'challenge-1', + name: 'Marathon Match', + status: 'ACTIVE', +} + +const submissions: Submission[] = [ + { + challengeId: 'challenge-1', + createdAt: '2026-07-01T10:00:00.000Z', + createdBy: 'member-1', + id: 'system-submission', + memberHandle: 'system-user', + reviewSummation: [ + { + metadata: { + testProcess: 'system', + }, + }, + ], + type: 'SUBMISSION', + }, + { + challengeId: 'challenge-1', + createdAt: '2026-07-01T11:00:00.000Z', + createdBy: 'member-2', + id: 'provisional-submission', + memberHandle: 'provisional-user', + reviewSummation: [ + { + metadata: { + testProcess: 'provisional', + }, + }, + ], + type: 'SUBMISSION', + }, + { + challengeId: 'challenge-1', + createdAt: '2026-07-01T12:00:00.000Z', + createdBy: 'member-3', + id: 'example-submission', + memberHandle: 'example-user', + reviewSummation: [ + { + metadata: { + testType: 'example', + }, + }, + ], + type: 'SUBMISSION', + }, +] + +describe('SubmissionsSection', () => { + beforeEach(() => { + jest.clearAllMocks() + + mockIsMarathonMatchChallenge.mockReturnValue(true) + mockUseDownloadAllSubmissions.mockReturnValue({ + downloadAll: jest.fn(), + isDownloading: false, + progress: 0, + }) + mockUseDownloadSubmission.mockReturnValue({ + downloadSubmission: jest.fn(), + isLoading: {}, + }) + mockUseFetchSubmissions.mockReturnValue({ + error: undefined, + isError: false, + isLoading: false, + mutate: jest.fn(), + submissions, + total: submissions.length, + }) + mockFetchMembersByUserIds.mockResolvedValue([]) + }) + + it('filters marathon submissions by test type', () => { + render( + , + ) + + fireEvent.change(screen.getByLabelText('Test type'), { + target: { + value: 'system', + }, + }) + + expect(screen.getByText('system-submission')) + .toBeTruthy() + expect(screen.queryByText('provisional-submission')) + .toBeNull() + expect(screen.queryByText('example-submission')) + .toBeNull() + expect(screen.getByTestId('pagination-total').textContent) + .toBe('1') + + fireEvent.change(screen.getByLabelText('Test type'), { + target: { + value: 'example', + }, + }) + + expect(screen.getByText('example-submission')) + .toBeTruthy() + expect(screen.queryByText('system-submission')) + .toBeNull() + expect(screen.queryByText('provisional-submission')) + .toBeNull() + }) +}) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx index 70662c63d..f73961cd5 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/SubmissionsSection/SubmissionsSection.tsx @@ -36,6 +36,7 @@ import { getSubmissionInitialScore, getSubmissionProvisionalScore, getSubmissionSystemScore, + getSubmissionTestProgress, isMarathonMatchChallenge, showErrorToast, } from '../../../../../lib/utils' @@ -57,6 +58,7 @@ interface FilterState { handle: string minScore: string startDate: string + testType: string } interface SubmissionsSectionProps { @@ -330,6 +332,37 @@ function matchesFilterHandle(submission: Submission, handleFilter: string): bool .includes(normalizedHandleFilter) } +/** + * Checks whether a submission matches the selected marathon test type. + * @param submission Submission row being evaluated. + * @param testTypeFilter Selected normalized test type filter value. + * @param useMarathonMatchScores Whether the current challenge is a Marathon Match. + * @returns True when the filter is empty, the challenge is not Marathon Match, or the current test type matches. + * Used by `matchesFilters` before the submissions table is sorted and paginated. + */ +function matchesFilterTestType( + submission: Submission, + testTypeFilter: string, + useMarathonMatchScores: boolean, +): boolean { + if (!testTypeFilter || !useMarathonMatchScores) { + return true + } + + const normalizedTestTypeFilter = normalizeValue(testTypeFilter) + .toLowerCase() + + return getSubmissionTestProgress(submission).process === normalizedTestTypeFilter +} + +/** + * Checks whether a submission satisfies all active submissions list filters. + * @param submission Submission row being evaluated. + * @param filters Current filter values from the submissions form. + * @param useMarathonMatchScores Whether the current challenge is a Marathon Match. + * @returns True when the submission should remain visible in the table. + * Used before sorting and paginating the submissions list. + */ function matchesFilters( submission: Submission, filters: FilterState, @@ -343,6 +376,10 @@ function matchesFilters( return false } + if (!matchesFilterTestType(submission, filters.testType, useMarathonMatchScores)) { + return false + } + return matchesFilterScore(submission, filters.minScore, useMarathonMatchScores) } @@ -361,6 +398,7 @@ export const SubmissionsSection: FC = ( handle: '', minScore: '', startDate: '', + testType: '', }) const [memberCache, setMemberCache] = useState({}) const [page, setPage] = useState(1) @@ -584,7 +622,9 @@ export const SubmissionsSection: FC = ( setPerPage(nextPerPage) }, []) - const handleFilterChange = useCallback((event: ChangeEvent): void => { + const handleFilterChange = useCallback(( + event: ChangeEvent, + ): void => { const fieldName: string = event.target.name const fieldValue: string = event.target.value @@ -642,6 +682,26 @@ export const SubmissionsSection: FC = ( /> + {isMarathonMatch + ? ( + + ) + : undefined} +