]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Frontend tests
authorshamoon <4887959+shamoon@users.noreply.github.com>
Thu, 6 Nov 2025 04:07:50 +0000 (20:07 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Thu, 6 Nov 2025 04:07:50 +0000 (20:07 -0800)
src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts
src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts

index 7dc2c89570667534c219d84f55a4bfd83fa9262c..8c2171ff7b6a7ef29147a4087981e9b06e0cefb6 100644 (file)
@@ -1,7 +1,149 @@
+import { Clipboard } from '@angular/cdk/clipboard'
+import {
+  ComponentFixture,
+  TestBed,
+  fakeAsync,
+  tick,
+} from '@angular/core/testing'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
+import { FileVersion } from 'src/app/data/share-link'
+import {
+  ShareLinkBundleStatus,
+  ShareLinkBundleSummary,
+} from 'src/app/data/share-link-bundle'
+import { ToastService } from 'src/app/services/toast.service'
+import { environment } from 'src/environments/environment'
+import { ShareLinkBundleDialogComponent } from './share-link-bundle-dialog.component'
+
+class MockToastService {
+  showInfo = jest.fn()
+  showError = jest.fn()
+}
+
 describe('ShareLinkBundleDialogComponent', () => {
-  it('is pending implementation', () => {
-    pending(
-      'ShareLinkBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
+  let component: ShareLinkBundleDialogComponent
+  let fixture: ComponentFixture<ShareLinkBundleDialogComponent>
+  let clipboard: Clipboard
+  let toastService: MockToastService
+  let activeModal: NgbActiveModal
+  let originalApiBaseUrl: string
+
+  beforeEach(() => {
+    originalApiBaseUrl = environment.apiBaseUrl
+    toastService = new MockToastService()
+
+    TestBed.configureTestingModule({
+      imports: [
+        ShareLinkBundleDialogComponent,
+        NgxBootstrapIconsModule.pick(allIcons),
+      ],
+      providers: [
+        NgbActiveModal,
+        { provide: ToastService, useValue: toastService },
+      ],
+    })
+
+    fixture = TestBed.createComponent(ShareLinkBundleDialogComponent)
+    component = fixture.componentInstance
+    clipboard = TestBed.inject(Clipboard)
+    activeModal = TestBed.inject(NgbActiveModal)
+    fixture.detectChanges()
+  })
+
+  afterEach(() => {
+    jest.clearAllTimers()
+    environment.apiBaseUrl = originalApiBaseUrl
+  })
+
+  it('builds payload and emits confirm on submit', () => {
+    const confirmSpy = jest.spyOn(component.confirmClicked, 'emit')
+    component.documents = [
+      { id: 1, title: 'Doc 1' } as any,
+      { id: 2, title: 'Doc 2' } as any,
+    ]
+    component.form.setValue({
+      shareArchiveVersion: false,
+      expirationDays: 3,
+    })
+
+    component.submit()
+
+    expect(component.payload).toEqual({
+      document_ids: [1, 2],
+      file_version: FileVersion.Original,
+      expiration_days: 3,
+    })
+    expect(component.buttonsEnabled).toBe(false)
+    expect(confirmSpy).toHaveBeenCalled()
+  })
+
+  it('ignores submit when bundle already created', () => {
+    component.createdBundle = { id: 1 } as ShareLinkBundleSummary
+    const confirmSpy = jest.spyOn(component, 'confirm')
+    component.submit()
+    expect(confirmSpy).not.toHaveBeenCalled()
+  })
+
+  it('limits preview to ten documents', () => {
+    const docs = Array.from({ length: 12 }).map((_, index) => ({
+      id: index + 1,
+    }))
+    component.documents = docs as any
+
+    expect(component.selectionCount).toBe(12)
+    expect(component.documentPreview).toHaveLength(10)
+    expect(component.documentPreview[0].id).toBe(1)
+  })
+
+  it('copies share link and resets state after timeout', fakeAsync(() => {
+    const copySpy = jest.spyOn(clipboard, 'copy').mockReturnValue(true)
+    const bundle = {
+      slug: 'bundle-slug',
+      status: ShareLinkBundleStatus.Ready,
+    } as ShareLinkBundleSummary
+
+    component.copy(bundle)
+
+    expect(copySpy).toHaveBeenCalledWith(component.getShareUrl(bundle))
+    expect(component.copied).toBe(true)
+    expect(toastService.showInfo).toHaveBeenCalled()
+
+    tick(3000)
+    expect(component.copied).toBe(false)
+  }))
+
+  it('generates share URLs based on API base URL', () => {
+    environment.apiBaseUrl = 'https://example.com/api/'
+    expect(
+      component.getShareUrl({ slug: 'abc' } as ShareLinkBundleSummary)
+    ).toBe('https://example.com/share/abc')
+  })
+
+  it('opens manage dialog when callback provided', () => {
+    const manageSpy = jest.fn()
+    component.onOpenManage = manageSpy
+    component.openManage()
+    expect(manageSpy).toHaveBeenCalled()
+  })
+
+  it('falls back to cancel when manage callback missing', () => {
+    const cancelSpy = jest.spyOn(component, 'cancel')
+    component.onOpenManage = undefined
+    component.openManage()
+    expect(cancelSpy).toHaveBeenCalled()
+  })
+
+  it('maps status and file version labels', () => {
+    expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain(
+      'Processing'
     )
+    expect(component.fileVersionLabel(FileVersion.Archive)).toContain('Archive')
+  })
+
+  it('closes dialog when cancel invoked', () => {
+    const closeSpy = jest.spyOn(activeModal, 'close')
+    component.cancel()
+    expect(closeSpy).toHaveBeenCalled()
   })
 })
index 3e86aa2250c018120c833d537a4b2ef492a8c261..1784f6ff6e2d2cd93ab5b7aa659bbefe7a319c79 100644 (file)
@@ -1,7 +1,250 @@
+import { Clipboard } from '@angular/cdk/clipboard'
+import {
+  ComponentFixture,
+  TestBed,
+  fakeAsync,
+  tick,
+} from '@angular/core/testing'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
+import { of, throwError } from 'rxjs'
+import { FileVersion } from 'src/app/data/share-link'
+import {
+  ShareLinkBundleStatus,
+  ShareLinkBundleSummary,
+} from 'src/app/data/share-link-bundle'
+import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
+import { ToastService } from 'src/app/services/toast.service'
+import { environment } from 'src/environments/environment'
+import { ShareLinkBundleManageDialogComponent } from './share-link-bundle-manage-dialog.component'
+
+class MockShareLinkBundleService {
+  listAllBundles = jest.fn()
+  delete = jest.fn()
+  rebuildBundle = jest.fn()
+}
+
+class MockToastService {
+  showInfo = jest.fn()
+  showError = jest.fn()
+}
+
 describe('ShareLinkBundleManageDialogComponent', () => {
-  it('is pending implementation', () => {
-    pending(
-      'ShareLinkBundleManageDialogComponent tests will be implemented once the dialog logic is finalized.'
-    )
+  let component: ShareLinkBundleManageDialogComponent
+  let fixture: ComponentFixture<ShareLinkBundleManageDialogComponent>
+  let service: MockShareLinkBundleService
+  let toastService: MockToastService
+  let clipboard: Clipboard
+  let activeModal: NgbActiveModal
+  let originalApiBaseUrl: string
+
+  beforeEach(() => {
+    service = new MockShareLinkBundleService()
+    toastService = new MockToastService()
+    originalApiBaseUrl = environment.apiBaseUrl
+
+    service.listAllBundles.mockReturnValue(of([]))
+    service.delete.mockReturnValue(of(true))
+    service.rebuildBundle.mockReturnValue(of(sampleBundle()))
+
+    TestBed.configureTestingModule({
+      imports: [
+        ShareLinkBundleManageDialogComponent,
+        NgxBootstrapIconsModule.pick(allIcons),
+      ],
+      providers: [
+        NgbActiveModal,
+        { provide: ShareLinkBundleService, useValue: service },
+        { provide: ToastService, useValue: toastService },
+      ],
+    })
+
+    fixture = TestBed.createComponent(ShareLinkBundleManageDialogComponent)
+    component = fixture.componentInstance
+    clipboard = TestBed.inject(Clipboard)
+    activeModal = TestBed.inject(NgbActiveModal)
+  })
+
+  afterEach(() => {
+    component.ngOnDestroy()
+    fixture.destroy()
+    environment.apiBaseUrl = originalApiBaseUrl
+    jest.clearAllMocks()
   })
+
+  const sampleBundle = (overrides: Partial<ShareLinkBundleSummary> = {}) =>
+    ({
+      id: 1,
+      slug: 'bundle-slug',
+      created: new Date().toISOString(),
+      document_count: 1,
+      documents: [1],
+      status: ShareLinkBundleStatus.Pending,
+      file_version: FileVersion.Archive,
+      ...overrides,
+    }) as ShareLinkBundleSummary
+
+  it('loads bundles on init and polls periodically', fakeAsync(() => {
+    const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })]
+    service.listAllBundles.mockReset()
+    service.listAllBundles
+      .mockReturnValueOnce(of(bundles))
+      .mockReturnValue(of(bundles))
+
+    fixture.detectChanges()
+    tick()
+
+    expect(service.listAllBundles).toHaveBeenCalledTimes(1)
+    expect(component.bundles).toEqual(bundles)
+    expect(component.loading).toBe(false)
+    expect(component.error).toBeNull()
+
+    tick(5000)
+    expect(service.listAllBundles).toHaveBeenCalledTimes(2)
+  }))
+
+  it('handles errors when loading bundles', fakeAsync(() => {
+    service.listAllBundles.mockReset()
+    service.listAllBundles
+      .mockReturnValueOnce(throwError(() => new Error('load fail')))
+      .mockReturnValue(of([]))
+
+    fixture.detectChanges()
+    tick()
+
+    expect(component.error).toContain('Failed to load share link bundles.')
+    expect(toastService.showError).toHaveBeenCalled()
+    expect(component.loading).toBe(false)
+
+    tick(5000)
+    expect(service.listAllBundles).toHaveBeenCalledTimes(2)
+  }))
+
+  it('copies bundle links when ready', fakeAsync(() => {
+    jest.spyOn(clipboard, 'copy').mockReturnValue(true)
+    fixture.detectChanges()
+    tick()
+
+    const readyBundle = sampleBundle({
+      slug: 'ready-slug',
+      status: ShareLinkBundleStatus.Ready,
+    })
+    component.copy(readyBundle)
+
+    expect(clipboard.copy).toHaveBeenCalledWith(
+      component.getShareUrl(readyBundle)
+    )
+    expect(component.copiedSlug).toBe('ready-slug')
+    expect(toastService.showInfo).toHaveBeenCalled()
+
+    tick(3000)
+    expect(component.copiedSlug).toBeNull()
+  }))
+
+  it('ignores copy requests for non-ready bundles', fakeAsync(() => {
+    const copySpy = jest.spyOn(clipboard, 'copy')
+    fixture.detectChanges()
+    tick()
+    component.copy(sampleBundle({ status: ShareLinkBundleStatus.Pending }))
+    expect(copySpy).not.toHaveBeenCalled()
+  }))
+
+  it('deletes bundles and refreshes list', fakeAsync(() => {
+    service.listAllBundles.mockReturnValue(of([]))
+    service.delete.mockReturnValue(of(true))
+
+    fixture.detectChanges()
+    tick()
+
+    component.delete(sampleBundle())
+    tick()
+
+    expect(service.delete).toHaveBeenCalled()
+    expect(toastService.showInfo).toHaveBeenCalledWith(
+      expect.stringContaining('deleted.')
+    )
+    expect(service.listAllBundles).toHaveBeenCalledTimes(2)
+    expect(component.loading).toBe(false)
+  }))
+
+  it('handles delete errors gracefully', fakeAsync(() => {
+    service.listAllBundles.mockReturnValue(of([]))
+    service.delete.mockReturnValue(throwError(() => new Error('delete fail')))
+
+    fixture.detectChanges()
+    tick()
+
+    component.delete(sampleBundle())
+    tick()
+
+    expect(toastService.showError).toHaveBeenCalled()
+    expect(component.loading).toBe(false)
+  }))
+
+  it('retries bundle build and replaces existing entry', fakeAsync(() => {
+    service.listAllBundles.mockReturnValue(of([]))
+    const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready })
+    service.rebuildBundle.mockReturnValue(of(updated))
+
+    fixture.detectChanges()
+    tick()
+
+    component.bundles = [sampleBundle()]
+    component.retry(component.bundles[0])
+    tick()
+
+    expect(service.rebuildBundle).toHaveBeenCalledWith(updated.id)
+    expect(component.bundles[0].status).toBe(ShareLinkBundleStatus.Ready)
+    expect(toastService.showInfo).toHaveBeenCalled()
+  }))
+
+  it('adds new bundle when retry returns unknown entry', fakeAsync(() => {
+    service.listAllBundles.mockReturnValue(of([]))
+    service.rebuildBundle.mockReturnValue(
+      of(sampleBundle({ id: 99, slug: 'new-slug' }))
+    )
+
+    fixture.detectChanges()
+    tick()
+
+    component.bundles = [sampleBundle()]
+    component.retry({ id: 99 } as ShareLinkBundleSummary)
+    tick()
+
+    expect(component.bundles.find((bundle) => bundle.id === 99)).toBeTruthy()
+  }))
+
+  it('handles retry errors', fakeAsync(() => {
+    service.listAllBundles.mockReturnValue(of([]))
+    service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail')))
+
+    fixture.detectChanges()
+    tick()
+
+    component.retry(sampleBundle())
+    tick()
+
+    expect(toastService.showError).toHaveBeenCalled()
+  }))
+
+  it('maps helpers and closes dialog', fakeAsync(() => {
+    service.listAllBundles.mockReturnValue(of([]))
+    fixture.detectChanges()
+    tick()
+
+    expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain(
+      'Processing'
+    )
+    expect(component.fileVersionLabel(FileVersion.Original)).toContain(
+      'Original'
+    )
+
+    environment.apiBaseUrl = 'https://example.com/api/'
+    const url = component.getShareUrl(sampleBundle({ slug: 'sluggy' }))
+    expect(url).toBe('https://example.com/share/sluggy')
+
+    const closeSpy = jest.spyOn(activeModal, 'close')
+    component.close()
+    expect(closeSpy).toHaveBeenCalled()
+  }))
 })