feat: Admin Area General Settings and Categories 1.14

This commit is contained in:
OpenVelo Agent
2026-07-22 18:38:12 +00:00
parent 63f2746e56
commit 493e4b244d
5 changed files with 455 additions and 298 deletions
@@ -0,0 +1,125 @@
import '@angular/compiler';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import {
syncCategoryForm,
CategoryFormMode,
} from '../../frontend/src/app/features/admin/categories/category-form-modal.component';
import { AdminCategory } from '../../frontend/src/app/core/services/admin.service';
function buildForm(): FormGroup<{
name: FormControl<string>;
abbreviation: FormControl<string>;
description: FormControl<string>;
}> {
const fb = new FormBuilder();
return fb.nonNullable.group({
name: fb.nonNullable.control('', []),
abbreviation: fb.nonNullable.control('', []),
description: fb.nonNullable.control(''),
});
}
function makeRow(overrides: Partial<AdminCategory> = {}): AdminCategory {
return {
id: 'row-1',
name: 'Cryptography',
abbreviation: 'CRY',
description: 'Cryptographic challenges',
iconPath: '',
isSystem: true,
systemKey: 'CRY',
createdAt: '',
updatedAt: '',
...overrides,
};
}
describe('syncCategoryForm - edit prefill', () => {
it('populates name, abbreviation, description for a system row and locks abbreviation', () => {
const form = buildForm();
const result = syncCategoryForm(form, 'edit', makeRow());
expect(form.controls.name.value).toBe('Cryptography');
expect(form.controls.abbreviation.value).toBe('CRY');
expect(form.controls.description.value).toBe('Cryptographic challenges');
expect(result.abbreviationReadonly).toBe(true);
expect(result.iconPreview).toBeNull();
});
it('does not lock abbreviation for a user (non-system) row', () => {
const form = buildForm();
const result = syncCategoryForm(
form,
'edit',
makeRow({
name: 'Misc',
abbreviation: 'MSC',
description: 'Other',
isSystem: false,
systemKey: null,
}),
);
expect(form.controls.name.value).toBe('Misc');
expect(form.controls.abbreviation.value).toBe('MSC');
expect(form.controls.description.value).toBe('Other');
expect(result.abbreviationReadonly).toBe(false);
});
it('re-populates when invoked a second time with a different category', () => {
const form = buildForm();
syncCategoryForm(
form,
'edit',
makeRow({ name: 'Alpha', abbreviation: 'AAA', description: 'first', isSystem: false, systemKey: null }),
);
expect(form.controls.name.value).toBe('Alpha');
expect(form.controls.abbreviation.value).toBe('AAA');
const result = syncCategoryForm(
form,
'edit',
makeRow({ name: 'Bravo', abbreviation: 'BBB', description: 'second', isSystem: false, systemKey: null }),
);
expect(form.controls.name.value).toBe('Bravo');
expect(form.controls.abbreviation.value).toBe('BBB');
expect(form.controls.description.value).toBe('second');
expect(result.abbreviationReadonly).toBe(false);
});
it('clears the form when invoked with mode="create" and a null category', () => {
const form = buildForm();
syncCategoryForm(form, 'edit', makeRow());
expect(form.controls.name.value).toBe('Cryptography');
const result = syncCategoryForm(form, 'create', null);
expect(form.controls.name.value).toBe('');
expect(form.controls.abbreviation.value).toBe('');
expect(form.controls.description.value).toBe('');
expect(result.abbreviationReadonly).toBe(false);
expect(result.iconPreview).toBeNull();
});
it('marks controls as pristine and untouched after prefill', () => {
const form = buildForm();
syncCategoryForm(form, 'edit', makeRow());
expect(form.controls.name.pristine).toBe(true);
expect(form.controls.name.touched).toBe(false);
expect(form.controls.abbreviation.pristine).toBe(true);
expect(form.controls.abbreviation.touched).toBe(false);
expect(form.controls.description.pristine).toBe(true);
expect(form.controls.description.touched).toBe(false);
});
it('returns iconPreview from category.iconPath when set', () => {
const form = buildForm();
const result = syncCategoryForm(
form,
'edit',
makeRow({ iconPath: '/uploads/icons/cry.png' }),
);
expect(result.iconPreview).toBe('/uploads/icons/cry.png');
});
});
+7 -1
View File
@@ -17,7 +17,13 @@ module.exports = {
roots: [__dirname],
testMatch: ['<rootDir>/frontend/**/*.spec.ts'],
transform: {
'^.+\\.ts$': ['ts-jest', { tsconfig: '<rootDir>/frontend.tsconfig.json' }],
'^.+\\.(ts|js|mjs)$': ['ts-jest', { tsconfig: '<rootDir>/frontend.tsconfig.json', useESM: false }],
},
transformIgnorePatterns: [
'/node_modules/(?!(@angular|rxjs|tslib)/)',
],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.mjs$': '$1',
},
setupFiles: ['<rootDir>/frontend/jest.setup.ts'],
},