frontend unit tests

toasts component testing

conditional import of angular setup-jest for vscode-jest support

Update jest.config.js

Create open-documents.service.spec.ts

Add unit tests for all REST services

settings service test

Remove component from settings service test

Create permissions.service.spec.ts

upload documents service tests

Update package.json

Create toast.service.spec.ts

Tasks service test

Statistics widget component tests

Update permissions.service.ts

Create app.component.spec.ts

settings component testing

tasks component unit testing

Management list component generic tests

Some management component tests

document notes component unit tests

Create document-list.component.spec.ts

Create save-view-config-dialog.component.spec.ts

Create filter-editor.component.spec.ts

small and large document cards unit testing

Create bulk-editor.component.spec.ts

document detail unit tests

saving work on documentdetail component spec

Create document-asn.component.spec.ts

dashboard & widgets unit testing

Fix ResizeObserver mock

common component unit tests

fix some merge errors

Update app-frame.component.spec.ts

Create page-header.component.spec.ts

input component unit tests

FilterableDropdownComponent unit testing

and found minor errors

update taskservice unit tests

Edit dialogs unit tests

Create date-dropdown.component.spec.ts

Remove selectors from guard tests

confirm dialog component tests

app frame component test

Miscellaneous component tests

Update document-list-view.service.spec.ts

directives unit tests

Remove unused resizeobserver mock

guard unit tests

Update query-params.spec.ts

try to fix flaky playwright

filter rules utils & testing

Interceptor unit tests

Pipes unit testing

Utils unit tests

Update upload-documents.service.spec.ts

consumer status service tests

Update setup-jest.ts

Create document-list-view.service.spec.ts

Update app-routing.module.ts
This commit is contained in:
shamoon
2023-05-23 15:02:54 -07:00
parent 0f9c642f0f
commit 181673c9a3
145 changed files with 14832 additions and 169 deletions

View File

@@ -0,0 +1,70 @@
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"?'
)
})
})

View File

@@ -0,0 +1,68 @@
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"?')
})
})

View File

@@ -0,0 +1,71 @@
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)
})
})

View File

@@ -0,0 +1,232 @@
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()
})
})

View File

@@ -28,7 +28,10 @@ import {
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 {
@@ -135,7 +138,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
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(
@@ -156,7 +159,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
backdrop: 'static',
})
activeModal.componentInstance.object = object
activeModal.componentInstance.dialogMode = 'edit'
activeModal.componentInstance.dialogMode = EditDialogMode.EDIT
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
@@ -172,9 +175,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
})
}
getDeleteMessage(object: T) {
return $localize`Do you really want to delete the ${this.typeName}?`
}
abstract getDeleteMessage(object: T)
filterDocuments(object: ObjectWithId) {
this.documentListViewService.quickFilter([

View File

@@ -0,0 +1,484 @@
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')
})
})

View File

@@ -44,6 +44,7 @@ import { MailAccountService } from 'src/app/services/rest/mail-account.service'
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,
@@ -225,9 +226,9 @@ export class SettingsComponent
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
@@ -579,8 +580,8 @@ export class SettingsComponent
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()
}
@@ -646,7 +647,9 @@ export class SettingsComponent
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))
@@ -718,7 +721,9 @@ export class SettingsComponent
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))
@@ -780,7 +785,9 @@ export class SettingsComponent
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))
@@ -842,7 +849,9 @@ export class SettingsComponent
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))

View File

@@ -0,0 +1,68 @@
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"?'
)
})
})

View File

@@ -0,0 +1,70 @@
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"?'
)
})
})

View File

@@ -107,25 +107,25 @@
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed&nbsp;<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&nbsp;<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&nbsp;<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&nbsp;<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>

View File

@@ -0,0 +1,272 @@
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,
])
})
})

View File

@@ -1,6 +1,6 @@
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'
@@ -51,8 +51,8 @@ export class TasksComponent
}
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, {
@@ -91,7 +91,7 @@ export class TasksComponent
}
get currentTasks(): PaperlessTask[] {
let tasks: PaperlessTask[]
let tasks: PaperlessTask[] = []
switch (this.activeTab) {
case 'queued':
tasks = this.tasksService.queuedFileTasks
@@ -105,8 +105,6 @@ export class TasksComponent
case 'failed':
tasks = this.tasksService.failedFileTasks
break
default:
break
}
return tasks
}