deliverables.service.spec.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { TestBed } from '@angular/core/testing';
  2. import { Deliverable, DeliverableGroup } from './api/models';
  3. import { FmodeApi } from './api/fmode-api';
  4. import { DeliverablesService } from './deliverables.service';
  5. /**
  6. * 成果服务。
  7. *
  8. * 重点回归:
  9. * ① 空态与错误态**必须区分**(AC-08.4:失败不能伪装成「暂无成果」)
  10. * ② 搜索过滤后,分组计数要跟着变(否则显示「3 条」实际只有 1 条)
  11. * ③ 月份分组按时间倒序
  12. */
  13. describe('DeliverablesService', () => {
  14. const apiStub = {
  15. fetchDeliverables: jasmine.createSpy('fetchDeliverables'),
  16. fetchDeliverablesByProject: jasmine.createSpy('fetchDeliverablesByProject'),
  17. };
  18. let svc: DeliverablesService;
  19. function d(id: string, title: string, deliveredAt: string, extra: Partial<Deliverable> = {}): Deliverable {
  20. return {
  21. id,
  22. agentId: 'a1',
  23. title,
  24. artifacts: [],
  25. tags: [],
  26. deliveredAt,
  27. state: 'delivered',
  28. ...extra,
  29. };
  30. }
  31. beforeEach(() => {
  32. apiStub.fetchDeliverables.calls.reset();
  33. apiStub.fetchDeliverablesByProject.calls.reset();
  34. apiStub.fetchDeliverables.and.returnValue(Promise.resolve([]));
  35. apiStub.fetchDeliverablesByProject.and.returnValue(Promise.resolve([]));
  36. TestBed.resetTestingModule();
  37. TestBed.configureTestingModule({
  38. providers: [DeliverablesService, { provide: FmodeApi, useValue: apiStub }],
  39. });
  40. svc = TestBed.inject(DeliverablesService);
  41. });
  42. describe('加载与状态', () => {
  43. it('空列表 → state=empty(不是 ready)', async () => {
  44. await svc.load();
  45. expect(svc.state()).toBe('empty');
  46. expect(svc.empty()).toBeTrue();
  47. });
  48. it('有数据 → state=ready', async () => {
  49. apiStub.fetchDeliverables.and.returnValue(Promise.resolve([d('1', '报告', '2026-10-01')]));
  50. await svc.load();
  51. expect(svc.state()).toBe('ready');
  52. expect(svc.all().length).toBe(1);
  53. });
  54. it('🔴 接口失败 → state=error,且不伪装成「暂无成果」', async () => {
  55. apiStub.fetchDeliverables.and.returnValue(Promise.reject(new Error('网络异常')));
  56. await svc.load();
  57. expect(svc.state()).toBe('error');
  58. expect(svc.error()).toContain('网络异常');
  59. // 关键:错误态时 empty 必须为 false,否则 UI 会显示「暂无成果」
  60. expect(svc.empty()).toBeFalse();
  61. });
  62. it('ensureLoaded 加载过就不再重复请求', async () => {
  63. apiStub.fetchDeliverables.and.returnValue(Promise.resolve([d('1', 'x', '2026-10-01')]));
  64. await svc.ensureLoaded();
  65. await svc.ensureLoaded();
  66. expect(apiStub.fetchDeliverables.calls.count()).toBe(1);
  67. });
  68. it('refresh 强制重新拉取(跳过缓存)', async () => {
  69. apiStub.fetchDeliverables.and.returnValue(Promise.resolve([d('1', 'x', '2026-10-01')]));
  70. await svc.ensureLoaded();
  71. await svc.refresh();
  72. expect(apiStub.fetchDeliverables.calls.count()).toBe(2);
  73. });
  74. });
  75. describe('搜索过滤', () => {
  76. beforeEach(async () => {
  77. apiStub.fetchDeliverables.and.returnValue(
  78. Promise.resolve([
  79. d('1', '登录页设计', '2026-10-01', { projectName: '项目A' }),
  80. d('2', '接口文档', '2026-10-02', { projectName: '项目A' }),
  81. d('3', '登录页原型', '2026-09-15', { projectName: '项目B' }),
  82. ]),
  83. );
  84. await svc.load();
  85. });
  86. it('无关键字 → 全部返回', () => {
  87. expect(svc.filtered().length).toBe(3);
  88. });
  89. it('按标题过滤', () => {
  90. svc.keyword.set('登录');
  91. expect(svc.filtered().map((x) => x.id)).toEqual(['1', '3']);
  92. });
  93. it('按项目名过滤', () => {
  94. svc.keyword.set('项目B');
  95. expect(svc.filtered().map((x) => x.id)).toEqual(['3']);
  96. });
  97. it('大小写不敏感', () => {
  98. svc.keyword.set('ABC');
  99. expect(svc.filtered().length).toBe(0);
  100. });
  101. it('🔴 无结果时 filteredEmpty 为真(供 UI 显示「没有匹配」而不是「暂无成果」)', () => {
  102. svc.keyword.set('不存在的东西');
  103. expect(svc.filteredEmpty()).toBeTrue();
  104. // 但整体 empty 仍是 false(确实有成果,只是没匹配上)
  105. expect(svc.empty()).toBeFalse();
  106. });
  107. });
  108. describe('🔴 分组计数跟随搜索(曾在 UI 上显示错的条数)', () => {
  109. beforeEach(async () => {
  110. apiStub.fetchDeliverables.and.returnValue(Promise.resolve([]));
  111. apiStub.fetchDeliverablesByProject.and.returnValue(
  112. Promise.resolve([
  113. {
  114. project: 'p1',
  115. projectName: '项目A',
  116. count: 3,
  117. items: [d('1', '登录页', '2026-10-01'), d('2', '接口', '2026-10-02'), d('3', '报表', '2026-10-03')],
  118. } as DeliverableGroup,
  119. ]),
  120. );
  121. await svc.load();
  122. });
  123. it('无关键字 → 计数是原始值', () => {
  124. expect(svc.groups()[0].count).toBe(3);
  125. });
  126. it('过滤后 count 跟着变(不是恒为 3)', () => {
  127. svc.keyword.set('登录');
  128. expect(svc.groups()[0].items.length).toBe(1);
  129. expect(svc.groups()[0].count).toBe(1); // 关键断言
  130. });
  131. it('组内全被过滤掉 → 整组消失', () => {
  132. svc.keyword.set('完全不存在');
  133. expect(svc.groups().length).toBe(0);
  134. });
  135. });
  136. describe('月份分组', () => {
  137. it('按 YYYY-MM 分组,且倒序', async () => {
  138. apiStub.fetchDeliverables.and.returnValue(
  139. Promise.resolve([
  140. d('1', 'a', '2026-09-10'),
  141. d('2', 'b', '2026-10-05'),
  142. d('3', 'c', '2026-10-20'),
  143. ]),
  144. );
  145. await svc.load();
  146. const groups = svc.monthGroups();
  147. expect(groups.map((g) => g.key)).toEqual(['2026-10', '2026-09']);
  148. expect(groups[0].items.length).toBe(2);
  149. });
  150. });
  151. describe('按 agent / 项目 / id 查询', () => {
  152. beforeEach(async () => {
  153. apiStub.fetchDeliverables.and.returnValue(
  154. Promise.resolve([
  155. d('1', 'a', '2026-10-01', { agentId: 'lj', projectId: 'p1' }),
  156. d('2', 'b', '2026-10-02', { agentId: 'lj-test', projectId: 'p1' }),
  157. d('3', 'c', '2026-10-03', { agentId: 'lj', projectId: 'p2' }),
  158. ]),
  159. );
  160. await svc.load();
  161. });
  162. it('byAgent 只返回该 agent 的', () => {
  163. expect(svc.byAgent('lj').map((x) => x.id)).toEqual(['1', '3']);
  164. });
  165. it('byProject 只返回该项目的', () => {
  166. expect(svc.byProject('p1').map((x) => x.id)).toEqual(['1', '2']);
  167. });
  168. it('byId 命中 / 未命中', () => {
  169. expect(svc.byId('2')?.title).toBe('b');
  170. expect(svc.byId('nope')).toBeUndefined();
  171. });
  172. });
  173. });