Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 => (
<button
disabled={props.disabled}
onClick={props.onClick}
type='button'
>
{props.label}
</button>
),
}), {
virtual: true,
})

jest.mock('../../../../../lib/assets/icons/lock.svg', () => ({
ReactComponent: (): JSX.Element => <svg data-testid='lock-icon' />,
}))

jest.mock('../../../../../lib/components', () => ({
ArtifactsModal: (): JSX.Element => <div>Artifacts modal</div>,
Pagination: (props: { total: number }): JSX.Element => (
<div data-testid='pagination-total'>{props.total}</div>
),
SubmissionRunnerLogsModal: (): JSX.Element => <div>Runner logs modal</div>,
SubmissionsTable: (props: { submissions: Submission[] }): JSX.Element => (
<div data-testid='submissions-table'>
{props.submissions.map(submission => (
<div data-testid='submission-row' key={submission.id}>
<span>{submission.id}</span>
<span>{submission.memberHandle}</span>
</div>
))}
</div>
),
}))

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(
<SubmissionsSection
challenge={baseChallenge}
challengeId='challenge-1'
/>,
)

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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
getSubmissionInitialScore,
getSubmissionProvisionalScore,
getSubmissionSystemScore,
getSubmissionTestProgress,
isMarathonMatchChallenge,
showErrorToast,
} from '../../../../../lib/utils'
Expand All @@ -57,6 +58,7 @@ interface FilterState {
handle: string
minScore: string
startDate: string
testType: string
}

interface SubmissionsSectionProps {
Expand Down Expand Up @@ -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,
Expand All @@ -343,6 +376,10 @@ function matchesFilters(
return false
}

if (!matchesFilterTestType(submission, filters.testType, useMarathonMatchScores)) {
return false
}

return matchesFilterScore(submission, filters.minScore, useMarathonMatchScores)
}

Expand All @@ -361,6 +398,7 @@ export const SubmissionsSection: FC<SubmissionsSectionProps> = (
handle: '',
minScore: '',
startDate: '',
testType: '',
})
const [memberCache, setMemberCache] = useState<MemberCache>({})
const [page, setPage] = useState<number>(1)
Expand Down Expand Up @@ -584,7 +622,9 @@ export const SubmissionsSection: FC<SubmissionsSectionProps> = (
setPerPage(nextPerPage)
}, [])

const handleFilterChange = useCallback((event: ChangeEvent<HTMLInputElement>): void => {
const handleFilterChange = useCallback((
event: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
): void => {
const fieldName: string = event.target.name
const fieldValue: string = event.target.value

Expand Down Expand Up @@ -642,6 +682,26 @@ export const SubmissionsSection: FC<SubmissionsSectionProps> = (
/>
</label>

{isMarathonMatch
? (
<label className={styles.filterLabel} htmlFor='submission-test-type-filter'>
Test type
<select
className={styles.filterInput}
id='submission-test-type-filter'
name='testType'
onChange={handleFilterChange}
value={filters.testType}
>
<option value=''>All</option>
<option value='system'>System</option>
<option value='provisional'>Provisional</option>
<option value='example'>Example</option>
</select>
</label>
)
: undefined}

<label className={styles.filterLabel} htmlFor='submission-start-date-filter'>
From
<input
Expand Down
Loading