feat: Admin Area Challenges: List, Import/Export and Add/Edit Modal 1.03

This commit is contained in:
OpenVelo Agent
2026-07-22 22:18:07 +00:00
parent edb3feb457
commit d20e37d84b
7 changed files with 472 additions and 103 deletions
@@ -0,0 +1,98 @@
import {
importErrorMessage,
summarizeImportResult,
} from '../../frontend/src/app/features/admin/challenges/challenges.pure';
describe('Job 903: import error + result helpers', () => {
it('summarizeImportResult reports imported/skipped/warning counts', () => {
const r = {
imported: 2,
skipped: [{ name: 'X', reason: 'Unknown category: Y' }],
warnings: ['1 skipped.'],
};
const out = summarizeImportResult(r as any);
expect(out).toContain('Imported 2');
expect(out).toContain('Skipped 1');
expect(out).toContain('1 warning');
});
it('importErrorMessage prefers the nested error.message envelope', () => {
expect(importErrorMessage({ error: { message: 'Server said no' } }, 'fallback')).toBe('Server said no');
});
it('importErrorMessage falls back to err.message', () => {
expect(importErrorMessage({ message: 'boom' }, 'fallback')).toBe('boom');
});
it('importErrorMessage falls back to the supplied default when no message is available', () => {
expect(importErrorMessage({}, 'fallback')).toBe('fallback');
expect(importErrorMessage(null, 'fallback')).toBe('fallback');
});
});
describe('Job 903: confirmed import handler contract', () => {
function makeDoc() {
return { format: 'hipctf-challenges', version: 1, exportedAt: '', challenges: [] };
}
it('a successful confirm yields a closed-modal state and a status banner', async () => {
const state = {
importOpen: true as boolean,
importDoc: makeDoc() as any,
importPreview: { total: 2, conflicts: [], unknownCategories: [], warnings: [] } as any,
importResult: null as any,
importError: null as string | null,
importing: false,
statusMessage: null as string | null,
};
// Simulate onImportConfirm body using pure helpers.
try {
const result = { imported: 2, skipped: [], warnings: [] };
state.statusMessage = summarizeImportResult(result as any);
await Promise.resolve();
// Close-modal branch (the new behavior).
state.importOpen = false;
state.importDoc = null;
state.importPreview = null;
state.importResult = null;
state.importError = null;
} catch (err) {
state.importError = importErrorMessage(err, 'Failed to import challenges');
} finally {
state.importing = false;
}
expect(state.importOpen).toBe(false);
expect(state.importDoc).toBeNull();
expect(state.importPreview).toBeNull();
expect(state.importResult).toBeNull();
expect(state.importError).toBeNull();
expect(state.statusMessage).toContain('Imported 2');
expect(state.importing).toBe(false);
});
it('a rejected confirm keeps the modal open and surfaces the error message', async () => {
const state = {
importOpen: true as boolean,
importDoc: makeDoc() as any,
importPreview: { total: 1, conflicts: [], unknownCategories: [], warnings: [] } as any,
importResult: null as any,
importError: null as string | null,
importing: false,
statusMessage: null as string | null,
};
try {
throw { error: { message: 'Server said no' } };
} catch (err) {
state.importError = importErrorMessage(err, 'Failed to import challenges');
} finally {
state.importing = false;
}
expect(state.importOpen).toBe(true);
expect(state.importError).toBe('Server said no');
expect(state.importing).toBe(false);
});
});