import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; import { LearningReportDetail, LearningReportPage } from '../admin.models'; import { OperationsService } from '../operations.service'; import { OperationsLearningReportsComponent, printableReviewSchedule, shuffleReportWords } from './operations-learning-reports.component'; const sourceStatus = { state: 'synced' as const, label: '学习记录已同步', message: '', lastSuccessAt: '2026-09-04T12:00:00.000Z', lagSeconds: 0 }; const report = { targetId: 'study-common-1', generalId: 855751, studentObjectId: 'student-1', studentId: 1825, studentName: '测试学员', studentMobileMasked: '135****1234', coachId: 1665, coachName: '测试陪练', courseId: 291, courseName: '人教版', appointmentId: 854951, appointmentAt: '2026-09-04T12:00:00.000Z', studyAt: '2026-09-04T12:00:00.000Z', studyDate: '2026-09-04', classTypeLabel: '30 分钟', learnedCount: 3, masteredCount: 2, reviewDueCount: 1, unmarkedCount: 0, masteryRate: 66.7, status: 'available' as const, statusLabel: '可生成', canExport: true, updatedAt: '2026-09-04T12:00:00.000Z', }; describe('OperationsLearningReportsComponent', () => { let fixture: ComponentFixture; let operations: jasmine.SpyObj; beforeEach(async () => { const page: LearningReportPage = { page: 1, pageSize: 20, total: 1, items: [report], summary: { reportCount: 1, studentCount: 1, learnedCount: 3, masteredCount: 2, reviewDueCount: 1, masteryRate: 66.7 }, sourceStatus, refreshedAt: '2026-09-04T12:00:00.000Z' }; const detail: LearningReportDetail = { ...report, words: [{ index: 1, generalId: 1, word: 'apple', phonetic: '', meaning: '苹果', example: '', result: 'mastered', resultLabel: '已掌握' }], reviewSchedule: [{ day: 1, date: '2026-09-05' }], score: 5, studentComment: '很好', coachComment: '继续复习', generatedAt: '2026-09-04T12:00:00.000Z', sourceStatus, countMismatch: false }; operations = jasmine.createSpyObj('OperationsService', ['learningReports', 'learningReportDetail', 'learningReportExport']); operations.learningReports.and.resolveTo(page); operations.learningReportDetail.and.resolveTo(detail); operations.learningReportExport.and.resolveTo(detail); await TestBed.configureTestingModule({ imports: [OperationsLearningReportsComponent], providers: [provideRouter([]), { provide: OperationsService, useValue: operations }, { provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: convertToParamMap({ studentObjectId: 'student-1' }) } } }], }).compileComponents(); fixture = TestBed.createComponent(OperationsLearningReportsComponent); fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges(); }); it('scopes a member entry to the selected student and renders report metrics', () => { expect(operations.learningReports).toHaveBeenCalledWith(1, 20, '', jasmine.objectContaining({ studentObjectId: 'student-1' }), false); const element: HTMLElement = fixture.nativeElement; expect(element.textContent).toContain('单词学习报表'); expect(element.textContent).toContain('测试学员'); expect(element.textContent).toContain('66.7%'); }); it('opens the complete word report through the detail cloud operation', async () => { await fixture.componentInstance.open('study-common-1'); fixture.detectChanges(); expect(operations.learningReportDetail).toHaveBeenCalledWith('study-common-1'); expect((fixture.nativeElement as HTMLElement).textContent).toContain('apple'); expect((fixture.nativeElement as HTMLElement).textContent).toContain('21天复习计划'); }); it('submits every visible filter when the operator queries reports', async () => { const component = fixture.componentInstance; operations.learningReports.calls.reset(); component.month.set('2026-09'); component.dateFrom.set('2026-09-01'); component.dateTo.set('2026-09-04'); component.search.set('熊诗琦'); component.coachSearch.set('樊颖'); component.courseSearch.set('人教版'); component.status.set('available'); await component.applyFilters(); expect(operations.learningReports).toHaveBeenCalledWith(1, 20, '熊诗琦', { month: '2026-09', dateFrom: '2026-09-01', dateTo: '2026-09-04', status: 'available', coachSearch: '樊颖', courseSearch: '人教版', studentObjectId: 'student-1', }, false); }); it('resets every editable filter and immediately reloads the default month', async () => { const component = fixture.componentInstance; operations.learningReports.calls.reset(); component.month.set('2025-02'); component.dateFrom.set('2025-02-01'); component.dateTo.set('2025-02-28'); component.search.set('旧学员'); component.coachSearch.set('旧陪练'); component.courseSearch.set('旧课程'); component.status.set('missing'); await component.resetFilters(); const now = new Date(); const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; expect(component.month()).toBe(currentMonth); expect(component.dateFrom()).toBe(''); expect(component.dateTo()).toBe(''); expect(component.search()).toBe(''); expect(component.coachSearch()).toBe(''); expect(component.courseSearch()).toBe(''); expect(component.status()).toBe(''); expect(operations.learningReports).toHaveBeenCalledWith(1, 20, '', { month: currentMonth, dateFrom: '', dateTo: '', status: '', coachSearch: '', courseSearch: '', studentObjectId: 'student-1', }, false); }); it('shows an explicit searching state until the query finishes', async () => { const component = fixture.componentInstance; const currentPage = component.data() as LearningReportPage; let resolveRequest!: (value: LearningReportPage) => void; operations.learningReports.and.returnValue(new Promise((resolve) => { resolveRequest = resolve; })); const pending = component.applyFilters(); fixture.detectChanges(); const queryButton = (fixture.nativeElement as HTMLElement).querySelector('.query-action'); expect(component.searching()).toBeTrue(); expect(queryButton?.disabled).toBeTrue(); expect(queryButton?.textContent).toContain('正在查找'); resolveRequest(currentPage); await pending; fixture.detectChanges(); expect(component.searching()).toBeFalse(); expect(queryButton?.textContent).toContain('查找'); }); it('prints only the selected language without answers in examples, comments or the review word list', async () => { const component = fixture.componentInstance; await component.open('study-common-1'); const original = component.detail()!; component.detail.set({ ...original, words: [{ ...original.words[0], phonetic: '/apple-hint/', example: 'apple 苹果' }], coachComment: 'apple 苹果' }); for (const variant of ['english', 'chinese'] as const) { component.selectVariant(variant); fixture.detectChanges(); const sheet = (fixture.nativeElement as HTMLElement).querySelector('.pdf-sheet')!; expect(sheet.textContent).toContain(variant === 'english' ? 'apple' : '苹果'); expect(sheet.textContent).not.toContain(variant === 'english' ? '苹果' : 'apple'); expect(sheet.querySelectorAll('.answer-space').length).toBe(1); expect(sheet.querySelector('.comments')).toBeNull(); expect(sheet.querySelectorAll('.review-count').length).toBe(10); expect(Array.from(sheet.querySelectorAll('.review-count')).every((cell) => !cell.textContent?.trim())).toBeTrue(); expect(sheet.textContent).not.toContain('记住词数'); expect(sheet.textContent).toContain('遗忘词数'); expect(sheet.querySelector('.review-words')?.textContent).toContain('本次学习 1 个单词'); expect(sheet.querySelector('.review-words')?.textContent).not.toContain('苹果'); } component.selectVariant('full'); fixture.detectChanges(); const sheet = (fixture.nativeElement as HTMLElement).querySelector('.pdf-sheet')!; expect(sheet.textContent).toContain('apple'); expect(sheet.textContent).toContain('苹果'); expect(sheet.querySelector('.comments')).not.toBeNull(); }); it('creates ten review dates across months and years and preserves stored dates', async () => { await fixture.componentInstance.open('study-common-1'); const detail = fixture.componentInstance.detail()!; const schedule = printableReviewSchedule({ ...detail, studyDate: '2026-09-21', reviewSchedule: [] }); expect(schedule.map((item) => item.day)).toEqual([1, 2, 3, 5, 7, 9, 11, 14, 17, 21]); expect(schedule.map((item) => item.date)).toEqual(['2026-09-22', '2026-09-23', '2026-09-24', '2026-09-26', '2026-09-28', '2026-09-30', '2026-10-02', '2026-10-05', '2026-10-08', '2026-10-12']); expect(printableReviewSchedule({ ...detail, studyDate: '2026-12-31', reviewSchedule: [] })[0].date).toBe('2027-01-01'); expect(printableReviewSchedule({ ...detail, studyDate: '2028-02-28', reviewSchedule: [] })[0].date).toBe('2028-02-29'); expect(printableReviewSchedule({ ...detail, studyDate: '2026-09-21' })[0]).toEqual({ day: 1, date: '2026-09-05', inferred: false }); expect(printableReviewSchedule({ ...detail, studyDate: '', reviewSchedule: [] }).every((item) => item.date === '')).toBeTrue(); expect(printableReviewSchedule({ ...detail, studyDate: '2026-02-30', reviewSchedule: [] }).every((item) => item.date === '')).toBeTrue(); }); it('keeps the report stable while exporting and resets the variant on a new preview', async () => { const component = fixture.componentInstance; await component.open('study-common-1'); component.selectVariant('english'); component.exportingId.set('study-common-1'); component.selectVariant('chinese'); component.close(); await component.open('another-report'); expect(component.reportVariant()).toBe('english'); expect(component.detail()?.targetId).toBe('study-common-1'); expect(operations.learningReportDetail).toHaveBeenCalledTimes(1); component.exportingId.set(''); await component.open('study-common-1'); expect(component.reportVariant()).toBe('full'); }); it('reshuffles either worksheet on each entry without changing the original report', async () => { const component = fixture.componentInstance; await component.open('study-common-1'); const original = component.detail()!; const words = ['apple', 'banana', 'orange', 'grape'].map((word, index) => ({ ...original.words[0], word, index: index + 1, generalId: index + 1 })); component.detail.set({ ...original, words }); spyOn(Math, 'random').and.returnValue(0.999); component.selectVariant('english'); const first = component.displayWords().map((word) => word.word); expect(first).not.toEqual(words.map((word) => word.word)); component.selectVariant('chinese'); const second = component.displayWords().map((word) => word.word); expect(second).not.toEqual(first); component.selectVariant('chinese'); expect(component.displayWords().map((word) => word.word)).not.toEqual(second); expect([...component.displayWords()].sort((a, b) => a.index - b.index)).toEqual(words); component.selectVariant('full'); expect(component.displayWords()).toEqual(words); expect(words.map((word) => word.word)).toEqual(['apple', 'banana', 'orange', 'grape']); expect(shuffleReportWords([])).toEqual([]); expect(shuffleReportWords([words[0]])).toEqual([words[0]]); }); });