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
99 changes: 98 additions & 1 deletion __tests__/shared/components/challenge-detail/Header/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
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,
}));
Expand All @@ -21,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 @@ -82,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 @@ -129,6 +179,53 @@ 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', () => {
Expand Down
157 changes: 157 additions & 0 deletions __tests__/shared/containers/SubmissionPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { errors as mockedErrors } from 'topcoder-react-lib';
import { getChallengeSubmissions as mockedGetChallengeSubmissions } from 'services/submissions';

import { SubmissionsPageContainer } from 'containers/SubmissionPage';

jest.mock('topcoder-react-lib', () => ({
actions: {
challenge: {},
},
errors: {
fireErrorMessage: jest.fn(),
},
}));

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

jest.mock('actions/page/submission', () => ({
page: {
submission: {},
},
}));
jest.mock('actions/page/challenge-details', () => ({
page: {
challengeDetails: {},
},
}));
jest.mock('actions/tc-communities', () => ({
tcCommunity: {},
}));
jest.mock('components/SubmissionPage', () => () => null);
jest.mock('components/tc-communities/AccessDenied', () => ({
__esModule: true,
CAUSE: {
NOT_AUTHORIZED: 'NOT_AUTHORIZED',
},
default: () => null,
}));
jest.mock('components/LoadingIndicator', () => () => null);
jest.mock('topcoder-react-ui-kit', () => ({
PrimaryButton: () => null,
}));

function createContainerProps(overrides = {}) {
return {
challenge: {},
challengeId: 'challenge-id',
metadata: [],
submit: jest.fn(),
tokenV2: 'token-v2',
tokenV3: 'token-v3',
track: 'Design',
userId: 'member-id',
...overrides,
};
}

describe('SubmissionsPageContainer submission limits', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('skips the limit lookup for unlimited challenges', async () => {
const props = createContainerProps();
const container = new SubmissionsPageContainer(props);
const body = {};

await container.handleSubmit(body);

expect(mockedGetChallengeSubmissions).not.toHaveBeenCalled();
expect(props.submit).toHaveBeenCalledWith(
'token-v3',
'token-v2',
'challenge-id',
body,
'Design',
);
});

test('submits while a limited challenge still has an available slot', async () => {
mockedGetChallengeSubmissions.mockResolvedValue({
data: [{ id: 'submission-1' }],
});
const props = createContainerProps({
metadata: [{
name: 'submissionLimit',
value: JSON.stringify({
count: '2',
limit: 'true',
unlimited: 'false',
}),
}],
});
const container = new SubmissionsPageContainer(props);
const body = {};

await container.handleSubmit(body);

expect(mockedGetChallengeSubmissions).toHaveBeenCalledWith(
'token-v3',
'challenge-id',
{ memberId: 'member-id' },
);
expect(props.submit).toHaveBeenCalled();
expect(mockedErrors.fireErrorMessage).not.toHaveBeenCalled();
});

test('shows the limit message and does not submit when the limit is reached', async () => {
mockedGetChallengeSubmissions.mockResolvedValue({
data: [{ id: 'submission-1' }],
});
const props = createContainerProps({
metadata: [{
name: 'submissionLimit',
value: JSON.stringify({
count: '1',
limit: 'true',
unlimited: 'false',
}),
}],
});
const container = new SubmissionsPageContainer(props);

await container.handleSubmit({});

expect(props.submit).not.toHaveBeenCalled();
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('does not submit when the existing-submission lookup fails', async () => {
mockedGetChallengeSubmissions.mockRejectedValue(new Error('network error'));
const props = createContainerProps({
metadata: [{
name: 'submissionLimit',
value: JSON.stringify({
count: '1',
limit: 'true',
unlimited: 'false',
}),
}],
});
const container = new SubmissionsPageContainer(props);

await container.handleSubmit({});

expect(props.submit).not.toHaveBeenCalled();
expect(mockedErrors.fireErrorMessage).toHaveBeenCalledWith(
'Unable to Verify Submission Limit',
'We could not verify your existing submissions. Please try again.',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-env jest */
import {
getSubmissionLimit,
getSubmissionLimitReachedMessage,
} from '../../../../src/shared/utils/challenge-detail/submission-limit';

describe('getSubmissionLimit', () => {
test('returns null when submission-limit metadata is missing', () => {
expect(getSubmissionLimit([])).toBeNull();
});

test('returns null for the current unlimited payload', () => {
expect(getSubmissionLimit([{
name: 'submissionLimit',
value: JSON.stringify({
count: '',
limit: 'false',
unlimited: 'true',
}),
}])).toBeNull();
});

test('returns the count for the current limited payload', () => {
expect(getSubmissionLimit([{
name: 'submissionLimit',
value: JSON.stringify({
count: '3',
limit: 'true',
unlimited: 'false',
}),
}])).toBe(3);
});

test('supports legacy numeric values', () => {
expect(getSubmissionLimit([{
name: 'submissionLimit',
value: 1,
}])).toBe(1);
expect(getSubmissionLimit([{
name: 'submissionLimit',
value: '2',
}])).toBe(2);
});

test('returns null for malformed and invalid counts', () => {
expect(getSubmissionLimit([{
name: 'submissionLimit',
value: '{invalid',
}])).toBeNull();
expect(getSubmissionLimit([{
name: 'submissionLimit',
value: JSON.stringify({
count: '0',
limit: 'true',
unlimited: 'false',
}),
}])).toBeNull();
});
});

describe('getSubmissionLimitReachedMessage', () => {
test('uses the requested singular limit message', () => {
expect(getSubmissionLimitReachedMessage(1)).toBe(
'This challenge allows only one submission, and you\'ve already submitted.'
+ ' To replace it, delete your existing submission first.',
);
});

test('uses a plural message for larger limits', () => {
expect(getSubmissionLimitReachedMessage(3)).toBe(
'This challenge allows only 3 submissions, and you\'ve already reached that limit.'
+ ' To replace one, delete an existing submission first.',
);
});
});
Loading
Loading