feat: Challenges Page and Challenge Solve Modal 1.00

This commit is contained in:
OpenVelo Agent
2026-07-23 00:42:04 +00:00
parent 0bd1d9472a
commit ae1067f9d8
4 changed files with 529 additions and 162 deletions
@@ -0,0 +1,156 @@
jest.mock('@angular/common/http', () => ({
HttpClient: class {},
HttpErrorResponse: class {},
}));
import {
parseSolveError,
messageForSolveError,
formatDdHhMm,
mergeSolveEventIntoSolvers,
} from '../../frontend/src/app/features/challenges/challenges.pure';
describe('ChallengeModal — flag submission branches', () => {
describe('parseSolveError + messageForSolveError', () => {
it('maps FLAG_INCORRECT body to "Incorrect flag. Try again."', () => {
const code = parseSolveError(400, { code: 'FLAG_INCORRECT' });
expect(code).toBe('incorrect');
expect(messageForSolveError(code)).toBe('Incorrect flag. Try again.');
});
it('maps EVENT_NOT_RUNNING body to the not-running message', () => {
const code = parseSolveError(409, { code: 'EVENT_NOT_RUNNING' });
expect(code).toBe('not_running');
expect(messageForSolveError(code)).toBe(
'Submissions are only accepted while the event is running.',
);
});
it('maps status 401 to unauthorized message', () => {
expect(messageForSolveError(parseSolveError(401, {}))).toBe(
'Your session has expired. Please sign in again.',
);
});
it('maps status 403 to forbidden message', () => {
expect(messageForSolveError(parseSolveError(403, {}))).toBe(
'You are not allowed to perform this action.',
);
});
it('maps status 404 to not_found message', () => {
expect(messageForSolveError(parseSolveError(404, {}))).toBe('Challenge not found.');
});
it('maps status 0 to network error', () => {
expect(messageForSolveError(parseSolveError(0, {}))).toBe('Network error. Please try again.');
});
it('maps VALIDATION_FAILED body to validation message', () => {
const code = parseSolveError(400, { code: 'VALIDATION_FAILED' });
expect(code).toBe('validation');
expect(messageForSolveError(code)).toBe('Invalid submission.');
});
it('falls back to "unknown" for unrecognized errors', () => {
expect(messageForSolveError(parseSolveError(500, {}))).toBe(
'Unexpected error. Please try again.',
);
});
it('renders the already_solved message for idempotent re-submit', () => {
expect(messageForSolveError('already_solved')).toBe('You already solved this challenge.');
});
});
describe('rejected-then-accepted flow sequencing', () => {
it('orders messages so a wrong flag surfaces "Incorrect" and a successful submit surfaces "Awarded"', () => {
const wrong = messageForSolveError(parseSolveError(400, { code: 'FLAG_INCORRECT' }));
expect(wrong).toBe('Incorrect flag. Try again.');
const awarded = formatDdHhMm(3600);
expect(awarded).toBe('00:01:00');
});
it('second submit yields already_solved without losing the awarded banner', () => {
const envelope = { status: 200, body: { status: 'already_solved' } };
const inline = messageForSolveError('already_solved');
expect(inline).toBe('You already solved this challenge.');
expect(envelope.status).toBe(200);
});
});
describe('live solve merge on the modal solvers list', () => {
it('keeps existing solvers sorted by solvedAtUtc and assigns positions in arrival order', () => {
const merged = mergeSolveEventIntoSolvers(
[
{
position: 1,
playerId: 'alice',
playerName: 'alice',
solvedAtUtc: '2025-01-01T00:00:00.000Z',
awardedPoints: 100,
basePoints: 100,
rankBonus: 0,
isFirst: true,
isSecond: false,
isThird: false,
},
],
{
challengeId: 'c1',
playerId: 'bob',
playerName: 'bob',
awardedPoints: 50,
rankBonus: 0,
awardedAtUtc: '2025-01-02T00:00:00.000Z',
position: 2,
},
);
expect(merged.map((r) => r.playerName)).toEqual(['alice', 'bob']);
expect(merged.map((r) => r.position)).toEqual([1, 2]);
});
it('does not double-insert when the same (playerId, solvedAtUtc) is replayed', () => {
const seed = [
{
position: 1,
playerId: 'alice',
playerName: 'alice',
solvedAtUtc: '2025-01-01T00:00:00.000Z',
awardedPoints: 100,
basePoints: 100,
rankBonus: 0,
isFirst: true,
isSecond: false,
isThird: false,
},
];
const dup = mergeSolveEventIntoSolvers(seed, {
challengeId: 'c1',
playerId: 'alice',
playerName: 'alice',
awardedPoints: 100,
rankBonus: 0,
awardedAtUtc: '2025-01-01T00:00:00.000Z',
position: 1,
});
expect(dup).toHaveLength(1);
});
it('marks gold/silver/bronze on the merged row based on the SSE position', () => {
const merged = mergeSolveEventIntoSolvers([], {
challengeId: 'c1',
playerId: 'gold',
playerName: 'gold',
awardedPoints: 100,
rankBonus: 50,
awardedAtUtc: '2025-01-01T00:00:00.000Z',
position: 1,
});
expect(merged[0].isFirst).toBe(true);
expect(merged[0].isSecond).toBe(false);
expect(merged[0].isThird).toBe(false);
expect(merged[0].rankBonus).toBe(50);
});
});
});