Skip to content

Commit

Permalink
Fix client and server tests
Browse files Browse the repository at this point in the history
  • Loading branch information
eceeeren committed Dec 20, 2024
1 parent c18def3 commit e1d937d
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ public ResponseEntity<byte[]> getLecturePdfAttachmentsMerged(@PathVariable Long
* @return The requested file, 403 if the logged-in user is not allowed to access it, or 404 if the file doesn't exist
*/
@GetMapping("files/attachments/attachment-unit/{attachmentUnitId}/*")
@EnforceAtLeastInstructor
@EnforceAtLeastEditor
public ResponseEntity<byte[]> getAttachmentUnitAttachment(@PathVariable Long attachmentUnitId) {
log.debug("REST request to get the file for attachment unit {} for students", attachmentUnitId);
AttachmentUnit attachmentUnit = attachmentUnitRepository.findByIdElseThrow(attachmentUnitId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { ArtemisSharedModule } from 'app/shared/shared.module';
import { onError } from 'app/shared/util/global.utils';
import { AlertService } from 'app/core/util/alert.service';
import { PdfPreviewEnlargedCanvasComponent } from 'app/lecture/pdf-preview/pdf-preview-enlarged-canvas/pdf-preview-enlarged-canvas.component';
import { clone } from 'lodash-es';
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';

@Component({
Expand Down Expand Up @@ -46,7 +45,7 @@ export class PdfPreviewThumbnailGridComponent implements OnChanges {

ngOnChanges(changes: SimpleChanges): void {
if (changes['hiddenPages']) {
this.newHiddenPages.set(clone(this.hiddenPages()!));
this.newHiddenPages.set(new Set(this.hiddenPages()!));
}
if (changes['currentPdfUrl']) {
this.loadPdf(this.currentPdfUrl()!, this.appendFile()!);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,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<number>();
this.hiddenPages().forEach((hiddenPage) => {
// Adjust hiddenPage based on the deleted pages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 { ElementRef, InputSignal, Signal, SimpleChanges } from '@angular/core';

jest.mock('pdfjs-dist', () => {
return {
Expand Down Expand Up @@ -58,45 +59,104 @@ describe('PdfPreviewThumbnailGridComponent', () => {
jest.clearAllMocks();
});

it('should update newHiddenPages when hiddenPages changes', () => {
const initialHiddenPages = new Set<number>([1, 2, 3]);
const updatedHiddenPages = new Set<number>([4, 5, 6]);

component.hiddenPages = jest.fn(() => updatedHiddenPages) as unknown as InputSignal<Set<number>>;

const changes: SimpleChanges = {
hiddenPages: {
currentValue: updatedHiddenPages,
previousValue: initialHiddenPages,
firstChange: false,
isFirstChange: () => false,
},
};
component.ngOnChanges(changes);

expect(component.newHiddenPages()).toEqual(new Set(updatedHiddenPages));
});

it('should load the PDF when currentPdfUrl changes', async () => {
const mockLoadPdf = jest.spyOn(component, 'loadPdf').mockResolvedValue();
const initialPdfUrl = 'initial.pdf';
const updatedPdfUrl = 'updated.pdf';

let currentPdfUrlValue = initialPdfUrl;
component.currentPdfUrl = jest.fn(() => currentPdfUrlValue) as unknown as InputSignal<string>;
component.appendFile = jest.fn(() => false) as unknown as InputSignal<boolean>;

currentPdfUrlValue = updatedPdfUrl;
const changes: SimpleChanges = {
currentPdfUrl: {
currentValue: updatedPdfUrl,
previousValue: initialPdfUrl,
firstChange: false,
isFirstChange: () => false,
},
};
await component.ngOnChanges(changes);

expect(mockLoadPdf).toHaveBeenCalledWith(updatedPdfUrl, false);
});

it('should load PDF and render pages', async () => {
const spyCreateCanvas = jest.spyOn(component, 'createCanvas');
const spyCreateCanvasContainer = jest.spyOn(component, 'createCanvasContainer');
const spyCreateCanvas = jest.spyOn(component as any, 'createCanvas');

await component.loadOrAppendPdf('fake-url');
await component.loadPdf('fake-url', false);

expect(spyCreateCanvas).toHaveBeenCalled();
expect(spyCreateCanvasContainer).toHaveBeenCalled();
expect(component.totalPages()).toBe(1);
expect(component.totalPagesArray().size).toBe(1);
});

it('should toggle enlarged view state', () => {
const mockCanvas = document.createElement('canvas');
component.displayEnlargedCanvas(mockCanvas);
component.originalCanvas.set(mockCanvas);
component.isEnlargedView.set(true);
expect(component.isEnlargedView()).toBeTruthy();

component.isEnlargedView.set(false);
expect(component.isEnlargedView()).toBeFalsy();
});

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', () => {
fixture.componentRef.setInput('hiddenPages', new Set([1]));
component.toggleVisibility(1, new MouseEvent('click'));
expect(component.hiddenPages()!.has(1)).toBeFalse();

container.dispatchEvent(new Event('mouseenter'));
expect(overlay!.style.opacity).toBe('1');
component.toggleVisibility(2, new MouseEvent('click'));
expect(component.hiddenPages()!.has(2)).toBeTrue();
});

container.dispatchEvent(new Event('mouseleave'));
expect(overlay!.style.opacity).toBe('0');
it('should select and deselect pages', () => {
component.togglePageSelection(1, { target: { checked: true } } as unknown as Event);
expect(component.selectedPages().has(1)).toBeTrue();

component.togglePageSelection(1, { target: { checked: false } } as unknown as Event);
expect(component.selectedPages().has(1)).toBeFalse();
});

it('should handle click event on overlay to trigger displayEnlargedCanvas', () => {
const displayEnlargedCanvasSpy = jest.spyOn(component, 'displayEnlargedCanvas');
it('should handle enlarged view correctly for a specific page', () => {
const mockCanvas = document.createElement('canvas');
const container = component.createCanvasContainer(mockCanvas, 1);
const overlay = container.querySelector('div');

overlay!.dispatchEvent(new Event('click'));
expect(displayEnlargedCanvasSpy).toHaveBeenCalledWith(mockCanvas);
const container = document.createElement('div');
container.id = 'pdf-page-1';
container.appendChild(mockCanvas);

component.pdfContainer = jest.fn(() => ({
nativeElement: {
querySelector: jest.fn((selector: string) => {
if (selector === '#pdf-page-1 canvas') {
return mockCanvas;
}
return null;
}),
},
})) as unknown as Signal<ElementRef<HTMLDivElement>>;

component.displayEnlargedCanvas(1);

expect(component.originalCanvas()).toBe(mockCanvas);
expect(component.isEnlargedView()).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service';
import { MAX_FILE_SIZE } from 'app/shared/constants/input.constants';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { of, throwError } from 'rxjs';
import { AttachmentService } from 'app/lecture/attachment.service';
Expand Down Expand Up @@ -72,6 +72,7 @@ describe('PdfPreviewComponent', () => {
getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))),
update: jest.fn().mockReturnValue(of({})),
delete: jest.fn().mockReturnValue(of({})),
getHiddenSlides: jest.fn().mockReturnValue(of([1, 2, 3])),
};
lectureUnitServiceMock = {
delete: jest.fn().mockReturnValue(of({})),
Expand Down Expand Up @@ -124,15 +125,15 @@ describe('PdfPreviewComponent', () => {
expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled();
});

it('should load attachment unit file and verify service calls when attachment unit data is available', () => {
it('should load attachment unit file and verify service calls when attachment unit data is available', fakeAsync(() => {
routeMock.data = of({
course: { id: 1, name: 'Example Course' },
attachmentUnit: { id: 1, name: 'Chapter 1' },
attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } },
});
component.ngOnInit();
tick();
expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1);
expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled();
});
}));

it('should handle errors and trigger alert when loading an attachment file fails', () => {
const errorResponse = new HttpErrorResponse({
Expand All @@ -151,10 +152,10 @@ describe('PdfPreviewComponent', () => {
expect(alertServiceSpy).toHaveBeenCalled();
});

it('should handle errors and trigger alert when loading an attachment unit file fails', () => {
it('should handle errors and trigger alert when loading an attachment unit file fails', fakeAsync(() => {
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,
Expand All @@ -167,10 +168,10 @@ describe('PdfPreviewComponent', () => {
const alertServiceSpy = jest.spyOn(alertServiceMock, 'error');

component.ngOnInit();
fixture.detectChanges();
tick();

expect(alertServiceSpy).toHaveBeenCalled();
});
}));
});

describe('Unsubscribing from Observables', () => {
Expand All @@ -180,18 +181,18 @@ describe('PdfPreviewComponent', () => {
expect(spySub).toHaveBeenCalled();
});

it('should unsubscribe attachmentUnit subscription during component destruction', () => {
it('should unsubscribe attachmentUnit subscription during component destruction', fakeAsync(() => {
routeMock.data = of({
course: { id: 1, name: 'Example Course' },
attachmentUnit: { id: 1, name: 'Chapter 1' },
attachmentUnit: { id: 1, name: 'Chapter 1', lecture: { id: 1 } },
});
component.ngOnInit();
fixture.detectChanges();
tick();
expect(component.attachmentUnitSub).toBeDefined();
const spySub = jest.spyOn(component.attachmentUnitSub, 'unsubscribe');
component.ngOnDestroy();
expect(spySub).toHaveBeenCalled();
});
}));
});

describe('File Input Handling', () => {
Expand All @@ -206,6 +207,40 @@ describe('PdfPreviewComponent', () => {
});
});

describe('Get Hidden Pages', () => {
it('should return an array of hidden page numbers based on button IDs', () => {
document.body.innerHTML = `
<button id="hide-show-button-1" class="hide-show-btn btn-success"></button>
<button id="hide-show-button-3" class="hide-show-btn btn-success"></button>
<button id="hide-show-button-5" class="hide-show-btn btn-success"></button>
`;

const hiddenPages = component.getHiddenPages();
expect(hiddenPages).toEqual([1, 3, 5]);
});

it('should return an empty array if no matching elements are found', () => {
document.body.innerHTML = `
<button id="other-button-1" class="btn btn-danger"></button>
<button id="random-button-2" class="btn btn-primary"></button>
`;

const hiddenPages = component.getHiddenPages();
expect(hiddenPages).toEqual([]);
});

it('should ignore elements without valid IDs', () => {
document.body.innerHTML = `
<button id="hide-show-button-1" class="hide-show-btn btn-success"></button>
<button id="hide-show-button-invalid" class="hide-show-btn btn-success"></button>
<button id="hide-show-button-2" class="hide-show-btn btn-success"></button>
`;

const hiddenPages = component.getHiddenPages();
expect(hiddenPages).toEqual([1, 2]);
});
});

describe('Attachment Updating', () => {
it('should update attachment successfully and show success alert', () => {
component.attachment.set({ id: 1, version: 1 });
Expand Down Expand Up @@ -234,37 +269,6 @@ describe('PdfPreviewComponent', () => {
expect(attachmentServiceMock.update).toHaveBeenCalled();
expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' });
});

it('should update attachment unit successfully and show success alert', () => {
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');
});

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));

component.updateAttachmentWithFile();

expect(attachmentUnitServiceMock.update).toHaveBeenCalledWith(1, 1, expect.any(FormData));
expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Update failed' });
});
});

describe('PDF Merging', () => {
Expand Down Expand Up @@ -418,4 +422,63 @@ describe('PdfPreviewComponent', () => {
expect(alertServiceMock.error).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.attachmentUpdateError', { error: 'Deletion failed' });
});
});

describe('Create Student Version of Attachment', () => {
it('should create a new PDF file with specified hidden pages removed', async () => {
const hiddenPages = [2, 4];
const mockPdfBytes = new Uint8Array([1, 2, 3, 4]).buffer;
const mockFileName = 'test-file';
const updatedPdfBytes = new Uint8Array([5, 6, 7]).buffer;

const mockAttachmentUnit = {
attachment: {
name: mockFileName,
},
};

const hiddenPdfDoc = {
removePage: jest.fn(),
save: jest.fn().mockResolvedValue(updatedPdfBytes),
};

PDFDocument.load = jest.fn().mockResolvedValue(hiddenPdfDoc);

component.attachmentUnit.set(mockAttachmentUnit as any);
component.currentPdfBlob.set(new Blob([mockPdfBytes], { type: 'application/pdf' }));
component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(mockPdfBytes);

const result = await component.createStudentVersionOfAttachment(hiddenPages);

expect(PDFDocument.load).toHaveBeenCalledWith(mockPdfBytes);
expect(hiddenPdfDoc.removePage).toHaveBeenCalledTimes(hiddenPages.length);
expect(hiddenPdfDoc.removePage).toHaveBeenCalledWith(1); // 2-1 (zero-indexed)
expect(hiddenPdfDoc.removePage).toHaveBeenCalledWith(3); // 4-1 (zero-indexed)
expect(hiddenPdfDoc.save).toHaveBeenCalled();
expect(result).toBeInstanceOf(File);
expect(result!.name).toBe(`${mockFileName}.pdf`);
expect(result!.type).toBe('application/pdf');
});

it('should handle errors and call alertService.error if something goes wrong', async () => {
const hiddenPages = [2, 4];
const errorMessage = 'Failed to load PDF';
PDFDocument.load = jest.fn().mockRejectedValue(new Error(errorMessage));

const alertServiceErrorSpy = jest.spyOn(alertServiceMock, 'error');

const mockAttachmentUnit = {
attachment: {
name: 'test-file',
},
};
component.attachmentUnit.set(mockAttachmentUnit as any);
component.currentPdfBlob.set(new Blob(['existing pdf'], { type: 'application/pdf' }));
component.currentPdfBlob()!.arrayBuffer = jest.fn().mockResolvedValue(new ArrayBuffer(8));

const result = await component.createStudentVersionOfAttachment(hiddenPages);

expect(alertServiceErrorSpy).toHaveBeenCalledWith('artemisApp.attachment.pdfPreview.pageDeleteError', { error: errorMessage });
expect(result).toBeUndefined();
});
});
});

0 comments on commit e1d937d

Please sign in to comment.