| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- 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<OperationsLearningReportsComponent>;
- let operations: jasmine.SpyObj<OperationsService>;
- 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>('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<HTMLButtonElement>('.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]]);
- });
- });
|