AI Implementation feature(861): Admin Area Challenges: List, Import/Export and Add/Edit Modal (#40)
This commit was merged in pull request #40.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
AdminChallengeListItem,
|
||||
ImportPreviewResponse,
|
||||
ImportResultResponse,
|
||||
} from '../../frontend/src/app/core/services/admin.service';
|
||||
import {
|
||||
buildExportFilename,
|
||||
categoryIconSrc,
|
||||
deriveEmptyState,
|
||||
difficultyClass,
|
||||
formatFileSize,
|
||||
summarizeImportPreview,
|
||||
summarizeImportResult,
|
||||
} from '../../frontend/src/app/features/admin/challenges/challenges.pure';
|
||||
|
||||
function makeRow(overrides: Partial<AdminChallengeListItem> = {}): AdminChallengeListItem {
|
||||
return {
|
||||
id: 'c1',
|
||||
name: 'Alpha',
|
||||
categoryAbbreviation: 'CRY',
|
||||
categoryIconPath: '/uploads/icons/CRY.png',
|
||||
difficulty: 'LOW',
|
||||
protocol: 'WEB',
|
||||
enabled: true,
|
||||
createdAt: '2026-07-22T00:00:00.000Z',
|
||||
updatedAt: '2026-07-22T00:00:00.000Z',
|
||||
fileCount: 0,
|
||||
solveCount: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('challenges.pure helpers', () => {
|
||||
it('formatFileSize formats bytes / KB / MB', () => {
|
||||
expect(formatFileSize(0)).toBe('0 B');
|
||||
expect(formatFileSize(512)).toBe('512 B');
|
||||
expect(formatFileSize(2048)).toMatch(/KB$/);
|
||||
expect(formatFileSize(5 * 1024 * 1024)).toMatch(/MB$/);
|
||||
});
|
||||
|
||||
it('buildExportFilename derives an ISO-date filename', () => {
|
||||
const fixed = new Date('2026-07-22T12:00:00.000Z');
|
||||
expect(buildExportFilename(fixed)).toBe('challenges-export-2026-07-22.json');
|
||||
});
|
||||
|
||||
it('deriveEmptyState distinguishes empty-list, no-results-search, and loaded states', () => {
|
||||
expect(deriveEmptyState([], '', false)).toEqual({ kind: 'empty-list' });
|
||||
expect(deriveEmptyState([], 'crypto', false)).toEqual({
|
||||
kind: 'no-results-search',
|
||||
search: 'crypto',
|
||||
});
|
||||
expect(deriveEmptyState([makeRow()], '', false)).toBeNull();
|
||||
expect(deriveEmptyState([], '', true)).toBeNull();
|
||||
});
|
||||
|
||||
it('summarizeImportPreview builds a single-line preview summary', () => {
|
||||
const preview: ImportPreviewResponse = {
|
||||
total: 3,
|
||||
conflicts: ['Alpha', 'Beta'],
|
||||
unknownCategories: ['UNKNOWN'],
|
||||
warnings: [],
|
||||
};
|
||||
const out = summarizeImportPreview(preview);
|
||||
expect(out).toContain('Import 3 challenges');
|
||||
expect(out).toContain('overwritten');
|
||||
expect(out).toContain('missing categories');
|
||||
});
|
||||
|
||||
it('summarizeImportPreview handles empty document', () => {
|
||||
const preview: ImportPreviewResponse = {
|
||||
total: 0,
|
||||
conflicts: [],
|
||||
unknownCategories: [],
|
||||
warnings: [],
|
||||
};
|
||||
expect(summarizeImportPreview(preview)).toContain('no challenges');
|
||||
});
|
||||
|
||||
it('summarizeImportResult reports imported/skipped/warning counts', () => {
|
||||
const r: ImportResultResponse = {
|
||||
imported: 2,
|
||||
skipped: [{ name: 'X', reason: 'Unknown category: Y' }],
|
||||
warnings: ['1 skipped.'],
|
||||
};
|
||||
const out = summarizeImportResult(r);
|
||||
expect(out).toContain('Imported 2');
|
||||
expect(out).toContain('Skipped 1');
|
||||
expect(out).toContain('1 warning');
|
||||
});
|
||||
|
||||
it('difficultyClass returns the appropriate badge class', () => {
|
||||
expect(difficultyClass('LOW')).toContain('difficulty-low');
|
||||
expect(difficultyClass('MEDIUM')).toContain('difficulty-medium');
|
||||
expect(difficultyClass('HIGH')).toContain('difficulty-high');
|
||||
});
|
||||
|
||||
it('categoryIconSrc returns the supplied icon path or an empty string', () => {
|
||||
expect(categoryIconSrc('/uploads/icons/X.png', 'X')).toBe('/uploads/icons/X.png');
|
||||
expect(categoryIconSrc('', 'X')).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user