From 0d228ea5ce9fabad4715689535878c95ceab4527 Mon Sep 17 00:00:00 2001 From: Ece Eren Date: Fri, 22 Nov 2024 01:24:37 +0100 Subject: [PATCH] Fix tests --- .../lecture/web/AttachmentResource.java | 4 +- .../webapp/app/lecture/attachment.service.ts | 2 +- .../pdf-preview/pdf-preview.component.ts | 4 +- ...f-preview-thumbnail-grid.component.spec.ts | 143 ++++++++++--- .../pdf-preview/pdf-preview.component.spec.ts | 198 ++++++++++++++---- 5 files changed, 279 insertions(+), 72 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/lecture/web/AttachmentResource.java b/src/main/java/de/tum/cit/aet/artemis/lecture/web/AttachmentResource.java index d4d4b887b745..3ba588e7b8b3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/lecture/web/AttachmentResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/lecture/web/AttachmentResource.java @@ -219,12 +219,12 @@ else if (attachment.getExercise() != null) { } /** - * GET /attachments/:parentAttachmentId/hiddenAttachment : retrieve the hidden attachment associated with the given parent attachment ID. + * GET /attachments/:parentAttachmentId/hidden-attachment : retrieve the hidden attachment associated with the given parent attachment ID. * * @param parentAttachmentId the ID of the parent attachment for which to retrieve the hidden attachment * @return the ResponseEntity with status 200 (OK) and the Attachment in the body, or a 404 (Not Found) if no matching attachment exists */ - @GetMapping("attachments/{parentAttachmentId}/hiddenAttachment") + @GetMapping("attachments/{parentAttachmentId}/hidden-attachment") @EnforceAtLeastTutor public ResponseEntity getAttachmentByParentAttachmentId(@PathVariable Long parentAttachmentId) { log.debug("REST request to get attachment by the parent attachment Id : {}", parentAttachmentId); diff --git a/src/main/webapp/app/lecture/attachment.service.ts b/src/main/webapp/app/lecture/attachment.service.ts index ae576a073e86..6ad5cfbd18b1 100644 --- a/src/main/webapp/app/lecture/attachment.service.ts +++ b/src/main/webapp/app/lecture/attachment.service.ts @@ -156,7 +156,7 @@ export class AttachmentService { */ getAttachmentByParentAttachmentId(parentAttachmentId: number): Observable { return this.http - .get(`${this.resourceUrl}/${parentAttachmentId}/hiddenAttachment`, { observe: 'response' }) + .get(`${this.resourceUrl}/${parentAttachmentId}/hidden-attachment`, { observe: 'response' }) .pipe(map((res: EntityResponseType) => this.convertAttachmentResponseDatesFromServer(res))); } } diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts index c68899f3a863..37d9ab1a64e9 100644 --- a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -54,7 +54,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { private readonly attachmentService = inject(AttachmentService); private readonly attachmentUnitService = inject(AttachmentUnitService); private readonly lectureUnitService = inject(LectureUnitService); - private readonly alertService = inject(AlertService); + readonly alertService = inject(AlertService); private readonly router = inject(Router); dialogErrorSource = new Subject(); @@ -301,7 +301,7 @@ export class PdfPreviewComponent implements OnInit, OnDestroy { * Updates hidden pages after selected pages are deleted. * @param pagesToDelete - Array of pages to be deleted (0-indexed). */ - private updateHiddenPages(pagesToDelete: number[]) { + updateHiddenPages(pagesToDelete: number[]) { const updatedHiddenPages = new Set(); this.hiddenPages().forEach((hiddenPage) => { // Adjust hiddenPage based on the deleted pages diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts index 968e6830506f..c6be89e3e003 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview-thumbnail-grid.component.spec.ts @@ -6,6 +6,9 @@ import { AlertService } from 'app/core/util/alert.service'; import { HttpClientModule } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { PdfPreviewThumbnailGridComponent } from 'app/lecture/pdf-preview/pdf-preview-thumbnail-grid/pdf-preview-thumbnail-grid.component'; +import { getDocument } from 'pdfjs-dist'; +import * as GlobalUtils from 'app/shared/util/global.utils'; +import { ElementRef, Signal } from '@angular/core'; jest.mock('pdfjs-dist', () => { return { @@ -37,6 +40,7 @@ describe('PdfPreviewThumbnailGridComponent', () => { beforeEach(async () => { alertServiceMock = { error: jest.fn(), + addAlert: jest.fn(), }; await TestBed.configureTestingModule({ @@ -58,45 +62,130 @@ describe('PdfPreviewThumbnailGridComponent', () => { jest.clearAllMocks(); }); + it('should handle changes to inputs correctly in ngOnChanges', async () => { + const initialHiddenPages = new Set([1, 2]); + const updatedHiddenPages = new Set([3, 4]); + const initialPdfUrl = 'old-pdf-url'; + const updatedPdfUrl = 'new-pdf-url'; + + fixture.componentRef.setInput('hiddenPages', initialHiddenPages); + fixture.componentRef.setInput('currentPdfUrl', initialPdfUrl); + fixture.detectChanges(); // Trigger initial bindings + + const loadPdfSpy = jest.spyOn(component, 'loadPdf').mockImplementation(); + + fixture.componentRef.setInput('hiddenPages', updatedHiddenPages); + fixture.componentRef.setInput('currentPdfUrl', updatedPdfUrl); + + component.ngOnChanges({ + hiddenPages: { + previousValue: initialHiddenPages, + currentValue: updatedHiddenPages, + firstChange: false, + isFirstChange: () => false, + }, + currentPdfUrl: { + previousValue: initialPdfUrl, + currentValue: updatedPdfUrl, + firstChange: false, + isFirstChange: () => false, + }, + }); + + expect(component.newHiddenPages()).toEqual(updatedHiddenPages); // Check newHiddenPages signal is updated + expect(loadPdfSpy).toHaveBeenCalledWith(updatedPdfUrl, component.appendFile()); // Verify loadPdf is called with correct args + }); + it('should load PDF and render pages', async () => { - const spyCreateCanvas = jest.spyOn(component, 'createCanvas'); - const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer'); + fixture.componentRef.setInput('currentPdfUrl', 'fake-url'); + fixture.componentRef.setInput('appendFile', false); - await component.loadOrAppendPdf('fake-url'); + const mockCanvas = document.createElement('canvas'); + jest.spyOn(component as any, 'createCanvas').mockReturnValue(mockCanvas); - expect(spyCreateCanvas).toHaveBeenCalled(); - expect(spyCreateCanvasContainer).toHaveBeenCalled(); - expect(component.totalPages()).toBe(1); + await component.loadPdf('fake-url', false); + + expect(getDocument).toHaveBeenCalledWith('fake-url'); + expect(component.totalPagesArray().size).toBe(1); }); - it('should toggle enlarged view state', () => { - const mockCanvas = document.createElement('canvas'); - component.displayEnlargedCanvas(mockCanvas); - expect(component.isEnlargedView()).toBeTruthy(); + it('should create and configure a canvas element for the given viewport', () => { + const mockViewport = { + width: 600, + height: 800, + }; + + const canvas = (component as any).createCanvas(mockViewport); - component.isEnlargedView.set(false); - expect(component.isEnlargedView()).toBeFalsy(); + expect(canvas).toBeInstanceOf(HTMLCanvasElement); + expect(canvas.width).toBe(mockViewport.width); + expect(canvas.height).toBe(mockViewport.height); + expect(canvas.style.display).toBe('block'); + expect(canvas.style.width).toBe('100%'); + expect(canvas.style.height).toBe('100%'); }); - it('should handle mouseenter and mouseleave events correctly', () => { - const mockCanvas = document.createElement('canvas'); - const container = component.createCanvasContainer(mockCanvas, 1); - const overlay = container.querySelector('div'); + it('should toggle visibility of a page correctly', () => { + const hiddenPagesMock = new Set([1, 2]); + fixture.componentRef.setInput('hiddenPages', hiddenPagesMock); + component.toggleVisibility(2, new Event('click')); + + expect(hiddenPagesMock.has(2)).toBeFalsy(); + expect(hiddenPagesMock.has(1)).toBeTruthy(); + }); - container.dispatchEvent(new Event('mouseenter')); - expect(overlay!.style.opacity).toBe('1'); + it('should toggle selection of a page correctly', () => { + const mockEvent = { target: { checked: true } } as unknown as Event; - container.dispatchEvent(new Event('mouseleave')); - expect(overlay!.style.opacity).toBe('0'); + const initialSelectedPages = new Set(); + component.selectedPages.set(initialSelectedPages); + fixture.detectChanges(); + + component.togglePageSelection(1, mockEvent); + + expect(component.selectedPages().has(1)).toBeTruthy(); + + (mockEvent.target as HTMLInputElement).checked = false; + component.togglePageSelection(1, mockEvent); + + expect(component.selectedPages().has(1)).toBeFalsy(); }); - it('should handle click event on overlay to trigger displayEnlargedCanvas', () => { - const displayEnlargedCanvasSpy = jest.spyOn(component, 'displayEnlargedCanvas'); - const mockCanvas = document.createElement('canvas'); - const container = component.createCanvasContainer(mockCanvas, 1); - const overlay = container.querySelector('div'); + it('should handle PDF load errors gracefully', async () => { + const errorMessage = 'Error loading PDF'; + + (getDocument as jest.Mock).mockReturnValue({ + promise: Promise.reject(new Error(errorMessage)), + }); - overlay!.dispatchEvent(new Event('click')); - expect(displayEnlargedCanvasSpy).toHaveBeenCalledWith(mockCanvas); + const onErrorSpy = jest.spyOn(GlobalUtils, 'onError').mockImplementation(); + await component.loadPdf('invalid-url', false); + expect(onErrorSpy).toHaveBeenCalledWith(alertServiceMock, new Error(errorMessage)); + onErrorSpy.mockRestore(); + }); + + it('should set the selected canvas as the originalCanvas and enable enlarged view', () => { + const mockCanvas = document.createElement('canvas'); + const mockDiv = document.createElement('div'); + mockDiv.id = 'pdf-page-1'; + mockDiv.appendChild(mockCanvas); + + const pdfContainerMock: ElementRef = { + nativeElement: { + querySelector: jest.fn((selector: string) => { + if (selector === '#pdf-page-1 canvas') { + return mockCanvas; + } + return null; + }), + }, + } as unknown as ElementRef; + + component.pdfContainer = jest.fn(() => pdfContainerMock) as unknown as Signal>; + component.displayEnlargedCanvas(1); + + expect(pdfContainerMock.nativeElement.querySelector).toHaveBeenCalledWith('#pdf-page-1 canvas'); + expect(component.originalCanvas()).toBe(mockCanvas); + expect(component.isEnlargedView()).toBeTrue(); }); }); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts index 2f0b2ed366f7..b2b1b8609352 100644 --- a/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/pdf-preview/pdf-preview.component.spec.ts @@ -67,11 +67,14 @@ describe('PdfPreviewComponent', () => { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), update: jest.fn().mockReturnValue(of({})), delete: jest.fn().mockReturnValue(of({})), + create: jest.fn().mockReturnValue(of({})), + getAttachmentByParentAttachmentId: jest.fn(), }; attachmentUnitServiceMock = { getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), update: jest.fn().mockReturnValue(of({})), delete: jest.fn().mockReturnValue(of({})), + getHiddenSlides: jest.fn(), }; lectureUnitServiceMock = { delete: jest.fn().mockReturnValue(of({})), @@ -127,11 +130,19 @@ describe('PdfPreviewComponent', () => { it('should load attachment unit file and verify service calls when attachment unit data is available', () => { routeMock.data = of({ course: { id: 1, name: 'Example Course' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, + attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } }, }); + + const mockBlob = new Blob(['mock content'], { type: 'application/pdf' }); + attachmentUnitServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); + attachmentUnitServiceMock.getHiddenSlides = jest.fn().mockReturnValue(of([1, 2, 3])); + component.ngOnInit(); + expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); - expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled(); + expect(attachmentUnitServiceMock.getHiddenSlides).toHaveBeenCalledWith(1, 1); + expect(component.currentPdfBlob()).toBeDefined(); + expect(component.hiddenPages().size).toBe(3); }); it('should handle errors and trigger alert when loading an attachment file fails', () => { @@ -154,22 +165,25 @@ describe('PdfPreviewComponent', () => { it('should handle errors and trigger alert when loading an attachment unit file fails', () => { routeMock.data = of({ course: { id: 1, name: 'Example Course' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, + attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } }, }); + const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found', error: 'File not found', }); - const attachmentUnitService = TestBed.inject(AttachmentUnitService); - jest.spyOn(attachmentUnitService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + attachmentUnitServiceMock.getAttachmentFile.mockReturnValue(throwError(() => errorResponse)); + attachmentUnitServiceMock.getHiddenSlides.mockReturnValue(of([])); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); component.ngOnInit(); fixture.detectChanges(); - expect(alertServiceSpy).toHaveBeenCalled(); + expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(alertServiceSpy).toHaveBeenCalledWith('error.http.404'); }); }); @@ -179,18 +193,109 @@ describe('PdfPreviewComponent', () => { component.ngOnDestroy(); expect(spySub).toHaveBeenCalled(); }); + }); - it('should unsubscribe attachmentUnit subscription during component destruction', () => { - routeMock.data = of({ - course: { id: 1, name: 'Example Course' }, - attachmentUnit: { id: 1, name: 'Chapter 1' }, + describe('Getting Hidden Pages', () => { + it('should return an array of hidden page numbers from DOM elements with specific IDs and classes', () => { + const mockElement1 = document.createElement('button'); + mockElement1.id = 'hide-show-button-1'; + mockElement1.className = 'hide-show-btn btn-success'; + + const mockElement2 = document.createElement('button'); + mockElement2.id = 'hide-show-button-2'; + mockElement2.className = 'hide-show-btn btn-success'; + + const mockElement3 = document.createElement('button'); + mockElement3.id = 'hide-show-button-3'; + mockElement3.className = 'hide-show-btn btn-success'; + + document.body.appendChild(mockElement1); + document.body.appendChild(mockElement2); + document.body.appendChild(mockElement3); + + const hiddenPages = component.getHiddenPages(); + expect(hiddenPages).toEqual([1, 2, 3]); + + document.body.removeChild(mockElement1); + document.body.removeChild(mockElement2); + document.body.removeChild(mockElement3); + }); + + it('should return an empty array if no matching elements are found', () => { + document.body.innerHTML = ''; + const hiddenPages = component.getHiddenPages(); + expect(hiddenPages).toEqual([]); + }); + + it('should ignore elements with IDs that do not match the expected pattern', () => { + const mockElement1 = document.createElement('button'); + mockElement1.id = 'hide-show-button-1'; + mockElement1.className = 'hide-show-btn btn-success'; + + const mockElement2 = document.createElement('button'); + mockElement2.id = 'non-matching-id'; + mockElement2.className = 'hide-show-btn btn-success'; + + document.body.appendChild(mockElement1); + document.body.appendChild(mockElement2); + + const hiddenPages = component.getHiddenPages(); + expect(hiddenPages).toEqual([1]); + + document.body.removeChild(mockElement1); + document.body.removeChild(mockElement2); + }); + }); + + describe('Create Hidden Version of Attachment', () => { + it('should create a hidden version of the PDF by removing specified pages', async () => { + component.attachmentUnit.set({ + attachment: { name: 'example' }, }); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.attachmentUnitSub).toBeDefined(); - const spySub = jest.spyOn(component.attachmentUnitSub, 'unsubscribe'); - component.ngOnDestroy(); - expect(spySub).toHaveBeenCalled(); + + const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); + mockBlob.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); + component.currentPdfBlob.set(mockBlob); + + const mockPdfDoc = { + removePage: jest.fn(), + save: jest.fn().mockResolvedValue(new Uint8Array([1, 2, 3])), + }; + (PDFDocument.load as jest.Mock).mockResolvedValue(mockPdfDoc); + + const hiddenPages = [1, 3, 5]; + const result = await component.createHiddenVersionOfAttachment(hiddenPages); + + expect(PDFDocument.load).toHaveBeenCalledWith(await mockBlob.arrayBuffer()); + expect(mockPdfDoc.removePage).toHaveBeenCalledWith(4); + expect(mockPdfDoc.removePage).toHaveBeenCalledWith(2); + expect(mockPdfDoc.removePage).toHaveBeenCalledWith(0); + expect(mockPdfDoc.removePage).toHaveBeenCalledTimes(3); + expect(mockPdfDoc.save).toHaveBeenCalled(); + + expect(result).toBeInstanceOf(File); + expect(result?.name).toBe('example.pdf'); + expect(result?.type).toBe('application/pdf'); + }); + + it('should handle errors and display an alert when PDF processing fails', async () => { + component.attachmentUnit.set({ + attachment: { name: 'example' }, + }); + + const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); + mockBlob.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8)); + component.currentPdfBlob.set(mockBlob); + + (PDFDocument.load as jest.Mock).mockRejectedValue(new Error('PDF processing failed')); + + const alertServiceSpy = jest.spyOn(component.alertService, 'error'); + const hiddenPages = [1, 3, 5]; + const result = await component.createHiddenVersionOfAttachment(hiddenPages); + + expect(PDFDocument.load).toHaveBeenCalledWith(await mockBlob.arrayBuffer()); + expect(alertServiceSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: 'PDF processing failed' }); + expect(result).toBeUndefined(); }); }); @@ -235,35 +340,19 @@ describe('PdfPreviewComponent', () => { expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); }); - it('should update attachment unit successfully and show success alert', () => { + it('should display success alert and navigate when finalHiddenPages is empty', async () => { component.attachment.set(undefined); - component.attachmentUnit.set({ - id: 1, - lecture: { id: 1 }, - attachment: { id: 1, version: 1 }, - }); - attachmentUnitServiceMock.update.mockReturnValue(of({})); - - component.updateAttachmentWithFile(); - - expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); - expect(alertServiceMock.success).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); - }); + component.attachmentUnit.set({ id: 1, lecture: { id: 1 }, attachment: { id: 2, name: 'Attachment Name', version: 1 } }); + component.course.set({ id: 1 }); - it('should handle errors when updating an attachment unit fails', () => { - component.attachment.set(undefined); - component.attachmentUnit.set({ - id: 1, - lecture: { id: 1 }, - attachment: { id: 1, version: 1 }, - }); - const errorResponse = { message: 'Update failed' }; - attachmentUnitServiceMock.update.mockReturnValue(throwError(() => errorResponse)); + attachmentUnitServiceMock.update.mockReturnValue(of({})); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'success'); + const routerSpy = jest.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true); component.updateAttachmentWithFile(); - expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData)); - expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' }); + expect(alertServiceSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateSuccess'); + expect(routerSpy).toHaveBeenCalledWith(['course-management', 1, 'lectures', 1, 'unit-management']); }); }); @@ -369,6 +458,35 @@ describe('PdfPreviewComponent', () => { }); }); + describe('Update Hidden Pages', () => { + it('should not change hidden pages if no pages are deleted', () => { + component.hiddenPages.set(new Set([1, 3, 5])); + + const pagesToDelete: number[] = []; + component.updateHiddenPages(pagesToDelete); + + expect(component.hiddenPages()).toEqual(new Set([1, 3, 5])); + }); + + it('should handle an empty set of hidden pages', () => { + component.hiddenPages.set(new Set()); + + const pagesToDelete = [0, 1, 2]; + component.updateHiddenPages(pagesToDelete); + + expect(component.hiddenPages()).toEqual(new Set()); + }); + + it('should not adjust hidden pages that are not affected by the deletion', () => { + component.hiddenPages.set(new Set([4, 6, 8])); + + const pagesToDelete = [0, 1]; + component.updateHiddenPages(pagesToDelete); + + expect(component.hiddenPages()).toEqual(new Set([2, 4, 6])); + }); + }); + describe('Attachment Deletion', () => { it('should delete the attachment and navigate to attachments on success', () => { component.attachment.set({ id: 1, lecture: { id: 2 } });