import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { AdminSessionService } from '../admin-session.service'; import { OperationsService } from '../operations.service'; import { AdminDashboardComponent } from './admin-dashboard.component'; describe('AdminDashboardComponent', () => { let fixture: ComponentFixture; let operations: jasmine.SpyObj; let sessions: jasmine.SpyObj; beforeEach(async () => { operations = jasmine.createSpyObj('OperationsService', ['dashboard', 'dashboardSnapshot', 'scheduleTrend']); operations.dashboardSnapshot.and.returnValue(null); sessions = jasmine.createSpyObj('AdminSessionService', ['applyIdentity']); operations.dashboard.and.resolveTo({ identity: { objectId: 'admin-1', username: 'ops', displayName: '运营经理', roles: ['admin', 'ops-manager'], isSuperAdmin: false, operationsRole: 'ops-manager' }, date: '2026-08-20', metrics: [ { key: 'members', label: '在学会员', value: 27, hint: '当前有正式学习任务', route: '/admin/members', tone: 'blue' }, { key: 'today', label: '今日课程', value: 37, hint: '今天计划进行的课程', route: '/admin/schedule', tone: 'amber' }, { key: 'coaches', label: '在岗陪练', value: 39, hint: '实时业务账号', route: '/admin/coaches', tone: 'green' }, { key: 'pay', label: '本月预估工资', value: 120, hint: '按有效上课记录估算', route: '/admin/payroll', tone: 'rose' } ], alerts: [], scheduleTrend: { period: 'week', periodLabel: '近 7 天', dateFrom: '2026-08-14', dateTo: '2026-08-20', points: Array.from({ length: 7 }, (_, index) => ({ key: `2026-08-${14 + index}`, label: `8月${14 + index}日`, shortLabel: `周${index}`, courseCount: index + 1, studentCount: index, completedCount: index, isToday: index === 6 })), todayCourseCount: 7, todayStudentCount: 6, totalCourseCount: 28, totalStudentCount: 12, maxValue: 7, refreshedAt: '2026-08-20T04:00:00.000Z', liveSourceAvailable: true, liveSourceMessage: '' } }); operations.scheduleTrend.and.resolveTo({ period: 'month', periodLabel: '本月', dateFrom: '2026-08-01', dateTo: '2026-08-20', points: [{ key: '2026-08-20', label: '8月20日', shortLabel: '20日', courseCount: 12, studentCount: 9, completedCount: 5, isToday: true }], todayCourseCount: 12, todayStudentCount: 9, totalCourseCount: 12, totalStudentCount: 9, maxValue: 12, refreshedAt: '2026-08-20T04:00:00.000Z', liveSourceAvailable: true, liveSourceMessage: '' }); await TestBed.configureTestingModule({ imports: [AdminDashboardComponent], providers: [ provideRouter([]), { provide: OperationsService, useValue: operations }, { provide: AdminSessionService, useValue: sessions } ] }).compileComponents(); fixture = TestBed.createComponent(AdminDashboardComponent); }); it('refreshes the stored operations role from the cloud identity', async () => { fixture.detectChanges(); await fixture.whenStable(); expect(sessions.applyIdentity).toHaveBeenCalledWith(jasmine.objectContaining({ operationsRole: 'ops-manager' })); }); it('restores a recent dashboard without repeating its summary request', async () => { const data = await operations.dashboard(); operations.dashboard.calls.reset(); operations.dashboardSnapshot.and.returnValue({ data, ageMs: 1_000 }); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); expect(operations.dashboard).not.toHaveBeenCalled(); expect(fixture.nativeElement.querySelector('.welcome')?.textContent).toContain('运营经理'); expect(fixture.componentInstance.loading()).toBeFalse(); }); it('keeps the previous dashboard visible when a stale snapshot cannot refresh', async () => { const data = await operations.dashboard(); operations.dashboard.calls.reset(); operations.dashboard.and.rejectWith(new Error('暂时不可用')); operations.dashboardSnapshot.and.returnValue({ data, ageMs: 31_000 }); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); expect(operations.dashboard).toHaveBeenCalledTimes(1); expect(fixture.nativeElement.querySelector('.welcome')?.textContent).toContain('运营经理'); expect(fixture.nativeElement.querySelector('.refresh-error')?.textContent).toContain('暂时不可用'); }); it('checks an old dashboard in the background when returning to its saved route', async () => { const data = await operations.dashboard(); operations.dashboard.calls.reset(); operations.dashboardSnapshot.and.returnValue({ data, ageMs: 1_000 }); fixture.detectChanges(); await fixture.whenStable(); operations.dashboardSnapshot.and.returnValue({ data, ageMs: 31_000 }); fixture.componentInstance.onOperationsRouteAttach(); expect(fixture.componentInstance.loading()).toBeFalse(); await fixture.whenStable(); expect(operations.dashboard).toHaveBeenCalledTimes(1); }); it('loads the default weekly schedule trend and switches to month', async () => { fixture.detectChanges(); await fixture.whenStable(); expect(fixture.componentInstance.trend()?.points.length).toBe(7); expect(fixture.componentInstance.selectedTrendPoint()?.courseCount).toBe(7); await fixture.componentInstance.selectTrendPeriod('month'); expect(operations.scheduleTrend).toHaveBeenCalledWith('month'); expect(fixture.componentInstance.trend()?.period).toBe('month'); expect(fixture.componentInstance.chartLine('courseCount')).toContain(','); }); it('merges daily course data into the trend and keeps only coach and pay overview links', async () => { fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); const element: HTMLElement = fixture.nativeElement; expect(element.querySelector('.metric-grid')).toBeNull(); expect(element.querySelector('.trend-stat.coaches')?.textContent).toContain('39'); expect(element.querySelector('.trend-stat.pay')?.textContent).toContain('120'); expect(element.querySelector('.trend-stat.courses')?.textContent).toContain('7'); expect(element.querySelector('.trend-stat.students')?.textContent).toContain('6'); expect(element.querySelectorAll('.trend-stat')).toHaveSize(4); }); });