Skip to content
Open
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
Expand Up @@ -150,6 +150,10 @@
color: #ea1900;
}

.testStatusCancelled {
color: #767676;
}

.level-1 {
color: #555 !important;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SubmissionsTable } from './SubmissionsTable'

jest.mock('~/libs/ui', () => ({
IconOutline: {
BanIcon: (): JSX.Element => <svg data-testid='ban-icon' />,
ClockIcon: (): JSX.Element => <svg data-testid='clock-icon' />,
XCircleIcon: (): JSX.Element => <svg data-testid='x-circle-icon' />,
},
Expand Down Expand Up @@ -55,7 +56,7 @@ jest.mock('../../utils', () => ({
metadata?: {
testProcess?: 'example' | 'provisional' | 'system'
testProgress?: number
testStatus?: 'FAILED' | 'IN PROGRESS' | 'SUCCESS'
testStatus?: 'CANCELLED' | 'FAILED' | 'IN PROGRESS' | 'SUCCESS'
testType?: 'example' | 'provisional' | 'system'
}
}>
Expand Down Expand Up @@ -337,6 +338,45 @@ describe('SubmissionsTable', () => {
.toBeTruthy()
})

it('renders a cancelled test status without a score for a superseded marathon run', () => {
render(
<SubmissionsTable
canDownloadSubmissions
challengeId='challenge-123'
onDownloadSubmission={jest.fn()}
onOpenArtifacts={jest.fn()}
onSort={jest.fn()}
showMarathonMatchTestProgress
sortBy='createdAt'
sortOrder='desc'
submissions={[
{
challengeId: 'challenge-123',
createdBy: 'member-1',
id: 'submission-1',
reviewSummation: [
{
aggregateScore: -1,
isProvisional: true,
metadata: {
testProcess: 'provisional',
testProgress: 1,
testStatus: 'CANCELLED',
},
},
],
type: 'SUBMISSION',
},
]}
/>,
)

expect(screen.getByRole('img', { name: 'Test status: CANCELLED' }))
.toBeTruthy()
expect(screen.queryByText('-1.00'))
.toBeNull()
})

it('renders marathon scores from provisional and system summations only', () => {
render(
<SubmissionsTable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ function getCreatedAt(submission: Submission): string {
|| ''
}

/**
* Returns whether a marathon test status means no score was produced.
* @param status Normalized test status from review summation metadata.
* @returns `true` while tests are running and when a run was cancelled.
* Used by `SubmissionsTable` so the placeholder aggregate score written for a
* superseded scorer run is not displayed as a real score.
*/
function isUnscoredTestStatus(status?: string): boolean {
return status === 'IN PROGRESS' || status === 'CANCELLED'
}

function formatScore(value?: number, emptyValue: string = 'N/A'): string {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return emptyValue
Expand Down Expand Up @@ -276,6 +287,19 @@ function renderTestStatusIcon(status: string | undefined): ReactElement | undefi
)
}

if (status === 'CANCELLED') {
return (
<span
aria-label='Test status: CANCELLED'
className={classNames(styles.testStatusIcon, styles.testStatusCancelled)}
role='img'
title='CANCELLED'
>
<IconOutline.BanIcon aria-hidden='true' />
</span>
)
}

return undefined
}

Expand Down Expand Up @@ -384,17 +408,17 @@ export const SubmissionsTable: FC<SubmissionsTableProps> = (
const emptyScoreValue = props.showMarathonMatchTestProgress
? '-'
: 'N/A'
const isInitialScoreInProgress = testProgress?.status === 'IN PROGRESS'
const isInitialScoreUnscored = isUnscoredTestStatus(testProgress?.status)
&& (
testProgress.process === 'example'
|| testProgress.process === 'provisional'
testProgress?.process === 'example'
|| testProgress?.process === 'provisional'
)
const isFinalScoreInProgress = testProgress?.status === 'IN PROGRESS'
&& testProgress.process === 'system'
const initialScore = isInitialScoreInProgress
const isFinalScoreUnscored = isUnscoredTestStatus(testProgress?.status)
&& testProgress?.process === 'system'
const initialScore = isInitialScoreUnscored
? 'N/A'
: formatScore(initialScoreValue, emptyScoreValue)
const finalScore = isFinalScoreInProgress
const finalScore = isFinalScoreUnscored
? 'N/A'
: formatScore(finalScoreValue, emptyScoreValue)
const reviewTab = submission.type === 'CHECKPOINT_SUBMISSION'
Expand Down
2 changes: 1 addition & 1 deletion src/apps/work/src/lib/models/Submission.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type SubmissionStatus = 'active' | 'completed' | 'deleted' | 'failed' | 'pending' | string
export type MarathonMatchTestProcess = 'provisional' | 'system' | string
export type MarathonMatchTestStatus = 'FAILED' | 'IN PROGRESS' | 'SUCCESS' | string
export type MarathonMatchTestStatus = 'CANCELLED' | 'FAILED' | 'IN PROGRESS' | 'SUCCESS' | string

export interface SubmissionReview {
createdAt?: string
Expand Down
20 changes: 20 additions & 0 deletions src/apps/work/src/lib/utils/challenge.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ describe('challenge utils', () => {
})
})

it('surfaces cancelled provisional runs superseded by a newer submission', () => {
expect(getSubmissionTestProgress({
reviewSummation: [
{
isProvisional: true,
metadata: {
testProcess: 'provisional',
testProgress: 1,
testStatus: 'CANCELLED',
},
},
],
}))
.toEqual({
process: 'provisional',
progressPercent: '100%',
status: 'CANCELLED',
})
})

it('prefers a completed provisional process over a later example process', () => {
expect(getSubmissionTestProgress({
reviewSummation: [
Expand Down
16 changes: 12 additions & 4 deletions src/apps/work/src/lib/utils/challenge.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface SubmissionScore {
}

type MarathonMatchScoreProcess = 'example' | 'provisional' | 'system'
type MarathonMatchTestStatusDisplay = 'CANCELLED' | 'FAILED' | 'IN PROGRESS' | 'SUCCESS'

interface ScoredSubmissionLike {
review?: Array<{
Expand All @@ -34,7 +35,7 @@ interface ScoredSubmissionLike {
export interface SubmissionTestProgressDisplay {
process?: MarathonMatchScoreProcess
progressPercent?: string
status?: 'FAILED' | 'IN PROGRESS' | 'SUCCESS'
status?: MarathonMatchTestStatusDisplay
}

interface SubmissionTestProgressCandidate extends SubmissionTestProgressDisplay {
Expand Down Expand Up @@ -270,14 +271,21 @@ function normalizeTestProcess(value: unknown): MarathonMatchScoreProcess | undef
* @param value Metadata status value from Review API.
* @returns Supported UI status or `undefined` when the status is absent/unknown.
* Used by `getSubmissionTestProgress` before choosing the current summation.
* `CANCELLED` marks a scorer that was stopped because the member submitted a
* newer solution, so the run is terminal without producing a score.
*/
function normalizeTestStatus(value: unknown): 'FAILED' | 'IN PROGRESS' | 'SUCCESS' | undefined {
function normalizeTestStatus(value: unknown): MarathonMatchTestStatusDisplay | undefined {
const normalized = typeof value === 'string'
? value.trim()
.toUpperCase()
: ''

if (normalized === 'FAILED' || normalized === 'IN PROGRESS' || normalized === 'SUCCESS') {
if (
normalized === 'CANCELLED'
|| normalized === 'FAILED'
|| normalized === 'IN PROGRESS'
|| normalized === 'SUCCESS'
) {
return normalized
}

Expand Down Expand Up @@ -355,7 +363,7 @@ function toSubmissionTestProgressCandidate(
const progress = normalizeTestProgress(entry.metadata?.testProgress)
let statusPriority = 0

if (status === 'FAILED') {
if (status === 'FAILED' || status === 'CANCELLED') {
statusPriority = 2
} else if (status === 'SUCCESS') {
statusPriority = 1
Expand Down
Loading