module.exports = {
- moduleNameMapper: {
- '@core/(.*)': '<rootDir>/src/app/core/$1',
- },
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
- testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
+ testPathIgnorePatterns: [
+ '/node_modules/',
+ '/e2e/',
+ 'abstract-name-filter-service',
+ 'abstract-paperless-service',
+ ],
+ transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
+ moduleNameMapper: {
+ '^src/(.*)': '<rootDir>/src/$1',
+ },
}
"jest": "28.1.3",
"jest-environment-jsdom": "^29.5.0",
"jest-preset-angular": "^12.2.6",
+ "jest-websocket-mock": "^2.4.0",
"ts-node": "~10.9.1",
"typescript": "~4.9.5",
"wait-on": "^7.0.1"
"node": ">=8"
}
},
+ "node_modules/jest-websocket-mock": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.4.0.tgz",
+ "integrity": "sha512-AOwyuRw6fgROXHxMOiTDl1/T4dh3fV4jDquha5N0csS/PNp742HeTZWPAuKppVRSQ8s3fUGgJHoyZT9JDO0hMA==",
+ "dev": true,
+ "dependencies": {
+ "jest-diff": "^28.0.2",
+ "mock-socket": "^9.1.0"
+ }
+ },
"node_modules/jest-worker": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
"node": ">=10"
}
},
+ "node_modules/mock-socket": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz",
+ "integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"ng": "ng",
"start": "ng serve",
"build": "ng build",
- "test": "ng test",
+ "test": "ng test --no-watch --coverage",
"lint": "ng lint"
},
"private": true,
"jest": "28.1.3",
"jest-environment-jsdom": "^29.5.0",
"jest-preset-angular": "^12.2.6",
+ "jest-websocket-mock": "^2.4.0",
"ts-node": "~10.9.1",
"typescript": "~4.9.5",
"wait-on": "^7.0.1"
import { jest } from '@jest/globals'
+if (process.env.NODE_ENV === 'test') {
+ require('jest-preset-angular/setup-jest')
+}
+import '@angular/localize/init'
+import { TextEncoder, TextDecoder } from 'util'
+global.TextEncoder = TextEncoder
+global.TextDecoder = TextDecoder
+
+import { registerLocaleData } from '@angular/common'
+import localeAr from '@angular/common/locales/ar'
+import localeBe from '@angular/common/locales/be'
+import localeCa from '@angular/common/locales/ca'
+import localeCs from '@angular/common/locales/cs'
+import localeDa from '@angular/common/locales/da'
+import localeDe from '@angular/common/locales/de'
+import localeEnGb from '@angular/common/locales/en-GB'
+import localeEs from '@angular/common/locales/es'
+import localeFi from '@angular/common/locales/fi'
+import localeFr from '@angular/common/locales/fr'
+import localeIt from '@angular/common/locales/it'
+import localeLb from '@angular/common/locales/lb'
+import localeNl from '@angular/common/locales/nl'
+import localePl from '@angular/common/locales/pl'
+import localePt from '@angular/common/locales/pt'
+import localeRo from '@angular/common/locales/ro'
+import localeRu from '@angular/common/locales/ru'
+import localeSl from '@angular/common/locales/sl'
+import localeSr from '@angular/common/locales/sr'
+import localeSv from '@angular/common/locales/sv'
+import localeTr from '@angular/common/locales/tr'
+import localeZh from '@angular/common/locales/zh'
+
+registerLocaleData(localeAr)
+registerLocaleData(localeBe)
+registerLocaleData(localeCa)
+registerLocaleData(localeCs)
+registerLocaleData(localeDa)
+registerLocaleData(localeDe)
+registerLocaleData(localeEnGb)
+registerLocaleData(localeEs)
+registerLocaleData(localeFi)
+registerLocaleData(localeFr)
+registerLocaleData(localeIt)
+registerLocaleData(localeLb)
+registerLocaleData(localeNl)
+registerLocaleData(localePl)
+registerLocaleData(localePt, 'pt-BR')
+registerLocaleData(localePt, 'pt-PT')
+registerLocaleData(localeRo)
+registerLocaleData(localeRu)
+registerLocaleData(localeSl)
+registerLocaleData(localeSr)
+registerLocaleData(localeSv)
+registerLocaleData(localeTr)
+registerLocaleData(localeZh)
/* global mocks for jsdom */
const mock = () => {
value: () => ['-webkit-appearance'],
})
+Object.defineProperty(window, 'ResizeObserver', { value: mock() })
+
Object.defineProperty(document.body.style, 'transform', {
value: () => {
return {
PermissionType,
} from './services/permissions.service'
-const routes: Routes = [
+export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: '',
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ TestBed,
+ discardPeriodicTasks,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { By } from '@angular/platform-browser'
+import { Router } from '@angular/router'
+import { RouterTestingModule } from '@angular/router/testing'
+import { NgxFileDropModule } from 'ngx-file-drop'
+import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
+import { Subject } from 'rxjs'
+import { routes } from './app-routing.module'
+import { AppComponent } from './app.component'
+import { ToastsComponent } from './components/common/toasts/toasts.component'
+import {
+ ConsumerStatusService,
+ FileStatus,
+} from './services/consumer-status.service'
+import { PermissionsService } from './services/permissions.service'
+import { ToastService, Toast } from './services/toast.service'
+import { UploadDocumentsService } from './services/upload-documents.service'
+import { SettingsService } from './services/settings.service'
+
+describe('AppComponent', () => {
+ let component: AppComponent
+ let fixture: ComponentFixture<AppComponent>
+ let tourService: TourService
+ let consumerStatusService: ConsumerStatusService
+ let permissionsService: PermissionsService
+ let toastService: ToastService
+ let router: Router
+ let settingsService: SettingsService
+ let uploadDocumentsService: UploadDocumentsService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [AppComponent, ToastsComponent],
+ providers: [],
+ imports: [
+ HttpClientTestingModule,
+ TourNgBootstrapModule,
+ RouterTestingModule.withRoutes(routes),
+ NgxFileDropModule,
+ ],
+ }).compileComponents()
+
+ tourService = TestBed.inject(TourService)
+ consumerStatusService = TestBed.inject(ConsumerStatusService)
+ permissionsService = TestBed.inject(PermissionsService)
+ settingsService = TestBed.inject(SettingsService)
+ toastService = TestBed.inject(ToastService)
+ router = TestBed.inject(Router)
+ uploadDocumentsService = TestBed.inject(UploadDocumentsService)
+ fixture = TestBed.createComponent(AppComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should initialize the tour service & toggle class on body for styling', fakeAsync(() => {
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
+ fixture.detectChanges()
+ const tourSpy = jest.spyOn(tourService, 'initialize')
+ component.ngOnInit()
+ expect(tourSpy).toHaveBeenCalled()
+ tourService.start()
+ expect(document.body.classList).toContain('tour-active')
+ tourService.end()
+ tick(500)
+ expect(document.body.classList).not.toContain('tour-active')
+ }))
+
+ it('should display toast on document consumed with link if user has access', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ let toast: Toast
+ toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
+ .mockReturnValue(fileStatusSubject)
+ component.ngOnInit()
+ fileStatusSubject.next(new FileStatus())
+ expect(toastSpy).toHaveBeenCalled()
+ expect(toast.action).not.toBeUndefined()
+ })
+
+ it('should display toast on document consumed without link if user does not have access', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
+ let toast: Toast
+ toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
+ .mockReturnValue(fileStatusSubject)
+ component.ngOnInit()
+ fileStatusSubject.next(new FileStatus())
+ expect(toastSpy).toHaveBeenCalled()
+ expect(toast.action).toBeUndefined()
+ })
+
+ it('should display toast on document added', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentDetected')
+ .mockReturnValue(fileStatusSubject)
+ component.ngOnInit()
+ fileStatusSubject.next(new FileStatus())
+ expect(toastSpy).toHaveBeenCalled()
+ })
+
+ it('should suppress dashboard notifications if set', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest.spyOn(settingsService, 'get').mockReturnValue(true)
+ jest.spyOn(router, 'url', 'get').mockReturnValue('/dashboard')
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentDetected')
+ .mockReturnValue(fileStatusSubject)
+ component.ngOnInit()
+ fileStatusSubject.next(new FileStatus())
+ expect(toastSpy).not.toHaveBeenCalled()
+ })
+
+ it('should display toast on document failed', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
+ .mockReturnValue(fileStatusSubject)
+ component.ngOnInit()
+ fileStatusSubject.next(new FileStatus())
+ expect(toastSpy).toHaveBeenCalled()
+ })
+
+ it('should disable drag-drop if on dashboard', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/dashboard')
+ expect(component.dragDropEnabled).toBeFalsy()
+ jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/documents')
+ expect(component.dragDropEnabled).toBeTruthy()
+ })
+
+ it('should enable drag-drop if user has permissions', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ expect(component.dragDropEnabled).toBeTruthy()
+ })
+
+ it('should disable drag-drop if user does not have permissions', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
+ expect(component.dragDropEnabled).toBeFalsy()
+ })
+
+ it('should support drag drop', fakeAsync(() => {
+ expect(component.fileIsOver).toBeFalsy()
+ component.fileOver()
+ tick(1)
+ fixture.detectChanges()
+ expect(component.fileIsOver).toBeTruthy()
+ const dropzone = fixture.debugElement.query(
+ By.css('.global-dropzone-overlay')
+ )
+ expect(dropzone).not.toBeNull()
+ component.fileLeave()
+ tick(700)
+ fixture.detectChanges()
+ expect(dropzone.classes['hide']).toBeTruthy()
+ // drop
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
+ component.dropped([])
+ tick(3000)
+ expect(toastSpy).toHaveBeenCalled()
+ expect(uploadSpy).toHaveBeenCalled()
+ }))
+})
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { AppFrameComponent } from './app-frame.component'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { BrowserModule } from '@angular/platform-browser'
+import { RouterTestingModule } from '@angular/router/testing'
+import { SettingsService } from 'src/app/services/settings.service'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
+import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { of } from 'rxjs'
+import { ToastService } from 'src/app/services/toast.service'
+import { environment } from 'src/environments/environment'
+import { OpenDocumentsService } from 'src/app/services/open-documents.service'
+import { ActivatedRoute, Router } from '@angular/router'
+import { DocumentDetailComponent } from '../document-detail/document-detail.component'
+import { SearchService } from 'src/app/services/rest/search.service'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
+import { routes } from 'src/app/app-routing.module'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+
+const document = { id: 2, title: 'Hello world' }
+
+describe('AppFrameComponent', () => {
+ let component: AppFrameComponent
+ let fixture: ComponentFixture<AppFrameComponent>
+ let httpTestingController: HttpTestingController
+ let settingsService: SettingsService
+ let permissionsService: PermissionsService
+ let remoteVersionService: RemoteVersionService
+ let toastService: ToastService
+ let openDocumentsService: OpenDocumentsService
+ let searchService: SearchService
+ let documentListViewService: DocumentListViewService
+ let router: Router
+ let savedViewSpy
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [AppFrameComponent, IfPermissionsDirective],
+ imports: [
+ HttpClientTestingModule,
+ BrowserModule,
+ RouterTestingModule.withRoutes(routes),
+ NgbModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ providers: [
+ SettingsService,
+ SavedViewService,
+ PermissionsService,
+ RemoteVersionService,
+ IfPermissionsDirective,
+ ToastService,
+ OpenDocumentsService,
+ SearchService,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ firstChild: {
+ component: DocumentDetailComponent,
+ },
+ snapshot: {
+ firstChild: {
+ component: DocumentDetailComponent,
+ params: {
+ id: document.id,
+ },
+ },
+ },
+ },
+ },
+ PermissionsGuard,
+ ],
+ }).compileComponents()
+
+ settingsService = TestBed.inject(SettingsService)
+ const savedViewService = TestBed.inject(SavedViewService)
+ permissionsService = TestBed.inject(PermissionsService)
+ remoteVersionService = TestBed.inject(RemoteVersionService)
+ toastService = TestBed.inject(ToastService)
+ openDocumentsService = TestBed.inject(OpenDocumentsService)
+ searchService = TestBed.inject(SearchService)
+ documentListViewService = TestBed.inject(DocumentListViewService)
+ router = TestBed.inject(Router)
+
+ jest
+ .spyOn(settingsService, 'displayName', 'get')
+ .mockReturnValue('Hello World')
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+
+ savedViewSpy = jest.spyOn(savedViewService, 'initialize')
+
+ fixture = TestBed.createComponent(AppFrameComponent)
+ component = fixture.componentInstance
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+
+ fixture.detectChanges()
+ })
+
+ it('should initialize the saved view service', () => {
+ expect(savedViewSpy).toHaveBeenCalled()
+ })
+
+ it('should check for update if enabled', () => {
+ const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
+ updateCheckSpy.mockImplementation(() => {
+ return of({
+ version: 'v100.0',
+ update_available: true,
+ })
+ })
+ settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, true)
+ component.ngOnInit()
+ expect(updateCheckSpy).toHaveBeenCalled()
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain('Update available')
+ })
+
+ it('should check not for update if disabled', () => {
+ const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
+ settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
+ component.ngOnInit()
+ fixture.detectChanges()
+ expect(updateCheckSpy).not.toHaveBeenCalled()
+ expect(fixture.nativeElement.textContent).not.toContain('Update available')
+ })
+
+ it('should check for update if was disabled and then enabled', () => {
+ const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
+ settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
+ component.setUpdateChecking(true)
+ fixture.detectChanges()
+ expect(updateCheckSpy).toHaveBeenCalled()
+ })
+
+ it('should show error on toggle update checking if store settings fails', () => {
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
+ component.setUpdateChecking(true)
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}ui_settings/`)
+ .flush('error', {
+ status: 500,
+ statusText: 'error',
+ })
+ expect(toastSpy).toHaveBeenCalled()
+ })
+
+ it('should support toggling slim sidebar and saving', fakeAsync(() => {
+ const saveSettingSpy = jest.spyOn(settingsService, 'set')
+ expect(component.slimSidebarEnabled).toBeFalsy()
+ expect(component.slimSidebarAnimating).toBeFalsy()
+ component.toggleSlimSidebar()
+ expect(component.slimSidebarAnimating).toBeTruthy()
+ tick(200)
+ expect(component.slimSidebarAnimating).toBeFalsy()
+ expect(component.slimSidebarEnabled).toBeTruthy()
+ expect(saveSettingSpy).toHaveBeenCalledWith(
+ SETTINGS_KEYS.SLIM_SIDEBAR,
+ true
+ )
+ }))
+
+ it('should show error on toggle slim sidebar if store settings fails', () => {
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ component.toggleSlimSidebar()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}ui_settings/`)
+ .flush('error', {
+ status: 500,
+ statusText: 'error',
+ })
+ expect(toastSpy).toHaveBeenCalled()
+ })
+
+ it('should support collapsable menu', () => {
+ const button: HTMLButtonElement = (
+ fixture.nativeElement as HTMLDivElement
+ ).querySelector('button[data-toggle=collapse]')
+ button.dispatchEvent(new MouseEvent('click'))
+ expect(component.isMenuCollapsed).toBeFalsy()
+ component.closeMenu()
+ expect(component.isMenuCollapsed).toBeTruthy()
+ })
+
+ it('should support close document & navigate on close current doc', () => {
+ const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
+ closeSpy.mockReturnValue(of(true))
+ const routerSpy = jest.spyOn(router, 'navigate')
+ component.closeDocument(document)
+ expect(closeSpy).toHaveBeenCalledWith(document)
+ expect(routerSpy).toHaveBeenCalled()
+ })
+
+ it('should support close all documents & navigate on close current doc', () => {
+ const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
+ closeAllSpy.mockReturnValue(of(true))
+ const routerSpy = jest.spyOn(router, 'navigate')
+ component.closeAll()
+ expect(closeAllSpy).toHaveBeenCalled()
+ expect(routerSpy).toHaveBeenCalled()
+ })
+
+ it('should close all documents on logout', () => {
+ const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
+ component.onLogout()
+ expect(closeAllSpy).toHaveBeenCalled()
+ })
+
+ it('should warn before close if dirty documents', () => {
+ jest.spyOn(openDocumentsService, 'hasDirty').mockReturnValue(true)
+ expect(component.canDeactivate()).toBeFalsy()
+ })
+
+ it('should call autocomplete endpoint on input', fakeAsync(() => {
+ const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
+ component.searchAutoComplete(of('hello')).subscribe()
+ tick(250)
+ expect(autocompleteSpy).toHaveBeenCalled()
+
+ component.searchAutoComplete(of('hello world 1')).subscribe()
+ tick(250)
+ expect(autocompleteSpy).toHaveBeenCalled()
+ }))
+
+ it('should support reset search field', () => {
+ const resetSpy = jest.spyOn(component, 'resetSearchField')
+ const input = (fixture.nativeElement as HTMLDivElement).querySelector(
+ 'input'
+ ) as HTMLInputElement
+ input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }))
+ expect(resetSpy).toHaveBeenCalled()
+ })
+
+ it('should support choosing a search item', () => {
+ expect(component.searchField.value).toEqual('')
+ component.itemSelected({ item: 'hello', preventDefault: () => true })
+ expect(component.searchField.value).toEqual('hello ')
+ component.itemSelected({ item: 'world', preventDefault: () => true })
+ expect(component.searchField.value).toEqual('hello world ')
+ })
+
+ it('should navigate via quickFilter on search', () => {
+ const str = 'hello world '
+ component.searchField.patchValue(str)
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.search()
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: str.trim(),
+ },
+ ])
+ })
+})
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService,
- private permissionsService: PermissionsService
+ permissionsService: PermissionsService
) {
super()
}
versionString = `${environment.appTitle} ${environment.version}`
- appRemoteVersion
+ appRemoteVersion: AppRemoteVersion
isMenuCollapsed: boolean = true
this.toastService.showError(
$localize`An error occurred while saving settings.`
)
- console.log(error)
+ console.warn(error)
},
})
}
this.toastService.showError(
$localize`An error occurred while saving update checking settings.`
)
- console.log(error)
+ console.warn(error)
},
})
if (enable) {
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { ClearableBadgeComponent } from './clearable-badge.component'
+
+describe('ClearableBadgeComponent', () => {
+ let component: ClearableBadgeComponent
+ let fixture: ComponentFixture<ClearableBadgeComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [ClearableBadgeComponent],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(ClearableBadgeComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support selected', () => {
+ component.selected = true
+ expect(component.active).toBeTruthy()
+ })
+
+ it('should support numbered', () => {
+ component.number = 3
+ fixture.detectChanges()
+ expect(component.active).toBeTruthy()
+ expect((fixture.nativeElement as HTMLDivElement).textContent).toContain('3')
+ })
+
+ it('should support selected', () => {
+ let clearedResult
+ component.selected = true
+ fixture.detectChanges()
+ component.cleared.subscribe((clear) => {
+ clearedResult = clear
+ })
+ fixture.nativeElement
+ .querySelectorAll('button')[0]
+ .dispatchEvent(new MouseEvent('click'))
+ expect(clearedResult).toBeTruthy()
+ })
+})
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ discardPeriodicTasks,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { ConfirmDialogComponent } from './confirm-dialog.component'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { Subject } from 'rxjs'
+
+describe('ConfirmDialogComponent', () => {
+ let component: ConfirmDialogComponent
+ let modal: NgbActiveModal
+ let fixture: ComponentFixture<ConfirmDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [ConfirmDialogComponent, SafeHtmlPipe],
+ providers: [NgbActiveModal, SafeHtmlPipe],
+ imports: [],
+ }).compileComponents()
+
+ modal = TestBed.inject(NgbActiveModal)
+
+ fixture = TestBed.createComponent(ConfirmDialogComponent)
+ component = fixture.componentInstance
+ component.title = 'Confirm delete'
+ component.messageBold = 'Do you really want to delete document file.pdf?'
+ component.message =
+ 'The files for this document will be deleted permanently. This operation cannot be undone.'
+ component.btnClass = 'btn-danger'
+ component.btnCaption = 'Delete document'
+
+ fixture.detectChanges()
+ })
+
+ it('should support alternative', () => {
+ let alternativeClickedResult
+ let alternativeSubjectResult
+ component.alternativeClicked.subscribe((result) => {
+ alternativeClickedResult = true
+ })
+ component.alternative()
+ // with subject
+ const subject = new Subject<boolean>()
+ component.alternativeSubject = subject
+ subject.asObservable().subscribe((result) => {
+ alternativeSubjectResult = result
+ })
+ component.alternative()
+ expect(alternativeClickedResult).toBeTruthy()
+ expect(alternativeSubjectResult).toBeTruthy()
+ })
+
+ it('should support confirm', () => {
+ let confirmClickedResult
+ let confirmSubjectResult
+ component.confirmClicked.subscribe((result) => {
+ confirmClickedResult = true
+ })
+ component.confirm()
+ // with subject
+ const subject = new Subject<boolean>()
+ component.confirmSubject = subject
+ subject.asObservable().subscribe((result) => {
+ confirmSubjectResult = result
+ })
+ component.confirm()
+ expect(confirmClickedResult).toBeTruthy()
+ expect(confirmSubjectResult).toBeTruthy()
+ })
+
+ it('should support cancel & close modal', () => {
+ let confirmSubjectResult
+ const closeModalSpy = jest.spyOn(modal, 'close')
+ component.cancel()
+ const subject = new Subject<boolean>()
+ component.confirmSubject = subject
+ subject.asObservable().subscribe((result) => {
+ confirmSubjectResult = result
+ })
+ component.cancel()
+ // with subject
+ expect(closeModalSpy).toHaveBeenCalled()
+ expect(confirmSubjectResult).toBeFalsy()
+ })
+
+ it('should support delay confirm', fakeAsync(() => {
+ component.confirmButtonEnabled = false
+ component.delayConfirm(1)
+ expect(component.confirmButtonEnabled).toBeFalsy()
+ tick(1500)
+ fixture.detectChanges()
+ expect(component.confirmButtonEnabled).toBeTruthy()
+ discardPeriodicTasks()
+ }))
+})
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+let fixture: ComponentFixture<DateDropdownComponent>
+import {
+ DateDropdownComponent,
+ DateSelection,
+ RelativeDate,
+} from './date-dropdown.component'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { SettingsService } from 'src/app/services/settings.service'
+import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { DatePipe } from '@angular/common'
+
+describe('DateDropdownComponent', () => {
+ let component: DateDropdownComponent
+ let httpTestingController: HttpTestingController
+ let settingsService: SettingsService
+ let settingsSpy
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DateDropdownComponent,
+ ClearableBadgeComponent,
+ CustomDatePipe,
+ ],
+ providers: [SettingsService, CustomDatePipe, DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ NgbModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ }).compileComponents()
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ settingsService = TestBed.inject(SettingsService)
+ settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
+
+ fixture = TestBed.createComponent(DateDropdownComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should use a localized date placeholder', () => {
+ expect(component.datePlaceHolder).toEqual('mm/dd/yyyy')
+ expect(settingsSpy).toHaveBeenCalled()
+ })
+
+ it('should support date input, emit change', fakeAsync(() => {
+ let result: string
+ component.dateAfterChange.subscribe((date) => (result = date))
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
+ input.value = '5/30/2023'
+ input.dispatchEvent(new Event('change'))
+ tick(500)
+ expect(result).not.toBeNull()
+ }))
+
+ it('should support date select, emit datesSet change', fakeAsync(() => {
+ let result: DateSelection
+ component.datesSet.subscribe((date) => (result = date))
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
+ input.value = '5/30/2023'
+ input.dispatchEvent(new Event('dateSelect'))
+ tick(500)
+ expect(result).not.toBeNull()
+ }))
+
+ it('should support relative dates', fakeAsync(() => {
+ let result: DateSelection
+ component.datesSet.subscribe((date) => (result = date))
+ component.setRelativeDate(null)
+ component.setRelativeDate(RelativeDate.LAST_7_DAYS)
+ tick(500)
+ expect(result).toEqual({
+ after: null,
+ before: null,
+ relativeDateID: RelativeDate.LAST_7_DAYS,
+ })
+ }))
+
+ it('should support report if active', () => {
+ component.relativeDate = RelativeDate.LAST_7_DAYS
+ expect(component.isActive).toBeTruthy()
+ component.relativeDate = null
+ component.dateAfter = '2023-05-30'
+ expect(component.isActive).toBeTruthy()
+ component.dateAfter = null
+ component.dateBefore = '2023-05-30'
+ expect(component.isActive).toBeTruthy()
+ component.dateBefore = null
+ expect(component.isActive).toBeFalsy()
+ })
+
+ it('should support reset', () => {
+ component.dateAfter = '2023-05-30'
+ component.reset()
+ expect(component.dateAfter).toBeNull()
+ })
+
+ it('should support clearAfter', () => {
+ component.dateAfter = '2023-05-30'
+ component.clearAfter()
+ expect(component.dateAfter).toBeNull()
+ })
+
+ it('should support clearBefore', () => {
+ component.dateBefore = '2023-05-30'
+ component.clearBefore()
+ expect(component.dateBefore).toBeNull()
+ })
+
+ it('should limit keyboard events', () => {
+ const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
+ let event: KeyboardEvent = new KeyboardEvent('keypress', {
+ key: '9',
+ })
+ let eventSpy = jest.spyOn(event, 'preventDefault')
+ input.dispatchEvent(event)
+ expect(eventSpy).not.toHaveBeenCalled()
+
+ event = new KeyboardEvent('keypress', {
+ key: '{',
+ })
+ eventSpy = jest.spyOn(event, 'preventDefault')
+ input.dispatchEvent(event)
+ expect(eventSpy).toHaveBeenCalled()
+ })
+})
-import { formatDate } from '@angular/common'
import {
Component,
EventEmitter,
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+
+describe('CorrespondentEditDialogComponent', () => {
+ let component: CorrespondentEditDialogComponent
+ let fixture: ComponentFixture<CorrespondentEditDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ CorrespondentEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PermissionsFormComponent,
+ ],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(CorrespondentEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+
+describe('DocumentTypeEditDialogComponent', () => {
+ let component: DocumentTypeEditDialogComponent
+ let fixture: ComponentFixture<DocumentTypeEditDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentTypeEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PermissionsFormComponent,
+ ],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(DocumentTypeEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { Component } from '@angular/core'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import {
+ FormGroup,
+ FormControl,
+ FormsModule,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import { TagService } from 'src/app/services/rest/tag.service'
+import { UserService } from 'src/app/services/rest/user.service'
+import { SettingsService } from 'src/app/services/settings.service'
+import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
+import {
+ DEFAULT_MATCHING_ALGORITHM,
+ MATCH_ALL,
+ MATCH_AUTO,
+ MATCH_NONE,
+} from 'src/app/data/matching-model'
+import { of } from 'rxjs'
+import { environment } from 'src/environments/environment'
+
+@Component({
+ template: `
+ <div>
+ <h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
+ </div>
+ `,
+})
+class TestComponent extends EditDialogComponent<PaperlessTag> {
+ constructor(
+ service: TagService,
+ activeModal: NgbActiveModal,
+ userService: UserService,
+ settingsService: SettingsService
+ ) {
+ super(service, activeModal, userService, settingsService)
+ }
+
+ getForm(): FormGroup<any> {
+ return new FormGroup({
+ name: new FormControl(''),
+ color: new FormControl(''),
+ is_inbox_tag: new FormControl(false),
+ permissions_form: new FormControl(null),
+ matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
+ })
+ }
+}
+
+const currentUser = {
+ id: 99,
+ username: 'user99',
+}
+
+const permissions = {
+ view: {
+ users: [11],
+ groups: [],
+ },
+ change: {
+ users: [],
+ groups: [2],
+ },
+}
+
+const tag = {
+ id: 1,
+ name: 'Tag 1',
+ color: '#fff000',
+ is_inbox_tag: false,
+ matching_algorithm: MATCH_AUTO,
+ owner: 10,
+ permissions,
+}
+
+describe('EditDialogComponent', () => {
+ let component: TestComponent
+ let fixture: ComponentFixture<TestComponent>
+ let tagService: TagService
+ let activeModal: NgbActiveModal
+ let httpTestingController: HttpTestingController
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent],
+ providers: [
+ NgbActiveModal,
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 13,
+ username: 'user1',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: SettingsService,
+ useValue: {
+ currentUser,
+ },
+ },
+ TagService,
+ ],
+ imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ tagService = TestBed.inject(TagService)
+ activeModal = TestBed.inject(NgbActiveModal)
+ httpTestingController = TestBed.inject(HttpTestingController)
+
+ fixture = TestBed.createComponent(TestComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should interpolate object permissions', () => {
+ component.object = tag
+ component.dialogMode = EditDialogMode.EDIT
+ component.ngOnInit()
+
+ expect(component.objectForm.get('permissions_form').value).toEqual({
+ owner: tag.owner,
+ set_permissions: permissions,
+ })
+ })
+
+ it('should delay close enabled', fakeAsync(() => {
+ expect(component.closeEnabled).toBeFalsy()
+ component.ngOnInit()
+ tick(100)
+ expect(component.closeEnabled).toBeTruthy()
+ }))
+
+ it('should set default owner when in create mode', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ component.ngOnInit()
+ expect(component.objectForm.get('permissions_form').value.owner).toEqual(
+ currentUser.id
+ )
+ // cover optional chaining
+ component.objectForm.removeControl('permissions_form')
+ component.ngOnInit()
+ })
+
+ it('should detect if pattern required', () => {
+ expect(component.patternRequired).toBeFalsy()
+ component.objectForm.get('matching_algorithm').setValue(MATCH_AUTO)
+ expect(component.patternRequired).toBeFalsy()
+ component.objectForm.get('matching_algorithm').setValue(MATCH_NONE)
+ expect(component.patternRequired).toBeFalsy()
+ component.objectForm.get('matching_algorithm').setValue(MATCH_ALL)
+ expect(component.patternRequired).toBeTruthy()
+ // coverage
+ component.objectForm = null
+ expect(component.patternRequired).toBeTruthy()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ // coverage
+ component.dialogMode = null
+ fixture.detectChanges()
+ })
+
+ it('should close on cancel', () => {
+ const closeSpy = jest.spyOn(activeModal, 'close')
+ component.cancel()
+ expect(closeSpy).toHaveBeenCalled()
+ })
+
+ it('should update an object on save in edit mode', () => {
+ const updateSpy = jest.spyOn(tagService, 'update')
+ component.dialogMode = EditDialogMode.EDIT
+ component.save()
+ expect(updateSpy).toHaveBeenCalled()
+ })
+
+ it('should create an object on save in edit mode', () => {
+ const createSpy = jest.spyOn(tagService, 'create')
+ component.dialogMode = EditDialogMode.CREATE
+ component.save()
+ expect(createSpy).toHaveBeenCalled()
+ })
+
+ it('should close on successful save', () => {
+ const closeSpy = jest.spyOn(activeModal, 'close')
+ const successSpy = jest.spyOn(component.succeeded, 'emit')
+ component.save()
+ httpTestingController.expectOne(`${environment.apiBaseUrl}tags/`).flush({})
+ expect(closeSpy).toHaveBeenCalled()
+ expect(successSpy).toHaveBeenCalled()
+ })
+
+ it('should not close on failed save', () => {
+ const closeSpy = jest.spyOn(activeModal, 'close')
+ const failedSpy = jest.spyOn(component.failed, 'next')
+ component.save()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}tags/`)
+ .flush('error', {
+ status: 500,
+ statusText: 'error',
+ })
+ expect(closeSpy).not.toHaveBeenCalled()
+ expect(failedSpy).toHaveBeenCalled()
+ expect(component.error).toEqual('error')
+ })
+})
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
import { SettingsService } from 'src/app/services/settings.service'
+export enum EditDialogMode {
+ CREATE = 0,
+ EDIT = 1,
+}
+
@Directive()
export abstract class EditDialogComponent<
T extends ObjectWithPermissions | ObjectWithId
users: PaperlessUser[]
@Input()
- dialogMode: string = 'create'
+ dialogMode: EditDialogMode = EditDialogMode.CREATE
@Input()
object: T
this.userService.listAll().subscribe((r) => {
this.users = r.results
- if (this.dialogMode === 'create') {
+ if (this.dialogMode === EditDialogMode.CREATE) {
this.objectForm.get('permissions_form')?.setValue({
owner: this.settingsService.currentUser.id,
})
return $localize`Edit item`
}
- getSaveErrorMessage(error: string) {
- return $localize`Could not save element: ${error}`
- }
-
getTitle() {
switch (this.dialogMode) {
- case 'create':
+ case EditDialogMode.CREATE:
return this.getCreateTitle()
- case 'edit':
+ case EditDialogMode.EDIT:
return this.getEditTitle()
default:
break
var newObject = Object.assign(Object.assign({}, this.object), formValues)
var serverResponse: Observable<T>
switch (this.dialogMode) {
- case 'create':
+ case EditDialogMode.CREATE:
serverResponse = this.service.create(newObject)
break
- case 'edit':
+ case EditDialogMode.EDIT:
serverResponse = this.service.update(newObject)
default:
break
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+import { GroupEditDialogComponent } from './group-edit-dialog.component'
+import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
+
+describe('GroupEditDialogComponent', () => {
+ let component: GroupEditDialogComponent
+ let fixture: ComponentFixture<GroupEditDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ GroupEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PermissionsFormComponent,
+ PermissionsSelectComponent,
+ ],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(GroupEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ discardPeriodicTasks,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component'
+import { PasswordComponent } from '../../input/password/password.component'
+import { CheckComponent } from '../../input/check/check.component'
+import { IMAPSecurity } from 'src/app/data/paperless-mail-account'
+import { environment } from 'src/environments/environment'
+
+describe('MailAccountEditDialogComponent', () => {
+ let component: MailAccountEditDialogComponent
+ let fixture: ComponentFixture<MailAccountEditDialogComponent>
+ let httpController: HttpTestingController
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ MailAccountEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ CheckComponent,
+ PermissionsFormComponent,
+ PasswordComponent,
+ ],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ httpController = TestBed.inject(HttpTestingController)
+
+ fixture = TestBed.createComponent(MailAccountEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+
+ it('should support test mail account and show appropriate expiring alert', fakeAsync(() => {
+ component.object = {
+ name: 'example',
+ imap_server: 'imap.example.com',
+ username: 'user',
+ password: 'pass',
+ imap_port: 443,
+ imap_security: IMAPSecurity.SSL,
+ is_token: false,
+ }
+
+ // success
+ component.test()
+ httpController
+ .expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
+ .flush({ success: true })
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain(
+ 'Successfully connected'
+ )
+ tick(6000)
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).not.toContain(
+ 'Successfully connected'
+ )
+
+ // not success
+ component.test()
+ httpController
+ .expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
+ .flush({ success: false })
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain('Unable to connect')
+
+ // error
+ component.test()
+ httpController
+ .expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
+ .flush({}, { status: 500, statusText: 'error' })
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain('Unable to connect')
+ tick(6000)
+ }))
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component'
+import { NumberComponent } from '../../input/number/number.component'
+import { TagsComponent } from '../../input/tags/tags.component'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { MailAccountService } from 'src/app/services/rest/mail-account.service'
+import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
+import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
+import { of } from 'rxjs'
+import {
+ MailAction,
+ MailMetadataCorrespondentOption,
+} from 'src/app/data/paperless-mail-rule'
+
+describe('MailRuleEditDialogComponent', () => {
+ let component: MailRuleEditDialogComponent
+ let fixture: ComponentFixture<MailRuleEditDialogComponent>
+ let accountService: MailAccountService
+ let correspondentService: CorrespondentService
+ let documentTypeService: DocumentTypeService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ MailRuleEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PermissionsFormComponent,
+ NumberComponent,
+ TagsComponent,
+ SafeHtmlPipe,
+ ],
+ providers: [
+ NgbActiveModal,
+ {
+ provide: MailAccountService,
+ useValue: {
+ listAll: () => of([]),
+ },
+ },
+ {
+ provide: CorrespondentService,
+ useValue: {
+ listAll: () => of([]),
+ },
+ },
+ {
+ provide: DocumentTypeService,
+ useValue: {
+ listAll: () => of([]),
+ },
+ },
+ ],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(MailRuleEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+
+ it('should support optional fields', () => {
+ expect(component.showCorrespondentField).toBeFalsy()
+ component.objectForm
+ .get('assign_correspondent_from')
+ .setValue(MailMetadataCorrespondentOption.FromCustom)
+ expect(component.showCorrespondentField).toBeTruthy()
+
+ expect(component.showActionParamField).toBeFalsy()
+ component.objectForm.get('action').setValue(MailAction.Move)
+ expect(component.showActionParamField).toBeTruthy()
+ component.objectForm.get('action').setValue('')
+ expect(component.showActionParamField).toBeFalsy()
+ component.objectForm.get('action').setValue(MailAction.Tag)
+ expect(component.showActionParamField).toBeTruthy()
+
+ // coverage of optional chaining
+ component.objectForm = null
+ expect(component.showCorrespondentField).toBeFalsy()
+ expect(component.showActionParamField).toBeFalsy()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+
+describe('StoragePathEditDialogComponent', () => {
+ let component: StoragePathEditDialogComponent
+ let fixture: ComponentFixture<StoragePathEditDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ StoragePathEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PermissionsFormComponent,
+ SafeHtmlPipe,
+ ],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(StoragePathEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+import { TagEditDialogComponent } from './tag-edit-dialog.component'
+import { ColorComponent } from '../../input/color/color.component'
+import { CheckComponent } from '../../input/check/check.component'
+
+describe('TagEditDialogComponent', () => {
+ let component: TagEditDialogComponent
+ let fixture: ComponentFixture<TagEditDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TagEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PermissionsFormComponent,
+ ColorComponent,
+ CheckComponent,
+ ],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(TagEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { EditDialogMode } from '../edit-dialog.component'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SelectComponent } from '../../input/select/select.component'
+import {
+ AbstractControl,
+ FormsModule,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { TextComponent } from '../../input/text/text.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
+import { UserEditDialogComponent } from './user-edit-dialog.component'
+import { PasswordComponent } from '../../input/password/password.component'
+import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
+import { GroupService } from 'src/app/services/rest/group.service'
+import { of } from 'rxjs'
+
+describe('UserEditDialogComponent', () => {
+ let component: UserEditDialogComponent
+ let fixture: ComponentFixture<UserEditDialogComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ UserEditDialogComponent,
+ IfPermissionsDirective,
+ IfOwnerDirective,
+ SelectComponent,
+ TextComponent,
+ PasswordComponent,
+ PermissionsFormComponent,
+ PermissionsSelectComponent,
+ ],
+ providers: [
+ NgbActiveModal,
+ {
+ provide: GroupService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 1,
+ permissions: ['dummy_perms'],
+ },
+ ],
+ }),
+ },
+ },
+ ],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(UserEditDialogComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support create and edit modes', () => {
+ component.dialogMode = EditDialogMode.CREATE
+ const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
+ const editTitleSpy = jest.spyOn(component, 'getEditTitle')
+ fixture.detectChanges()
+ expect(createTitleSpy).toHaveBeenCalled()
+ expect(editTitleSpy).not.toHaveBeenCalled()
+ component.dialogMode = EditDialogMode.EDIT
+ fixture.detectChanges()
+ expect(editTitleSpy).toHaveBeenCalled()
+ })
+
+ it('should disable user permissions select on toggle superuser', () => {
+ const control: AbstractControl =
+ component.objectForm.get('user_permissions')
+ expect(control.disabled).toBeFalsy()
+ component.objectForm.get('is_superuser').setValue(true)
+ component.onToggleSuperUser()
+ expect(control.disabled).toBeTruthy()
+ })
+
+ it('should update inherited permissions', () => {
+ component.objectForm.get('groups').setValue(null)
+ expect(component.inheritedPermissions).toEqual([])
+ component.objectForm.get('groups').setValue([1])
+ expect(component.inheritedPermissions).toEqual(['dummy_perms'])
+ component.objectForm.get('groups').setValue([2])
+ expect(component.inheritedPermissions).toEqual([])
+ })
+
+ it('should detect whether password was changed in form on save', () => {
+ component.objectForm.get('password').setValue(null)
+ component.save()
+ expect(component.passwordIsSet).toBeFalsy()
+
+ // unchanged pw
+ component.objectForm.get('password').setValue('*******')
+ component.save()
+ expect(component.passwordIsSet).toBeFalsy()
+
+ // unchanged pw
+ component.objectForm.get('password').setValue('helloworld')
+ component.save()
+ expect(component.passwordIsSet).toBeTruthy()
+ })
+})
<div *ngIf="selectionModel.items" class="items" #buttonItems>
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index">
<app-toggleable-dropdown-button
- *ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
+ *ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled">
</app-toggleable-dropdown-button>
</ng-container>
</div>
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import {
+ ChangedItems,
+ FilterableDropdownComponent,
+ FilterableDropdownSelectionModel,
+ Intersection,
+ LogicalOperator,
+} from './filterable-dropdown.component'
+import { FilterPipe } from 'src/app/pipes/filter.pipe'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import {
+ DEFAULT_MATCHING_ALGORITHM,
+ MATCH_ALL,
+} from 'src/app/data/matching-model'
+import {
+ ToggleableDropdownButtonComponent,
+ ToggleableItemState,
+} from './toggleable-dropdown-button/toggleable-dropdown-button.component'
+import { TagComponent } from '../tag/tag.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
+
+const items: PaperlessTag[] = [
+ {
+ id: 1,
+ name: 'Tag1',
+ is_inbox_tag: false,
+ matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
+ },
+ {
+ id: 2,
+ name: 'Tag2',
+ is_inbox_tag: true,
+ matching_algorithm: MATCH_ALL,
+ match: 'str',
+ },
+]
+
+const nullItem = {
+ id: null,
+ name: 'Not assigned',
+}
+
+let selectionModel: FilterableDropdownSelectionModel
+
+describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
+ let component: FilterableDropdownComponent
+ let fixture: ComponentFixture<FilterableDropdownComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ FilterableDropdownComponent,
+ FilterPipe,
+ ToggleableDropdownButtonComponent,
+ TagComponent,
+ ClearableBadgeComponent,
+ ],
+ providers: [FilterPipe],
+ imports: [NgbModule, FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(FilterableDropdownComponent)
+ component = fixture.componentInstance
+ selectionModel = new FilterableDropdownSelectionModel()
+ })
+
+ it('should sanitize title', () => {
+ expect(component.name).toBeNull()
+ component.title = 'Foo Bar'
+ expect(component.name).toEqual('foo_bar')
+ })
+
+ it('should support reset', () => {
+ component.items = items
+ component.selectionModel = selectionModel
+ selectionModel.set(items[0].id, ToggleableItemState.Selected)
+ expect(selectionModel.getSelectedItems()).toHaveLength(1)
+ expect(selectionModel.isDirty()).toBeTruthy()
+ component.reset()
+ expect(selectionModel.getSelectedItems()).toHaveLength(0)
+ expect(selectionModel.isDirty()).toBeFalsy()
+ })
+
+ it('should report document counts', () => {
+ component.documentCounts = [
+ {
+ id: items[0].id,
+ document_count: 12,
+ },
+ ]
+ expect(component.getUpdatedDocumentCount(items[0].id)).toEqual(12)
+ expect(component.getUpdatedDocumentCount(items[1].id)).toBeUndefined() // coverate of optional chaining
+ })
+
+ it('should emit change when items selected', () => {
+ component.items = items
+ component.selectionModel = selectionModel
+ let newModel: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe((model) => (newModel = model))
+ expect(newModel).toBeUndefined()
+
+ selectionModel.set(items[0].id, ToggleableItemState.Selected)
+ expect(selectionModel.isDirty()).toBeTruthy()
+ expect(newModel.getSelectedItems()).toEqual([items[0]])
+ expect(newModel.getExcludedItems()).toEqual([])
+
+ selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
+ expect(newModel.getSelectedItems()).toEqual([])
+
+ expect(component.items).toEqual([nullItem, ...items])
+ })
+
+ it('should emit change when items excluded', () => {
+ component.items = items
+ component.selectionModel = selectionModel
+ let newModel: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe((model) => (newModel = model))
+ expect(newModel).toBeUndefined()
+ selectionModel.toggle(items[0].id)
+ expect(newModel.getSelectedItems()).toEqual([items[0]])
+ })
+
+ it('should emit change when items excluded', () => {
+ component.items = items
+ component.selectionModel = selectionModel
+ let newModel: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe((model) => (newModel = model))
+
+ selectionModel.set(items[0].id, ToggleableItemState.Excluded)
+ expect(newModel.getSelectedItems()).toEqual([])
+ expect(newModel.getExcludedItems()).toEqual([items[0]])
+
+ selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
+ expect(newModel.getSelectedItems()).toEqual([])
+ expect(newModel.getExcludedItems()).toEqual([])
+ })
+
+ it('should exclude items when excluded and not editing', () => {
+ component.items = items
+ component.manyToOne = true
+ component.selectionModel = selectionModel
+ selectionModel.set(items[0].id, ToggleableItemState.Selected)
+ component.excludeClicked(items[0].id)
+ expect(selectionModel.getSelectedItems()).toEqual([])
+ expect(selectionModel.getExcludedItems()).toEqual([items[0]])
+ })
+
+ it('should toggle when items excluded and editing', () => {
+ component.items = items
+ component.manyToOne = true
+ component.editing = true
+ component.selectionModel = selectionModel
+ selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
+ component.excludeClicked(items[0].id)
+ expect(selectionModel.getSelectedItems()).toEqual([items[0]])
+ expect(selectionModel.getExcludedItems()).toEqual([])
+ })
+
+ it('should hide count for item if adding will increase size of set', () => {
+ component.items = items
+ component.manyToOne = true
+ component.selectionModel = selectionModel
+ expect(component.hideCount(items[0])).toBeFalsy()
+ selectionModel.logicalOperator = LogicalOperator.Or
+ expect(component.hideCount(items[0])).toBeTruthy()
+ })
+
+ it('should enforce single select when editing', () => {
+ component.editing = true
+ component.items = items
+ component.selectionModel = selectionModel
+ let newModel: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe((model) => (newModel = model))
+
+ expect(selectionModel.singleSelect).toEqual(true)
+ selectionModel.toggle(items[0].id)
+ selectionModel.toggle(items[1].id)
+ expect(newModel.getSelectedItems()).toEqual([items[1]])
+ })
+
+ it('should support manyToOne selecting', () => {
+ component.items = items
+ selectionModel.manyToOne = false
+ component.selectionModel = selectionModel
+ component.manyToOne = true
+ expect(component.manyToOne).toBeTruthy()
+ let newModel: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe((model) => (newModel = model))
+
+ expect(selectionModel.singleSelect).toEqual(false)
+ selectionModel.toggle(items[0].id)
+ selectionModel.toggle(items[1].id)
+ expect(newModel.getSelectedItems()).toEqual([items[0], items[1]])
+ })
+
+ it('should dynamically enable / disable modifier toggle', () => {
+ component.items = items
+ component.selectionModel = selectionModel
+ expect(component.modifierToggleEnabled).toBeTruthy()
+ selectionModel.toggle(null)
+ expect(component.modifierToggleEnabled).toBeFalsy()
+ component.manyToOne = true
+ expect(component.modifierToggleEnabled).toBeFalsy()
+ selectionModel.toggle(items[0].id)
+ selectionModel.toggle(items[1].id)
+ expect(component.modifierToggleEnabled).toBeTruthy()
+ })
+
+ it('should apply changes and close when apply button clicked', () => {
+ component.items = items
+ component.editing = true
+ component.selectionModel = selectionModel
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ selectionModel.toggle(items[0].id)
+ fixture.detectChanges()
+ expect(component.modelIsDirty).toBeTruthy()
+ let applyResult: ChangedItems
+ const closeSpy = jest.spyOn(component.dropdown, 'close')
+ component.apply.subscribe((result) => (applyResult = result))
+ const applyButton = Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
+ ).find((b) => b.textContent.includes('Apply'))
+ applyButton.dispatchEvent(new MouseEvent('click'))
+ expect(closeSpy).toHaveBeenCalled()
+ expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
+ })
+
+ it('should apply on close if enabled', () => {
+ component.items = items
+ component.editing = true
+ component.applyOnClose = true
+ component.selectionModel = selectionModel
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ selectionModel.toggle(items[0].id)
+ fixture.detectChanges()
+ expect(component.modelIsDirty).toBeTruthy()
+ let applyResult: ChangedItems
+ component.apply.subscribe((result) => (applyResult = result))
+ component.dropdown.close()
+ expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
+ })
+
+ it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
+ component.items = items
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+ expect(document.activeElement).toEqual(
+ component.listFilterTextInput.nativeElement
+ )
+ expect(
+ Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
+ ).filter((b) => b.textContent.includes('Tag'))
+ ).toHaveLength(2)
+ component.filterText = 'Tag2'
+ fixture.detectChanges()
+ expect(
+ Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
+ ).filter((b) => b.textContent.includes('Tag'))
+ ).toHaveLength(1)
+ component.dropdown.close()
+ expect(component.filterText).toHaveLength(0)
+ }))
+
+ it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
+ component.items = items
+ expect(component.selectionModel.getSelectedItems()).toEqual([])
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+ component.filterText = 'Tag2'
+ fixture.detectChanges()
+ const closeSpy = jest.spyOn(component.dropdown, 'close')
+ component.listFilterTextInput.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Enter' })
+ )
+ expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
+ tick(300)
+ expect(closeSpy).toHaveBeenCalled()
+ }))
+
+ it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
+ component.items = items
+ component.editing = true
+ let applyResult: ChangedItems
+ component.apply.subscribe((result) => (applyResult = result))
+ expect(component.selectionModel.getSelectedItems()).toEqual([])
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+ component.filterText = 'Tag2'
+ fixture.detectChanges()
+ component.listFilterTextInput.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Enter' })
+ )
+ expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
+ tick(300)
+ expect(applyResult).toEqual({ itemsToAdd: [items[1]], itemsToRemove: [] })
+ }))
+
+ it('should support arrow keyboard navigation', fakeAsync(() => {
+ component.items = items
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+ const filterInputEl: HTMLInputElement =
+ component.listFilterTextInput.nativeElement
+ expect(document.activeElement).toEqual(filterInputEl)
+ const itemButtons = Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
+ ).filter((b) => b.textContent.includes('Tag'))
+ filterInputEl.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(itemButtons[0])
+ itemButtons[0].dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(itemButtons[1])
+ itemButtons[1].dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(itemButtons[0])
+ itemButtons[0].dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(filterInputEl)
+ filterInputEl.value = 'foo'
+ component.filterText = 'foo'
+
+ // dont move focus if we're traversing the field
+ filterInputEl.selectionStart = 1
+ expect(document.activeElement).toEqual(filterInputEl)
+
+ // now we're at end, so move focus
+ filterInputEl.selectionStart = 3
+ filterInputEl.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(itemButtons[0])
+ }))
+
+ it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
+ component.items = items
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+ const filterInputEl: HTMLInputElement =
+ component.listFilterTextInput.nativeElement
+ expect(document.activeElement).toEqual(filterInputEl)
+ const itemButtons = Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
+ ).filter((b) => b.textContent.includes('Tag'))
+ filterInputEl.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
+ )
+ itemButtons[0].focus() // normally handled by browser
+ itemButtons[0].dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
+ )
+ itemButtons[1].focus() // normally handled by browser
+ itemButtons[1].dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'Tab',
+ shiftKey: true,
+ bubbles: true,
+ })
+ )
+ itemButtons[0].focus() // normally handled by browser
+ itemButtons[0].dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(itemButtons[1])
+ }))
+
+ it('should support arrow keyboard navigation after click', fakeAsync(() => {
+ component.items = items
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+ const filterInputEl: HTMLInputElement =
+ component.listFilterTextInput.nativeElement
+ expect(document.activeElement).toEqual(filterInputEl)
+ const itemButtons = Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
+ ).filter((b) => b.textContent.includes('Tag'))
+ fixture.nativeElement
+ .querySelector('app-toggleable-dropdown-button')
+ .dispatchEvent(new MouseEvent('click'))
+ itemButtons[0].focus() // normally handled by browser
+ expect(document.activeElement).toEqual(itemButtons[0])
+ itemButtons[0].dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
+ )
+ expect(document.activeElement).toEqual(itemButtons[1])
+ }))
+
+ it('should toggle logical operator', fakeAsync(() => {
+ component.items = items
+ component.manyToOne = true
+ selectionModel.set(items[0].id, ToggleableItemState.Selected)
+ selectionModel.set(items[1].id, ToggleableItemState.Selected)
+ component.selectionModel = selectionModel
+ let changedResult: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe(
+ (result) => (changedResult = result)
+ )
+
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+
+ expect(component.modifierToggleEnabled).toBeTruthy()
+ const operatorButtons: HTMLInputElement[] = Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
+ ).filter((b) => ['and', 'or'].includes(b.value))
+ expect(operatorButtons[0].checked).toBeTruthy()
+ operatorButtons[1].dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+ expect(selectionModel.logicalOperator).toEqual(LogicalOperator.Or)
+ expect(changedResult.logicalOperator).toEqual(LogicalOperator.Or)
+ }))
+
+ it('should toggle intersection include / exclude', fakeAsync(() => {
+ component.items = items
+ selectionModel.set(items[0].id, ToggleableItemState.Selected)
+ selectionModel.set(items[1].id, ToggleableItemState.Selected)
+ component.selectionModel = selectionModel
+ let changedResult: FilterableDropdownSelectionModel
+ component.selectionModelChange.subscribe(
+ (result) => (changedResult = result)
+ )
+
+ fixture.nativeElement
+ .querySelector('button')
+ .dispatchEvent(new MouseEvent('click')) // open
+ fixture.detectChanges()
+ tick(100)
+
+ expect(component.modifierToggleEnabled).toBeTruthy()
+ const intersectionButtons: HTMLInputElement[] = Array.from(
+ (fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
+ ).filter((b) => ['include', 'exclude'].includes(b.value))
+ expect(intersectionButtons[0].checked).toBeTruthy()
+ intersectionButtons[1].dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+ expect(selectionModel.intersection).toEqual(Intersection.Exclude)
+ expect(changedResult.intersection).toEqual(Intersection.Exclude)
+ expect(changedResult.getSelectedItems()).toEqual([])
+ expect(changedResult.getExcludedItems()).toEqual(items)
+ }))
+
+ it('FilterableDropdownSelectionModel should sort items by state', () => {
+ component.items = items
+ component.selectionModel = selectionModel
+ selectionModel.toggle(items[1].id)
+ selectionModel.apply()
+ expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]])
+ })
+})
toggle(id: number, fireEvent = true) {
let state = this.temporarySelectionStates.get(id)
if (
- state == null ||
+ state == undefined ||
(state != ToggleableItemState.Selected &&
state != ToggleableItemState.Excluded)
) {
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ ToggleableDropdownButtonComponent,
+ ToggleableItemState,
+} from './toggleable-dropdown-button.component'
+import { TagComponent } from '../../tag/tag.component'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+
+describe('ToggleableDropdownButtonComponent', () => {
+ let component: ToggleableDropdownButtonComponent
+ let fixture: ComponentFixture<ToggleableDropdownButtonComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [ToggleableDropdownButtonComponent, TagComponent],
+ providers: [],
+ imports: [],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(ToggleableDropdownButtonComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should recognize a tag', () => {
+ component.item = {
+ id: 1,
+ name: 'Test Tag',
+ is_inbox_tag: false,
+ } as PaperlessTag
+
+ fixture.detectChanges()
+ expect(component.isTag).toBeTruthy()
+ })
+
+ it('should report toggled state', () => {
+ expect(component.isChecked()).toBeFalsy()
+ expect(component.isPartiallyChecked()).toBeFalsy()
+ expect(component.isExcluded()).toBeFalsy()
+
+ component.state = ToggleableItemState.Selected
+ expect(component.isChecked()).toBeTruthy()
+ expect(component.isPartiallyChecked()).toBeFalsy()
+ expect(component.isExcluded()).toBeFalsy()
+
+ component.state = ToggleableItemState.PartiallySelected
+ expect(component.isPartiallyChecked()).toBeTruthy()
+ expect(component.isChecked()).toBeFalsy()
+ expect(component.isExcluded()).toBeFalsy()
+
+ component.state = ToggleableItemState.Excluded
+ expect(component.isExcluded()).toBeTruthy()
+ expect(component.isChecked()).toBeFalsy()
+ expect(component.isPartiallyChecked()).toBeFalsy()
+ })
+
+ it('should emit exclude event when selected and then toggled', () => {
+ let excludeResult
+ let toggleResult
+ component.state = ToggleableItemState.Selected
+ component.exclude.subscribe(() => (excludeResult = true))
+ component.toggle.subscribe(() => (toggleResult = true))
+ const button = fixture.nativeElement.querySelector('button')
+ button.dispatchEvent(new MouseEvent('click'))
+ expect(excludeResult).toBeTruthy()
+ expect(toggleResult).toBeFalsy()
+ })
+
+ it('should emit toggle event when not selected and then toggled', () => {
+ let excludeResult
+ let toggleResult
+ component.state = ToggleableItemState.Excluded
+ component.exclude.subscribe(() => (excludeResult = true))
+ component.toggle.subscribe(() => (toggleResult = true))
+ const button = fixture.nativeElement.querySelector('button')
+ button.dispatchEvent(new MouseEvent('click'))
+ expect(excludeResult).toBeFalsy()
+ expect(toggleResult).toBeTruthy()
+ })
+})
--- /dev/null
+import { Component } from '@angular/core'
+import { AbstractInputComponent } from './abstract-input'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+
+@Component({
+ template: `
+ <div>
+ <input
+ #inputField
+ type="text"
+ class="form-control"
+ [class.is-invalid]="error"
+ [id]="inputId"
+ [(ngModel)]="value"
+ (change)="onChange(value)"
+ [disabled]="disabled"
+ />
+ </div>
+ `,
+})
+class TestComponent extends AbstractInputComponent<string> {
+ constructor() {
+ super()
+ }
+}
+
+describe(`AbstractInputComponent`, () => {
+ let component: TestComponent
+ let fixture: ComponentFixture<TestComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent],
+ providers: [],
+ imports: [FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(TestComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should assign uuid', () => {
+ component.ngOnInit()
+ expect(component.inputId).not.toBeUndefined()
+ })
+
+ it('should support focus', () => {
+ const focusSpy = jest.spyOn(component.inputField.nativeElement, 'focus')
+ component.focus()
+ expect(focusSpy).toHaveBeenCalled()
+ })
+})
<div class="mb-3 form-check">
- <input type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
+ <input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
<label class="form-check-label" [for]="inputId">{{title}}</label>
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div>
</div>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { CheckComponent } from './check.component'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+
+describe('CheckComponent', () => {
+ let component: CheckComponent
+ let fixture: ComponentFixture<CheckComponent>
+ let input: HTMLInputElement
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [CheckComponent],
+ providers: [],
+ imports: [FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(CheckComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ input = component.inputField.nativeElement
+ })
+
+ it('should support use of checkbox', () => {
+ input.checked = true
+ input.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.value).toBeTruthy()
+
+ input.checked = false
+ input.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.value).toBeFalsy()
+ })
+})
-import { Component, forwardRef, Input, OnInit } from '@angular/core'
-import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
-import { v4 as uuidv4 } from 'uuid'
+import { Component, forwardRef } from '@angular/core'
+import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@Component({
</ng-template>
- <input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
+ <input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { ColorComponent } from './color.component'
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
+import { ColorSliderModule } from 'ngx-color/slider'
+
+describe('ColorComponent', () => {
+ let component: ColorComponent
+ let fixture: ComponentFixture<ColorComponent>
+ let input: HTMLInputElement
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [ColorComponent],
+ providers: [],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ NgbPopoverModule,
+ ColorSliderModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(ColorComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ input = component.inputField.nativeElement
+ })
+
+ it('should support use of input', () => {
+ input.value = '#ff0000'
+ component.colorChanged(input.value)
+ fixture.detectChanges()
+ expect(component.value).toEqual('#ff0000')
+ })
+
+ it('should set swatch color', () => {
+ const swatch: HTMLSpanElement = fixture.nativeElement.querySelector(
+ 'span.input-group-text'
+ )
+ expect(swatch.style.backgroundColor).toEqual('')
+ component.value = '#ff0000'
+ fixture.detectChanges()
+ expect(swatch.style.backgroundColor).toEqual('rgb(255, 0, 0)')
+ })
+
+ it('should show color slider popover', () => {
+ component.value = '#ff0000'
+ input.dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+ expect(
+ fixture.nativeElement.querySelector('ngb-popover-window')
+ ).not.toBeUndefined()
+ expect(
+ fixture.nativeElement.querySelector('color-slider')
+ ).not.toBeUndefined()
+ fixture.nativeElement
+ .querySelector('color-slider')
+ .dispatchEvent(new Event('change'))
+ })
+
+ it('should allow randomize color and update value', () => {
+ expect(component.value).toBeUndefined()
+ component.randomize()
+ expect(component.value).not.toBeUndefined()
+ })
+})
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
- <input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
+ <input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { DateComponent } from './date.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ NgbDateParserFormatter,
+ NgbDatepickerModule,
+} from '@ng-bootstrap/ng-bootstrap'
+import { RouterTestingModule } from '@angular/router/testing'
+import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
+
+describe('DateComponent', () => {
+ let component: DateComponent
+ let fixture: ComponentFixture<DateComponent>
+ let input: HTMLInputElement
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [DateComponent],
+ providers: [
+ {
+ provide: NgbDateParserFormatter,
+ useClass: LocalizedDateParserFormatter,
+ },
+ ],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ NgbDatepickerModule,
+ RouterTestingModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(DateComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ input = component.inputField.nativeElement
+ })
+
+ it('should support use of input field', () => {
+ input.value = '5/14/20'
+ input.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.value).toEqual({ day: 14, month: 5, year: 2020 })
+ })
+
+ it('should use localzed placeholder from settings', () => {
+ component.ngOnInit()
+ expect(component.placeholder).toEqual('mm/dd/yyyy')
+ })
+
+ it('should support suggestions', () => {
+ expect(component.value).toBeUndefined()
+ component.suggestions = ['2023-05-31', '2014-05-14']
+ fixture.detectChanges()
+ const suggestionAnchor: HTMLAnchorElement =
+ fixture.nativeElement.querySelector('a')
+ suggestionAnchor.click()
+ expect(component.value).toEqual({ day: 31, month: 5, year: 2023 })
+ })
+
+ it('should limit keyboard events', () => {
+ let event: KeyboardEvent = new KeyboardEvent('keypress', {
+ key: '9',
+ })
+ let eventSpy = jest.spyOn(event, 'preventDefault')
+ input.dispatchEvent(event)
+ expect(eventSpy).not.toHaveBeenCalled()
+
+ event = new KeyboardEvent('keypress', {
+ key: '{',
+ })
+ eventSpy = jest.spyOn(event, 'preventDefault')
+ input.dispatchEvent(event)
+ expect(eventSpy).toHaveBeenCalled()
+ })
+
+ it('should support paste', () => {
+ expect(component.value).toBeUndefined()
+ const date = '5/4/20'
+ const clipboardData = {
+ dropEffect: null,
+ effectAllowed: null,
+ files: null,
+ items: null,
+ types: null,
+ clearData: null,
+ getData: () => date,
+ setData: null,
+ setDragImage: null,
+ }
+ const event = new Event('paste')
+ event['clipboardData'] = clipboardData
+ input.dispatchEvent(event)
+ expect(component.value).toEqual({ day: 4, month: 5, year: 2020 })
+ })
+})
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
- <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
+ <input #inputField type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
</div>
<div class="invalid-feedback">
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { NumberComponent } from './number.component'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { of } from 'rxjs'
+
+describe('NumberComponent', () => {
+ let component: NumberComponent
+ let fixture: ComponentFixture<NumberComponent>
+ let input: HTMLInputElement
+ let documentService: DocumentService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [NumberComponent],
+ providers: [DocumentService],
+ imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(NumberComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ documentService = TestBed.inject(DocumentService)
+ fixture.detectChanges()
+ input = component.inputField.nativeElement
+ })
+
+ // TODO: why doesnt this work?
+ // it('should support use of input field', () => {
+ // expect(component.value).toBeUndefined()
+ // input.stepUp()
+ // console.log(input.value);
+
+ // input.dispatchEvent(new Event('change'))
+ // fixture.detectChanges()
+ // expect(component.value).toEqual('3')
+ // })
+
+ it('should support +1 ASN', () => {
+ const listAllSpy = jest.spyOn(documentService, 'listFiltered')
+ listAllSpy
+ .mockReturnValueOnce(
+ of({
+ count: 1,
+ all: [1],
+ results: [
+ {
+ id: 1,
+ archive_serial_number: 1000,
+ },
+ ],
+ })
+ )
+ .mockReturnValueOnce(
+ of({
+ count: 0,
+ all: [],
+ results: [],
+ })
+ )
+ expect(component.value).toBeUndefined()
+ component.nextAsn()
+ expect(component.value).toEqual(1001)
+
+ // this time results are empty
+ component.value = undefined
+ component.nextAsn()
+ expect(component.value).toEqual(1)
+
+ component.value = 1002
+ component.nextAsn()
+ expect(component.value).toEqual(1002)
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ ReactiveFormsModule,
+ NG_VALUE_ACCESSOR,
+} from '@angular/forms'
+import { PasswordComponent } from './password.component'
+
+describe('PasswordComponent', () => {
+ let component: PasswordComponent
+ let fixture: ComponentFixture<PasswordComponent>
+ let input: HTMLInputElement
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [PasswordComponent],
+ providers: [],
+ imports: [FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(PasswordComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ input = component.inputField.nativeElement
+ })
+
+ it('should support use of input field', () => {
+ expect(component.value).toBeUndefined()
+ // TODO: why doesnt this work?
+ // input.value = 'foo'
+ // input.dispatchEvent(new Event('change'))
+ // fixture.detectChanges()
+ // expect(component.value).toEqual('foo')
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ ReactiveFormsModule,
+ NG_VALUE_ACCESSOR,
+} from '@angular/forms'
+import { PermissionsFormComponent } from './permissions-form.component'
+import { SelectComponent } from '../../select/select.component'
+import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
+import { PermissionsGroupComponent } from '../permissions-group/permissions-group.component'
+import { PermissionsUserComponent } from '../permissions-user/permissions-user.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { NgSelectModule } from '@ng-select/ng-select'
+
+describe('PermissionsFormComponent', () => {
+ let component: PermissionsFormComponent
+ let fixture: ComponentFixture<PermissionsFormComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ PermissionsFormComponent,
+ SelectComponent,
+ PermissionsGroupComponent,
+ PermissionsUserComponent,
+ ],
+ providers: [],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ NgbAccordionModule,
+ HttpClientTestingModule,
+ NgSelectModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(PermissionsFormComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should support use of select for owner', () => {
+ const changeSpy = jest.spyOn(component, 'onChange')
+ component.ngOnInit()
+ component.users = [
+ {
+ id: 2,
+ username: 'foo',
+ },
+ {
+ id: 3,
+ username: 'bar',
+ },
+ ]
+ component.form.get('owner').patchValue(2)
+ fixture.detectChanges()
+ expect(changeSpy).toHaveBeenCalledWith({
+ owner: 2,
+ set_permissions: {
+ view: { users: [], groups: [] },
+ change: { users: [], groups: [] },
+ },
+ })
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { PermissionsGroupComponent } from './permissions-group.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { GroupService } from 'src/app/services/rest/group.service'
+import { of } from 'rxjs'
+
+describe('PermissionsGroupComponent', () => {
+ let component: PermissionsGroupComponent
+ let fixture: ComponentFixture<PermissionsGroupComponent>
+ let groupService: GroupService
+ let groupServiceSpy
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [PermissionsGroupComponent],
+ providers: [GroupService],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ NgSelectModule,
+ ],
+ }).compileComponents()
+
+ groupService = TestBed.inject(GroupService)
+ groupServiceSpy = jest.spyOn(groupService, 'listAll').mockReturnValue(
+ of({
+ count: 2,
+ all: [2, 3],
+ results: [
+ {
+ id: 2,
+ name: 'Group 2',
+ },
+ {
+ id: 3,
+ name: 'Group 3',
+ },
+ ],
+ })
+ )
+ fixture = TestBed.createComponent(PermissionsGroupComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should get groups, support use of select', () => {
+ component.writeValue({ id: 2, name: 'Group 2' })
+ expect(component.value).toEqual({ id: 2, name: 'Group 2' })
+ expect(groupServiceSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { PermissionsUserComponent } from './permissions-user.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { GroupService } from 'src/app/services/rest/group.service'
+import { of } from 'rxjs'
+import { UserService } from 'src/app/services/rest/user.service'
+
+describe('PermissionsUserComponent', () => {
+ let component: PermissionsUserComponent
+ let fixture: ComponentFixture<PermissionsUserComponent>
+ let userService: UserService
+ let userServiceSpy
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [PermissionsUserComponent],
+ providers: [UserService],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ NgSelectModule,
+ ],
+ }).compileComponents()
+
+ userService = TestBed.inject(UserService)
+ userServiceSpy = jest.spyOn(userService, 'listAll').mockReturnValue(
+ of({
+ count: 2,
+ all: [2, 3],
+ results: [
+ {
+ id: 2,
+ name: 'User 2',
+ },
+ {
+ id: 3,
+ name: 'User 3',
+ },
+ ],
+ })
+ )
+ fixture = TestBed.createComponent(PermissionsUserComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should get users, support use of select', () => {
+ component.writeValue({ id: 2, name: 'User 2' })
+ expect(component.value).toEqual({ id: 2, name: 'User 2' })
+ expect(userServiceSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import {
+ FormsModule,
+ ReactiveFormsModule,
+ NG_VALUE_ACCESSOR,
+} from '@angular/forms'
+import { SelectComponent } from './select.component'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import {
+ DEFAULT_MATCHING_ALGORITHM,
+ MATCH_ALL,
+} from 'src/app/data/matching-model'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { RouterTestingModule } from '@angular/router/testing'
+
+const items: PaperlessTag[] = [
+ {
+ id: 1,
+ name: 'Tag1',
+ is_inbox_tag: false,
+ matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
+ },
+ {
+ id: 2,
+ name: 'Tag2',
+ is_inbox_tag: true,
+ matching_algorithm: MATCH_ALL,
+ match: 'str',
+ },
+ {
+ id: 10,
+ name: 'Tag10',
+ is_inbox_tag: false,
+ matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
+ },
+]
+
+describe('SelectComponent', () => {
+ let component: SelectComponent
+ let fixture: ComponentFixture<SelectComponent>
+ let input: HTMLInputElement
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [SelectComponent],
+ providers: [],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ RouterTestingModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(SelectComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should support private items', () => {
+ component.value = 3
+ component.items = items
+ expect(component.items).toContainEqual({
+ id: 3,
+ name: 'Private',
+ private: true,
+ })
+
+ component.checkForPrivateItems([4, 5])
+ expect(component.items).toContainEqual({
+ id: 4,
+ name: 'Private',
+ private: true,
+ })
+ expect(component.items).toContainEqual({
+ id: 5,
+ name: 'Private',
+ private: true,
+ })
+ })
+
+ it('should support suggestions', () => {
+ expect(component.value).toBeUndefined()
+ component.items = items
+ component.suggestions = [1, 2]
+ fixture.detectChanges()
+ const suggestionAnchor: HTMLAnchorElement =
+ fixture.nativeElement.querySelector('a')
+ suggestionAnchor.click()
+ expect(component.value).toEqual(1)
+ })
+
+ it('should support create new and emit the value', () => {
+ expect(component.allowCreateNew).toBeFalsy()
+ component.items = items
+ let createNewVal
+ component.createNew.subscribe((v) => (createNewVal = v))
+ expect(component.allowCreateNew).toBeTruthy()
+ component.onSearch({ term: 'foo' })
+ component.addItem(undefined)
+ expect(createNewVal).toEqual('foo')
+ component.addItem('bar')
+ expect(createNewVal).toEqual('bar')
+ component.onSearch({ term: 'baz' })
+ component.clickNew()
+ expect(createNewVal).toEqual('baz')
+ })
+
+ it('should clear search term on blur after delay', fakeAsync(() => {
+ const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
+ component.onBlur()
+ tick(3000)
+ expect(clearSpy).toHaveBeenCalled()
+ }))
+})
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import {
+ FormsModule,
+ ReactiveFormsModule,
+ NG_VALUE_ACCESSOR,
+} from '@angular/forms'
+import { TagsComponent } from './tags.component'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import {
+ DEFAULT_MATCHING_ALGORITHM,
+ MATCH_ALL,
+} from 'src/app/data/matching-model'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { RouterTestingModule } from '@angular/router/testing'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { of } from 'rxjs'
+import { TagService } from 'src/app/services/rest/tag.service'
+import {
+ NgbModal,
+ NgbModalModule,
+ NgbModalRef,
+} from '@ng-bootstrap/ng-bootstrap'
+
+const tags: PaperlessTag[] = [
+ {
+ id: 1,
+ name: 'Tag1',
+ is_inbox_tag: false,
+ matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
+ },
+ {
+ id: 2,
+ name: 'Tag2',
+ is_inbox_tag: true,
+ matching_algorithm: MATCH_ALL,
+ match: 'str',
+ },
+ {
+ id: 10,
+ name: 'Tag10',
+ is_inbox_tag: false,
+ matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
+ },
+]
+
+describe('TagsComponent', () => {
+ let component: TagsComponent
+ let fixture: ComponentFixture<TagsComponent>
+ let input: HTMLInputElement
+ let modalService: NgbModal
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [TagsComponent],
+ providers: [
+ {
+ provide: TagService,
+ useValue: {
+ listAll: () => of(tags),
+ },
+ },
+ ],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ NgSelectModule,
+ RouterTestingModule,
+ HttpClientTestingModule,
+ NgbModalModule,
+ ],
+ }).compileComponents()
+
+ modalService = TestBed.inject(NgbModal)
+ fixture = TestBed.createComponent(TagsComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+
+ window.PointerEvent = MouseEvent as any
+ })
+
+ it('should support suggestions', () => {
+ expect(component.value).toBeUndefined()
+ component.value = []
+ component.tags = tags
+ component.suggestions = [1, 2]
+ fixture.detectChanges()
+ const suggestionAnchor: HTMLAnchorElement =
+ fixture.nativeElement.querySelector('a')
+ suggestionAnchor.click()
+ expect(component.value).toEqual([1])
+ })
+
+ it('should support create new and open a modal', () => {
+ let activeInstances: NgbModalRef[]
+ modalService.activeInstances.subscribe((v) => (activeInstances = v))
+ component.createTag('foo')
+ expect(modalService.hasOpenModals()).toBeTruthy()
+ expect(activeInstances[0].componentInstance.object.name).toEqual('foo')
+ })
+
+ it('should support create new using last search term and open a modal', () => {
+ let activeInstances: NgbModalRef[]
+ modalService.activeInstances.subscribe((v) => (activeInstances = v))
+ component.onSearch({ term: 'bar' })
+ component.createTag()
+ expect(modalService.hasOpenModals()).toBeTruthy()
+ expect(activeInstances[0].componentInstance.object.name).toEqual('bar')
+ })
+
+ it('should clear search term on blur after delay', fakeAsync(() => {
+ const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
+ component.onBlur()
+ tick(3000)
+ expect(clearSpy).toHaveBeenCalled()
+ }))
+
+ it('support remove tags', () => {
+ component.tags = tags
+ component.value = [1, 2]
+ component.removeTag(new PointerEvent('point'), 2)
+ expect(component.value).toEqual([1])
+
+ component.disabled = true
+ component.removeTag(new PointerEvent('point'), 1)
+ expect(component.value).toEqual([1])
+ })
+
+ it('should get tags', () => {
+ expect(component.getTag(2)).toBeNull()
+ component.tags = tags
+ expect(component.getTag(2)).toEqual(tags[1])
+ expect(component.getTag(4)).toBeUndefined()
+ })
+})
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { TagService } from 'src/app/services/rest/tag.service'
+import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
@Component({
providers: [
var modal = this.modalService.open(TagEditDialogComponent, {
backdrop: 'static',
})
- modal.componentInstance.dialogMode = 'create'
+ modal.componentInstance.dialogMode = EditDialogMode.CREATE
if (name) modal.componentInstance.object = { name: name }
else if (this._lastSearchTerm)
modal.componentInstance.object = { name: this._lastSearchTerm }
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ ReactiveFormsModule,
+ NG_VALUE_ACCESSOR,
+} from '@angular/forms'
+import { TextComponent } from './text.component'
+
+describe('TextComponent', () => {
+ let component: TextComponent
+ let fixture: ComponentFixture<TextComponent>
+ let input: HTMLInputElement
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [TextComponent],
+ providers: [],
+ imports: [FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(TextComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ input = component.inputField.nativeElement
+ })
+
+ it('should support use of input field', () => {
+ expect(component.value).toBeUndefined()
+ // TODO: why doesnt this work?
+ // input.value = 'foo'
+ // input.dispatchEvent(new Event('change'))
+ // fixture.detectChanges()
+ // expect(component.value).toEqual('foo')
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { Title } from '@angular/platform-browser'
+import { PageHeaderComponent } from './page-header.component'
+import { environment } from 'src/environments/environment'
+
+describe('PageHeaderComponent', () => {
+ let component: PageHeaderComponent
+ let fixture: ComponentFixture<PageHeaderComponent>
+ let titleService: Title
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [PageHeaderComponent],
+ providers: [],
+ imports: [],
+ }).compileComponents()
+
+ titleService = TestBed.inject(Title)
+ fixture = TestBed.createComponent(PageHeaderComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should display title + subtitle', () => {
+ component.title = 'Foo'
+ component.subTitle = 'Bar'
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain('FooBar')
+ })
+
+ it('should set html title', () => {
+ const titleSpy = jest.spyOn(titleService, 'setTitle')
+ component.title = 'Foo Bar'
+ expect(titleSpy).toHaveBeenCalledWith(`Foo Bar - ${environment.appTitle}`)
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { PermissionsDialogComponent } from './permissions-dialog.component'
+import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { UserService } from 'src/app/services/rest/user.service'
+import { of } from 'rxjs'
+import { PermissionsFormComponent } from '../input/permissions/permissions-form/permissions-form.component'
+import { SelectComponent } from '../input/select/select.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
+import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
+
+const set_permissions = {
+ owner: 10,
+ set_permissions: {
+ view: {
+ users: [1],
+ groups: [],
+ },
+ edit: {
+ users: [1],
+ groups: [],
+ },
+ },
+}
+
+describe('PermissionsDialogComponent', () => {
+ let component: PermissionsDialogComponent
+ let fixture: ComponentFixture<PermissionsDialogComponent>
+ let modal: NgbActiveModal
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ PermissionsDialogComponent,
+ SafeHtmlPipe,
+ SelectComponent,
+ PermissionsFormComponent,
+ PermissionsUserComponent,
+ PermissionsGroupComponent,
+ ],
+ providers: [
+ NgbActiveModal,
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 1,
+ username: 'user1',
+ },
+ {
+ id: 10,
+ username: 'user10',
+ },
+ ],
+ }),
+ },
+ },
+ ],
+ imports: [
+ HttpClientTestingModule,
+ NgSelectModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ modal = TestBed.inject(NgbActiveModal)
+ fixture = TestBed.createComponent(PermissionsDialogComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should return permissions', () => {
+ component.form.get('permissions_form').setValue(set_permissions)
+ expect(component.permissions).toEqual(set_permissions)
+ })
+
+ it('should close modal on cancel', () => {
+ const closeSpy = jest.spyOn(modal, 'close')
+ component.cancelClicked()
+ expect(closeSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { of } from 'rxjs'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { UserService } from 'src/app/services/rest/user.service'
+import {
+ OwnerFilterType,
+ PermissionsFilterDropdownComponent,
+ PermissionsSelectionModel,
+} from './permissions-filter-dropdown.component'
+import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
+import { SettingsService } from 'src/app/services/settings.service'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+
+const currentUserID = 13
+
+describe('PermissionsFilterDropdownComponent', () => {
+ let component: PermissionsFilterDropdownComponent
+ let fixture: ComponentFixture<PermissionsFilterDropdownComponent>
+ let ownerFilterSetResult: PermissionsSelectionModel
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ PermissionsFilterDropdownComponent,
+ ClearableBadgeComponent,
+ IfPermissionsDirective,
+ ],
+ providers: [
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 1,
+ username: 'user1',
+ },
+ {
+ id: 10,
+ username: 'user10',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ {
+ provide: SettingsService,
+ useValue: {
+ currentUser: {
+ id: currentUserID,
+ },
+ },
+ },
+ ],
+ imports: [
+ HttpClientTestingModule,
+ NgSelectModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(PermissionsFilterDropdownComponent)
+ component = fixture.componentInstance
+ component.ownerFilterSet.subscribe(
+ (model) => (ownerFilterSetResult = model)
+ )
+ component.selectionModel = new PermissionsSelectionModel()
+
+ fixture.detectChanges()
+ })
+
+ it('should report is active', () => {
+ component.setFilter(OwnerFilterType.NONE)
+ expect(component.isActive).toBeFalsy()
+ component.setFilter(OwnerFilterType.OTHERS)
+ expect(component.isActive).toBeTruthy()
+ component.setFilter(OwnerFilterType.NONE)
+ component.selectionModel.hideUnowned = true
+ expect(component.isActive).toBeTruthy()
+ })
+
+ it('should support reset', () => {
+ component.setFilter(OwnerFilterType.OTHERS)
+ expect(component.selectionModel.ownerFilter).not.toEqual(
+ OwnerFilterType.NONE
+ )
+ component.reset()
+ expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
+ })
+
+ it('should toggle owner filter type when users selected', () => {
+ component.selectionModel.ownerFilter = OwnerFilterType.NONE
+
+ // this would normally be done by select component
+ component.selectionModel.includeUsers = [12]
+ component.onUserSelect()
+ expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.OTHERS)
+
+ // this would normally be done by select component
+ component.selectionModel.includeUsers = null
+ component.onUserSelect()
+
+ expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
+ })
+ it('should emit a selection model depending on the type of owner filter set', () => {
+ component.selectionModel.ownerFilter = OwnerFilterType.NONE
+
+ component.setFilter(OwnerFilterType.SELF)
+ expect(ownerFilterSetResult).toEqual({
+ excludeUsers: [],
+ hideUnowned: false,
+ includeUsers: [],
+ ownerFilter: OwnerFilterType.SELF,
+ userID: currentUserID,
+ })
+
+ component.setFilter(OwnerFilterType.NOT_SELF)
+ expect(ownerFilterSetResult).toEqual({
+ excludeUsers: [currentUserID],
+ hideUnowned: false,
+ includeUsers: [],
+ ownerFilter: OwnerFilterType.NOT_SELF,
+ userID: null,
+ })
+
+ component.setFilter(OwnerFilterType.NONE)
+ expect(ownerFilterSetResult).toEqual({
+ excludeUsers: [],
+ hideUnowned: false,
+ includeUsers: [],
+ ownerFilter: OwnerFilterType.NONE,
+ userID: null,
+ })
+
+ component.setFilter(OwnerFilterType.UNOWNED)
+ expect(ownerFilterSetResult).toEqual({
+ excludeUsers: [],
+ hideUnowned: false,
+ includeUsers: [],
+ ownerFilter: OwnerFilterType.UNOWNED,
+ userID: null,
+ })
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { PermissionsSelectComponent } from './permissions-select.component'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import {
+ PermissionAction,
+ PermissionType,
+} from 'src/app/services/permissions.service'
+import { By } from '@angular/platform-browser'
+
+const permissions = [
+ 'add_document',
+ 'view_document',
+ 'change_document',
+ 'delete_document',
+ 'change_tag',
+ 'view_documenttype',
+]
+
+const inheritedPermissions = ['change_tag', 'view_documenttype']
+
+describe('PermissionsSelectComponent', () => {
+ let component: PermissionsSelectComponent
+ let fixture: ComponentFixture<PermissionsSelectComponent>
+ let permissionsChangeResult: Permissions
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [PermissionsSelectComponent],
+ providers: [],
+ imports: [FormsModule, ReactiveFormsModule, NgbModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(PermissionsSelectComponent)
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ component = fixture.componentInstance
+ component.registerOnChange((r) => (permissionsChangeResult = r))
+ fixture.detectChanges()
+ })
+
+ it('should create controls for all PermissionType and PermissionAction', () => {
+ expect(Object.values(component.form.controls)).toHaveLength(
+ Object.keys(PermissionType).length
+ )
+ for (var type in component.form.controls) {
+ expect(
+ Object.values(component.form.controls[type].controls)
+ ).toHaveLength(Object.keys(PermissionAction).length)
+ }
+ // coverage
+ component.registerOnTouched(() => {})
+ component.setDisabledState(true)
+ })
+
+ it('should allow toggle all on / off', () => {
+ component.ngOnInit()
+ expect(component.typesWithAllActions.values).toHaveLength(0)
+ component.toggleAll({ target: { checked: true } }, 'Tag')
+ expect(component.typesWithAllActions).toContain('Tag')
+ component.toggleAll({ target: { checked: false } }, 'Tag')
+ expect(component.typesWithAllActions.values).toHaveLength(0)
+ })
+
+ it('should update on permissions set', () => {
+ component.ngOnInit()
+ component.writeValue(permissions)
+ expect(permissionsChangeResult).toEqual(permissions)
+ expect(component.typesWithAllActions).toContain('Document')
+ })
+
+ it('should update checkboxes on permissions set', () => {
+ component.ngOnInit()
+ component.writeValue(permissions)
+ fixture.detectChanges()
+ const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
+ expect(input1.nativeElement.checked).toBeTruthy()
+ const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
+ expect(input2.nativeElement.checked).toBeTruthy()
+ })
+
+ it('disable checkboxes when permissions are inherited', () => {
+ component.ngOnInit()
+ component.inheritedPermissions = inheritedPermissions
+ expect(component.isInherited('Document', 'Add')).toBeFalsy()
+ expect(component.isInherited('Document')).toBeFalsy()
+ expect(component.isInherited('Tag', 'Change')).toBeTruthy()
+ const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
+ expect(input1.nativeElement.disabled).toBeFalsy()
+ const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
+ expect(input2.nativeElement.disabled).toBeTruthy()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { SelectComponent } from '../input/select/select.component'
+import { SelectDialogComponent } from './select-dialog.component'
+
+describe('SelectDialogComponent', () => {
+ let component: SelectDialogComponent
+ let fixture: ComponentFixture<SelectDialogComponent>
+ let modal: NgbActiveModal
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [SelectDialogComponent, SelectComponent],
+ providers: [NgbActiveModal],
+ imports: [NgSelectModule, FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ modal = TestBed.inject(NgbActiveModal)
+ fixture = TestBed.createComponent(SelectDialogComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should close modal on cancel', () => {
+ const closeSpy = jest.spyOn(modal, 'close')
+ component.cancelClicked()
+ expect(closeSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { TagComponent } from './tag.component'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import { By } from '@angular/platform-browser'
+
+const tag: PaperlessTag = {
+ id: 1,
+ color: '#ff0000',
+ name: 'Tag1',
+}
+
+describe('TagComponent', () => {
+ let component: TagComponent
+ let fixture: ComponentFixture<TagComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [TagComponent],
+ providers: [],
+ imports: [],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(TagComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should create tag with background color', () => {
+ component.tag = tag
+ fixture.detectChanges()
+ expect(
+ fixture.debugElement.query(By.css('span')).nativeElement.style
+ .backgroundColor
+ ).toEqual('rgb(255, 0, 0)')
+ })
+
+ it('should handle private tags', () => {
+ expect(
+ fixture.debugElement.query(By.css('span')).nativeElement.textContent
+ ).toEqual('Private')
+ })
+
+ it('should support clickable option', () => {
+ component.tag = tag
+ fixture.detectChanges()
+ expect(fixture.debugElement.query(By.css('a.badge'))).toBeNull()
+ component.clickable = true
+ fixture.detectChanges()
+ expect(fixture.debugElement.query(By.css('a.badge'))).not.toBeNull()
+ })
+})
--- /dev/null
+import {
+ TestBed,
+ discardPeriodicTasks,
+ fakeAsync,
+ flush,
+} from '@angular/core/testing'
+import { ToastService } from 'src/app/services/toast.service'
+import { ToastsComponent } from './toasts.component'
+import { ComponentFixture } from '@angular/core/testing'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { of } from 'rxjs'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+
+describe('ToastsComponent', () => {
+ let component: ToastsComponent
+ let fixture: ComponentFixture<ToastsComponent>
+ let toastService: ToastService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [ToastsComponent],
+ imports: [HttpClientTestingModule, NgbModule],
+ providers: [
+ {
+ provide: ToastService,
+ useValue: {
+ getToasts: () =>
+ of([
+ {
+ title: 'Title',
+ content: 'content',
+ delay: 5000,
+ },
+ {
+ title: 'Error',
+ content: 'Error content',
+ delay: 5000,
+ error: new Error('Error message'),
+ },
+ ]),
+ },
+ },
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(ToastsComponent)
+ component = fixture.componentInstance
+
+ toastService = TestBed.inject(ToastService)
+
+ fixture.detectChanges()
+ })
+
+ it('should call getToasts and return toasts', fakeAsync(() => {
+ const spy = jest.spyOn(toastService, 'getToasts').mockReset()
+
+ component.ngOnInit()
+ fixture.detectChanges()
+
+ expect(spy).toHaveBeenCalled()
+ expect(component.toasts).toContainEqual({
+ title: 'Title',
+ content: 'content',
+ delay: 5000,
+ })
+
+ component.ngOnDestroy()
+ flush()
+ discardPeriodicTasks()
+ }))
+
+ it('should show a toast', fakeAsync(() => {
+ component.ngOnInit()
+ fixture.detectChanges()
+
+ expect(fixture.nativeElement.textContent).toContain('Title')
+
+ component.ngOnDestroy()
+ flush()
+ discardPeriodicTasks()
+ }))
+
+ it('should show an error if given with toast', fakeAsync(() => {
+ component.ngOnInit()
+ fixture.detectChanges()
+
+ expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
+ expect(fixture.nativeElement.textContent).toContain('Error message')
+
+ component.ngOnDestroy()
+ flush()
+ discardPeriodicTasks()
+ }))
+})
toasts: Toast[] = []
ngOnDestroy(): void {
- this.subscription.unsubscribe()
+ this.subscription?.unsubscribe()
}
ngOnInit(): void {
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { DashboardComponent } from './dashboard.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { SettingsService } from 'src/app/services/settings.service'
+import { StatisticsWidgetComponent } from './widgets/statistics-widget/statistics-widget.component'
+import { PageHeaderComponent } from '../common/page-header/page-header.component'
+import { WidgetFrameComponent } from './widgets/widget-frame/widget-frame.component'
+import { UploadFileWidgetComponent } from './widgets/upload-file-widget/upload-file-widget.component'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { By } from '@angular/platform-browser'
+import { SavedViewWidgetComponent } from './widgets/saved-view-widget/saved-view-widget.component'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { NgxFileDropModule } from 'ngx-file-drop'
+import { RouterTestingModule } from '@angular/router/testing'
+import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
+
+describe('DashboardComponent', () => {
+ let component: DashboardComponent
+ let fixture: ComponentFixture<DashboardComponent>
+ let settingsService: SettingsService
+ let tourService: TourService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DashboardComponent,
+ StatisticsWidgetComponent,
+ PageHeaderComponent,
+ WidgetFrameComponent,
+ UploadFileWidgetComponent,
+ IfPermissionsDirective,
+ SavedViewWidgetComponent,
+ ],
+ providers: [
+ PermissionsGuard,
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ {
+ provide: SavedViewService,
+ useValue: {
+ dashboardViews: [
+ {
+ id: 1,
+ name: 'saved view 1',
+ show_on_dashboard: true,
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: [],
+ },
+ {
+ id: 2,
+ name: 'saved view 2',
+ show_on_dashboard: true,
+ sort_field: 'created',
+ sort_reverse: true,
+ filter_rules: [],
+ },
+ ],
+ },
+ },
+ ],
+ imports: [
+ NgbAlertModule,
+ HttpClientTestingModule,
+ NgxFileDropModule,
+ RouterTestingModule,
+ TourNgBootstrapModule,
+ ],
+ }).compileComponents()
+
+ settingsService = TestBed.inject(SettingsService)
+ settingsService.currentUser = {
+ first_name: 'Foo',
+ last_name: 'Bar',
+ }
+ tourService = TestBed.inject(TourService)
+ fixture = TestBed.createComponent(DashboardComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should show a welcome message', () => {
+ expect(component.subtitle).toEqual(`Hello Foo, welcome to Paperless-ngx`)
+ settingsService.currentUser = {
+ id: 1,
+ }
+ expect(component.subtitle).toEqual(`Welcome to Paperless-ngx`)
+ })
+
+ it('should show dashboard widgets', () => {
+ expect(
+ fixture.debugElement.queryAll(By.directive(SavedViewWidgetComponent))
+ ).toHaveLength(2)
+ })
+
+ it('should end tour service if still running and welcome widget dismissed', () => {
+ jest.spyOn(tourService, 'getStatus').mockReturnValueOnce(1)
+ const endSpy = jest.spyOn(tourService, 'end')
+ component.completeTour()
+ expect(endSpy).toHaveBeenCalled()
+ })
+
+ it('should save tour completion if it was stopped and welcome widget dismissed', () => {
+ jest.spyOn(tourService, 'getStatus').mockReturnValueOnce(0)
+ const settingsCompleteTourSpy = jest.spyOn(settingsService, 'completeTour')
+ component.completeTour()
+ expect(settingsCompleteTourSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { Router } from '@angular/router'
+import { RouterTestingModule } from '@angular/router/testing'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { of, Subject } from 'rxjs'
+import { routes } from 'src/app/app-routing.module'
+import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
+import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
+import {
+ ConsumerStatusService,
+ FileStatus,
+} from 'src/app/services/consumer-status.service'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
+import { SavedViewWidgetComponent } from './saved-view-widget.component'
+
+const savedView: PaperlessSavedView = {
+ id: 1,
+ name: 'Saved View 1',
+ sort_field: 'added',
+ sort_reverse: true,
+ show_in_sidebar: true,
+ show_on_dashboard: true,
+ filter_rules: [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '1,2',
+ },
+ ],
+}
+
+const documentResults = [
+ {
+ id: 2,
+ title: 'doc2',
+ },
+ {
+ id: 3,
+ title: 'doc3',
+ },
+]
+
+describe('SavedViewWidgetComponent', () => {
+ let component: SavedViewWidgetComponent
+ let fixture: ComponentFixture<SavedViewWidgetComponent>
+ let documentService: DocumentService
+ let consumerStatusService: ConsumerStatusService
+ let documentListViewService: DocumentListViewService
+ let router: Router
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ SavedViewWidgetComponent,
+ WidgetFrameComponent,
+ IfPermissionsDirective,
+ CustomDatePipe,
+ DocumentTitlePipe,
+ ],
+ providers: [
+ PermissionsGuard,
+ DocumentService,
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ CustomDatePipe,
+ DatePipe,
+ ],
+ imports: [
+ HttpClientTestingModule,
+ NgbModule,
+ RouterTestingModule.withRoutes(routes),
+ ],
+ }).compileComponents()
+
+ documentService = TestBed.inject(DocumentService)
+ consumerStatusService = TestBed.inject(ConsumerStatusService)
+ documentListViewService = TestBed.inject(DocumentListViewService)
+ router = TestBed.inject(Router)
+ fixture = TestBed.createComponent(SavedViewWidgetComponent)
+ component = fixture.componentInstance
+ component.savedView = savedView
+
+ fixture.detectChanges()
+ })
+
+ it('should show a list of documents', () => {
+ jest.spyOn(documentService, 'listFiltered').mockReturnValue(
+ of({
+ all: [2, 3],
+ count: 2,
+ results: documentResults,
+ })
+ )
+ component.ngOnInit()
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain('doc2')
+ expect(fixture.debugElement.nativeElement.textContent).toContain('doc3')
+ })
+
+ it('should call api endpoint and load results', () => {
+ const listAllSpy = jest.spyOn(documentService, 'listFiltered')
+ listAllSpy.mockReturnValue(
+ of({
+ all: [2, 3],
+ count: 2,
+ results: documentResults,
+ })
+ )
+ component.ngOnInit()
+ expect(listAllSpy).toHaveBeenCalledWith(
+ 1,
+ 10,
+ savedView.sort_field,
+ savedView.sort_reverse,
+ savedView.filter_rules,
+ {
+ truncate_content: true,
+ }
+ )
+ fixture.detectChanges()
+ expect(component.documents).toEqual(documentResults)
+ })
+
+ it('should reload on document consumption finished', () => {
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
+ .mockReturnValue(fileStatusSubject)
+ const reloadSpy = jest.spyOn(component, 'reload')
+ component.ngOnInit()
+ fileStatusSubject.next(new FileStatus())
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+
+ it('should navigate on showAll', () => {
+ const routerSpy = jest.spyOn(router, 'navigate')
+ component.showAll()
+ expect(routerSpy).toHaveBeenCalledWith(['view', savedView.id])
+ savedView.show_in_sidebar = false
+ component.showAll()
+ expect(routerSpy).toHaveBeenCalledWith(['documents'], {
+ queryParams: { view: savedView.id },
+ })
+ })
+
+ it('should navigate via quickfilter on click tag', () => {
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.clickTag({ id: 11, name: 'Tag11' }, new MouseEvent('click'))
+ expect(qfSpy).toHaveBeenCalledWith([
+ { rule_type: FILTER_HAS_TAGS_ALL, value: '11' },
+ ])
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { StatisticsWidgetComponent } from './statistics-widget.component'
+import { ComponentFixture } from '@angular/core/testing'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
+import { environment } from 'src/environments/environment'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from 'src/app/app-routing.module'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+
+describe('StatisticsWidgetComponent', () => {
+ let component: StatisticsWidgetComponent
+ let fixture: ComponentFixture<StatisticsWidgetComponent>
+ let httpTestingController: HttpTestingController
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [StatisticsWidgetComponent, WidgetFrameComponent],
+ providers: [PermissionsGuard],
+ imports: [
+ HttpClientTestingModule,
+ NgbModule,
+ RouterTestingModule.withRoutes(routes),
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(StatisticsWidgetComponent)
+ component = fixture.componentInstance
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+
+ fixture.detectChanges()
+ })
+
+ it('should call api statistics endpoint', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}statistics/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should display inbox link with count', () => {
+ const mockStats = {
+ documents_total: 200,
+ documents_inbox: 18,
+ inbox_tag: 10,
+ }
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}statistics/`
+ )
+
+ req.flush(mockStats)
+ fixture.detectChanges()
+
+ const goToInboxSpy = jest.spyOn(component, 'goToInbox')
+
+ expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
+ 'inbox:18'
+ )
+ const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement
+ expect(link).not.toBeNull()
+ link.click()
+ expect(goToInboxSpy).toHaveBeenCalled()
+ })
+
+ it('should display mime types with counts', () => {
+ const mockStats = {
+ documents_total: 200,
+ documents_inbox: 18,
+ inbox_tag: 10,
+ document_file_type_counts: [
+ {
+ mime_type: 'application/pdf',
+ mime_type_count: 160,
+ },
+ {
+ mime_type: 'text/plain',
+ mime_type_count: 20,
+ },
+ {
+ mime_type: 'text/csv',
+ mime_type_count: 20,
+ },
+ ],
+ character_count: 162312,
+ }
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}statistics/`
+ )
+
+ req.flush(mockStats)
+ fixture.detectChanges()
+
+ expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
+ 'PDF(80%)'
+ )
+ expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
+ 'TXT(10%)'
+ )
+ expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
+ 'CSV(10%)'
+ )
+ })
+})
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { By } from '@angular/platform-browser'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbModule,
+ NgbAlertModule,
+ NgbAlert,
+ NgbCollapse,
+} from '@ng-bootstrap/ng-bootstrap'
+import { NgxFileDropModule } from 'ngx-file-drop'
+import { routes } from 'src/app/app-routing.module'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import {
+ ConsumerStatusService,
+ FileStatus,
+ FileStatusPhase,
+} from 'src/app/services/consumer-status.service'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
+import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
+import { UploadFileWidgetComponent } from './upload-file-widget.component'
+
+describe('UploadFileWidgetComponent', () => {
+ let component: UploadFileWidgetComponent
+ let fixture: ComponentFixture<UploadFileWidgetComponent>
+ let consumerStatusService: ConsumerStatusService
+ let uploadDocumentsService: UploadDocumentsService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ UploadFileWidgetComponent,
+ WidgetFrameComponent,
+ IfPermissionsDirective,
+ ],
+ providers: [
+ PermissionsGuard,
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ ],
+ imports: [
+ HttpClientTestingModule,
+ NgbModule,
+ RouterTestingModule.withRoutes(routes),
+ NgxFileDropModule,
+ NgbAlertModule,
+ ],
+ }).compileComponents()
+
+ consumerStatusService = TestBed.inject(ConsumerStatusService)
+ uploadDocumentsService = TestBed.inject(UploadDocumentsService)
+ fixture = TestBed.createComponent(UploadFileWidgetComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should support drop files', () => {
+ const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
+ component.dropped([])
+ expect(uploadSpy).toHaveBeenCalled()
+ // coverage
+ component.fileLeave(null)
+ component.fileOver(null)
+ })
+
+ it('should generate stats summary', () => {
+ mockConsumerStatuses(consumerStatusService)
+ expect(component.getStatusSummary()).toEqual(
+ 'Processing: 6, Failed: 1, Added: 4'
+ )
+ })
+
+ it('should report an upload progress summary', () => {
+ mockConsumerStatuses(consumerStatusService)
+ expect(component.getTotalUploadProgress()).toEqual(0.75)
+ })
+
+ it('should change color by status phase', () => {
+ const processingStatus = new FileStatus()
+ processingStatus.phase = FileStatusPhase.PROCESSING
+ expect(component.getStatusColor(processingStatus)).toEqual('primary')
+ const failedStatus = new FileStatus()
+ failedStatus.phase = FileStatusPhase.FAILED
+ expect(component.getStatusColor(failedStatus)).toEqual('danger')
+ const successStatus = new FileStatus()
+ successStatus.phase = FileStatusPhase.SUCCESS
+ expect(component.getStatusColor(successStatus)).toEqual('success')
+ })
+
+ it('should enforce a maximum number of alerts', () => {
+ mockConsumerStatuses(consumerStatusService)
+ fixture.detectChanges()
+ // 5 total, 1 hidden
+ expect(fixture.debugElement.queryAll(By.directive(NgbAlert))).toHaveLength(
+ 6
+ )
+ expect(
+ fixture.debugElement
+ .query(By.directive(NgbCollapse))
+ .queryAll(By.directive(NgbAlert))
+ ).toHaveLength(1)
+ })
+
+ it('should allow dismissing an alert', () => {
+ const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
+ component.dismiss(new FileStatus())
+ expect(dismissSpy).toHaveBeenCalled()
+ })
+
+ it('should allow dismissing all alerts', () => {
+ const dismissSpy = jest.spyOn(consumerStatusService, 'dismissCompleted')
+ component.dismissCompleted()
+ expect(dismissSpy).toHaveBeenCalled()
+ })
+})
+
+function mockConsumerStatuses(consumerStatusService) {
+ const partialUpload1 = new FileStatus()
+ partialUpload1.currentPhaseProgress = 50
+ partialUpload1.currentPhaseMaxProgress = 50
+ const partialUpload2 = new FileStatus()
+ partialUpload2.currentPhaseProgress = 25
+ partialUpload2.currentPhaseMaxProgress = 50
+ jest
+ .spyOn(consumerStatusService, 'getConsumerStatus')
+ .mockImplementation((phase) => {
+ switch (phase) {
+ case FileStatusPhase.FAILED:
+ return [new FileStatus()]
+ case FileStatusPhase.PROCESSING:
+ return [new FileStatus(), new FileStatus()]
+ case FileStatusPhase.STARTED:
+ return [new FileStatus(), new FileStatus(), new FileStatus()]
+ case FileStatusPhase.SUCCESS:
+ return [
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ ]
+ case FileStatusPhase.UPLOADING:
+ return [partialUpload1, partialUpload2]
+ default:
+ return [
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ ]
+ }
+ })
+ jest
+ .spyOn(consumerStatusService, 'getConsumerStatusNotCompleted')
+ .mockImplementation(() => {
+ return [
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ new FileStatus(),
+ ]
+ })
+}
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
}
- getStatusCompleted() {
- return this.consumerStatusService.getConsumerStatusCompleted()
- }
getTotalUploadProgress() {
let current = 0
let max = 0
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { By } from '@angular/platform-browser'
+import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
+import { WelcomeWidgetComponent } from './welcome-widget.component'
+
+describe('WelcomeWidgetComponent', () => {
+ let component: WelcomeWidgetComponent
+ let fixture: ComponentFixture<WelcomeWidgetComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [WelcomeWidgetComponent, WidgetFrameComponent],
+ providers: [PermissionsGuard],
+ imports: [NgbAlertModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(WelcomeWidgetComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should be dismissable', () => {
+ let dismissResult
+ component.dismiss.subscribe(() => (dismissResult = true))
+ fixture.debugElement
+ .query(By.directive(NgbAlert))
+ .triggerEventHandler('closed')
+ expect(dismissResult).toBeTruthy()
+ })
+})
--- /dev/null
+import { Component } from '@angular/core'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { By } from '@angular/platform-browser'
+import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { WidgetFrameComponent } from './widget-frame.component'
+
+@Component({
+ template: `
+ <div>
+ <button
+ *appIfObjectPermissions="{
+ object: { id: 2, owner: user1 },
+ action: 'view'
+ }"
+ >
+ Some Text
+ </button>
+ </div>
+ `,
+})
+class TestComponent extends WidgetFrameComponent {}
+
+describe('WidgetFrameComponent', () => {
+ let component: WidgetFrameComponent
+ let fixture: ComponentFixture<WidgetFrameComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [WidgetFrameComponent, WidgetFrameComponent],
+ providers: [PermissionsGuard],
+ imports: [NgbAlertModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(WidgetFrameComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should show title', () => {
+ component.title = 'Foo'
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain('Foo')
+ })
+
+ it('should show loading indicator', () => {
+ expect(fixture.debugElement.query(By.css('.spinner-border'))).toBeNull()
+ component.loading = true
+ fixture.detectChanges()
+ expect(fixture.debugElement.query(By.css('.spinner-border'))).not.toBeNull()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
+import { of } from 'rxjs'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { DocumentAsnComponent } from './document-asn.component'
+import { RouterTestingModule } from '@angular/router/testing'
+import { FilterRule } from 'src/app/data/filter-rule'
+import { routes } from 'src/app/app-routing.module'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+
+describe('DocumentAsnComponent', () => {
+ let component: DocumentAsnComponent
+ let fixture: ComponentFixture<DocumentAsnComponent>
+ let router: Router
+ let activatedRoute: ActivatedRoute
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [DocumentAsnComponent],
+ providers: [
+ {
+ provide: DocumentService,
+ useValue: {
+ listAllFilteredIds: (rules: FilterRule[]) =>
+ rules[0].value === '1234' ? of([1]) : of([]),
+ },
+ },
+ PermissionsGuard,
+ ],
+ imports: [RouterTestingModule.withRoutes(routes)],
+ }).compileComponents()
+
+ router = TestBed.inject(Router)
+ activatedRoute = TestBed.inject(ActivatedRoute)
+ fixture = TestBed.createComponent(DocumentAsnComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should navigate on valid asn', () => {
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ id: '1234' })))
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ component.ngOnInit()
+ expect(navigateSpy).toHaveBeenCalledWith(['documents', 1])
+ })
+
+ it('should 404 on invalid asn', () => {
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ id: '5578' })))
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ component.ngOnInit()
+ expect(navigateSpy).toHaveBeenCalledWith(['404'])
+ })
+})
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+ discardPeriodicTasks,
+} from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { By } from '@angular/platform-browser'
+import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbModal,
+ NgbModule,
+ NgbModalModule,
+ NgbModalRef,
+ NgbDateStruct,
+} from '@ng-bootstrap/ng-bootstrap'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { PdfViewerComponent } from 'ng2-pdf-viewer'
+import { of, throwError } from 'rxjs'
+import { routes } from 'src/app/app-routing.module'
+import {
+ FILTER_FULLTEXT_MORELIKE,
+ FILTER_CORRESPONDENT,
+ FILTER_DOCUMENT_TYPE,
+ FILTER_STORAGE_PATH,
+ FILTER_HAS_TAGS_ALL,
+ FILTER_CREATED_AFTER,
+ FILTER_CREATED_BEFORE,
+} from 'src/app/data/filter-rule-type'
+import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
+import { PaperlessDocument } from 'src/app/data/paperless-document'
+import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
+import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import { OpenDocumentsService } from 'src/app/services/open-documents.service'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
+import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { StoragePathService } from 'src/app/services/rest/storage-path.service'
+import { UserService } from 'src/app/services/rest/user.service'
+import { SettingsService } from 'src/app/services/settings.service'
+import { ToastService } from 'src/app/services/toast.service'
+import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
+import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
+import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
+import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
+import { DateComponent } from '../common/input/date/date.component'
+import { NumberComponent } from '../common/input/number/number.component'
+import { PermissionsFormComponent } from '../common/input/permissions/permissions-form/permissions-form.component'
+import { SelectComponent } from '../common/input/select/select.component'
+import { TagsComponent } from '../common/input/tags/tags.component'
+import { TextComponent } from '../common/input/text/text.component'
+import { PageHeaderComponent } from '../common/page-header/page-header.component'
+import { DocumentNotesComponent } from '../document-notes/document-notes.component'
+import { DocumentDetailComponent } from './document-detail.component'
+
+const doc: PaperlessDocument = {
+ id: 3,
+ title: 'Doc 3',
+ correspondent: 11,
+ document_type: 21,
+ storage_path: 31,
+ tags: [41, 42, 43],
+ content: 'text content',
+ added: new Date(),
+ created: new Date(),
+ archive_serial_number: null,
+ original_file_name: 'file.pdf',
+ owner: null,
+ user_can_change: true,
+ notes: [
+ {
+ created: new Date(),
+ note: 'note 1',
+ user: 1,
+ },
+ {
+ created: new Date(),
+ note: 'note 2',
+ user: 2,
+ },
+ ],
+}
+
+describe('DocumentDetailComponent', () => {
+ let component: DocumentDetailComponent
+ let fixture: ComponentFixture<DocumentDetailComponent>
+ let router: Router
+ let activatedRoute: ActivatedRoute
+ let documentService: DocumentService
+ let openDocumentsService: OpenDocumentsService
+ let modalService: NgbModal
+ let toastService: ToastService
+ let documentListViewService: DocumentListViewService
+ let settingsService: SettingsService
+
+ let currentUserCan = true
+ let currentUserHasObjectPermissions = true
+ let currentUserOwnsObject = true
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentDetailComponent,
+ DocumentTitlePipe,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ TagsComponent,
+ SelectComponent,
+ TextComponent,
+ NumberComponent,
+ DateComponent,
+ DocumentNotesComponent,
+ CustomDatePipe,
+ DocumentTypeEditDialogComponent,
+ CorrespondentEditDialogComponent,
+ StoragePathEditDialogComponent,
+ IfOwnerDirective,
+ PermissionsFormComponent,
+ SafeHtmlPipe,
+ ConfirmDialogComponent,
+ PdfViewerComponent,
+ SafeUrlPipe,
+ ],
+ providers: [
+ DocumentTitlePipe,
+ {
+ provide: CorrespondentService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 11,
+ name: 'Correspondent11',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: DocumentTypeService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 21,
+ name: 'DocumentType21',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: StoragePathService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 31,
+ name: 'StoragePath31',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 1,
+ username: 'user1',
+ },
+ {
+ id: 2,
+ username: 'user2',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => currentUserCan,
+ currentUserHasObjectPermissions: () =>
+ currentUserHasObjectPermissions,
+ currentUserOwnsObject: () => currentUserOwnsObject,
+ },
+ },
+ PermissionsGuard,
+ CustomDatePipe,
+ DatePipe,
+ ],
+ imports: [
+ RouterTestingModule.withRoutes(routes),
+ HttpClientTestingModule,
+ NgbModule,
+ NgSelectModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbModalModule,
+ ],
+ }).compileComponents()
+
+ router = TestBed.inject(Router)
+ activatedRoute = TestBed.inject(ActivatedRoute)
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ id: 3 })))
+ openDocumentsService = TestBed.inject(OpenDocumentsService)
+ documentService = TestBed.inject(DocumentService)
+ modalService = TestBed.inject(NgbModal)
+ toastService = TestBed.inject(ToastService)
+ documentListViewService = TestBed.inject(DocumentListViewService)
+ settingsService = TestBed.inject(SettingsService)
+ fixture = TestBed.createComponent(DocumentDetailComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should load four tabs via url params', () => {
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ id: 3, section: 'notes' })))
+ jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(null)
+ jest
+ .spyOn(openDocumentsService, 'openDocument')
+ .mockReturnValueOnce(of(true))
+ fixture.detectChanges()
+ expect(component.activeNavID).toEqual(5) // DocumentDetailNavIDs.Notes
+ })
+
+ it('should change url on tab switch', () => {
+ initNormally()
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ component.nav.select(5)
+ component.nav.navChange.next({
+ activeId: 1,
+ nextId: 5,
+ preventDefault: () => {},
+ })
+ fixture.detectChanges()
+ expect(navigateSpy).toHaveBeenCalledWith(['documents', 3, 'notes'])
+ })
+
+ it('should update title after debounce', fakeAsync(() => {
+ initNormally()
+ component.titleInput.value = 'Foo Bar'
+ component.titleSubject.next('Foo Bar')
+ tick(1000)
+ expect(component.documentForm.get('title').value).toEqual('Foo Bar')
+ discardPeriodicTasks()
+ }))
+
+ it('should update title before doc change if wasnt updated via debounce', fakeAsync(() => {
+ initNormally()
+ component.titleInput.value = 'Foo Bar'
+ component.titleInput.inputField.nativeElement.dispatchEvent(
+ new Event('change')
+ )
+ tick(1000)
+ expect(component.documentForm.get('title').value).toEqual('Foo Bar')
+ }))
+
+ it('should load non-open document via param', () => {
+ initNormally()
+ expect(component.document).toEqual(doc)
+ })
+
+ it('should load already-opened document via param', () => {
+ jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
+ jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(doc)
+ fixture.detectChanges() // calls ngOnInit
+ expect(component.document).toEqual(doc)
+ })
+
+ it('should disable form if user cannot edit', () => {
+ currentUserHasObjectPermissions = false
+ initNormally()
+ expect(component.documentForm.disabled).toBeTruthy()
+ })
+
+ it('should support creating document type', () => {
+ initNormally()
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ component.createDocumentType('NewDocType2')
+ expect(modalSpy).toHaveBeenCalled()
+ openModal.componentInstance.succeeded.next({ id: 12, name: 'NewDocType12' })
+ expect(component.documentForm.get('document_type').value).toEqual(12)
+ })
+
+ it('should support creating correspondent', () => {
+ initNormally()
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ component.createCorrespondent('NewCorrrespondent12')
+ expect(modalSpy).toHaveBeenCalled()
+ openModal.componentInstance.succeeded.next({
+ id: 12,
+ name: 'NewCorrrespondent12',
+ })
+ expect(component.documentForm.get('correspondent').value).toEqual(12)
+ })
+
+ it('should support creating storage path', () => {
+ initNormally()
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ component.createStoragePath('NewStoragePath12')
+ expect(modalSpy).toHaveBeenCalled()
+ openModal.componentInstance.succeeded.next({
+ id: 12,
+ name: 'NewStoragePath12',
+ })
+ expect(component.documentForm.get('storage_path').value).toEqual(12)
+ })
+
+ it('should allow dischard changes', () => {
+ initNormally()
+ component.title = 'Foo Bar'
+ fixture.detectChanges()
+ jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
+ component.discard()
+ fixture.detectChanges()
+ expect(component.title).toEqual(doc.title)
+ expect(openDocumentsService.hasDirty()).toBeFalsy()
+ // this time with error, mostly for coverage
+ component.title = 'Foo Bar'
+ fixture.detectChanges()
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ jest
+ .spyOn(documentService, 'get')
+ .mockReturnValueOnce(throwError(() => new Error('unable to discard')))
+ component.discard()
+ fixture.detectChanges()
+ expect(navigateSpy).toHaveBeenCalledWith(['404'])
+ })
+
+ it('should 404 on invalid id', () => {
+ jest.spyOn(documentService, 'get').mockReturnValueOnce(of(null))
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ fixture.detectChanges()
+ expect(navigateSpy).toHaveBeenCalledWith(['404'])
+ })
+
+ it('should support save, close and show success toast', () => {
+ initNormally()
+ component.title = 'Foo Bar'
+ const closeSpy = jest.spyOn(component, 'close')
+ const updateSpy = jest.spyOn(documentService, 'update')
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+ updateSpy.mockImplementation((o) => of(doc))
+ component.save()
+ expect(updateSpy).toHaveBeenCalled()
+ expect(closeSpy).toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
+ })
+
+ it('should show toast error on save if error occurs', () => {
+ currentUserHasObjectPermissions = true
+ initNormally()
+ component.title = 'Foo Bar'
+ const closeSpy = jest.spyOn(component, 'close')
+ const updateSpy = jest.spyOn(documentService, 'update')
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ updateSpy.mockImplementation(() =>
+ throwError(() => new Error('failed to save'))
+ )
+ component.save()
+ expect(updateSpy).toHaveBeenCalled()
+ expect(closeSpy).not.toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalledWith(
+ 'Error saving document: failed to save'
+ )
+ })
+
+ it('should show error toast on save but close if user can no longer edit', () => {
+ currentUserHasObjectPermissions = false
+ initNormally()
+ component.title = 'Foo Bar'
+ const closeSpy = jest.spyOn(component, 'close')
+ const updateSpy = jest.spyOn(documentService, 'update')
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+ updateSpy.mockImplementation(() =>
+ throwError(() => new Error('failed to save'))
+ )
+ component.save()
+ expect(updateSpy).toHaveBeenCalled()
+ expect(closeSpy).toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
+ })
+
+ it('should allow save and next', () => {
+ initNormally()
+ const nextDocId = 100
+ component.title = 'Foo Bar'
+ const updateSpy = jest.spyOn(documentService, 'update')
+ updateSpy.mockReturnValue(of(doc))
+ const nextSpy = jest.spyOn(documentListViewService, 'getNext')
+ nextSpy.mockReturnValue(of(nextDocId))
+ const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
+ closeSpy.mockReturnValue(of(true))
+ const navigateSpy = jest.spyOn(router, 'navigate')
+
+ component.saveEditNext()
+ expect(updateSpy).toHaveBeenCalled()
+ expect(navigateSpy).toHaveBeenCalledWith(['documents', nextDocId])
+ expect
+ })
+
+ it('should show toast error on saveAll if error occurs', () => {
+ currentUserHasObjectPermissions = true
+ initNormally()
+ component.title = 'Foo Bar'
+ const closeSpy = jest.spyOn(component, 'close')
+ const updateSpy = jest.spyOn(documentService, 'update')
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ updateSpy.mockImplementation(() =>
+ throwError(() => new Error('failed to save'))
+ )
+ component.saveEditNext()
+ expect(updateSpy).toHaveBeenCalled()
+ expect(closeSpy).not.toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalledWith(
+ 'Error saving document: failed to save'
+ )
+ })
+
+ it('should allow close and navigate to documents by default', () => {
+ initNormally()
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ component.close()
+ expect(navigateSpy).toHaveBeenCalledWith(['documents'])
+ })
+
+ it('should allow close and navigate to documents by default', () => {
+ initNormally()
+ jest
+ .spyOn(documentListViewService, 'activeSavedViewId', 'get')
+ .mockReturnValue(77)
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ component.close()
+ expect(navigateSpy).toHaveBeenCalledWith(['view', 77])
+ })
+
+ it('should not close if e.g. user-cancelled', () => {
+ initNormally()
+ jest.spyOn(openDocumentsService, 'closeDocument').mockReturnValue(of(false))
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ component.close()
+ expect(navigateSpy).not.toHaveBeenCalled()
+ })
+
+ it('should support delete, ask for confirmation', () => {
+ initNormally()
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ const deleteSpy = jest.spyOn(documentService, 'delete')
+ deleteSpy.mockReturnValue(of(true))
+ component.delete()
+ expect(modalSpy).toHaveBeenCalled()
+ const modalCloseSpy = jest.spyOn(openModal, 'close')
+ openModal.componentInstance.confirmClicked.next()
+ expect(deleteSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).toHaveBeenCalled()
+ })
+
+ it('should allow retry delete if error', () => {
+ initNormally()
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ const deleteSpy = jest.spyOn(documentService, 'delete')
+ deleteSpy.mockReturnValueOnce(throwError(() => new Error('one time')))
+ component.delete()
+ expect(modalSpy).toHaveBeenCalled()
+ const modalCloseSpy = jest.spyOn(openModal, 'close')
+ openModal.componentInstance.confirmClicked.next()
+ expect(deleteSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).not.toHaveBeenCalled()
+ deleteSpy.mockReturnValueOnce(of(true))
+ // retry
+ openModal.componentInstance.confirmClicked.next()
+ expect(deleteSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).toHaveBeenCalled()
+ })
+
+ it('should support more like quick filter', () => {
+ initNormally()
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.moreLike()
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_FULLTEXT_MORELIKE,
+ value: doc.id.toString(),
+ },
+ ])
+ })
+
+ it('should support redo ocr, confirm and close modal after started', () => {
+ initNormally()
+ const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit')
+ bulkEditSpy.mockReturnValue(of(true))
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+ component.redoOcr()
+ const modalCloseSpy = jest.spyOn(openModal, 'close')
+ openModal.componentInstance.confirmClicked.next()
+ expect(bulkEditSpy).toHaveBeenCalledWith([doc.id], 'redo_ocr', {})
+ expect(modalSpy).toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).toHaveBeenCalled()
+ })
+
+ it('should show error if redo ocr call fails', () => {
+ initNormally()
+ const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit')
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ component.redoOcr()
+ const modalCloseSpy = jest.spyOn(openModal, 'close')
+ bulkEditSpy.mockReturnValue(throwError(() => new Error('error occurred')))
+ openModal.componentInstance.confirmClicked.next()
+ expect(toastSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).not.toHaveBeenCalled()
+ })
+
+ it('should support next doc', () => {
+ initNormally()
+ const serviceSpy = jest.spyOn(documentListViewService, 'getNext')
+ const routerSpy = jest.spyOn(router, 'navigate')
+ serviceSpy.mockReturnValue(of(100))
+ component.nextDoc()
+ expect(serviceSpy).toHaveBeenCalled()
+ expect(routerSpy).toHaveBeenCalledWith(['documents', 100])
+ })
+
+ it('should support previous doc', () => {
+ initNormally()
+ const serviceSpy = jest.spyOn(documentListViewService, 'getPrevious')
+ const routerSpy = jest.spyOn(router, 'navigate')
+ serviceSpy.mockReturnValue(of(100))
+ component.previousDoc()
+ expect(serviceSpy).toHaveBeenCalled()
+ expect(routerSpy).toHaveBeenCalledWith(['documents', 100])
+ })
+
+ it('should support password-protected PDFs with a password field', () => {
+ initNormally()
+ component.onError({ name: 'PasswordException' }) // normally dispatched by pdf viewer
+ expect(component.requiresPassword).toBeTruthy()
+ fixture.detectChanges()
+ expect(
+ fixture.debugElement.query(By.css('input[type=password]'))
+ ).not.toBeUndefined()
+ component.password = 'foo'
+ component.pdfPreviewLoaded({ numPages: 1000 } as any)
+ expect(component.requiresPassword).toBeFalsy()
+ })
+
+ it('should support Enter key in password field', () => {
+ initNormally()
+ component.onError({ name: 'PasswordException' }) // normally dispatched by pdf viewer
+ fixture.detectChanges()
+ expect(component.password).toBeUndefined()
+ const pwField = fixture.debugElement.query(By.css('input[type=password]'))
+ pwField.nativeElement.value = 'foobar'
+ pwField.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Enter' })
+ )
+ expect(component.password).toEqual('foobar')
+ })
+
+ it('should update n pages after pdf loaded', () => {
+ initNormally()
+ component.pdfPreviewLoaded({ numPages: 1000 } as any)
+ expect(component.previewNumPages).toEqual(1000)
+ })
+
+ it('should support updating notes dynamically', () => {
+ const notes = [
+ {
+ id: 1,
+ note: 'hello world',
+ },
+ ]
+ initNormally()
+ const refreshSpy = jest.spyOn(openDocumentsService, 'refreshDocument')
+ component.notesUpdated(notes) // called by notes component
+ expect(component.document.notes).toEqual(notes)
+ expect(refreshSpy).toHaveBeenCalled()
+ })
+
+ it('should support quick filtering by correspondent', () => {
+ initNormally()
+ const object = {
+ id: 22,
+ name: 'Correspondent22',
+ last_correspondence: new Date(),
+ } as PaperlessCorrespondent
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.filterDocuments([object])
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: object.id.toString(),
+ },
+ ])
+ })
+
+ it('should support quick filtering by doc type', () => {
+ initNormally()
+ const object = { id: 22, name: 'DocumentType22' } as PaperlessDocumentType
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.filterDocuments([object])
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: object.id.toString(),
+ },
+ ])
+ })
+
+ it('should support quick filtering by storage path', () => {
+ initNormally()
+ const object = {
+ id: 22,
+ name: 'StoragePath22',
+ path: '/foo/bar/',
+ } as PaperlessStoragePath
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.filterDocuments([object])
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_STORAGE_PATH,
+ value: object.id.toString(),
+ },
+ ])
+ })
+
+ it('should support quick filtering by all tags', () => {
+ initNormally()
+ const object1 = {
+ id: 22,
+ name: 'Tag22',
+ is_inbox_tag: true,
+ color: '#ff0000',
+ text_color: '#000000',
+ } as PaperlessTag
+ const object2 = {
+ id: 23,
+ name: 'Tag22',
+ is_inbox_tag: true,
+ color: '#ff0000',
+ text_color: '#000000',
+ } as PaperlessTag
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.filterDocuments([object1, object2])
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: object1.id.toString(),
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: object2.id.toString(),
+ },
+ ])
+ })
+
+ it('should support quick filtering by date after - 1d and before +1d', () => {
+ initNormally()
+ const object = { year: 2023, month: 5, day: 14 } as NgbDateStruct
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ component.filterDocuments([object])
+ expect(qfSpy).toHaveBeenCalledWith([
+ {
+ rule_type: FILTER_CREATED_AFTER,
+ value: '2023-05-13',
+ },
+ {
+ rule_type: FILTER_CREATED_BEFORE,
+ value: '2023-05-15',
+ },
+ ])
+ })
+
+ it('should detect RTL languages and add css class to content textarea', () => {
+ initNormally()
+ component.metadata = { lang: 'he' }
+ component.nav.select(2) // content
+ fixture.detectChanges()
+ expect(component.isRTL).toBeTruthy()
+ expect(fixture.debugElement.queryAll(By.css('textarea.rtl'))).not.toBeNull()
+ })
+
+ it('should display built-in pdf viewer if not disabled', () => {
+ initNormally()
+ component.metadata = { has_archive_version: true }
+ jest.spyOn(settingsService, 'get').mockReturnValue(false)
+ expect(component.useNativePdfViewer).toBeFalsy()
+ fixture.detectChanges()
+ expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
+ })
+
+ it('should display native pdf viewer if enabled', () => {
+ initNormally()
+ component.metadata = { has_archive_version: true }
+ jest.spyOn(settingsService, 'get').mockReturnValue(true)
+ expect(component.useNativePdfViewer).toBeTruthy()
+ fixture.detectChanges()
+ expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
+ })
+
+ it('should attempt to retrieve metadata', () => {
+ const metadataSpy = jest.spyOn(documentService, 'getMetadata')
+ metadataSpy.mockReturnValue(of({ has_archive_version: true }))
+ initNormally()
+ expect(metadataSpy).toHaveBeenCalled()
+ })
+
+ it('should show an error if failed metadata retrieval', () => {
+ const error = new Error('metadata error')
+ jest
+ .spyOn(documentService, 'getMetadata')
+ .mockReturnValue(throwError(() => error))
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ initNormally()
+ expect(toastSpy).toHaveBeenCalledWith(
+ 'Error retrieving metadata',
+ 10000,
+ error
+ )
+ })
+
+ function initNormally() {
+ jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
+ jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(null)
+ jest
+ .spyOn(openDocumentsService, 'openDocument')
+ .mockReturnValueOnce(of(true))
+ fixture.detectChanges()
+ }
+})
FILTER_CORRESPONDENT,
FILTER_CREATED_AFTER,
FILTER_CREATED_BEFORE,
- FILTER_CREATED_DAY,
- FILTER_CREATED_MONTH,
- FILTER_CREATED_YEAR,
FILTER_DOCUMENT_TYPE,
FILTER_FULLTEXT_MORELIKE,
FILTER_HAS_TAGS_ALL,
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
import { HttpClient } from '@angular/common/http'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
-import { FilterRule } from 'src/app/data/filter-rule'
+import { EditDialogMode } from '../common/edit-dialog/edit-dialog.component'
import { ObjectWithId } from 'src/app/data/object-with-id'
+import { FilterRule } from 'src/app/data/filter-rule'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
enum DocumentDetailNavIDs {
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {
backdrop: 'static',
})
- modal.componentInstance.dialogMode = 'create'
+ modal.componentInstance.dialogMode = EditDialogMode.CREATE
if (newName) modal.componentInstance.object = { name: newName }
modal.componentInstance.succeeded
.pipe(
var modal = this.modalService.open(CorrespondentEditDialogComponent, {
backdrop: 'static',
})
- modal.componentInstance.dialogMode = 'create'
+ modal.componentInstance.dialogMode = EditDialogMode.CREATE
if (newName) modal.componentInstance.object = { name: newName }
modal.componentInstance.succeeded
.pipe(
var modal = this.modalService.open(StoragePathEditDialogComponent, {
backdrop: 'static',
})
- modal.componentInstance.dialogMode = 'create'
+ modal.componentInstance.dialogMode = EditDialogMode.CREATE
if (newName) modal.componentInstance.object = { name: newName }
modal.componentInstance.succeeded
.pipe(
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { MetadataCollapseComponent } from './metadata-collapse.component'
+import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'
+
+const metadata = [
+ {
+ namespace: 'http://ns.adobe.com/pdf/1.3/',
+ prefix: 'pdf',
+ key: 'Producer',
+ value: 'pikepdf 2.2.0',
+ },
+ {
+ namespace: 'http://ns.adobe.com/xap/1.0/',
+ prefix: 'xmp',
+ key: 'ModifyDate',
+ value: '2020-12-21T08:42:26+00:00',
+ },
+]
+
+describe('MetadataCollapseComponent', () => {
+ let component: MetadataCollapseComponent
+ let fixture: ComponentFixture<MetadataCollapseComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [MetadataCollapseComponent],
+ providers: [],
+ imports: [NgbCollapseModule],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(MetadataCollapseComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should display metadata', () => {
+ component.title = 'Foo'
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain('Foo')
+ })
+
+ it('should display metadata', () => {
+ component.metadata = metadata
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ 'pikepdf 2.2.0'
+ )
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ 'ModifyDate'
+ )
+ })
+})
--- /dev/null
+import {
+ HttpTestingController,
+ HttpClientTestingModule,
+} from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { By } from '@angular/platform-browser'
+import {
+ NgbModal,
+ NgbModule,
+ NgbModalModule,
+ NgbModalRef,
+} from '@ng-bootstrap/ng-bootstrap'
+import { of, throwError } from 'rxjs'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { FilterPipe } from 'src/app/pipes/filter.pipe'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
+import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
+import {
+ SelectionData,
+ DocumentService,
+} from 'src/app/services/rest/document.service'
+import { StoragePathService } from 'src/app/services/rest/storage-path.service'
+import { TagService } from 'src/app/services/rest/tag.service'
+import { SettingsService } from 'src/app/services/settings.service'
+import { ToastService } from 'src/app/services/toast.service'
+import { environment } from 'src/environments/environment'
+import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
+import { FilterableDropdownComponent } from '../../common/filterable-dropdown/filterable-dropdown.component'
+import { ToggleableDropdownButtonComponent } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
+import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
+import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
+import { BulkEditorComponent } from './bulk-editor.component'
+import { SelectComponent } from '../../common/input/select/select.component'
+import { UserService } from 'src/app/services/rest/user.service'
+import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
+import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { GroupService } from 'src/app/services/rest/group.service'
+
+const selectionData: SelectionData = {
+ selected_tags: [
+ { id: 12, document_count: 3 },
+ { id: 22, document_count: 1 },
+ { id: 19, document_count: 0 },
+ ],
+ selected_correspondents: [{ id: 33, document_count: 1 }],
+ selected_document_types: [{ id: 44, document_count: 3 }],
+ selected_storage_paths: [
+ { id: 66, document_count: 3 },
+ { id: 55, document_count: 0 },
+ ],
+}
+
+describe('BulkEditorComponent', () => {
+ let component: BulkEditorComponent
+ let fixture: ComponentFixture<BulkEditorComponent>
+ let permissionsService: PermissionsService
+ let documentListViewService: DocumentListViewService
+ let documentService: DocumentService
+ let toastService: ToastService
+ let modalService: NgbModal
+ let httpTestingController: HttpTestingController
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ BulkEditorComponent,
+ IfPermissionsDirective,
+ FilterableDropdownComponent,
+ ToggleableDropdownButtonComponent,
+ FilterPipe,
+ ConfirmDialogComponent,
+ SafeHtmlPipe,
+ PermissionsDialogComponent,
+ PermissionsFormComponent,
+ SelectComponent,
+ PermissionsGroupComponent,
+ PermissionsUserComponent,
+ ],
+ providers: [
+ PermissionsService,
+ {
+ provide: TagService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ { id: 12, name: 'tag12' },
+ { id: 22, name: 'tag22' },
+ ],
+ }),
+ },
+ },
+ {
+ provide: CorrespondentService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [{ id: 33, name: 'correspondent33' }],
+ }),
+ },
+ },
+ {
+ provide: DocumentTypeService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [{ id: 44, name: 'doctype44' }],
+ }),
+ },
+ },
+ {
+ provide: StoragePathService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ { id: 66, name: 'storagepath66' },
+ { id: 55, name: 'storagepath55' },
+ ],
+ }),
+ },
+ },
+ FilterPipe,
+ SettingsService,
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [{ id: 1, username: 'user1' }],
+ }),
+ },
+ },
+ {
+ provide: GroupService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [],
+ }),
+ },
+ },
+ ],
+ imports: [
+ HttpClientTestingModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbModule,
+ NgbModalModule,
+ NgSelectModule,
+ ],
+ }).compileComponents()
+
+ permissionsService = TestBed.inject(PermissionsService)
+ documentListViewService = TestBed.inject(DocumentListViewService)
+ documentService = TestBed.inject(DocumentService)
+ toastService = TestBed.inject(ToastService)
+ modalService = TestBed.inject(NgbModal)
+ httpTestingController = TestBed.inject(HttpTestingController)
+
+ fixture = TestBed.createComponent(BulkEditorComponent)
+ component = fixture.componentInstance
+ })
+
+ afterEach(async () => {
+ httpTestingController.verify()
+ })
+
+ it('should apply selection data to tags menu', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ fixture.detectChanges()
+ expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 5, 7]))
+ jest
+ .spyOn(documentService, 'getSelectionData')
+ .mockReturnValue(of(selectionData))
+ component.openTagsDropdown()
+ expect(component.tagSelectionModel.selectionSize()).toEqual(1)
+ })
+
+ it('should apply selection data to correspondents menu', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ fixture.detectChanges()
+ expect(
+ component.correspondentSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 5, 7]))
+ jest
+ .spyOn(documentService, 'getSelectionData')
+ .mockReturnValue(of(selectionData))
+ component.openCorrespondentDropdown()
+ expect(component.correspondentSelectionModel.items).toHaveLength(2)
+ expect(component.correspondentSelectionModel.selectionSize()).toEqual(0)
+ })
+
+ it('should apply selection data to doc types menu', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ fixture.detectChanges()
+ expect(
+ component.documentTypeSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 5, 7]))
+ jest
+ .spyOn(documentService, 'getSelectionData')
+ .mockReturnValue(of(selectionData))
+ component.openDocumentTypeDropdown()
+ expect(component.documentTypeSelectionModel.selectionSize()).toEqual(1)
+ })
+
+ it('should apply selection data to storage path menu', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ fixture.detectChanges()
+ expect(
+ component.storagePathsSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 5, 7]))
+ jest
+ .spyOn(documentService, 'getSelectionData')
+ .mockReturnValue(of(selectionData))
+ component.openStoragePathDropdown()
+ expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
+ })
+
+ it('should execute modify tags bulk operation', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = false
+ fixture.detectChanges()
+ component.setTags({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'modify_tags',
+ parameters: { add_tags: [101], remove_tags: [] },
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should execute modify tags bulk operation with confirmation dialog if enabled', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setTags({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirm()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
+ .flush(true)
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should set modal dialog text accordingly for tag edit confirmation', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setTags({
+ itemsToAdd: [],
+ itemsToRemove: [{ id: 101, name: 'Tag 101' }],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will remove the tag "Tag 101" from 2 selected document(s).'
+ )
+ modal.close()
+ component.setTags({
+ itemsToAdd: [],
+ itemsToRemove: [
+ { id: 101, name: 'Tag 101' },
+ { id: 102, name: 'Tag 102' },
+ ],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will remove the tags "Tag 101" and "Tag 102" from 2 selected document(s).'
+ )
+ modal.close()
+ component.setTags({
+ itemsToAdd: [
+ { id: 101, name: 'Tag 101' },
+ { id: 102, name: 'Tag 102' },
+ ],
+ itemsToRemove: [],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will add the tags "Tag 101" and "Tag 102" to 2 selected document(s).'
+ )
+ modal.close()
+ component.setTags({
+ itemsToAdd: [
+ { id: 101, name: 'Tag 101' },
+ { id: 102, name: 'Tag 102' },
+ ],
+ itemsToRemove: [{ id: 103, name: 'Tag 103' }],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will add the tags "Tag 101" and "Tag 102" and remove the tags "Tag 103" on 2 selected document(s).'
+ )
+ })
+
+ it('should execute modify correspondent bulk operation', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = false
+ fixture.detectChanges()
+ component.setCorrespondents({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'set_correspondent',
+ parameters: { correspondent: 101 },
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should execute modify correspondent bulk operation with confirmation dialog if enabled', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setCorrespondents({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirm()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
+ .flush(true)
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should set modal dialog text accordingly for correspondent edit confirmation', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setCorrespondents({
+ itemsToAdd: [],
+ itemsToRemove: [{ id: 101, name: 'Correspondent 101' }],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will remove the correspondent from 2 selected document(s).'
+ )
+ modal.close()
+ component.setCorrespondents({
+ itemsToAdd: [{ id: 101, name: 'Correspondent 101' }],
+ itemsToRemove: [],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will assign the correspondent "Correspondent 101" to 2 selected document(s).'
+ )
+ })
+
+ it('should execute modify document type bulk operation', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = false
+ fixture.detectChanges()
+ component.setDocumentTypes({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'set_document_type',
+ parameters: { document_type: 101 },
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should execute modify document type bulk operation with confirmation dialog if enabled', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setDocumentTypes({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirm()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
+ .flush(true)
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should set modal dialog text accordingly for document type edit confirmation', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setDocumentTypes({
+ itemsToAdd: [],
+ itemsToRemove: [{ id: 101, name: 'DocType 101' }],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will remove the document type from 2 selected document(s).'
+ )
+ modal.close()
+ component.setDocumentTypes({
+ itemsToAdd: [{ id: 101, name: 'DocType 101' }],
+ itemsToRemove: [],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will assign the document type "DocType 101" to 2 selected document(s).'
+ )
+ })
+
+ it('should execute modify storage path bulk operation', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = false
+ fixture.detectChanges()
+ component.setStoragePaths({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'set_storage_path',
+ parameters: { storage_path: 101 },
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should execute modify storage path bulk operation with confirmation dialog if enabled', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setStoragePaths({
+ itemsToAdd: [{ id: 101 }],
+ itemsToRemove: [],
+ })
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirm()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
+ .flush(true)
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should set modal dialog text accordingly for storage path edit confirmation', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setStoragePaths({
+ itemsToAdd: [],
+ itemsToRemove: [{ id: 101, name: 'StoragePath 101' }],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will remove the storage path from 2 selected document(s).'
+ )
+ modal.close()
+ component.setStoragePaths({
+ itemsToAdd: [{ id: 101, name: 'StoragePath 101' }],
+ itemsToRemove: [],
+ })
+ expect(modal.componentInstance.message).toEqual(
+ 'This operation will assign the storage path "StoragePath 101" to 2 selected document(s).'
+ )
+ })
+
+ it('should only execute bulk operations when changes are detected', () => {
+ component.setTags({
+ itemsToAdd: [],
+ itemsToRemove: [],
+ })
+ component.setCorrespondents({
+ itemsToAdd: [],
+ itemsToRemove: [],
+ })
+ component.setDocumentTypes({
+ itemsToAdd: [],
+ itemsToRemove: [],
+ })
+ component.setStoragePaths({
+ itemsToAdd: [],
+ itemsToRemove: [],
+ })
+ httpTestingController.expectNone(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ })
+
+ it('should support bulk delete with confirmation', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.applyDelete()
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirm()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'delete',
+ parameters: {},
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should not be accessible with insufficient global permissions', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
+ fixture.detectChanges()
+ const dropdown = fixture.debugElement.query(
+ By.directive(FilterableDropdownComponent)
+ )
+ expect(dropdown).toBeNull()
+ })
+
+ it('should disable with insufficient object permissions', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(false)
+ fixture.detectChanges()
+ const button = fixture.debugElement
+ .query(By.directive(FilterableDropdownComponent))
+ .query(By.css('button'))
+ expect(button.nativeElement.disabled).toBeTruthy()
+ })
+
+ it('should show a warning toast on bulk edit error', () => {
+ jest
+ .spyOn(documentService, 'bulkEdit')
+ .mockReturnValue(
+ throwError(() => new Error('error executing bulk operation'))
+ )
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = false
+ fixture.detectChanges()
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ component.setTags({
+ itemsToAdd: [{ id: 0 }],
+ itemsToRemove: [],
+ })
+ expect(toastSpy).toHaveBeenCalled()
+ })
+
+ it('should support redo ocr', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.redoOcrSelected()
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirm()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'redo_ocr',
+ parameters: {},
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+
+ it('should support bulk download with archive, originals or both and file formatting', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.downloadForm.get('downloadFileTypeArchive').patchValue(true)
+ fixture.detectChanges()
+ let downloadSpy = jest.spyOn(documentService, 'bulkDownload')
+ //archive
+ component.downloadSelected()
+ expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'archive', false)
+ //originals
+ component.downloadForm.get('downloadFileTypeArchive').patchValue(false)
+ component.downloadForm.get('downloadFileTypeOriginals').patchValue(true)
+ component.downloadSelected()
+ expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'originals', false)
+ //both
+ component.downloadForm.get('downloadFileTypeArchive').patchValue(true)
+ component.downloadSelected()
+ expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'both', false)
+ //formatting
+ component.downloadForm.get('downloadUseFormatting').patchValue(true)
+ component.downloadSelected()
+ expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'both', true)
+
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/bulk_download/`
+ )
+ })
+
+ it('should support bulk permissions update', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[0]))
+ jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
+ jest
+ .spyOn(documentListViewService, 'documents', 'get')
+ .mockReturnValue([{ id: 3 }, { id: 4 }])
+ jest
+ .spyOn(documentListViewService, 'selected', 'get')
+ .mockReturnValue(new Set([3, 4]))
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockReturnValue(true)
+ component.showConfirmationDialogs = true
+ fixture.detectChanges()
+ component.setPermissions()
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirmClicked.next()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/bulk_edit/`
+ )
+ req.flush(true)
+ expect(req.request.body).toEqual({
+ documents: [3, 4],
+ method: 'set_permissions',
+ parameters: undefined,
+ })
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ ) // list reload
+ httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ ) // listAllFilteredIds
+ })
+})
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { By } from '@angular/platform-browser'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbPopoverModule,
+ NgbTooltipModule,
+ NgbProgressbarModule,
+} from '@ng-bootstrap/ng-bootstrap'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
+import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
+import { DocumentCardLargeComponent } from './document-card-large.component'
+
+const doc = {
+ id: 10,
+ title: 'Document 10',
+ tags: [3, 4, 5],
+ correspondent: 8,
+ document_type: 10,
+ storage_path: null,
+ notes: [
+ {
+ id: 11,
+ note: 'This is some note content bananas',
+ },
+ ],
+ content:
+ 'Cupcake ipsum dolor sit amet ice cream. Donut shortbread cheesecake caramels tiramisu pastry caramels chocolate bar. Tart tootsie roll muffin icing cotton candy topping sweet roll. Pie lollipop dragée sesame snaps donut tart pudding. Oat cake apple pie danish danish candy canes. Shortbread candy canes sesame snaps muffin tiramisu marshmallow chocolate bar halvah. Cake lemon drops candy apple pie carrot cake bonbon halvah pastry gummi bears. Sweet roll candy ice cream sesame snaps marzipan cookie ice cream. Cake cheesecake apple pie muffin candy toffee lollipop. Carrot cake oat cake cookie biscuit cupcake cake marshmallow. Sweet roll jujubes carrot cake cheesecake cake candy canes sweet roll gingerbread jelly beans. Apple pie sugar plum oat cake halvah cake. Pie oat cake chocolate cake cookie gingerbread marzipan. Lemon drops cheesecake lollipop danish marzipan candy.',
+}
+
+describe('DocumentCardLargeComponent', () => {
+ let component: DocumentCardLargeComponent
+ let fixture: ComponentFixture<DocumentCardLargeComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentCardLargeComponent,
+ DocumentTitlePipe,
+ CustomDatePipe,
+ IfPermissionsDirective,
+ SafeUrlPipe,
+ ],
+ providers: [DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ NgbPopoverModule,
+ NgbTooltipModule,
+ NgbProgressbarModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(DocumentCardLargeComponent)
+ component = fixture.componentInstance
+ component.document = doc
+ fixture.detectChanges()
+ })
+
+ it('should display a document', () => {
+ expect(fixture.nativeElement.textContent).toContain('Document 10')
+ expect(fixture.nativeElement.textContent).toContain('Cupcake ipsum')
+ })
+
+ it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
+ component.mouseEnterPreview()
+ expect(component.popover.isOpen()).toBeTruthy()
+ expect(component.popoverHidden).toBeTruthy()
+ tick(600)
+ expect(component.popoverHidden).toBeFalsy()
+ component.mouseLeaveCard()
+
+ component.mouseEnterPreview()
+ tick(100)
+ component.mouseLeavePreview()
+ tick(600)
+ expect(component.popover.isOpen()).toBeFalsy()
+ }))
+
+ it('should trim content', () => {
+ expect(component.contentTrimmed).toHaveLength(503) // includes ...
+ })
+
+ it('should display search hits with colored score', () => {
+ // high
+ component.document.__search_hit__ = {
+ score: 0.9,
+ rank: 1,
+ highlights: 'cheesecake',
+ }
+ fixture.detectChanges()
+ let search_hit = fixture.debugElement.query(By.css('.search-score'))
+ expect(search_hit).not.toBeUndefined()
+ expect(component.searchScoreClass).toEqual('success')
+
+ // medium
+ component.document.__search_hit__.score = 0.6
+ fixture.detectChanges()
+ search_hit = fixture.debugElement.query(By.css('.search-score'))
+ expect(search_hit).not.toBeUndefined()
+ expect(component.searchScoreClass).toEqual('warning')
+
+ // low
+ component.document.__search_hit__.score = 0.1
+ fixture.detectChanges()
+ search_hit = fixture.debugElement.query(By.css('.search-score'))
+ expect(search_hit).not.toBeUndefined()
+ expect(component.searchScoreClass).toEqual('danger')
+ })
+
+ it('should display note highlights', () => {
+ component.document.__search_hit__ = {
+ score: 0.9,
+ rank: 1,
+ note_highlights: '<span>bananas</span>',
+ }
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent).toContain('bananas')
+ expect(component.searchNoteHighlights).toContain('<span>bananas</span>')
+ })
+})
get contentTrimmed() {
return (
- this.document.content.substr(0, 500) +
+ this.document.content.substring(0, 500) +
(this.document.content.length > 500 ? '...' : '')
)
}
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbPopoverModule,
+ NgbTooltipModule,
+ NgbProgressbarModule,
+} from '@ng-bootstrap/ng-bootstrap'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
+import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
+import { DocumentCardSmallComponent } from './document-card-small.component'
+import { of } from 'rxjs'
+import { By } from '@angular/platform-browser'
+import { TagComponent } from '../../common/tag/tag.component'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+
+const doc = {
+ id: 10,
+ title: 'Document 10',
+ tags: [1, 2, 3, 4, 5, 6, 7, 8],
+ correspondent: 8,
+ document_type: 10,
+ storage_path: null,
+ notes: [
+ {
+ id: 11,
+ note: 'This is some note content bananas',
+ },
+ ],
+ tags$: of([
+ { id: 1, name: 'Tag1' },
+ { id: 2, name: 'Tag2' },
+ { id: 3, name: 'Tag3' },
+ { id: 4, name: 'Tag4' },
+ { id: 5, name: 'Tag5' },
+ { id: 6, name: 'Tag6' },
+ { id: 7, name: 'Tag7' },
+ { id: 8, name: 'Tag8' },
+ ]),
+ content:
+ 'Cupcake ipsum dolor sit amet ice cream. Donut shortbread cheesecake caramels tiramisu pastry caramels chocolate bar. Tart tootsie roll muffin icing cotton candy topping sweet roll. Pie lollipop dragée sesame snaps donut tart pudding. Oat cake apple pie danish danish candy canes. Shortbread candy canes sesame snaps muffin tiramisu marshmallow chocolate bar halvah. Cake lemon drops candy apple pie carrot cake bonbon halvah pastry gummi bears. Sweet roll candy ice cream sesame snaps marzipan cookie ice cream. Cake cheesecake apple pie muffin candy toffee lollipop. Carrot cake oat cake cookie biscuit cupcake cake marshmallow. Sweet roll jujubes carrot cake cheesecake cake candy canes sweet roll gingerbread jelly beans. Apple pie sugar plum oat cake halvah cake. Pie oat cake chocolate cake cookie gingerbread marzipan. Lemon drops cheesecake lollipop danish marzipan candy.',
+}
+
+describe('DocumentCardSmallComponent', () => {
+ let component: DocumentCardSmallComponent
+ let fixture: ComponentFixture<DocumentCardSmallComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentCardSmallComponent,
+ DocumentTitlePipe,
+ CustomDatePipe,
+ IfPermissionsDirective,
+ SafeUrlPipe,
+ TagComponent,
+ ],
+ providers: [DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ NgbPopoverModule,
+ NgbTooltipModule,
+ NgbProgressbarModule,
+ ],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(DocumentCardSmallComponent)
+ component = fixture.componentInstance
+ component.document = Object.assign({}, doc)
+ fixture.detectChanges()
+ })
+
+ it('should display a document, limit tags to 5', () => {
+ expect(fixture.nativeElement.textContent).toContain('Document 10')
+ expect(
+ fixture.debugElement.queryAll(By.directive(TagComponent))
+ ).toHaveLength(5)
+ component.document.tags = [1, 2]
+ component.document.tags$ = of([
+ { id: 1 } as PaperlessTag,
+ { id: 2 } as PaperlessTag,
+ ])
+ fixture.detectChanges()
+ expect(
+ fixture.debugElement.queryAll(By.directive(TagComponent))
+ ).toHaveLength(2)
+ })
+
+ it('should increase limit tags to 6 if no notes', () => {
+ component.document.notes = []
+ fixture.detectChanges()
+ expect(
+ fixture.debugElement.queryAll(By.directive(TagComponent))
+ ).toHaveLength(6)
+ })
+
+ it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
+ component.mouseEnterPreview()
+ expect(component.popover.isOpen()).toBeTruthy()
+ expect(component.popoverHidden).toBeTruthy()
+ tick(600)
+ expect(component.popoverHidden).toBeFalsy()
+ component.mouseLeaveCard()
+
+ component.mouseEnterPreview()
+ tick(100)
+ component.mouseLeavePreview()
+ tick(600)
+ expect(component.popover.isOpen()).toBeFalsy()
+ }))
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { DocumentListComponent } from './document-list.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from 'src/app/app-routing.module'
+import { FilterEditorComponent } from './filter-editor/filter-editor.component'
+import { PermissionsFilterDropdownComponent } from '../common/permissions-filter-dropdown/permissions-filter-dropdown.component'
+import { DateDropdownComponent } from '../common/date-dropdown/date-dropdown.component'
+import { FilterableDropdownComponent } from '../common/filterable-dropdown/filterable-dropdown.component'
+import { PageHeaderComponent } from '../common/page-header/page-header.component'
+import { BulkEditorComponent } from './bulk-editor/bulk-editor.component'
+import { FilterPipe } from 'src/app/pipes/filter.pipe'
+import {
+ NgbDatepickerModule,
+ NgbDropdown,
+ NgbDropdownItem,
+ NgbDropdownModule,
+ NgbModal,
+ NgbModalRef,
+ NgbPagination,
+ NgbPopoverModule,
+ NgbTooltipModule,
+} from '@ng-bootstrap/ng-bootstrap'
+import { ClearableBadgeComponent } from '../common/clearable-badge/clearable-badge.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { ToggleableDropdownButtonComponent } from '../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { DatePipe } from '@angular/common'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import {
+ ConsumerStatusService,
+ FileStatus,
+} from 'src/app/services/consumer-status.service'
+import { Subject, of, throwError } from 'rxjs'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
+import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
+import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
+import {
+ FILTER_FULLTEXT_MORELIKE,
+ FILTER_FULLTEXT_QUERY,
+ FILTER_HAS_TAGS_ANY,
+} from 'src/app/data/filter-rule-type'
+import { By } from '@angular/platform-browser'
+import { SortableDirective } from 'src/app/directives/sortable.directive'
+import { ToastService } from 'src/app/services/toast.service'
+import { DocumentCardSmallComponent } from './document-card-small/document-card-small.component'
+import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
+import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
+import { UsernamePipe } from 'src/app/pipes/username.pipe'
+import { PaperlessDocument } from 'src/app/data/paperless-document'
+import {
+ DOCUMENT_SORT_FIELDS,
+ DOCUMENT_SORT_FIELDS_FULLTEXT,
+ DocumentService,
+} from 'src/app/services/rest/document.service'
+import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
+import { TextComponent } from '../common/input/text/text.component'
+import { CheckComponent } from '../common/input/check/check.component'
+import { HttpErrorResponse } from '@angular/common/http'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { SettingsService } from 'src/app/services/settings.service'
+import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
+
+const docs: PaperlessDocument[] = [
+ {
+ id: 1,
+ title: 'Doc1',
+ notes: [],
+ tags$: new Subject(),
+ content: 'document content 1',
+ },
+ {
+ id: 2,
+ title: 'Doc2',
+ notes: [],
+ tags$: new Subject(),
+ content: 'document content 2',
+ },
+ {
+ id: 3,
+ title: 'Doc3',
+ notes: [],
+ tags$: new Subject(),
+ content: 'document content 3',
+ },
+]
+
+describe('DocumentListComponent', () => {
+ let component: DocumentListComponent
+ let fixture: ComponentFixture<DocumentListComponent>
+ let documentListService: DocumentListViewService
+ let documentService: DocumentService
+ let consumerStatusService: ConsumerStatusService
+ let savedViewService: SavedViewService
+ let router: Router
+ let activatedRoute: ActivatedRoute
+ let toastService: ToastService
+ let modalService: NgbModal
+ let settingsService: SettingsService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentListComponent,
+ PageHeaderComponent,
+ FilterEditorComponent,
+ FilterableDropdownComponent,
+ DateDropdownComponent,
+ PermissionsFilterDropdownComponent,
+ ToggleableDropdownButtonComponent,
+ BulkEditorComponent,
+ ClearableBadgeComponent,
+ DocumentCardSmallComponent,
+ DocumentCardLargeComponent,
+ ConfirmDialogComponent,
+ SaveViewConfigDialogComponent,
+ TextComponent,
+ CheckComponent,
+ IfPermissionsDirective,
+ FilterPipe,
+ CustomDatePipe,
+ SortableDirective,
+ DocumentTitlePipe,
+ UsernamePipe,
+ SafeHtmlPipe,
+ ],
+ providers: [
+ FilterPipe,
+ CustomDatePipe,
+ DatePipe,
+ DocumentTitlePipe,
+ UsernamePipe,
+ SafeHtmlPipe,
+ PermissionsGuard,
+ ],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ FormsModule,
+ ReactiveFormsModule,
+ NgbDropdownModule,
+ NgbDatepickerModule,
+ NgbPopoverModule,
+ NgbTooltipModule,
+ ],
+ }).compileComponents()
+
+ documentListService = TestBed.inject(DocumentListViewService)
+ documentService = TestBed.inject(DocumentService)
+ consumerStatusService = TestBed.inject(ConsumerStatusService)
+ savedViewService = TestBed.inject(SavedViewService)
+ router = TestBed.inject(Router)
+ activatedRoute = TestBed.inject(ActivatedRoute)
+ toastService = TestBed.inject(ToastService)
+ modalService = TestBed.inject(NgbModal)
+ settingsService = TestBed.inject(SettingsService)
+ fixture = TestBed.createComponent(DocumentListComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should load display mode from local storage', () => {
+ window.localStorage.setItem('document-list:displayMode', 'largeCards')
+ fixture.detectChanges()
+ expect(component.displayMode).toEqual('largeCards')
+ component.displayMode = 'smallCards'
+ component.saveDisplayMode()
+ expect(window.localStorage.getItem('document-list:displayMode')).toEqual(
+ 'smallCards'
+ )
+ })
+
+ it('should reload on new document consumed', () => {
+ const reloadSpy = jest.spyOn(documentListService, 'reload')
+ const fileStatusSubject = new Subject<FileStatus>()
+ jest
+ .spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
+ .mockReturnValue(fileStatusSubject)
+ fixture.detectChanges()
+ fileStatusSubject.next(new FileStatus())
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+
+ it('should show score sort fields on fulltext queries', () => {
+ documentListService.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '10',
+ },
+ ]
+ fixture.detectChanges()
+ expect(component.getSortFields()).toEqual(DOCUMENT_SORT_FIELDS)
+
+ documentListService.filterRules = [
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'foo',
+ },
+ ]
+ fixture.detectChanges()
+ expect(component.getSortFields()).toEqual(DOCUMENT_SORT_FIELDS_FULLTEXT)
+ })
+
+ it('should determine if filtered, support reset', () => {
+ fixture.detectChanges()
+ documentListService.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '10',
+ },
+ ]
+ documentListService.isReloading = false
+ fixture.detectChanges()
+ expect(component.isFiltered).toBeTruthy()
+ expect(fixture.nativeElement.textContent.match(/Reset/g)).toHaveLength(2)
+ component.resetFilters()
+ fixture.detectChanges()
+ expect(fixture.nativeElement.textContent.match(/Reset/g)).toHaveLength(1)
+ })
+
+ it('should load saved view from URL', () => {
+ const view: PaperlessSavedView = {
+ id: 10,
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '20',
+ },
+ ],
+ }
+ const queryParams = { id: view.id.toString() }
+ const getSavedViewSpy = jest.spyOn(savedViewService, 'getCached')
+ getSavedViewSpy.mockReturnValue(of(view))
+ const activateSavedViewSpy = jest.spyOn(
+ documentListService,
+ 'activateSavedViewWithQueryParams'
+ )
+ activateSavedViewSpy.mockImplementation((view, params) => {})
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap(queryParams)))
+ activatedRoute.snapshot.queryParams = queryParams
+ fixture.detectChanges()
+ expect(getSavedViewSpy).toHaveBeenCalledWith(view.id)
+ expect(activateSavedViewSpy).toHaveBeenCalledWith(
+ view,
+ convertToParamMap(queryParams)
+ )
+ })
+
+ it('should 404 on load saved view from URL if no view', () => {
+ jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(null)) // e.g. no saved view found
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ id: '10' })))
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ fixture.detectChanges()
+ expect(navigateSpy).toHaveBeenCalledWith(['404'])
+ })
+
+ it('should load saved view from query params', () => {
+ const view: PaperlessSavedView = {
+ id: 10,
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '20',
+ },
+ ],
+ }
+ const getSavedViewSpy = jest.spyOn(savedViewService, 'getCached')
+ getSavedViewSpy.mockReturnValue(of(view))
+ jest
+ .spyOn(activatedRoute, 'queryParamMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ view: view.id.toString() })))
+ fixture.detectChanges()
+ expect(getSavedViewSpy).toHaveBeenCalledWith(view.id)
+ })
+
+ it('should support 3 different display modes', () => {
+ jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
+ fixture.detectChanges()
+ const displayModeButtons = fixture.debugElement.queryAll(
+ By.css('input[type="radio"]')
+ )
+ expect(component.displayMode).toEqual('smallCards')
+
+ displayModeButtons[0].nativeElement.checked = true
+ displayModeButtons[0].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.displayMode).toEqual('details')
+ expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(3)
+
+ displayModeButtons[1].nativeElement.checked = true
+ displayModeButtons[1].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.displayMode).toEqual('smallCards')
+ expect(
+ fixture.debugElement.queryAll(By.directive(DocumentCardSmallComponent))
+ ).toHaveLength(3)
+
+ displayModeButtons[2].nativeElement.checked = true
+ displayModeButtons[2].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.displayMode).toEqual('largeCards')
+ expect(
+ fixture.debugElement.queryAll(By.directive(DocumentCardLargeComponent))
+ ).toHaveLength(3)
+ })
+
+ it('should support setting sort field', () => {
+ expect(documentListService.sortField).toEqual('created')
+ fixture.detectChanges()
+ const sortDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdown)
+ )[1]
+ const asnSortFieldButton = sortDropdown.query(By.directive(NgbDropdownItem))
+
+ asnSortFieldButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(documentListService.sortField).toEqual('archive_serial_number')
+ documentListService.sortField = 'created'
+ })
+
+ it('should support setting sort field by table head', () => {
+ jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
+ fixture.detectChanges()
+ expect(documentListService.sortField).toEqual('created')
+
+ const detailsDisplayModeButton = fixture.debugElement.query(
+ By.css('input[type="radio"]')
+ )
+ detailsDisplayModeButton.nativeElement.checked = true
+ detailsDisplayModeButton.triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.displayMode).toEqual('details')
+
+ const sortTh = fixture.debugElement.query(By.directive(SortableDirective))
+ sortTh.triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(documentListService.sortField).toEqual('archive_serial_number')
+ documentListService.sortField = 'created'
+ expect(documentListService.sortReverse).toBeFalsy()
+ component.listSortReverse = true
+ expect(documentListService.sortReverse).toBeTruthy()
+ })
+
+ it('should support select all, none, page & range', () => {
+ jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
+ jest
+ .spyOn(documentService, 'listAllFilteredIds')
+ .mockReturnValue(of(docs.map((d) => d.id)))
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(0)
+ const docCards = fixture.debugElement.queryAll(
+ By.directive(DocumentCardLargeComponent)
+ )
+ const displayModeButtons = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )
+
+ const selectAllSpy = jest.spyOn(documentListService, 'selectAll')
+ displayModeButtons[2].triggerEventHandler('click')
+ expect(selectAllSpy).toHaveBeenCalled()
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(3)
+ docCards.forEach((card) => {
+ expect(card.context.selected).toBeTruthy()
+ })
+
+ const selectNoneSpy = jest.spyOn(documentListService, 'selectNone')
+ displayModeButtons[0].triggerEventHandler('click')
+ expect(selectNoneSpy).toHaveBeenCalled()
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(0)
+ docCards.forEach((card) => {
+ expect(card.context.selected).toBeFalsy()
+ })
+
+ const selectPageSpy = jest.spyOn(documentListService, 'selectPage')
+ displayModeButtons[1].triggerEventHandler('click')
+ expect(selectPageSpy).toHaveBeenCalled()
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(3)
+ docCards.forEach((card) => {
+ expect(card.context.selected).toBeTruthy()
+ })
+
+ component.toggleSelected(docs[0], new MouseEvent('click'))
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(2)
+ // reset
+ displayModeButtons[0].triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(0)
+
+ // select a range
+ component.toggleSelected(docs[0], new MouseEvent('click'))
+ component.toggleSelected(
+ docs[2],
+ new MouseEvent('click', { shiftKey: true })
+ )
+ fixture.detectChanges()
+ expect(documentListService.selected.size).toEqual(3)
+ })
+
+ it('should support saving an edited view', () => {
+ const view: PaperlessSavedView = {
+ id: 10,
+ name: 'Saved View 10',
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '20',
+ },
+ ],
+ }
+ jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
+ const queryParams = { view: view.id.toString() }
+ jest
+ .spyOn(activatedRoute, 'queryParamMap', 'get')
+ .mockReturnValue(of(convertToParamMap(queryParams)))
+ activatedRoute.snapshot.queryParams = queryParams
+ router.routerState.snapshot.url = '/view/10/'
+ fixture.detectChanges()
+ expect(documentListService.activeSavedViewId).toEqual(10)
+
+ const modifiedView = Object.assign({}, view)
+ delete modifiedView.name
+ const savedViewServicePatch = jest.spyOn(savedViewService, 'patch')
+ savedViewServicePatch.mockReturnValue(of(modifiedView))
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+
+ component.saveViewConfig()
+ expect(savedViewServicePatch).toHaveBeenCalledWith(modifiedView)
+ expect(toastSpy).toHaveBeenCalledWith(
+ `View "${view.name}" saved successfully.`
+ )
+ })
+
+ it('should support edited view saving as', () => {
+ const view: PaperlessSavedView = {
+ id: 10,
+ name: 'Saved View 10',
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '20',
+ },
+ ],
+ }
+ jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
+ const queryParams = { view: view.id.toString() }
+ jest
+ .spyOn(activatedRoute, 'queryParamMap', 'get')
+ .mockReturnValue(of(convertToParamMap(queryParams)))
+ activatedRoute.snapshot.queryParams = queryParams
+ router.routerState.snapshot.url = '/view/10/'
+ fixture.detectChanges()
+ expect(documentListService.activeSavedViewId).toEqual(10)
+
+ const modifiedView = Object.assign({}, view)
+ modifiedView.name = 'Foo Bar'
+
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ const modalSpy = jest.spyOn(modalService, 'open')
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+ const savedViewServiceCreate = jest.spyOn(savedViewService, 'create')
+ savedViewServiceCreate.mockReturnValueOnce(of(modifiedView))
+ component.saveViewConfigAs()
+
+ const modalCloseSpy = jest.spyOn(openModal, 'close')
+ openModal.componentInstance.saveClicked.next({
+ name: 'Foo Bar',
+ show_on_dashboard: true,
+ show_in_sidebar: true,
+ })
+ expect(savedViewServiceCreate).toHaveBeenCalled()
+ expect(modalSpy).toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalled()
+ expect(modalCloseSpy).toHaveBeenCalled()
+ })
+
+ it('should handle error on edited view saving as', () => {
+ const view: PaperlessSavedView = {
+ id: 10,
+ name: 'Saved View 10',
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '20',
+ },
+ ],
+ }
+ jest.spyOn(savedViewService, 'getCached').mockReturnValue(of(view))
+ const queryParams = { view: view.id.toString() }
+ jest
+ .spyOn(activatedRoute, 'queryParamMap', 'get')
+ .mockReturnValue(of(convertToParamMap(queryParams)))
+ activatedRoute.snapshot.queryParams = queryParams
+ router.routerState.snapshot.url = '/view/10/'
+ fixture.detectChanges()
+ expect(documentListService.activeSavedViewId).toEqual(10)
+
+ const modifiedView = Object.assign({}, view)
+ modifiedView.name = 'Foo Bar'
+
+ let openModal: NgbModalRef
+ modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
+ jest.spyOn(savedViewService, 'create').mockReturnValueOnce(
+ throwError(
+ () =>
+ new HttpErrorResponse({
+ error: { filter_rules: [{ value: '11' }] },
+ })
+ )
+ )
+ component.saveViewConfigAs()
+
+ openModal.componentInstance.saveClicked.next({
+ name: 'Foo Bar',
+ show_on_dashboard: true,
+ show_in_sidebar: true,
+ })
+ expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] })
+ })
+
+ it('should navigate to a document', () => {
+ fixture.detectChanges()
+ const routerSpy = jest.spyOn(router, 'navigate')
+ component.openDocumentDetail({ id: 99 })
+ expect(routerSpy).toHaveBeenCalledWith(['documents', 99])
+ })
+
+ it('should support checking if notes enabled to hide column', () => {
+ jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
+ fixture.detectChanges()
+ expect(documentListService.sortField).toEqual('created')
+
+ const detailsDisplayModeButton = fixture.debugElement.query(
+ By.css('input[type="radio"]')
+ )
+ detailsDisplayModeButton.nativeElement.checked = true
+ detailsDisplayModeButton.triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.displayMode).toEqual('details')
+
+ expect(
+ fixture.debugElement.queryAll(By.directive(SortableDirective))
+ ).toHaveLength(9)
+
+ expect(component.notesEnabled).toBeTruthy()
+ settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false)
+ fixture.detectChanges()
+ expect(component.notesEnabled).toBeFalsy()
+ expect(
+ fixture.debugElement.queryAll(By.directive(SortableDirective))
+ ).toHaveLength(8)
+ })
+
+ it('should support toggle on document objects', () => {
+ // TODO: this is just for coverage atm
+ fixture.detectChanges()
+ component.clickTag(1)
+ component.clickCorrespondent(2)
+ component.clickDocumentType(3)
+ component.clickStoragePath(4)
+ })
+
+ it('should support quick filter on document more like', () => {
+ fixture.detectChanges()
+ const qfSpy = jest.spyOn(documentListService, 'quickFilter')
+ component.clickMoreLike(99)
+ expect(qfSpy).toHaveBeenCalledWith([
+ { rule_type: FILTER_FULLTEXT_MORELIKE, value: '99' },
+ ])
+ })
+})
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
+import { FilterRule } from 'src/app/data/filter-rule'
import {
- FilterRule,
filterRulesDiffer,
isFullTextFilterRule,
-} from 'src/app/data/filter-rule'
+} from 'src/app/utils/filter-rules'
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+} from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { By } from '@angular/platform-browser'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbDropdownModule,
+ NgbDatepickerModule,
+ NgbDropdownItem,
+} from '@ng-bootstrap/ng-bootstrap'
+import { NgSelectComponent } from '@ng-select/ng-select'
+import { of } from 'rxjs'
+import {
+ FILTER_TITLE,
+ FILTER_TITLE_CONTENT,
+ FILTER_ASN,
+ FILTER_ASN_ISNULL,
+ FILTER_ASN_GT,
+ FILTER_ASN_LT,
+ FILTER_FULLTEXT_QUERY,
+ FILTER_FULLTEXT_MORELIKE,
+ FILTER_CREATED_AFTER,
+ FILTER_CREATED_BEFORE,
+ FILTER_ADDED_AFTER,
+ FILTER_ADDED_BEFORE,
+ FILTER_HAS_TAGS_ALL,
+ FILTER_HAS_TAGS_ANY,
+ FILTER_HAS_ANY_TAG,
+ FILTER_DOES_NOT_HAVE_TAG,
+ FILTER_CORRESPONDENT,
+ FILTER_HAS_CORRESPONDENT_ANY,
+ FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+ FILTER_DOCUMENT_TYPE,
+ FILTER_HAS_DOCUMENT_TYPE_ANY,
+ FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+ FILTER_STORAGE_PATH,
+ FILTER_HAS_STORAGE_PATH_ANY,
+ FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+ FILTER_OWNER,
+ FILTER_OWNER_ANY,
+ FILTER_OWNER_DOES_NOT_INCLUDE,
+ FILTER_OWNER_ISNULL,
+} from 'src/app/data/filter-rule-type'
+import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
+import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
+import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import { PaperlessUser } from 'src/app/data/paperless-user'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { FilterPipe } from 'src/app/pipes/filter.pipe'
+import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
+import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { StoragePathService } from 'src/app/services/rest/storage-path.service'
+import { TagService } from 'src/app/services/rest/tag.service'
+import { UserService } from 'src/app/services/rest/user.service'
+import { SettingsService } from 'src/app/services/settings.service'
+import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
+import { DateDropdownComponent } from '../../common/date-dropdown/date-dropdown.component'
+import {
+ FilterableDropdownComponent,
+ LogicalOperator,
+ Intersection,
+} from '../../common/filterable-dropdown/filterable-dropdown.component'
+import { ToggleableDropdownButtonComponent } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
+import {
+ PermissionsFilterDropdownComponent,
+ OwnerFilterType,
+} from '../../common/permissions-filter-dropdown/permissions-filter-dropdown.component'
+import { FilterEditorComponent } from './filter-editor.component'
+
+const tags: PaperlessTag[] = [
+ {
+ id: 2,
+ name: 'Tag2',
+ },
+ {
+ id: 3,
+ name: 'Tag3',
+ },
+]
+
+const correspondents: PaperlessCorrespondent[] = [
+ {
+ id: 12,
+ name: 'Corresp12',
+ },
+ {
+ id: 13,
+ name: 'Corresp13',
+ },
+]
+
+const document_types: PaperlessDocumentType[] = [
+ {
+ id: 22,
+ name: 'DocType22',
+ },
+ {
+ id: 23,
+ name: 'DocType23',
+ },
+]
+
+const storage_paths: PaperlessStoragePath[] = [
+ {
+ id: 32,
+ name: 'StoragePath32',
+ },
+ {
+ id: 33,
+ name: 'StoragePath33',
+ },
+]
+
+const users: PaperlessUser[] = [
+ {
+ id: 1,
+ username: 'user1',
+ },
+]
+
+describe('FilterEditorComponent', () => {
+ let component: FilterEditorComponent
+ let fixture: ComponentFixture<FilterEditorComponent>
+ let documentService: DocumentService
+ let settingsService: SettingsService
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ FilterEditorComponent,
+ FilterableDropdownComponent,
+ PermissionsFilterDropdownComponent,
+ FilterPipe,
+ IfPermissionsDirective,
+ ClearableBadgeComponent,
+ ToggleableDropdownButtonComponent,
+ DateDropdownComponent,
+ CustomDatePipe,
+ ],
+ providers: [
+ FilterPipe,
+ CustomDatePipe,
+ DatePipe,
+ {
+ provide: TagService,
+ useValue: {
+ listAll: () => of({ results: tags }),
+ },
+ },
+ {
+ provide: CorrespondentService,
+ useValue: {
+ listAll: () => of({ results: correspondents }),
+ },
+ },
+ {
+ provide: DocumentTypeService,
+ useValue: {
+ listAll: () => of({ results: document_types }),
+ },
+ },
+ {
+ provide: StoragePathService,
+ useValue: {
+ listAll: () => of({ results: storage_paths }),
+ },
+ },
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () => of({ results: users }),
+ },
+ },
+ SettingsService,
+ ],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ NgbDropdownModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbDatepickerModule,
+ ],
+ }).compileComponents()
+
+ documentService = TestBed.inject(DocumentService)
+ settingsService = TestBed.inject(SettingsService)
+ settingsService.currentUser = users[0]
+ fixture = TestBed.createComponent(FilterEditorComponent)
+ component = fixture.componentInstance
+ component.filterRules = []
+ fixture.detectChanges()
+ tick()
+ }))
+
+ // SET filterRules
+
+ it('should ingest text filter rules for doc title', fakeAsync(() => {
+ expect(component.textFilter).toEqual(null)
+ component.filterRules = [
+ {
+ rule_type: FILTER_TITLE,
+ value: 'foo',
+ },
+ ]
+ expect(component.textFilter).toEqual('foo')
+ expect(component.textFilterTarget).toEqual('title') // TEXT_FILTER_TARGET_TITLE
+ }))
+
+ it('should ingest text filter rules for doc title + content', fakeAsync(() => {
+ expect(component.textFilter).toEqual(null)
+ component.filterRules = [
+ {
+ rule_type: FILTER_TITLE_CONTENT,
+ value: 'foo',
+ },
+ ]
+ expect(component.textFilter).toEqual('foo')
+ expect(component.textFilterTarget).toEqual('title-content') // TEXT_FILTER_TARGET_TITLE_CONTENT
+ }))
+
+ it('should ingest text filter rules for doc asn', fakeAsync(() => {
+ expect(component.textFilter).toEqual(null)
+ component.filterRules = [
+ {
+ rule_type: FILTER_ASN,
+ value: 'foo',
+ },
+ ]
+ expect(component.textFilter).toEqual('foo')
+ expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
+ }))
+
+ it('should ingest text filter rules for doc asn is null', fakeAsync(() => {
+ expect(component.textFilterTarget).toEqual('title-content')
+ expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
+ component.filterRules = [
+ {
+ rule_type: FILTER_ASN_ISNULL,
+ value: 'true',
+ },
+ ]
+ expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
+ expect(component.textFilterModifier).toEqual('is null') // TEXT_FILTER_MODIFIER_NULL
+ }))
+
+ it('should ingest text filter rules for doc asn is not null', fakeAsync(() => {
+ expect(component.textFilterTarget).toEqual('title-content')
+ expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
+ component.filterRules = [
+ {
+ rule_type: FILTER_ASN_ISNULL,
+ value: 'false',
+ },
+ ]
+ expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
+ expect(component.textFilterModifier).toEqual('not null') // TEXT_FILTER_MODIFIER_NOTNULL
+ }))
+
+ it('should ingest text filter rules for doc asn greater than', fakeAsync(() => {
+ expect(component.textFilterTarget).toEqual('title-content')
+ expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
+ component.filterRules = [
+ {
+ rule_type: FILTER_ASN_GT,
+ value: '0',
+ },
+ ]
+ expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
+ expect(component.textFilterModifier).toEqual('greater') // TEXT_FILTER_MODIFIER_GT
+ }))
+
+ it('should ingest text filter rules for doc asn less than', fakeAsync(() => {
+ expect(component.textFilterTarget).toEqual('title-content')
+ expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
+ component.filterRules = [
+ {
+ rule_type: FILTER_ASN_LT,
+ value: '1000000',
+ },
+ ]
+ expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
+ expect(component.textFilterModifier).toEqual('less') // TEXT_FILTER_MODIFIER_LT
+ }))
+
+ it('should ingest text filter rules for fulltext query', fakeAsync(() => {
+ expect(component.textFilter).toEqual(null)
+ component.filterRules = [
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'foo,bar',
+ },
+ ]
+ expect(component.textFilter).toEqual('foo,bar')
+ expect(component.textFilterTarget).toEqual('fulltext-query') // TEXT_FILTER_TARGET_FULLTEXT_QUERY
+ }))
+
+ it('should ingest text filter rules for fulltext query that include date created', fakeAsync(() => {
+ expect(component.dateCreatedRelativeDate).toBeNull()
+ component.filterRules = [
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'created:[-1 week to now]',
+ },
+ ]
+ expect(component.dateCreatedRelativeDate).toEqual(0) // RELATIVE_DATE_QUERYSTRINGS['-1 week to now']
+ expect(component.textFilter).toBeNull()
+ }))
+
+ it('should ingest text filter rules for fulltext query that include date added', fakeAsync(() => {
+ expect(component.dateAddedRelativeDate).toBeNull()
+ component.filterRules = [
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'added:[-1 week to now]',
+ },
+ ]
+ expect(component.dateAddedRelativeDate).toEqual(0) // RELATIVE_DATE_QUERYSTRINGS['-1 week to now']
+ expect(component.textFilter).toBeNull()
+ }))
+
+ it('should ingest text filter rules for more like', fakeAsync(() => {
+ const moreLikeSpy = jest.spyOn(documentService, 'get')
+ moreLikeSpy.mockReturnValue(of({ id: 1, title: 'Foo Bar' }))
+ expect(component.textFilter).toEqual(null)
+ component.filterRules = [
+ {
+ rule_type: FILTER_FULLTEXT_MORELIKE,
+ value: '1',
+ },
+ ]
+ expect(component.textFilterTarget).toEqual('fulltext-morelike') // TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
+ expect(moreLikeSpy).toHaveBeenCalledWith(1)
+ expect(component.textFilter).toEqual('Foo Bar')
+ // we have to do this here because it cant be done by user input
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_FULLTEXT_MORELIKE,
+ value: '1',
+ },
+ ])
+ }))
+
+ it('should ingest filter rules for date created after', fakeAsync(() => {
+ expect(component.dateCreatedAfter).toBeNull()
+ component.filterRules = [
+ {
+ rule_type: FILTER_CREATED_AFTER,
+ value: '2023-05-14',
+ },
+ ]
+ expect(component.dateCreatedAfter).toEqual('2023-05-14')
+ }))
+
+ it('should ingest filter rules for date created before', fakeAsync(() => {
+ expect(component.dateCreatedBefore).toBeNull()
+ component.filterRules = [
+ {
+ rule_type: FILTER_CREATED_BEFORE,
+ value: '2023-05-14',
+ },
+ ]
+ expect(component.dateCreatedBefore).toEqual('2023-05-14')
+ }))
+
+ it('should ingest filter rules for date added after', fakeAsync(() => {
+ expect(component.dateAddedAfter).toBeNull()
+ component.filterRules = [
+ {
+ rule_type: FILTER_ADDED_AFTER,
+ value: '2023-05-14',
+ },
+ ]
+ expect(component.dateAddedAfter).toEqual('2023-05-14')
+ }))
+
+ it('should ingest filter rules for date added before', fakeAsync(() => {
+ expect(component.dateAddedBefore).toBeNull()
+ component.filterRules = [
+ {
+ rule_type: FILTER_ADDED_BEFORE,
+ value: '2023-05-14',
+ },
+ ]
+ expect(component.dateAddedBefore).toEqual('2023-05-14')
+ }))
+
+ it('should ingest filter rules for has all tags', fakeAsync(() => {
+ expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '2',
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '3',
+ },
+ ]
+ expect(component.tagSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.And
+ )
+ expect(component.tagSelectionModel.getSelectedItems()).toEqual(tags)
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: null,
+ },
+ ]
+ component.toggleTag(2) // coverage
+ }))
+
+ it('should ingest filter rules for has any tags', fakeAsync(() => {
+ expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '2',
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '3',
+ },
+ ]
+ expect(component.tagSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.tagSelectionModel.getSelectedItems()).toEqual(tags)
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for has any tag', fakeAsync(() => {
+ expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_ANY_TAG,
+ value: '1',
+ },
+ ]
+ expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(1)
+ expect(component.tagSelectionModel.get(null)).toBeTruthy()
+ }))
+
+ it('should ingest filter rules for exclude tag(s)', fakeAsync(() => {
+ expect(component.tagSelectionModel.getExcludedItems()).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_TAG,
+ value: '2',
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_TAG,
+ value: '3',
+ },
+ ]
+ expect(component.tagSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.And
+ )
+ expect(component.tagSelectionModel.getExcludedItems()).toEqual(tags)
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_TAG,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for has correspondent', fakeAsync(() => {
+ expect(
+ component.correspondentSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: '12',
+ },
+ ]
+ expect(component.correspondentSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.correspondentSelectionModel.intersection).toEqual(
+ Intersection.Include
+ )
+ expect(component.correspondentSelectionModel.getSelectedItems()).toEqual([
+ correspondents[0],
+ ])
+ component.toggleCorrespondent(12) // coverage
+ }))
+
+ it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
+ expect(
+ component.correspondentSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+ value: '12',
+ },
+ {
+ rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+ value: '13',
+ },
+ ]
+ expect(component.correspondentSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.correspondentSelectionModel.intersection).toEqual(
+ Intersection.Include
+ )
+ expect(component.correspondentSelectionModel.getSelectedItems()).toEqual(
+ correspondents
+ )
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for does not have any of correspondents', fakeAsync(() => {
+ expect(
+ component.correspondentSelectionModel.getExcludedItems()
+ ).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+ value: '12',
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+ value: '13',
+ },
+ ]
+ expect(component.correspondentSelectionModel.intersection).toEqual(
+ Intersection.Exclude
+ )
+ expect(component.correspondentSelectionModel.getExcludedItems()).toEqual(
+ correspondents
+ )
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for has document type', fakeAsync(() => {
+ expect(
+ component.documentTypeSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: '22',
+ },
+ ]
+ expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.documentTypeSelectionModel.intersection).toEqual(
+ Intersection.Include
+ )
+ expect(component.documentTypeSelectionModel.getSelectedItems()).toEqual([
+ document_types[0],
+ ])
+ component.toggleDocumentType(22) // coverage
+ }))
+
+ it('should ingest filter rules for has any of document types', fakeAsync(() => {
+ expect(
+ component.documentTypeSelectionModel.getSelectedItems()
+ ).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+ value: '22',
+ },
+ {
+ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+ value: '23',
+ },
+ ]
+ expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.documentTypeSelectionModel.intersection).toEqual(
+ Intersection.Include
+ )
+ expect(component.documentTypeSelectionModel.getSelectedItems()).toEqual(
+ document_types
+ )
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for does not have any of document types', fakeAsync(() => {
+ expect(
+ component.documentTypeSelectionModel.getExcludedItems()
+ ).toHaveLength(0)
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+ value: '22',
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+ value: '23',
+ },
+ ]
+ expect(component.documentTypeSelectionModel.intersection).toEqual(
+ Intersection.Exclude
+ )
+ expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual(
+ document_types
+ )
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for has storage path', fakeAsync(() => {
+ expect(component.storagePathSelectionModel.getSelectedItems()).toHaveLength(
+ 0
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_STORAGE_PATH,
+ value: '32',
+ },
+ ]
+ expect(component.storagePathSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.storagePathSelectionModel.intersection).toEqual(
+ Intersection.Include
+ )
+ expect(component.storagePathSelectionModel.getSelectedItems()).toEqual([
+ storage_paths[0],
+ ])
+ component.toggleStoragePath(32) // coverage
+ }))
+
+ it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
+ expect(component.storagePathSelectionModel.getSelectedItems()).toHaveLength(
+ 0
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+ value: '32',
+ },
+ {
+ rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+ value: '33',
+ },
+ ]
+ expect(component.storagePathSelectionModel.logicalOperator).toEqual(
+ LogicalOperator.Or
+ )
+ expect(component.storagePathSelectionModel.intersection).toEqual(
+ Intersection.Include
+ )
+ expect(component.storagePathSelectionModel.getSelectedItems()).toEqual(
+ storage_paths
+ )
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for does not have any of storage paths', fakeAsync(() => {
+ expect(component.storagePathSelectionModel.getExcludedItems()).toHaveLength(
+ 0
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+ value: '32',
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+ value: '33',
+ },
+ ]
+ expect(component.storagePathSelectionModel.intersection).toEqual(
+ Intersection.Exclude
+ )
+ expect(component.storagePathSelectionModel.getExcludedItems()).toEqual(
+ storage_paths
+ )
+ // coverage
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+ value: null,
+ },
+ ]
+ }))
+
+ it('should ingest filter rules for owner', fakeAsync(() => {
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.NONE
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER,
+ value: '100',
+ },
+ ]
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.SELF
+ )
+ expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
+ expect(component.permissionsSelectionModel.userID).toEqual(100)
+ }))
+
+ it('should ingest filter rules for owner is others', fakeAsync(() => {
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.NONE
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_ANY,
+ value: '50',
+ },
+ ]
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.OTHERS
+ )
+ expect(component.permissionsSelectionModel.includeUsers).toContain(50)
+ }))
+
+ it('should ingest filter rules for owner does not include others', fakeAsync(() => {
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.NONE
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
+ value: '50',
+ },
+ ]
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.NOT_SELF
+ )
+ expect(component.permissionsSelectionModel.excludeUsers).toContain(50)
+ }))
+
+ it('should ingest filter rules for owner is null', fakeAsync(() => {
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.NONE
+ )
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_ISNULL,
+ value: 'true',
+ },
+ ]
+ expect(component.permissionsSelectionModel.ownerFilter).toEqual(
+ OwnerFilterType.UNOWNED
+ )
+ expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
+ }))
+
+ it('should ingest filter rules for owner is not null', fakeAsync(() => {
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_ISNULL,
+ value: 'false',
+ },
+ ]
+ expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_ISNULL,
+ value: '0',
+ },
+ ]
+ expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
+ }))
+
+ // GET filterRules
+
+ it('should convert user input to correct filter rules on text field search title + content', fakeAsync(() => {
+ component.textFilterInput.nativeElement.value = 'foo'
+ component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ fixture.detectChanges()
+ tick(400) // debounce time
+ expect(component.textFilter).toEqual('foo')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_TITLE_CONTENT,
+ value: 'foo',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on text field search title only', fakeAsync(() => {
+ component.textFilterInput.nativeElement.value = 'foo'
+ component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ const textFieldTargetDropdown = fixture.debugElement.query(
+ By.directive(NgbDropdownItem)
+ )
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_TITLE
+ fixture.detectChanges()
+ tick(400) // debounce time
+ expect(component.textFilter).toEqual('foo')
+ expect(component.textFilterTarget).toEqual('title')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_TITLE,
+ value: 'foo',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on text field search equals asn', fakeAsync(() => {
+ component.textFilterInput.nativeElement.value = '1234'
+ component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ const textFieldTargetDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )[2]
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN
+ fixture.detectChanges()
+ tick(400) // debounce time
+ expect(component.textFilterTarget).toEqual('asn')
+ expect(component.textFilterModifier).toEqual('equals')
+ expect(component.textFilter).toEqual('1234')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ASN,
+ value: '1234',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on text field search greater than asn', fakeAsync(() => {
+ component.textFilterInput.nativeElement.value = '123'
+ component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ const textFieldTargetDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )[2]
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN
+ fixture.detectChanges()
+ tick(400) // debounce time
+ const textFieldModifierSelect = fixture.debugElement.query(By.css('select'))
+ textFieldModifierSelect.nativeElement.value = 'greater' // TEXT_FILTER_MODIFIER_GT
+ textFieldModifierSelect.nativeElement.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.textFilterTarget).toEqual('asn')
+ expect(component.textFilterModifier).toEqual('greater')
+ expect(component.textFilter).toEqual('123')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ASN_GT,
+ value: '123',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on text field search less than asn', fakeAsync(() => {
+ component.textFilterInput.nativeElement.value = '999'
+ component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ const textFieldTargetDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )[2]
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN
+ fixture.detectChanges()
+ tick(400) // debounce time
+ const textFieldModifierSelect = fixture.debugElement.query(By.css('select'))
+ textFieldModifierSelect.nativeElement.value = 'less' // TEXT_FILTER_MODIFIER_LT
+ textFieldModifierSelect.nativeElement.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.textFilterTarget).toEqual('asn')
+ expect(component.textFilterModifier).toEqual('less')
+ expect(component.textFilter).toEqual('999')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ASN_LT,
+ value: '999',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on asn is null', fakeAsync(() => {
+ const textFieldTargetDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )[2]
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN
+ fixture.detectChanges()
+ const textFieldModifierSelect = fixture.debugElement.query(By.css('select'))
+ textFieldModifierSelect.nativeElement.value = 'is null' // TEXT_FILTER_MODIFIER_LT
+ textFieldModifierSelect.nativeElement.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.textFilterTarget).toEqual('asn')
+ expect(component.textFilterModifier).toEqual('is null')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ASN_ISNULL,
+ value: 'true',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on asn is not null', fakeAsync(() => {
+ const textFieldTargetDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )[2]
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN
+ fixture.detectChanges()
+ const textFieldModifierSelect = fixture.debugElement.query(By.css('select'))
+ textFieldModifierSelect.nativeElement.value = 'not null' // TEXT_FILTER_MODIFIER_LT
+ textFieldModifierSelect.nativeElement.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(component.textFilterTarget).toEqual('asn')
+ expect(component.textFilterModifier).toEqual('not null')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ASN_ISNULL,
+ value: 'false',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on full text query', fakeAsync(() => {
+ component.textFilterInput.nativeElement.value = 'foo'
+ component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ const textFieldTargetDropdown = fixture.debugElement.queryAll(
+ By.directive(NgbDropdownItem)
+ )[3]
+ textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN
+ fixture.detectChanges()
+ tick(400)
+ expect(component.textFilterTarget).toEqual('fulltext-query')
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'foo',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on tag select not assigned', fakeAsync(() => {
+ const tagsFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[0]
+ tagsFilterableDropdown.triggerEventHandler('opened')
+ const tagButton = tagsFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )[0]
+ tagButton.triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_ANY_TAG,
+ value: 'false',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on tag selections', fakeAsync(() => {
+ const tagsFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[0] // Tags dropdown
+ tagsFilterableDropdown.triggerEventHandler('opened')
+ const tagButtons = tagsFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )
+ tagButtons[1].triggerEventHandler('toggle')
+ tagButtons[2].triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags[1].id.toString(),
+ },
+ ])
+ const toggleOperatorButtons = tagsFilterableDropdown.queryAll(
+ By.css('input[type=radio]')
+ )
+ toggleOperatorButtons[1].nativeElement.checked = true
+ toggleOperatorButtons[1].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: tags[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: tags[1].id.toString(),
+ },
+ ])
+ tagButtons[2].triggerEventHandler('exclude')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_TAG,
+ value: tags[1].id.toString(),
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on correspondent selections', fakeAsync(() => {
+ const correspondentsFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[1] // Corresp dropdown
+ correspondentsFilterableDropdown.triggerEventHandler('opened')
+ const correspondentButtons = correspondentsFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )
+ correspondentButtons[1].triggerEventHandler('toggle')
+ correspondentButtons[2].triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+ value: correspondents[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+ value: correspondents[1].id.toString(),
+ },
+ ])
+ const toggleIntersectionButtons = correspondentsFilterableDropdown.queryAll(
+ By.css('input[type=radio]')
+ )
+ toggleIntersectionButtons[1].nativeElement.checked = true
+ toggleIntersectionButtons[1].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+ value: correspondents[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+ value: correspondents[1].id.toString(),
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on correspondent select not assigned', fakeAsync(() => {
+ const correspondentsFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[1]
+ correspondentsFilterableDropdown.triggerEventHandler('opened')
+ const notAssignedButton = correspondentsFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )[0]
+ notAssignedButton.triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: null,
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
+ const documentTypesFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[2] // DocType dropdown
+ documentTypesFilterableDropdown.triggerEventHandler('opened')
+ const documentTypeButtons = documentTypesFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )
+ documentTypeButtons[1].triggerEventHandler('toggle')
+ documentTypeButtons[2].triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+ value: document_types[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+ value: document_types[1].id.toString(),
+ },
+ ])
+ const toggleIntersectionButtons = documentTypesFilterableDropdown.queryAll(
+ By.css('input[type=radio]')
+ )
+ toggleIntersectionButtons[1].nativeElement.checked = true
+ toggleIntersectionButtons[1].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+ value: document_types[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+ value: document_types[1].id.toString(),
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on doc type select not assigned', fakeAsync(() => {
+ const docTypesFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[2]
+ docTypesFilterableDropdown.triggerEventHandler('opened')
+ const notAssignedButton = docTypesFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )[0]
+ notAssignedButton.triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: null,
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
+ const storagePathFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[3] // StoragePath dropdown
+ storagePathFilterableDropdown.triggerEventHandler('opened')
+ const storagePathButtons = storagePathFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )
+ storagePathButtons[1].triggerEventHandler('toggle')
+ storagePathButtons[2].triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+ value: storage_paths[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+ value: storage_paths[1].id.toString(),
+ },
+ ])
+ const toggleIntersectionButtons = storagePathFilterableDropdown.queryAll(
+ By.css('input[type=radio]')
+ )
+ toggleIntersectionButtons[1].nativeElement.checked = true
+ toggleIntersectionButtons[1].triggerEventHandler('change')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+ value: storage_paths[0].id.toString(),
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+ value: storage_paths[1].id.toString(),
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on storage path select not assigned', fakeAsync(() => {
+ const storagePathsFilterableDropdown = fixture.debugElement.queryAll(
+ By.directive(FilterableDropdownComponent)
+ )[3]
+ storagePathsFilterableDropdown.triggerEventHandler('opened')
+ const notAssignedButton = storagePathsFilterableDropdown.queryAll(
+ By.directive(ToggleableDropdownButtonComponent)
+ )[0]
+ notAssignedButton.triggerEventHandler('toggle')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_STORAGE_PATH,
+ value: null,
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on date created after', fakeAsync(() => {
+ const dateCreatedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[0]
+ const dateCreatedAfter = dateCreatedDropdown.queryAll(By.css('input'))[0]
+
+ dateCreatedAfter.nativeElement.value = '05/14/2023'
+ // dateCreatedAfter.triggerEventHandler('change')
+ // TODO: why isnt ngModel triggering this on change?
+ component.dateCreatedAfter = '2023-05-14'
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_CREATED_AFTER,
+ value: '2023-05-14',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on date created before', fakeAsync(() => {
+ const dateCreatedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[0]
+ const dateCreatedBefore = dateCreatedDropdown.queryAll(By.css('input'))[1]
+
+ dateCreatedBefore.nativeElement.value = '05/14/2023'
+ // dateCreatedBefore.triggerEventHandler('change')
+ // TODO: why isnt ngModel triggering this on change?
+ component.dateCreatedBefore = '2023-05-14'
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_CREATED_BEFORE,
+ value: '2023-05-14',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on date created with relative date', fakeAsync(() => {
+ const dateCreatedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[0]
+ const dateCreatedBeforeRelativeButton = dateCreatedDropdown.queryAll(
+ By.css('button')
+ )[1]
+ dateCreatedBeforeRelativeButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'created:[-1 week to now]',
+ },
+ ])
+ }))
+
+ it('should carry over text filtering on date created with relative date', fakeAsync(() => {
+ component.textFilter = 'foo'
+ const dateCreatedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[0]
+ const dateCreatedBeforeRelativeButton = dateCreatedDropdown.queryAll(
+ By.css('button')
+ )[1]
+ dateCreatedBeforeRelativeButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'foo,created:[-1 week to now]',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on date added after', fakeAsync(() => {
+ const dateAddedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[1]
+ const dateAddedAfter = dateAddedDropdown.queryAll(By.css('input'))[0]
+
+ dateAddedAfter.nativeElement.value = '05/14/2023'
+ // dateAddedAfter.triggerEventHandler('change')
+ // TODO: why isnt ngModel triggering this on change?
+ component.dateAddedAfter = '2023-05-14'
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ADDED_AFTER,
+ value: '2023-05-14',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on date added before', fakeAsync(() => {
+ const dateAddedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[1]
+ const dateAddedBefore = dateAddedDropdown.queryAll(By.css('input'))[1]
+
+ dateAddedBefore.nativeElement.value = '05/14/2023'
+ // dateAddedBefore.triggerEventHandler('change')
+ // TODO: why isnt ngModel triggering this on change?
+ component.dateAddedBefore = '2023-05-14'
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_ADDED_BEFORE,
+ value: '2023-05-14',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter rules on date added with relative date', fakeAsync(() => {
+ const dateAddedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[1]
+ const dateAddedBeforeRelativeButton = dateAddedDropdown.queryAll(
+ By.css('button')
+ )[1]
+ dateAddedBeforeRelativeButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'added:[-1 week to now]',
+ },
+ ])
+ }))
+
+ it('should carry over text filtering on date added with relative date', fakeAsync(() => {
+ component.textFilter = 'foo'
+ const dateAddedDropdown = fixture.debugElement.queryAll(
+ By.directive(DateDropdownComponent)
+ )[1]
+ const dateAddedBeforeRelativeButton = dateAddedDropdown.queryAll(
+ By.css('button')
+ )[1]
+ dateAddedBeforeRelativeButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'foo,added:[-1 week to now]',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter on permissions select my docs', fakeAsync(() => {
+ const permissionsDropdown = fixture.debugElement.query(
+ By.directive(PermissionsFilterDropdownComponent)
+ )
+ const myDocsButton = permissionsDropdown.queryAll(By.css('button'))[2]
+ myDocsButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ tick(400)
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER,
+ value: '1',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter on permissions select shared with me', fakeAsync(() => {
+ const permissionsDropdown = fixture.debugElement.query(
+ By.directive(PermissionsFilterDropdownComponent)
+ )
+ const sharedWithMe = permissionsDropdown.queryAll(By.css('button'))[3]
+ sharedWithMe.triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
+ value: '1',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter on permissions select shared with me', fakeAsync(() => {
+ const permissionsDropdown = fixture.debugElement.query(
+ By.directive(PermissionsFilterDropdownComponent)
+ )
+ const sharedWithMeButton = permissionsDropdown.queryAll(By.css('button'))[3]
+ sharedWithMeButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
+ value: '1',
+ },
+ ])
+ component.permissionsSelectionModel.excludeUsers.push(2)
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
+ value: '1,2',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => {
+ const permissionsDropdown = fixture.debugElement.query(
+ By.directive(PermissionsFilterDropdownComponent)
+ )
+ const unownedButton = permissionsDropdown.queryAll(By.css('button'))[4]
+ unownedButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER_ISNULL,
+ value: 'true',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter on permissions select others', fakeAsync(() => {
+ const permissionsDropdown = fixture.debugElement.query(
+ By.directive(PermissionsFilterDropdownComponent)
+ )
+ const userSelect = permissionsDropdown.query(
+ By.directive(NgSelectComponent)
+ )
+ // TODO: mock input in code
+ // userSelect.query(By.css('input')).nativeElement.value = '3'
+ // userSelect.triggerEventHandler('change')
+ component.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS
+ component.permissionsSelectionModel.includeUsers.push(3)
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER_ANY,
+ value: '3',
+ },
+ ])
+ }))
+
+ it('should convert user input to correct filter on permissions hide unowned', fakeAsync(() => {
+ const permissionsDropdown = fixture.debugElement.query(
+ By.directive(PermissionsFilterDropdownComponent)
+ )
+ const ownerToggle = permissionsDropdown.query(
+ By.css('input[type=checkbox]')
+ )
+ ownerToggle.nativeElement.checked = true
+ // ownerToggle.triggerEventHandler('change')
+ // TODO: ngModel isnt doing this here
+ component.permissionsSelectionModel.hideUnowned = true
+ fixture.detectChanges()
+ expect(component.filterRules).toEqual([
+ {
+ rule_type: FILTER_OWNER_ISNULL,
+ value: 'false',
+ },
+ ])
+ }))
+
+ // The rest
+
+ it('should support setting selection data', () => {
+ component.selectionData = null
+ component.selectionData = {
+ selected_storage_paths: [
+ { id: 2, document_count: 1 },
+ { id: 3, document_count: 0 },
+ ],
+ selected_correspondents: [
+ { id: 12, document_count: 1 },
+ { id: 13, document_count: 0 },
+ ],
+ selected_tags: [
+ { id: 22, document_count: 1 },
+ { id: 23, document_count: 0 },
+ ],
+ selected_document_types: [
+ { id: 32, document_count: 1 },
+ { id: 33, document_count: 0 },
+ ],
+ }
+ })
+
+ it('should generate filter names', () => {
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+ value: '12',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual(
+ `Correspondent: ${correspondents[0].name}`
+ )
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: null,
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Without correspondent')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+ value: '22',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual(
+ `Document type: ${document_types[0].name}`
+ )
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_DOCUMENT_TYPE,
+ value: null,
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Without document type')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+ value: '32',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual(
+ `Storage path: ${storage_paths[0].name}`
+ )
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_STORAGE_PATH,
+ value: null,
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Without storage path')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '2',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual(`Tag: ${tags[0].name}`)
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_ANY_TAG,
+ value: 'false',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Without any tag')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_TITLE,
+ value: 'foo',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Title: foo')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_ASN,
+ value: '1234',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('ASN: 1234')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER,
+ value: '1',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Owner: 1')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
+ value: '1',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Owner not in: 1')
+
+ component.filterRules = [
+ {
+ rule_type: FILTER_OWNER_ISNULL,
+ value: 'true',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('Without an owner')
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '2',
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '3',
+ },
+ ]
+ expect(component.generateFilterName()).toEqual('')
+ })
+
+ it('should support resetting filter rules', () => {
+ const rules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '2',
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '3',
+ },
+ ]
+ component.unmodifiedFilterRules = rules
+ component.filterRules = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: '2',
+ },
+ {
+ rule_type: FILTER_DOES_NOT_HAVE_TAG,
+ value: '3',
+ },
+ ]
+ component.resetSelected()
+ expect(component.filterRules).toEqual(rules)
+ })
+
+ it('should support resetting text field', () => {
+ component.textFilter = 'foo'
+ component.resetTextField()
+ expect(component.textFilter).toEqual('')
+ })
+
+ it('should support Enter / Esc key on text field', () => {
+ component.textFilterInput.nativeElement.value = 'foo'
+ component.textFilterInput.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Enter' })
+ )
+ expect(component.textFilter).toEqual('foo')
+ component.textFilterInput.nativeElement.value = 'foo bar'
+ component.textFilterInput.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { key: 'Escape' })
+ )
+ expect(component.textFilter).toEqual('')
+ })
+})
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
-import { filterRulesDiffer, FilterRule } from 'src/app/data/filter-rule'
+import { FilterRule } from 'src/app/data/filter-rule'
+import { filterRulesDiffer } from 'src/app/utils/filter-rules'
import {
FILTER_ADDED_AFTER,
FILTER_ADDED_BEFORE,
OwnerFilterType,
PermissionsSelectionModel,
} from '../../common/permissions-filter-dropdown/permissions-filter-dropdown.component'
-import { SettingsService } from 'src/app/services/settings.service'
const TEXT_FILTER_TARGET_TITLE = 'title'
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
generateFilterName() {
if (this.filterRules.length == 1) {
let rule = this.filterRules[0]
- switch (this.filterRules[0].rule_type) {
+ switch (rule.rule_type) {
+ case FILTER_CORRESPONDENT:
case FILTER_HAS_CORRESPONDENT_ANY:
if (rule.value) {
return $localize`Correspondent: ${
return $localize`Without correspondent`
}
+ case FILTER_DOCUMENT_TYPE:
case FILTER_HAS_DOCUMENT_TYPE_ANY:
if (rule.value) {
- return $localize`Type: ${
+ return $localize`Document type: ${
this.documentTypes.find((dt) => dt.id == +rule.value)?.name
}`
} else {
return $localize`Without document type`
}
+ case FILTER_STORAGE_PATH:
+ case FILTER_HAS_STORAGE_PATH_ANY:
+ if (rule.value) {
+ return $localize`Storage path: ${
+ this.storagePaths.find((sp) => sp.id == +rule.value)?.name
+ }`
+ } else {
+ return $localize`Without storage path`
+ }
+
case FILTER_HAS_TAGS_ALL:
return $localize`Tag: ${
this.tags.find((t) => t.id == +rule.value)?.name
private tagService: TagService,
private correspondentService: CorrespondentService,
private documentService: DocumentService,
- private storagePathService: StoragePathService,
- private settingsService: SettingsService
+ private storagePathService: StoragePathService
) {}
@ViewChild('textFilterInput')
) {
filterRules.push({
rule_type: FILTER_FULLTEXT_MORELIKE,
- value: this._moreLikeId?.toString(),
+ value: this._moreLikeId.toString(),
})
}
if (this.tagSelectionModel.isNoneSelected()) {
--- /dev/null
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { SaveViewConfigDialogComponent } from './save-view-config-dialog.component'
+import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
+import { By } from '@angular/platform-browser'
+import { TextComponent } from '../../common/input/text/text.component'
+import { CheckComponent } from '../../common/input/check/check.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+
+describe('SaveViewConfigDialogComponent', () => {
+ let component: SaveViewConfigDialogComponent
+ let fixture: ComponentFixture<SaveViewConfigDialogComponent>
+ let modal: NgbActiveModal
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ SaveViewConfigDialogComponent,
+ TextComponent,
+ CheckComponent,
+ ],
+ providers: [NgbActiveModal],
+ imports: [NgbModalModule, FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ modal = TestBed.inject(NgbActiveModal)
+ fixture = TestBed.createComponent(SaveViewConfigDialogComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ tick()
+ }))
+
+ it('should support default name', () => {
+ const name = 'Tag: Inbox'
+ let result
+ component.saveClicked.subscribe((saveResult) => (result = saveResult))
+ component.defaultName = name
+ component.save()
+ expect(component.defaultName).toEqual(name)
+ expect(result).toEqual({
+ name,
+ showInSideBar: false,
+ showOnDashboard: false,
+ })
+ })
+
+ it('should support user input', () => {
+ const name = 'Tag: Inbox'
+ let result
+ component.saveClicked.subscribe((saveResult) => (result = saveResult))
+
+ const nameInput = fixture.debugElement
+ .query(By.directive(TextComponent))
+ .query(By.css('input'))
+ nameInput.nativeElement.value = name
+ component.saveViewConfigForm.get('name').patchValue(name) // normally done by angular
+
+ const sidebarCheckInput = fixture.debugElement
+ .queryAll(By.directive(CheckComponent))[0]
+ .query(By.css('input'))
+ sidebarCheckInput.nativeElement.checked = true
+ component.saveViewConfigForm.get('showInSideBar').patchValue(true) // normally done by angular
+
+ const dashboardCheckInput = fixture.debugElement
+ .queryAll(By.directive(CheckComponent))[1]
+ .query(By.css('input'))
+ dashboardCheckInput.nativeElement.checked = true
+ component.saveViewConfigForm.get('showOnDashboard').patchValue(true) // normally done by angular
+
+ component.save()
+ expect(result).toEqual({
+ name,
+ showInSideBar: true,
+ showOnDashboard: true,
+ })
+ })
+
+ it('should support default name', () => {
+ const saveClickedSpy = jest.spyOn(component.saveClicked, 'emit')
+ const modalCloseSpy = jest.spyOn(modal, 'close')
+ component.cancel()
+ expect(saveClickedSpy).not.toHaveBeenCalled()
+ expect(modalCloseSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { DocumentNotesComponent } from './document-notes.component'
+import { UserService } from 'src/app/services/rest/user.service'
+import { of, throwError } from 'rxjs'
+import { DocumentNotesService } from 'src/app/services/rest/document-notes.service'
+import { ToastService } from 'src/app/services/toast.service'
+import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { DatePipe } from '@angular/common'
+import { By } from '@angular/platform-browser'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+
+const notes: PaperlessDocumentNote[] = [
+ {
+ id: 23,
+ note: 'Note 23',
+ user: 1,
+ },
+ {
+ id: 24,
+ note: 'Note 24',
+ user: 1,
+ },
+ {
+ id: 25,
+ note: 'Note 25',
+ user: 2,
+ },
+ {
+ id: 30,
+ note: 'Note 30',
+ user: 3,
+ },
+]
+
+describe('DocumentNotesComponent', () => {
+ let component: DocumentNotesComponent
+ let fixture: ComponentFixture<DocumentNotesComponent>
+ let notesService: DocumentNotesService
+ let toastService: ToastService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentNotesComponent,
+ CustomDatePipe,
+ IfPermissionsDirective,
+ ],
+ providers: [
+ {
+ provide: UserService,
+ useValue: {
+ listAll: () =>
+ of({
+ results: [
+ {
+ id: 1,
+ username: 'user1',
+ first_name: 'User1',
+ last_name: 'Lastname1',
+ },
+ {
+ id: 2,
+ username: 'user2',
+ },
+ {
+ id: 3,
+ username: 'user3',
+ },
+ ],
+ }),
+ },
+ },
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ CustomDatePipe,
+ DatePipe,
+ ],
+ imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
+ }).compileComponents()
+
+ notesService = TestBed.inject(DocumentNotesService)
+ toastService = TestBed.inject(ToastService)
+ fixture = TestBed.createComponent(DocumentNotesComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should display notes with user name / username', () => {
+ component.notes = notes
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ notes[0].note
+ )
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ notes[1].note
+ )
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ notes[2].note
+ )
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ notes[3].note
+ )
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ 'User1 Lastname1'
+ )
+ expect(fixture.debugElement.nativeElement.textContent).toContain('user2')
+ expect(fixture.debugElement.nativeElement.textContent).toContain('user3')
+ })
+
+ it('should handle note user display in all situations', () => {
+ expect(component.displayName({ id: 1, user: 1 })).toEqual(
+ 'User1 Lastname1 (user1)'
+ )
+ expect(component.displayName({ id: 1, user: 2 })).toEqual('user2')
+ expect(component.displayName({ id: 1, user: 4 })).toEqual('')
+ expect(component.displayName({ id: 1 })).toEqual('')
+ })
+
+ it('should support note entry, show error if fails', () => {
+ component.documentId = 12
+ const note = 'This is the new note.'
+ const noteTextArea = fixture.debugElement.query(By.css('textarea'))
+ noteTextArea.nativeElement.value = note
+ noteTextArea.nativeElement.dispatchEvent(new Event('input'))
+ fixture.detectChanges()
+ const addSpy = jest.spyOn(notesService, 'addNote')
+ addSpy.mockReturnValueOnce(throwError(() => new Error('error saving note')))
+ const toastsSpy = jest.spyOn(toastService, 'showError')
+ const addButton = fixture.debugElement.query(By.css('button'))
+ addButton.triggerEventHandler('click')
+ expect(addSpy).toHaveBeenCalledWith(12, note)
+ expect(toastsSpy).toHaveBeenCalled()
+
+ addSpy.mockReturnValueOnce(of([...notes, { id: 31, note, user: 1 }]))
+ addButton.triggerEventHandler('click')
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain(note)
+ })
+
+ it('should support note save on ctrl+Enter', () => {
+ component.documentId = 12
+ const note = 'This is the new note.'
+ const noteTextArea = fixture.debugElement.query(By.css('textarea'))
+ noteTextArea.nativeElement.value = note
+ const addSpy = jest.spyOn(component, 'addNote')
+ noteTextArea.nativeElement.dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Enter', ctrlKey: true })
+ )
+ expect(addSpy).toHaveBeenCalled()
+ })
+
+ it('should support delete note, show error if fails', () => {
+ component.documentId = 12
+ component.notes = notes
+ fixture.detectChanges()
+ const deleteButton = fixture.debugElement.queryAll(By.css('button'))[1] // 0 is add button
+ const deleteSpy = jest.spyOn(notesService, 'deleteNote')
+ const toastsSpy = jest.spyOn(toastService, 'showError')
+ deleteSpy.mockReturnValueOnce(
+ throwError(() => new Error('error deleting note'))
+ )
+ deleteButton.triggerEventHandler('click')
+ expect(deleteSpy).toHaveBeenCalledWith(12, notes[0].id)
+ expect(toastsSpy).toHaveBeenCalled()
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ notes[0].note
+ )
+
+ deleteSpy.mockReturnValueOnce(of(notes.slice(1, 2)))
+ deleteButton.triggerEventHandler('click')
+ expect(deleteSpy).toHaveBeenCalledWith(12, notes[0].id)
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).not.toContain(
+ notes[0].note
+ )
+ })
+})
import { DocumentNotesService } from 'src/app/services/rest/document-notes.service'
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
import { FormControl, FormGroup } from '@angular/forms'
-import { first } from 'rxjs/operators'
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { UserService } from 'src/app/services/rest/user.service'
const user = this.users?.find((u) => u.id === note.user)
if (!user) return ''
const nameComponents = []
- if (user.first_name) nameComponents.unshift(user.first_name)
- if (user.last_name) nameComponents.unshift(user.last_name)
+ if (user.first_name) nameComponents.push(user.first_name)
+ if (user.last_name) nameComponents.push(user.last_name)
if (user.username) {
if (nameComponents.length > 0) nameComponents.push(`(${user.username})`)
else nameComponents.push(user.username)
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { CorrespondentListComponent } from './correspondent-list.component'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { DatePipe } from '@angular/common'
+import { SortableDirective } from 'src/app/directives/sortable.directive'
+import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
+import { of } from 'rxjs'
+
+describe('CorrespondentListComponent', () => {
+ let component: CorrespondentListComponent
+ let fixture: ComponentFixture<CorrespondentListComponent>
+ let correspondentsService: CorrespondentService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ CorrespondentListComponent,
+ SortableDirective,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ ],
+ providers: [DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ NgbPaginationModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ }).compileComponents()
+
+ correspondentsService = TestBed.inject(CorrespondentService)
+ jest.spyOn(correspondentsService, 'listFiltered').mockReturnValue(
+ of({
+ count: 3,
+ all: [1, 2, 3],
+ results: [
+ {
+ id: 1,
+ name: 'Correspondent1',
+ },
+ {
+ id: 2,
+ name: 'Correspondent2',
+ },
+ {
+ id: 3,
+ name: 'Correspondent3',
+ },
+ ],
+ })
+ )
+ fixture = TestBed.createComponent(CorrespondentListComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ // Tests are included in management-list.compontent.spec.ts
+
+ it('should use correct delete message', () => {
+ expect(
+ component.getDeleteMessage({ id: 1, name: 'Correspondent1' })
+ ).toEqual(
+ 'Do you really want to delete the correspondent "Correspondent1"?'
+ )
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { DatePipe } from '@angular/common'
+import { SortableDirective } from 'src/app/directives/sortable.directive'
+import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { of } from 'rxjs'
+import { DocumentTypeListComponent } from './document-type-list.component'
+import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
+
+describe('DocumentTypeListComponent', () => {
+ let component: DocumentTypeListComponent
+ let fixture: ComponentFixture<DocumentTypeListComponent>
+ let documentTypeService: DocumentTypeService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DocumentTypeListComponent,
+ SortableDirective,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ ],
+ providers: [DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ NgbPaginationModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ }).compileComponents()
+
+ documentTypeService = TestBed.inject(DocumentTypeService)
+ jest.spyOn(documentTypeService, 'listFiltered').mockReturnValue(
+ of({
+ count: 3,
+ all: [1, 2, 3],
+ results: [
+ {
+ id: 1,
+ name: 'DocumentType1',
+ },
+ {
+ id: 2,
+ name: 'DocumentType2',
+ },
+ {
+ id: 3,
+ name: 'DocumentType3',
+ },
+ ],
+ })
+ )
+ fixture = TestBed.createComponent(DocumentTypeListComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ // Tests are included in management-list.compontent.spec.ts
+
+ it('should use correct delete message', () => {
+ expect(
+ component.getDeleteMessage({ id: 1, name: 'DocumentType1' })
+ ).toEqual('Do you really want to delete the document type "DocumentType1"?')
+ })
+})
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { LogService } from 'src/app/services/rest/log.service'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { LogsComponent } from './logs.component'
+import { of, throwError } from 'rxjs'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
+import { BrowserModule, By } from '@angular/platform-browser'
+
+const paperless_logs = [
+ '[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
+ '[2023-05-29 04:05:00,622] [DEBUG] [paperless.classifier] Gathering data from database...',
+ '[2023-05-29 04:05:01,213] [DEBUG] [paperless.tasks] Training data unchanged.',
+ '[2023-06-11 00:30:01,774] [INFO] [paperless.sanity_checker] Document contains no OCR data',
+ '[2023-06-11 00:30:01,774] [WARNING] [paperless.sanity_checker] Made up',
+ '[2023-06-11 00:30:01,774] [ERROR] [paperless.sanity_checker] Document contains no OCR data',
+ '[2023-06-11 00:30:01,774] [CRITICAL] [paperless.sanity_checker] Document contains no OCR data',
+]
+const mail_logs = [
+ '[2023-06-09 01:10:00,666] [DEBUG] [paperless_mail] Rule inbox@example.com.Incoming: Searching folder with criteria (SINCE 10-May-2023 UNSEEN)',
+ '[2023-06-09 01:10:01,385] [DEBUG] [paperless_mail] Rule inbox@example.com.Incoming: Processed 3 matching mail(s)',
+]
+
+describe('LogsComponent', () => {
+ let component: LogsComponent
+ let fixture: ComponentFixture<LogsComponent>
+ let logService: LogService
+ let logSpy
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [LogsComponent, PageHeaderComponent],
+ providers: [],
+ imports: [HttpClientTestingModule, BrowserModule, NgbModule],
+ }).compileComponents()
+
+ logService = TestBed.inject(LogService)
+ jest.spyOn(logService, 'list').mockReturnValue(of(['paperless', 'mail']))
+ logSpy = jest.spyOn(logService, 'get')
+ logSpy.mockImplementation((id) => {
+ return of(id === 'paperless' ? paperless_logs : mail_logs)
+ })
+ fixture = TestBed.createComponent(LogsComponent)
+ component = fixture.componentInstance
+ window.HTMLElement.prototype.scroll = function () {} // mock scroll
+ fixture.detectChanges()
+ })
+
+ it('should display logs with first log initially', () => {
+ expect(logSpy).toHaveBeenCalledWith('paperless')
+ fixture.detectChanges()
+ expect(fixture.debugElement.nativeElement.textContent).toContain(
+ paperless_logs[0]
+ )
+ })
+
+ it('should load log when tab clicked', () => {
+ fixture.debugElement
+ .queryAll(By.directive(NgbNavLink))[1]
+ .nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(logSpy).toHaveBeenCalledWith('mail')
+ })
+
+ it('should handle error with no logs', () => {
+ logSpy.mockReturnValueOnce(
+ throwError(() => new Error('error getting logs'))
+ )
+ component.reloadLogs()
+ expect(component.logs).toHaveLength(0)
+ })
+})
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { By } from '@angular/platform-browser'
+import {
+ NgbModal,
+ NgbModalModule,
+ NgbModalRef,
+ NgbPaginationModule,
+} from '@ng-bootstrap/ng-bootstrap'
+import { of, throwError } from 'rxjs'
+import { PaperlessTag } from 'src/app/data/paperless-tag'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SortableDirective } from 'src/app/directives/sortable.directive'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+import { TagService } from 'src/app/services/rest/tag.service'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { TagListComponent } from '../tag-list/tag-list.component'
+import { ManagementListComponent } from './management-list.component'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { ToastService } from 'src/app/services/toast.service'
+import { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component'
+import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from 'src/app/app-routing.module'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { MATCH_AUTO } from 'src/app/data/matching-model'
+import { MATCH_NONE } from 'src/app/data/matching-model'
+import { MATCH_LITERAL } from 'src/app/data/matching-model'
+
+const tags: PaperlessTag[] = [
+ {
+ id: 1,
+ name: 'Tag1 Foo',
+ matching_algorithm: MATCH_LITERAL,
+ match: 'foo',
+ },
+ {
+ id: 2,
+ name: 'Tag2',
+ matching_algorithm: MATCH_NONE,
+ },
+ {
+ id: 3,
+ name: 'Tag3',
+ matching_algorithm: MATCH_AUTO,
+ },
+]
+
+describe('ManagementListComponent', () => {
+ let component: ManagementListComponent<PaperlessTag>
+ let fixture: ComponentFixture<ManagementListComponent<PaperlessTag>>
+ let tagService: TagService
+ let modalService: NgbModal
+ let toastService: ToastService
+ let documentListViewService: DocumentListViewService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TagListComponent,
+ SortableDirective,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ SafeHtmlPipe,
+ ConfirmDialogComponent,
+ ],
+ providers: [
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ currentUserHasObjectPermissions: () => true,
+ currentUserOwnsObject: () => true,
+ },
+ },
+ DatePipe,
+ PermissionsGuard,
+ ],
+ imports: [
+ HttpClientTestingModule,
+ NgbPaginationModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbModalModule,
+ RouterTestingModule.withRoutes(routes),
+ ],
+ }).compileComponents()
+
+ tagService = TestBed.inject(TagService)
+ jest
+ .spyOn(tagService, 'listFiltered')
+ .mockImplementation(
+ (page, pageSize, sortField, sortReverse, nameFilter, fullPerms) => {
+ const results = nameFilter
+ ? tags.filter((t) => t.name.toLowerCase().includes(nameFilter))
+ : tags
+ return of({
+ count: results.length,
+ all: results.map((o) => o.id),
+ results,
+ })
+ }
+ )
+ modalService = TestBed.inject(NgbModal)
+ toastService = TestBed.inject(ToastService)
+ documentListViewService = TestBed.inject(DocumentListViewService)
+ fixture = TestBed.createComponent(TagListComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ // These tests are shared among all management list components
+
+ it('should support filtering, clear on Esc key', fakeAsync(() => {
+ const nameFilterInput = fixture.debugElement.query(By.css('input'))
+ nameFilterInput.nativeElement.value = 'foo'
+ // nameFilterInput.nativeElement.dispatchEvent(new Event('input'))
+ component.nameFilter = 'foo' // subject normally triggered by ngModel
+ tick(400) // debounce
+ fixture.detectChanges()
+ expect(component.data).toEqual([tags[0]])
+
+ nameFilterInput.nativeElement.dispatchEvent(
+ new KeyboardEvent('keyup', { code: 'Escape' })
+ )
+ tick(400) // debounce
+ fixture.detectChanges()
+ expect(component.nameFilter).toBeNull()
+ expect(component.data).toEqual(tags)
+ }))
+
+ it('should support create, show notification on error / success', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ const reloadSpy = jest.spyOn(component, 'reloadData')
+
+ const createButton = fixture.debugElement.queryAll(By.css('button'))[0]
+ createButton.triggerEventHandler('click')
+
+ expect(modal).not.toBeUndefined()
+ const editDialog =
+ modal.componentInstance as EditDialogComponent<PaperlessTag>
+
+ // fail first
+ editDialog.failed.emit({ error: 'error creating item' })
+ expect(toastErrorSpy).toHaveBeenCalled()
+ expect(reloadSpy).not.toHaveBeenCalled()
+
+ // succeed
+ editDialog.succeeded.emit()
+ expect(toastInfoSpy).toHaveBeenCalled()
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+
+ it('should support edit, show notification on error / success', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ const reloadSpy = jest.spyOn(component, 'reloadData')
+
+ const editButton = fixture.debugElement.queryAll(By.css('button'))[3]
+ editButton.triggerEventHandler('click')
+
+ expect(modal).not.toBeUndefined()
+ const editDialog =
+ modal.componentInstance as EditDialogComponent<PaperlessTag>
+ expect(editDialog.object).toEqual(tags[0])
+
+ // fail first
+ editDialog.failed.emit({ error: 'error editing item' })
+ expect(toastErrorSpy).toHaveBeenCalled()
+ expect(reloadSpy).not.toHaveBeenCalled()
+
+ // succeed
+ editDialog.succeeded.emit()
+ expect(toastInfoSpy).toHaveBeenCalled()
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+
+ it('should support delete, show notification on error / success', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const deleteSpy = jest.spyOn(tagService, 'delete')
+ const reloadSpy = jest.spyOn(component, 'reloadData')
+
+ const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4]
+ deleteButton.triggerEventHandler('click')
+
+ expect(modal).not.toBeUndefined()
+ const editDialog = modal.componentInstance as ConfirmDialogComponent
+
+ // fail first
+ deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
+ editDialog.confirmClicked.emit()
+ expect(toastErrorSpy).toHaveBeenCalled()
+ expect(reloadSpy).not.toHaveBeenCalled()
+
+ // succeed
+ deleteSpy.mockReturnValueOnce(of(true))
+ editDialog.confirmClicked.emit()
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+
+ it('should support quick filter for objects', () => {
+ const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
+ const filterButton = fixture.debugElement.queryAll(By.css('button'))[2]
+ filterButton.triggerEventHandler('click')
+ expect(qfSpy).toHaveBeenCalledWith([
+ { rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
+ ]) // subclasses set the filter rule type
+ })
+
+ it('should reload on sort', () => {
+ const reloadSpy = jest.spyOn(component, 'reloadData')
+ const sortable = fixture.debugElement.query(By.directive(SortableDirective))
+ sortable.triggerEventHandler('click')
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+})
import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
-import { EditDialogComponent } from '../../common/edit-dialog/edit-dialog.component'
+import {
+ EditDialogComponent,
+ EditDialogMode,
+} from '../../common/edit-dialog/edit-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
export interface ManagementListColumn {
var activeModal = this.modalService.open(this.editDialogComponent, {
backdrop: 'static',
})
- activeModal.componentInstance.dialogMode = 'create'
+ activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
backdrop: 'static',
})
activeModal.componentInstance.object = object
- activeModal.componentInstance.dialogMode = 'edit'
+ activeModal.componentInstance.dialogMode = EditDialogMode.EDIT
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
})
}
- getDeleteMessage(object: T) {
- return $localize`Do you really want to delete the ${this.typeName}?`
- }
+ abstract getDeleteMessage(object: T)
filterDocuments(object: ObjectWithId) {
this.documentListViewService.quickFilter([
--- /dev/null
+import { ViewportScroller, DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import {
+ ComponentFixture,
+ TestBed,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { By } from '@angular/platform-browser'
+import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbModal,
+ NgbModule,
+ NgbNavLink,
+ NgbModalRef,
+} from '@ng-bootstrap/ng-bootstrap'
+import { of, throwError } from 'rxjs'
+import { routes } from 'src/app/app-routing.module'
+import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
+import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule'
+import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
+import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { GroupService } from 'src/app/services/rest/group.service'
+import { MailAccountService } from 'src/app/services/rest/mail-account.service'
+import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
+import { UserService } from 'src/app/services/rest/user.service'
+import { SettingsService } from 'src/app/services/settings.service'
+import { ToastService, Toast } from 'src/app/services/toast.service'
+import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
+import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
+import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
+import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
+import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
+import { CheckComponent } from '../../common/input/check/check.component'
+import { ColorComponent } from '../../common/input/color/color.component'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { SettingsComponent } from './settings.component'
+
+const savedViews = [
+ { id: 1, name: 'view1' },
+ { id: 2, name: 'view2' },
+]
+const users = [
+ { id: 1, username: 'user1' },
+ { id: 2, username: 'user2' },
+]
+const groups = [
+ { id: 1, name: 'group1' },
+ { id: 2, name: 'group2' },
+]
+const mailAccounts = [
+ { id: 1, name: 'account1' },
+ { id: 2, name: 'account2' },
+]
+const mailRules = [
+ { id: 1, name: 'rule1' },
+ { id: 2, name: 'rule2' },
+]
+
+describe('SettingsComponent', () => {
+ let component: SettingsComponent
+ let fixture: ComponentFixture<SettingsComponent>
+ let modalService: NgbModal
+ let router: Router
+ let settingsService: SettingsService
+ let savedViewService: SavedViewService
+ let activatedRoute: ActivatedRoute
+ let viewportScroller: ViewportScroller
+ let toastService: ToastService
+ let userService: UserService
+ let groupService: GroupService
+ let mailAccountService: MailAccountService
+ let mailRuleService: MailRuleService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ SettingsComponent,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ CustomDatePipe,
+ ConfirmDialogComponent,
+ CheckComponent,
+ ColorComponent,
+ ],
+ providers: [
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ CustomDatePipe,
+ DatePipe,
+ PermissionsGuard,
+ ],
+ imports: [
+ NgbModule,
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ }).compileComponents()
+
+ modalService = TestBed.inject(NgbModal)
+ router = TestBed.inject(Router)
+ activatedRoute = TestBed.inject(ActivatedRoute)
+ viewportScroller = TestBed.inject(ViewportScroller)
+ toastService = TestBed.inject(ToastService)
+ settingsService = TestBed.inject(SettingsService)
+ userService = TestBed.inject(UserService)
+ jest.spyOn(userService, 'listAll').mockReturnValue(
+ of({
+ all: users.map((u) => u.id),
+ count: users.length,
+ results: users.concat([]),
+ })
+ )
+ groupService = TestBed.inject(GroupService)
+ jest.spyOn(groupService, 'listAll').mockReturnValue(
+ of({
+ all: groups.map((g) => g.id),
+ count: groups.length,
+ results: groups.concat([]),
+ })
+ )
+ savedViewService = TestBed.inject(SavedViewService)
+ jest.spyOn(savedViewService, 'listAll').mockReturnValue(
+ of({
+ all: savedViews.map((v) => v.id),
+ count: savedViews.length,
+ results: (savedViews as PaperlessSavedView[]).concat([]),
+ })
+ )
+ mailAccountService = TestBed.inject(MailAccountService)
+ jest.spyOn(mailAccountService, 'listAll').mockReturnValue(
+ of({
+ all: mailAccounts.map((a) => a.id),
+ count: mailAccounts.length,
+ results: (mailAccounts as PaperlessMailAccount[]).concat([]),
+ })
+ )
+ mailRuleService = TestBed.inject(MailRuleService)
+ jest.spyOn(mailRuleService, 'listAll').mockReturnValue(
+ of({
+ all: mailRules.map((r) => r.id),
+ count: mailRules.length,
+ results: (mailRules as PaperlessMailRule[]).concat([]),
+ })
+ )
+
+ fixture = TestBed.createComponent(SettingsComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
+ const navigateSpy = jest.spyOn(router, 'navigate')
+ const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
+ tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
+ tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(navigateSpy).toHaveBeenCalledWith(['settings', 'savedviews'])
+ tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(navigateSpy).toHaveBeenCalledWith(['settings', 'mail'])
+ tabButtons[4].nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(navigateSpy).toHaveBeenCalledWith(['settings', 'usersgroups'])
+
+ const initSpy = jest.spyOn(component, 'initialize')
+ component.isDirty = true // mock dirty
+ navigateSpy.mockResolvedValueOnce(false) // nav rejected cause dirty
+ tabButtons[0].nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(navigateSpy).toHaveBeenCalledWith(['settings', 'general'])
+ expect(initSpy).not.toHaveBeenCalled()
+
+ navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty
+ tabButtons[1].nativeElement.dispatchEvent(new MouseEvent('click'))
+ expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
+ expect(initSpy).toHaveBeenCalled()
+ })
+
+ it('should support direct link to tab by URL, scroll if needed', () => {
+ jest
+ .spyOn(activatedRoute, 'paramMap', 'get')
+ .mockReturnValue(of(convertToParamMap({ section: 'mail' })))
+ activatedRoute.snapshot.fragment = '#mail'
+ const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
+ component.ngOnInit()
+ expect(component.activeNavID).toEqual(4) // Mail
+ component.ngAfterViewInit()
+ expect(scrollSpy).toHaveBeenCalledWith('#mail')
+ })
+
+ it('should lazy load tab data', () => {
+ const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
+
+ expect(component.savedViews).toBeUndefined()
+ tabButtons[2].nativeElement.dispatchEvent(
+ new MouseEvent('mouseover', { bubbles: true })
+ )
+ expect(component.savedViews).not.toBeUndefined()
+
+ expect(component.mailAccounts).toBeUndefined()
+ tabButtons[3].nativeElement.dispatchEvent(
+ new MouseEvent('mouseover', { bubbles: true })
+ )
+ expect(component.mailAccounts).not.toBeUndefined()
+
+ expect(component.users).toBeUndefined()
+ tabButtons[4].nativeElement.dispatchEvent(
+ new MouseEvent('mouseover', { bubbles: true })
+ )
+ expect(component.users).not.toBeUndefined()
+ })
+
+ it('should support save saved views, show error', () => {
+ component.maybeInitializeTab(3) // SavedViews
+
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
+
+ // saved views error first
+ savedViewPatchSpy.mockReturnValueOnce(
+ throwError(() => new Error('unable to save saved views'))
+ )
+ component.saveSettings()
+ expect(toastErrorSpy).toHaveBeenCalled()
+ expect(savedViewPatchSpy).toHaveBeenCalled()
+ toastSpy.mockClear()
+ toastErrorSpy.mockClear()
+ savedViewPatchSpy.mockClear()
+
+ // succeed saved views
+ savedViewPatchSpy.mockReturnValueOnce(
+ of(savedViews as PaperlessSavedView[])
+ )
+ component.saveSettings()
+ expect(toastErrorSpy).not.toHaveBeenCalled()
+ expect(savedViewPatchSpy).toHaveBeenCalled()
+ })
+
+ it('should support save local settings updating appearance settings and calling API, show error', () => {
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastSpy = jest.spyOn(toastService, 'show')
+ const storeSpy = jest.spyOn(settingsService, 'storeSettings')
+ const appearanceSettingsSpy = jest.spyOn(
+ settingsService,
+ 'updateAppearanceSettings'
+ )
+ const setSpy = jest.spyOn(settingsService, 'set')
+
+ // error first
+ storeSpy.mockReturnValueOnce(
+ throwError(() => new Error('unable to save settings'))
+ )
+ component.saveSettings()
+ expect(toastErrorSpy).toHaveBeenCalled()
+ expect(storeSpy).toHaveBeenCalled()
+ expect(appearanceSettingsSpy).not.toHaveBeenCalled()
+ expect(setSpy).toHaveBeenCalledTimes(19)
+
+ // succeed
+ storeSpy.mockReturnValueOnce(of(true))
+ component.saveSettings()
+ expect(toastSpy).toHaveBeenCalled()
+ expect(appearanceSettingsSpy).toHaveBeenCalled()
+ })
+
+ it('should offer reload if settings changes require', () => {
+ let toast: Toast
+ toastService.getToasts().subscribe((t) => (toast = t[0]))
+ component.initialize(true) // reset
+ component.store.getValue()['displayLanguage'] = 'en-US'
+ component.store.getValue()['updateCheckingEnabled'] = false
+ component.settingsForm.value.displayLanguage = 'en-GB'
+ component.settingsForm.value.updateCheckingEnabled = true
+ jest.spyOn(settingsService, 'storeSettings').mockReturnValueOnce(of(true))
+ component.saveSettings()
+ expect(toast.actionName).toEqual('Reload now')
+ })
+
+ it('should allow setting theme color, visually apply change immediately but not save', () => {
+ const appearanceSpy = jest.spyOn(
+ settingsService,
+ 'updateAppearanceSettings'
+ )
+ const colorInput = fixture.debugElement.query(By.directive(ColorComponent))
+ colorInput.query(By.css('input')).nativeElement.value = '#ff0000'
+ colorInput
+ .query(By.css('input'))
+ .nativeElement.dispatchEvent(new Event('change'))
+ fixture.detectChanges()
+ expect(appearanceSpy).toHaveBeenCalled()
+ expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('')
+ component.clearThemeColor()
+ })
+
+ it('should support delete saved view', () => {
+ component.maybeInitializeTab(3) // SavedViews
+ const toastSpy = jest.spyOn(toastService, 'showInfo')
+ const deleteSpy = jest.spyOn(savedViewService, 'delete')
+ deleteSpy.mockReturnValue(of(true))
+ component.deleteSavedView(savedViews[0] as PaperlessSavedView)
+ expect(deleteSpy).toHaveBeenCalled()
+ expect(toastSpy).toHaveBeenCalledWith(
+ `Saved view "${savedViews[0].name}" deleted.`
+ )
+ })
+
+ it('should support edit / create user, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.editUser(users[0])
+ const editDialog = modal.componentInstance as UserEditDialogComponent
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ editDialog.failed.emit()
+ expect(toastErrorSpy).toBeCalled()
+ settingsService.currentUser = users[1] // simulate logged in as different user
+ editDialog.succeeded.emit(users[0])
+ expect(toastInfoSpy).toHaveBeenCalledWith(
+ `Saved user "${users[0].username}".`
+ )
+ })
+
+ it('should support delete user, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.deleteUser(users[0])
+ const deleteDialog = modal.componentInstance as ConfirmDialogComponent
+ const deleteSpy = jest.spyOn(userService, 'delete')
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ const listAllSpy = jest.spyOn(userService, 'listAll')
+ deleteSpy.mockReturnValueOnce(
+ throwError(() => new Error('error deleting user'))
+ )
+ deleteDialog.confirm()
+ expect(toastErrorSpy).toBeCalled()
+ deleteSpy.mockReturnValueOnce(of(true))
+ deleteDialog.confirm()
+ expect(listAllSpy).toHaveBeenCalled()
+ expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user')
+ })
+
+ it('should logout current user if password changed, after delay', fakeAsync(() => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.editUser(users[0])
+ const editDialog = modal.componentInstance as UserEditDialogComponent
+ editDialog.passwordIsSet = true
+ settingsService.currentUser = users[0] // simulate logged in as same user
+ editDialog.succeeded.emit(users[0])
+ fixture.detectChanges()
+ Object.defineProperty(window, 'location', {
+ value: {
+ href: 'http://localhost/',
+ },
+ writable: true, // possibility to override
+ })
+ tick(2600)
+ expect(window.location.href).toContain('logout')
+ }))
+
+ it('should support edit / create group, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.editGroup(groups[0])
+ const editDialog = modal.componentInstance as GroupEditDialogComponent
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ editDialog.failed.emit()
+ expect(toastErrorSpy).toBeCalled()
+ editDialog.succeeded.emit(groups[0])
+ expect(toastInfoSpy).toHaveBeenCalledWith(
+ `Saved group "${groups[0].name}".`
+ )
+ })
+
+ it('should support delete group, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.deleteGroup(users[0])
+ const deleteDialog = modal.componentInstance as ConfirmDialogComponent
+ const deleteSpy = jest.spyOn(groupService, 'delete')
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ const listAllSpy = jest.spyOn(groupService, 'listAll')
+ deleteSpy.mockReturnValueOnce(
+ throwError(() => new Error('error deleting group'))
+ )
+ deleteDialog.confirm()
+ expect(toastErrorSpy).toBeCalled()
+ deleteSpy.mockReturnValueOnce(of(true))
+ deleteDialog.confirm()
+ expect(listAllSpy).toHaveBeenCalled()
+ expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group')
+ })
+
+ it('should get group name', () => {
+ component.maybeInitializeTab(5) // UsersGroups
+ expect(component.getGroupName(1)).toEqual(groups[0].name)
+ expect(component.getGroupName(11)).toEqual('')
+ })
+
+ it('should support edit / create mail account, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.editMailAccount(mailAccounts[0] as PaperlessMailAccount)
+ const editDialog = modal.componentInstance as MailAccountEditDialogComponent
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ editDialog.failed.emit()
+ expect(toastErrorSpy).toBeCalled()
+ editDialog.succeeded.emit(mailAccounts[0])
+ expect(toastInfoSpy).toHaveBeenCalledWith(
+ `Saved account "${mailAccounts[0].name}".`
+ )
+ })
+
+ it('should support delete mail account, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.deleteMailAccount(mailAccounts[0] as PaperlessMailAccount)
+ const deleteDialog = modal.componentInstance as ConfirmDialogComponent
+ const deleteSpy = jest.spyOn(mailAccountService, 'delete')
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ const listAllSpy = jest.spyOn(mailAccountService, 'listAll')
+ deleteSpy.mockReturnValueOnce(
+ throwError(() => new Error('error deleting mail account'))
+ )
+ deleteDialog.confirm()
+ expect(toastErrorSpy).toBeCalled()
+ deleteSpy.mockReturnValueOnce(of(true))
+ deleteDialog.confirm()
+ expect(listAllSpy).toHaveBeenCalled()
+ expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account')
+ })
+
+ it('should support edit / create mail rule, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.editMailRule(mailRules[0] as PaperlessMailRule)
+ const editDialog = modal.componentInstance as MailRuleEditDialogComponent
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ editDialog.failed.emit()
+ expect(toastErrorSpy).toBeCalled()
+ editDialog.succeeded.emit(mailRules[0])
+ expect(toastInfoSpy).toHaveBeenCalledWith(
+ `Saved rule "${mailRules[0].name}".`
+ )
+ })
+
+ it('should support delete mail rule, show error if needed', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
+ component.deleteMailRule(mailRules[0] as PaperlessMailRule)
+ const deleteDialog = modal.componentInstance as ConfirmDialogComponent
+ const deleteSpy = jest.spyOn(mailRuleService, 'delete')
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
+ deleteSpy.mockReturnValueOnce(
+ throwError(() => new Error('error deleting mail rule'))
+ )
+ deleteDialog.confirm()
+ expect(toastErrorSpy).toBeCalled()
+ deleteSpy.mockReturnValueOnce(of(true))
+ deleteDialog.confirm()
+ expect(listAllSpy).toHaveBeenCalled()
+ expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule')
+ })
+})
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
+import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
enum SettingsNavIDs {
General = 1,
onNavChange(navChangeEvent: NgbNavChangeEvent) {
this.maybeInitializeTab(navChangeEvent.nextId)
- const [foundNavIDkey, foundNavIDValue] = Object.entries(
- SettingsNavIDs
- ).find(([navIDkey, navIDValue]) => navIDValue == navChangeEvent.nextId)
+ const [foundNavIDkey] = Object.entries(SettingsNavIDs).find(
+ ([, navIDValue]) => navIDValue == navChangeEvent.nextId
+ )
if (foundNavIDkey)
// if its dirty we need to wait for confirmation
this.router
delay: 5000,
}
if (reloadRequired) {
- ;(savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`),
- (savedToast.actionName = $localize`Reload now`)
+ savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`
+ savedToast.actionName = $localize`Reload now`
savedToast.action = () => {
location.reload()
}
backdrop: 'static',
size: 'xl',
})
- modal.componentInstance.dialogMode = user ? 'edit' : 'create'
+ modal.componentInstance.dialogMode = user
+ ? EditDialogMode.EDIT
+ : EditDialogMode.CREATE
modal.componentInstance.object = user
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
backdrop: 'static',
size: 'lg',
})
- modal.componentInstance.dialogMode = group ? 'edit' : 'create'
+ modal.componentInstance.dialogMode = group
+ ? EditDialogMode.EDIT
+ : EditDialogMode.CREATE
modal.componentInstance.object = group
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
backdrop: 'static',
size: 'xl',
})
- modal.componentInstance.dialogMode = account ? 'edit' : 'create'
+ modal.componentInstance.dialogMode = account
+ ? EditDialogMode.EDIT
+ : EditDialogMode.CREATE
modal.componentInstance.object = account
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
backdrop: 'static',
size: 'xl',
})
- modal.componentInstance.dialogMode = rule ? 'edit' : 'create'
+ modal.componentInstance.dialogMode = rule
+ ? EditDialogMode.EDIT
+ : EditDialogMode.CREATE
modal.componentInstance.object = rule
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
+import { of } from 'rxjs'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SortableDirective } from 'src/app/directives/sortable.directive'
+import { StoragePathService } from 'src/app/services/rest/storage-path.service'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { StoragePathListComponent } from './storage-path-list.component'
+
+describe('StoragePathListComponent', () => {
+ let component: StoragePathListComponent
+ let fixture: ComponentFixture<StoragePathListComponent>
+ let storagePathService: StoragePathService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ StoragePathListComponent,
+ SortableDirective,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ ],
+ providers: [DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ NgbPaginationModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ }).compileComponents()
+
+ storagePathService = TestBed.inject(StoragePathService)
+ jest.spyOn(storagePathService, 'listFiltered').mockReturnValue(
+ of({
+ count: 3,
+ all: [1, 2, 3],
+ results: [
+ {
+ id: 1,
+ name: 'StoragePath1',
+ },
+ {
+ id: 2,
+ name: 'StoragePath2',
+ },
+ {
+ id: 3,
+ name: 'StoragePath3',
+ },
+ ],
+ })
+ )
+ fixture = TestBed.createComponent(StoragePathListComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ // Tests are included in management-list.compontent.spec.ts
+
+ it('should use correct delete message', () => {
+ expect(component.getDeleteMessage({ id: 1, name: 'StoragePath1' })).toEqual(
+ 'Do you really want to delete the storage path "StoragePath1"?'
+ )
+ })
+})
--- /dev/null
+import { DatePipe } from '@angular/common'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
+import { of } from 'rxjs'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { SortableDirective } from 'src/app/directives/sortable.directive'
+import { TagService } from 'src/app/services/rest/tag.service'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { TagListComponent } from './tag-list.component'
+import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
+
+describe('TagListComponent', () => {
+ let component: TagListComponent
+ let fixture: ComponentFixture<TagListComponent>
+ let tagService: TagService
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TagListComponent,
+ SortableDirective,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ SafeHtmlPipe,
+ ],
+ providers: [DatePipe],
+ imports: [
+ HttpClientTestingModule,
+ NgbPaginationModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
+ }).compileComponents()
+
+ tagService = TestBed.inject(TagService)
+ jest.spyOn(tagService, 'listFiltered').mockReturnValue(
+ of({
+ count: 3,
+ all: [1, 2, 3],
+ results: [
+ {
+ id: 1,
+ name: 'Tag1',
+ },
+ {
+ id: 2,
+ name: 'Tag2',
+ },
+ {
+ id: 3,
+ name: 'Tag3',
+ },
+ ],
+ })
+ )
+ fixture = TestBed.createComponent(TagListComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ // Tests are included in management-list.compontent.spec.ts
+
+ it('should use correct delete message', () => {
+ expect(component.getDeleteMessage({ id: 1, name: 'Tag1' })).toEqual(
+ 'Do you really want to delete the tag "Tag1"?'
+ )
+ })
+})
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed">
- <a ngbNavLink i18n>Failed <span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-1">{{tasksService.failedFileTasks.length}}</span></a>
+ <a ngbNavLink i18n>Failed<span *ngIf="tasksService.failedFileTasks.length > 0" class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
- <a ngbNavLink i18n>Complete <span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.completedFileTasks.length}}</span></a>
+ <a ngbNavLink i18n>Complete<span *ngIf="tasksService.completedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span></a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
- <a ngbNavLink i18n>Started <span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.startedFileTasks.length}}</span></a>
+ <a ngbNavLink i18n>Started<span *ngIf="tasksService.startedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span></a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
- <a ngbNavLink i18n>Queued <span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-1">{{tasksService.queuedFileTasks.length}}</span></a>
+ <a ngbNavLink i18n>Queued<span *ngIf="tasksService.queuedFileTasks.length > 0" class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span></a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template>
--- /dev/null
+import { DatePipe } from '@angular/common'
+import {
+ HttpTestingController,
+ HttpClientTestingModule,
+} from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { By } from '@angular/platform-browser'
+import { Router } from '@angular/router'
+import { RouterTestingModule } from '@angular/router/testing'
+import {
+ NgbModal,
+ NgbModule,
+ NgbNavItem,
+ NgbModalRef,
+} from '@ng-bootstrap/ng-bootstrap'
+import { routes } from 'src/app/app-routing.module'
+import {
+ PaperlessTask,
+ PaperlessTaskType,
+ PaperlessTaskStatus,
+} from 'src/app/data/paperless-task'
+import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
+import { PermissionsService } from 'src/app/services/permissions.service'
+import { TasksService } from 'src/app/services/tasks.service'
+import { environment } from 'src/environments/environment'
+import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { TasksComponent } from './tasks.component'
+import { PermissionsGuard } from 'src/app/guards/permissions.guard'
+
+const tasks: PaperlessTask[] = [
+ {
+ id: 467,
+ task_id: '11ca1a5b-9f81-442c-b2c8-7e4ae53657f1',
+ task_file_name: 'test.pdf',
+ date_created: new Date('2023-03-01T10:26:03.093116Z'),
+ date_done: new Date('2023-03-01T10:26:07.223048Z'),
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Failed,
+ result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
+ acknowledged: false,
+ related_document: null,
+ },
+ {
+ id: 466,
+ task_id: '10ca1a5b-3c08-442c-b2c8-7e4ae53657f1',
+ task_file_name: '191092.pdf',
+ date_created: new Date('2023-03-01T09:26:03.093116Z'),
+ date_done: new Date('2023-03-01T09:26:07.223048Z'),
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Failed,
+ result:
+ '191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
+ acknowledged: false,
+ related_document: null,
+ },
+ {
+ id: 465,
+ task_id: '3612d477-bb04-44e3-985b-ac580dd496d8',
+ task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
+ date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
+ date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Pending,
+ result: null,
+ acknowledged: false,
+ related_document: null,
+ },
+ {
+ id: 464,
+ task_id: '2eac4716-2aa6-4dcd-9953-264e11656d7e',
+ task_file_name: 'paperless-mail-l4dkg8ir',
+ date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
+ date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Complete,
+ result: 'Success. New document id 422 created',
+ acknowledged: false,
+ related_document: 422,
+ },
+ {
+ id: 463,
+ task_id: '28125528-1575-4d6b-99e6-168906e8fa5c',
+ task_file_name: 'onlinePaymentSummary.pdf',
+ date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
+ date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Complete,
+ result: 'Success. New document id 421 created',
+ acknowledged: false,
+ related_document: 421,
+ },
+ {
+ id: 462,
+ task_id: 'a5b9ca47-0c8e-490f-a04c-6db5d5fc09e5',
+ task_file_name: 'paperless-mail-_rrpmqk6',
+ date_created: new Date('2023-06-07T02:54:35.694916Z'),
+ date_done: null,
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Started,
+ result: null,
+ acknowledged: false,
+ related_document: null,
+ },
+]
+
+describe('TasksComponent', () => {
+ let component: TasksComponent
+ let fixture: ComponentFixture<TasksComponent>
+ let tasksService: TasksService
+ let modalService: NgbModal
+ let router: Router
+ let httpTestingController: HttpTestingController
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [
+ TasksComponent,
+ PageHeaderComponent,
+ IfPermissionsDirective,
+ CustomDatePipe,
+ ConfirmDialogComponent,
+ ],
+ providers: [
+ {
+ provide: PermissionsService,
+ useValue: {
+ currentUserCan: () => true,
+ },
+ },
+ CustomDatePipe,
+ DatePipe,
+ PermissionsGuard,
+ ],
+ imports: [
+ NgbModule,
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ ],
+ }).compileComponents()
+
+ tasksService = TestBed.inject(TasksService)
+ httpTestingController = TestBed.inject(HttpTestingController)
+ modalService = TestBed.inject(NgbModal)
+ router = TestBed.inject(Router)
+ fixture = TestBed.createComponent(TasksComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}tasks/`)
+ .flush(tasks)
+ })
+
+ it('should display file tasks in 4 tabs by status', () => {
+ const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavItem))
+
+ let currentTasksLength = tasks.filter(
+ (t) => t.status === PaperlessTaskStatus.Failed
+ ).length
+ component.activeTab = 'failed'
+ fixture.detectChanges()
+ expect(tabButtons[0].nativeElement.textContent).toEqual(
+ `Failed${currentTasksLength}`
+ )
+ expect(
+ fixture.debugElement.queryAll(By.css('input[type="checkbox"]'))
+ ).toHaveLength(currentTasksLength + 1)
+
+ currentTasksLength = tasks.filter(
+ (t) => t.status === PaperlessTaskStatus.Complete
+ ).length
+ component.activeTab = 'completed'
+ fixture.detectChanges()
+ expect(tabButtons[1].nativeElement.textContent).toEqual(
+ `Complete${currentTasksLength}`
+ )
+
+ currentTasksLength = tasks.filter(
+ (t) => t.status === PaperlessTaskStatus.Started
+ ).length
+ component.activeTab = 'started'
+ fixture.detectChanges()
+ expect(tabButtons[2].nativeElement.textContent).toEqual(
+ `Started${currentTasksLength}`
+ )
+
+ currentTasksLength = tasks.filter(
+ (t) => t.status === PaperlessTaskStatus.Pending
+ ).length
+ component.activeTab = 'queued'
+ fixture.detectChanges()
+ expect(tabButtons[3].nativeElement.textContent).toEqual(
+ `Queued${currentTasksLength}`
+ )
+ })
+
+ it('should to go page 1 between tab switch', () => {
+ component.page = 10
+ component.duringTabChange(2)
+ expect(component.page).toEqual(1)
+ })
+
+ it('should support expanding / collapsing one task at a time', () => {
+ component.expandTask(tasks[0])
+ expect(component.expandedTask).toEqual(tasks[0].id)
+ component.expandTask(tasks[1])
+ expect(component.expandedTask).toEqual(tasks[1].id)
+ component.expandTask(tasks[1])
+ expect(component.expandedTask).toBeUndefined()
+ })
+
+ it('should support dismiss single task', () => {
+ const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
+ component.dismissTask(tasks[0])
+ expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id]))
+ })
+
+ it('should support dismiss specific checked tasks', () => {
+ component.toggleSelected(tasks[0])
+ component.toggleSelected(tasks[1])
+ component.toggleSelected(tasks[3])
+ component.toggleSelected(tasks[3]) // uncheck, for coverage
+ const selected = new Set([tasks[0].id, tasks[1].id])
+ expect(component.selectedTasks).toEqual(selected)
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
+ fixture.detectChanges()
+ component.dismissTasks()
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirmClicked.emit()
+ expect(dismissSpy).toHaveBeenCalledWith(selected)
+ })
+
+ it('should support dismiss all tasks', () => {
+ let modal: NgbModalRef
+ modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
+ const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
+ component.dismissTasks()
+ expect(modal).not.toBeUndefined()
+ modal.componentInstance.confirmClicked.emit()
+ expect(dismissSpy).toHaveBeenCalledWith(new Set(tasks.map((t) => t.id)))
+ })
+
+ it('should support toggle all tasks', () => {
+ const toggleCheck = fixture.debugElement.query(
+ By.css('input[type=checkbox]')
+ )
+ toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+ expect(component.selectedTasks).toEqual(
+ new Set(
+ tasks
+ .filter((t) => t.status === PaperlessTaskStatus.Failed)
+ .map((t) => t.id)
+ )
+ )
+ toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+ expect(component.selectedTasks).toEqual(new Set())
+ })
+
+ it('should support dismiss and open a document', () => {
+ const routerSpy = jest.spyOn(router, 'navigate')
+ component.dismissAndGo(tasks[3])
+ expect(routerSpy).toHaveBeenCalledWith([
+ 'documents',
+ tasks[3].related_document,
+ ])
+ })
+})
import { Component, OnInit, OnDestroy } from '@angular/core'
import { Router } from '@angular/router'
-import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, first } from 'rxjs'
import { PaperlessTask } from 'src/app/data/paperless-task'
import { TasksService } from 'src/app/services/tasks.service'
}
dismissTasks(task: PaperlessTask = undefined) {
- let tasks = task ? new Set([task.id]) : this.selectedTasks
- if (!task && this.selectedTasks.size == 0)
+ let tasks = task ? new Set([task.id]) : new Set(this.selectedTasks.values())
+ if (!task && tasks.size == 0)
tasks = new Set(this.tasksService.allFileTasks.map((t) => t.id))
if (tasks.size > 1) {
let modal = this.modalService.open(ConfirmDialogComponent, {
}
get currentTasks(): PaperlessTask[] {
- let tasks: PaperlessTask[]
+ let tasks: PaperlessTask[] = []
switch (this.activeTab) {
case 'queued':
tasks = this.tasksService.queuedFileTasks
case 'failed':
tasks = this.tasksService.failedFileTasks
break
- default:
- break
}
return tasks
}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { NotFoundComponent } from './not-found.component'
+
+describe('NotFoundComponent', () => {
+ let component: NotFoundComponent
+ let fixture: ComponentFixture<NotFoundComponent>
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [NotFoundComponent],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(NotFoundComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should create component', () => {
+ expect(component).toBeTruthy()
+ expect(fixture.nativeElement.textContent).toContain('404 Not Found')
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { ComponentWithPermissions } from './with-permissions.component'
+
+describe('ComponentWithPermissions', () => {
+ let component: ComponentWithPermissions
+
+ beforeEach(async () => {
+ TestBed.configureTestingModule({
+ declarations: [ComponentWithPermissions],
+ })
+ })
+
+ it('should include permissions classes', () => {
+ component = new ComponentWithPermissions()
+ expect(component.PermissionAction).not.toBeNull()
+ expect(component.PermissionType).not.toBeNull()
+ })
+})
-import {
- FILTER_FULLTEXT_MORELIKE,
- FILTER_FULLTEXT_QUERY,
-} from './filter-rule-type'
-
-export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
- if (filterRules) {
- let newRules: FilterRule[] = []
- for (let rule of filterRules) {
- newRules.push({ rule_type: rule.rule_type, value: rule.value })
- }
- return newRules
- } else {
- return null
- }
-}
-
-export function isFullTextFilterRule(filterRules: FilterRule[]): boolean {
- return (
- filterRules.find(
- (r) =>
- r.rule_type == FILTER_FULLTEXT_QUERY ||
- r.rule_type == FILTER_FULLTEXT_MORELIKE
- ) != null
- )
-}
-
-export function filterRulesDiffer(
- filterRulesA: FilterRule[],
- filterRulesB: FilterRule[]
-): boolean {
- let differ = false
- if (filterRulesA.length != filterRulesB.length) {
- differ = true
- } else {
- differ = filterRulesA.some((rule) => {
- return (
- filterRulesB.find(
- (fri) => fri.rule_type == rule.rule_type && fri.value == rule.value
- ) == undefined
- )
- })
- }
- return differ
-}
-
export interface FilterRule {
rule_type: number
value: string
--- /dev/null
+import { Component } from '@angular/core'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { IfObjectPermissionsDirective } from './if-object-permissions.directive'
+import { PermissionsService } from '../services/permissions.service'
+
+@Component({
+ template: `
+ <div>
+ <button
+ *appIfObjectPermissions="{
+ object: { id: 2, owner: user1 },
+ action: 'view'
+ }"
+ >
+ Some Text
+ </button>
+ </div>
+ `,
+})
+class TestComponent {}
+
+describe('IfObjectPermissionsDirective', () => {
+ let fixture: ComponentFixture<TestComponent>
+ let permissionsService: PermissionsService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [IfObjectPermissionsDirective, TestComponent],
+ providers: [PermissionsService],
+ })
+ permissionsService = TestBed.inject(PermissionsService)
+ })
+
+ it('should create element if user has object permissions', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockImplementation(() => {
+ return true
+ })
+
+ fixture = TestBed.createComponent(TestComponent)
+
+ fixture.detectChanges()
+
+ const rootEl = (fixture.nativeElement as HTMLDivElement).children[0]
+ expect(rootEl.querySelectorAll('button').length).toEqual(1)
+ })
+
+ it('should not create element if user does not have object permissions', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserHasObjectPermissions')
+ .mockImplementation(() => {
+ return false
+ })
+
+ fixture = TestBed.createComponent(TestComponent)
+
+ fixture.detectChanges()
+
+ const rootEl = (fixture.nativeElement as HTMLDivElement).children[0]
+ expect(rootEl.querySelectorAll('button').length).toEqual(0)
+ })
+})
--- /dev/null
+import { Component } from '@angular/core'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { IfOwnerDirective } from './if-owner.directive'
+import { PermissionsService } from '../services/permissions.service'
+
+@Component({
+ template: `
+ <div>
+ <button *appIfOwner="{ id: 2, owner: user1 }">Some Text</button>
+ </div>
+ `,
+})
+class TestComponent {}
+
+describe('IfOwnerDirective', () => {
+ let fixture: ComponentFixture<TestComponent>
+ let permissionsService: PermissionsService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [IfOwnerDirective, TestComponent],
+ providers: [PermissionsService],
+ })
+ permissionsService = TestBed.inject(PermissionsService)
+ })
+
+ it('should create element if user owns object', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserOwnsObject')
+ .mockImplementation(() => {
+ return true
+ })
+
+ fixture = TestBed.createComponent(TestComponent)
+
+ fixture.detectChanges()
+
+ const rootEl = (fixture.nativeElement as HTMLDivElement).children[0]
+ expect(rootEl.querySelectorAll('button').length).toEqual(1)
+ })
+
+ it('should not create element if user does not own object', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserOwnsObject')
+ .mockImplementation(() => {
+ return false
+ })
+
+ fixture = TestBed.createComponent(TestComponent)
+
+ fixture.detectChanges()
+
+ const rootEl = (fixture.nativeElement as HTMLDivElement).children[0]
+ expect(rootEl.querySelectorAll('button').length).toEqual(0)
+ })
+})
--- /dev/null
+import { Component } from '@angular/core'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { IfPermissionsDirective } from './if-permissions.directive'
+import { PermissionsService } from '../services/permissions.service'
+
+@Component({
+ template: `
+ <div>
+ <button *appIfPermissions="{ action: 'add', type: '%s_user' }">
+ Some Text
+ </button>
+ </div>
+ `,
+})
+class TestComponent {}
+
+describe('IfPermissionsDirective', () => {
+ let fixture: ComponentFixture<TestComponent>
+ let permissionsService: PermissionsService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [IfPermissionsDirective, TestComponent],
+ providers: [PermissionsService],
+ })
+ permissionsService = TestBed.inject(PermissionsService)
+ })
+
+ it('should create element if user has permissions', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockImplementation(() => {
+ return true
+ })
+
+ fixture = TestBed.createComponent(TestComponent)
+
+ fixture.detectChanges()
+
+ const rootEl = (fixture.nativeElement as HTMLDivElement).children[0]
+ expect(rootEl.querySelectorAll('button').length).toEqual(1)
+ })
+
+ it('should not create element if user has does not have permissions', () => {
+ jest.spyOn(permissionsService, 'currentUserCan').mockImplementation(() => {
+ return false
+ })
+
+ fixture = TestBed.createComponent(TestComponent)
+
+ fixture.detectChanges()
+
+ const rootEl = (fixture.nativeElement as HTMLDivElement).children[0]
+ expect(rootEl.querySelectorAll('button').length).toEqual(0)
+ })
+})
--- /dev/null
+import { Component, DebugElement } from '@angular/core'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { SortEvent, SortableDirective } from './sortable.directive'
+import { By } from '@angular/platform-browser'
+
+@Component({
+ template: `
+ <table class="table">
+ <thead>
+ <th></th>
+ <th class="d-none d-lg-table-cell" appSortable="archive_serial_number">
+ ASN
+ </th>
+ <th class="d-none d-md-table-cell" appSortable="correspondent__name">
+ Correspondent
+ </th>
+ </thead>
+ <tbody>
+ <tr>
+ <td></td>
+ <td></td>
+ <td></td>
+ </tr>
+ </tbody>
+ </table>
+ `,
+})
+class TestComponent {}
+
+describe('SortableDirective', () => {
+ let fixture: ComponentFixture<TestComponent>
+ let directive: SortableDirective
+ let des: DebugElement[] // the elements w/ the directive
+
+ beforeEach(() => {
+ fixture = TestBed.configureTestingModule({
+ declarations: [SortableDirective, TestComponent],
+ }).createComponent(TestComponent)
+
+ fixture.detectChanges() // initial binding
+
+ // all elements with an attached SortableDirective
+ des = fixture.debugElement.queryAll(By.directive(SortableDirective))
+
+ directive = des[1].injector.get(SortableDirective)
+ directive.currentSortField = 'correspondent__name'
+ })
+
+ it('should have three 2 sortable elements', () => {
+ expect(des.length).toBe(2)
+ })
+
+ it('should trigger sort on click', () => {
+ const tableCell = des[1].nativeElement as HTMLTableCellElement
+
+ let sortEvent: SortEvent
+ directive.sort.subscribe((event) => {
+ directive.currentSortReverse = event.reverse
+ sortEvent = event
+ })
+
+ expect(directive.currentSortReverse).toBeFalsy()
+
+ tableCell.dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+
+ expect(sortEvent).not.toBeNull()
+ expect(sortEvent.column).toEqual('correspondent__name')
+ expect(sortEvent.reverse).toBeTruthy()
+
+ tableCell.dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+
+ expect(sortEvent.reverse).toBeFalsy()
+ })
+
+ it('should change column to sort when clicked', () => {
+ const tableCell = des[1].nativeElement as HTMLTableCellElement
+
+ let sortEvent: SortEvent
+ directive.sort.subscribe((event) => {
+ directive.currentSortReverse = event.reverse
+ sortEvent = event
+ })
+
+ directive.currentSortField = 'archive_serial_number'
+
+ tableCell.dispatchEvent(new MouseEvent('click'))
+ fixture.detectChanges()
+
+ expect(sortEvent.column).toEqual('correspondent__name')
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from '../app-routing.module'
+import { Component } from '@angular/core'
+import { ComponentCanDeactivate, DirtyDocGuard } from './dirty-doc.guard'
+
+@Component({})
+class GenericDirtyDocComponent implements ComponentCanDeactivate {
+ canDeactivate: () => boolean
+}
+
+describe('DirtyDocGuard', () => {
+ let guard: DirtyDocGuard
+ let component: ComponentCanDeactivate
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [DirtyDocGuard, NgbModal, GenericDirtyDocComponent],
+ imports: [RouterTestingModule.withRoutes(routes), NgbModule],
+ declarations: [GenericDirtyDocComponent],
+ }).compileComponents()
+
+ guard = TestBed.inject(DirtyDocGuard)
+ const fixture = TestBed.createComponent(GenericDirtyDocComponent)
+ component = fixture.componentInstance
+ window.confirm = jest.fn().mockImplementation(() => true)
+
+ fixture.detectChanges()
+ })
+
+ it('should deactivate if component is not dirty', () => {
+ component.canDeactivate = () => true
+ const confirmSpy = jest.spyOn(window, 'confirm')
+ const canDeactivate = guard.canDeactivate(component)
+
+ expect(canDeactivate).toBeTruthy()
+ expect(confirmSpy).not.toHaveBeenCalled()
+ })
+
+ it('should not deactivate if component is dirty', () => {
+ component.canDeactivate = () => false
+ const confirmSpy = jest.spyOn(window, 'confirm')
+ const canDeactivate = guard.canDeactivate(component)
+
+ expect(confirmSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from '../app-routing.module'
+import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
+import { DirtyFormGuard } from './dirty-form.guard'
+import { DirtyComponent } from '@ngneat/dirty-check-forms'
+import { ActivatedRoute } from '@angular/router'
+import { Component } from '@angular/core'
+
+@Component({})
+class GenericDirtyComponent implements DirtyComponent {
+ isDirty$: boolean
+}
+
+describe('DirtyFormGuard', () => {
+ let guard: DirtyFormGuard
+ let component: DirtyComponent
+ let route: ActivatedRoute
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ DirtyFormGuard,
+ NgbModal,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: {},
+ },
+ },
+ GenericDirtyComponent,
+ ],
+ imports: [RouterTestingModule.withRoutes(routes), NgbModule],
+ declarations: [ConfirmDialogComponent, GenericDirtyComponent],
+ }).compileComponents()
+
+ guard = TestBed.inject(DirtyFormGuard)
+ route = TestBed.inject(ActivatedRoute)
+ const fixture = TestBed.createComponent(GenericDirtyComponent)
+ component = fixture.componentInstance
+
+ fixture.detectChanges()
+ })
+
+ it('should deactivate if component is not dirty', () => {
+ component.isDirty$ = false
+ const confirmSpy = jest.spyOn(guard, 'confirmChanges')
+ const canDeactivate = guard.canDeactivate(component, route.snapshot)
+ canDeactivate.subscribe()
+
+ expect(canDeactivate).toBeTruthy()
+ expect(confirmSpy).not.toHaveBeenCalled()
+ })
+
+ it('should offer confirm before deactivate if component is dirty', () => {
+ component.isDirty$ = true
+ const confirmSpy = jest.spyOn(guard, 'confirmChanges')
+ const canDeactivate = guard.canDeactivate(component, route.snapshot)
+ canDeactivate.subscribe()
+
+ expect(canDeactivate).toHaveProperty('source') // Observable
+ expect(confirmSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { DirtySavedViewGuard } from './dirty-saved-view.guard'
+import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
+import { SettingsService } from '../services/settings.service'
+import { DocumentListComponent } from '../components/document-list/document-list.component'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from '../app-routing.module'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
+
+describe('DirtySavedViewGuard', () => {
+ let guard: DirtySavedViewGuard
+ let settingsService: SettingsService
+ let modalService: NgbModal
+ let component: DocumentListComponent
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ DirtySavedViewGuard,
+ SettingsService,
+ NgbModal,
+ DocumentListComponent,
+ ],
+ imports: [
+ RouterTestingModule.withRoutes(routes),
+ HttpClientTestingModule,
+ ],
+ declarations: [ConfirmDialogComponent],
+ })
+
+ settingsService = TestBed.inject(SettingsService)
+ modalService = TestBed.inject(NgbModal)
+ guard = TestBed.inject(DirtySavedViewGuard)
+ const fixture = TestBed.createComponent(DocumentListComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should deactivate if component is not dirty', () => {
+ jest
+ .spyOn(DocumentListComponent.prototype, 'savedViewIsModified', 'get')
+ .mockImplementation(() => {
+ return false
+ })
+ const canDeactivate = guard.canDeactivate(component)
+
+ expect(canDeactivate).toBeTruthy()
+ })
+
+ it('should not warn on deactivate if component is dirty & setting disabled', () => {
+ jest
+ .spyOn(DocumentListComponent.prototype, 'savedViewIsModified', 'get')
+ .mockImplementation(() => {
+ return true
+ })
+
+ jest.spyOn(settingsService, 'get').mockImplementation(() => {
+ return false
+ })
+
+ const modalSpy = jest.spyOn(modalService, 'open')
+
+ const canDeactivate = guard.canDeactivate(component)
+
+ expect(canDeactivate).toBeTruthy()
+ expect(modalSpy).not.toHaveBeenCalled()
+
+ const saveSpy = jest.spyOn(component, 'saveViewConfig')
+ expect(saveSpy).not.toHaveBeenCalled()
+ })
+
+ it('should warn on deactivate if component is dirty & setting enabled', () => {
+ jest
+ .spyOn(DocumentListComponent.prototype, 'savedViewIsModified', 'get')
+ .mockImplementation(() => {
+ return true
+ })
+
+ jest.spyOn(settingsService, 'get').mockImplementation(() => {
+ return true
+ })
+
+ const modalSpy = jest.spyOn(modalService, 'open')
+
+ let modal: NgbModalRef
+
+ modalService.activeInstances.subscribe((ngbmodalRef) => {
+ modal = ngbmodalRef[0]
+ })
+
+ const canDeactivate = guard.canDeactivate(component)
+
+ expect(canDeactivate).toHaveProperty('closed') // returns confirm dialog subject
+ expect(modalSpy).toHaveBeenCalled()
+ expect(modal).not.toBeNull()
+
+ const saveSpy = jest.spyOn(component, 'saveViewConfig')
+ modal.componentInstance.alternativeClicked.emit()
+ expect(saveSpy).toHaveBeenCalled()
+ })
+
+ it('should not save if user proceeds on warn', () => {
+ jest
+ .spyOn(DocumentListComponent.prototype, 'savedViewIsModified', 'get')
+ .mockImplementation(() => {
+ return true
+ })
+
+ jest.spyOn(settingsService, 'get').mockImplementation(() => {
+ return true
+ })
+
+ const modalSpy = jest.spyOn(modalService, 'open')
+
+ let modal: NgbModalRef
+
+ modalService.activeInstances.subscribe((ngbmodalRef) => {
+ modal = ngbmodalRef[0]
+ })
+
+ const canDeactivate = guard.canDeactivate(component)
+
+ expect(canDeactivate).toHaveProperty('closed') // returns confirm dialog subject
+ expect(modalSpy).toHaveBeenCalled()
+ expect(modal).not.toBeNull()
+
+ const saveSpy = jest.spyOn(component, 'saveViewConfig')
+ modal.componentInstance.confirmClicked.emit()
+ expect(saveSpy).not.toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { PermissionsGuard } from './permissions.guard'
+import {
+ PermissionAction,
+ PermissionType,
+ PermissionsService,
+} from '../services/permissions.service'
+import { ActivatedRoute } from '@angular/router'
+import { RouterStateSnapshot } from '@angular/router'
+import { TourService } from 'ngx-ui-tour-ng-bootstrap'
+import { ToastService } from '../services/toast.service'
+import { RouterState } from '@angular/router'
+
+describe('PermissionsGuard', () => {
+ let guard: PermissionsGuard
+ let permissionsService: PermissionsService
+ let route: ActivatedRoute
+ let routerState: RouterState
+ let tourService: TourService
+ let toastService: ToastService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ PermissionsGuard,
+ PermissionsService,
+ {
+ provide: ActivatedRoute,
+ useValue: {
+ snapshot: {
+ data: {
+ requiredPermission: {
+ action: PermissionAction.View,
+ type: PermissionType.Document,
+ },
+ },
+ },
+ },
+ },
+ {
+ provide: RouterState,
+ useValue: {
+ snapshot: {
+ url: '/documents',
+ },
+ },
+ },
+ TourService,
+ ToastService,
+ ],
+ })
+
+ permissionsService = TestBed.inject(PermissionsService)
+ tourService = TestBed.inject(TourService)
+ toastService = TestBed.inject(ToastService)
+ guard = TestBed.inject(PermissionsGuard)
+ route = TestBed.inject(ActivatedRoute)
+ routerState = TestBed.inject(RouterState)
+ })
+
+ it('should activate if user has permissions', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserCan')
+ .mockImplementation((action, type) => {
+ return true
+ })
+
+ const canActivate = guard.canActivate(route.snapshot, routerState.snapshot)
+
+ expect(canActivate).toBeTruthy()
+ })
+
+ it('should not activate if user does not have permissions', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserCan')
+ .mockImplementation((action, type) => {
+ return false
+ })
+
+ const canActivate = guard.canActivate(route.snapshot, routerState.snapshot)
+
+ expect(canActivate).toHaveProperty('root') // returns UrlTree
+ })
+
+ it('should not activate if user does not have permissions and tour is running', () => {
+ jest
+ .spyOn(permissionsService, 'currentUserCan')
+ .mockImplementation((action, type) => {
+ return false
+ })
+ jest.spyOn(tourService, 'getStatus').mockImplementation(() => 2)
+
+ const toastSpy = jest.spyOn(toastService, 'showError')
+
+ const canActivate = guard.canActivate(route.snapshot, routerState.snapshot)
+
+ expect(canActivate).toHaveProperty('root') // returns UrlTree
+ expect(toastSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { ApiVersionInterceptor } from './api-version.interceptor'
+import { HttpEvent, HttpRequest } from '@angular/common/http'
+import { of } from 'rxjs'
+import { environment } from 'src/environments/environment'
+
+describe('ApiVersionInterceptor', () => {
+ let interceptor: ApiVersionInterceptor
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ApiVersionInterceptor],
+ })
+
+ interceptor = TestBed.inject(ApiVersionInterceptor)
+ })
+
+ it('should add api version to headers', () => {
+ interceptor.intercept(new HttpRequest('GET', 'https://example.com'), {
+ handle: (request) => {
+ const header = request.headers['lazyUpdate'][0]
+ expect(header.name).toEqual('Accept')
+ expect(header.value).toEqual(
+ `application/json; version=${environment.apiVersion}`
+ )
+ return of({} as HttpEvent<any>)
+ },
+ })
+ })
+})
import { TestBed } from '@angular/core/testing'
-
import { CsrfInterceptor } from './csrf.interceptor'
+import { Meta } from '@angular/platform-browser'
+import { HttpEvent, HttpRequest } from '@angular/common/http'
+import { of } from 'rxjs'
+import { CookieService } from 'ngx-cookie-service'
describe('CsrfInterceptor', () => {
- beforeEach(() =>
+ let interceptor: CsrfInterceptor
+ let meta: Meta
+ let cookieService: CookieService
+
+ beforeEach(() => {
TestBed.configureTestingModule({
- providers: [CsrfInterceptor],
+ providers: [CsrfInterceptor, Meta, CookieService],
})
- )
- it('should be created', () => {
- const interceptor: CsrfInterceptor = TestBed.inject(CsrfInterceptor)
- expect(interceptor).toBeTruthy()
+ meta = TestBed.inject(Meta)
+ cookieService = TestBed.inject(CookieService)
+ interceptor = TestBed.inject(CsrfInterceptor)
+ })
+
+ it('should get csrf token', () => {
+ meta.addTag({ name: 'cookie_prefix', content: 'ngx-' }, true)
+ const cookieServiceSpy = jest.spyOn(cookieService, 'get')
+ cookieServiceSpy.mockReturnValue('csrftoken')
+ interceptor.intercept(new HttpRequest('GET', 'https://example.com'), {
+ handle: (request) => {
+ expect(request.headers['lazyUpdate'][0]['name']).toEqual('X-CSRFToken')
+ return of({} as HttpEvent<any>)
+ },
+ })
+ expect(cookieServiceSpy).toHaveBeenCalled()
})
})
if (this.meta.getTag('name=cookie_prefix')) {
prefix = this.meta.getTag('name=cookie_prefix').content
}
- let csrfToken = this.cookieService.get(`${prefix ? prefix : ''}csrftoken`)
+ let csrfToken = this.cookieService.get(`${prefix}csrftoken`)
if (csrfToken) {
request = request.clone({
setHeaders: {
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { CustomDatePipe } from './custom-date.pipe'
+import { SettingsService } from '../services/settings.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { DatePipe } from '@angular/common'
+
+describe('CustomDatePipe', () => {
+ let datePipe: CustomDatePipe
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [CustomDatePipe, SettingsService, DatePipe],
+ imports: [HttpClientTestingModule],
+ })
+
+ datePipe = TestBed.inject(CustomDatePipe)
+ })
+
+ it('should parse date strings with additional options', () => {
+ expect(datePipe.transform('5/4/23')).toEqual('May 4, 2023')
+ expect(
+ datePipe.transform(
+ '5/4/23',
+ 'mediumDate',
+ 'America/Los_Angeles',
+ 'iso-8601'
+ )
+ ).toEqual('2023-05-04')
+ })
+})
import { DocumentTitlePipe } from './document-title.pipe'
describe('DocumentTitlePipe', () => {
- it('create an instance', () => {
+ it('should return a value if not null', () => {
const pipe = new DocumentTitlePipe()
- expect(pipe).toBeTruthy()
+ expect(pipe.transform('some string')).toEqual('some string')
+ expect(pipe.transform(null)).toEqual('(no title)')
})
})
import { FileSizePipe } from './file-size.pipe'
describe('FileSizePipe', () => {
- it('create an instance', () => {
+ it('should return file size', () => {
const pipe = new FileSizePipe()
- expect(pipe).toBeTruthy()
+ expect(pipe.transform(1024, 1)).toEqual('1.0 KB')
+ expect(pipe.transform(1024 * 1024, 1)).toEqual('1.0 MB')
+ expect(
+ pipe.transform(1024, {
+ bytes: 0,
+ KB: 3,
+ MB: 1,
+ GB: 1,
+ TB: 2,
+ PB: 2,
+ })
+ ).toEqual('1.000 KB')
+ expect(pipe.transform(NaN, 1)).toEqual('?')
})
})
--- /dev/null
+import { MatchingModel } from '../data/matching-model'
+import { FilterPipe } from './filter.pipe'
+
+describe('FilterPipe', () => {
+ it('should filter matchingmodel items', () => {
+ const pipe = new FilterPipe()
+ const items: MatchingModel[] = [
+ {
+ id: 1,
+ name: 'Hello World',
+ slug: 'slug-1',
+ },
+ {
+ id: 2,
+ name: 'Hello',
+ slug: 'slug-2',
+ },
+ ]
+ let itemsReturned = pipe.transform(items, 'world')
+ expect(itemsReturned).toEqual([items[0]])
+
+ itemsReturned = pipe.transform(null, 'world')
+ expect(itemsReturned).toEqual([])
+
+ itemsReturned = pipe.transform(items, null)
+ expect(itemsReturned).toEqual(items)
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { SafeHtmlPipe } from './safehtml.pipe'
+import { BrowserModule, DomSanitizer } from '@angular/platform-browser'
+
+describe('SafeHtmlPipe', () => {
+ let pipe: SafeHtmlPipe
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [SafeHtmlPipe],
+ imports: [BrowserModule],
+ })
+ pipe = TestBed.inject(SafeHtmlPipe)
+ })
+
+ it('should bypass security and trust the url', () => {
+ const html = '<div>some content</div>'
+ const domSanitizer = TestBed.inject(DomSanitizer)
+ const sanitizerSpy = jest.spyOn(domSanitizer, 'bypassSecurityTrustHtml')
+ let safeHtml = pipe.transform(html)
+ expect(safeHtml).not.toBeNull()
+ expect(sanitizerSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { SafeUrlPipe } from './safeurl.pipe'
+import { BrowserModule, DomSanitizer } from '@angular/platform-browser'
+
+describe('SafeUrlPipe', () => {
+ let pipe: SafeUrlPipe
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [SafeUrlPipe],
+ imports: [BrowserModule],
+ })
+ pipe = TestBed.inject(SafeUrlPipe)
+ })
+
+ it('should bypass security and trust the url', () => {
+ const url = 'https://example.com'
+ const domSanitizer = TestBed.inject(DomSanitizer)
+ const sanitizerSpy = jest.spyOn(
+ domSanitizer,
+ 'bypassSecurityTrustResourceUrl'
+ )
+
+ let safeResourceUrl = pipe.transform(url)
+ expect(safeResourceUrl).not.toBeNull()
+ expect(sanitizerSpy).toHaveBeenCalled()
+
+ safeResourceUrl = pipe.transform(null)
+ expect(safeResourceUrl).not.toBeNull()
+ expect(sanitizerSpy).toHaveBeenCalled()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { UsernamePipe } from './username.pipe'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { PermissionsService } from '../services/permissions.service'
+import { UserService } from '../services/rest/user.service'
+
+describe('UsernamePipe', () => {
+ let pipe: UsernamePipe
+ let httpTestingController: HttpTestingController
+ let permissionsService: PermissionsService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [UsernamePipe, PermissionsService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ permissionsService = TestBed.inject(PermissionsService)
+ const permissionsSpy = jest.spyOn(permissionsService, 'currentUserCan')
+ permissionsSpy.mockImplementation((action, type) => {
+ return true
+ })
+ pipe = TestBed.inject(UsernamePipe)
+ })
+
+ afterEach(() => {
+ httpTestingController.verify()
+ })
+
+ it('should transform user id to username', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}users/?page=1&page_size=100000`
+ )
+ req.flush({
+ results: [
+ {
+ id: 2,
+ username: 'username2',
+ },
+ {
+ id: 3,
+ username: 'username3',
+ first_name: 'User',
+ last_name: 'Name3',
+ },
+ ],
+ })
+
+ let username = pipe.transform(2)
+ expect(username).toEqual('username2')
+
+ username = pipe.transform(3)
+ expect(username).toEqual('User Name3')
+
+ username = pipe.transform(4)
+ expect(username).toEqual('')
+ })
+
+ it('should show generic label when no users retrieved', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}users/?page=1&page_size=100000`
+ )
+ req.flush(null)
+
+ let username = pipe.transform(4)
+ expect(username).toEqual('Shared')
+ })
+})
import { YesNoPipe } from './yes-no.pipe'
describe('YesNoPipe', () => {
- it('create an instance', () => {
+ it('should convert booleans to yes / no', () => {
const pipe = new YesNoPipe()
- expect(pipe).toBeTruthy()
+ expect(pipe.transform(true)).toEqual('Yes')
+ expect(pipe.transform(false)).toEqual('No')
})
})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import {
+ ConsumerStatusService,
+ FILE_STATUS_MESSAGES,
+ FileStatusPhase,
+} from './consumer-status.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { DocumentService } from './rest/document.service'
+import { HttpEventType, HttpResponse } from '@angular/common/http'
+import WS from 'jest-websocket-mock'
+
+describe('ConsumerStatusService', () => {
+ let httpTestingController: HttpTestingController
+ let consumerStatusService: ConsumerStatusService
+ let documentService: DocumentService
+ const server = new WS(
+ `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
+ { jsonProtocol: true }
+ )
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ConsumerStatusService, DocumentService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ consumerStatusService = TestBed.inject(ConsumerStatusService)
+ documentService = TestBed.inject(DocumentService)
+ })
+
+ afterEach(() => {
+ httpTestingController.verify()
+ })
+
+ it('should update status on websocket processing progress', () => {
+ const task_id = '1234'
+ const status = consumerStatusService.newFileUpload('file.pdf')
+ expect(status.getProgress()).toEqual(0)
+
+ consumerStatusService.connect()
+
+ consumerStatusService
+ .onDocumentConsumptionFinished()
+ .subscribe((filestatus) => {
+ expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS)
+ })
+
+ consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
+ expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
+ })
+
+ server.send({
+ task_id,
+ filename: 'file.pdf',
+ current_progress: 50,
+ max_progress: 100,
+ document_id: 12,
+ status: 'STARTING',
+ })
+
+ expect(status.getProgress()).toBeCloseTo(0.6) // 0.8 * 50/100
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
+ status,
+ ])
+
+ server.send({
+ task_id,
+ filename: 'file.pdf',
+ current_progress: 100,
+ max_progress: 100,
+ document_id: 12,
+ status: 'SUCCESS',
+ message: FILE_STATUS_MESSAGES.finished,
+ })
+
+ expect(status.getProgress()).toEqual(1)
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
+ 0
+ )
+ expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
+
+ consumerStatusService.disconnect()
+ })
+
+ it('should update status on websocket failed progress', () => {
+ const task_id = '1234'
+ const status = consumerStatusService.newFileUpload('file.pdf')
+ status.taskId = task_id
+ consumerStatusService.connect()
+
+ consumerStatusService
+ .onDocumentConsumptionFailed()
+ .subscribe((filestatus) => {
+ expect(filestatus.phase).toEqual(FileStatusPhase.FAILED)
+ })
+
+ server.send({
+ task_id,
+ filename: 'file.pdf',
+ current_progress: 50,
+ max_progress: 100,
+ document_id: 12,
+ })
+
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
+ status,
+ ])
+
+ server.send({
+ task_id,
+ filename: 'file.pdf',
+ current_progress: 50,
+ max_progress: 100,
+ document_id: 12,
+ status: 'FAILED',
+ message: FILE_STATUS_MESSAGES.document_already_exists,
+ })
+
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
+ 0
+ )
+ expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
+ })
+
+ it('should update status on upload progress', () => {
+ const task_id = '1234'
+ const status = consumerStatusService.newFileUpload('file.pdf')
+
+ documentService.uploadDocument({}).subscribe((event) => {
+ if (event.type === HttpEventType.Response) {
+ status.taskId = event.body['task_id']
+ status.message = $localize`Upload complete, waiting...`
+ } else if (event.type === HttpEventType.UploadProgress) {
+ status.updateProgress(
+ FileStatusPhase.UPLOADING,
+ event.loaded,
+ event.total
+ )
+ }
+ })
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/post_document/`
+ )
+
+ req.event(
+ new HttpResponse({
+ body: {
+ task_id,
+ },
+ })
+ )
+
+ req.event({
+ type: HttpEventType.UploadProgress,
+ loaded: 100,
+ total: 300,
+ })
+
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
+ ).toEqual([status])
+ expect(consumerStatusService.getConsumerStatus()).toEqual([status])
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
+ status,
+ ])
+
+ req.event({
+ type: HttpEventType.UploadProgress,
+ loaded: 300,
+ total: 300,
+ })
+
+ expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300
+ })
+
+ it('should support dismiss completed', () => {
+ consumerStatusService.connect()
+ server.send({
+ task_id: '1234',
+ filename: 'file.pdf',
+ current_progress: 100,
+ max_progress: 100,
+ document_id: 12,
+ status: 'SUCCESS',
+ message: 'finished',
+ })
+
+ expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
+ consumerStatusService.dismissCompleted()
+ expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
+ })
+
+ it('should support dismiss', () => {
+ const task_id = '1234'
+ const status = consumerStatusService.newFileUpload('file.pdf')
+ status.taskId = task_id
+ status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
+
+ const status2 = consumerStatusService.newFileUpload('file2.pdf')
+ status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
+
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
+ ).toEqual([status, status2])
+ expect(consumerStatusService.getConsumerStatus()).toEqual([status, status2])
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
+ status,
+ status2,
+ ])
+
+ consumerStatusService.dismiss(status)
+ expect(consumerStatusService.getConsumerStatus()).toEqual([status2])
+
+ consumerStatusService.dismiss(status2)
+ expect(consumerStatusService.getConsumerStatus()).toHaveLength(0)
+ })
+
+ it('should support fail', () => {
+ const task_id = '1234'
+ const status = consumerStatusService.newFileUpload('file.pdf')
+ status.taskId = task_id
+ status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
+ 1
+ )
+ expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
+ consumerStatusService.fail(status, 'fail')
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
+ 0
+ )
+ expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
+ })
+
+ it('should notify of document created on status message without upload', () => {
+ consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
+ expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
+ })
+
+ server.send({
+ task_id: '1234',
+ filename: 'file.pdf',
+ current_progress: 50,
+ max_progress: 100,
+ document_id: 12,
+ status: 'STARTING',
+ })
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { DocumentListViewService } from './document-list-view.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { Subscription } from 'rxjs'
+import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
+import { Params, Router, convertToParamMap } from '@angular/router'
+import {
+ FILTER_HAS_TAGS_ALL,
+ FILTER_HAS_TAGS_ANY,
+} from '../data/filter-rule-type'
+import { PaperlessSavedView } from '../data/paperless-saved-view'
+import { FilterRule } from '../data/filter-rule'
+import { RouterTestingModule } from '@angular/router/testing'
+import { routes } from 'src/app/app-routing.module'
+import { PermissionsGuard } from '../guards/permissions.guard'
+import { SettingsService } from './settings.service'
+import { SETTINGS_KEYS } from '../data/paperless-uisettings'
+
+const documents = [
+ {
+ id: 1,
+ title: 'Doc 1',
+ content: 'some content',
+ tags: [1, 2, 3],
+ correspondent: 11,
+ document_type: 3,
+ storage_path: 8,
+ },
+ {
+ id: 2,
+ title: 'Doc 2',
+ content: 'some content',
+ },
+ {
+ id: 3,
+ title: 'Doc 3',
+ content: 'some content',
+ },
+ {
+ id: 4,
+ title: 'Doc 4',
+ content: 'some content',
+ },
+ {
+ id: 5,
+ title: 'Doc 5',
+ content: 'some content',
+ },
+ {
+ id: 6,
+ title: 'Doc 6',
+ content: 'some content',
+ },
+]
+const full_results = {
+ count: documents.length,
+ results: documents,
+}
+
+const tags__id__all = '9'
+const filterRules: FilterRule[] = [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags__id__all,
+ },
+]
+
+const view: PaperlessSavedView = {
+ id: 3,
+ name: 'Saved View',
+ sort_field: 'added',
+ sort_reverse: true,
+ filter_rules: filterRules,
+}
+
+describe('DocumentListViewService', () => {
+ let httpTestingController: HttpTestingController
+ let documentListViewService: DocumentListViewService
+ let subscriptions: Subscription[] = []
+ let router: Router
+ let settingsService: SettingsService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [DocumentListViewService, PermissionsGuard, SettingsService],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ ],
+ declarations: [ConfirmDialogComponent],
+ teardown: { destroyAfterEach: true },
+ })
+
+ sessionStorage.clear()
+ httpTestingController = TestBed.inject(HttpTestingController)
+ documentListViewService = TestBed.inject(DocumentListViewService)
+ settingsService = TestBed.inject(SettingsService)
+ router = TestBed.inject(Router)
+ })
+
+ afterEach(() => {
+ httpTestingController.verify()
+ sessionStorage.clear()
+ })
+
+ afterAll(() => {
+ subscriptions?.forEach((subscription) => {
+ subscription.unsubscribe()
+ })
+ })
+
+ it('should reload the list', () => {
+ expect(documentListViewService.currentPage).toEqual(1)
+ documentListViewService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(full_results)
+ httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/selection_data/`
+ )
+ expect(req.request.method).toEqual('GET')
+ expect(documentListViewService.isReloading).toBeFalsy()
+ expect(documentListViewService.activeSavedViewId).toBeNull()
+ expect(documentListViewService.activeSavedViewTitle).toBeNull()
+ expect(documentListViewService.collectionSize).toEqual(documents.length)
+ expect(documentListViewService.getLastPage()).toEqual(1)
+ })
+
+ it('should handle error on page request out of range', () => {
+ documentListViewService.currentPage = 50
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=50&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush([], { status: 404, statusText: 'Unexpected error' })
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ expect(documentListViewService.currentPage).toEqual(1)
+ })
+
+ it('should handle error on filtering request', () => {
+ documentListViewService.currentPage = 1
+ const tags__id__in = 'hello'
+ const filterRulesAny = [
+ {
+ rule_type: FILTER_HAS_TAGS_ANY,
+ value: tags__id__in,
+ },
+ ]
+ documentListViewService.filterRules = filterRulesAny
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(
+ { archive_serial_number: 'hello' },
+ { status: 404, statusText: 'Unexpected error' }
+ )
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ // reset the list
+ documentListViewService.filterRules = []
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ })
+
+ it('should support setting sort', () => {
+ expect(documentListViewService.sortField).toEqual('created')
+ expect(documentListViewService.sortReverse).toBeTruthy()
+ documentListViewService.setSort('added', false)
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=added&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ expect(documentListViewService.sortField).toEqual('added')
+ expect(documentListViewService.sortReverse).toBeFalsy()
+
+ documentListViewService.sortField = 'created'
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=created&truncate_content=true`
+ )
+ expect(documentListViewService.sortField).toEqual('created')
+ documentListViewService.sortReverse = true
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ expect(documentListViewService.sortReverse).toBeTruthy()
+ })
+
+ it('should load from query params', () => {
+ expect(documentListViewService.currentPage).toEqual(1)
+ const page = 2
+ const sort = 'added'
+ const reverse = true
+ const params: Params = {
+ page,
+ sort,
+ reverse,
+ }
+ documentListViewService.loadFromQueryParams(convertToParamMap(params))
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=${page}&page_size=${
+ documentListViewService.currentPageSize
+ }&ordering=${reverse ? '-' : ''}${sort}&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ expect(documentListViewService.currentPage).toEqual(page)
+ expect(documentListViewService.filterRules).toEqual([])
+ })
+
+ it('should load filter rules from query params', () => {
+ const sort = 'added'
+ const reverse = true
+ const params: Params = {
+ sort,
+ reverse,
+ tags__id__all,
+ }
+ documentListViewService.loadFromQueryParams(convertToParamMap(params))
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
+ )
+ expect(req.request.method).toEqual('GET')
+ expect(documentListViewService.filterRules).toEqual([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags__id__all,
+ },
+ ])
+ req.flush(full_results)
+ httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/selection_data/`
+ )
+ })
+
+ it('should use filter rules to update query params', () => {
+ documentListViewService.filterRules = filterRules
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should support quick filter', () => {
+ documentListViewService.quickFilter(filterRules)
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=${documentListViewService.currentPage}&page_size=${documentListViewService.currentPageSize}&ordering=-created&truncate_content=true&tags__id__all=${tags__id__all}`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should support loading saved view', () => {
+ const routerSpy = jest.spyOn(router, 'navigate')
+ documentListViewService.activateSavedView(view)
+ expect(routerSpy).toHaveBeenCalledWith(['view', view.id])
+ documentListViewService.activateSavedView(null)
+ })
+
+ it('should support loading saved view view query params', () => {
+ const page = 2
+ const params: Params = {
+ view: view.id,
+ page,
+ }
+ documentListViewService.activateSavedViewWithQueryParams(
+ view,
+ convertToParamMap(params)
+ )
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=${page}&page_size=${documentListViewService.currentPageSize}&ordering=-added&truncate_content=true&tags__id__all=${tags__id__all}`
+ )
+ expect(req.request.method).toEqual('GET')
+ // reset the list
+ documentListViewService.currentPage = 1
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true&tags__id__all=9`
+ )
+ documentListViewService.filterRules = []
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-added&truncate_content=true`
+ )
+ documentListViewService.sortField = 'created'
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ documentListViewService.activateSavedView(null)
+ })
+
+ it('should support navigating next / previous', () => {
+ documentListViewService.filterRules = []
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(documentListViewService.currentPage).toEqual(1)
+ documentListViewService.currentPageSize = 3
+ documentListViewService.reload()
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush({
+ count: 3,
+ results: documents.slice(0, 3),
+ })
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}documents/selection_data/`)
+ .flush([])
+ expect(documentListViewService.hasNext(documents[0].id)).toBeTruthy()
+ expect(documentListViewService.hasPrevious(documents[0].id)).toBeFalsy()
+ documentListViewService.getNext(documents[0].id).subscribe((docId) => {
+ expect(docId).toEqual(documents[1].id)
+ })
+ documentListViewService.getNext(documents[2].id).subscribe((docId) => {
+ expect(docId).toEqual(documents[3].id)
+ expect(documentListViewService.currentPage).toEqual(2)
+ })
+ documentListViewService.getPrevious(documents[3].id).subscribe((docId) => {
+ expect(docId).toEqual(documents[2].id)
+ expect(documentListViewService.currentPage).toEqual(1)
+ })
+ })
+
+ it('should update page size from settings', () => {
+ settingsService.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, 10)
+ documentListViewService.updatePageSize()
+ expect(documentListViewService.currentPageSize).toEqual(10)
+ })
+
+ it('should support select a document', () => {
+ documentListViewService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(full_results)
+ httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/selection_data/`
+ )
+ documentListViewService.toggleSelected(documents[0])
+ expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
+ documentListViewService.toggleSelected(documents[0])
+ expect(documentListViewService.isSelected(documents[0])).toBeFalsy()
+ })
+
+ it('should support select all', () => {
+ documentListViewService.selectAll()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(full_results)
+ expect(documentListViewService.selected.size).toEqual(documents.length)
+ expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
+ documentListViewService.selectNone()
+ })
+
+ it('should support select page', () => {
+ documentListViewService.currentPageSize = 3
+ documentListViewService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=3&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush({
+ count: 3,
+ results: documents.slice(0, 3),
+ })
+ httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/selection_data/`
+ )
+ documentListViewService.selectPage()
+ expect(documentListViewService.selected.size).toEqual(3)
+ expect(documentListViewService.isSelected(documents[5])).toBeFalsy()
+ })
+
+ it('should support select range', () => {
+ documentListViewService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(full_results)
+ httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/selection_data/`
+ )
+ documentListViewService.toggleSelected(documents[0])
+ expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
+ documentListViewService.selectRangeTo(documents[2])
+ expect(documentListViewService.isSelected(documents[1])).toBeTruthy()
+ documentListViewService.selectRangeTo(documents[4])
+ expect(documentListViewService.isSelected(documents[3])).toBeTruthy()
+ })
+
+ it('should support selection range reduction', () => {
+ documentListViewService.selectAll()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(full_results)
+ expect(documentListViewService.selected.size).toEqual(6)
+
+ documentListViewService.filterRules = filterRules
+ httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9`
+ )
+ const reqs = httpTestingController.match(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id&tags__id__all=9`
+ )
+ reqs[0].flush({
+ count: 3,
+ results: documents.slice(0, 3),
+ })
+ expect(documentListViewService.selected.size).toEqual(3)
+ })
+})
import { Injectable } from '@angular/core'
import { ParamMap, Router } from '@angular/router'
import { Observable, first } from 'rxjs'
+import { FilterRule } from '../data/filter-rule'
import {
filterRulesDiffer,
cloneFilterRules,
- FilterRule,
isFullTextFilterRule,
-} from '../data/filter-rule'
+} from '../utils/filter-rules'
import { PaperlessDocument } from '../data/paperless-document'
import { PaperlessSavedView } from '../data/paperless-saved-view'
import { SETTINGS_KEYS } from '../data/paperless-uisettings'
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { OpenDocumentsService } from './open-documents.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { Subscription } from 'rxjs'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
+import { OPEN_DOCUMENT_SERVICE } from '../data/storage-keys'
+
+const documents = [
+ {
+ id: 1,
+ title: 'Doc 1',
+ content: 'some content',
+ tags: [1, 2, 3],
+ correspondent: 11,
+ document_type: 3,
+ storage_path: 8,
+ },
+ {
+ id: 2,
+ title: 'Doc 2',
+ content: 'some content',
+ },
+ {
+ id: 3,
+ title: 'Doc 3',
+ content: 'some content',
+ },
+ {
+ id: 4,
+ title: 'Doc 4',
+ content: 'some content',
+ },
+ {
+ id: 5,
+ title: 'Doc 5',
+ content: 'some content',
+ },
+ {
+ id: 6,
+ title: 'Doc 6',
+ content: 'some content',
+ },
+]
+
+describe('OpenDocumentsService', () => {
+ let httpTestingController: HttpTestingController
+ let openDocumentsService: OpenDocumentsService
+ let modalService: NgbModal
+ let subscriptions: Subscription[] = []
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [OpenDocumentsService, NgbModal],
+ imports: [HttpClientTestingModule],
+ declarations: [ConfirmDialogComponent],
+ })
+
+ sessionStorage.clear()
+ httpTestingController = TestBed.inject(HttpTestingController)
+ openDocumentsService = TestBed.inject(OpenDocumentsService)
+ modalService = TestBed.inject(NgbModal)
+ })
+
+ afterEach(() => {
+ httpTestingController.verify()
+ })
+
+ afterAll(() => {
+ subscriptions?.forEach((subscription) => {
+ subscription.unsubscribe()
+ })
+ })
+
+ it('should open documents', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(1)
+ const doc = openDocumentsService.getOpenDocument(documents[0].id)
+ expect(doc.id).toEqual(documents[0].id)
+ })
+
+ it('should limit number of open documents', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[1]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[2]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[3]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[4]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[5]).subscribe()
+ )
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(5)
+ })
+
+ it('should close documents', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(1)
+ openDocumentsService.closeDocument(documents[0])
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(0)
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[1]).subscribe()
+ )
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(2)
+ subscriptions.push(openDocumentsService.closeAll().subscribe())
+ })
+
+ it('should allow set dirty status, warn on close', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ openDocumentsService.setDirty(documents[0], false)
+ expect(openDocumentsService.hasDirty()).toBeFalsy()
+ openDocumentsService.setDirty(documents[0], true)
+ expect(openDocumentsService.hasDirty()).toBeTruthy()
+ const modalSpy = jest.spyOn(modalService, 'open')
+ subscriptions.push(
+ openDocumentsService.closeDocument(documents[0]).subscribe()
+ )
+ expect(modalSpy).toHaveBeenCalled()
+ })
+
+ it('should allow set dirty status, warn on closeAll', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[1]).subscribe()
+ )
+ openDocumentsService.setDirty(documents[0], true)
+ expect(openDocumentsService.hasDirty()).toBeTruthy()
+ const modalSpy = jest.spyOn(modalService, 'open')
+ subscriptions.push(openDocumentsService.closeAll().subscribe())
+ expect(modalSpy).toHaveBeenCalled()
+ })
+
+ it('should load open documents from localStorage', () => {
+ sessionStorage.setItem(
+ OPEN_DOCUMENT_SERVICE.DOCUMENTS,
+ JSON.stringify(documents)
+ )
+ const testOpenDocumentsService = new OpenDocumentsService(
+ null,
+ modalService
+ )
+ expect(testOpenDocumentsService.getOpenDocuments()).toHaveLength(
+ documents.length
+ )
+ })
+
+ it('should remove open documents from localStorage on error', () => {
+ sessionStorage.setItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS, 'hello world')
+ const testOpenDocumentsService = new OpenDocumentsService(
+ null,
+ modalService
+ )
+ expect(testOpenDocumentsService.getOpenDocuments()).toHaveLength(0)
+ expect(sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)).toBeNull()
+ })
+
+ it('should save open documents to localStorage', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[0]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[1]).subscribe()
+ )
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[2]).subscribe()
+ )
+ openDocumentsService.save()
+ const localStorageDocs = JSON.parse(
+ sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)
+ )
+ expect(localStorageDocs).toContainEqual(documents[0])
+ expect(localStorageDocs).toContainEqual(documents[1])
+ expect(localStorageDocs).toContainEqual(documents[2])
+ })
+
+ it('should refresh documents', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[1]).subscribe()
+ )
+ openDocumentsService.refreshDocument(documents[1].id)
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/${documents[1].id}/?full_perms=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush(documents[1])
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(1)
+ })
+
+ it('should handle error on refresh documents', () => {
+ subscriptions.push(
+ openDocumentsService.openDocument(documents[1]).subscribe()
+ )
+ openDocumentsService.refreshDocument(documents[1].id)
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/${documents[1].id}/?full_perms=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.error(new ErrorEvent('timeout'))
+ expect(openDocumentsService.getOpenDocuments()).toHaveLength(0)
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import {
+ PermissionAction,
+ PermissionType,
+ PermissionsService,
+} from './permissions.service'
+import { PaperlessDocument } from '../data/paperless-document'
+
+describe('PermissionsService', () => {
+ let permissionsService: PermissionsService
+
+ const docUnowned: PaperlessDocument = {
+ title: 'Doc title',
+ owner: null,
+ }
+
+ const docOwned: PaperlessDocument = {
+ title: 'Doc title 2',
+ owner: 1,
+ }
+
+ const docNotOwned: PaperlessDocument = {
+ title: 'Doc title 3',
+ owner: 2,
+ }
+
+ const docUserViewGranted: PaperlessDocument = {
+ title: 'Doc title 4',
+ owner: 2,
+ permissions: {
+ view: {
+ users: [1],
+ groups: [],
+ },
+ change: {
+ users: [],
+ groups: [],
+ },
+ },
+ }
+
+ const docUserEditGranted: PaperlessDocument = {
+ title: 'Doc title 5',
+ owner: 2,
+ permissions: {
+ view: {
+ users: [1],
+ groups: [],
+ },
+ change: {
+ users: [1],
+ groups: [],
+ },
+ },
+ }
+
+ const docGroupViewGranted: PaperlessDocument = {
+ title: 'Doc title 4',
+ owner: 2,
+ permissions: {
+ view: {
+ users: [],
+ groups: [1],
+ },
+ change: {
+ users: [],
+ groups: [],
+ },
+ },
+ }
+
+ const docGroupEditGranted: PaperlessDocument = {
+ title: 'Doc title 5',
+ owner: 2,
+ permissions: {
+ view: {
+ users: [],
+ groups: [1],
+ },
+ change: {
+ users: [],
+ groups: [1],
+ },
+ },
+ }
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [PermissionsService],
+ })
+
+ permissionsService = TestBed.inject(PermissionsService)
+ })
+
+ it('correctly interpolates action codes to keys', () => {
+ expect(
+ permissionsService.getPermissionCode(
+ PermissionAction.View,
+ PermissionType.Document
+ )
+ ).toEqual('view_document')
+ expect(permissionsService.getPermissionKeys('view_document')).toEqual({
+ actionKey: 'View', // PermissionAction.View
+ typeKey: 'Document', // PermissionType.Document
+ })
+ })
+
+ it('correctly checks explicit global permissions', () => {
+ permissionsService.initialize(
+ [
+ 'change_savedview',
+ 'change_schedule',
+ 'change_failure',
+ 'delete_token',
+ 'add_mailrule',
+ 'view_failure',
+ 'view_groupresult',
+ 'add_note',
+ 'change_taskresult',
+ 'view_tag',
+ 'view_user',
+ 'add_tag',
+ 'change_processedmail',
+ 'change_session',
+ 'view_taskattributes',
+ 'delete_groupresult',
+ 'delete_correspondent',
+ 'delete_schedule',
+ 'delete_contenttype',
+ 'view_chordcounter',
+ 'view_success',
+ 'delete_documenttype',
+ 'add_tokenproxy',
+ 'delete_paperlesstask',
+ 'add_log',
+ 'view_mailaccount',
+ 'add_uisettings',
+ 'view_savedview',
+ 'view_uisettings',
+ 'delete_storagepath',
+ 'delete_frontendsettings',
+ 'change_paperlesstask',
+ 'view_taskresult',
+ 'delete_processedmail',
+ 'view_processedmail',
+ 'view_session',
+ 'delete_chordcounter',
+ 'view_note',
+ 'delete_session',
+ 'view_document',
+ 'change_mailaccount',
+ 'delete_taskattributes',
+ 'add_groupobjectpermission',
+ 'view_mailrule',
+ 'change_savedviewfilterrule',
+ 'change_log',
+ 'change_comment',
+ 'add_mailaccount',
+ 'add_frontendsettings',
+ 'add_userobjectpermission',
+ 'delete_note',
+ 'view_token',
+ 'add_failure',
+ 'delete_user',
+ 'add_success',
+ 'view_ormq',
+ 'view_tokenproxy',
+ 'delete_uisettings',
+ 'change_groupobjectpermission',
+ 'add_logentry',
+ 'add_ormq',
+ 'view_frontendsettings',
+ 'view_schedule',
+ 'change_taskattributes',
+ 'view_documenttype',
+ 'view_logentry',
+ 'change_correspondent',
+ 'add_groupresult',
+ 'delete_groupobjectpermission',
+ 'change_mailrule',
+ 'change_permission',
+ 'delete_log',
+ 'view_userobjectpermission',
+ 'view_correspondent',
+ 'delete_document',
+ 'change_uisettings',
+ 'change_storagepath',
+ 'change_document',
+ 'delete_tokenproxy',
+ 'change_note',
+ 'delete_permission',
+ 'change_contenttype',
+ 'add_token',
+ 'change_success',
+ 'delete_logentry',
+ 'view_savedviewfilterrule',
+ 'delete_task',
+ 'add_savedview',
+ 'add_paperlesstask',
+ 'add_task',
+ 'change_documenttype',
+ 'add_documenttype',
+ 'change_token',
+ 'view_task',
+ 'view_permission',
+ 'change_task',
+ 'delete_userobjectpermission',
+ 'change_group',
+ 'add_group',
+ 'change_tag',
+ 'change_chordcounter',
+ 'add_storagepath',
+ 'delete_group',
+ 'add_taskattributes',
+ 'delete_mailaccount',
+ 'delete_tag',
+ 'add_schedule',
+ 'delete_failure',
+ 'delete_mailrule',
+ 'add_savedviewfilterrule',
+ 'change_ormq',
+ 'change_logentry',
+ 'add_taskresult',
+ 'view_group',
+ 'delete_comment',
+ 'add_contenttype',
+ 'add_document',
+ 'change_tokenproxy',
+ 'delete_success',
+ 'add_comment',
+ 'delete_ormq',
+ 'add_processedmail',
+ 'view_paperlesstask',
+ 'delete_savedview',
+ 'change_user',
+ 'add_session',
+ 'view_groupobjectpermission',
+ 'add_user',
+ 'add_correspondent',
+ 'delete_taskresult',
+ 'view_contenttype',
+ 'view_storagepath',
+ 'add_permission',
+ 'change_userobjectpermission',
+ 'delete_savedviewfilterrule',
+ 'change_groupresult',
+ 'add_chordcounter',
+ 'view_log',
+ 'view_comment',
+ 'change_frontendsettings',
+ ],
+ {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ }
+ )
+
+ Object.values(PermissionType).forEach((type) => {
+ Object.values(PermissionAction).forEach((action) => {
+ expect(permissionsService.currentUserCan(action, type)).toBeTruthy()
+ })
+ })
+
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ })
+
+ Object.values(PermissionType).forEach((type) => {
+ Object.values(PermissionAction).forEach((action) => {
+ expect(permissionsService.currentUserCan(action, type)).toBeFalsy()
+ })
+ })
+ })
+
+ it('correctly checks global permissions for superuser', () => {
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ is_superuser: true,
+ })
+
+ Object.values(PermissionType).forEach((type) => {
+ Object.values(PermissionAction).forEach((action) => {
+ expect(permissionsService.currentUserCan(action, type)).toBeTruthy()
+ })
+ })
+ })
+
+ it('correctly checks object owner permissions', () => {
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ id: 1,
+ })
+
+ expect(permissionsService.currentUserOwnsObject(docUnowned)).toBeTruthy()
+ expect(permissionsService.currentUserOwnsObject(docOwned)).toBeTruthy()
+ expect(permissionsService.currentUserOwnsObject(docNotOwned)).toBeFalsy()
+ })
+
+ it('correctly checks object owner permissions for superuser', () => {
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ id: 1,
+ is_superuser: true,
+ })
+
+ expect(permissionsService.currentUserOwnsObject(docUnowned)).toBeTruthy()
+ expect(permissionsService.currentUserOwnsObject(docOwned)).toBeTruthy()
+ expect(permissionsService.currentUserOwnsObject(docNotOwned)).toBeTruthy()
+ })
+
+ it('correctly checks granted object permissions', () => {
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ id: 1,
+ })
+
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.View,
+ docNotOwned
+ )
+ ).toBeFalsy()
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.View,
+ docUserViewGranted
+ )
+ ).toBeTruthy()
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.Change,
+ docUserEditGranted
+ )
+ ).toBeTruthy()
+ })
+
+ it('correctly checks granted object permissions for superuser', () => {
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ id: 1,
+ is_superuser: true,
+ })
+
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.View,
+ docNotOwned
+ )
+ ).toBeTruthy()
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.View,
+ docUserViewGranted
+ )
+ ).toBeTruthy()
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.Change,
+ docUserEditGranted
+ )
+ ).toBeTruthy()
+ })
+
+ it('correctly checks granted object permissions from group', () => {
+ permissionsService.initialize([], {
+ username: 'testuser',
+ last_name: 'User',
+ first_name: 'Test',
+ id: 1,
+ groups: [1],
+ })
+
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.View,
+ docNotOwned
+ )
+ ).toBeFalsy()
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.View,
+ docGroupViewGranted
+ )
+ ).toBeTruthy()
+ expect(
+ permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.Change,
+ docGroupEditGranted
+ )
+ ).toBeTruthy()
+ })
+})
action: PermissionAction,
type: PermissionType
): boolean {
- return this.permissions.includes(this.getPermissionCode(action, type))
+ return (
+ this.currentUser?.is_superuser ||
+ this.permissions?.includes(this.getPermissionCode(action, type))
+ )
}
public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { AbstractNameFilterService } from './abstract-name-filter-service'
+import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
+
+let httpTestingController: HttpTestingController
+let service: AbstractNameFilterService<any>
+let subscription: Subscription
+
+export const commonAbstractNameFilterPaperlessServiceTests = (
+ endpoint,
+ ServiceClass
+) => {
+ commonAbstractPaperlessServiceTests(endpoint, ServiceClass)
+
+ describe(`Common name filter service tests for ${endpoint}`, () => {
+ test('should call appropriate api endpoint for list filtering', () => {
+ const page = 2
+ const pageSize = 50
+ const sortField = 'name'
+ const sortReverse = true
+ const nameFilter = 'hello'
+ const fullPerms = true
+ subscription = service
+ .listFiltered(
+ page,
+ pageSize,
+ sortField,
+ sortReverse,
+ nameFilter,
+ fullPerms
+ )
+ .subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=${page}&page_size=${pageSize}&ordering=-${sortField}&name__icontains=${nameFilter}&full_perms=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush([])
+ })
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+ // TestBed.configureTestingModule({
+ // providers: [ServiceClass],
+ // imports: [HttpClientTestingModule],
+ // teardown: { destroyAfterEach: true },
+ // })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(ServiceClass)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ // httpTestingController.verify()
+ })
+}
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { AbstractPaperlessService } from './abstract-paperless-service'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+
+let httpTestingController: HttpTestingController
+let service: AbstractPaperlessService<any>
+let subscription: Subscription
+
+export const commonAbstractPaperlessServiceTests = (endpoint, ServiceClass) => {
+ describe(`Common service tests for ${endpoint}`, () => {
+ test('should call appropriate api endpoint for list all', () => {
+ subscription = service.listAll().subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush([])
+ })
+
+ test('should call appropriate api endpoint for get a single object', () => {
+ const id = 0
+ subscription = service.get(id).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${id}/`
+ )
+ expect(req.request.method).toEqual('GET')
+ req.flush([])
+ })
+
+ test('should call appropriate api endpoint for create a single object', () => {
+ const o = {
+ name: 'Name',
+ }
+ subscription = service.create(o).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/`
+ )
+ expect(req.request.method).toEqual('POST')
+ req.flush([])
+ })
+
+ test('should call appropriate api endpoint for delete a single object', () => {
+ const id = 10
+ const o = {
+ name: 'Name',
+ id,
+ }
+ subscription = service.delete(o).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${id}/`
+ )
+ expect(req.request.method).toEqual('DELETE')
+ req.flush([])
+ })
+
+ test('should call appropriate api endpoint for update a single object', () => {
+ const id = 10
+ const o = {
+ name: 'Name',
+ id,
+ }
+
+ // some services need to call listAll first
+ subscription = service.listAll().subscribe()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ req.flush({
+ results: [o],
+ })
+ subscription.unsubscribe()
+
+ subscription = service.update(o).subscribe()
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${id}/`
+ )
+ expect(req.request.method).toEqual('PUT')
+ req.flush([])
+ })
+
+ test('should call appropriate api endpoint for patch a single object', () => {
+ const id = 10
+ const o = {
+ name: 'Name',
+ id,
+ }
+ subscription = service.patch(o).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${id}/`
+ )
+ expect(req.request.method).toEqual('PATCH')
+ req.flush([])
+ })
+ })
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ServiceClass],
+ imports: [HttpClientTestingModule],
+ teardown: { destroyAfterEach: true },
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(ServiceClass)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ // httpTestingController.verify()
+ })
+}
constructor(protected http: HttpClient, private resourceName: string) {}
- protected getResourceUrl(id?: number, action?: string): string {
+ protected getResourceUrl(id: number = null, action: string = null): string {
let url = `${this.baseUrl}${this.resourceName}/`
- if (id) {
+ if (id !== null) {
url += `${id}/`
}
if (action) {
--- /dev/null
+import { CorrespondentService } from './correspondent.service'
+import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+
+commonAbstractNameFilterPaperlessServiceTests(
+ 'correspondents',
+ CorrespondentService
+)
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
+import { MailFilterAttachmentType } from 'src/app/data/paperless-mail-rule'
+import { MailMetadataTitleOption } from 'src/app/data/paperless-mail-rule'
+import { MailAction } from 'src/app/data/paperless-mail-rule'
+import { DocumentNotesService } from './document-notes.service'
+
+let httpTestingController: HttpTestingController
+let service: DocumentNotesService
+let subscription: Subscription
+const documentId = 12
+const endpoint = 'documents'
+const endpoint2 = 'notes'
+const notes = [
+ {
+ created: new Date(),
+ note: 'contents 1',
+ user: 1,
+ },
+ {
+ created: new Date(),
+ note: 'contents 2',
+ user: 1,
+ },
+ {
+ created: new Date(),
+ note: 'contents 3',
+ user: 2,
+ },
+]
+
+// run common tests
+commonAbstractPaperlessServiceTests(endpoint, DocumentNotesService)
+
+describe(`Additional service tests for DocumentNotesService`, () => {
+ test('should call correct api endpoint on get notes', () => {
+ subscription = service.getNotes(documentId).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documentId}/${endpoint2}/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ test('should call correct api endpoint on add note', () => {
+ const content = 'some new text'
+ subscription = service.addNote(documentId, content).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documentId}/${endpoint2}/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ note: content,
+ })
+ })
+
+ test('should call correct api endpoint on delete note', () => {
+ const noteId = 11
+ subscription = service.deleteNote(documentId, noteId).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documentId}/${endpoint2}/?id=${noteId}`
+ )
+ expect(req.request.method).toEqual('DELETE')
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(DocumentNotesService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+})
--- /dev/null
+import { DocumentTypeService } from './document-type.service'
+import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+
+commonAbstractNameFilterPaperlessServiceTests(
+ 'document_types',
+ DocumentTypeService
+)
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { DocumentService } from './document.service'
+import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
+
+let httpTestingController: HttpTestingController
+let service: DocumentService
+let subscription: Subscription
+const endpoint = 'documents'
+const documents = [
+ {
+ id: 1,
+ title: 'Doc 1',
+ content: 'some content',
+ tags: [1, 2, 3],
+ correspondent: 11,
+ document_type: 3,
+ storage_path: 8,
+ },
+ {
+ id: 2,
+ title: 'Doc 2',
+ content: 'some content',
+ },
+ {
+ id: 3,
+ title: 'Doc 3',
+ content: 'some content',
+ },
+]
+
+describe(`DocumentService`, () => {
+ // common tests e.g. commonAbstractPaperlessServiceTests differ slightly
+ it('should call appropriate api endpoint for list all', () => {
+ subscription = service.listAll().subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should call appropriate api endpoint for get a single document', () => {
+ subscription = service.get(documents[0].id).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/?full_perms=true`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should call appropriate api endpoint for create a single document', () => {
+ subscription = service.create(documents[0]).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/`
+ )
+ expect(req.request.method).toEqual('POST')
+ })
+
+ it('should call appropriate api endpoint for delete a single document', () => {
+ subscription = service.delete(documents[0]).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
+ )
+ expect(req.request.method).toEqual('DELETE')
+ })
+
+ it('should call appropriate api endpoint for update a single document', () => {
+ subscription = service.update(documents[0]).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
+ )
+ expect(req.request.method).toEqual('PUT')
+ })
+
+ it('should call appropriate api endpoint for patch a single document', () => {
+ subscription = service.patch(documents[0]).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/`
+ )
+ expect(req.request.method).toEqual('PATCH')
+ })
+
+ it('should call appropriate api endpoint for listing all documents ids in a filter set', () => {
+ subscription = service
+ .listAllFilteredIds([
+ {
+ rule_type: FILTER_TITLE,
+ value: 'apple',
+ },
+ ])
+ .subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000&fields=id&title__icontains=apple`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should call appropriate api endpoint for uploading a document', () => {
+ subscription = service.uploadDocument(documents[0]).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/post_document/`
+ )
+ expect(req.request.method).toEqual('POST')
+ })
+
+ it('should call appropriate api endpoint for getting metadata', () => {
+ subscription = service.getMetadata(documents[0].id).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/metadata/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should call appropriate api endpoint for getting selection data', () => {
+ const ids = [documents[0].id]
+ subscription = service.getSelectionData(ids).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/selection_data/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ documents: ids,
+ })
+ })
+
+ it('should call appropriate api endpoint for getting suggestions', () => {
+ subscription = service.getSuggestions(documents[0].id).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/suggestions/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should call appropriate api endpoint for bulk download', () => {
+ const ids = [1, 2, 3]
+ const content = 'both'
+ const useFilenameFormatting = false
+ subscription = service
+ .bulkDownload(ids, content, useFilenameFormatting)
+ .subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/bulk_download/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ documents: ids,
+ content,
+ follow_formatting: useFilenameFormatting,
+ })
+ })
+
+ it('should call appropriate api endpoint for bulk edit', () => {
+ const ids = [1, 2, 3]
+ const method = 'modify_tags'
+ const parameters = {
+ add_tags: [15],
+ remove_tags: [6],
+ }
+ subscription = service.bulkEdit(ids, method, parameters).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/bulk_edit/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ documents: ids,
+ method,
+ parameters,
+ })
+ })
+
+ it('should return the correct preview URL for a single document', () => {
+ let url = service.getPreviewUrl(documents[0].id)
+ expect(url).toEqual(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/preview/`
+ )
+ url = service.getPreviewUrl(documents[0].id, true)
+ expect(url).toEqual(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/preview/?original=true`
+ )
+ })
+
+ it('should return the correct thumb URL for a single document', () => {
+ let url = service.getThumbUrl(documents[0].id)
+ expect(url).toEqual(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/thumb/`
+ )
+ })
+
+ it('should return the correct download URL for a single document', () => {
+ let url = service.getDownloadUrl(documents[0].id)
+ expect(url).toEqual(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/`
+ )
+ url = service.getDownloadUrl(documents[0].id, true)
+ expect(url).toEqual(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/download/?original=true`
+ )
+ })
+
+ it('should add observables to document', () => {
+ subscription = service
+ .listFiltered(1, 25, 'title', false, [])
+ .subscribe((result) => {
+ expect(result.results).toHaveLength(3)
+ const doc = result.results[0]
+ expect(doc.correspondent$).not.toBeNull()
+ expect(doc.document_type$).not.toBeNull()
+ expect(doc.tags$).not.toBeNull()
+ expect(doc.storage_path$).not.toBeNull()
+ })
+ httpTestingController
+ .expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=25&ordering=title`
+ )
+ .flush({
+ results: documents,
+ })
+ })
+
+ it('should set search query', () => {
+ const searchQuery = 'hello'
+ service.searchQuery = searchQuery
+ let url = service.getPreviewUrl(documents[0].id)
+ expect(url).toEqual(
+ `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/preview/#search="${searchQuery}"`
+ )
+ })
+})
+
+beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [DocumentService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(DocumentService)
+})
+
+afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+})
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { GroupService } from './group.service'
+import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+
+let httpTestingController: HttpTestingController
+let service: GroupService
+let subscription: Subscription
+const endpoint = 'groups'
+const group = {
+ name: 'Group Name',
+ id: 1,
+ user_count: 1,
+ permissions: [
+ 'change_savedview',
+ 'change_schedule',
+ 'change_failure',
+ 'delete_token',
+ 'add_mailrule',
+ 'view_failure',
+ 'view_groupresult',
+ 'add_note',
+ 'change_taskresult',
+ 'view_tag',
+ 'view_user',
+ 'add_tag',
+ 'change_processedmail',
+ 'change_session',
+ 'view_taskattributes',
+ 'delete_groupresult',
+ 'delete_correspondent',
+ 'delete_schedule',
+ 'delete_contenttype',
+ 'view_chordcounter',
+ 'view_success',
+ 'delete_documenttype',
+ 'add_tokenproxy',
+ 'delete_paperlesstask',
+ 'add_log',
+ 'view_mailaccount',
+ 'add_uisettings',
+ 'view_savedview',
+ 'view_uisettings',
+ 'delete_storagepath',
+ 'delete_frontendsettings',
+ 'change_paperlesstask',
+ 'view_taskresult',
+ 'delete_processedmail',
+ 'view_processedmail',
+ 'view_session',
+ 'delete_chordcounter',
+ 'view_note',
+ 'delete_session',
+ 'view_document',
+ 'change_mailaccount',
+ 'delete_taskattributes',
+ 'add_groupobjectpermission',
+ 'view_mailrule',
+ 'change_savedviewfilterrule',
+ 'change_log',
+ 'change_comment',
+ 'add_mailaccount',
+ 'add_frontendsettings',
+ 'add_userobjectpermission',
+ 'delete_note',
+ 'view_token',
+ 'add_failure',
+ 'delete_user',
+ 'add_success',
+ 'view_ormq',
+ 'view_tokenproxy',
+ 'delete_uisettings',
+ 'change_groupobjectpermission',
+ 'add_logentry',
+ 'add_ormq',
+ 'view_frontendsettings',
+ 'view_schedule',
+ 'change_taskattributes',
+ 'view_documenttype',
+ 'view_logentry',
+ 'change_correspondent',
+ 'add_groupresult',
+ 'delete_groupobjectpermission',
+ 'change_mailrule',
+ 'change_permission',
+ 'delete_log',
+ 'view_userobjectpermission',
+ 'view_correspondent',
+ 'delete_document',
+ 'change_uisettings',
+ 'change_storagepath',
+ 'change_document',
+ 'delete_tokenproxy',
+ 'change_note',
+ 'delete_permission',
+ 'change_contenttype',
+ 'add_token',
+ 'change_success',
+ 'delete_logentry',
+ 'view_savedviewfilterrule',
+ 'delete_task',
+ 'add_savedview',
+ 'add_paperlesstask',
+ 'add_task',
+ 'change_documenttype',
+ 'add_documenttype',
+ 'change_token',
+ 'view_task',
+ 'view_permission',
+ 'change_task',
+ 'delete_userobjectpermission',
+ 'change_group',
+ 'add_group',
+ 'change_tag',
+ 'change_chordcounter',
+ 'add_storagepath',
+ 'delete_group',
+ 'add_taskattributes',
+ 'delete_mailaccount',
+ 'delete_tag',
+ 'add_schedule',
+ 'delete_failure',
+ 'delete_mailrule',
+ 'add_savedviewfilterrule',
+ 'change_ormq',
+ 'change_logentry',
+ 'add_taskresult',
+ 'view_group',
+ 'delete_comment',
+ 'add_contenttype',
+ 'add_document',
+ 'change_tokenproxy',
+ 'delete_success',
+ 'add_comment',
+ 'delete_ormq',
+ 'add_processedmail',
+ 'view_paperlesstask',
+ 'delete_savedview',
+ 'change_user',
+ 'add_session',
+ 'view_groupobjectpermission',
+ 'add_user',
+ 'add_correspondent',
+ 'delete_taskresult',
+ 'view_contenttype',
+ 'view_storagepath',
+ 'add_permission',
+ 'change_userobjectpermission',
+ 'delete_savedviewfilterrule',
+ 'change_groupresult',
+ 'add_chordcounter',
+ 'view_log',
+ 'view_comment',
+ 'change_frontendsettings',
+ ],
+}
+
+// run common tests
+commonAbstractNameFilterPaperlessServiceTests(endpoint, GroupService)
+
+describe('Additional service tests for GroupService', () => {
+ it('should retain permissions on update', () => {
+ subscription = service.listAll().subscribe()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ req.flush({
+ results: [group],
+ })
+ subscription.unsubscribe()
+
+ subscription = service.update(group).subscribe()
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${group.id}/`
+ )
+ expect(req.request.body.permissions).toHaveLength(group.permissions.length)
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(GroupService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+})
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { LogService } from './log.service'
+
+let httpTestingController: HttpTestingController
+let service: LogService
+let subscription: Subscription
+const endpoint = 'logs'
+
+describe('LogService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [LogService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(LogService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+
+ it('should call correct api endpoint on logs list', () => {
+ subscription = service.list().subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('should call correct api endpoint on logs get', () => {
+ const id: string = 'mail'
+ subscription = service.get(id).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${id}/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+})
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
+import { MailAccountService } from './mail-account.service'
+import { IMAPSecurity } from 'src/app/data/paperless-mail-account'
+
+let httpTestingController: HttpTestingController
+let service: MailAccountService
+let subscription: Subscription
+const endpoint = 'mail_accounts'
+const mail_accounts = [
+ {
+ name: 'Mail Account',
+ id: 1,
+ imap_server: 'imap.example.com',
+ imap_port: 443,
+ imap_security: IMAPSecurity.SSL,
+ username: 'user',
+ password: 'pass',
+ is_token: false,
+ },
+ {
+ name: 'Mail Account 2',
+ id: 2,
+ imap_server: 'imap.example.com',
+ imap_port: 443,
+ imap_security: IMAPSecurity.SSL,
+ username: 'user',
+ password: 'pass',
+ is_token: false,
+ },
+ {
+ name: 'Mail Account 3',
+ id: 3,
+ imap_server: 'imap.example.com',
+ imap_port: 443,
+ imap_security: IMAPSecurity.SSL,
+ username: 'user',
+ password: 'pass',
+ is_token: false,
+ },
+]
+
+// run common tests
+commonAbstractPaperlessServiceTests(endpoint, MailAccountService)
+
+describe(`Additional service tests for MailAccountService`, () => {
+ it('should correct api endpoint on test account', () => {
+ subscription = service.test(mail_accounts[0]).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/test/`
+ )
+ expect(req.request.method).toEqual('POST')
+ })
+
+ it('should support patchMany', () => {
+ subscription = service.patchMany(mail_accounts).subscribe()
+ mail_accounts.forEach((mail_account) => {
+ const reqs = httpTestingController.match(
+ `${environment.apiBaseUrl}${endpoint}/${mail_account.id}/`
+ )
+ expect(reqs).toHaveLength(1)
+ expect(reqs[0].request.method).toEqual('PATCH')
+ })
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(MailAccountService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+})
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
+import { MailRuleService } from './mail-rule.service'
+import { MailFilterAttachmentType } from 'src/app/data/paperless-mail-rule'
+import { MailMetadataTitleOption } from 'src/app/data/paperless-mail-rule'
+import { MailAction } from 'src/app/data/paperless-mail-rule'
+
+let httpTestingController: HttpTestingController
+let service: MailRuleService
+let subscription: Subscription
+const endpoint = 'mail_rules'
+const mail_rules = [
+ {
+ name: 'Mail Rule',
+ id: 1,
+ account: 1,
+ order: 1,
+ folder: 'INBOX',
+ filter_from: null,
+ filter_to: null,
+ filter_subject: null,
+ filter_body: null,
+ filter_attachment_filename: null,
+ maximum_age: 30,
+ attachment_type: MailFilterAttachmentType.Everything,
+ action: MailAction.MarkRead,
+ assign_title_from: MailMetadataTitleOption.FromSubject,
+ },
+ {
+ name: 'Mail Rule 2',
+ id: 2,
+ account: 1,
+ order: 1,
+ folder: 'INBOX',
+ filter_from: null,
+ filter_to: null,
+ filter_subject: null,
+ filter_body: null,
+ filter_attachment_filename: null,
+ maximum_age: 30,
+ attachment_type: MailFilterAttachmentType.Everything,
+ action: MailAction.Delete,
+ assign_title_from: MailMetadataTitleOption.FromSubject,
+ },
+ {
+ name: 'Mail Rule 3',
+ id: 3,
+ account: 1,
+ order: 1,
+ folder: 'INBOX',
+ filter_from: null,
+ filter_to: null,
+ filter_subject: null,
+ filter_body: null,
+ filter_attachment_filename: null,
+ maximum_age: 30,
+ attachment_type: MailFilterAttachmentType.Everything,
+ action: MailAction.Flag,
+ assign_title_from: MailMetadataTitleOption.FromSubject,
+ },
+]
+
+// run common tests
+commonAbstractPaperlessServiceTests(endpoint, MailRuleService)
+
+describe(`Additional service tests for MailRuleService`, () => {
+ it('should support patchMany', () => {
+ subscription = service.patchMany(mail_rules).subscribe()
+ mail_rules.forEach((mail_rule) => {
+ const reqs = httpTestingController.match(
+ `${environment.apiBaseUrl}${endpoint}/${mail_rule.id}/`
+ )
+ expect(reqs).toHaveLength(1)
+ expect(reqs[0].request.method).toEqual('PATCH')
+ })
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(MailRuleService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+})
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { RemoteVersionService } from './remote-version.service'
+
+let httpTestingController: HttpTestingController
+let service: RemoteVersionService
+let subscription: Subscription
+const endpoint = 'remote_version'
+
+describe('RemoteVersionService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [RemoteVersionService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(RemoteVersionService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+
+ it('should call correct api endpoint on update check', () => {
+ subscription = service.checkForUpdates().subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+})
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
+import { SavedViewService } from './saved-view.service'
+
+let httpTestingController: HttpTestingController
+let service: SavedViewService
+let subscription: Subscription
+const endpoint = 'saved_views'
+const saved_views = [
+ {
+ name: 'Saved View',
+ id: 1,
+ show_on_dashboard: true,
+ show_in_sidebar: true,
+ sort_field: 'name',
+ sort_reverse: true,
+ filter_rules: [],
+ },
+ {
+ name: 'Saved View 2',
+ id: 2,
+ show_on_dashboard: false,
+ show_in_sidebar: false,
+ sort_field: 'name',
+ sort_reverse: true,
+ filter_rules: [],
+ },
+ {
+ name: 'Saved View 3',
+ id: 3,
+ show_on_dashboard: true,
+ show_in_sidebar: false,
+ sort_field: 'name',
+ sort_reverse: true,
+ filter_rules: [],
+ },
+]
+
+// run common tests
+commonAbstractPaperlessServiceTests(endpoint, SavedViewService)
+
+describe(`Additional service tests for SavedViewService`, () => {
+ it('should retrieve saved views and sort them', () => {
+ service.initialize()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ req.flush({
+ results: saved_views,
+ })
+ expect(service.allViews).toHaveLength(3)
+ expect(service.dashboardViews).toHaveLength(2)
+ expect(service.sidebarViews).toHaveLength(1)
+ })
+
+ it('should support patchMany', () => {
+ subscription = service.patchMany(saved_views).subscribe()
+ saved_views.forEach((saved_view) => {
+ const reqs = httpTestingController.match(
+ `${environment.apiBaseUrl}${endpoint}/${saved_view.id}/`
+ )
+ expect(reqs).toHaveLength(1)
+ expect(reqs[0].request.method).toEqual('PATCH')
+ })
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(SavedViewService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+})
--- /dev/null
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { SearchService } from './search.service'
+
+let httpTestingController: HttpTestingController
+let service: SearchService
+let subscription: Subscription
+const endpoint = 'search/autocomplete'
+
+describe('SearchService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [SearchService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(SearchService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+
+ it('should call correct api endpoint on autocomplete', () => {
+ const term = 'apple'
+ subscription = service.autocomplete(term).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?term=${term}`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+})
--- /dev/null
+import { StoragePathService } from './storage-path.service'
+import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+
+commonAbstractNameFilterPaperlessServiceTests(
+ 'storage_paths',
+ StoragePathService
+)
--- /dev/null
+import { TagService } from './tag.service'
+import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+
+commonAbstractNameFilterPaperlessServiceTests('tags', TagService)
--- /dev/null
+import { HttpTestingController } from '@angular/common/http/testing'
+import { Subscription } from 'rxjs'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+import { UserService } from './user.service'
+
+let httpTestingController: HttpTestingController
+let service: UserService
+let subscription: Subscription
+const endpoint = 'users'
+const user = {
+ username: 'username',
+ id: 1,
+ user_permissions: [
+ 'change_savedview',
+ 'change_schedule',
+ 'change_failure',
+ 'delete_token',
+ 'add_mailrule',
+ 'view_failure',
+ 'view_groupresult',
+ 'add_note',
+ 'change_taskresult',
+ 'view_tag',
+ 'view_user',
+ 'add_tag',
+ 'change_processedmail',
+ 'change_session',
+ 'view_taskattributes',
+ 'delete_groupresult',
+ 'delete_correspondent',
+ 'delete_schedule',
+ 'delete_contenttype',
+ 'view_chordcounter',
+ 'view_success',
+ 'delete_documenttype',
+ 'add_tokenproxy',
+ 'delete_paperlesstask',
+ 'add_log',
+ 'view_mailaccount',
+ 'add_uisettings',
+ 'view_savedview',
+ 'view_uisettings',
+ 'delete_storagepath',
+ 'delete_frontendsettings',
+ 'change_paperlesstask',
+ 'view_taskresult',
+ 'delete_processedmail',
+ 'view_processedmail',
+ 'view_session',
+ 'delete_chordcounter',
+ 'view_note',
+ 'delete_session',
+ 'view_document',
+ 'change_mailaccount',
+ 'delete_taskattributes',
+ 'add_groupobjectpermission',
+ 'view_mailrule',
+ 'change_savedviewfilterrule',
+ 'change_log',
+ 'change_comment',
+ 'add_mailaccount',
+ 'add_frontendsettings',
+ 'add_userobjectpermission',
+ 'delete_note',
+ 'view_token',
+ 'add_failure',
+ 'delete_user',
+ 'add_success',
+ 'view_ormq',
+ 'view_tokenproxy',
+ 'delete_uisettings',
+ 'change_groupobjectpermission',
+ 'add_logentry',
+ 'add_ormq',
+ 'view_frontendsettings',
+ 'view_schedule',
+ 'change_taskattributes',
+ 'view_documenttype',
+ 'view_logentry',
+ 'change_correspondent',
+ 'add_groupresult',
+ 'delete_groupobjectpermission',
+ 'change_mailrule',
+ 'change_permission',
+ 'delete_log',
+ 'view_userobjectpermission',
+ 'view_correspondent',
+ 'delete_document',
+ 'change_uisettings',
+ 'change_storagepath',
+ 'change_document',
+ 'delete_tokenproxy',
+ 'change_note',
+ 'delete_permission',
+ 'change_contenttype',
+ 'add_token',
+ 'change_success',
+ 'delete_logentry',
+ 'view_savedviewfilterrule',
+ 'delete_task',
+ 'add_savedview',
+ 'add_paperlesstask',
+ 'add_task',
+ 'change_documenttype',
+ 'add_documenttype',
+ 'change_token',
+ 'view_task',
+ 'view_permission',
+ 'change_task',
+ 'delete_userobjectpermission',
+ 'change_group',
+ 'add_group',
+ 'change_tag',
+ 'change_chordcounter',
+ 'add_storagepath',
+ 'delete_group',
+ 'add_taskattributes',
+ 'delete_mailaccount',
+ 'delete_tag',
+ 'add_schedule',
+ 'delete_failure',
+ 'delete_mailrule',
+ 'add_savedviewfilterrule',
+ 'change_ormq',
+ 'change_logentry',
+ 'add_taskresult',
+ 'view_group',
+ 'delete_comment',
+ 'add_contenttype',
+ 'add_document',
+ 'change_tokenproxy',
+ 'delete_success',
+ 'add_comment',
+ 'delete_ormq',
+ 'add_processedmail',
+ 'view_paperlesstask',
+ 'delete_savedview',
+ 'change_user',
+ 'add_session',
+ 'view_groupobjectpermission',
+ 'add_user',
+ 'add_correspondent',
+ 'delete_taskresult',
+ 'view_contenttype',
+ 'view_storagepath',
+ 'add_permission',
+ 'change_userobjectpermission',
+ 'delete_savedviewfilterrule',
+ 'change_groupresult',
+ 'add_chordcounter',
+ 'view_log',
+ 'view_comment',
+ 'change_frontendsettings',
+ ],
+}
+
+// run common tests
+commonAbstractNameFilterPaperlessServiceTests(endpoint, UserService)
+
+describe('Additional service tests for UserService', () => {
+ it('should retain permissions on update', () => {
+ subscription = service.listAll().subscribe()
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ req.flush({
+ results: [user],
+ })
+ subscription.unsubscribe()
+
+ subscription = service.update(user).subscribe()
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/${user.id}/`
+ )
+ expect(req.request.body.user_permissions).toHaveLength(
+ user.user_permissions.length
+ )
+ })
+
+ beforeEach(() => {
+ // Dont need to setup again
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(UserService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { SettingsService } from './settings.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { RouterTestingModule } from '@angular/router/testing'
+import { environment } from 'src/environments/environment'
+import { Subscription } from 'rxjs'
+import { PaperlessUiSettings } from '../data/paperless-uisettings'
+import { SETTINGS_KEYS } from '../data/paperless-uisettings'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { AppModule } from '../app.module'
+
+describe('SettingsService', () => {
+ let httpTestingController: HttpTestingController
+ let settingsService: SettingsService
+ let subscription: Subscription
+
+ const ui_settings: PaperlessUiSettings = {
+ user: {
+ username: 'testuser',
+ first_name: 'Test',
+ last_name: 'User',
+ id: 1,
+ is_superuser: true,
+ },
+ settings: {
+ language: '',
+ bulk_edit: { confirmation_dialogs: true, apply_on_close: false },
+ documentListSize: 50,
+ dark_mode: { use_system: true, enabled: 'false', thumb_inverted: 'true' },
+ theme: { color: '#9fbf2f' },
+ document_details: { native_pdf_viewer: false },
+ date_display: { date_locale: '', date_format: 'mediumDate' },
+ notifications: {
+ consumer_new_documents: true,
+ consumer_success: true,
+ consumer_failed: true,
+ consumer_suppress_on_dashboard: true,
+ },
+ comments_enabled: true,
+ slim_sidebar: false,
+ update_checking: { enabled: false, backend_setting: 'default' },
+ saved_views: { warn_on_unsaved_change: true },
+ notes_enabled: true,
+ tour_complete: false,
+ },
+ permissions: [],
+ }
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [],
+ providers: [SettingsService],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule,
+ NgbModule,
+ FormsModule,
+ ReactiveFormsModule,
+ AppModule,
+ ],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ settingsService = TestBed.inject(SettingsService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+
+ it('calls ui_settings api endpoint on initialize', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('calls ui_settings api endpoint with POST on store', () => {
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ req.flush(ui_settings)
+
+ subscription = settingsService.storeSettings().subscribe()
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ settings: ui_settings.settings,
+ })
+ })
+
+ it('correctly loads settings of various types', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ req.flush(ui_settings)
+
+ expect(settingsService.displayName).toEqual('Test')
+ expect(settingsService.getLanguage()).toEqual('')
+ expect(settingsService.get(SETTINGS_KEYS.DARK_MODE_ENABLED)).toBeFalsy()
+ expect(
+ settingsService.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT)
+ ).toBeTruthy()
+ expect(settingsService.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)).toEqual(50)
+ expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('#9fbf2f')
+ })
+
+ it('correctly allows updating settings of various types', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ req.flush(ui_settings)
+
+ settingsService.setLanguage('de-de')
+ settingsService.set(SETTINGS_KEYS.DARK_MODE_ENABLED, true)
+ settingsService.set(
+ SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT,
+ false
+ )
+ settingsService.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, 25)
+ settingsService.set(SETTINGS_KEYS.THEME_COLOR, '#000000')
+
+ expect(settingsService.getLanguage()).toEqual('de-de')
+ expect(settingsService.get(SETTINGS_KEYS.DARK_MODE_ENABLED)).toBeTruthy()
+ expect(
+ settingsService.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT)
+ ).toBeFalsy()
+ expect(settingsService.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)).toEqual(25)
+ expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('#000000')
+ })
+
+ it('updates appearnce settings', () => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ req.flush(ui_settings)
+
+ expect(
+ document.body.style.getPropertyValue('--pngx-primary-lightness')
+ ).toEqual('')
+
+ const addClassSpy = jest.spyOn(settingsService.renderer, 'addClass')
+ const removeClassSpy = jest.spyOn(settingsService.renderer, 'removeClass')
+
+ settingsService.updateAppearanceSettings(true, true, '#fff000')
+ expect(addClassSpy).toHaveBeenCalledWith(document.body, 'primary-light')
+ expect(addClassSpy).toHaveBeenCalledWith(
+ document.body,
+ 'color-scheme-system'
+ )
+ expect(
+ document.body.style.getPropertyValue('--pngx-primary-lightness')
+ ).toEqual('50%')
+
+ settingsService.updateAppearanceSettings(false, false, '#000000')
+ expect(addClassSpy).toHaveBeenCalledWith(document.body, 'primary-light')
+ expect(removeClassSpy).toHaveBeenCalledWith(
+ document.body,
+ 'color-scheme-system'
+ )
+ expect(
+ document.body.style.getPropertyValue('--pngx-primary-lightness')
+ ).toEqual('0%')
+
+ settingsService.updateAppearanceSettings(false, true, '#ffffff')
+ expect(addClassSpy).toHaveBeenCalledWith(document.body, 'primary-dark')
+ expect(removeClassSpy).toHaveBeenCalledWith(
+ document.body,
+ 'color-scheme-system'
+ )
+ expect(addClassSpy).toHaveBeenCalledWith(document.body, 'color-scheme-dark')
+ expect(
+ document.body.style.getPropertyValue('--pngx-primary-lightness')
+ ).toEqual('100%')
+ })
+
+ it('migrates settings automatically', () => {
+ const oldSettings = Object.assign({}, ui_settings)
+ delete oldSettings.settings['documentListSize']
+ window.localStorage.setItem(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, '50')
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ req.flush(oldSettings)
+
+ req = httpTestingController.match(
+ `${environment.apiBaseUrl}ui_settings/`
+ )[0]
+ expect(req.request.method).toEqual('POST')
+ })
+
+ it('updates settings on complete tour', () => {
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}ui_settings/`
+ )
+ req.flush(ui_settings)
+
+ settingsService.completeTour()
+
+ req = httpTestingController.match(
+ `${environment.apiBaseUrl}ui_settings/`
+ )[0]
+ expect(req.request.method).toEqual('POST')
+ })
+})
providedIn: 'root',
})
export class SettingsService {
- private renderer: Renderer2
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
private settings: Object = {}
public settingsSaved: EventEmitter<any> = new EventEmitter()
+ private _renderer: Renderer2
+ public get renderer(): Renderer2 {
+ return this._renderer
+ }
+
constructor(
rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document,
private savedViewService: SavedViewService,
private permissionsService: PermissionsService
) {
- this.renderer = rendererFactory.createRenderer(null, null)
+ this._renderer = rendererFactory.createRenderer(null, null)
}
// this is called by the app initializer in app.module
themeColor ??= this.get(SETTINGS_KEYS.THEME_COLOR)
if (darkModeUseSystem) {
- this.renderer.addClass(this.document.body, 'color-scheme-system')
- this.renderer.removeClass(this.document.body, 'color-scheme-dark')
+ this._renderer.addClass(this.document.body, 'color-scheme-system')
+ this._renderer.removeClass(this.document.body, 'color-scheme-dark')
} else {
- this.renderer.removeClass(this.document.body, 'color-scheme-system')
+ this._renderer.removeClass(this.document.body, 'color-scheme-system')
darkModeEnabled
- ? this.renderer.addClass(this.document.body, 'color-scheme-dark')
- : this.renderer.removeClass(this.document.body, 'color-scheme-dark')
+ ? this._renderer.addClass(this.document.body, 'color-scheme-dark')
+ : this._renderer.removeClass(this.document.body, 'color-scheme-dark')
}
// remove these in case they were there
- this.renderer.removeClass(this.document.body, 'primary-dark')
- this.renderer.removeClass(this.document.body, 'primary-light')
+ this._renderer.removeClass(this.document.body, 'primary-dark')
+ this._renderer.removeClass(this.document.body, 'primary-light')
if (themeColor) {
const hsl = hexToHsl(themeColor)
const bgBrightnessEstimate = estimateBrightnessForColor(themeColor)
if (bgBrightnessEstimate == BRIGHTNESS.DARK) {
- this.renderer.addClass(this.document.body, 'primary-dark')
- this.renderer.removeClass(this.document.body, 'primary-light')
+ this._renderer.addClass(this.document.body, 'primary-dark')
+ this._renderer.removeClass(this.document.body, 'primary-light')
} else {
- this.renderer.addClass(this.document.body, 'primary-light')
- this.renderer.removeClass(this.document.body, 'primary-dark')
+ this._renderer.addClass(this.document.body, 'primary-light')
+ this._renderer.removeClass(this.document.body, 'primary-dark')
}
- this.renderer.setStyle(
+ this._renderer.setStyle(
document.body,
'--pngx-primary',
`${+hsl.h * 360},${hsl.s * 100}%`,
RendererStyleFlags2.DashCase
)
- this.renderer.setStyle(
+ this._renderer.setStyle(
document.body,
'--pngx-primary-lightness',
`${hsl.l * 100}%`,
RendererStyleFlags2.DashCase
)
} else {
- this.renderer.removeStyle(
+ this._renderer.removeStyle(
document.body,
'--pngx-primary',
RendererStyleFlags2.DashCase
)
- this.renderer.removeStyle(
+ this._renderer.removeStyle(
document.body,
'--pngx-primary-lightness',
RendererStyleFlags2.DashCase
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { TasksService } from './tasks.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { PaperlessTaskType } from '../data/paperless-task'
+import { PaperlessTaskStatus } from '../data/paperless-task'
+
+describe('TasksService', () => {
+ let httpTestingController: HttpTestingController
+ let tasksService: TasksService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [TasksService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ tasksService = TestBed.inject(TasksService)
+ })
+
+ afterEach(() => {
+ httpTestingController.verify()
+ })
+
+ it('calls tasks api endpoint on reload', () => {
+ tasksService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}tasks/`
+ )
+ expect(req.request.method).toEqual('GET')
+ })
+
+ it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
+ tasksService.dismissTasks(new Set([1, 2, 3]))
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}acknowledge_tasks/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ tasks: [1, 2, 3],
+ })
+ req.flush([])
+ // reload is then called
+ httpTestingController.expectOne(`${environment.apiBaseUrl}tasks/`).flush([])
+ })
+
+ it('sorts tasks returned from api', () => {
+ expect(tasksService.total).toEqual(0)
+ const mockTasks = [
+ {
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Complete,
+ acknowledged: false,
+ task_id: '1234',
+ task_file_name: 'file1.pdf',
+ date_created: new Date(),
+ },
+ {
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Failed,
+ acknowledged: false,
+ task_id: '1235',
+ task_file_name: 'file2.pdf',
+ date_created: new Date(),
+ },
+ {
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Pending,
+ acknowledged: false,
+ task_id: '1236',
+ task_file_name: 'file3.pdf',
+ date_created: new Date(),
+ },
+ {
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Started,
+ acknowledged: false,
+ task_id: '1237',
+ task_file_name: 'file4.pdf',
+ date_created: new Date(),
+ },
+ {
+ type: PaperlessTaskType.File,
+ status: PaperlessTaskStatus.Complete,
+ acknowledged: false,
+ task_id: '1238',
+ task_file_name: 'file5.pdf',
+ date_created: new Date(),
+ },
+ ]
+
+ tasksService.reload()
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}tasks/`
+ )
+
+ req.flush(mockTasks)
+
+ expect(tasksService.allFileTasks).toHaveLength(5)
+ expect(tasksService.completedFileTasks).toHaveLength(2)
+ expect(tasksService.failedFileTasks).toHaveLength(1)
+ expect(tasksService.queuedFileTasks).toHaveLength(1)
+ expect(tasksService.startedFileTasks).toHaveLength(1)
+ })
+})
private fileTasks: PaperlessTask[] = []
public get total(): number {
- return this.fileTasks?.length
+ return this.fileTasks.length
}
public get allFileTasks(): PaperlessTask[] {
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { ToastService } from './toast.service'
+
+describe('ToastService', () => {
+ let toastService: ToastService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ToastService],
+ })
+
+ toastService = TestBed.inject(ToastService)
+ })
+
+ it('adds toast on show', () => {
+ const toast = {
+ title: 'Title',
+ content: 'content',
+ delay: 5000,
+ }
+ toastService.show(toast)
+
+ toastService.getToasts().subscribe((toasts) => {
+ expect(toasts).toContainEqual(toast)
+ })
+ })
+
+ it('creates toasts with defaults on showInfo and showError', () => {
+ toastService.showInfo('Info toast')
+ toastService.showError('Error toast')
+
+ toastService.getToasts().subscribe((toasts) => {
+ expect(toasts).toContainEqual({
+ content: 'Info toast',
+ delay: 5000,
+ })
+ expect(toasts).toContainEqual({
+ content: 'Error toast',
+ delay: 10000,
+ })
+ })
+ })
+
+ it('removes toast on close', () => {
+ const toast = {
+ title: 'Title',
+ content: 'content',
+ delay: 5000,
+ }
+ toastService.show(toast)
+ toastService.closeToast(toast)
+
+ toastService.getToasts().subscribe((toasts) => {
+ expect(toasts).toHaveLength(0)
+ })
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { UploadDocumentsService } from './upload-documents.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { HttpEventType, HttpResponse } from '@angular/common/http'
+import {
+ ConsumerStatusService,
+ FileStatusPhase,
+} from './consumer-status.service'
+
+describe('UploadDocumentsService', () => {
+ let httpTestingController: HttpTestingController
+ let uploadDocumentsService: UploadDocumentsService
+ let consumerStatusService: ConsumerStatusService
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [UploadDocumentsService, ConsumerStatusService],
+ imports: [HttpClientTestingModule],
+ })
+
+ httpTestingController = TestBed.inject(HttpTestingController)
+ uploadDocumentsService = TestBed.inject(UploadDocumentsService)
+ consumerStatusService = TestBed.inject(ConsumerStatusService)
+ })
+
+ afterEach(() => {
+ httpTestingController.verify()
+ })
+
+ it('calls post_document api endpoint on upload', () => {
+ const fileEntry = {
+ name: 'file.pdf',
+ isDirectory: false,
+ isFile: true,
+ file: (callback) => {
+ return callback(
+ new File(
+ [new Blob(['testing'], { type: 'application/pdf' })],
+ 'file.pdf'
+ )
+ )
+ },
+ }
+ uploadDocumentsService.uploadFiles([
+ {
+ relativePath: 'path/to/file.pdf',
+ fileEntry,
+ },
+ ])
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/post_document/`
+ )
+ expect(req.request.method).toEqual('POST')
+
+ req.flush('123-456')
+ })
+
+ it('updates progress during upload and failure', () => {
+ const fileEntry = {
+ name: 'file.pdf',
+ isDirectory: false,
+ isFile: true,
+ file: (callback) => {
+ return callback(
+ new File(
+ [new Blob(['testing'], { type: 'application/pdf' })],
+ 'file.pdf'
+ )
+ )
+ },
+ }
+ uploadDocumentsService.uploadFiles([
+ {
+ relativePath: 'path/to/file.pdf',
+ fileEntry,
+ },
+ ])
+
+ expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
+ 1
+ )
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
+ ).toHaveLength(0)
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/post_document/`
+ )
+
+ req.event({
+ type: HttpEventType.UploadProgress,
+ loaded: 100,
+ total: 300,
+ })
+
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
+ ).toHaveLength(1)
+ })
+
+ it('updates progress on failure', () => {
+ const fileEntry = {
+ name: 'file.pdf',
+ isDirectory: false,
+ isFile: true,
+ file: (callback) => {
+ return callback(
+ new File(
+ [new Blob(['testing'], { type: 'application/pdf' })],
+ 'file.pdf'
+ )
+ )
+ },
+ }
+ uploadDocumentsService.uploadFiles([
+ {
+ relativePath: 'path/to/file.pdf',
+ fileEntry,
+ },
+ ])
+
+ let req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/post_document/`
+ )
+
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
+ ).toHaveLength(0)
+
+ req.flush(
+ {},
+ {
+ status: 400,
+ statusText: 'failed',
+ }
+ )
+
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
+ ).toHaveLength(1)
+
+ uploadDocumentsService.uploadFiles([
+ {
+ relativePath: 'path/to/file.pdf',
+ fileEntry,
+ },
+ ])
+
+ req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/post_document/`
+ )
+
+ req.flush(
+ {},
+ {
+ status: 500,
+ statusText: 'failed',
+ }
+ )
+
+ expect(
+ consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
+ ).toHaveLength(2)
+ })
+})
} from './consumer-status.service'
import { DocumentService } from './rest/document.service'
import { Subscription } from 'rxjs'
-import { SettingsService } from './settings.service'
@Injectable({
providedIn: 'root',
constructor(
private documentService: DocumentService,
- private consumerStatusService: ConsumerStatusService,
- private settings: SettingsService
+ private consumerStatusService: ConsumerStatusService
) {}
uploadFiles(files: NgxFileDropEntry[]) {
--- /dev/null
+import {
+ BRIGHTNESS,
+ computeLuminance,
+ estimateBrightnessForColor,
+ hexToHsl,
+ randomColor,
+ rgbToHsl,
+} from './color'
+
+describe('Color Utils', () => {
+ it('should convert hex to hsl', () => {
+ let hsl = hexToHsl('#0000FF')
+ expect(hsl).toEqual({
+ h: 0.6666666666666666,
+ s: 1,
+ l: 0.5,
+ })
+ })
+
+ it('should compute luminance', () => {
+ let luminance = computeLuminance({ r: 0, g: 0, b: 0 })
+ expect(luminance).toEqual(0)
+ luminance = computeLuminance({ r: 255, g: 255, b: 255 })
+ expect(luminance).toEqual(1)
+ luminance = computeLuminance({ r: 128, g: 128, b: 128 })
+ expect(luminance).toBeCloseTo(0.22)
+ })
+
+ it('should estimate brightness', () => {
+ let brightness = estimateBrightnessForColor('#FFFF00') // yellow
+ expect(brightness).toEqual(BRIGHTNESS.LIGHT)
+ brightness = estimateBrightnessForColor('#800000') // maroon
+ expect(brightness).toEqual(BRIGHTNESS.DARK)
+ })
+
+ it('should convert rgb to hsl', () => {
+ let hsl = rgbToHsl(0, 255, 0)
+ expect(hsl).toEqual([0.3333333333333333, 1, 0.5])
+ hsl = rgbToHsl(255, 255, 0)
+ expect(hsl).toEqual([0.16666666666666666, 1, 0.5])
+ hsl = rgbToHsl(0, 0, 255)
+ expect(hsl).toEqual([0.6666666666666666, 1, 0.5])
+ hsl = rgbToHsl(128, 128, 128)
+ expect(hsl).toEqual([0, 0, 0.5019607843137255])
+ })
+
+ it('should return a random color', () => {
+ expect(randomColor()).not.toBeNull()
+ })
+})
--- /dev/null
+import { cloneFilterRules } from './filter-rules'
+import { FilterRule } from '../data/filter-rule'
+import {
+ FILTER_FULLTEXT_QUERY,
+ FILTER_HAS_TAGS_ALL,
+} from '../data/filter-rule-type'
+import { isFullTextFilterRule } from './filter-rules'
+import { filterRulesDiffer } from './filter-rules'
+
+const filterRules: FilterRule[] = [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '9',
+ },
+]
+
+describe('FilterRules Utils', () => {
+ it('should clone filter rules', () => {
+ let rules = cloneFilterRules(filterRules)
+ expect(rules).toEqual(filterRules)
+
+ rules = cloneFilterRules(null)
+ expect(rules).toBeNull()
+ })
+
+ it('should determine if filter rule is a full text rule', () => {
+ const rules = [
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'hello',
+ },
+ ]
+ expect(isFullTextFilterRule(rules)).toBeTruthy()
+ expect(isFullTextFilterRule(filterRules)).toBeFalsy()
+ })
+
+ it('should determine if filter rule sets differ', () => {
+ const rules2 = [
+ {
+ rule_type: FILTER_FULLTEXT_QUERY,
+ value: 'hello',
+ },
+ ]
+ expect(filterRulesDiffer(filterRules, [])).toBeTruthy()
+ expect(filterRulesDiffer(filterRules, rules2)).toBeTruthy()
+ expect(filterRulesDiffer(filterRules, filterRules)).toBeFalsy()
+ })
+})
--- /dev/null
+import { FilterRule } from '../data/filter-rule'
+import {
+ FILTER_FULLTEXT_MORELIKE,
+ FILTER_FULLTEXT_QUERY,
+} from '../data/filter-rule-type'
+
+export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
+ if (filterRules) {
+ let newRules: FilterRule[] = []
+ for (let rule of filterRules) {
+ newRules.push({ rule_type: rule.rule_type, value: rule.value })
+ }
+ return newRules
+ } else {
+ return null
+ }
+}
+
+export function isFullTextFilterRule(filterRules: FilterRule[]): boolean {
+ return (
+ filterRules.find(
+ (r) =>
+ r.rule_type == FILTER_FULLTEXT_QUERY ||
+ r.rule_type == FILTER_FULLTEXT_MORELIKE
+ ) != null
+ )
+}
+
+export function filterRulesDiffer(
+ filterRulesA: FilterRule[],
+ filterRulesB: FilterRule[]
+): boolean {
+ let differ = false
+ if (filterRulesA.length != filterRulesB.length) {
+ differ = true
+ } else {
+ differ = filterRulesA.some((rule) => {
+ return (
+ filterRulesB.find(
+ (fri) => fri.rule_type == rule.rule_type && fri.value == rule.value
+ ) == undefined
+ )
+ })
+ }
+ return differ
+}
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { LocalizedDateParserFormatter } from './ngb-date-parser-formatter'
+import { SettingsService } from '../services/settings.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+
+describe('LocalizedDateParserFormatter', () => {
+ let dateParserFormatter: LocalizedDateParserFormatter
+ let settingsService: SettingsService
+ let httpTestingController: HttpTestingController
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [LocalizedDateParserFormatter, SettingsService],
+ imports: [HttpClientTestingModule],
+ })
+
+ dateParserFormatter = TestBed.inject(LocalizedDateParserFormatter)
+ settingsService = TestBed.inject(SettingsService)
+ httpTestingController = TestBed.inject(HttpTestingController)
+ })
+
+ it('should parse date to struct by locale', () => {
+ let val = dateParserFormatter.parse('5/4/2023')
+ expect(val).toEqual({ day: 4, month: 5, year: 2023 })
+ val = dateParserFormatter.parse('5/4/23')
+ expect(val.day).toEqual(4)
+ expect(val.month).toEqual(5)
+ expect(val.year).toEqual(2023)
+ val = dateParserFormatter.parse('05042023')
+ expect(val.day).toEqual(4)
+ expect(val.month).toEqual(5)
+ expect(val.year).toEqual(2023)
+ val = dateParserFormatter.parse('12/13')
+ expect(val.day).toEqual(13)
+ expect(val.month).toEqual(12)
+ expect(val.year).toEqual(new Date().getFullYear())
+
+ settingsService.setLanguage('de-de') // dd.mm.yyyy
+ val = dateParserFormatter.parse('04.05.2023')
+ expect(val).toEqual({ day: 4, month: 5, year: 2023 })
+ val = dateParserFormatter.parse('04052023')
+ expect(val).toEqual({ day: 4, month: 5, year: 2023 })
+
+ settingsService.setLanguage('tr-tr') // yyyy-mm-dd
+ val = dateParserFormatter.parse('2023-05-04')
+ expect(val).toEqual({ day: 4, month: 5, year: 2023 })
+ val = dateParserFormatter.parse('20230504')
+ expect(val).toEqual({ day: 4, month: 5, year: 2023 })
+ })
+
+ it('should parse date struct to string by locale', () => {
+ const dateStruct = {
+ day: 4,
+ month: 5,
+ year: 2023,
+ }
+ let dateStr = dateParserFormatter.format(dateStruct)
+ expect(dateStr).toEqual('05/04/2023')
+
+ settingsService.setLanguage('de-de') // dd.mm.yyyy
+ dateStr = dateParserFormatter.format(dateStruct)
+ expect(dateStr).toEqual('04.05.2023')
+ })
+})
--- /dev/null
+import { TestBed } from '@angular/core/testing'
+import { ISODateAdapter } from './ngb-iso-date-adapter'
+
+describe('ISODateAdapter', () => {
+ let isoDateAdapter: ISODateAdapter
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [ISODateAdapter],
+ })
+ isoDateAdapter = TestBed.inject(ISODateAdapter)
+ })
+
+ it('should parse ISO date to struct', () => {
+ let val = isoDateAdapter.fromModel('2023-05-04')
+ expect(val.day).toEqual(4)
+ expect(val.month).toEqual(5)
+ expect(val.year).toEqual(2023)
+
+ val = isoDateAdapter.fromModel(null)
+ expect(val).toBeNull()
+
+ val = isoDateAdapter.fromModel('5/4/23')
+ expect(val.day).toEqual(4)
+ expect(val.month).toEqual(5)
+ expect(val.year).toEqual(2023)
+ })
+
+ it('should parse struct to ISO date', () => {
+ let val = isoDateAdapter.toModel({
+ day: 4,
+ month: 5,
+ year: 2023,
+ })
+ expect(val).toEqual('2023-05-04')
+
+ val = isoDateAdapter.toModel(null)
+ expect(val).toBeNull()
+ })
+})
--- /dev/null
+import { convertToParamMap } from '@angular/router'
+import { FilterRule } from '../data/filter-rule'
+import {
+ FILTER_CORRESPONDENT,
+ FILTER_HAS_ANY_TAG,
+ FILTER_HAS_TAGS_ALL,
+} from '../data/filter-rule-type'
+import { paramsToViewState } from './query-params'
+import { paramsFromViewState } from './query-params'
+import { queryParamsFromFilterRules } from './query-params'
+import { filterRulesFromQueryParams } from './query-params'
+
+const tags__id__all = '9'
+const filterRules: FilterRule[] = [
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags__id__all,
+ },
+]
+
+describe('QueryParams Utils', () => {
+ it('should convert view state to params', () => {
+ let params = paramsFromViewState({
+ sortField: 'added',
+ sortReverse: true,
+ currentPage: 2,
+ filterRules,
+ })
+ expect(params).toEqual({
+ sort: 'added',
+ reverse: 1,
+ page: 2,
+ tags__id__all,
+ })
+
+ params = paramsFromViewState({
+ sortField: 'created',
+ sortReverse: false,
+ currentPage: NaN,
+ filterRules: [],
+ })
+ expect(params).toEqual({
+ sort: 'created',
+ reverse: undefined,
+ page: 1,
+ })
+
+ params = paramsFromViewState(
+ {
+ sortField: 'created',
+ sortReverse: false,
+ currentPage: 1,
+ filterRules: [],
+ },
+ true
+ )
+ expect(params).toEqual({
+ page: undefined,
+ })
+ })
+
+ it('should convert params to view state', () => {
+ const params = {
+ sort: 'created',
+ reverse: 1,
+ page: 1,
+ }
+ const state = paramsToViewState(convertToParamMap(params))
+ expect(state).toMatchObject({
+ currentPage: 1,
+ sortField: 'created',
+ sortReverse: true,
+ filterRules: [],
+ })
+ })
+
+ it('should convert params to filter rules', () => {
+ let params = queryParamsFromFilterRules(filterRules)
+ expect(params).toEqual({
+ tags__id__all,
+ })
+
+ params = queryParamsFromFilterRules([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: null,
+ },
+ ])
+ expect(params).toEqual({
+ correspondent__isnull: 1,
+ })
+
+ params = queryParamsFromFilterRules([
+ {
+ rule_type: FILTER_HAS_ANY_TAG,
+ value: 'true',
+ },
+ ])
+ expect(params).toEqual({
+ is_tagged: 1,
+ })
+
+ params = queryParamsFromFilterRules([
+ {
+ rule_type: FILTER_HAS_ANY_TAG,
+ value: 'false',
+ },
+ ])
+ expect(params).toEqual({
+ is_tagged: 0,
+ })
+
+ params = queryParamsFromFilterRules([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags__id__all,
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '14',
+ },
+ ])
+ expect(params).toEqual({
+ tags__id__all: tags__id__all + ',14',
+ })
+
+ params = queryParamsFromFilterRules(null)
+ expect(params).toBeNull()
+ })
+
+ it('should convert filter rules to query params', () => {
+ let rules = filterRulesFromQueryParams(
+ convertToParamMap({
+ tags__id__all,
+ })
+ )
+ expect(rules).toEqual([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags__id__all,
+ },
+ ])
+
+ rules = filterRulesFromQueryParams(
+ convertToParamMap({
+ tags__id__all: tags__id__all + ',13',
+ })
+ )
+ expect(rules).toEqual([
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: tags__id__all,
+ },
+ {
+ rule_type: FILTER_HAS_TAGS_ALL,
+ value: '13',
+ },
+ ])
+
+ rules = filterRulesFromQueryParams(
+ convertToParamMap({
+ correspondent__id: '12',
+ })
+ )
+ expect(rules).toEqual([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: '12',
+ },
+ ])
+
+ rules = filterRulesFromQueryParams(
+ convertToParamMap({
+ is_tagged: 'true',
+ })
+ )
+ expect(rules).toEqual([
+ {
+ rule_type: FILTER_HAS_ANY_TAG,
+ value: 'true',
+ },
+ ])
+
+ rules = filterRulesFromQueryParams(
+ convertToParamMap({
+ correspondent__isnull: '1',
+ })
+ )
+ expect(rules).toEqual([
+ {
+ rule_type: FILTER_CORRESPONDENT,
+ value: null,
+ },
+ ])
+ })
+})
params[PAGE_PARAMETER] = isNaN(viewState.currentPage)
? 1
: viewState.currentPage
- if (pageOnly && viewState.currentPage == 1) params[PAGE_PARAMETER] = null
+ if (pageOnly && viewState.currentPage == 1) params[PAGE_PARAMETER] = undefined
return params
}