|
|
@@ -0,0 +1,117 @@
|
|
|
+import { signal } from '@angular/core';
|
|
|
+import { TestBed } from '@angular/core/testing';
|
|
|
+import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
|
|
|
+import { of, Subject } from 'rxjs';
|
|
|
+import { ApiService } from '../../core/api.service';
|
|
|
+import { SessionService } from '../../core/session.service';
|
|
|
+import { LessonReportComponent } from './lesson-report.component';
|
|
|
+import { LessonReportPdfComponent } from './lesson-report-pdf.component';
|
|
|
+import { LessonReportDownloadService } from './lesson-report-download.service';
|
|
|
+import { LessonReportData, lessonPdfReport } from './lesson-report-pdf';
|
|
|
+
|
|
|
+const reportFixture = (): LessonReportData => ({
|
|
|
+ order: {GeneralID:901, szyh:20, HoneyName:'测试学员', plxm:'测试老师', yykcmc:'九年级词汇', yysj:'2026-10-22 14:00', dszt:30, dslx:2, teacherPay:30},
|
|
|
+ feedback: {feedbackStatus:'submitted',contentSummary:'完成本节单词学习',reviewSummary:'复习生词',trainingSummary:'第3次课',homework:'完成课后复习',coachComment:'继续加油'},
|
|
|
+ records: [{generalId:1001,dqrq:'2026-10-22',learned:3,ygg:1,djq:1}],
|
|
|
+ antiForget: {total:10,completed:0}, refreshedAt:'2026-10-22T14:00:00Z',
|
|
|
+});
|
|
|
+const wordsFixture = () => [
|
|
|
+ {GeneralID:1,Title:'lantern',check:1,detail:[{yb:'ˈlæntən',sy:'n.灯笼;提灯'}]},
|
|
|
+ {GeneralID:2,Title:'eve',check:2,detail:[{yb:'iːv',sy:'n.前夜;前夕'}]},
|
|
|
+ {GeneralID:3,Title:'tomb',check:0,detail:[{yb:'tuːm',sy:'n.坟墓'}]},
|
|
|
+];
|
|
|
+
|
|
|
+describe('teacher learning report PDF download', () => {
|
|
|
+ let report: LessonReportData;
|
|
|
+ const groupId = signal(3);
|
|
|
+ let api: jasmine.SpyObj<ApiService>;
|
|
|
+ let download: jasmine.SpyObj<LessonReportDownloadService>;
|
|
|
+ beforeEach(() => {
|
|
|
+ report = reportFixture(); groupId.set(3);
|
|
|
+ api = jasmine.createSpyObj('ApiService',['get']);
|
|
|
+ api.get.and.callFake((action: string) => of({retcode:0,result:(action==='app_lesson_report'?report:action==='e_record_detail'?wordsFixture():{batches:[]}) as any}));
|
|
|
+ download = jasmine.createSpyObj('LessonReportDownloadService',['save']);
|
|
|
+ download.save.and.resolveTo();
|
|
|
+ TestBed.configureTestingModule({providers:[provideRouter([]),
|
|
|
+ {provide:ActivatedRoute,useValue:{snapshot:{queryParamMap:convertToParamMap({sid:'901'})}}},
|
|
|
+ {provide:ApiService,useValue:api},{provide:SessionService,useValue:{groupId}},
|
|
|
+ {provide:LessonReportDownloadService,useValue:download},
|
|
|
+ ]});
|
|
|
+ });
|
|
|
+ function screen() { const fixture=TestBed.createComponent(LessonReportComponent);fixture.detectChanges();return fixture; }
|
|
|
+ it('offers the teacher a footer download and fetches saved word results before rendering the full report',async()=>{
|
|
|
+ const fixture=screen();
|
|
|
+ expect(fixture.nativeElement.querySelector('footer .download-pdf').textContent).toContain('下载 PDF');
|
|
|
+ expect(fixture.nativeElement.querySelector('footer a').textContent).toContain('查看学习记录');
|
|
|
+ download.save.and.callFake(async(element, filename)=>{
|
|
|
+ expect(filename).toBe('小树陪练-单词学习报表-测试学员-2026-10-22-901.pdf');
|
|
|
+ expect(element.querySelectorAll('.word-row').length).toBe(3);
|
|
|
+ expect(element.textContent).toContain('n.灯笼;提灯');
|
|
|
+ expect(element.textContent).toContain('已掌握');expect(element.textContent).toContain('待复习');expect(element.textContent).toContain('未标记');
|
|
|
+ expect(element.textContent).toContain('21天复习计划');expect(element.textContent).toContain('完成课后复习');
|
|
|
+ expect(element.textContent).not.toMatch(/工资|收入|¥/);
|
|
|
+ });
|
|
|
+ await fixture.componentInstance.downloadPdf();fixture.detectChanges();
|
|
|
+ expect(api.get).toHaveBeenCalledWith('e_record_detail',{id:1001,studentId:20});
|
|
|
+ expect(download.save).toHaveBeenCalledTimes(1);
|
|
|
+ expect(fixture.nativeElement.querySelector('[role="status"]').textContent).toContain('PDF 已生成');
|
|
|
+ expect(fixture.nativeElement.querySelector('.pdf-render')).toBeNull();
|
|
|
+ });
|
|
|
+ it('blocks duplicate clicks and enables retry after a failed record read',async()=>{
|
|
|
+ const fixture=screen();const pending=new Subject<any>();
|
|
|
+ api.get.and.callFake((action:string)=>action==='e_record_detail'?pending:of({retcode:0,result:report as any}));
|
|
|
+ const first=fixture.componentInstance.downloadPdf();await Promise.resolve();
|
|
|
+ await fixture.componentInstance.downloadPdf();fixture.detectChanges();
|
|
|
+ expect(fixture.nativeElement.querySelector('.download-pdf').disabled).toBeTrue();
|
|
|
+ expect(api.get.calls.allArgs().filter(args=>args[0]==='e_record_detail').length).toBe(1);
|
|
|
+ pending.error(new Error('单词读取失败'));await first;fixture.detectChanges();
|
|
|
+ expect(download.save).not.toHaveBeenCalled();expect(fixture.nativeElement.querySelector('[role="alert"]').textContent).toContain('单词读取失败');
|
|
|
+ expect(fixture.nativeElement.querySelector('.download-pdf').disabled).toBeFalse();
|
|
|
+ api.get.and.callFake((action:string)=>of({retcode:0,result:(action==='e_record_detail'?wordsFixture():report) as any}));
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(download.save).toHaveBeenCalledTimes(1);
|
|
|
+ });
|
|
|
+ it('does not export incomplete lessons or empty record lists',async()=>{
|
|
|
+ const fixture=screen();report.order['dszt']=10;
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(fixture.componentInstance.exportError()).toContain('请先完成课程');
|
|
|
+ report.order['dszt']=30;report.records=[];
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(fixture.componentInstance.exportError()).toContain('暂无学习记录');
|
|
|
+ expect(download.save).not.toHaveBeenCalled();
|
|
|
+ });
|
|
|
+ it('keeps all saved records in one file and reports generation failures',async()=>{
|
|
|
+ report.records.push({...report.records[0],generalId:1002});const fixture=screen();
|
|
|
+ download.save.and.callFake(async element=>{
|
|
|
+ expect(element.querySelectorAll('.pdf-sheet').length).toBe(2);throw new Error('PDF 生成失败');
|
|
|
+ });
|
|
|
+ await fixture.componentInstance.downloadPdf();
|
|
|
+ expect(fixture.componentInstance.exportError()).toBe('PDF 生成失败');
|
|
|
+ expect(fixture.componentInstance.exporting()).toBeFalse();expect(fixture.componentInstance.pdfReports()).toEqual([]);
|
|
|
+ });
|
|
|
+ it('keeps the teacher-only download out of the student screen',async()=>{
|
|
|
+ groupId.set(1);const fixture=screen();
|
|
|
+ expect(fixture.nativeElement.querySelector('.download-pdf')).toBeNull();
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(download.save).not.toHaveBeenCalled();
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+describe('learning report PDF rendering',()=>{
|
|
|
+ it('uses stored mastery checks and valid review dates without inferring an invalid date',()=>{
|
|
|
+ const report=reportFixture();const record={...report.records[0],fxrl:'2026-10-24,2026-10-25'};
|
|
|
+ const pdf=lessonPdfReport(report,record,wordsFixture());
|
|
|
+ expect([pdf.masteredCount,pdf.reviewDueCount,pdf.unmarkedCount]).toEqual([1,1,1]);
|
|
|
+ expect(pdf.reviewSchedule[0].date).toBe('2026-10-24');expect(pdf.reviewSchedule[9].date).toBe('2026-11-12');
|
|
|
+ expect(lessonPdfReport(report,{dqrq:'2026-02-30'},wordsFixture()).reviewSchedule[0].date).toBe('');
|
|
|
+ });
|
|
|
+ it('generates a real multipage A4 PDF with the printable Angular report',async()=>{
|
|
|
+ const fixture=TestBed.createComponent(LessonReportPdfComponent);
|
|
|
+ const words=Array.from({length:45},(_,index)=>({...wordsFixture()[index%3],GeneralID:index+1}));
|
|
|
+ const pdfReport=lessonPdfReport(reportFixture(),reportFixture().records[0],words);
|
|
|
+ fixture.componentRef.setInput('reports',[pdfReport]);fixture.detectChanges();
|
|
|
+ const service=new LessonReportDownloadService();
|
|
|
+ const blob=await service.createPdf(fixture.nativeElement);
|
|
|
+ expect(blob.type).toBe('application/pdf');expect(blob.size).toBeGreaterThan(20_000);
|
|
|
+ const text=await blob.text();expect(text.startsWith('%PDF-')).toBeTrue();
|
|
|
+ expect((text.match(/\/Type \/Page\b/g)||[]).length).toBeGreaterThan(1);
|
|
|
+ const dimensions=text.match(/\/MediaBox \[0 0 ([\d.]+) ([\d.]+)\]/);
|
|
|
+ expect(Number(dimensions?.[1])).toBeCloseTo(595.28,1);expect(Number(dimensions?.[2])).toBeCloseTo(841.89,1);
|
|
|
+ },30_000);
|
|
|
+});
|