| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994 |
- import { ComponentFixture, TestBed } from '@angular/core/testing';
- import { ActivatedRoute } from '@angular/router';
- import { of } from 'rxjs';
- import { AdminFieldSchema } from '../admin.models';
- import { CloudFunctionsService } from '../cloud-functions.service';
- import { AdminResourceComponent } from './admin-resource.component';
- describe('AdminResourceComponent', () => {
- let fixture: ComponentFixture<AdminResourceComponent>;
- let component: AdminResourceComponent;
- const functions = { admin: jasmine.createSpy('admin') };
- const objectField: AdminFieldSchema = { name: 'payload', type: 'Object', writable: true };
- beforeEach(async () => {
- functions.admin.calls.reset();
- await TestBed.configureTestingModule({
- imports: [AdminResourceComponent],
- providers: [
- { provide: ActivatedRoute, useValue: { paramMap: of({ get: () => 'CommonModel' }) } },
- { provide: CloudFunctionsService, useValue: functions },
- ],
- }).compileComponents();
- fixture = TestBed.createComponent(AdminResourceComponent);
- component = fixture.componentInstance;
- });
- it('keeps invalid JSON text visible and blocks saving it as a string', async () => {
- component.setField(objectField, '{invalid');
- expect(component.inputValue(objectField)).toBe('{invalid');
- expect(component.fieldErrors()['payload']).toContain('JSON 对象');
- await component.save();
- expect(functions.admin).not.toHaveBeenCalled();
- expect(component.error()).toContain('payload');
- });
- it('accepts a valid JSON object as structured data', () => {
- component.setField(objectField, '{"enabled":true}');
- expect(component.fieldErrors()['payload']).toBeUndefined();
- expect(component.draft()['payload']).toEqual({ enabled: true });
- });
- it('keeps preferred columns first and rounds page counts upward', () => {
- component.schema.set({
- className: 'CommonModel', label: '内容', writable: true, supportsSoftDelete: true,
- fields: [
- { name: 'misc', type: 'String', writable: true },
- { name: 'title', type: 'String', writable: true },
- { name: 'objectId', type: 'String', writable: false },
- ],
- });
- component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 21, results: [] });
- expect(component.columns().map((field) => field.name)).toEqual(['objectId', 'title', 'misc']);
- expect(component.pageCount()).toBe(2);
- });
- it('uses the dedicated cloud operation to reset an existing user password', async () => {
- functions.admin.and.resolveTo({ objectId: 'user-object' });
- component.className.set('_User');
- component.draftId.set('user-object');
- component.newPassword.set('SafePass123');
- component.confirmPassword.set('SafePass123');
- await component.resetPassword();
- expect(functions.admin).toHaveBeenCalledWith('resetPassword', { className: '_User', objectId: 'user-object', newPassword: 'SafePass123' });
- expect(component.passwordMessage()).toContain('已安全重置');
- });
- it('uses the dedicated ordinary-account operation without generic role fields', async () => {
- component.className.set('_User');
- component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
- component.createUsername.set('student-1001');
- component.createPassword.set('SafePass123');
- component.createConfirmPassword.set('SafePass123');
- component.createDisplayName.set('新学员');
- component.createMobile.set('13800138000');
- functions.admin.and.callFake(async (operation: string) => operation === 'createUser'
- ? { objectId: 'new-user', userId: 1001, groupId: 1 }
- : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
- await component.createUser();
- expect(functions.admin.calls.first().args).toEqual(['createUser', {
- username: 'student-1001', password: 'SafePass123', displayName: '新学员', mobile: '13800138000',
- }]);
- expect(component.editorOpen()).toBeFalse();
- });
- it('rejects mismatched account passwords before calling the cloud function', async () => {
- component.className.set('_User');
- component.createUsername.set('student-1002');
- component.createPassword.set('SafePass123');
- component.createConfirmPassword.set('Different123');
- await component.createUser();
- expect(functions.admin).not.toHaveBeenCalled();
- expect(component.createUserError()).toContain('不一致');
- });
- it('uses the protected batch operation to lock a user and reports revoked sessions', async () => {
- spyOn(window, 'confirm').and.returnValue(true);
- component.className.set('_User');
- component.draftId.set('member-object');
- component.draft.set({ objectId: 'member-object', isDisabled: false });
- component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
- functions.admin.and.callFake(async (operation: string) => operation === 'userBatch'
- ? { action: 'lock', updated: 1, revokedSessions: 2, results: [{ objectId: 'member-object', isDisabled: true }] }
- : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
- await component.setUserDisabled(true);
- expect(functions.admin.calls.first().args).toEqual(['userBatch', { className: '_User', action: 'lock', objectIds: ['member-object'], reason: '' }]);
- expect(component.draft()['isDisabled']).toBeTrue();
- expect(component.userActionMessage()).toContain('2 个会话');
- });
- it('moves a user through the dedicated group synchronization operation', async () => {
- component.className.set('_User');
- component.draftId.set('member-object');
- component.draft.set({ objectId: 'member-object', legacyGroupId: 1 });
- component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
- component.selectedUserGroupId.set(3);
- functions.admin.and.callFake(async (operation: string) => operation === 'userBatch'
- ? { action: 'move', updated: 1, revokedSessions: 0, results: [{ objectId: 'member-object', legacyGroupId: 3, legacyUserData: { GroupID: 3 } }] }
- : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
- await component.moveUserGroup();
- expect(functions.admin.calls.first().args).toEqual(['userBatch', { className: '_User', action: 'move', objectIds: ['member-object'], groupId: 3 }]);
- expect(component.draft()['legacyGroupId']).toBe(3);
- });
- it('routes group creation through the atomic group operation', async () => {
- component.className.set('Group');
- component.schema.set({ className: 'Group', label: '用户组', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ groupName: '普通会员', parentGroupId: 0, regSelect: true });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGroup'
- ? { objectId: 'group-object', groupId: 1, groupName: '普通会员' }
- : { className: 'Group', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGroup', {
- className: 'Group', objectId: undefined, fields: { groupName: '普通会员', parentGroupId: 0, regSelect: true },
- }]);
- });
- it('routes node creation through the structural cloud operation', async () => {
- component.className.set('Node');
- component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ nodeName: '新栏目', nodeDir: 'new-node', parentId: 999, nodeId: 999 });
- component.targetNodeParentId.set(16);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveNode'
- ? { objectId: 'node-object', nodeId: 401, nodeName: '新栏目', parentId: 16 }
- : { className: 'Node', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveNode', {
- className: 'Node', objectId: undefined, fields: { nodeName: '新栏目', nodeDir: 'new-node', parentId: 999, nodeId: 999 }, parentId: 16,
- }]);
- });
- it('moves a node through the cycle-protected batch operation', async () => {
- component.className.set('Node');
- component.draftId.set('node-object');
- component.draft.set({ objectId: 'node-object', nodeId: 40, parentId: 16, zstatus: 99 });
- component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
- component.targetNodeParentId.set(29);
- functions.admin.and.callFake(async (operation: string) => operation === 'nodeBatch'
- ? { action: 'move', updated: 1, results: [{ objectId: 'node-object', nodeId: 40, parentId: 29, zstatus: 99 }] }
- : { className: 'Node', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyNodeAction('move');
- expect(functions.admin.calls.first().args).toEqual(['nodeBatch', { className: 'Node', action: 'move', objectIds: ['node-object'], parentId: 29 }]);
- expect(component.draft()['parentId']).toBe(29);
- expect(component.nodeActionMessage()).toContain('29');
- });
- it('routes special creation through the structural cloud operation', async () => {
- component.className.set('Special');
- component.schema.set({ className: 'Special', label: '专题', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ specName: '新专题', specDir: 'new-special', specId: 999, pid: 999 });
- component.targetSpecialParentId.set(1);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveSpecial'
- ? { objectId: 'special-object', specId: 10, specName: '新专题', pid: 1 }
- : { className: 'Special', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveSpecial', {
- className: 'Special', objectId: undefined, fields: { specName: '新专题', specDir: 'new-special', specId: 999, pid: 999 }, pid: 1,
- }]);
- });
- it('moves a special through the dedicated batch operation', async () => {
- component.className.set('Special');
- component.draftId.set('special-object');
- component.draft.set({ objectId: 'special-object', specId: 10, pid: 1 });
- component.schema.set({ className: 'Special', label: '专题', writable: true, supportsSoftDelete: false, fields: [] });
- component.targetSpecialParentId.set(0);
- functions.admin.and.callFake(async (operation: string) => operation === 'specialBatch'
- ? { action: 'move', updated: 1, results: [{ objectId: 'special-object', specId: 10, pid: 0 }] }
- : { className: 'Special', page: 1, pageSize: 20, total: 1, results: [] });
- await component.moveSpecial();
- expect(functions.admin.calls.first().args).toEqual(['specialBatch', { className: 'Special', action: 'move', objectIds: ['special-object'], pid: 0 }]);
- expect(component.specialActionMessage()).toContain('0');
- });
- it('updates existing model metadata through the protected operation', async () => {
- component.className.set('Model');
- component.draftId.set('model-object');
- component.schema.set({ className: 'Model', label: '内容模型', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draft.set({ objectId: 'model-object', modelId: 2, tableName: 'ZL_C_Article', modelName: '文章模型', description: '安全说明' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveModelMetadata'
- ? { objectId: 'model-object', modelId: 2, modelName: '文章模型', description: '安全说明' }
- : { className: 'Model', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveModelMetadata', {
- className: 'Model', objectId: 'model-object', fields: { objectId: 'model-object', modelId: 2, tableName: 'ZL_C_Article', modelName: '文章模型', description: '安全说明' },
- }]);
- });
- it('updates existing model-field presentation metadata without structural writes', async () => {
- component.className.set('ModelField');
- component.draftId.set('field-object');
- component.schema.set({ className: 'ModelField', label: '模型字段', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draft.set({ objectId: 'field-object', fieldId: 3, modelId: 2, fieldName: 'author', fieldType: 'TextType', fieldAlias: '作者' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveModelFieldMetadata'
- ? { objectId: 'field-object', fieldId: 3, fieldAlias: '作者' }
- : { className: 'ModelField', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveModelFieldMetadata', {
- className: 'ModelField', objectId: 'field-object', fields: { objectId: 'field-object', fieldId: 3, modelId: 2, fieldName: 'author', fieldType: 'TextType', fieldAlias: '作者' },
- }]);
- });
- it('updates an existing guestbook entry through the dedicated editor', async () => {
- component.className.set('Guestbook');
- component.draftId.set('guest-object');
- component.schema.set({ className: 'Guestbook', label: '留言', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draft.set({ objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGuestbook'
- ? { objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' }
- : { className: 'Guestbook', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGuestbook', {
- className: 'Guestbook', objectId: 'guest-object', fields: { objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' },
- }]);
- });
- it('audits a guestbook entry through the workflow operation', async () => {
- component.className.set('Guestbook');
- component.draftId.set('guest-object');
- component.draft.set({ objectId: 'guest-object', gid: 10, status: 0 });
- component.schema.set({ className: 'Guestbook', label: '留言', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- functions.admin.and.callFake(async (operation: string) => operation === 'guestbookBatch'
- ? { action: 'audit', updated: 1, results: [{ objectId: 'guest-object', gid: 10, status: 99 }] }
- : { className: 'Guestbook', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyGuestbookAction('audit');
- expect(functions.admin.calls.first().args).toEqual(['guestbookBatch', { className: 'Guestbook', action: 'audit', objectIds: ['guest-object'] }]);
- expect(component.selectedGuestbookStatus()).toBe(99);
- });
- it('publishes an admin reply through the parent-safe cloud operation', async () => {
- component.className.set('Guestbook');
- component.draftId.set('guest-object');
- component.guestbookReplyTitle.set('回复标题');
- component.guestbookReplyContent.set('回复正文');
- functions.admin.and.resolveTo({ objectId: 'reply-object', gid: 11, parentid: 10, status: 99 });
- await component.replyGuestbook();
- expect(functions.admin).toHaveBeenCalledWith('guestbookReply', { className: 'Guestbook', parentObjectId: 'guest-object', title: '回复标题', content: '回复正文' });
- expect(component.guestbookActionMessage()).toContain('已发布');
- });
- it('creates a guest category through the atomic category operation', async () => {
- component.className.set('Guestcate');
- component.schema.set({ className: 'Guestcate', label: '留言分类', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ catename: '新贴吧版块', gtype: 0, parentId: 999, cateid: 999 });
- component.guestCategoryType.set(1);
- component.targetGuestCategoryParentId.set(7);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGuestCategory'
- ? { objectId: 'category-object', cateid: 20, catename: '新贴吧版块', gtype: 1, parentId: 7 }
- : { className: 'Guestcate', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGuestCategory', {
- className: 'Guestcate', objectId: undefined, fields: { catename: '新贴吧版块', gtype: 0, parentId: 999, cateid: 999 }, gtype: 1, parentId: 7,
- }]);
- });
- it('creates a question category through the atomic structural operation', async () => {
- component.className.set('ExamClass');
- component.schema.set({ className: 'ExamClass', label: '试题分类', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ cClassName: '英语', cId: 999, cClassid: 999 });
- component.targetExamClassParentId.set(7);
- component.examClassType.set(2);
- component.examClassOrder.set(8);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveExamClass'
- ? { objectId: 'exam-class-object', cId: 20, cClassName: '英语', cClassid: 7, cClassType: 2, cOrderBy: 8 }
- : { className: 'ExamClass', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveExamClass', {
- className: 'ExamClass', objectId: undefined, fields: { cClassName: '英语', cId: 999, cClassid: 999 }, parentId: 7, classType: 2, orderId: 8,
- }]);
- });
- it('creates an exam point through the atomic structural operation', async () => {
- component.className.set('ExamPoint');
- component.schema.set({ className: 'ExamPoint', label: '考点管理', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ testPoint: '第一考点', id: 999, tid: 999 });
- component.targetExamPointParentId.set(3);
- component.examPointOrder.set(5);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveExamPoint'
- ? { objectId: 'exam-point-object', id: 20, testPoint: '第一考点', tid: 3, orderBy: 5 }
- : { className: 'ExamPoint', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveExamPoint', {
- className: 'ExamPoint', objectId: undefined, fields: { testPoint: '第一考点', id: 999, tid: 999 }, parentId: 3, orderId: 5,
- }]);
- });
- it('creates a knowledge node through the subject-safe structural operation', async () => {
- component.className.set('QuestionsKnowledge');
- component.schema.set({ className: 'QuestionsKnowledge', label: '知识点', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ kName: '现在完成时', kId: 999, kClassId: 999, status: 1, grade: 3, isSys: 0 });
- component.targetKnowledgeClassId.set(7);
- component.targetKnowledgeParentId.set(2);
- component.knowledgeOrder.set(5);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveKnowledge'
- ? { objectId: 'knowledge-object', kId: 20, kName: '现在完成时', kClassId: 7, pid: 2, kOrderBy: 5 }
- : { className: 'QuestionsKnowledge', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveKnowledge', {
- className: 'QuestionsKnowledge', objectId: undefined, fields: { kName: '现在完成时', kId: 999, kClassId: 999, status: 1, grade: 3, isSys: 0 }, classId: 7, parentId: 2, orderId: 5,
- }]);
- });
- it('creates an exam teacher through the subject-safe operation', async () => {
- component.className.set('ExTeacher');
- component.schema.set({ className: 'ExTeacher', label: '考试教师', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ tname: '张老师', tclsss: 999, post: '高级教师', teach: '英语', remark: '简介' });
- component.targetTeacherClassId.set(7);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveExamTeacher'
- ? { objectId: 'teacher-object', id: 20, tname: '张老师', tclsss: 7 }
- : { className: 'ExTeacher', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveExamTeacher', {
- className: 'ExTeacher', objectId: undefined, fields: { tname: '张老师', tclsss: 999, post: '高级教师', teach: '英语', remark: '简介' }, classId: 7,
- }]);
- });
- it('creates a dictionary category through the collision-safe operation', async () => {
- component.className.set('Datadiccategory');
- component.schema.set({ className: 'Datadiccategory', label: '数据字典分类', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ categoryname: '学习状态', diccateid: 999, isused: true });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveDictionaryCategory'
- ? { objectId: 'dictionary-category-object', diccateid: 20, categoryname: '学习状态', isused: true }
- : { className: 'Datadiccategory', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveDictionaryCategory', {
- className: 'Datadiccategory', objectId: undefined, fields: { categoryname: '学习状态', diccateid: 999, isused: true },
- }]);
- });
- it('creates a dictionary item through the category-safe operation', async () => {
- component.className.set('Datadic');
- component.schema.set({ className: 'Datadic', label: '数据字典项', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ dicname: '已完成', dicid: 999, diccate: 999, isused: true });
- component.targetDictionaryCategoryId.set(20);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveDictionaryItem'
- ? { objectId: 'dictionary-item-object', dicid: 30, diccate: 20, dicname: '已完成', isused: true }
- : { className: 'Datadic', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveDictionaryItem', {
- className: 'Datadic', objectId: undefined, fields: { dicname: '已完成', dicid: 999, diccate: 999, isused: true }, categoryId: 20,
- }]);
- });
- it('batch disables selected dictionary records through the dedicated operation', async () => {
- component.className.set('Datadic');
- component.selectedIds.set(['dictionary-a', 'dictionary-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'dictionaryBatch'
- ? { action: 'disable', updated: 2, results: [] }
- : { className: 'Datadic', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkDictionaryAction('disable');
- expect(functions.admin.calls.first().args).toEqual(['dictionaryBatch', { className: 'Datadic', action: 'disable', objectIds: ['dictionary-a', 'dictionary-b'] }]);
- expect(component.bulkMessage()).toContain('2 条字典记录');
- });
- it('creates a multi-level dictionary category through the dedicated operation', async () => {
- component.className.set('GradeCate');
- component.schema.set({ className: 'GradeCate', label: '多级字典分类', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ cateName: '地区联动', gradeField: '省|市', cateId: 999, remark: '两级地区' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGradeCategory'
- ? { objectId: 'grade-category-object', cateId: 20, cateName: '地区联动', gradeField: '省|市' }
- : { className: 'GradeCate', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGradeCategory', {
- className: 'GradeCate', objectId: undefined, fields: { cateName: '地区联动', gradeField: '省|市', cateId: 999, remark: '两级地区' },
- }]);
- });
- it('creates a multi-level dictionary option through the parent-safe operation', async () => {
- component.className.set('Grade');
- component.schema.set({ className: 'Grade', label: '多级字典选项', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ gradeName: '杭州市', gradeId: 999, cate: 999, parentId: 999, grade: 999 });
- component.targetGradeCategoryId.set(20); component.targetGradeParentId.set(30);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGradeOption'
- ? { objectId: 'grade-option-object', gradeId: 40, cate: 20, parentId: 30, grade: 2, gradeName: '杭州市' }
- : { className: 'Grade', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGradeOption', {
- className: 'Grade', objectId: undefined, fields: { gradeName: '杭州市', gradeId: 999, cate: 999, parentId: 999, grade: 999 }, categoryId: 20, parentId: 30,
- }]);
- });
- it('batch deletes selected multi-level dictionary options with the dedicated operation', async () => {
- component.className.set('Grade'); component.selectedIds.set(['grade-a', 'grade-b']); spyOn(window, 'confirm').and.returnValue(true);
- functions.admin.and.callFake(async (operation: string) => operation === 'gradeBatch'
- ? { action: 'delete', updated: 2, results: [] }
- : { className: 'Grade', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkGradeDelete();
- expect(functions.admin.calls.first().args).toEqual(['gradeBatch', { className: 'Grade', action: 'delete', objectIds: ['grade-a', 'grade-b'] }]);
- expect(component.bulkMessage()).toContain('2 条多级字典记录');
- });
- it('updates an existing currency through the validated dedicated operation', async () => {
- component.className.set('Currency');
- component.schema.set({ className: 'Currency', label: '货币管理', writable: true, supportsSoftDelete: false, fields: [] });
- component.draftId.set('currency-object');
- component.draft.set({ title: '测试币', currencyName: 'TST', currencySymbol: 'T$', currentExchange: 1.25, remark: '测试', id: 999 });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveCurrency'
- ? { objectId: 'currency-object', id: 20, title: '测试币', currencyName: 'TST' }
- : { className: 'Currency', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveCurrency', {
- className: 'Currency', objectId: 'currency-object', fields: { title: '测试币', currencyName: 'TST', currencySymbol: 'T$', currentExchange: 1.25, remark: '测试', id: 999 },
- }]);
- });
- it('batch deletes selected currencies through the tenant-safe operation', async () => {
- component.className.set('Currency'); component.selectedIds.set(['currency-a']); spyOn(window, 'confirm').and.returnValue(true);
- functions.admin.and.callFake(async (operation: string) => operation === 'currencyBatch'
- ? { action: 'delete', updated: 1, results: [] }
- : { className: 'Currency', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkCurrencyDelete();
- expect(functions.admin.calls.first().args).toEqual(['currencyBatch', { className: 'Currency', action: 'delete', objectIds: ['currency-a'] }]);
- expect(component.bulkMessage()).toContain('1 条货币记录');
- });
- it('updates an existing holiday through the validated dedicated operation', async () => {
- component.className.set('SysHoliday');
- component.schema.set({ className: 'SysHoliday', label: '节假日管理', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('holiday-object');
- component.draft.set({ name: '春节', htype: '农历', hdate: '01月01日', hdateStart: 1, hdateEnd: 6, zstatus: 99, id: 999, sourceKey: 'blocked' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveHoliday'
- ? { objectId: 'holiday-object', id: 1, name: '春节' }
- : { className: 'SysHoliday', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveHoliday', {
- className: 'SysHoliday', objectId: 'holiday-object', fields: { name: '春节', htype: '农历', hdate: '01月01日', hdateStart: 1, hdateEnd: 6, zstatus: 99, id: 999, sourceKey: 'blocked' },
- }]);
- });
- it('batch deletes selected holidays through the tenant-safe operation', async () => {
- component.className.set('SysHoliday'); component.selectedIds.set(['holiday-a', 'holiday-b']); spyOn(window, 'confirm').and.returnValue(true);
- functions.admin.and.callFake(async (operation: string) => operation === 'holidayBatch'
- ? { action: 'delete', updated: 2, results: [] }
- : { className: 'SysHoliday', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkHolidayDelete();
- expect(functions.admin.calls.first().args).toEqual(['holidayBatch', { className: 'SysHoliday', action: 'delete', objectIds: ['holiday-a', 'holiday-b'] }]);
- expect(component.bulkMessage()).toContain('2 条节假日记录');
- });
- it('loads shortcut navigation through the dedicated filtered operation', async () => {
- component.className.set('Search');
- component.schema.set({ className: 'Search', label: '后台快捷入口', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.filterSearchNavigationType.set(2); component.filterSearchNavigationState.set(1); component.filterSearchNavigationElite.set(0); component.search.set('学习');
- functions.admin.and.resolveTo({ className: 'Search', page: 2, pageSize: 20, total: 21, results: [] });
- await component.load(2);
- expect(functions.admin.calls.first().args).toEqual(['searchNavigations', { className: 'Search', page: 2, pageSize: 20, search: '学习', type: 2, state: 1, elite: 0 }]);
- });
- it('updates an existing shortcut through the validated dedicated operation', async () => {
- component.className.set('Search');
- component.schema.set({ className: 'Search', label: '后台快捷入口', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('search-object');
- component.draft.set({ name: '学习记录', fileUrl: '/admin/resources/DailyStudyRecord', ico: 'list', openType: 0, mobile: 1, size: 2, bkcolor: '#1E86EA', userGroup: '1,2', eliteLevel: 1, id: 19, state: 1 });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveSearchNavigation'
- ? { objectId: 'search-object', id: 19, name: '学习记录' }
- : { className: 'Search', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveSearchNavigation', {
- className: 'Search', objectId: 'search-object', fields: { name: '学习记录', fileUrl: '/admin/resources/DailyStudyRecord', ico: 'list', openType: 0, mobile: 1, size: 2, bkcolor: '#1E86EA', userGroup: '1,2', eliteLevel: 1, id: 19, state: 1 },
- }]);
- });
- it('orders selected shortcuts through the dedicated batch operation', async () => {
- component.className.set('Search'); component.selectedIds.set(['search-a', 'search-b']); component.searchNavigationOrderStart.set(7);
- functions.admin.and.callFake(async (operation: string) => operation === 'searchNavigationBatch'
- ? { action: 'order', updated: 2, results: [] }
- : { className: 'Search', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkSearchNavigationAction('order');
- expect(functions.admin.calls.first().args).toEqual(['searchNavigationBatch', { className: 'Search', action: 'order', objectIds: ['search-a', 'search-b'], orders: [{ objectId: 'search-a', orderId: 7 }, { objectId: 'search-b', orderId: 8 }] }]);
- expect(component.bulkMessage()).toContain('2 条快捷入口');
- });
- it('loads advertising content through the dedicated zone and status filters', async () => {
- component.className.set('AdInfo');
- component.schema.set({ className: 'AdInfo', label: '广告内容管理', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.filterAdInfoZoneId.set('5'); component.filterAdStatus.set(99); component.search.set('横幅');
- functions.admin.and.resolveTo({ className: 'AdInfo', page: 2, pageSize: 20, total: 21, results: [] });
- await component.load(2);
- expect(functions.admin.calls.first().args).toEqual(['adInfos', { className: 'AdInfo', page: 2, pageSize: 20, search: '横幅', zoneId: '5', status: 99 }]);
- });
- it('updates an existing advertisement through the zone-safe operation', async () => {
- component.className.set('AdInfo');
- component.schema.set({ className: 'AdInfo', label: '广告内容管理', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('ad-info-object'); component.targetAdZoneId.set('5');
- component.draft.set({ objectId: 'ad-info-object', id: 5, name: '矩形横幅广告', zoneId: '999', priority: '2', zstatus: 99, sdate: '2026-08-01T00:00:00.000Z', edate: '2026-09-01T00:00:00.000Z' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveAdInfo'
- ? { objectId: 'ad-info-object', id: 5, name: '矩形横幅广告', zoneId: '5' }
- : { className: 'AdInfo', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveAdInfo', {
- className: 'AdInfo', objectId: 'ad-info-object', fields: { objectId: 'ad-info-object', id: 5, name: '矩形横幅广告', zoneId: '999', priority: '2', zstatus: 99, sdate: '2026-08-01T00:00:00.000Z', edate: '2026-09-01T00:00:00.000Z' }, zoneId: '5',
- }]);
- });
- it('pauses selected advertising zones through the dedicated batch operation', async () => {
- component.className.set('AdZone'); component.selectedIds.set(['zone-a', 'zone-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'adZoneBatch'
- ? { action: 'pause', updated: 2, results: [] }
- : { className: 'AdZone', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkAdvertisementAction('pause');
- expect(functions.admin.calls.first().args).toEqual(['adZoneBatch', { className: 'AdZone', action: 'pause', objectIds: ['zone-a', 'zone-b'] }]);
- expect(component.bulkMessage()).toContain('2 条广告位');
- });
- it('loads active campaign previews without executing legacy HTML', async () => {
- component.className.set('AdZone'); component.draftId.set('zone-object');
- functions.admin.and.resolveTo({ zone: { objectId: 'zone-object', id: '1' }, total: 1, results: [{ objectId: 'ad-object', name: '文字广告', html: '<script>unsafe()</script>' }] });
- await component.loadAdPreview();
- expect(functions.admin).toHaveBeenCalledWith('adPreview', { zoneObjectId: 'zone-object' });
- expect(component.adPreview()[0]['html']).toContain('<script>');
- });
- it('loads font-shape materials through the dedicated category filter', async () => {
- component.className.set('FontPicShape');
- component.schema.set({ className: 'FontPicShape', label: '字体图形素材', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.filterFontShapeTypeId.set(2); component.search.set('鹿');
- functions.admin.and.resolveTo({ className: 'FontPicShape', page: 2, pageSize: 20, total: 21, results: [] });
- await component.load(2);
- expect(functions.admin.calls.first().args).toEqual(['fontShapes', { className: 'FontPicShape', page: 2, pageSize: 20, search: '鹿', typeId: 2 }]);
- });
- it('updates existing font-shape metadata without sending the blocked file field', async () => {
- component.className.set('FontPicShape');
- component.schema.set({ className: 'FontPicShape', label: '字体图形素材', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('shape-object'); component.targetFontShapeTypeId.set(2);
- component.draft.set({ objectId: 'shape-object', id: 2, name: '鹿', typeId: 999, shape: '/legacy/deer.png', remarks: '动物' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveFontShape'
- ? { objectId: 'shape-object', id: 2, name: '鹿', typeId: 2, shape: '/legacy/deer.png' }
- : { className: 'FontPicShape', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveFontShape', {
- className: 'FontPicShape', objectId: 'shape-object', fields: { objectId: 'shape-object', id: 2, name: '鹿', typeId: 999, shape: '/legacy/deer.png', remarks: '动物' }, typeId: 2,
- }]);
- });
- it('deletes selected font-shape categories through the reference-safe operation', async () => {
- component.className.set('FontPicShapeType'); component.selectedIds.set(['shape-type-a']); spyOn(window, 'confirm').and.returnValue(true);
- functions.admin.and.callFake(async (operation: string) => operation === 'fontShapeTypeBatch'
- ? { action: 'delete', updated: 1, results: [] }
- : { className: 'FontPicShapeType', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkFontShapeDelete();
- expect(functions.admin.calls.first().args).toEqual(['fontShapeTypeBatch', { className: 'FontPicShapeType', action: 'delete', objectIds: ['shape-type-a'] }]);
- expect(component.bulkMessage()).toContain('1 条图形分类');
- });
- it('loads survey questions through the selected legacy survey id', async () => {
- component.className.set('DesignQuestion');
- component.schema.set({ className: 'DesignQuestion', label: '问卷题目', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.filterSurveyId.set(2); component.search.set('阅读');
- functions.admin.and.resolveTo({ className: 'DesignQuestion', page: 2, pageSize: 20, total: 21, results: [] });
- await component.load(2, false);
- expect(functions.admin.calls.first().args).toEqual(['surveyQuestions', { className: 'DesignQuestion', page: 2, pageSize: 20, search: '阅读', askId: 2 }]);
- });
- it('updates existing survey metadata through the dedicated operation', async () => {
- component.className.set('DesignAsk');
- component.schema.set({ className: 'DesignAsk', label: '问卷调查', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('survey-object'); component.draft.set({ objectId: 'survey-object', id: 2, title: '第二组问题', zstatus: 99 });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveSurvey'
- ? { objectId: 'survey-object', id: 2, title: '第二组问题', zstatus: 99 }
- : { className: 'DesignAsk', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveSurvey', { className: 'DesignAsk', objectId: 'survey-object', fields: { objectId: 'survey-object', id: 2, title: '第二组问题', zstatus: 99 } }]);
- });
- it('starts selected surveys through the dedicated batch operation', async () => {
- component.className.set('DesignAsk'); component.selectedIds.set(['survey-a']);
- functions.admin.and.callFake(async (operation: string) => operation === 'surveyBatch'
- ? { action: 'start', updated: 1, results: [] }
- : { className: 'DesignAsk', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyBulkSurveyAction('start');
- expect(functions.admin.calls.first().args).toEqual(['surveyBatch', { className: 'DesignAsk', action: 'start', objectIds: ['survey-a'] }]);
- expect(component.bulkMessage()).toContain('1 条问卷记录');
- });
- it('loads a privacy-safe aggregate for the selected survey results', async () => {
- component.className.set('DesignAnswer'); component.filterSurveyId.set(2);
- functions.admin.and.resolveTo({ survey: { id: 2, title: '第二组问题' }, responseCount: 35, invalidAnswers: 1, questions: [] });
- await component.loadSurveyChart();
- expect(functions.admin).toHaveBeenCalledWith('surveyResultChart', { askId: 2 });
- expect(component.surveyChart()?.responseCount).toBe(35);
- });
- it('loads customer-service seats through the dedicated operation', async () => {
- component.className.set('ServiceSeat');
- component.schema.set({ className: 'ServiceSeat', label: '客服席位', writable: true, creatable: true, supportsSoftDelete: false, fields: [] });
- component.search.set('admin'); functions.admin.and.resolveTo({ className: 'ServiceSeat', page: 1, pageSize: 20, total: 1, results: [] });
- await component.load(1, false);
- expect(functions.admin).toHaveBeenCalledWith('serviceSeats', { className: 'ServiceSeat', page: 1, pageSize: 20, search: 'admin' });
- });
- it('creates a service seat while binding an existing account', async () => {
- component.className.set('ServiceSeat');
- component.schema.set({ className: 'ServiceSeat', label: '客服席位', writable: true, creatable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ sName: '在线客服', sDefault: 1, sIndex: 2 }); component.targetServiceSeatUsername.set('admin');
- functions.admin.and.callFake(async (operation: string) => operation === 'saveServiceSeat'
- ? { objectId: 'seat-object', sId: 2, sName: '在线客服', sRemrk: 'admin' }
- : { className: 'ServiceSeat', page: 1, pageSize: 20, total: 2, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveServiceSeat', { className: 'ServiceSeat', objectId: undefined, fields: { sName: '在线客服', sDefault: 1, sIndex: 2 }, username: 'admin' }]);
- });
- it('deletes selected service welcome messages through the scoped operation', async () => {
- component.className.set('Temp'); component.selectedIds.set(['code-object']); spyOn(window, 'confirm').and.returnValue(true);
- functions.admin.and.callFake(async (operation: string) => operation === 'serviceBatch'
- ? { action: 'delete', updated: 1, results: [] }
- : { className: 'Temp', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkServiceDelete();
- expect(functions.admin.calls.first().args).toEqual(['serviceBatch', { className: 'Temp', action: 'delete', objectIds: ['code-object'] }]);
- expect(component.bulkMessage()).toContain('1 条客服记录');
- });
- it('loads stores through status-safe dedicated pagination', async () => {
- component.className.set('StoreApplication'); component.schema.set({ className: 'StoreApplication', label: '店铺管理', writable: true, creatable: false, supportsSoftDelete: false, fields: [] }); component.filterStoreStatus.set(99); component.search.set('逐浪');
- functions.admin.and.resolveTo({ className: 'StoreApplication', page: 1, pageSize: 20, total: 1, results: [] });
- await component.load(1, false);
- expect(functions.admin).toHaveBeenCalledWith('storeApplications', { className: 'StoreApplication', page: 1, pageSize: 20, search: '逐浪', status: 99 });
- });
- it('updates an existing store through account and style validation', async () => {
- component.className.set('StoreApplication'); component.schema.set({ className: 'StoreApplication', label: '店铺管理', writable: true, creatable: false, supportsSoftDelete: false, fields: [] }); component.draftId.set('store-object'); component.draft.set({ objectId: 'store-object', id: 1, storeName: '逐浪网店', userName: 'old' }); component.targetStoreUsername.set('admin'); component.targetStoreStyleId.set(2);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveStoreApplication' ? { objectId: 'store-object', id: 1, storeName: '逐浪网店', userName: 'admin', storeStyleId: 2 } : { className: 'StoreApplication', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveStoreApplication', { className: 'StoreApplication', objectId: 'store-object', fields: { objectId: 'store-object', id: 1, storeName: '逐浪网店', userName: 'old' }, username: 'admin', styleId: 2 }]);
- });
- it('audits selected stores without exposing the blocked delete action', async () => {
- component.className.set('StoreApplication'); component.selectedIds.set(['store-a']); functions.admin.and.callFake(async (operation: string) => operation === 'storeBatch' ? { action: 'audit', updated: 1, results: [] } : { className: 'StoreApplication', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyBulkStoreAction('audit');
- expect(functions.admin.calls.first().args).toEqual(['storeBatch', { className: 'StoreApplication', action: 'audit', objectIds: ['store-a'] }]);
- expect(component.bulkMessage()).toContain('1 条店铺记录');
- });
- it('updates a forum post through the dedicated workflow operation', async () => {
- component.className.set('GuestBar');
- component.draftId.set('bar-object');
- component.draft.set({ objectId: 'bar-object', id: 10, cateId: 7, status: 0, orderFlag: 0 });
- functions.admin.and.callFake(async (operation: string) => operation === 'barBatch'
- ? { action: 'top-global', updated: 1, results: [{ objectId: 'bar-object', id: 10, cateId: 7, status: 0, orderFlag: 2 }] }
- : { className: 'GuestBar', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyBarAction('top-global');
- expect(functions.admin.calls.first().args).toEqual(['barBatch', { className: 'GuestBar', action: 'top-global', objectIds: ['bar-object'] }]);
- expect(component.draft()['orderFlag']).toBe(2);
- });
- it('moves selected forum posts through the tenant-safe batch operation', async () => {
- component.className.set('GuestBar');
- component.selectedIds.set(['bar-a', 'bar-b']);
- component.targetBarCategoryId.set(8);
- functions.admin.and.callFake(async (operation: string) => operation === 'barBatch'
- ? { action: 'move', updated: 2, results: [] }
- : { className: 'GuestBar', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkBarAction('move');
- expect(functions.admin.calls.first().args).toEqual(['barBatch', { className: 'GuestBar', action: 'move', objectIds: ['bar-a', 'bar-b'], cateId: 8 }]);
- expect(component.bulkMessage()).toContain('2 条帖子');
- });
- it('updates content workflow state through the dedicated batch operation', async () => {
- component.className.set('CommonModel');
- component.draftId.set('content-object');
- component.draft.set({ objectId: 'content-object', status: 0, nodeId: 29 });
- component.schema.set({ className: 'CommonModel', label: '内容', writable: true, creatable: false, supportsSoftDelete: true, fields: [] });
- component.selectedContentStatus.set(99);
- functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
- ? { action: 'status', updated: 1, results: [{ objectId: 'content-object', status: 99, nodeId: 29 }] }
- : { className: 'CommonModel', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyContentAction('status');
- expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'status', objectIds: ['content-object'], status: 99 }]);
- expect(component.contentStatusValue()).toBe(99);
- expect(component.contentActionMessage()).toContain('已审核');
- });
- it('refreshes legacy content timestamps through the dedicated batch operation', async () => {
- component.className.set('CommonModel');
- component.draftId.set('content-object');
- component.draft.set({ objectId: 'content-object', status: 99, nodeId: 29 });
- functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
- ? { action: 'refresh', updated: 1, results: [{ objectId: 'content-object', status: 99, nodeId: 29, createTime: '2026-08-18T00:00:00.000Z', upDateTime: '2026-08-18T00:00:00.000Z' }] }
- : { className: 'CommonModel', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyContentAction('refresh');
- expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'refresh', objectIds: ['content-object'] }]);
- expect(component.contentActionMessage()).toContain('时间已刷新');
- });
- it('checks duplicate content titles through the tenant-safe compatibility operation', async () => {
- component.className.set('CommonModel');
- component.draft.set({ objectId: 'content-object', title: '同名内容' });
- functions.admin.and.resolveTo({ title: '同名内容', total: 2, results: [{ objectId: 'content-object' }, { objectId: 'content-other' }] });
- await component.checkDuplicateContentTitle();
- expect(functions.admin).toHaveBeenCalledWith('legacyContentUtility', { action: 'duplicate-title', title: '同名内容' });
- expect(component.duplicateContentTitles().length).toBe(2);
- expect(component.contentActionMessage()).toContain('2 条同名内容');
- });
- it('selects and clears all records on the current page', () => {
- component.className.set('CommonModel');
- component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 2, results: [{ objectId: 'content-a' }, { objectId: 'content-b' }] });
- component.togglePageSelection(true);
- expect(component.selectedIds()).toEqual(['content-a', 'content-b']);
- expect(component.allPageSelected()).toBeTrue();
- component.toggleSelection('content-a', false);
- expect(component.selectedIds()).toEqual(['content-b']);
- expect(component.allPageSelected()).toBeFalse();
- });
- it('sends all selected content ids through the bulk workflow operation', async () => {
- component.className.set('CommonModel');
- component.schema.set({ className: 'CommonModel', label: '内容', writable: true, creatable: false, supportsSoftDelete: true, fields: [] });
- component.selectedIds.set(['content-a', 'content-b']);
- component.selectedContentStatus.set(99);
- functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
- ? { action: 'status', updated: 2, results: [] }
- : { className: 'CommonModel', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkContentAction('status');
- expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'status', objectIds: ['content-a', 'content-b'], status: 99 }]);
- expect(component.bulkMessage()).toContain('2 条内容');
- expect(component.selectedIds()).toEqual([]);
- });
- it('recovers all selected nodes through the dedicated batch operation', async () => {
- component.className.set('Node');
- component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
- component.selectedIds.set(['node-a', 'node-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'nodeBatch'
- ? { action: 'recover', updated: 2, results: [] }
- : { className: 'Node', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkNodeAction('recover');
- expect(functions.admin.calls.first().args).toEqual(['nodeBatch', { className: 'Node', action: 'recover', objectIds: ['node-a', 'node-b'] }]);
- expect(component.bulkMessage()).toContain('2 个栏目');
- });
- it('loads and opens the virtual global content-tag resource through dedicated cloud operations', async () => {
- component.className.set('ContentTagKey');
- functions.admin.and.callFake(async (operation: string) => operation === 'schema'
- ? { className: 'ContentTagKey', label: '内容标签词库', writable: true, creatable: false, supportsSoftDelete: false, fields: [] }
- : operation === 'contentTagKeys'
- ? { className: 'ContentTagKey', page: 1, pageSize: 20, total: 1, results: [{ objectId: 'global-content-tags', count: 2, content: '教育|英语' }] }
- : { objectId: 'global-content-tags', count: 2, content: '教育|英语', tags: ['教育', '英语'] });
- await component.load(1, true);
- await component.openEdit({ objectId: 'global-content-tags' });
- expect(functions.admin).toHaveBeenCalledWith('contentTagKeys', { className: 'ContentTagKey', page: 1, pageSize: 20, search: '' });
- expect(functions.admin).toHaveBeenCalledWith('getContentTagKeys', { className: 'ContentTagKey', objectId: 'global-content-tags' });
- expect(component.tagKeyContent()).toBe('教育|英语');
- });
- it('saves global content tags through Parse Config instead of generic CRUD', async () => {
- component.className.set('ContentTagKey');
- component.schema.set({ className: 'ContentTagKey', label: '内容标签词库', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('global-content-tags');
- component.draft.set({ objectId: 'global-content-tags', count: 2, content: '教育|英语', tags: ['教育', '英语'] });
- component.tagKeyContent.set('教育 | 英语\n阅读');
- functions.admin.and.callFake(async (operation: string) => operation === 'saveContentTagKeys'
- ? { objectId: 'global-content-tags', count: 3, content: '教育|英语|阅读', tags: ['教育', '英语', '阅读'] }
- : { className: 'ContentTagKey', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveContentTagKeys', {
- className: 'ContentTagKey', objectId: 'global-content-tags', fields: { objectId: 'global-content-tags', count: 2, content: '教育|英语', tags: ['教育', '英语'] }, content: '教育 | 英语\n阅读',
- }]);
- });
- it('updates an existing user level through its protected operation', async () => {
- component.className.set('UserLevel');
- component.schema.set({ className: 'UserLevel', label: '积分等级', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draftId.set('level-object');
- component.draft.set({ objectId: 'level-object', id: 1, alias: '白银会员', pointRate: 100, image: '/level.png', remark: '等级备注' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveUserLevel'
- ? { objectId: 'level-object', id: 1, alias: '白银会员', pointRate: 100 }
- : { className: 'UserLevel', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveUserLevel', {
- className: 'UserLevel', objectId: 'level-object', fields: { objectId: 'level-object', id: 1, alias: '白银会员', pointRate: 100, image: '/level.png', remark: '等级备注' },
- }]);
- });
- it('loads the selected normalized money ledger with date filters', async () => {
- component.className.set('UserMoneyLog');
- component.schema.set({ className: 'UserMoneyLog', label: '资金积分流水', writable: false, creatable: false, supportsSoftDelete: false, fields: [] });
- component.filterUserMoneyType.set(4);
- component.filterUserMoneyStartDate.set('2026-08-01');
- component.filterUserMoneyEndDate.set('2026-08-18');
- functions.admin.and.resolveTo({ className: 'UserMoneyLog', page: 1, pageSize: 20, total: 1, results: [] });
- await component.load(1);
- expect(functions.admin).toHaveBeenCalledWith('userMoneyLogs', { className: 'UserMoneyLog', page: 1, pageSize: 20, search: '', type: 4, startDate: '2026-08-01', endDate: '2026-08-18' });
- });
- it('deletes selected user levels through the dedicated batch operation', async () => {
- spyOn(window, 'confirm').and.returnValue(true);
- component.className.set('UserLevel');
- component.selectedIds.set(['level-a', 'level-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'userLevelBatch'
- ? { action: 'delete', updated: 2, results: [] }
- : { className: 'UserLevel', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkUserLevelDelete();
- expect(functions.admin.calls.first().args).toEqual(['userLevelBatch', { className: 'UserLevel', action: 'delete', objectIds: ['level-a', 'level-b'] }]);
- expect(component.bulkMessage()).toContain('2 个积分等级');
- });
- it('loads encyclopedia entries through status and recommendation filters', async () => {
- component.className.set('Baike');
- component.schema.set({ className: 'Baike', label: '百科审核', writable: false, creatable: false, supportsSoftDelete: false, fields: [] });
- component.filterBaikeStatus.set(1);
- component.filterBaikeElite.set(0);
- functions.admin.and.resolveTo({ className: 'Baike', page: 1, pageSize: 20, total: 1, results: [] });
- await component.load(1);
- expect(functions.admin).toHaveBeenCalledWith('baikeEntries', { className: 'Baike', page: 1, pageSize: 20, search: '', status: 1, elite: 0 });
- });
- it('recommends selected encyclopedia entries through the dedicated workflow', async () => {
- component.className.set('Baike');
- component.selectedIds.set(['baike-a', 'baike-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'baikeBatch'
- ? { action: 'elite', updated: 2, results: [] }
- : { className: 'Baike', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkBaikeAction('elite');
- expect(functions.admin.calls.first().args).toEqual(['baikeBatch', { className: 'Baike', action: 'elite', objectIds: ['baike-a', 'baike-b'] }]);
- expect(component.bulkMessage()).toContain('2 个百科词条');
- });
- it('loads only CRM customer-type attributes through the dedicated operation', async () => {
- component.className.set('CRMSAttr');
- component.schema.set({ className: 'CRMSAttr', label: 'CRM 客户类型', writable: true, creatable: true, supportsSoftDelete: false, fields: [] });
- component.search.set('公司'); functions.admin.and.resolveTo({ className: 'CRMSAttr', page: 1, pageSize: 20, total: 1, results: [] });
- await component.load(1);
- expect(functions.admin).toHaveBeenCalledWith('crmClientTypes', { className: 'CRMSAttr', page: 1, pageSize: 20, search: '公司' });
- });
- it('saves CRM customer types through the protected lifecycle operation', async () => {
- component.className.set('CRMSAttr'); component.draft.set({ value: '渠道客户', remark: '渠道来源' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveCrmClientType' ? { objectId: 'crm-type', id: '4', value: '渠道客户' } : { className: 'CRMSAttr', page: 1, pageSize: 20, total: 4, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveCrmClientType', { className: 'CRMSAttr', objectId: undefined, fields: { value: '渠道客户', remark: '渠道来源' } }]);
- });
- it('deletes selected CRM customer types through the dedicated batch operation', async () => {
- spyOn(window, 'confirm').and.returnValue(true); component.className.set('CRMSAttr'); component.selectedIds.set(['crm-a', 'crm-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'crmClientTypeBatch' ? { action: 'delete', updated: 2 } : { className: 'CRMSAttr', page: 1, pageSize: 20, total: 0, results: [] });
- await component.applyBulkCrmClientTypeDelete();
- expect(functions.admin.calls.first().args).toEqual(['crmClientTypeBatch', { className: 'CRMSAttr', action: 'delete', objectIds: ['crm-a', 'crm-b'] }]);
- expect(component.bulkMessage()).toContain('2 个客户类型');
- });
- });
|