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; 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: '' }] }); await component.loadAdPreview(); expect(functions.admin).toHaveBeenCalledWith('adPreview', { zoneObjectId: 'zone-object' }); expect(component.adPreview()[0]['html']).toContain('