import { DatePipe } from '@angular/common' import { SimpleChange } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { Subject, of, throwError } from 'rxjs' import { DocumentVersionInfo } from 'src/app/data/document' import { DocumentService } from 'src/app/services/rest/document.service' import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { UploadState, WebsocketStatusService, } from 'src/app/services/websocket-status.service' import { DocumentVersionDropdownComponent } from './document-version-dropdown.component' describe('DocumentVersionDropdownComponent', () => { let component: DocumentVersionDropdownComponent let fixture: ComponentFixture let documentService: jest.Mocked< Pick > let toastService: jest.Mocked> let finished$: Subject<{ taskId: string }> let failed$: Subject<{ taskId: string; message?: string }> beforeEach(async () => { finished$ = new Subject<{ taskId: string }>() failed$ = new Subject<{ taskId: string; message?: string }>() documentService = { deleteVersion: jest.fn(), getVersions: jest.fn(), uploadVersion: jest.fn(), } toastService = { showError: jest.fn(), showInfo: jest.fn(), } await TestBed.configureTestingModule({ imports: [ DocumentVersionDropdownComponent, NgxBootstrapIconsModule.pick(allIcons), ], providers: [ DatePipe, { provide: DocumentService, useValue: documentService, }, { provide: SettingsService, useValue: { get: () => null, }, }, { provide: ToastService, useValue: toastService, }, { provide: WebsocketStatusService, useValue: { onDocumentConsumptionFinished: () => finished$, onDocumentConsumptionFailed: () => failed$, }, }, ], }).compileComponents() fixture = TestBed.createComponent(DocumentVersionDropdownComponent) component = fixture.componentInstance component.documentId = 3 component.selectedVersionId = 3 component.versions = [ { id: 3, is_root: true, checksum: 'aaaa', }, { id: 10, is_root: false, checksum: 'bbbb', }, ] fixture.detectChanges() }) it('selectVersion should emit the selected id', () => { const emitSpy = jest.spyOn(component.versionSelected, 'emit') component.selectVersion(10) expect(emitSpy).toHaveBeenCalledWith(10) }) it('deleteVersion should refresh versions and select fallback when deleting current selection', () => { const updatedVersions: DocumentVersionInfo[] = [ { id: 3, is_root: true, checksum: 'aaaa' }, { id: 20, is_root: false, checksum: 'cccc' }, ] component.selectedVersionId = 10 documentService.deleteVersion.mockReturnValue( of({ result: 'deleted', current_version_id: 3 }) ) documentService.getVersions.mockReturnValue( of({ id: 3, versions: updatedVersions } as any) ) const versionsEmitSpy = jest.spyOn(component.versionsUpdated, 'emit') const selectedEmitSpy = jest.spyOn(component.versionSelected, 'emit') component.deleteVersion(10) expect(documentService.deleteVersion).toHaveBeenCalledWith(3, 10) expect(documentService.getVersions).toHaveBeenCalledWith(3) expect(versionsEmitSpy).toHaveBeenCalledWith(updatedVersions) expect(selectedEmitSpy).toHaveBeenCalledWith(3) }) it('deleteVersion should show an error toast on failure', () => { const error = new Error('delete failed') documentService.deleteVersion.mockReturnValue(throwError(() => error)) component.deleteVersion(10) expect(toastService.showError).toHaveBeenCalledWith( 'Error deleting version', error ) }) it('onVersionFileSelected should upload and update versions after websocket success', () => { const versions: DocumentVersionInfo[] = [ { id: 3, is_root: true, checksum: 'aaaa' }, { id: 20, is_root: false, checksum: 'cccc' }, ] const file = new File(['test'], 'new-version.pdf', { type: 'application/pdf', }) const input = document.createElement('input') Object.defineProperty(input, 'files', { value: [file] }) component.newVersionLabel = ' Updated scan ' documentService.uploadVersion.mockReturnValue( of({ task_id: 'task-1' } as any) ) documentService.getVersions.mockReturnValue(of({ id: 3, versions } as any)) const versionsEmitSpy = jest.spyOn(component.versionsUpdated, 'emit') const selectedEmitSpy = jest.spyOn(component.versionSelected, 'emit') component.onVersionFileSelected({ target: input } as Event) finished$.next({ taskId: 'task-1' }) expect(documentService.uploadVersion).toHaveBeenCalledWith( 3, file, 'Updated scan' ) expect(toastService.showInfo).toHaveBeenCalled() expect(documentService.getVersions).toHaveBeenCalledWith(3) expect(versionsEmitSpy).toHaveBeenCalledWith(versions) expect(selectedEmitSpy).toHaveBeenCalledWith(20) expect(component.newVersionLabel).toEqual('') expect(component.versionUploadState).toEqual(UploadState.Idle) expect(component.versionUploadError).toBeNull() }) it('onVersionFileSelected should set failed state after websocket failure', () => { const file = new File(['test'], 'new-version.pdf', { type: 'application/pdf', }) const input = document.createElement('input') Object.defineProperty(input, 'files', { value: [file] }) documentService.uploadVersion.mockReturnValue(of('task-1')) component.onVersionFileSelected({ target: input } as Event) failed$.next({ taskId: 'task-1', message: 'processing failed' }) expect(component.versionUploadState).toEqual(UploadState.Failed) expect(component.versionUploadError).toEqual('processing failed') expect(documentService.getVersions).not.toHaveBeenCalled() }) it('onVersionFileSelected should fail when backend response has no task id', () => { const file = new File(['test'], 'new-version.pdf', { type: 'application/pdf', }) const input = document.createElement('input') Object.defineProperty(input, 'files', { value: [file] }) documentService.uploadVersion.mockReturnValue(of({} as any)) component.onVersionFileSelected({ target: input } as Event) expect(component.versionUploadState).toEqual(UploadState.Failed) expect(component.versionUploadError).toEqual('Missing task ID.') expect(documentService.getVersions).not.toHaveBeenCalled() }) it('onVersionFileSelected should show error when upload request fails', () => { const file = new File(['test'], 'new-version.pdf', { type: 'application/pdf', }) const input = document.createElement('input') Object.defineProperty(input, 'files', { value: [file] }) const error = new Error('upload failed') documentService.uploadVersion.mockReturnValue(throwError(() => error)) component.onVersionFileSelected({ target: input } as Event) expect(component.versionUploadState).toEqual(UploadState.Failed) expect(component.versionUploadError).toEqual('upload failed') expect(toastService.showError).toHaveBeenCalledWith( 'Error uploading new version', error ) }) it('ngOnChanges should clear upload status on document switch', () => { component.versionUploadState = UploadState.Failed component.versionUploadError = 'something failed' component.ngOnChanges({ documentId: new SimpleChange(3, 4, false), }) expect(component.versionUploadState).toEqual(UploadState.Idle) expect(component.versionUploadError).toBeNull() }) })