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
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
29 changes: 29 additions & 0 deletions __tests__/shared/routes/Topcoder/ThriveArticleRoute.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ContentfulLoader from 'containers/ContentfulLoader';
import LivePreview from 'components/Contentful/Article/LivePreview';
import LoadingIndicator from 'components/LoadingIndicator';
import ThriveArticleRoute from 'routes/Topcoder/ThriveArticleRoute';

const routeProps = search => ({
location: { search },
match: { params: { articleTitle: 'brand-new-article' } },
});

describe('Thrive article route', () => {
test('uses the authenticated graph before querying an unpublished slug', () => {
const output = ThriveArticleRoute(routeProps('?payloadLivePreview=1'));

expect(output.type).toBe(LivePreview);
expect(output.props.children.type).toBe(LoadingIndicator);
});

test('keeps ordinary routes on the published compatibility query', () => {
const output = ThriveArticleRoute(routeProps(''));

expect(output.type).toBe(ContentfulLoader);
expect(output.props.entryQueries).toEqual({
content_type: 'article',
'fields.slug': 'brand-new-article',
});
expect(output.props.spaceName).toBe('EDU');
});
});
109 changes: 109 additions & 0 deletions __tests__/shared/services/recruitCRM.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* eslint-env jest */
import fetch from 'isomorphic-fetch';
import { logger } from 'topcoder-react-lib';
import Service from '../../../src/shared/services/recruitCRM';

jest.mock('isomorphic-fetch', () => jest.fn());
jest.mock('topcoder-react-lib', () => ({
logger: {
error: jest.fn(),
},
}));

describe('RecruitCRM application service', () => {
const originalFormData = global.FormData;
const originalHeaders = global.Headers;
let formData;

beforeEach(() => {
formData = { append: jest.fn() };
global.FormData = jest.fn(() => formData);
global.Headers = jest.fn(headers => headers);
fetch.mockReset();
logger.error.mockReset();
});

afterAll(() => {
global.FormData = originalFormData;
global.Headers = originalHeaders;
});

it('returns the JSON application result for a successful proxy response', async () => {
const json = jest.fn().mockResolvedValue({ id: 'application-id' });
fetch.mockResolvedValue({
ok: true,
status: 200,
headers: { get: jest.fn(() => 'application/json; charset=utf-8') },
json,
});

const result = await new Service().applyForJob(
'job-slug',
{ resume: 'resume-file', first_name: 'Ada' },
'token-v3',
);

expect(result).toEqual({ id: 'application-id' });
expect(formData.append).toHaveBeenNthCalledWith(1, 'resume', 'resume-file');
expect(formData.append).toHaveBeenNthCalledWith(2, 'form', JSON.stringify({ first_name: 'Ada' }));
});

it('rejects an HTML edge error without trying to parse it as JSON', async () => {
const json = jest.fn();
fetch.mockResolvedValue({
ok: false,
status: 404,
headers: { get: jest.fn(() => 'text/html; charset=utf-8') },
json,
});

await expect(new Service().applyForJob(
'job-slug',
{ resume: 'resume-file' },
'token-v3',
)).rejects.toMatchObject({
message: "We couldn't submit your application. Please try again.",
status: 404,
});
expect(json).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledTimes(1);
});

it('rejects a successful response when its body is not JSON', async () => {
const json = jest.fn();
fetch.mockResolvedValue({
ok: true,
status: 200,
headers: { get: jest.fn(() => 'text/html') },
json,
});

await expect(new Service().applyForJob(
'job-slug',
{ resume: 'resume-file' },
'token-v3',
)).rejects.toMatchObject({
message: "We couldn't submit your application. Please try again.",
status: 200,
});
expect(json).not.toHaveBeenCalled();
});

it('replaces JSON parser details with the safe application error', async () => {
fetch.mockResolvedValue({
ok: true,
status: 200,
headers: { get: jest.fn(() => 'application/json') },
json: jest.fn().mockRejectedValue(new SyntaxError("Unexpected token '<'")),
});

await expect(new Service().applyForJob(
'job-slug',
{ resume: 'resume-file' },
'token-v3',
)).rejects.toMatchObject({
message: "We couldn't submit your application. Please try again.",
status: 200,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('getSubmissionStatus', () => {
})).toEqual({
hasReviewSummation: false,
isAccepted: false,
isCancelled: false,
isFailed: true,
});
});
Expand All @@ -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,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -257,6 +260,17 @@ function renderTestStatusIcon(status) {
</span>
);
}
if (status === 'CANCELLED') {
return (
<span
aria-label="Test status: CANCELLED"
role="img"
styleName="test-status-icon test-status-cancelled"
>
<IconTestCancelled />
</span>
);
}
return null;
}

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -395,6 +405,10 @@ button.column-1-1,
color: #ef476f;
}

.test-status-cancelled {
color: #767676;
}

.icon-search {
margin-left: 17px;
}
Expand Down
4 changes: 4 additions & 0 deletions src/shared/components/challenge-detail/icons/cancelled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading