paperless-ngx/src-ui/src/app/components/document-detail/document-detail.component.spec.ts
shamoon 10729f0362 Feature: Implement custom fields for documents (#4502)
Adds custom fields of certain data types, attachable to documents and searchable

Co-Authored-By: Trenton H <797416+stumpylog@users.noreply.github.com>
2023-11-05 17:27:23 -08:00

926 lines
32 KiB
TypeScript

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'
import { ShareLinksDropdownComponent } from '../common/share-links-dropdown/share-links-dropdown.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
import { PaperlessCustomFieldDataType } from 'src/app/data/paperless-custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
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,
},
],
custom_fields: [
{
field: 0,
document: 3,
created: new Date(),
value: 'custom foo bar',
},
],
}
const customFields = [
{
id: 0,
name: 'Field 1',
data_type: PaperlessCustomFieldDataType.String,
created: new Date(),
},
{
id: 1,
name: 'Custom Field 2',
data_type: PaperlessCustomFieldDataType.Integer,
created: new Date(),
},
]
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 customFieldsService: CustomFieldsService
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,
ShareLinksDropdownComponent,
CustomFieldsDropdownComponent,
],
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',
},
],
}),
},
},
CustomFieldsService,
{
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)
customFieldsService = TestBed.inject(CustomFieldsService)
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)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: customFields.length,
all: customFields.map((f) => f.id),
results: customFields,
})
)
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'], { replaceUrl: true })
})
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'], { replaceUrl: true })
})
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(true)
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
})
it('should support save without 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).not.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')
const error = new Error('failed to save')
updateSpy.mockImplementation(() => throwError(() => error))
component.save()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Error saving document', error)
})
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(true)
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 save & next 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')
const error = new Error('failed to save')
updateSpy.mockImplementation(() => throwError(() => error))
component.saveEditNext()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Error saving document', error)
})
it('should show save button and save & close or save & next', () => {
const nextSpy = jest.spyOn(component, 'hasNext')
nextSpy.mockReturnValueOnce(false)
fixture.detectChanges()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save')
).not.toBeUndefined()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & close')
).not.toBeUndefined()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & next')
).toBeUndefined()
nextSpy.mockReturnValue(true)
fixture.detectChanges()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & close')
).toBeUndefined()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & next')
).not.toBeUndefined()
})
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().toISOString(),
} 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', error)
})
it('should display custom fields', () => {
initNormally()
expect(fixture.debugElement.nativeElement.textContent).toContain(
customFields[0].name
)
})
it('should support add custom field, correctly send via post', () => {
initNormally()
const initialLength = doc.custom_fields.length
expect(component.customFieldFormFields).toHaveLength(initialLength)
component.addField(customFields[1])
fixture.detectChanges()
expect(component.document.custom_fields).toHaveLength(initialLength + 1)
expect(component.customFieldFormFields).toHaveLength(initialLength + 1)
expect(fixture.debugElement.nativeElement.textContent).toContain(
customFields[1].name
)
const updateSpy = jest.spyOn(documentService, 'update')
component.save(true)
expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(2)
expect(updateSpy.mock.lastCall[0].custom_fields[1]).toEqual({
field: customFields[1].id,
value: null,
})
})
it('should support remove custom field, correctly send via post', () => {
initNormally()
const initialLength = doc.custom_fields.length
expect(component.customFieldFormFields).toHaveLength(initialLength)
component.removeField(doc.custom_fields[0])
fixture.detectChanges()
expect(component.document.custom_fields).toHaveLength(initialLength - 1)
expect(component.customFieldFormFields).toHaveLength(initialLength - 1)
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Field 1'
)
const updateSpy = jest.spyOn(documentService, 'update')
component.save(true)
expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(
initialLength - 1
)
})
it('should show custom field errors', () => {
initNormally()
component.error = {
custom_fields: [
{},
{},
{ value: ['This field may not be null.'] },
{},
{ non_field_errors: ['Enter a valid URL.'] },
],
}
expect(component.getCustomFieldError(2)).toEqual([
'This field may not be null.',
])
expect(component.getCustomFieldError(4)).toEqual(['Enter a valid URL.'])
})
it('should refresh custom fields when created', () => {
initNormally()
const refreshSpy = jest.spyOn(component, 'refreshCustomFields')
fixture.debugElement
.query(By.directive(CustomFieldsDropdownComponent))
.triggerEventHandler('created')
expect(refreshSpy).toHaveBeenCalled()
})
function initNormally() {
jest
.spyOn(documentService, 'get')
.mockReturnValueOnce(of(Object.assign({}, doc)))
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(null)
jest
.spyOn(openDocumentsService, 'openDocument')
.mockReturnValueOnce(of(true))
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: customFields.length,
all: customFields.map((f) => f.id),
results: customFields,
})
)
fixture.detectChanges()
}
})