admin-resource.component.spec.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import { ComponentFixture, TestBed } from '@angular/core/testing';
  2. import { ActivatedRoute } from '@angular/router';
  3. import { of } from 'rxjs';
  4. import { AdminFieldSchema } from '../admin.models';
  5. import { CloudFunctionsService } from '../cloud-functions.service';
  6. import { AdminResourceComponent } from './admin-resource.component';
  7. describe('AdminResourceComponent', () => {
  8. let fixture: ComponentFixture<AdminResourceComponent>;
  9. let component: AdminResourceComponent;
  10. const functions = { admin: jasmine.createSpy('admin') };
  11. const objectField: AdminFieldSchema = { name: 'payload', type: 'Object', writable: true };
  12. beforeEach(async () => {
  13. functions.admin.calls.reset();
  14. await TestBed.configureTestingModule({
  15. imports: [AdminResourceComponent],
  16. providers: [
  17. { provide: ActivatedRoute, useValue: { paramMap: of({ get: () => 'CommonModel' }) } },
  18. { provide: CloudFunctionsService, useValue: functions },
  19. ],
  20. }).compileComponents();
  21. fixture = TestBed.createComponent(AdminResourceComponent);
  22. component = fixture.componentInstance;
  23. });
  24. it('keeps invalid JSON text visible and blocks saving it as a string', async () => {
  25. component.setField(objectField, '{invalid');
  26. expect(component.inputValue(objectField)).toBe('{invalid');
  27. expect(component.fieldErrors()['payload']).toContain('JSON 对象');
  28. await component.save();
  29. expect(functions.admin).not.toHaveBeenCalled();
  30. expect(component.error()).toContain('payload');
  31. });
  32. it('accepts a valid JSON object as structured data', () => {
  33. component.setField(objectField, '{"enabled":true}');
  34. expect(component.fieldErrors()['payload']).toBeUndefined();
  35. expect(component.draft()['payload']).toEqual({ enabled: true });
  36. });
  37. it('keeps preferred columns first and rounds page counts upward', () => {
  38. component.schema.set({
  39. className: 'CommonModel', label: '内容', writable: true, supportsSoftDelete: true,
  40. fields: [
  41. { name: 'misc', type: 'String', writable: true },
  42. { name: 'title', type: 'String', writable: true },
  43. { name: 'objectId', type: 'String', writable: false },
  44. ],
  45. });
  46. component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 21, results: [] });
  47. expect(component.columns().map((field) => field.name)).toEqual(['objectId', 'title', 'misc']);
  48. expect(component.pageCount()).toBe(2);
  49. });
  50. it('uses the dedicated cloud operation to reset an existing user password', async () => {
  51. functions.admin.and.resolveTo({ objectId: 'user-object' });
  52. component.className.set('_User');
  53. component.draftId.set('user-object');
  54. component.newPassword.set('SafePass123');
  55. component.confirmPassword.set('SafePass123');
  56. await component.resetPassword();
  57. expect(functions.admin).toHaveBeenCalledWith('resetPassword', { className: '_User', objectId: 'user-object', newPassword: 'SafePass123' });
  58. expect(component.passwordMessage()).toContain('已安全重置');
  59. });
  60. it('uses the dedicated ordinary-account operation without generic role fields', async () => {
  61. component.className.set('_User');
  62. component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
  63. component.createUsername.set('student-1001');
  64. component.createPassword.set('SafePass123');
  65. component.createConfirmPassword.set('SafePass123');
  66. component.createDisplayName.set('新学员');
  67. component.createMobile.set('13800138000');
  68. functions.admin.and.callFake(async (operation: string) => operation === 'createUser'
  69. ? { objectId: 'new-user', userId: 1001, groupId: 1 }
  70. : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
  71. await component.createUser();
  72. expect(functions.admin.calls.first().args).toEqual(['createUser', {
  73. username: 'student-1001', password: 'SafePass123', displayName: '新学员', mobile: '13800138000',
  74. }]);
  75. expect(component.editorOpen()).toBeFalse();
  76. });
  77. it('rejects mismatched account passwords before calling the cloud function', async () => {
  78. component.className.set('_User');
  79. component.createUsername.set('student-1002');
  80. component.createPassword.set('SafePass123');
  81. component.createConfirmPassword.set('Different123');
  82. await component.createUser();
  83. expect(functions.admin).not.toHaveBeenCalled();
  84. expect(component.createUserError()).toContain('不一致');
  85. });
  86. it('uses the protected batch operation to lock a user and reports revoked sessions', async () => {
  87. spyOn(window, 'confirm').and.returnValue(true);
  88. component.className.set('_User');
  89. component.draftId.set('member-object');
  90. component.draft.set({ objectId: 'member-object', isDisabled: false });
  91. component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
  92. functions.admin.and.callFake(async (operation: string) => operation === 'userBatch'
  93. ? { action: 'lock', updated: 1, revokedSessions: 2, results: [{ objectId: 'member-object', isDisabled: true }] }
  94. : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
  95. await component.setUserDisabled(true);
  96. expect(functions.admin.calls.first().args).toEqual(['userBatch', { className: '_User', action: 'lock', objectIds: ['member-object'], reason: '' }]);
  97. expect(component.draft()['isDisabled']).toBeTrue();
  98. expect(component.userActionMessage()).toContain('2 个会话');
  99. });
  100. it('moves a user through the dedicated group synchronization operation', async () => {
  101. component.className.set('_User');
  102. component.draftId.set('member-object');
  103. component.draft.set({ objectId: 'member-object', legacyGroupId: 1 });
  104. component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
  105. component.selectedUserGroupId.set(3);
  106. functions.admin.and.callFake(async (operation: string) => operation === 'userBatch'
  107. ? { action: 'move', updated: 1, revokedSessions: 0, results: [{ objectId: 'member-object', legacyGroupId: 3, legacyUserData: { GroupID: 3 } }] }
  108. : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
  109. await component.moveUserGroup();
  110. expect(functions.admin.calls.first().args).toEqual(['userBatch', { className: '_User', action: 'move', objectIds: ['member-object'], groupId: 3 }]);
  111. expect(component.draft()['legacyGroupId']).toBe(3);
  112. });
  113. it('routes group creation through the atomic group operation', async () => {
  114. component.className.set('Group');
  115. component.schema.set({ className: 'Group', label: '用户组', writable: true, supportsSoftDelete: false, fields: [] });
  116. component.draft.set({ groupName: '普通会员', parentGroupId: 0, regSelect: true });
  117. functions.admin.and.callFake(async (operation: string) => operation === 'saveGroup'
  118. ? { objectId: 'group-object', groupId: 1, groupName: '普通会员' }
  119. : { className: 'Group', page: 1, pageSize: 20, total: 1, results: [] });
  120. await component.save();
  121. expect(functions.admin.calls.first().args).toEqual(['saveGroup', {
  122. className: 'Group', objectId: undefined, fields: { groupName: '普通会员', parentGroupId: 0, regSelect: true },
  123. }]);
  124. });
  125. it('routes node creation through the structural cloud operation', async () => {
  126. component.className.set('Node');
  127. component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
  128. component.draft.set({ nodeName: '新栏目', nodeDir: 'new-node', parentId: 999, nodeId: 999 });
  129. component.targetNodeParentId.set(16);
  130. functions.admin.and.callFake(async (operation: string) => operation === 'saveNode'
  131. ? { objectId: 'node-object', nodeId: 401, nodeName: '新栏目', parentId: 16 }
  132. : { className: 'Node', page: 1, pageSize: 20, total: 1, results: [] });
  133. await component.save();
  134. expect(functions.admin.calls.first().args).toEqual(['saveNode', {
  135. className: 'Node', objectId: undefined, fields: { nodeName: '新栏目', nodeDir: 'new-node', parentId: 999, nodeId: 999 }, parentId: 16,
  136. }]);
  137. });
  138. it('moves a node through the cycle-protected batch operation', async () => {
  139. component.className.set('Node');
  140. component.draftId.set('node-object');
  141. component.draft.set({ objectId: 'node-object', nodeId: 40, parentId: 16, zstatus: 99 });
  142. component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
  143. component.targetNodeParentId.set(29);
  144. functions.admin.and.callFake(async (operation: string) => operation === 'nodeBatch'
  145. ? { action: 'move', updated: 1, results: [{ objectId: 'node-object', nodeId: 40, parentId: 29, zstatus: 99 }] }
  146. : { className: 'Node', page: 1, pageSize: 20, total: 1, results: [] });
  147. await component.applyNodeAction('move');
  148. expect(functions.admin.calls.first().args).toEqual(['nodeBatch', { className: 'Node', action: 'move', objectIds: ['node-object'], parentId: 29 }]);
  149. expect(component.draft()['parentId']).toBe(29);
  150. expect(component.nodeActionMessage()).toContain('29');
  151. });
  152. it('routes special creation through the structural cloud operation', async () => {
  153. component.className.set('Special');
  154. component.schema.set({ className: 'Special', label: '专题', writable: true, supportsSoftDelete: false, fields: [] });
  155. component.draft.set({ specName: '新专题', specDir: 'new-special', specId: 999, pid: 999 });
  156. component.targetSpecialParentId.set(1);
  157. functions.admin.and.callFake(async (operation: string) => operation === 'saveSpecial'
  158. ? { objectId: 'special-object', specId: 10, specName: '新专题', pid: 1 }
  159. : { className: 'Special', page: 1, pageSize: 20, total: 1, results: [] });
  160. await component.save();
  161. expect(functions.admin.calls.first().args).toEqual(['saveSpecial', {
  162. className: 'Special', objectId: undefined, fields: { specName: '新专题', specDir: 'new-special', specId: 999, pid: 999 }, pid: 1,
  163. }]);
  164. });
  165. it('moves a special through the dedicated batch operation', async () => {
  166. component.className.set('Special');
  167. component.draftId.set('special-object');
  168. component.draft.set({ objectId: 'special-object', specId: 10, pid: 1 });
  169. component.schema.set({ className: 'Special', label: '专题', writable: true, supportsSoftDelete: false, fields: [] });
  170. component.targetSpecialParentId.set(0);
  171. functions.admin.and.callFake(async (operation: string) => operation === 'specialBatch'
  172. ? { action: 'move', updated: 1, results: [{ objectId: 'special-object', specId: 10, pid: 0 }] }
  173. : { className: 'Special', page: 1, pageSize: 20, total: 1, results: [] });
  174. await component.moveSpecial();
  175. expect(functions.admin.calls.first().args).toEqual(['specialBatch', { className: 'Special', action: 'move', objectIds: ['special-object'], pid: 0 }]);
  176. expect(component.specialActionMessage()).toContain('0');
  177. });
  178. it('updates existing model metadata through the protected operation', async () => {
  179. component.className.set('Model');
  180. component.draftId.set('model-object');
  181. component.schema.set({ className: 'Model', label: '内容模型', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
  182. component.draft.set({ objectId: 'model-object', modelId: 2, tableName: 'ZL_C_Article', modelName: '文章模型', description: '安全说明' });
  183. functions.admin.and.callFake(async (operation: string) => operation === 'saveModelMetadata'
  184. ? { objectId: 'model-object', modelId: 2, modelName: '文章模型', description: '安全说明' }
  185. : { className: 'Model', page: 1, pageSize: 20, total: 1, results: [] });
  186. await component.save();
  187. expect(functions.admin.calls.first().args).toEqual(['saveModelMetadata', {
  188. className: 'Model', objectId: 'model-object', fields: { objectId: 'model-object', modelId: 2, tableName: 'ZL_C_Article', modelName: '文章模型', description: '安全说明' },
  189. }]);
  190. });
  191. it('updates existing model-field presentation metadata without structural writes', async () => {
  192. component.className.set('ModelField');
  193. component.draftId.set('field-object');
  194. component.schema.set({ className: 'ModelField', label: '模型字段', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
  195. component.draft.set({ objectId: 'field-object', fieldId: 3, modelId: 2, fieldName: 'author', fieldType: 'TextType', fieldAlias: '作者' });
  196. functions.admin.and.callFake(async (operation: string) => operation === 'saveModelFieldMetadata'
  197. ? { objectId: 'field-object', fieldId: 3, fieldAlias: '作者' }
  198. : { className: 'ModelField', page: 1, pageSize: 20, total: 1, results: [] });
  199. await component.save();
  200. expect(functions.admin.calls.first().args).toEqual(['saveModelFieldMetadata', {
  201. className: 'ModelField', objectId: 'field-object', fields: { objectId: 'field-object', fieldId: 3, modelId: 2, fieldName: 'author', fieldType: 'TextType', fieldAlias: '作者' },
  202. }]);
  203. });
  204. it('updates an existing guestbook entry through the dedicated editor', async () => {
  205. component.className.set('Guestbook');
  206. component.draftId.set('guest-object');
  207. component.schema.set({ className: 'Guestbook', label: '留言', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
  208. component.draft.set({ objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' });
  209. functions.admin.and.callFake(async (operation: string) => operation === 'saveGuestbook'
  210. ? { objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' }
  211. : { className: 'Guestbook', page: 1, pageSize: 20, total: 1, results: [] });
  212. await component.save();
  213. expect(functions.admin.calls.first().args).toEqual(['saveGuestbook', {
  214. className: 'Guestbook', objectId: 'guest-object', fields: { objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' },
  215. }]);
  216. });
  217. it('audits a guestbook entry through the workflow operation', async () => {
  218. component.className.set('Guestbook');
  219. component.draftId.set('guest-object');
  220. component.draft.set({ objectId: 'guest-object', gid: 10, status: 0 });
  221. component.schema.set({ className: 'Guestbook', label: '留言', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
  222. functions.admin.and.callFake(async (operation: string) => operation === 'guestbookBatch'
  223. ? { action: 'audit', updated: 1, results: [{ objectId: 'guest-object', gid: 10, status: 99 }] }
  224. : { className: 'Guestbook', page: 1, pageSize: 20, total: 1, results: [] });
  225. await component.applyGuestbookAction('audit');
  226. expect(functions.admin.calls.first().args).toEqual(['guestbookBatch', { className: 'Guestbook', action: 'audit', objectIds: ['guest-object'] }]);
  227. expect(component.selectedGuestbookStatus()).toBe(99);
  228. });
  229. it('publishes an admin reply through the parent-safe cloud operation', async () => {
  230. component.className.set('Guestbook');
  231. component.draftId.set('guest-object');
  232. component.guestbookReplyTitle.set('回复标题');
  233. component.guestbookReplyContent.set('回复正文');
  234. functions.admin.and.resolveTo({ objectId: 'reply-object', gid: 11, parentid: 10, status: 99 });
  235. await component.replyGuestbook();
  236. expect(functions.admin).toHaveBeenCalledWith('guestbookReply', { className: 'Guestbook', parentObjectId: 'guest-object', title: '回复标题', content: '回复正文' });
  237. expect(component.guestbookActionMessage()).toContain('已发布');
  238. });
  239. it('creates a guest category through the atomic category operation', async () => {
  240. component.className.set('Guestcate');
  241. component.schema.set({ className: 'Guestcate', label: '留言分类', writable: true, supportsSoftDelete: false, fields: [] });
  242. component.draft.set({ catename: '新贴吧版块', gtype: 0, parentId: 999, cateid: 999 });
  243. component.guestCategoryType.set(1);
  244. component.targetGuestCategoryParentId.set(7);
  245. functions.admin.and.callFake(async (operation: string) => operation === 'saveGuestCategory'
  246. ? { objectId: 'category-object', cateid: 20, catename: '新贴吧版块', gtype: 1, parentId: 7 }
  247. : { className: 'Guestcate', page: 1, pageSize: 20, total: 1, results: [] });
  248. await component.save();
  249. expect(functions.admin.calls.first().args).toEqual(['saveGuestCategory', {
  250. className: 'Guestcate', objectId: undefined, fields: { catename: '新贴吧版块', gtype: 0, parentId: 999, cateid: 999 }, gtype: 1, parentId: 7,
  251. }]);
  252. });
  253. it('creates a question category through the atomic structural operation', async () => {
  254. component.className.set('ExamClass');
  255. component.schema.set({ className: 'ExamClass', label: '试题分类', writable: true, supportsSoftDelete: false, fields: [] });
  256. component.draft.set({ cClassName: '英语', cId: 999, cClassid: 999 });
  257. component.targetExamClassParentId.set(7);
  258. component.examClassType.set(2);
  259. component.examClassOrder.set(8);
  260. functions.admin.and.callFake(async (operation: string) => operation === 'saveExamClass'
  261. ? { objectId: 'exam-class-object', cId: 20, cClassName: '英语', cClassid: 7, cClassType: 2, cOrderBy: 8 }
  262. : { className: 'ExamClass', page: 1, pageSize: 20, total: 1, results: [] });
  263. await component.save();
  264. expect(functions.admin.calls.first().args).toEqual(['saveExamClass', {
  265. className: 'ExamClass', objectId: undefined, fields: { cClassName: '英语', cId: 999, cClassid: 999 }, parentId: 7, classType: 2, orderId: 8,
  266. }]);
  267. });
  268. it('creates an exam point through the atomic structural operation', async () => {
  269. component.className.set('ExamPoint');
  270. component.schema.set({ className: 'ExamPoint', label: '考点管理', writable: true, supportsSoftDelete: false, fields: [] });
  271. component.draft.set({ testPoint: '第一考点', id: 999, tid: 999 });
  272. component.targetExamPointParentId.set(3);
  273. component.examPointOrder.set(5);
  274. functions.admin.and.callFake(async (operation: string) => operation === 'saveExamPoint'
  275. ? { objectId: 'exam-point-object', id: 20, testPoint: '第一考点', tid: 3, orderBy: 5 }
  276. : { className: 'ExamPoint', page: 1, pageSize: 20, total: 1, results: [] });
  277. await component.save();
  278. expect(functions.admin.calls.first().args).toEqual(['saveExamPoint', {
  279. className: 'ExamPoint', objectId: undefined, fields: { testPoint: '第一考点', id: 999, tid: 999 }, parentId: 3, orderId: 5,
  280. }]);
  281. });
  282. it('creates a knowledge node through the subject-safe structural operation', async () => {
  283. component.className.set('QuestionsKnowledge');
  284. component.schema.set({ className: 'QuestionsKnowledge', label: '知识点', writable: true, supportsSoftDelete: false, fields: [] });
  285. component.draft.set({ kName: '现在完成时', kId: 999, kClassId: 999, status: 1, grade: 3, isSys: 0 });
  286. component.targetKnowledgeClassId.set(7);
  287. component.targetKnowledgeParentId.set(2);
  288. component.knowledgeOrder.set(5);
  289. functions.admin.and.callFake(async (operation: string) => operation === 'saveKnowledge'
  290. ? { objectId: 'knowledge-object', kId: 20, kName: '现在完成时', kClassId: 7, pid: 2, kOrderBy: 5 }
  291. : { className: 'QuestionsKnowledge', page: 1, pageSize: 20, total: 1, results: [] });
  292. await component.save();
  293. expect(functions.admin.calls.first().args).toEqual(['saveKnowledge', {
  294. className: 'QuestionsKnowledge', objectId: undefined, fields: { kName: '现在完成时', kId: 999, kClassId: 999, status: 1, grade: 3, isSys: 0 }, classId: 7, parentId: 2, orderId: 5,
  295. }]);
  296. });
  297. it('creates an exam teacher through the subject-safe operation', async () => {
  298. component.className.set('ExTeacher');
  299. component.schema.set({ className: 'ExTeacher', label: '考试教师', writable: true, supportsSoftDelete: false, fields: [] });
  300. component.draft.set({ tname: '张老师', tclsss: 999, post: '高级教师', teach: '英语', remark: '简介' });
  301. component.targetTeacherClassId.set(7);
  302. functions.admin.and.callFake(async (operation: string) => operation === 'saveExamTeacher'
  303. ? { objectId: 'teacher-object', id: 20, tname: '张老师', tclsss: 7 }
  304. : { className: 'ExTeacher', page: 1, pageSize: 20, total: 1, results: [] });
  305. await component.save();
  306. expect(functions.admin.calls.first().args).toEqual(['saveExamTeacher', {
  307. className: 'ExTeacher', objectId: undefined, fields: { tname: '张老师', tclsss: 999, post: '高级教师', teach: '英语', remark: '简介' }, classId: 7,
  308. }]);
  309. });
  310. it('creates a dictionary category through the collision-safe operation', async () => {
  311. component.className.set('Datadiccategory');
  312. component.schema.set({ className: 'Datadiccategory', label: '数据字典分类', writable: true, supportsSoftDelete: false, fields: [] });
  313. component.draft.set({ categoryname: '学习状态', diccateid: 999, isused: true });
  314. functions.admin.and.callFake(async (operation: string) => operation === 'saveDictionaryCategory'
  315. ? { objectId: 'dictionary-category-object', diccateid: 20, categoryname: '学习状态', isused: true }
  316. : { className: 'Datadiccategory', page: 1, pageSize: 20, total: 1, results: [] });
  317. await component.save();
  318. expect(functions.admin.calls.first().args).toEqual(['saveDictionaryCategory', {
  319. className: 'Datadiccategory', objectId: undefined, fields: { categoryname: '学习状态', diccateid: 999, isused: true },
  320. }]);
  321. });
  322. it('creates a dictionary item through the category-safe operation', async () => {
  323. component.className.set('Datadic');
  324. component.schema.set({ className: 'Datadic', label: '数据字典项', writable: true, supportsSoftDelete: false, fields: [] });
  325. component.draft.set({ dicname: '已完成', dicid: 999, diccate: 999, isused: true });
  326. component.targetDictionaryCategoryId.set(20);
  327. functions.admin.and.callFake(async (operation: string) => operation === 'saveDictionaryItem'
  328. ? { objectId: 'dictionary-item-object', dicid: 30, diccate: 20, dicname: '已完成', isused: true }
  329. : { className: 'Datadic', page: 1, pageSize: 20, total: 1, results: [] });
  330. await component.save();
  331. expect(functions.admin.calls.first().args).toEqual(['saveDictionaryItem', {
  332. className: 'Datadic', objectId: undefined, fields: { dicname: '已完成', dicid: 999, diccate: 999, isused: true }, categoryId: 20,
  333. }]);
  334. });
  335. it('batch disables selected dictionary records through the dedicated operation', async () => {
  336. component.className.set('Datadic');
  337. component.selectedIds.set(['dictionary-a', 'dictionary-b']);
  338. functions.admin.and.callFake(async (operation: string) => operation === 'dictionaryBatch'
  339. ? { action: 'disable', updated: 2, results: [] }
  340. : { className: 'Datadic', page: 1, pageSize: 20, total: 2, results: [] });
  341. await component.applyBulkDictionaryAction('disable');
  342. expect(functions.admin.calls.first().args).toEqual(['dictionaryBatch', { className: 'Datadic', action: 'disable', objectIds: ['dictionary-a', 'dictionary-b'] }]);
  343. expect(component.bulkMessage()).toContain('2 条字典记录');
  344. });
  345. it('creates a multi-level dictionary category through the dedicated operation', async () => {
  346. component.className.set('GradeCate');
  347. component.schema.set({ className: 'GradeCate', label: '多级字典分类', writable: true, supportsSoftDelete: false, fields: [] });
  348. component.draft.set({ cateName: '地区联动', gradeField: '省|市', cateId: 999, remark: '两级地区' });
  349. functions.admin.and.callFake(async (operation: string) => operation === 'saveGradeCategory'
  350. ? { objectId: 'grade-category-object', cateId: 20, cateName: '地区联动', gradeField: '省|市' }
  351. : { className: 'GradeCate', page: 1, pageSize: 20, total: 1, results: [] });
  352. await component.save();
  353. expect(functions.admin.calls.first().args).toEqual(['saveGradeCategory', {
  354. className: 'GradeCate', objectId: undefined, fields: { cateName: '地区联动', gradeField: '省|市', cateId: 999, remark: '两级地区' },
  355. }]);
  356. });
  357. it('creates a multi-level dictionary option through the parent-safe operation', async () => {
  358. component.className.set('Grade');
  359. component.schema.set({ className: 'Grade', label: '多级字典选项', writable: true, supportsSoftDelete: false, fields: [] });
  360. component.draft.set({ gradeName: '杭州市', gradeId: 999, cate: 999, parentId: 999, grade: 999 });
  361. component.targetGradeCategoryId.set(20); component.targetGradeParentId.set(30);
  362. functions.admin.and.callFake(async (operation: string) => operation === 'saveGradeOption'
  363. ? { objectId: 'grade-option-object', gradeId: 40, cate: 20, parentId: 30, grade: 2, gradeName: '杭州市' }
  364. : { className: 'Grade', page: 1, pageSize: 20, total: 1, results: [] });
  365. await component.save();
  366. expect(functions.admin.calls.first().args).toEqual(['saveGradeOption', {
  367. className: 'Grade', objectId: undefined, fields: { gradeName: '杭州市', gradeId: 999, cate: 999, parentId: 999, grade: 999 }, categoryId: 20, parentId: 30,
  368. }]);
  369. });
  370. it('batch deletes selected multi-level dictionary options with the dedicated operation', async () => {
  371. component.className.set('Grade'); component.selectedIds.set(['grade-a', 'grade-b']); spyOn(window, 'confirm').and.returnValue(true);
  372. functions.admin.and.callFake(async (operation: string) => operation === 'gradeBatch'
  373. ? { action: 'delete', updated: 2, results: [] }
  374. : { className: 'Grade', page: 1, pageSize: 20, total: 0, results: [] });
  375. await component.applyBulkGradeDelete();
  376. expect(functions.admin.calls.first().args).toEqual(['gradeBatch', { className: 'Grade', action: 'delete', objectIds: ['grade-a', 'grade-b'] }]);
  377. expect(component.bulkMessage()).toContain('2 条多级字典记录');
  378. });
  379. it('updates a forum post through the dedicated workflow operation', async () => {
  380. component.className.set('GuestBar');
  381. component.draftId.set('bar-object');
  382. component.draft.set({ objectId: 'bar-object', id: 10, cateId: 7, status: 0, orderFlag: 0 });
  383. functions.admin.and.callFake(async (operation: string) => operation === 'barBatch'
  384. ? { action: 'top-global', updated: 1, results: [{ objectId: 'bar-object', id: 10, cateId: 7, status: 0, orderFlag: 2 }] }
  385. : { className: 'GuestBar', page: 1, pageSize: 20, total: 1, results: [] });
  386. await component.applyBarAction('top-global');
  387. expect(functions.admin.calls.first().args).toEqual(['barBatch', { className: 'GuestBar', action: 'top-global', objectIds: ['bar-object'] }]);
  388. expect(component.draft()['orderFlag']).toBe(2);
  389. });
  390. it('moves selected forum posts through the tenant-safe batch operation', async () => {
  391. component.className.set('GuestBar');
  392. component.selectedIds.set(['bar-a', 'bar-b']);
  393. component.targetBarCategoryId.set(8);
  394. functions.admin.and.callFake(async (operation: string) => operation === 'barBatch'
  395. ? { action: 'move', updated: 2, results: [] }
  396. : { className: 'GuestBar', page: 1, pageSize: 20, total: 2, results: [] });
  397. await component.applyBulkBarAction('move');
  398. expect(functions.admin.calls.first().args).toEqual(['barBatch', { className: 'GuestBar', action: 'move', objectIds: ['bar-a', 'bar-b'], cateId: 8 }]);
  399. expect(component.bulkMessage()).toContain('2 条帖子');
  400. });
  401. it('updates content workflow state through the dedicated batch operation', async () => {
  402. component.className.set('CommonModel');
  403. component.draftId.set('content-object');
  404. component.draft.set({ objectId: 'content-object', status: 0, nodeId: 29 });
  405. component.schema.set({ className: 'CommonModel', label: '内容', writable: true, creatable: false, supportsSoftDelete: true, fields: [] });
  406. component.selectedContentStatus.set(99);
  407. functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
  408. ? { action: 'status', updated: 1, results: [{ objectId: 'content-object', status: 99, nodeId: 29 }] }
  409. : { className: 'CommonModel', page: 1, pageSize: 20, total: 1, results: [] });
  410. await component.applyContentAction('status');
  411. expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'status', objectIds: ['content-object'], status: 99 }]);
  412. expect(component.contentStatusValue()).toBe(99);
  413. expect(component.contentActionMessage()).toContain('已审核');
  414. });
  415. it('selects and clears all records on the current page', () => {
  416. component.className.set('CommonModel');
  417. component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 2, results: [{ objectId: 'content-a' }, { objectId: 'content-b' }] });
  418. component.togglePageSelection(true);
  419. expect(component.selectedIds()).toEqual(['content-a', 'content-b']);
  420. expect(component.allPageSelected()).toBeTrue();
  421. component.toggleSelection('content-a', false);
  422. expect(component.selectedIds()).toEqual(['content-b']);
  423. expect(component.allPageSelected()).toBeFalse();
  424. });
  425. it('sends all selected content ids through the bulk workflow operation', async () => {
  426. component.className.set('CommonModel');
  427. component.schema.set({ className: 'CommonModel', label: '内容', writable: true, creatable: false, supportsSoftDelete: true, fields: [] });
  428. component.selectedIds.set(['content-a', 'content-b']);
  429. component.selectedContentStatus.set(99);
  430. functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
  431. ? { action: 'status', updated: 2, results: [] }
  432. : { className: 'CommonModel', page: 1, pageSize: 20, total: 2, results: [] });
  433. await component.applyBulkContentAction('status');
  434. expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'status', objectIds: ['content-a', 'content-b'], status: 99 }]);
  435. expect(component.bulkMessage()).toContain('2 条内容');
  436. expect(component.selectedIds()).toEqual([]);
  437. });
  438. it('recovers all selected nodes through the dedicated batch operation', async () => {
  439. component.className.set('Node');
  440. component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
  441. component.selectedIds.set(['node-a', 'node-b']);
  442. functions.admin.and.callFake(async (operation: string) => operation === 'nodeBatch'
  443. ? { action: 'recover', updated: 2, results: [] }
  444. : { className: 'Node', page: 1, pageSize: 20, total: 2, results: [] });
  445. await component.applyBulkNodeAction('recover');
  446. expect(functions.admin.calls.first().args).toEqual(['nodeBatch', { className: 'Node', action: 'recover', objectIds: ['node-a', 'node-b'] }]);
  447. expect(component.bulkMessage()).toContain('2 个栏目');
  448. });
  449. });