| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- import { TestBed } from '@angular/core/testing';
- import { Deliverable, DeliverableGroup } from './api/models';
- import { FmodeApi } from './api/fmode-api';
- import { DeliverablesService } from './deliverables.service';
- /**
- * 成果服务。
- *
- * 重点回归:
- * ① 空态与错误态**必须区分**(AC-08.4:失败不能伪装成「暂无成果」)
- * ② 搜索过滤后,分组计数要跟着变(否则显示「3 条」实际只有 1 条)
- * ③ 月份分组按时间倒序
- */
- describe('DeliverablesService', () => {
- const apiStub = {
- fetchDeliverables: jasmine.createSpy('fetchDeliverables'),
- fetchDeliverablesByProject: jasmine.createSpy('fetchDeliverablesByProject'),
- };
- let svc: DeliverablesService;
- function d(id: string, title: string, deliveredAt: string, extra: Partial<Deliverable> = {}): Deliverable {
- return {
- id,
- agentId: 'a1',
- title,
- artifacts: [],
- tags: [],
- deliveredAt,
- state: 'delivered',
- ...extra,
- };
- }
- beforeEach(() => {
- apiStub.fetchDeliverables.calls.reset();
- apiStub.fetchDeliverablesByProject.calls.reset();
- apiStub.fetchDeliverables.and.returnValue(Promise.resolve([]));
- apiStub.fetchDeliverablesByProject.and.returnValue(Promise.resolve([]));
- TestBed.resetTestingModule();
- TestBed.configureTestingModule({
- providers: [DeliverablesService, { provide: FmodeApi, useValue: apiStub }],
- });
- svc = TestBed.inject(DeliverablesService);
- });
- describe('加载与状态', () => {
- it('空列表 → state=empty(不是 ready)', async () => {
- await svc.load();
- expect(svc.state()).toBe('empty');
- expect(svc.empty()).toBeTrue();
- });
- it('有数据 → state=ready', async () => {
- apiStub.fetchDeliverables.and.returnValue(Promise.resolve([d('1', '报告', '2026-10-01')]));
- await svc.load();
- expect(svc.state()).toBe('ready');
- expect(svc.all().length).toBe(1);
- });
- it('🔴 接口失败 → state=error,且不伪装成「暂无成果」', async () => {
- apiStub.fetchDeliverables.and.returnValue(Promise.reject(new Error('网络异常')));
- await svc.load();
- expect(svc.state()).toBe('error');
- expect(svc.error()).toContain('网络异常');
- // 关键:错误态时 empty 必须为 false,否则 UI 会显示「暂无成果」
- expect(svc.empty()).toBeFalse();
- });
- it('ensureLoaded 加载过就不再重复请求', async () => {
- apiStub.fetchDeliverables.and.returnValue(Promise.resolve([d('1', 'x', '2026-10-01')]));
- await svc.ensureLoaded();
- await svc.ensureLoaded();
- expect(apiStub.fetchDeliverables.calls.count()).toBe(1);
- });
- it('refresh 强制重新拉取(跳过缓存)', async () => {
- apiStub.fetchDeliverables.and.returnValue(Promise.resolve([d('1', 'x', '2026-10-01')]));
- await svc.ensureLoaded();
- await svc.refresh();
- expect(apiStub.fetchDeliverables.calls.count()).toBe(2);
- });
- });
- describe('搜索过滤', () => {
- beforeEach(async () => {
- apiStub.fetchDeliverables.and.returnValue(
- Promise.resolve([
- d('1', '登录页设计', '2026-10-01', { projectName: '项目A' }),
- d('2', '接口文档', '2026-10-02', { projectName: '项目A' }),
- d('3', '登录页原型', '2026-09-15', { projectName: '项目B' }),
- ]),
- );
- await svc.load();
- });
- it('无关键字 → 全部返回', () => {
- expect(svc.filtered().length).toBe(3);
- });
- it('按标题过滤', () => {
- svc.keyword.set('登录');
- expect(svc.filtered().map((x) => x.id)).toEqual(['1', '3']);
- });
- it('按项目名过滤', () => {
- svc.keyword.set('项目B');
- expect(svc.filtered().map((x) => x.id)).toEqual(['3']);
- });
- it('大小写不敏感', () => {
- svc.keyword.set('ABC');
- expect(svc.filtered().length).toBe(0);
- });
- it('🔴 无结果时 filteredEmpty 为真(供 UI 显示「没有匹配」而不是「暂无成果」)', () => {
- svc.keyword.set('不存在的东西');
- expect(svc.filteredEmpty()).toBeTrue();
- // 但整体 empty 仍是 false(确实有成果,只是没匹配上)
- expect(svc.empty()).toBeFalse();
- });
- });
- describe('🔴 分组计数跟随搜索(曾在 UI 上显示错的条数)', () => {
- beforeEach(async () => {
- apiStub.fetchDeliverables.and.returnValue(Promise.resolve([]));
- apiStub.fetchDeliverablesByProject.and.returnValue(
- Promise.resolve([
- {
- project: 'p1',
- projectName: '项目A',
- count: 3,
- items: [d('1', '登录页', '2026-10-01'), d('2', '接口', '2026-10-02'), d('3', '报表', '2026-10-03')],
- } as DeliverableGroup,
- ]),
- );
- await svc.load();
- });
- it('无关键字 → 计数是原始值', () => {
- expect(svc.groups()[0].count).toBe(3);
- });
- it('过滤后 count 跟着变(不是恒为 3)', () => {
- svc.keyword.set('登录');
- expect(svc.groups()[0].items.length).toBe(1);
- expect(svc.groups()[0].count).toBe(1); // 关键断言
- });
- it('组内全被过滤掉 → 整组消失', () => {
- svc.keyword.set('完全不存在');
- expect(svc.groups().length).toBe(0);
- });
- });
- describe('月份分组', () => {
- it('按 YYYY-MM 分组,且倒序', async () => {
- apiStub.fetchDeliverables.and.returnValue(
- Promise.resolve([
- d('1', 'a', '2026-09-10'),
- d('2', 'b', '2026-10-05'),
- d('3', 'c', '2026-10-20'),
- ]),
- );
- await svc.load();
- const groups = svc.monthGroups();
- expect(groups.map((g) => g.key)).toEqual(['2026-10', '2026-09']);
- expect(groups[0].items.length).toBe(2);
- });
- });
- describe('按 agent / 项目 / id 查询', () => {
- beforeEach(async () => {
- apiStub.fetchDeliverables.and.returnValue(
- Promise.resolve([
- d('1', 'a', '2026-10-01', { agentId: 'lj', projectId: 'p1' }),
- d('2', 'b', '2026-10-02', { agentId: 'lj-test', projectId: 'p1' }),
- d('3', 'c', '2026-10-03', { agentId: 'lj', projectId: 'p2' }),
- ]),
- );
- await svc.load();
- });
- it('byAgent 只返回该 agent 的', () => {
- expect(svc.byAgent('lj').map((x) => x.id)).toEqual(['1', '3']);
- });
- it('byProject 只返回该项目的', () => {
- expect(svc.byProject('p1').map((x) => x.id)).toEqual(['1', '2']);
- });
- it('byId 命中 / 未命中', () => {
- expect(svc.byId('2')?.title).toBe('b');
- expect(svc.byId('nope')).toBeUndefined();
- });
- });
- });
|