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, }; }