From ff6259ad224c91ed1d33640462db22dfc0ee45de Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 25 Aug 2026 14:22:19 +1000 Subject: [PATCH] PM-5368: show cancelled Marathon Match scoring in My Submissions What was broken A Marathon Match submission whose scorer was stopped because the member submitted a newer solution stayed in the "Preparing" status with an empty Test Status column forever, as shown in the PM-5368 screenshots. Members had no way to tell that the run had been superseded rather than still being queued. Root cause The submission status helpers only recognized accepted and failed states, and the Test Status column only rendered IN PROGRESS, SUCCESS and FAILED. The CANCELLED test status that marathon-match-api-v6 now records was normalized away and the row fell back to the generic preparing label. What was changed - getSubmissionStatus returns a new isCancelled flag when a matching review summation reports a cancelled test run. - My Submissions list rows and the submission detail panel show a "Cancelled" status label, taking precedence over the accepted and preparing labels. - The Test Status column normalizes and renders CANCELLED using a new neutral cancelled icon, and the provisional/final score stays hidden because a cancelled run never produced a score. Any added/updated tests - __tests__/shared/utils/challenge-detail/submission-status.test.js: new case covering that a cancelled review summation reports isCancelled instead of leaving the submission in the preparing state; existing cases updated for the new flag. - __tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx: new case covering that getSubmissionTestProgress surfaces a cancelled run. --- .../MySubmissions/SubmissionsList/index.jsx | 18 ++++++++++++ .../submission-status.test.js | 25 ++++++++++++++++ .../MySubmissions/SubmissionsDetail/index.jsx | 7 +++-- .../SubmissionsDetail/styles.scss | 6 ++++ .../MySubmissions/SubmissionsList/index.jsx | 29 +++++++++++++++---- .../MySubmissions/SubmissionsList/styles.scss | 14 +++++++++ .../challenge-detail/icons/cancelled.svg | 4 +++ .../challenge-detail/submission-status.js | 22 ++++++++++++-- 8 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/shared/components/challenge-detail/icons/cancelled.svg diff --git a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx index 6f39e5a31..7bbd825b9 100644 --- a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx +++ b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx @@ -127,6 +127,24 @@ describe('getSubmissionTestProgress', () => { status: 'FAILED', }); }); + + it('surfaces cancelled scoring runs superseded by a newer submission', () => { + expect(getSubmissionTestProgress({ + reviewSummations: [ + { + metadata: { + testProcess: 'provisional', + testProgress: 1, + testStatus: 'CANCELLED', + }, + }, + ], + })).toEqual({ + process: 'provisional', + progressPercent: '100%', + status: 'CANCELLED', + }); + }); }); describe('isActiveTestStatus', () => { diff --git a/__tests__/shared/utils/challenge-detail/submission-status.test.js b/__tests__/shared/utils/challenge-detail/submission-status.test.js index 88c7baa7f..95d18c70e 100644 --- a/__tests__/shared/utils/challenge-detail/submission-status.test.js +++ b/__tests__/shared/utils/challenge-detail/submission-status.test.js @@ -10,6 +10,7 @@ describe('getSubmissionStatus', () => { })).toEqual({ hasReviewSummation: false, isAccepted: false, + isCancelled: false, isFailed: true, }); }); @@ -28,6 +29,30 @@ describe('getSubmissionStatus', () => { })).toEqual({ hasReviewSummation: false, isAccepted: false, + isCancelled: false, + isFailed: false, + }); + }); + + it('reports cancelled scoring runs instead of leaving them in preparing state', () => { + expect(getSubmissionStatus({ + status: 'ACTIVE', + submissionId: 'submission-1', + reviewSummations: [ + { + submissionId: 'submission-1', + isProvisional: true, + metadata: { + testProcess: 'provisional', + testProgress: 1, + testStatus: 'CANCELLED', + }, + }, + ], + })).toEqual({ + hasReviewSummation: true, + isAccepted: true, + isCancelled: true, isFailed: false, }); }); diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx index 1d90e04bb..7c0a01281 100644 --- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx +++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx @@ -146,10 +146,13 @@ class SubmissionsDetailView extends React.Component { const { onCancel, submission, onSortChange } = this.props; let { finalScore } = submission; const { sortedSubmissions } = this.state; - const { isAccepted, isFailed } = getSubmissionStatus(submission); + const { isAccepted, isCancelled, isFailed } = getSubmissionStatus(submission); let finalStatusStyleName = 'status-in-queue'; let finalStatusLabel = 'Preparing'; - if (isAccepted) { + if (isCancelled) { + finalStatusStyleName = 'status-cancelled'; + finalStatusLabel = 'Cancelled'; + } else if (isAccepted) { finalStatusStyleName = 'status-complete'; finalStatusLabel = 'Complete'; } else if (isFailed) { diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss index 5ca773525..46ae018c2 100644 --- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss +++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/styles.scss @@ -257,6 +257,12 @@ button.header-sort { font-weight: 500; } +.status-cancelled { + font-size: 14px; + color: #767676; + font-weight: 500; +} + .mobile-header { display: none; font-weight: 600; diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx index 647c82f7c..bab127b08 100644 --- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx +++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx @@ -17,6 +17,7 @@ import DateSortIcon from 'assets/images/icon-date-sort.svg'; import SortIcon from 'assets/images/icon-sort.svg'; import Tooltip from 'components/Tooltip'; import IconFail from '../../icons/failed.svg'; +import IconTestCancelled from '../../icons/cancelled.svg'; import IconTestInProgress from '../../icons/clock.svg'; import IconTestSuccess from '../../icons/check-mark.svg'; import DownloadIcon from '../../../SubmissionManagement/Icons/IconSquareDownload.svg'; @@ -127,17 +128,19 @@ function normalizeTestProcess(value) { */ function normalizeTestStatus(value) { const normalized = _.toUpper(_.toString(value || '').trim()); - if (['FAILED', 'IN PROGRESS', 'SUCCESS'].indexOf(normalized) >= 0) { + if (['CANCELLED', 'FAILED', 'IN PROGRESS', 'SUCCESS'].indexOf(normalized) >= 0) { return normalized; } return undefined; } /** - * Returns whether a Marathon Match test status still represents active testing. + * Returns whether a Marathon Match test status produced no score to display. + * Running tests have no score yet, and a run cancelled because the member + * submitted a newer solution never produces one. * * @param {String} status normalized test status from review summation metadata. - * @returns {Boolean} true when scoring should remain hidden until testing completes. + * @returns {Boolean} true when scoring should stay hidden for the submission. */ export function isActiveTestStatus(status) { const normalized = normalizeTestStatus(status); @@ -257,6 +260,17 @@ function renderTestStatusIcon(status) { ); } + if (status === 'CANCELLED') { + return ( + + + + ); + } return null; } @@ -662,10 +676,15 @@ class SubmissionsListView extends React.Component { } else { provisionalScore = 'N/A'; } - const { isAccepted, isFailed } = getSubmissionStatus(mySubmission); + const { + isAccepted, isCancelled, isFailed, + } = getSubmissionStatus(mySubmission); let statusStyleName = 'queue'; let statusLabel = 'Preparing'; - if (isAccepted) { + if (isCancelled) { + statusStyleName = 'cancelled'; + statusLabel = 'Cancelled'; + } else if (isAccepted) { statusStyleName = 'accepted'; statusLabel = 'Accepted'; } else if (isFailed) { diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss index 4c6a23fa3..59667c62d 100644 --- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss +++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/styles.scss @@ -61,6 +61,16 @@ line-height: 22px; } +.cancelled { + color: #767676; + + @include roboto-medium; + + font-weight: 500; + font-size: 14px; + line-height: 22px; +} + .h2 { @include roboto-bold; @@ -395,6 +405,10 @@ button.column-1-1, color: #ef476f; } +.test-status-cancelled { + color: #767676; +} + .icon-search { margin-left: 17px; } diff --git a/src/shared/components/challenge-detail/icons/cancelled.svg b/src/shared/components/challenge-detail/icons/cancelled.svg new file mode 100644 index 000000000..75caf86b1 --- /dev/null +++ b/src/shared/components/challenge-detail/icons/cancelled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/shared/utils/challenge-detail/submission-status.js b/src/shared/utils/challenge-detail/submission-status.js index eb27d54a6..57060c4fd 100644 --- a/src/shared/utils/challenge-detail/submission-status.js +++ b/src/shared/utils/challenge-detail/submission-status.js @@ -50,6 +50,19 @@ function hasFailedSubmissionStatus(submission) { return status === 'failed' || status === 'failure'; } +/** + * Returns whether a review summation reports a cancelled Marathon Match test run. + * Marathon Match scoring is cancelled when the member submits a newer solution + * while an earlier scorer is still running. + * + * @param {Object} summation review summation attached to a submission attempt. + * @returns {Boolean} true when the summation test status is cancelled. + */ +function hasCancelledTestStatus(summation) { + const status = _.toUpper(_.toString(_.get(summation, 'metadata.testStatus', '')).trim()); + return status === 'CANCELLED'; +} + export function getSubmissionReviewSummations(submission) { return collectReviewSummations(submission); } @@ -58,10 +71,12 @@ export function getSubmissionReviewSummations(submission) { * Builds display status flags for a challenge submission attempt. * Review summations indicate accepted scoring; when no accepted summation is * present, failed scan or submission states should display as failed instead of - * staying in the generic preparing state. + * staying in the generic preparing state. Cancelled scoring runs take precedence + * so a superseded Marathon Match submission is never shown as still preparing. * * @param {Object} submission submission attempt shown in challenge details. - * @returns {{hasReviewSummation: Boolean, isAccepted: Boolean, isFailed: Boolean}} status flags. + * @returns {{hasReviewSummation: Boolean, isAccepted: Boolean, isCancelled: Boolean, + * isFailed: Boolean}} status flags. */ export function getSubmissionStatus(submission) { const targetIdRaw = _.get(submission, 'submissionId', _.get(submission, 'id', null)); @@ -93,12 +108,15 @@ export function getSubmissionStatus(submission) { return hasFlag || type === 'provisional' || type === 'final'; }); + const isCancelled = reviewSummations.some(hasCancelledTestStatus); + const isFailed = !isAccepted && (hasVirusScanFailure(submission) || hasFailedSubmissionStatus(submission)); return { hasReviewSummation, isAccepted, + isCancelled, isFailed, }; }