|
|
@@ -7,7 +7,7 @@ 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';
|
|
|
+import { LessonReportData, lessonPdfReport, worksheetReports, lessonPdfVariants } 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},
|
|
|
@@ -30,8 +30,8 @@ describe('teacher learning report PDF download', () => {
|
|
|
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();
|
|
|
+ download = jasmine.createSpyObj('LessonReportDownloadService',['createPdf','download']);
|
|
|
+ download.createPdf.and.resolveTo(new Blob(['%PDF-test'],{type:'application/pdf'}));
|
|
|
TestBed.configureTestingModule({providers:[provideRouter([]),
|
|
|
{provide:ActivatedRoute,useValue:{snapshot:{queryParamMap:convertToParamMap({sid:'901'})}}},
|
|
|
{provide:ApiService,useValue:api},{provide:SessionService,useValue:{groupId}},
|
|
|
@@ -40,23 +40,66 @@ describe('teacher learning report PDF 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();
|
|
|
+ const fixture=screen();fixture.componentInstance.selectedVariants.set(['full']);
|
|
|
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');
|
|
|
+ download.createPdf.and.callFake(async(element)=>{
|
|
|
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(/工资|收入|¥/);
|
|
|
+ return new Blob(['%PDF-test']);
|
|
|
});
|
|
|
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(download.download).toHaveBeenCalledTimes(1);
|
|
|
+ expect(fixture.nativeElement.querySelector('[role="status"]').textContent).toContain('已生成 1 份 PDF');
|
|
|
expect(fixture.nativeElement.querySelector('.pdf-render')).toBeNull();
|
|
|
});
|
|
|
+ it('downloads all three formats by default and keeps answers out of the worksheets',async()=>{
|
|
|
+ const fixture=screen();const titles:string[]=[];
|
|
|
+ expect(fixture.nativeElement.querySelectorAll('.pdf-options input:checked').length).toBe(3);
|
|
|
+ download.createPdf.and.callFake(async element=>{
|
|
|
+ titles.push(element.querySelector('h1')!.textContent!);
|
|
|
+ const table=element.querySelector('.worksheet-table');
|
|
|
+ if(titles.length===2) {
|
|
|
+ expect(table?.textContent).toContain('lantern');expect(table?.textContent).not.toContain('灯笼');
|
|
|
+ } else if(titles.length===3) {
|
|
|
+ expect(table?.textContent).toContain('灯笼');expect(table?.textContent).not.toContain('lantern');
|
|
|
+ }
|
|
|
+ if(table) {
|
|
|
+ expect(table.querySelectorAll('.answer-space').length).toBe(3);
|
|
|
+ expect(element.querySelector('.report-summary')).toBeNull();expect(element.querySelector('.feedback-section')).toBeNull();
|
|
|
+ }
|
|
|
+ expect(download.download).not.toHaveBeenCalled();
|
|
|
+ return new Blob(['%PDF-'+titles.length]);
|
|
|
+ });
|
|
|
+ await fixture.componentInstance.downloadPdf();fixture.detectChanges();
|
|
|
+ expect(titles).toEqual(['单词学习报表','英文填中文复习表','中文填英文复习表']);
|
|
|
+ expect(download.download).toHaveBeenCalledTimes(3);
|
|
|
+ expect(new Set(download.download.calls.allArgs().map(args=>args[0].filename)).size).toBe(3);
|
|
|
+ expect(fixture.nativeElement.querySelectorAll('.generated-files a[download]').length).toBe(3);
|
|
|
+ expect(api.get.calls.allArgs().filter(args=>args[0]==='e_record_detail').length).toBe(1);
|
|
|
+ const revoke=spyOn(URL,'revokeObjectURL');fixture.destroy();expect(revoke).toHaveBeenCalledTimes(3);
|
|
|
+ });
|
|
|
+ it('downloads only selected formats and prevents an empty selection',async()=>{
|
|
|
+ const fixture=screen();const choices=fixture.nativeElement.querySelectorAll('.pdf-options input');
|
|
|
+ choices[0].click();choices[2].click();fixture.detectChanges();
|
|
|
+ await fixture.componentInstance.downloadPdf();
|
|
|
+ expect(download.download).toHaveBeenCalledTimes(1);
|
|
|
+ expect(download.download.calls.mostRecent().args[0].filename).toContain('英文填中文复习表');
|
|
|
+ fixture.detectChanges();choices[1].click();fixture.detectChanges();expect(fixture.nativeElement.querySelector('.download-pdf').disabled).toBeTrue();
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(download.download).toHaveBeenCalledTimes(1);
|
|
|
+ });
|
|
|
+ it('does not start partial downloads when a later PDF fails to generate',async()=>{
|
|
|
+ const fixture=screen();let count=0;
|
|
|
+ download.createPdf.and.callFake(async()=>{if(++count===2)throw Error('复习表生成失败');return new Blob(['%PDF-test']);});
|
|
|
+ await fixture.componentInstance.downloadPdf();
|
|
|
+ expect(download.download).not.toHaveBeenCalled();expect(fixture.componentInstance.generatedFiles()).toEqual([]);
|
|
|
+ expect(fixture.componentInstance.exportError()).toContain('复习表生成失败');
|
|
|
+ expect(fixture.componentInstance.exporting()).toBeFalse();
|
|
|
+ });
|
|
|
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}));
|
|
|
@@ -65,21 +108,21 @@ describe('teacher learning report PDF download', () => {
|
|
|
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(download.download).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);
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(download.download).toHaveBeenCalledTimes(3);
|
|
|
});
|
|
|
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();
|
|
|
+ expect(download.download).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=>{
|
|
|
+ download.createPdf.and.callFake(async element=>{
|
|
|
expect(element.querySelectorAll('.pdf-sheet').length).toBe(2);throw new Error('PDF 生成失败');
|
|
|
});
|
|
|
await fixture.componentInstance.downloadPdf();
|
|
|
@@ -89,7 +132,7 @@ describe('teacher learning report PDF download', () => {
|
|
|
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();
|
|
|
+ await fixture.componentInstance.downloadPdf();expect(download.download).not.toHaveBeenCalled();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
@@ -101,17 +144,28 @@ describe('learning report PDF rendering',()=>{
|
|
|
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('shuffles complete worksheets without changing the full report or losing words',()=>{
|
|
|
+ const report=lessonPdfReport(reportFixture(),reportFixture().records[0],wordsFixture());
|
|
|
+ const before=report.words.map(word=>word.word);spyOn(Math,'random').and.returnValue(.9999);
|
|
|
+ const worksheet=worksheetReports([report])[0];
|
|
|
+ expect(worksheet.words.map(word=>word.word)).not.toEqual(before);
|
|
|
+ expect(worksheet.words.map(word=>word.word).sort()).toEqual([...before].sort());
|
|
|
+ expect(report.words.map(word=>word.word)).toEqual(before);
|
|
|
+ });
|
|
|
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);
|
|
|
+ for(const variant of lessonPdfVariants) {
|
|
|
+ fixture.componentRef.setInput('variant',variant.key);fixture.detectChanges();
|
|
|
+ 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();
|
|
|
+ if(variant.key==='full') 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);
|
|
|
});
|