operations-learning-reports.component.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import { ComponentFixture, TestBed } from '@angular/core/testing';
  2. import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
  3. import { LearningReportDetail, LearningReportPage } from '../admin.models';
  4. import { OperationsService } from '../operations.service';
  5. import { OperationsLearningReportsComponent, printableReviewSchedule, shuffleReportWords } from './operations-learning-reports.component';
  6. const sourceStatus = { state: 'synced' as const, label: '学习记录已同步', message: '', lastSuccessAt: '2026-09-04T12:00:00.000Z', lagSeconds: 0 };
  7. const report = {
  8. targetId: 'study-common-1', generalId: 855751, studentObjectId: 'student-1', studentId: 1825, studentName: '测试学员', studentMobileMasked: '135****1234',
  9. coachId: 1665, coachName: '测试陪练', courseId: 291, courseName: '人教版', appointmentId: 854951, appointmentAt: '2026-09-04T12:00:00.000Z',
  10. studyAt: '2026-09-04T12:00:00.000Z', studyDate: '2026-09-04', classTypeLabel: '30 分钟', learnedCount: 3, masteredCount: 2,
  11. reviewDueCount: 1, unmarkedCount: 0, masteryRate: 66.7, status: 'available' as const, statusLabel: '可生成', canExport: true, updatedAt: '2026-09-04T12:00:00.000Z',
  12. };
  13. describe('OperationsLearningReportsComponent', () => {
  14. let fixture: ComponentFixture<OperationsLearningReportsComponent>;
  15. let operations: jasmine.SpyObj<OperationsService>;
  16. beforeEach(async () => {
  17. 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' };
  18. 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 };
  19. operations = jasmine.createSpyObj<OperationsService>('OperationsService', ['learningReports', 'learningReportDetail', 'learningReportExport']);
  20. operations.learningReports.and.resolveTo(page);
  21. operations.learningReportDetail.and.resolveTo(detail);
  22. operations.learningReportExport.and.resolveTo(detail);
  23. await TestBed.configureTestingModule({
  24. imports: [OperationsLearningReportsComponent],
  25. providers: [provideRouter([]), { provide: OperationsService, useValue: operations }, { provide: ActivatedRoute, useValue: { snapshot: { queryParamMap: convertToParamMap({ studentObjectId: 'student-1' }) } } }],
  26. }).compileComponents();
  27. fixture = TestBed.createComponent(OperationsLearningReportsComponent);
  28. fixture.detectChanges(); await fixture.whenStable(); fixture.detectChanges();
  29. });
  30. it('scopes a member entry to the selected student and renders report metrics', () => {
  31. expect(operations.learningReports).toHaveBeenCalledWith(1, 20, '', jasmine.objectContaining({ studentObjectId: 'student-1' }), false);
  32. const element: HTMLElement = fixture.nativeElement;
  33. expect(element.textContent).toContain('单词学习报表');
  34. expect(element.textContent).toContain('测试学员');
  35. expect(element.textContent).toContain('66.7%');
  36. });
  37. it('opens the complete word report through the detail cloud operation', async () => {
  38. await fixture.componentInstance.open('study-common-1');
  39. fixture.detectChanges();
  40. expect(operations.learningReportDetail).toHaveBeenCalledWith('study-common-1');
  41. expect((fixture.nativeElement as HTMLElement).textContent).toContain('apple');
  42. expect((fixture.nativeElement as HTMLElement).textContent).toContain('21天复习计划');
  43. });
  44. it('submits every visible filter when the operator queries reports', async () => {
  45. const component = fixture.componentInstance;
  46. operations.learningReports.calls.reset();
  47. component.month.set('2026-09');
  48. component.dateFrom.set('2026-09-01');
  49. component.dateTo.set('2026-09-04');
  50. component.search.set('熊诗琦');
  51. component.coachSearch.set('樊颖');
  52. component.courseSearch.set('人教版');
  53. component.status.set('available');
  54. await component.applyFilters();
  55. expect(operations.learningReports).toHaveBeenCalledWith(1, 20, '熊诗琦', {
  56. month: '2026-09',
  57. dateFrom: '2026-09-01',
  58. dateTo: '2026-09-04',
  59. status: 'available',
  60. coachSearch: '樊颖',
  61. courseSearch: '人教版',
  62. studentObjectId: 'student-1',
  63. }, false);
  64. });
  65. it('resets every editable filter and immediately reloads the default month', async () => {
  66. const component = fixture.componentInstance;
  67. operations.learningReports.calls.reset();
  68. component.month.set('2025-02');
  69. component.dateFrom.set('2025-02-01');
  70. component.dateTo.set('2025-02-28');
  71. component.search.set('旧学员');
  72. component.coachSearch.set('旧陪练');
  73. component.courseSearch.set('旧课程');
  74. component.status.set('missing');
  75. await component.resetFilters();
  76. const now = new Date();
  77. const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
  78. expect(component.month()).toBe(currentMonth);
  79. expect(component.dateFrom()).toBe('');
  80. expect(component.dateTo()).toBe('');
  81. expect(component.search()).toBe('');
  82. expect(component.coachSearch()).toBe('');
  83. expect(component.courseSearch()).toBe('');
  84. expect(component.status()).toBe('');
  85. expect(operations.learningReports).toHaveBeenCalledWith(1, 20, '', {
  86. month: currentMonth,
  87. dateFrom: '',
  88. dateTo: '',
  89. status: '',
  90. coachSearch: '',
  91. courseSearch: '',
  92. studentObjectId: 'student-1',
  93. }, false);
  94. });
  95. it('shows an explicit searching state until the query finishes', async () => {
  96. const component = fixture.componentInstance;
  97. const currentPage = component.data() as LearningReportPage;
  98. let resolveRequest!: (value: LearningReportPage) => void;
  99. operations.learningReports.and.returnValue(new Promise((resolve) => { resolveRequest = resolve; }));
  100. const pending = component.applyFilters();
  101. fixture.detectChanges();
  102. const queryButton = (fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>('.query-action');
  103. expect(component.searching()).toBeTrue();
  104. expect(queryButton?.disabled).toBeTrue();
  105. expect(queryButton?.textContent).toContain('正在查找');
  106. resolveRequest(currentPage);
  107. await pending;
  108. fixture.detectChanges();
  109. expect(component.searching()).toBeFalse();
  110. expect(queryButton?.textContent).toContain('查找');
  111. });
  112. it('prints only the selected language without answers in examples, comments or the review word list', async () => {
  113. const component = fixture.componentInstance;
  114. await component.open('study-common-1');
  115. const original = component.detail()!;
  116. component.detail.set({ ...original, words: [{ ...original.words[0], phonetic: '/apple-hint/', example: 'apple 苹果' }], coachComment: 'apple 苹果' });
  117. for (const variant of ['english', 'chinese'] as const) {
  118. component.selectVariant(variant); fixture.detectChanges();
  119. const sheet = (fixture.nativeElement as HTMLElement).querySelector('.pdf-sheet')!;
  120. expect(sheet.textContent).toContain(variant === 'english' ? 'apple' : '苹果');
  121. expect(sheet.textContent).not.toContain(variant === 'english' ? '苹果' : 'apple');
  122. expect(sheet.querySelectorAll('.answer-space').length).toBe(1);
  123. expect(sheet.querySelector('.comments')).toBeNull();
  124. expect(sheet.querySelectorAll('.review-count').length).toBe(10);
  125. expect(Array.from(sheet.querySelectorAll('.review-count')).every((cell) => !cell.textContent?.trim())).toBeTrue();
  126. expect(sheet.textContent).not.toContain('记住词数');
  127. expect(sheet.textContent).toContain('遗忘词数');
  128. expect(sheet.querySelector('.review-words')?.textContent).toContain('本次学习 1 个单词');
  129. expect(sheet.querySelector('.review-words')?.textContent).not.toContain('苹果');
  130. }
  131. component.selectVariant('full'); fixture.detectChanges();
  132. const sheet = (fixture.nativeElement as HTMLElement).querySelector('.pdf-sheet')!;
  133. expect(sheet.textContent).toContain('apple');
  134. expect(sheet.textContent).toContain('苹果');
  135. expect(sheet.querySelector('.comments')).not.toBeNull();
  136. });
  137. it('creates ten review dates across months and years and preserves stored dates', async () => {
  138. await fixture.componentInstance.open('study-common-1');
  139. const detail = fixture.componentInstance.detail()!;
  140. const schedule = printableReviewSchedule({ ...detail, studyDate: '2026-09-21', reviewSchedule: [] });
  141. expect(schedule.map((item) => item.day)).toEqual([1, 2, 3, 5, 7, 9, 11, 14, 17, 21]);
  142. 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']);
  143. expect(printableReviewSchedule({ ...detail, studyDate: '2026-12-31', reviewSchedule: [] })[0].date).toBe('2027-01-01');
  144. expect(printableReviewSchedule({ ...detail, studyDate: '2028-02-28', reviewSchedule: [] })[0].date).toBe('2028-02-29');
  145. expect(printableReviewSchedule({ ...detail, studyDate: '2026-09-21' })[0]).toEqual({ day: 1, date: '2026-09-05', inferred: false });
  146. expect(printableReviewSchedule({ ...detail, studyDate: '', reviewSchedule: [] }).every((item) => item.date === '')).toBeTrue();
  147. expect(printableReviewSchedule({ ...detail, studyDate: '2026-02-30', reviewSchedule: [] }).every((item) => item.date === '')).toBeTrue();
  148. });
  149. it('keeps the report stable while exporting and resets the variant on a new preview', async () => {
  150. const component = fixture.componentInstance;
  151. await component.open('study-common-1');
  152. component.selectVariant('english');
  153. component.exportingId.set('study-common-1');
  154. component.selectVariant('chinese'); component.close();
  155. await component.open('another-report');
  156. expect(component.reportVariant()).toBe('english');
  157. expect(component.detail()?.targetId).toBe('study-common-1');
  158. expect(operations.learningReportDetail).toHaveBeenCalledTimes(1);
  159. component.exportingId.set('');
  160. await component.open('study-common-1');
  161. expect(component.reportVariant()).toBe('full');
  162. });
  163. it('reshuffles either worksheet on each entry without changing the original report', async () => {
  164. const component = fixture.componentInstance;
  165. await component.open('study-common-1');
  166. const original = component.detail()!;
  167. const words = ['apple', 'banana', 'orange', 'grape'].map((word, index) => ({ ...original.words[0], word, index: index + 1, generalId: index + 1 }));
  168. component.detail.set({ ...original, words });
  169. spyOn(Math, 'random').and.returnValue(0.999);
  170. component.selectVariant('english');
  171. const first = component.displayWords().map((word) => word.word);
  172. expect(first).not.toEqual(words.map((word) => word.word));
  173. component.selectVariant('chinese');
  174. const second = component.displayWords().map((word) => word.word);
  175. expect(second).not.toEqual(first);
  176. component.selectVariant('chinese');
  177. expect(component.displayWords().map((word) => word.word)).not.toEqual(second);
  178. expect([...component.displayWords()].sort((a, b) => a.index - b.index)).toEqual(words);
  179. component.selectVariant('full');
  180. expect(component.displayWords()).toEqual(words);
  181. expect(words.map((word) => word.word)).toEqual(['apple', 'banana', 'orange', 'grape']);
  182. expect(shuffleReportWords([])).toEqual([]);
  183. expect(shuffleReportWords([words[0]])).toEqual([words[0]]);
  184. });
  185. });