Skip to content
Merged
143 changes: 141 additions & 2 deletions __tests__/shared/components/challenge-detail/Header/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
import React from 'react';
import Renderer from 'react-test-renderer/shallow';
import { errors as mockedErrors } from 'topcoder-react-lib';

import Header from 'components/challenge-detail/Header';
import TabSelector from 'components/challenge-detail/Header/TabSelector';

jest.mock('topcoder-react-lib', () => ({
challenge: {
filter: {},
},
errors: {
fireErrorMessage: jest.fn(),
},
services: {
api: {},
},
tc: {
CHALLENGE_STATUS: {
ACTIVE: 'ACTIVE',
COMPLETED: 'COMPLETED',
},
OLD_COMPETITION_TRACKS: {},
},
}));

jest.mock('topcoder-react-ui-kit', () => ({
PrimaryButton: () => null,
}));

jest.mock('react-responsive', () => ({
useMediaQuery: () => true,
}));

function collectText(node) {
if (typeof node === 'string') {
if (typeof node === 'string' || typeof node === 'number') {
return [node];
}

Expand All @@ -16,7 +45,28 @@ function collectText(node) {
.reduce((acc, child) => acc.concat(collectText(child)), []);
}

function renderHeader(challengeOverrides = {}) {
function findSubmitAction(node) {
if (!React.isValidElement(node)) {
return null;
}

if ((node.props.to || node.props.onClick)
&& collectText(node).includes('Submit a solution')) {
return node;
}

const children = React.Children.toArray(node.props.children);
for (let index = 0; index < children.length; index += 1) {
const match = findSubmitAction(children[index]);
if (match) {
return match;
}
}

return null;
}

function renderHeader(challengeOverrides = {}, propOverrides = {}) {
const renderer = new Renderer();
renderer.render(
<Header
Expand Down Expand Up @@ -77,13 +127,18 @@ function renderHeader(challengeOverrides = {}) {
unregisterFromChallenge={jest.fn()}
unregistering={false}
viewAsTable={false}
{...propOverrides}
/>,
);

return renderer.getRenderOutput();
}

describe('Challenge detail header actions', () => {
beforeEach(() => {
mockedErrors.fireErrorMessage.mockClear();
});

test('hides registration and submission actions for classic task challenges', () => {
const output = renderHeader({
type: 'Task',
Expand Down Expand Up @@ -124,4 +179,88 @@ describe('Challenge detail header actions', () => {
expect(collectText(output)).toContain('Register');
expect(collectText(output)).toContain('Submit a solution');
});

test('shows the limit-reached message instead of opening the submission page', () => {
const output = renderHeader({
metadata: [{
name: 'submissionLimit',
value: JSON.stringify({
count: '1',
limit: 'true',
unlimited: 'false',
}),
}],
}, {
hasRegistered: true,
mySubmissions: [{ id: 'submission-id' }],
});
const submitAction = findSubmitAction(output);

expect(submitAction.props.to).toBeUndefined();
submitAction.props.onClick();

expect(mockedErrors.fireErrorMessage).toHaveBeenCalledWith(
'Submission Limit Reached',
'This challenge allows only one submission, and you\'ve already submitted.'
+ ' To replace it, delete your existing submission first.',
);
});

test('keeps the submission page available while slots remain', () => {
const output = renderHeader({
metadata: [{
name: 'submissionLimit',
value: JSON.stringify({
count: '2',
limit: 'true',
unlimited: 'false',
}),
}],
}, {
hasRegistered: true,
mySubmissions: [{ id: 'submission-id' }],
});
const submitAction = findSubmitAction(output);

expect(submitAction.props.to).toBe('/challenges/challenge-id/submit');
expect(submitAction.props.onClick).toBeUndefined();
expect(mockedErrors.fireErrorMessage).not.toHaveBeenCalled();
});
});

describe('Challenge detail tab counts', () => {
test('renders the MM submission total independently of loaded attempts', () => {
const renderer = new Renderer();
renderer.render(
<TabSelector
challenge={{
id: 'challenge-id',
legacy: {},
metadata: [],
tags: [],
type: 'Marathon Match',
}}
checkpointCount={0}
hasRegistered
isLoggedIn
isMM
mySubmissions={[{ submissionId: 'latest-submission' }]}
mySubmissionsCount={3}
numOfCheckpointSubmissions={0}
numOfRegistrants={4}
numOfSubmissions={4}
numWinners={0}
onSelectorClicked={jest.fn()}
onSort={jest.fn()}
selectedView="submissions"
trackLower="data science"
viewAsTable={false}
/>,
);

const text = collectText(renderer.getRenderOutput());
const mySubmissionsLabelIndex = text.indexOf('My Submissions');

expect(text[mySubmissionsLabelIndex + 1]).toBe(3);
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,68 @@
import {
import { shallow } from 'enzyme';
import React from 'react';
import { getSubmissionDownloadUrl as mockedGetSubmissionDownloadUrl } from 'services/submissions';

import SubmissionsListView, {
getDisplayedScores,
isActiveTestStatus,
getSubmissionTestProgress,
} from '../../../../../../src/shared/components/challenge-detail/MySubmissions/SubmissionsList';

jest.mock('services/submissions', () => ({
getSubmissionDownloadUrl: jest.fn(),
}));

/**
* Renders a My Submissions row and returns its visible provisional score.
* Tests use this to compare score display behavior across scorer processes.
*
* @param {String} testProcess Review API test process metadata.
* @returns {String} provisional score displayed in the submission row.
* @throws {Error} Propagates errors raised while shallow-rendering SubmissionsListView.
*/
function renderProvisionalScore(testProcess) {
const wrapper = shallow(
<SubmissionsListView
auth={{ tokenV3: 'token' }}
challenge={{ id: 'challenge-id', metadata: [] }}
challengesUrl="/challenges"
hasRegistered
isLegacyMM={false}
mySubmissions={[
{
createdAt: '2026-08-04T01:05:51.000Z',
finalScore: 0,
initialScore: 75.82,
provisionalScore: 0,
reviewSummations: [
{
metadata: {
testProcess,
testProgress: 0.66,
testStatus: 'IN PROGRESS',
},
},
],
status: 'completed',
submissionId: 'submission-id',
},
]}
submissionEnded={false}
submissionsSort={{ field: '', sort: '' }}
unregistering={false}
/>,
);
const scoreColumn = wrapper.find('div')
.filterWhere((node) => {
const firstChild = node.children().at(0);
return firstChild.type() === 'div'
&& firstChild.text() === 'Provisional Score';
})
.first();

return scoreColumn.find('span').last().text();
}

describe('getDisplayedScores', () => {
test('shows final scores when a system review already produced one before review completes', () => {
expect(getDisplayedScores(
Expand Down Expand Up @@ -77,3 +136,68 @@ describe('isActiveTestStatus', () => {
expect(isActiveTestStatus('FAILED')).toBe(false);
});
});

describe('Marathon Match provisional score display', () => {
it('keeps the completed provisional score visible while system tests are running', () => {
expect(renderProvisionalScore('system')).toBe('75.82');
});

it('keeps the provisional score hidden while provisional tests are running', () => {
expect(renderProvisionalScore('provisional')).toBe('-');
});
});

describe('Marathon Match submission download', () => {
let originalCreateObjectURL;

beforeEach(() => {
originalCreateObjectURL = window.URL.createObjectURL;
});

afterEach(() => {
window.URL.createObjectURL = originalCreateObjectURL;
jest.restoreAllMocks();
jest.clearAllMocks();
});

it('opens the browser-safe signed URL for an opaque submission id', async () => {
const submissionId = 'BKzPfVv24EcINT';
const downloadUrl = 'https://storage.example.test/signed-mm-submission';
const link = document.createElement('a');
link.click = jest.fn();
const createObjectURL = jest.fn();
window.URL.createObjectURL = createObjectURL;
jest.spyOn(document, 'createElement').mockReturnValue(link);
mockedGetSubmissionDownloadUrl.mockResolvedValue(downloadUrl);

const wrapper = shallow(
<SubmissionsListView
auth={{ tokenV3: 'token-v3' }}
challenge={{ id: 'challenge-id', metadata: [] }}
challengesUrl="/challenges"
hasRegistered
isLegacyMM={false}
mySubmissions={[
{
createdAt: '2026-08-11T00:00:00.000Z',
status: 'completed',
submissionId,
},
]}
submissionEnded={false}
submissionsSort={{ field: '', sort: '' }}
unregistering={false}
/>,
);

wrapper.find('button[aria-label="Download submission"]').prop('onClick')();
await Promise.resolve();

expect(mockedGetSubmissionDownloadUrl).toHaveBeenCalledWith('token-v3', submissionId);
expect(link.href).toBe(downloadUrl);
expect(link.getAttribute('download')).toBe(`submission-${submissionId}.zip`);
expect(link.click).toHaveBeenCalledTimes(1);
expect(document.body.contains(link)).toBe(false);
expect(createObjectURL).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ function collectText(node) {
}

/**
* Shallow-renders an MM row and returns its provisional score column text.
* Shallow-renders an MM row and returns the selected score column text.
* Tests use this to compare score display behavior across scorer process states.
*
* @param {String} scoreHeader Header for the score column to inspect.
* @param {String} testProcess Review API test process metadata.
* @param {String} testStatus Review API test status metadata.
* @returns {Array<String|Number>} Provisional score column label and displayed value.
* @param {Number|null} finalScore Final score supplied by Review API.
* @returns {Array<String|Number>} Score column label and displayed value.
* @throws {Error} Propagates errors raised while shallow-rendering SubmissionRow.
*/
function renderProvisionalScore(testProcess, testStatus) {
function renderScore(scoreHeader, testProcess, testStatus, finalScore = null) {
const renderer = new Renderer();
renderer.render(
<SubmissionRow
Expand All @@ -73,9 +75,10 @@ function renderProvisionalScore(testProcess, testStatus) {
numWinners={0}
onShowPopup={jest.fn()}
openHistory={false}
showFinalResults
submissions={[
{
finalScore: null,
finalScore,
id: 'submission-id',
provisionalScore: 0,
reviewSummations: [
Expand All @@ -95,23 +98,35 @@ function renderProvisionalScore(testProcess, testStatus) {
/>,
);

const column = findColumnByHeader(renderer.getRenderOutput(), 'PROVISIONAL SCORE');
const column = findColumnByHeader(renderer.getRenderOutput(), scoreHeader);
return collectText(column);
}

describe('Marathon Match provisional score', () => {
it('shows N/A while provisional tests are still running', () => {
expect(renderProvisionalScore('provisional', 'IN PROGRESS'))
expect(renderScore('PROVISIONAL SCORE', 'provisional', 'IN PROGRESS'))
.toEqual(['PROVISIONAL SCORE', 'N/A']);
});

it('keeps a completed zero provisional score visible', () => {
expect(renderProvisionalScore('provisional', 'SUCCESS'))
expect(renderScore('PROVISIONAL SCORE', 'provisional', 'SUCCESS'))
.toEqual(['PROVISIONAL SCORE', 0]);
});

it('keeps the provisional score visible while system tests are running', () => {
expect(renderProvisionalScore('system', 'IN PROGRESS'))
expect(renderScore('PROVISIONAL SCORE', 'system', 'IN PROGRESS'))
.toEqual(['PROVISIONAL SCORE', 0]);
});
});

describe('Marathon Match final score', () => {
it('shows N/A while system tests are still running', () => {
expect(renderScore('FINAL SCORE', 'system', 'IN PROGRESS', 0))
.toEqual(['FINAL SCORE', 'N/A']);
});

it('keeps a completed zero final score visible', () => {
expect(renderScore('FINAL SCORE', 'system', 'SUCCESS', 0))
.toEqual(['FINAL SCORE', 0]);
});
});
Loading
Loading