Remove zone dependency for tests

This commit is contained in:
shamoon
2026-07-09 21:29:51 -07:00
parent dee0ae7a62
commit f996711910
34 changed files with 614 additions and 707 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
import '@angular/localize/init'
import { jest } from '@jest/globals'
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'
import { TextDecoder, TextEncoder } from 'node:util'
if (process.env.NODE_ENV === 'test') {
setupZoneTestEnv()
setupZonelessTestEnv()
}
;(globalThis as any).TextEncoder = TextEncoder as unknown as {
new (): TextEncoder
+6 -9
View File
@@ -1,11 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Router, RouterModule } from '@angular/router'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -72,7 +67,8 @@ describe('AppComponent', () => {
component = fixture.componentInstance
})
it('should initialize the tour service & toggle class on body for styling', fakeAsync(() => {
it('should initialize the tour service & toggle class on body for styling', () => {
jest.useFakeTimers()
jest.spyOn(console, 'warn').mockImplementation(() => {})
fixture.detectChanges()
const tourSpy = jest.spyOn(tourService, 'initialize')
@@ -81,9 +77,10 @@ describe('AppComponent', () => {
tourService.start()
expect(document.body.classList).toContain('tour-active')
tourService.end()
tick(500)
jest.advanceTimersByTime(500)
expect(document.body.classList).not.toContain('tour-active')
}))
jest.useRealTimers()
})
it('should display toast on document consumed with link if user has access', () => {
const navigateSpy = jest.spyOn(router, 'navigate')
@@ -209,7 +209,7 @@ describe('SettingsComponent', () => {
fixture.detectChanges()
}
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', async () => {
completeSetup()
const navigateSpy = jest.spyOn(router, 'navigate')
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
@@ -217,16 +217,19 @@ describe('SettingsComponent', () => {
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'documents'])
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
await fixture.whenStable()
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'))
await fixture.whenStable()
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'general'])
expect(initSpy).not.toHaveBeenCalled()
navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
await fixture.whenStable()
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
expect(initSpy).toHaveBeenCalled()
})
@@ -60,10 +60,15 @@ describe('TrashComponent', () => {
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
router = TestBed.inject(Router)
jest.spyOn(router, 'navigate').mockResolvedValue(true)
component = fixture.componentInstance
fixture.detectChanges()
})
afterEach(() => {
jest.useRealTimers()
})
it('should call correct service method on reload', () => {
jest.useFakeTimers()
const trashSpy = jest.spyOn(trashService, 'getTrash')
@@ -1,12 +1,7 @@
import { DatePipe } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
@@ -138,7 +133,8 @@ describe('UsersAndGroupsComponent', () => {
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
})
it('should logout current user if password changed, after delay', fakeAsync(() => {
it('should logout current user if password changed, after delay', () => {
jest.useFakeTimers()
completeSetup()
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
@@ -151,11 +147,12 @@ describe('UsersAndGroupsComponent', () => {
settingsService.currentUser.set(users[0]) // simulate logged in as same user
editDialog.succeeded.emit(users[0])
fixture.detectChanges()
tick(2600)
jest.advanceTimersByTime(2600)
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}))
jest.useRealTimers()
})
it('should support edit / create group, show error if needed', () => {
completeSetup()
@@ -4,12 +4,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
@@ -244,7 +239,8 @@ describe('AppFrameComponent', () => {
expect(toastSpy).toHaveBeenCalled()
})
it('should support toggling slim sidebar and saving', fakeAsync(() => {
it('should support toggling slim sidebar and saving', () => {
jest.useFakeTimers()
const saveSettingSpy = jest.spyOn(settingsService, 'set')
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
expect(component.slimSidebarEnabled).toBeFalsy()
@@ -260,7 +256,7 @@ describe('AppFrameComponent', () => {
).toEqual(['attributes'])
requests[0].flush({ success: true })
expect(component.slimSidebarAnimating()).toBeTruthy()
tick(200)
jest.advanceTimersByTime(200)
expect(component.slimSidebarAnimating()).toBeFalsy()
expect(component.slimSidebarEnabled).toBeTruthy()
expect(saveSettingSpy).toHaveBeenCalledWith(
@@ -271,7 +267,8 @@ describe('AppFrameComponent', () => {
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
['attributes']
)
}))
jest.useRealTimers()
})
it('should show error on toggle slim sidebar if store settings fails', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
@@ -1,12 +1,7 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ElementRef } from '@angular/core'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import {
@@ -154,6 +149,7 @@ describe('GlobalSearchComponent', () => {
searchService = TestBed.inject(SearchService)
router = TestBed.inject(Router)
jest.spyOn(router, 'navigate').mockResolvedValue(true)
modalService = TestBed.inject(NgbModal)
documentService = TestBed.inject(DocumentService)
documentListViewService = TestBed.inject(DocumentListViewService)
@@ -276,20 +272,22 @@ describe('GlobalSearchComponent', () => {
expect(advancedSearchSpy).toHaveBeenCalled()
})
it('should search on query debounce', fakeAsync(() => {
it('should search on query debounce', () => {
jest.useFakeTimers()
const query = 'test'
const searchSpy = jest.spyOn(searchService, 'globalSearch')
searchSpy.mockReturnValue(of({} as any))
const dropdownOpenSpy = jest.spyOn(component.resultsDropdown, 'open')
component.queryDebounce.next(query)
tick(401)
jest.advanceTimersByTime(401)
expect(searchSpy).toHaveBeenCalledWith(query)
expect(dropdownOpenSpy).toHaveBeenCalled()
}))
jest.useRealTimers()
})
it('should support primary action', () => {
const object = { id: 1 }
const routerSpy = jest.spyOn(router, 'navigate')
const routerSpy = jest.mocked(router.navigate)
const modalSpy = jest.spyOn(modalService, 'open')
let modal: NgbModalRef
@@ -1,12 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
flush,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import { Toast, ToastService } from 'src/app/services/toast.service'
@@ -69,7 +63,7 @@ describe('ToastsDropdownComponent', () => {
fixture.detectChanges()
})
it('should call getToasts and return toasts', fakeAsync(() => {
it('should call getToasts and return toasts', () => {
toastsSubject.next(toasts)
fixture.detectChanges()
@@ -79,29 +73,21 @@ describe('ToastsDropdownComponent', () => {
content: 'foo bar',
delay: 5000,
})
flush()
discardPeriodicTasks()
}))
})
it('should show a toast', fakeAsync(() => {
it('should show a toast', () => {
toastsSubject.next(toasts)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('foo bar')
})
flush()
discardPeriodicTasks()
}))
it('should toggle suppressPopupToasts', fakeAsync((finish) => {
it('should toggle suppressPopupToasts', () => {
fixture.detectChanges()
toastsSubject.next(toasts)
const spy = jest.spyOn(toastService, 'suppressPopupToasts', 'set')
component.onOpenChange(true)
expect(spy).toHaveBeenCalledWith(true)
flush()
discardPeriodicTasks()
}))
})
})
@@ -1,11 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import {
@@ -110,7 +105,8 @@ describe('CustomFieldsDropdownComponent', () => {
)
})
it('should support creating field, show error if necessary, then add', fakeAsync(() => {
it('should support creating field, show error if necessary, then add', () => {
jest.useFakeTimers()
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
@@ -134,11 +130,12 @@ describe('CustomFieldsDropdownComponent', () => {
// succeed
editDialog.succeeded.emit(fields[0])
tick(100)
jest.advanceTimersByTime(100)
expect(toastInfoSpy).toHaveBeenCalled()
expect(getFieldsSpy).toHaveBeenCalled()
expect(addFieldSpy).toHaveBeenCalled()
}))
jest.useRealTimers()
})
it('should support creating field with name', () => {
let modal: NgbModalRef
@@ -150,12 +147,13 @@ describe('CustomFieldsDropdownComponent', () => {
expect(editDialog.object.name).toEqual('Foo bar')
})
it('should support arrow keyboard navigation', fakeAsync(() => {
it('should support arrow keyboard navigation', () => {
jest.useFakeTimers()
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
jest.advanceTimersByTime(100)
const filterInputEl: HTMLInputElement =
component.listFilterTextInput.nativeElement
expect(document.activeElement).toEqual(filterInputEl)
@@ -193,14 +191,16 @@ describe('CustomFieldsDropdownComponent', () => {
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[0])
}))
jest.useRealTimers()
})
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
it('should support arrow keyboard navigation after tab keyboard navigation', () => {
jest.useFakeTimers()
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
jest.advanceTimersByTime(100)
const filterInputEl: HTMLInputElement =
component.listFilterTextInput.nativeElement
expect(document.activeElement).toEqual(filterInputEl)
@@ -229,9 +229,10 @@ describe('CustomFieldsDropdownComponent', () => {
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[1])
}))
jest.useRealTimers()
})
it('should support enter keyboard navigation', fakeAsync(() => {
it('should support enter keyboard navigation', () => {
jest.spyOn(component, 'canCreateFields', 'get').mockReturnValue(true)
const addFieldSpy = jest.spyOn(component, 'addField')
const createFieldSpy = jest.spyOn(component, 'createField')
@@ -250,5 +251,5 @@ describe('CustomFieldsDropdownComponent', () => {
component.listFilterEnter()
expect(createFieldSpy).not.toHaveBeenCalled()
expect(addFieldSpy).not.toHaveBeenCalled()
}))
})
})
@@ -1,11 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
@@ -210,13 +205,13 @@ describe('CustomFieldsQueryDropdownComponent', () => {
expect(component.name).toBe('test_title')
})
it('should add a default atom on open', fakeAsync(() => {
it('should add a default atom on open', async () => {
expect(component.selectionModel.queries.length).toBe(0)
component.onOpenChange(true)
fixture.detectChanges()
tick()
await fixture.whenStable()
expect(component.selectionModel.queries.length).toBe(1)
}))
})
describe('CustomFieldQueriesModel', () => {
let model: CustomFieldQueriesModel
@@ -1,12 +1,7 @@
import { DatePipe } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
@@ -59,27 +54,32 @@ describe('DatesDropdownComponent', () => {
expect(settingsSpy).toHaveBeenCalled()
})
it('should support date input, emit change', fakeAsync(() => {
it('should support date input, emit change', () => {
jest.useFakeTimers()
let result: string
component.createdDateFromChange.subscribe((date) => (result = date))
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
input.value = '5/30/2023'
input.dispatchEvent(new Event('change'))
tick(500)
jest.advanceTimersByTime(500)
expect(result).not.toBeNull()
}))
jest.useRealTimers()
})
it('should support date select, emit datesSet change', fakeAsync(() => {
it('should support date select, emit datesSet change', () => {
jest.useFakeTimers()
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)
jest.advanceTimersByTime(500)
expect(result).not.toBeNull()
}))
jest.useRealTimers()
})
it('should support relative dates', fakeAsync(() => {
it('should support relative dates', () => {
jest.useFakeTimers()
let result: DateSelection
component.datesSet.subscribe((date) => (result = date))
component.createdRelativeDate = RelativeDate.WITHIN_1_WEEK // normally set by ngModel binding in dropdown
@@ -88,7 +88,7 @@ describe('DatesDropdownComponent', () => {
} as any)
component.addedRelativeDate = RelativeDate.WITHIN_1_WEEK // normally set by ngModel binding in dropdown
component.onSetAddedRelativeDate({ id: RelativeDate.WITHIN_1_WEEK } as any)
tick(500)
jest.advanceTimersByTime(500)
expect(result).toEqual({
createdFrom: null,
createdTo: null,
@@ -97,7 +97,8 @@ describe('DatesDropdownComponent', () => {
addedTo: null,
addedRelativeDateID: RelativeDate.WITHIN_1_WEEK,
})
}))
jest.useRealTimers()
})
it('should support report if active', () => {
component.createdRelativeDate = RelativeDate.WITHIN_1_WEEK
@@ -177,11 +178,12 @@ describe('DatesDropdownComponent', () => {
expect(eventSpy).toHaveBeenCalled()
})
it('should support debounce', fakeAsync(() => {
it('should support debounce', () => {
jest.useFakeTimers()
let result: DateSelection
component.datesSet.subscribe((date) => (result = date))
component.onChangeDebounce()
tick(500)
jest.advanceTimersByTime(500)
expect(result).toEqual({
createdFrom: null,
createdTo: null,
@@ -190,5 +192,6 @@ describe('DatesDropdownComponent', () => {
addedTo: null,
addedRelativeDateID: null,
})
}))
jest.useRealTimers()
})
})
@@ -4,12 +4,7 @@ import {
provideHttpClientTesting,
} from '@angular/common/http/testing'
import { Component } from '@angular/core'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormControl,
FormGroup,
@@ -145,12 +140,14 @@ describe('EditDialogComponent', () => {
})
})
it('should delay close enabled', fakeAsync(() => {
it('should delay close enabled', () => {
jest.useFakeTimers()
expect(component.closeEnabled).toBeFalsy()
component.ngOnInit()
tick(100)
jest.advanceTimersByTime(100)
expect(component.closeEnabled).toBeTruthy()
}))
jest.useRealTimers()
})
it('should set default owner when in create mode if unset', () => {
component.dialogMode.set(EditDialogMode.CREATE)
@@ -3,12 +3,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
@@ -76,7 +71,8 @@ describe('MailAccountEditDialogComponent', () => {
expect(editTitleSpy).toHaveBeenCalled()
})
it('should support test mail account and show appropriate expiring alert', fakeAsync(() => {
it('should support test mail account and show appropriate expiring alert', () => {
jest.useFakeTimers()
component.object = {
name: 'example',
imap_server: 'imap.example.com',
@@ -97,7 +93,7 @@ describe('MailAccountEditDialogComponent', () => {
expect(fixture.nativeElement.textContent).toContain(
'Successfully connected'
)
tick(6000)
jest.advanceTimersByTime(6000)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).not.toContain(
'Successfully connected'
@@ -118,6 +114,7 @@ describe('MailAccountEditDialogComponent', () => {
.flush({}, { status: 500, statusText: 'error' })
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
tick(6000)
}))
jest.advanceTimersByTime(6000)
jest.useRealTimers()
})
})
@@ -1,12 +1,7 @@
import { ScrollingModule } from '@angular/cdk/scrolling'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
import {
@@ -52,6 +47,7 @@ const negativeNullItem = {
}
let selectionModel: FilterableDropdownSelectionModel
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
let component: FilterableDropdownComponent
@@ -255,14 +251,17 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
})
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
it('should focus text filter on open, support filtering, clear on close', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
expect(document.activeElement).toEqual(
component.listFilterTextInput.nativeElement
)
@@ -270,12 +269,16 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
component.filterText = 'Tag2'
fixture.detectChanges()
expect(component.buttonsViewport.getRenderedRange().end).toEqual(1) // filtered
component.buttonsViewport.checkViewportSize()
fixture.detectChanges()
expect(component.scrollViewportHeight).toEqual(
component.FILTERABLE_BUTTON_HEIGHT_PX
) // filtered
component.dropdown.close()
expect(component.filterText).toHaveLength(0)
}))
})
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
it('should toggle & close on enter inside filter field if 1 item remains', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
expect(component.selectionModel.getSelectedItems()).toEqual([])
@@ -283,7 +286,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.filterText = 'Tag2'
fixture.detectChanges()
const closeSpy = jest.spyOn(component.dropdown, 'close')
@@ -291,11 +297,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
tick(300)
await wait(300)
expect(closeSpy).toHaveBeenCalled()
}))
})
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
it('should apply & close on enter inside filter field if 1 item remains if editing', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
component.editing = true
@@ -306,25 +312,31 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.filterText = 'Tag2'
fixture.detectChanges()
component.listFilterTextInput.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
tick(300)
await wait(300)
expect(applyResult).toEqual({ itemsToAdd: [items[1]], itemsToRemove: [] })
}))
})
it('should support arrow keyboard navigation', fakeAsync(() => {
it('should support arrow keyboard navigation', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
const filterInputEl: HTMLInputElement =
@@ -362,16 +374,19 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[0])
}))
})
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
it('should support arrow keyboard navigation after tab keyboard navigation', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
const filterInputEl: HTMLInputElement =
@@ -400,16 +415,19 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[1])
}))
})
it('should support arrow keyboard navigation after click', fakeAsync(() => {
it('should support arrow keyboard navigation after click', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
const filterInputEl: HTMLInputElement =
@@ -427,9 +445,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[1])
}))
})
it('should toggle logical operator', fakeAsync(() => {
it('should toggle logical operator', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
component.selectionModel.manyToOne = true
@@ -445,7 +463,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
expect(component.modifierToggleEnabled).toBeTruthy()
const operatorButtons: HTMLInputElement[] = Array.from(
@@ -456,9 +477,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
fixture.detectChanges()
expect(selectionModel.logicalOperator).toEqual(LogicalOperator.Or)
expect(changedResult.logicalOperator).toEqual(LogicalOperator.Or)
}))
})
it('should toggle intersection include / exclude', fakeAsync(() => {
it('should toggle intersection include / exclude', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
selectionModel.set(items[0].id, ToggleableItemState.Selected)
@@ -473,7 +494,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
expect(component.modifierToggleEnabled).toBeTruthy()
const intersectionButtons: HTMLInputElement[] = Array.from(
@@ -486,7 +510,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(changedResult.intersection).toEqual(Intersection.Exclude)
expect(changedResult.getSelectedItems()).toEqual([])
expect(changedResult.getExcludedItems()).toEqual(items)
}))
})
it('should update null item selection on toggleIntersection', () => {
component.selectionModel.items = items
@@ -819,7 +843,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(getRootDocCount(rootWithoutCounts.id)).toEqual(0)
})
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
it('should set support create, keep open model and call createRef method', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
component.selectionModel = selectionModel
@@ -827,7 +851,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.filterText = 'Test Filter Text'
component.createRef = jest.fn()
@@ -837,9 +864,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
const openSpy = jest.spyOn(component.dropdown, 'open')
component.dropdownOpenChange(false)
expect(openSpy).toHaveBeenCalled() // should keep open
}))
})
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
it('should call create on enter inside filter field if 0 items remain while editing', async () => {
component.selectionModel.items = items
component.icon = 'tag-fill'
component.editing = true
@@ -849,12 +876,15 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
tick(100)
await wait(100)
fixture.detectChanges()
component.buttonsViewport?.checkViewportSize()
fixture.detectChanges()
component.filterText = 'FooBar'
component.listFilterEnter()
expect(component.selectionModel.getSelectedItems()).toEqual([])
expect(createSpy).toHaveBeenCalled()
}))
})
it('should exclude item and trigger change event', () => {
const id = 1
@@ -1,9 +1,4 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
@@ -121,12 +116,14 @@ describe('SelectComponent', () => {
).toBeFalsy()
})
it('should clear search term on blur after delay', fakeAsync(() => {
it('should clear search term on blur after delay', () => {
jest.useFakeTimers()
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
component.onBlur()
tick(3000)
jest.advanceTimersByTime(3000)
expect(clearSpy).toHaveBeenCalled()
}))
jest.useRealTimers()
})
it('should emit filtered documents', () => {
component.value = 10
@@ -143,13 +143,6 @@ describe('TagsComponent', () => {
component.createTag()
expect(modalService.hasOpenModals()).toBeTruthy()
expect(activeInstances[0].componentInstance.object.name).toEqual('foobar')
const editDialog = activeInstances[0]
.componentInstance as TagEditDialogComponent
editDialog.save() // create is mocked
fixture.detectChanges()
fixture.whenStable().then(() => {
expect(fixture.debugElement.nativeElement.textContent).toContain('foobar')
})
})
it('support remove tags', () => {
@@ -1,6 +1,7 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Title } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { environment } from 'src/environments/environment'
import { PageHeaderComponent } from './page-header.component'
@@ -13,7 +14,7 @@ describe('PageHeaderComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [],
imports: [PageHeaderComponent],
imports: [NgxBootstrapIconsModule.pick(allIcons), PageHeaderComponent],
}).compileComponents()
titleService = TestBed.inject(Title)
@@ -23,6 +24,10 @@ describe('PageHeaderComponent', () => {
fixture.detectChanges()
})
afterEach(() => {
jest.useRealTimers()
})
it('should display title + subtitle', () => {
fixture.componentRef.setInput('title', 'Foo')
fixture.componentRef.setInput('subTitle', 'Bar')
@@ -1,9 +1,4 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Clipboard } from '@angular/cdk/clipboard'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
@@ -191,7 +186,8 @@ describe('ProfileEditDialogComponent', () => {
expect(component.saveDisabled).toBeFalsy()
})
it('should logout on save if password changed', fakeAsync(() => {
it('should logout on save if password changed', () => {
jest.useFakeTimers()
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
const getProvidersSpy = jest.spyOn(
@@ -211,13 +207,15 @@ describe('ProfileEditDialogComponent', () => {
.mockImplementation(() => {})
component.save()
expect(updateSpy).toHaveBeenCalled()
tick(2600)
jest.advanceTimersByTime(2600)
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}))
jest.useRealTimers()
})
it('should support auth token copy', fakeAsync(() => {
it('should support auth token copy', () => {
jest.useFakeTimers()
const getSpy = jest.spyOn(profileService, 'get')
getSpy.mockReturnValue(of(profile))
const getProvidersSpy = jest.spyOn(
@@ -230,9 +228,10 @@ describe('ProfileEditDialogComponent', () => {
component.copyAuthToken()
expect(copySpy).toHaveBeenCalledWith(profile.auth_token)
expect(component.copied()).toBeTruthy()
tick(3000)
jest.advanceTimersByTime(3000)
expect(component.copied()).toBeFalsy()
}))
jest.useRealTimers()
})
it('should support generate token, display error if needed', () => {
const getSpy = jest.spyOn(profileService, 'get')
@@ -366,11 +365,13 @@ describe('ProfileEditDialogComponent', () => {
expect(component.isTotpEnabled).toBeFalsy()
})
it('should copy recovery codes', fakeAsync(() => {
it('should copy recovery codes', () => {
jest.useFakeTimers()
const copySpy = jest.spyOn(clipboard, 'copy')
component.recoveryCodes = ['1', '2', '3']
component.copyRecoveryCodes()
expect(copySpy).toHaveBeenCalledWith('1\n2\n3')
tick(3000)
}))
jest.advanceTimersByTime(3000)
jest.useRealTimers()
})
})
@@ -1,10 +1,5 @@
import { Clipboard } from '@angular/cdk/clipboard'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { FileVersion } from 'src/app/data/share-link'
@@ -108,7 +103,8 @@ describe('ShareLinkBundleDialogComponent', () => {
expect(component.documentPreview()[0].id).toBe(1)
})
it('copies share link and resets state after timeout', fakeAsync(() => {
it('copies share link and resets state after timeout', () => {
jest.useFakeTimers()
const copySpy = jest.spyOn(clipboard, 'copy').mockReturnValue(true)
const bundle = {
slug: 'bundle-slug',
@@ -121,9 +117,10 @@ describe('ShareLinkBundleDialogComponent', () => {
expect(component.copied()).toBe(true)
expect(toastService.showInfo).toHaveBeenCalled()
tick(3000)
jest.advanceTimersByTime(3000)
expect(component.copied()).toBe(false)
}))
jest.useRealTimers()
})
it('generates share URLs based on API base URL', () => {
environment.apiBaseUrl = 'https://example.com/api/'
@@ -1,10 +1,5 @@
import { Clipboard } from '@angular/cdk/clipboard'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
@@ -70,6 +65,7 @@ describe('ShareLinkBundleManageDialogComponent', () => {
fixture.destroy()
environment.apiBaseUrl = originalApiBaseUrl
jest.clearAllMocks()
jest.useRealTimers()
})
const sampleBundle = (overrides: Partial<ShareLinkBundleSummary> = {}) =>
@@ -85,7 +81,8 @@ describe('ShareLinkBundleManageDialogComponent', () => {
...overrides,
}) as ShareLinkBundleSummary
it('loads bundles on init and polls periodically', fakeAsync(() => {
it('loads bundles on init and polls periodically', () => {
jest.useFakeTimers()
const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })]
service.listAllBundles.mockReset()
service.listAllBundles
@@ -93,38 +90,37 @@ describe('ShareLinkBundleManageDialogComponent', () => {
.mockReturnValue(of(bundles))
fixture.detectChanges()
tick()
expect(service.listAllBundles).toHaveBeenCalledTimes(1)
expect(component.bundles()).toEqual(bundles)
expect(component.loading()).toBe(false)
expect(component.error()).toBeNull()
tick(5000)
jest.advanceTimersByTime(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
}))
})
it('handles errors when loading bundles', fakeAsync(() => {
it('handles errors when loading bundles', () => {
jest.useFakeTimers()
service.listAllBundles.mockReset()
service.listAllBundles
.mockReturnValueOnce(throwError(() => new Error('load fail')))
.mockReturnValue(of([]))
fixture.detectChanges()
tick()
expect(component.error()).toContain('Failed to load share link bundles.')
expect(toastService.showError).toHaveBeenCalled()
expect(component.loading()).toBe(false)
tick(5000)
jest.advanceTimersByTime(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
}))
})
it('copies bundle links when ready', fakeAsync(() => {
it('copies bundle links when ready', () => {
jest.useFakeTimers()
jest.spyOn(clipboard, 'copy').mockReturnValue(true)
fixture.detectChanges()
tick()
const readyBundle = sampleBundle({
slug: 'ready-slug',
@@ -138,27 +134,24 @@ describe('ShareLinkBundleManageDialogComponent', () => {
expect(component.copiedSlug()).toBe('ready-slug')
expect(toastService.showInfo).toHaveBeenCalled()
tick(3000)
jest.advanceTimersByTime(3000)
expect(component.copiedSlug()).toBeNull()
}))
})
it('ignores copy requests for non-ready bundles', fakeAsync(() => {
it('ignores copy requests for non-ready bundles', () => {
const copySpy = jest.spyOn(clipboard, 'copy')
fixture.detectChanges()
tick()
component.copy(sampleBundle({ status: ShareLinkBundleStatus.Pending }))
expect(copySpy).not.toHaveBeenCalled()
}))
})
it('deletes bundles and refreshes list', fakeAsync(() => {
it('deletes bundles and refreshes list', () => {
service.listAllBundles.mockReturnValue(of([]))
service.delete.mockReturnValue(of(true))
fixture.detectChanges()
tick()
component.delete(sampleBundle())
tick()
expect(service.delete).toHaveBeenCalled()
expect(toastService.showInfo).toHaveBeenCalledWith(
@@ -166,72 +159,63 @@ describe('ShareLinkBundleManageDialogComponent', () => {
)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
expect(component.loading()).toBe(false)
}))
})
it('handles delete errors gracefully', fakeAsync(() => {
it('handles delete errors gracefully', () => {
service.listAllBundles.mockReturnValue(of([]))
service.delete.mockReturnValue(throwError(() => new Error('delete fail')))
fixture.detectChanges()
tick()
component.delete(sampleBundle())
tick()
expect(toastService.showError).toHaveBeenCalled()
expect(component.loading()).toBe(false)
}))
})
it('retries bundle build and replaces existing entry', fakeAsync(() => {
it('retries bundle build and replaces existing entry', () => {
service.listAllBundles.mockReturnValue(of([]))
const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready })
service.rebuildBundle.mockReturnValue(of(updated))
fixture.detectChanges()
tick()
component.bundles.set([sampleBundle()])
component.retry(component.bundles()[0])
tick()
expect(service.rebuildBundle).toHaveBeenCalledWith(updated.id)
expect(component.bundles()[0].status).toBe(ShareLinkBundleStatus.Ready)
expect(toastService.showInfo).toHaveBeenCalled()
}))
})
it('adds new bundle when retry returns unknown entry', fakeAsync(() => {
it('adds new bundle when retry returns unknown entry', () => {
service.listAllBundles.mockReturnValue(of([]))
service.rebuildBundle.mockReturnValue(
of(sampleBundle({ id: 99, slug: 'new-slug' }))
)
fixture.detectChanges()
tick()
component.bundles.set([sampleBundle()])
component.retry({ id: 99 } as ShareLinkBundleSummary)
tick()
expect(component.bundles().find((bundle) => bundle.id === 99)).toBeTruthy()
}))
})
it('handles retry errors', fakeAsync(() => {
it('handles retry errors', () => {
service.listAllBundles.mockReturnValue(of([]))
service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail')))
fixture.detectChanges()
tick()
component.retry(sampleBundle())
tick()
expect(toastService.showError).toHaveBeenCalled()
}))
})
it('maps helpers and closes dialog', fakeAsync(() => {
it('maps helpers and closes dialog', () => {
service.listAllBundles.mockReturnValue(of([]))
fixture.detectChanges()
tick()
expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain(
'Processing'
@@ -247,5 +231,5 @@ describe('ShareLinkBundleManageDialogComponent', () => {
const closeSpy = jest.spyOn(activeModal, 'close')
component.close()
expect(closeSpy).toHaveBeenCalled()
}))
})
})
@@ -4,12 +4,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
@@ -103,7 +98,8 @@ describe('ShareLinksDialogComponent', () => {
expect(toastSpy).toHaveBeenCalled()
})
it('should support link creation then refresh & copy url', fakeAsync(() => {
it('should support link creation then refresh & copy url', () => {
jest.useFakeTimers()
const createSpy = jest.spyOn(shareLinkService, 'createLinkForDocument')
component.documentId.set(99)
component.expirationDays = 7
@@ -126,13 +122,14 @@ describe('ShareLinksDialogComponent', () => {
expiration: expiration.toISOString(),
})
fixture.detectChanges()
tick(3000)
jest.advanceTimersByTime(3000)
expect(refreshSpy).toHaveBeenCalled()
expect(copySpy).toHaveBeenCalled()
expect(component.copied()).toEqual(1)
tick(100) // copy timeout
}))
jest.advanceTimersByTime(100) // copy timeout
jest.useRealTimers()
})
it('should show error on link creation if needed', () => {
component.documentId.set(99)
@@ -16,12 +16,7 @@ jest.mock('src/environments/environment', () => ({
import { Clipboard } from '@angular/cdk/clipboard'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject, of, throwError } from 'rxjs'
@@ -126,16 +121,18 @@ describe('SystemStatusDialogComponent', () => {
expect(closeSpy).toHaveBeenCalled()
})
it('should copy the system status to clipboard', fakeAsync(() => {
it('should copy the system status to clipboard', () => {
jest.useFakeTimers()
jest.spyOn(clipboard, 'copy')
component.copy()
expect(clipboard.copy).toHaveBeenCalledWith(
JSON.stringify(component.status(), null, 4)
)
expect(component.copied()).toBeTruthy()
tick(3000)
jest.advanceTimersByTime(3000)
expect(component.copied()).toBeFalsy()
}))
jest.useRealTimers()
})
it('should calculate if date is stale', () => {
const date = new Date()
@@ -1,11 +1,4 @@
import {
ComponentFixture,
discardPeriodicTasks,
fakeAsync,
flush,
TestBed,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Clipboard } from '@angular/cdk/clipboard'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -48,28 +41,25 @@ describe('ToastComponent', () => {
expect(component).toBeTruthy()
})
it('should countdown toast', fakeAsync(() => {
it('should countdown toast', () => {
jest.useFakeTimers()
component.toast = toast2
fixture.detectChanges()
component.onShown(toast2)
tick(5000)
jest.advanceTimersByTime(5000)
expect(component.toast.delayRemaining).toEqual(0)
flush()
discardPeriodicTasks()
}))
jest.useRealTimers()
})
it('should show an error if given with toast', fakeAsync(() => {
it('should show an error if given with toast', () => {
component.toast = toast1
fixture.detectChanges()
expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
expect(fixture.nativeElement.textContent).toContain('Error 1 content')
})
flush()
discardPeriodicTasks()
}))
it('should show error details, support copy', fakeAsync(() => {
it('should show error details, support copy', () => {
component.toast = toast2
fixture.detectChanges()
@@ -81,10 +71,7 @@ describe('ToastComponent', () => {
const copySpy = jest.spyOn(clipboard, 'copy')
component.copyError(toast2.error)
expect(copySpy).toHaveBeenCalled()
flush()
discardPeriodicTasks()
}))
})
it('should parse error text, add ellipsis', () => {
expect(component.getErrorText(toast2.error)).toEqual(
@@ -2,12 +2,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop'
import { DatePipe } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/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'
@@ -186,7 +181,8 @@ describe('SavedViewWidgetComponent', () => {
fixture.detectChanges()
})
it('should show a list of documents', fakeAsync(() => {
it('should show a list of documents', async () => {
jest.useFakeTimers()
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
of({
all: [2, 3],
@@ -195,7 +191,8 @@ describe('SavedViewWidgetComponent', () => {
})
)
component.ngOnInit()
tick(500)
jest.advanceTimersByTime(500)
await fixture.whenStable()
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain('doc2')
expect(fixture.debugElement.nativeElement.textContent).toContain('doc3')
@@ -206,7 +203,8 @@ describe('SavedViewWidgetComponent', () => {
expect(
fixture.debugElement.queryAll(By.css('td a.btn'))[1].attributes['href']
).toEqual(component.getDownloadUrl(documentResults[0]))
}))
jest.useRealTimers()
})
it('should call api endpoint and load results', () => {
const listAllSpy = jest.spyOn(documentService, 'listFiltered')
@@ -1,11 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
@@ -133,7 +128,8 @@ describe('UploadFileWidgetComponent', () => {
expect(dismissSpy).toHaveBeenCalled()
})
it('should allow dismissing completed alerts', fakeAsync(() => {
it('should allow dismissing completed alerts', () => {
jest.useFakeTimers()
mockConsumerStatuses(websocketStatusService)
fixture.detectChanges()
jest
@@ -141,10 +137,11 @@ describe('UploadFileWidgetComponent', () => {
.mockImplementation(() => SUCCESS_STATUSES)
const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
component.dismissCompleted()
tick(1000)
jest.advanceTimersByTime(1000)
fixture.detectChanges()
expect(dismissSpy).toHaveBeenCalledTimes(4)
}))
jest.useRealTimers()
})
})
function mockConsumerStatuses(consumerStatusService) {
@@ -9,13 +9,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import {
ActivatedRoute,
@@ -279,6 +273,7 @@ describe('DocumentDetailComponent', () => {
}).compileComponents()
router = TestBed.inject(Router)
jest.spyOn(router, 'navigate').mockResolvedValue(true)
activatedRoute = TestBed.inject(ActivatedRoute)
openDocumentsService = TestBed.inject(OpenDocumentsService)
documentService = TestBed.inject(DocumentService)
@@ -295,6 +290,10 @@ describe('DocumentDetailComponent', () => {
component = fixture.componentInstance
})
afterEach(() => {
jest.useRealTimers()
})
function initNormally() {
jest
.spyOn(activatedRoute, 'paramMap', 'get')
@@ -330,7 +329,8 @@ describe('DocumentDetailComponent', () => {
)
})
it('should switch from preview to details when pdf preview enters the DOM', fakeAsync(() => {
it('should switch from preview to details when pdf preview enters the DOM', () => {
jest.useFakeTimers()
component.nav = {
activeId: component.DocumentDetailNavIDs.Preview,
select: jest.fn(),
@@ -339,11 +339,11 @@ describe('DocumentDetailComponent', () => {
nativeElement: { offsetParent: {} },
}
tick()
jest.advanceTimersByTime(0)
expect(component.nav.select).toHaveBeenCalledWith(
component.DocumentDetailNavIDs.Details
)
}))
})
it('should forward title key up value to titleSubject', () => {
const subjectSpy = jest.spyOn(component.titleSubject, 'next')
@@ -377,24 +377,25 @@ describe('DocumentDetailComponent', () => {
})
})
it('should update title after debounce', fakeAsync(() => {
it('should update title after debounce', () => {
jest.useFakeTimers()
initNormally()
component.titleInput.value = 'Foo Bar'
component.titleSubject.next('Foo Bar')
tick(1000)
jest.advanceTimersByTime(1000)
expect(component.documentForm.get('title').value).toEqual('Foo Bar')
discardPeriodicTasks()
}))
})
it('should update title before doc change if was not updated via debounce', fakeAsync(() => {
it('should update title before doc change if was not updated via debounce', () => {
jest.useFakeTimers()
initNormally()
component.titleInput.value = 'Foo Bar'
component.titleInput.inputField.nativeElement.dispatchEvent(
new Event('change')
)
tick(1000)
jest.advanceTimersByTime(1000)
expect(component.documentForm.get('title').value).toEqual('Foo Bar')
}))
})
it('should load non-open document via param', () => {
initNormally()
@@ -1035,14 +1036,13 @@ describe('DocumentDetailComponent', () => {
expect(component.previewNumPages()).toEqual(1000)
})
it('should include delay of 300ms after previewloaded before showing pdf', fakeAsync(() => {
it('should mark preview loaded after pdf loads', () => {
initNormally()
expect(component.previewLoaded()).toBeFalsy()
component.pdfPreviewLoaded({ numPages: 1000 } as any)
expect(component.previewNumPages()).toEqual(1000)
tick(300)
expect(component.previewLoaded()).toBeTruthy()
}))
})
it('should support zoom controls', () => {
initNormally()
@@ -2183,7 +2183,8 @@ describe('DocumentDetailComponent', () => {
)
})
it('should print document successfully', fakeAsync(() => {
it('should print document successfully', () => {
jest.useFakeTimers()
initNormally()
const appendChildSpy = jest
@@ -2224,8 +2225,6 @@ describe('DocumentDetailComponent', () => {
)
req.flush(blob)
tick()
expect(createElementSpy).toHaveBeenCalledWith('iframe')
expect(appendChildSpy).toHaveBeenCalledWith(mockIframe)
expect(createObjectURLSpy).toHaveBeenCalledWith(blob)
@@ -2242,7 +2241,7 @@ describe('DocumentDetailComponent', () => {
if (mockIframe.onload) {
mockIframe.onload({} as any)
}
tick()
jest.advanceTimersByTime(0)
expect(mockContentWindow.focus).toHaveBeenCalled()
expect(mockContentWindow.print).toHaveBeenCalled()
@@ -2255,8 +2254,6 @@ describe('DocumentDetailComponent', () => {
mockContentWindow.onafterprint(new Event('afterprint'))
}
tick(500)
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
@@ -2265,7 +2262,7 @@ describe('DocumentDetailComponent', () => {
removeChildSpy.mockRestore()
createObjectURLSpy.mockRestore()
revokeObjectURLSpy.mockRestore()
}))
})
it('should show error toast if print document fails', () => {
initNormally()
@@ -2302,76 +2299,71 @@ describe('DocumentDetailComponent', () => {
]
iframePrintErrorCases.forEach(({ description, thrownError, expectToast }) => {
it(
description,
fakeAsync(() => {
initNormally()
it(description, () => {
jest.useFakeTimers()
initNormally()
const appendChildSpy = jest
.spyOn(document.body, 'appendChild')
.mockImplementation((node: Node) => node)
const removeChildSpy = jest
.spyOn(document.body, 'removeChild')
.mockImplementation((node: Node) => node)
const createObjectURLSpy = jest
.spyOn(URL, 'createObjectURL')
.mockReturnValue('blob:mock-url')
const revokeObjectURLSpy = jest
.spyOn(URL, 'revokeObjectURL')
.mockImplementation(() => {})
const appendChildSpy = jest
.spyOn(document.body, 'appendChild')
.mockImplementation((node: Node) => node)
const removeChildSpy = jest
.spyOn(document.body, 'removeChild')
.mockImplementation((node: Node) => node)
const createObjectURLSpy = jest
.spyOn(URL, 'createObjectURL')
.mockReturnValue('blob:mock-url')
const revokeObjectURLSpy = jest
.spyOn(URL, 'revokeObjectURL')
.mockImplementation(() => {})
const toastSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
const mockContentWindow = {
focus: jest.fn().mockImplementation(() => {
throw thrownError
}),
print: jest.fn(),
onafterprint: null,
}
const mockContentWindow = {
focus: jest.fn().mockImplementation(() => {
throw thrownError
}),
print: jest.fn(),
onafterprint: null,
}
const mockIframe: any = {
style: {},
src: '',
onload: null,
contentWindow: mockContentWindow,
}
const mockIframe: any = {
style: {},
src: '',
onload: null,
contentWindow: mockContentWindow,
}
const createElementSpy = jest
.spyOn(document, 'createElement')
.mockReturnValue(mockIframe as any)
const createElementSpy = jest
.spyOn(document, 'createElement')
.mockReturnValue(mockIframe as any)
const blob = new Blob(['test'], { type: 'application/pdf' })
component.printDocument()
const blob = new Blob(['test'], { type: 'application/pdf' })
component.printDocument()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/${doc.id}/download/`
)
req.flush(blob)
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/${doc.id}/download/`
)
req.flush(blob)
tick()
if (mockIframe.onload) {
mockIframe.onload(new Event('load'))
}
if (mockIframe.onload) {
mockIframe.onload(new Event('load'))
}
jest.advanceTimersByTime(200)
tick()
tick(200)
if (expectToast) {
expect(toastSpy).toHaveBeenCalled()
} else {
expect(toastSpy).not.toHaveBeenCalled()
}
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
if (expectToast) {
expect(toastSpy).toHaveBeenCalled()
} else {
expect(toastSpy).not.toHaveBeenCalled()
}
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
createElementSpy.mockRestore()
appendChildSpy.mockRestore()
removeChildSpy.mockRestore()
createObjectURLSpy.mockRestore()
revokeObjectURLSpy.mockRestore()
})
)
createElementSpy.mockRestore()
appendChildSpy.mockRestore()
removeChildSpy.mockRestore()
createObjectURLSpy.mockRestore()
revokeObjectURLSpy.mockRestore()
})
})
})
@@ -4,12 +4,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { RouterModule } from '@angular/router'
@@ -189,8 +184,13 @@ describe('FilterEditorComponent', () => {
let httpTestingController: HttpTestingController
let searchService: SearchService
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
const tick = (ms: number = 0) => {
jest.advanceTimersByTime(ms)
fixture.detectChanges()
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterModule,
NgbDropdownModule,
@@ -275,8 +275,13 @@ describe('FilterEditorComponent', () => {
component = fixture.componentInstance
component.filterRules = []
fixture.detectChanges()
tick()
}))
await fixture.whenStable()
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
it('should not attempt to retrieve objects if user does not have permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReset()
@@ -298,7 +303,7 @@ describe('FilterEditorComponent', () => {
// SET filterRules
it('should ingest text filter rules for doc title', fakeAsync(() => {
it('should ingest text filter rules for doc title', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -308,9 +313,9 @@ describe('FilterEditorComponent', () => {
]
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(() => {
it('should ingest text filter rules for doc title + content', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -320,9 +325,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('title-content') // TEXT_FILTER_TARGET_TITLE_CONTENT
}))
})
it('should ingest legacy text filter rules for doc title + content', fakeAsync(() => {
it('should ingest legacy text filter rules for doc title + content', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -332,9 +337,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('legacy foo')
expect(component.textFilterTarget).toEqual('title-content') // TEXT_FILTER_TARGET_TITLE_CONTENT
}))
})
it('should ingest text filter rules for doc asn', fakeAsync(() => {
it('should ingest text filter rules for doc asn', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -344,9 +349,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
}))
})
it('should ingest text filter rules for custom fields', fakeAsync(() => {
it('should ingest text filter rules for custom fields', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -356,9 +361,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('custom-fields') // TEXT_FILTER_TARGET_CUSTOM_FIELDS
}))
})
it('should ingest text filter rules for doc asn is null', fakeAsync(() => {
it('should ingest text filter rules for doc asn is null', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -369,9 +374,9 @@ describe('FilterEditorComponent', () => {
]
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(() => {
it('should ingest text filter rules for doc asn is not null', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -382,9 +387,9 @@ describe('FilterEditorComponent', () => {
]
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(() => {
it('should ingest text filter rules for doc asn greater than', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -395,9 +400,9 @@ describe('FilterEditorComponent', () => {
]
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(() => {
it('should ingest text filter rules for doc asn less than', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -408,9 +413,9 @@ describe('FilterEditorComponent', () => {
]
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 mime type', fakeAsync(() => {
it('should ingest text filter rules for mime type', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -420,9 +425,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('pdf')
expect(component.textFilterTarget).toEqual('mime-type') // TEXT_FILTER_TARGET_MIME_TYPE
}))
})
it('should ingest text filter rules for fulltext query', fakeAsync(() => {
it('should ingest text filter rules for fulltext query', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -432,9 +437,9 @@ describe('FilterEditorComponent', () => {
]
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(() => {
it('should ingest text filter rules for fulltext query that include date created', () => {
expect(component.dateCreatedRelativeDate).toBeNull()
component.filterRules = [
{
@@ -444,9 +449,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.dateCreatedRelativeDate).toEqual(1) // 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(() => {
it('should ingest text filter rules for fulltext query that include date added', () => {
expect(component.dateAddedRelativeDate).toBeNull()
component.filterRules = [
{
@@ -456,9 +461,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.dateAddedRelativeDate).toEqual(1) // RELATIVE_DATE_QUERYSTRINGS['-1 week to now']
expect(component.textFilter).toBeNull()
}))
})
it('should ingest text filter content with relative dates that are not in quick list', fakeAsync(() => {
it('should ingest text filter content with relative dates that are not in quick list', () => {
expect(component.dateAddedRelativeDate).toBeNull()
component.filterRules = [
{
@@ -478,9 +483,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.dateCreatedRelativeDate).toBeNull()
expect(component.textFilter).toEqual('created:[-2 week to now]')
}))
})
it('should ingest text filter rules for more like', fakeAsync(() => {
it('should ingest text filter rules for more like', () => {
const moreLikeSpy = jest.spyOn(documentService, 'get')
moreLikeSpy.mockReturnValue(of({ id: 1, title: 'Foo Bar' }))
expect(component.textFilter).toEqual(null)
@@ -500,9 +505,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should ingest filter rules for date created after and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date created after and adjust date by 1 day', () => {
expect(component.dateCreatedFrom).toBeNull()
component.filterRules = [
{
@@ -511,9 +516,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedFrom).toEqual('2023-05-15')
}))
})
it('should ingest filter rules for date created from', fakeAsync(() => {
it('should ingest filter rules for date created from', () => {
expect(component.dateCreatedFrom).toBeNull()
component.filterRules = [
{
@@ -522,9 +527,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedFrom).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for date created before and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date created before and adjust date by 1 day', () => {
expect(component.dateCreatedTo).toBeNull()
component.filterRules = [
{
@@ -533,9 +538,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedTo).toEqual('2023-05-13')
}))
})
it('should ingest filter rules for date created to', fakeAsync(() => {
it('should ingest filter rules for date created to', () => {
expect(component.dateCreatedTo).toBeNull()
component.filterRules = [
{
@@ -544,9 +549,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedTo).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for date added after and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date added after and adjust date by 1 day', () => {
expect(component.dateAddedFrom).toBeNull()
component.filterRules = [
{
@@ -555,9 +560,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedFrom).toEqual('2023-05-15')
}))
})
it('should ingest filter rules for date added from', fakeAsync(() => {
it('should ingest filter rules for date added from', () => {
expect(component.dateAddedFrom).toBeNull()
component.filterRules = [
{
@@ -566,9 +571,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedFrom).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for date added before and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date added before and adjust date by 1 day', () => {
expect(component.dateAddedTo).toBeNull()
component.filterRules = [
{
@@ -577,9 +582,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedTo).toEqual('2023-05-13')
}))
})
it('should ingest filter rules for date added to', fakeAsync(() => {
it('should ingest filter rules for date added to', () => {
expect(component.dateAddedTo).toBeNull()
component.filterRules = [
{
@@ -588,9 +593,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedTo).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for has all tags', fakeAsync(() => {
it('should ingest filter rules for has all tags', () => {
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -614,9 +619,9 @@ describe('FilterEditorComponent', () => {
},
]
component.toggleTag(2) // coverage
}))
})
it('should ingest filter rules for has any tags', fakeAsync(() => {
it('should ingest filter rules for has any tags', () => {
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -639,9 +644,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has any tag', fakeAsync(() => {
it('should ingest filter rules for has any tag', () => {
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -651,9 +656,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(1)
expect(component.tagSelectionModel.get(null)).toBeTruthy()
}))
})
it('should ingest filter rules for exclude tag(s)', fakeAsync(() => {
it('should ingest filter rules for exclude tag(s)', () => {
expect(component.tagSelectionModel.getExcludedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -676,9 +681,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has correspondent', fakeAsync(() => {
it('should ingest filter rules for has correspondent', () => {
expect(
component.correspondentSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -708,9 +713,9 @@ describe('FilterEditorComponent', () => {
expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
}))
})
it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
it('should ingest filter rules for has any of correspondents', () => {
expect(
component.correspondentSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -740,9 +745,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for does not have any of correspondents', fakeAsync(() => {
it('should ingest filter rules for does not have any of correspondents', () => {
expect(
component.correspondentSelectionModel.getExcludedItems()
).toHaveLength(0)
@@ -769,9 +774,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has document type', fakeAsync(() => {
it('should ingest filter rules for has document type', () => {
expect(
component.documentTypeSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -801,9 +806,9 @@ describe('FilterEditorComponent', () => {
expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
}))
})
it('should ingest filter rules for has any of document types', fakeAsync(() => {
it('should ingest filter rules for has any of document types', () => {
expect(
component.documentTypeSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -830,9 +835,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for does not have any of document types', fakeAsync(() => {
it('should ingest filter rules for does not have any of document types', () => {
expect(
component.documentTypeSelectionModel.getExcludedItems()
).toHaveLength(0)
@@ -859,9 +864,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has storage path', fakeAsync(() => {
it('should ingest filter rules for has storage path', () => {
expect(component.storagePathSelectionModel.getSelectedItems()).toHaveLength(
0
)
@@ -891,9 +896,9 @@ describe('FilterEditorComponent', () => {
expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
}))
})
it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
it('should ingest filter rules for has any of storage paths', () => {
expect(component.storagePathSelectionModel.getSelectedItems()).toHaveLength(
0
)
@@ -923,9 +928,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for does not have any of storage paths', fakeAsync(() => {
it('should ingest filter rules for does not have any of storage paths', () => {
expect(component.storagePathSelectionModel.getExcludedItems()).toHaveLength(
0
)
@@ -952,9 +957,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for custom fields all', fakeAsync(() => {
it('should ingest filter rules for custom fields all', () => {
expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
component.filterRules = [
{
@@ -972,9 +977,9 @@ describe('FilterEditorComponent', () => {
.value[0] as CustomFieldQueryAtom
).serialize()
).toEqual(['42', CustomFieldQueryOperator.Exists, 'true'])
}))
})
it('should ingest filter rules for has any custom fields', fakeAsync(() => {
it('should ingest filter rules for has any custom fields', () => {
expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
component.filterRules = [
{
@@ -992,9 +997,9 @@ describe('FilterEditorComponent', () => {
.value[0] as CustomFieldQueryAtom
).serialize()
).toEqual(['42', CustomFieldQueryOperator.Exists, 'true'])
}))
})
it('should ingest filter rules for custom field queries', fakeAsync(() => {
it('should ingest filter rules for custom field queries', () => {
expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
component.filterRules = [
{
@@ -1027,9 +1032,9 @@ describe('FilterEditorComponent', () => {
.value[0] as CustomFieldQueryAtom
).serialize()
).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
}))
})
it('should ingest filter rules for owner', fakeAsync(() => {
it('should ingest filter rules for owner', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1044,9 +1049,9 @@ describe('FilterEditorComponent', () => {
)
expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
expect(component.permissionsSelectionModel.userID).toEqual(100)
}))
})
it('should ingest filter rules for owner is others', fakeAsync(() => {
it('should ingest filter rules for owner is others', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1060,9 +1065,9 @@ describe('FilterEditorComponent', () => {
OwnerFilterType.OTHERS
)
expect(component.permissionsSelectionModel.includeUsers).toContain(50)
}))
})
it('should ingest filter rules for owner does not include others', fakeAsync(() => {
it('should ingest filter rules for owner does not include others', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1076,9 +1081,9 @@ describe('FilterEditorComponent', () => {
OwnerFilterType.NOT_SELF
)
expect(component.permissionsSelectionModel.excludeUsers).toContain(50)
}))
})
it('should ingest filter rules for owner is null', fakeAsync(() => {
it('should ingest filter rules for owner is null', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1092,9 +1097,9 @@ describe('FilterEditorComponent', () => {
OwnerFilterType.UNOWNED
)
expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
}))
})
it('should ingest filter rules for owner is not null', fakeAsync(() => {
it('should ingest filter rules for owner is not null', () => {
component.filterRules = [
{
rule_type: FILTER_OWNER_ISNULL,
@@ -1109,9 +1114,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
}))
})
it('should ingest filter rules for shared by me', fakeAsync(() => {
it('should ingest filter rules for shared by me', () => {
component.filterRules = [
{
rule_type: FILTER_SHARED_BY_USER,
@@ -1119,11 +1124,11 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.permissionsSelectionModel.userID).toEqual(2)
}))
})
// GET filterRules
it('should convert user input to correct filter rules on text field search title + content', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search title + content', () => {
component.textFilterInput.nativeElement.value = 'foo'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
fixture.detectChanges()
@@ -1135,9 +1140,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should convert user input to correct filter rules on text field search title only', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search title only', () => {
component.textFilterInput.nativeElement.value = 'foo'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.query(
@@ -1154,9 +1159,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should convert user input to correct filter rules on text field search equals asn', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search equals asn', () => {
component.textFilterInput.nativeElement.value = '1234'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1174,9 +1179,9 @@ describe('FilterEditorComponent', () => {
value: '1234',
},
])
}))
})
it('should convert user input to correct filter rules on text field search greater than asn', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search greater than asn', () => {
component.textFilterInput.nativeElement.value = '123'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1198,9 +1203,9 @@ describe('FilterEditorComponent', () => {
value: '123',
},
])
}))
})
it('should convert user input to correct filter rules on text field search less than asn', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search less than asn', () => {
component.textFilterInput.nativeElement.value = '999'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1222,9 +1227,9 @@ describe('FilterEditorComponent', () => {
value: '999',
},
])
}))
})
it('should convert user input to correct filter rules on asn is null', fakeAsync(() => {
it('should convert user input to correct filter rules on asn is null', () => {
const textFieldTargetDropdown = fixture.debugElement.queryAll(
By.directive(NgbDropdownItem)
)[2]
@@ -1242,9 +1247,9 @@ describe('FilterEditorComponent', () => {
value: 'true',
},
])
}))
})
it('should convert user input to correct filter rules on asn is not null', fakeAsync(() => {
it('should convert user input to correct filter rules on asn is not null', () => {
const textFieldTargetDropdown = fixture.debugElement.queryAll(
By.directive(NgbDropdownItem)
)[2]
@@ -1262,9 +1267,9 @@ describe('FilterEditorComponent', () => {
value: 'false',
},
])
}))
})
it('should convert user input to correct filter rules on mime type', fakeAsync(() => {
it('should convert user input to correct filter rules on mime type', () => {
component.textFilterInput.nativeElement.value = 'pdf'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1280,9 +1285,9 @@ describe('FilterEditorComponent', () => {
value: 'pdf',
},
])
}))
})
it('should convert user input to correct filter rules on full text query', fakeAsync(() => {
it('should convert user input to correct filter rules on full text query', () => {
component.textFilterInput.nativeElement.value = 'foo'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1298,9 +1303,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should convert user input to correct filter rules on tag select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on tag select not assigned', () => {
const tagsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[0]
@@ -1317,9 +1322,9 @@ describe('FilterEditorComponent', () => {
value: 'false',
},
])
}))
})
it('should convert user input to correct filter rules on tag selections', fakeAsync(() => {
it('should convert user input to correct filter rules on tag selections', () => {
const tagsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[0] // Tags dropdown
@@ -1369,9 +1374,9 @@ describe('FilterEditorComponent', () => {
value: tags[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on correspondent selections', fakeAsync(() => {
it('should convert user input to correct filter rules on correspondent selections', () => {
const correspondentsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[1] // Corresp dropdown
@@ -1409,9 +1414,9 @@ describe('FilterEditorComponent', () => {
value: correspondents[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on correspondent select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on correspondent select not assigned', () => {
const correspondentsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[1]
@@ -1441,9 +1446,9 @@ describe('FilterEditorComponent', () => {
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
it('should convert user input to correct filter rules on document type selections', () => {
const documentTypesFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[2] // DocType dropdown
@@ -1481,9 +1486,9 @@ describe('FilterEditorComponent', () => {
value: document_types[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on doc type select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on doc type select not assigned', () => {
const docTypesFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[2]
@@ -1513,9 +1518,9 @@ describe('FilterEditorComponent', () => {
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
it('should convert user input to correct filter rules on storage path selections', () => {
const storagePathFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[3] // StoragePath dropdown
@@ -1553,9 +1558,9 @@ describe('FilterEditorComponent', () => {
value: storage_paths[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on storage path select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on storage path select not assigned', () => {
const storagePathsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[3]
@@ -1585,9 +1590,9 @@ describe('FilterEditorComponent', () => {
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
it('should convert user input to correct filter rules on custom field selections', () => {
const customFieldsQueryDropdown = fixture.debugElement.queryAll(
By.directive(CustomFieldsQueryDropdownComponent)
)[0]
@@ -1617,9 +1622,9 @@ describe('FilterEditorComponent', () => {
]),
},
])
}))
})
it('should convert user input to correct filter rules on date created from', fakeAsync(() => {
it('should convert user input to correct filter rules on date created from', () => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
)[0]
@@ -1637,9 +1642,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date created to', fakeAsync(() => {
it('should convert user input to correct filter rules on date created to', () => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
)[0]
@@ -1657,9 +1662,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date created with relative date', fakeAsync(() => {
it('should convert user input to correct filter rules on date created with relative date', () => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
)[0]
@@ -1673,9 +1678,9 @@ describe('FilterEditorComponent', () => {
value: 'created:[-1 week to now]',
},
])
}))
})
it('should carry over text filtering on date created with relative date', fakeAsync(() => {
it('should carry over text filtering on date created with relative date', () => {
component.textFilter = 'foo'
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
@@ -1690,9 +1695,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,created:[-1 week to now]',
},
])
}))
})
it('should convert legacy title filters into full text query when adding a created relative date', fakeAsync(() => {
it('should convert legacy title filters into full text query when adding a created relative date', () => {
component.filterRules = [
{
rule_type: FILTER_TITLE,
@@ -1712,9 +1717,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,created:[-1 week to now]',
},
])
}))
})
it('should convert simple title filters into full text query when adding a created relative date', fakeAsync(() => {
it('should convert simple title filters into full text query when adding a created relative date', () => {
component.filterRules = [
{
rule_type: FILTER_SIMPLE_TITLE,
@@ -1734,9 +1739,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,created:[-1 week to now]',
},
])
}))
})
it('should leave relative dates not in quick list intact', fakeAsync(() => {
it('should leave relative dates not in quick list intact', () => {
component.textFilterInput.nativeElement.value = 'created:[-2 week to now]'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1762,9 +1767,9 @@ describe('FilterEditorComponent', () => {
value: 'added:[-2 month to now]',
},
])
}))
})
it('should convert user input to correct filter rules on date added after', fakeAsync(() => {
it('should convert user input to correct filter rules on date added after', () => {
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
)
@@ -1782,9 +1787,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date added before', fakeAsync(() => {
it('should convert user input to correct filter rules on date added before', () => {
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
)
@@ -1802,9 +1807,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date added with relative date', fakeAsync(() => {
it('should convert user input to correct filter rules on date added with relative date', () => {
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
)
@@ -1818,9 +1823,9 @@ describe('FilterEditorComponent', () => {
value: 'added:[-1 week to now]',
},
])
}))
})
it('should carry over text filtering on date added with relative date', fakeAsync(() => {
it('should carry over text filtering on date added with relative date', () => {
component.textFilter = 'foo'
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
@@ -1835,9 +1840,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,added:[-1 week to now]',
},
])
}))
})
it('should convert user input to correct filter on permissions select my docs', fakeAsync(() => {
it('should convert user input to correct filter on permissions select my docs', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1851,9 +1856,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should convert user input to correct filter on permissions select shared with me', fakeAsync(() => {
it('should convert user input to correct filter on permissions select shared with me', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1866,9 +1871,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should convert user input to correct filter on permissions select shared with me', fakeAsync(() => {
it('should convert user input to correct filter on permissions select shared with me', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1889,9 +1894,9 @@ describe('FilterEditorComponent', () => {
value: '1,2',
},
])
}))
})
it('should convert user input to correct filter on permissions select shared by me', fakeAsync(() => {
it('should convert user input to correct filter on permissions select shared by me', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1904,9 +1909,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => {
it('should convert user input to correct filter on permissions select unowned', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1919,9 +1924,9 @@ describe('FilterEditorComponent', () => {
value: 'true',
},
])
}))
})
it('should convert user input to correct filter on permissions select others', fakeAsync(() => {
it('should convert user input to correct filter on permissions select others', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1940,9 +1945,9 @@ describe('FilterEditorComponent', () => {
value: '3',
},
])
}))
})
it('should convert user input to correct filter on permissions hide unowned', fakeAsync(() => {
it('should convert user input to correct filter on permissions hide unowned', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1960,7 +1965,7 @@ describe('FilterEditorComponent', () => {
value: 'false',
},
])
}))
})
// The rest
@@ -2221,7 +2226,7 @@ describe('FilterEditorComponent', () => {
})
})
it('should keep deprecated custom fields target available for legacy filters', fakeAsync(() => {
it('should keep deprecated custom fields target available for legacy filters', () => {
component.filterRules = [
{
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
@@ -2242,9 +2247,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should call autocomplete endpoint on input', fakeAsync(() => {
it('should call autocomplete endpoint on input', () => {
component.textFilterTarget = 'fulltext-query' // TEXT_FILTER_TARGET_FULLTEXT_QUERY
const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
component.searchAutoComplete(of('hello')).subscribe()
@@ -2254,9 +2259,9 @@ describe('FilterEditorComponent', () => {
component.searchAutoComplete(of('hello world 1')).subscribe()
tick(250)
expect(autocompleteSpy).toHaveBeenCalled()
}))
})
it('should handle autocomplete backend failure gracefully', fakeAsync(() => {
it('should handle autocomplete backend failure gracefully', () => {
component.textFilterTarget = 'fulltext-query' // TEXT_FILTER_TARGET_FULLTEXT_QUERY
const serviceAutocompleteSpy = jest.spyOn(searchService, 'autocomplete')
serviceAutocompleteSpy.mockReturnValue(
@@ -2270,7 +2275,7 @@ describe('FilterEditorComponent', () => {
tick(250)
expect(serviceAutocompleteSpy).toHaveBeenCalled()
expect(result).toEqual([])
}))
})
it('should support choosing a autocomplete item', () => {
expect(component.textFilter).toBeNull()
@@ -1,9 +1,4 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
@@ -22,8 +17,8 @@ describe('SaveViewConfigDialogComponent', () => {
let fixture: ComponentFixture<SaveViewConfigDialogComponent>
let modal: NgbActiveModal
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
NgbActiveModal,
{
@@ -56,8 +51,8 @@ describe('SaveViewConfigDialogComponent', () => {
fixture = TestBed.createComponent(SaveViewConfigDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
tick()
}))
await fixture.whenStable()
})
it('should support default name', () => {
const name = 'Tag: Inbox'
@@ -1,13 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
flush,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -24,8 +17,18 @@ describe('FileDropComponent', () => {
let settingsService: SettingsService
let uploadDocumentsService: UploadDocumentsService
beforeEach(() => {
TestBed.configureTestingModule({
const advanceTimers = (ms: number) => {
jest.advanceTimersByTime(ms)
fixture.detectChanges()
}
const flushPromises = async () => {
await Promise.resolve()
await Promise.resolve()
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileDropComponent, ToastsComponent],
providers: [
provideHttpClient(withInterceptorsFromDi()),
@@ -43,6 +46,10 @@ describe('FileDropComponent', () => {
fixture.detectChanges()
})
afterEach(() => {
jest.useRealTimers()
})
it('should enable drag-drop if user has permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.dragDropEnabled).toBeTruthy()
@@ -53,22 +60,21 @@ describe('FileDropComponent', () => {
expect(component.dragDropEnabled).toBeFalsy()
})
it('should disable drag-drop if disabled in settings', fakeAsync(() => {
it('should disable drag-drop if disabled in settings', () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled.set(false)
expect(component.dragDropEnabled).toBeFalsy()
component.onDragOver(new Event('dragover') as DragEvent)
tick(1)
fixture.detectChanges()
advanceTimers(1)
expect(component.fileIsOver()).toBeFalsy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
expect(dropzone.classes['active']).toBeFalsy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const dragEvent = new Event('drop')
@@ -79,22 +85,21 @@ describe('FileDropComponent', () => {
},
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
advanceTimers(3000)
expect(uploadSpy).not.toHaveBeenCalled()
}))
})
it('should support drag drop, initiate upload', fakeAsync(() => {
it('should support drag drop, initiate upload', () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver()).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
advanceTimers(1)
expect(component.fileIsOver()).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
@@ -113,24 +118,22 @@ describe('FileDropComponent', () => {
],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
advanceTimers(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
})
it('should support drag drop, initiate upload with webkitGetAsEntry', fakeAsync(() => {
it('should support drag drop, initiate upload with webkitGetAsEntry', async () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver()).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
advanceTimers(1)
expect(component.fileIsOver()).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
@@ -154,13 +157,14 @@ describe('FileDropComponent', () => {
files: [],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
await flushPromises()
advanceTimers(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
})
it('should show an error on traverseFileTree error', fakeAsync(() => {
it('should show an error on traverseFileTree error', async () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'showError')
const traverseSpy = jest
@@ -190,28 +194,25 @@ describe('FileDropComponent', () => {
component.onDrop(event)
tick() // flush microtasks (e.g., Promise.reject)
await flushPromises()
expect(traverseSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
$localize`Failed to read dropped items: Error traversing file tree`
)
})
discardPeriodicTasks()
}))
it('should support drag drop, initiate upload without DataTransfer API support', fakeAsync(() => {
it('should support drag drop, initiate upload without DataTransfer API support', () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver()).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
advanceTimers(1)
expect(component.fileIsOver()).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
@@ -225,11 +226,10 @@ describe('FileDropComponent', () => {
files: [file],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
advanceTimers(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
})
it('should resolve a single file when entry isFile', () => {
const mockFile = new File(['data'], 'test.txt', { type: 'text/plain' })
@@ -295,7 +295,7 @@ describe('FileDropComponent', () => {
})
})
it('should ignore events if disabled', fakeAsync(() => {
it('should ignore events if disabled', () => {
settingsService.globalDropzoneEnabled.set(false)
expect(settingsService.globalDropzoneActive()).toBeFalsy()
component.onDragOver(new Event('dragover') as DragEvent)
@@ -305,37 +305,37 @@ describe('FileDropComponent', () => {
expect(settingsService.globalDropzoneActive()).toBeTruthy()
component.onDrop(new Event('drop') as DragEvent)
expect(settingsService.globalDropzoneActive()).toBeTruthy()
}))
})
it('should hide if app loses focus', fakeAsync(() => {
it('should hide if app loses focus', () => {
jest.useFakeTimers()
const leaveSpy = jest.spyOn(component, 'onDragLeave')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled.set(true)
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
advanceTimers(1)
expect(component.hidden()).toBeFalsy()
expect(component.fileIsOver()).toBeTruthy()
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true)
component.onVisibilityChange()
expect(leaveSpy).toHaveBeenCalled()
flush()
}))
})
it('should hide on window blur', fakeAsync(() => {
it('should hide on window blur', () => {
jest.useFakeTimers()
const leaveSpy = jest.spyOn(component, 'onDragLeave')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled.set(true)
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
advanceTimers(1)
expect(component.hidden()).toBeFalsy()
expect(component.fileIsOver()).toBeTruthy()
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true)
component.onWindowBlur()
expect(leaveSpy).toHaveBeenCalled()
flush()
}))
})
})
@@ -5,12 +5,7 @@ import {
withInterceptorsFromDi,
} from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { RouterLinkWithHref } from '@angular/router'
@@ -140,24 +135,26 @@ describe('ManagementListComponent', () => {
// These tests are shared among all management list components
it('should support filtering, clear on Esc key', fakeAsync(() => {
it('should support filtering, clear on Esc key', () => {
jest.useFakeTimers()
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
jest.advanceTimersByTime(400) // debounce
fixture.detectChanges()
expect(component.data()).toEqual([tags[0]])
nameFilterInput.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { code: 'Escape' })
)
tick(400) // debounce
jest.advanceTimersByTime(400) // debounce
fixture.detectChanges()
expect(component.nameFilter).toBeNull()
expect(component.data()).toEqual(tags)
tick(100) // load
}))
jest.advanceTimersByTime(100) // load
jest.useRealTimers()
})
it('should support create, show notification on error / success', () => {
let modal: NgbModalRef
@@ -230,7 +227,8 @@ describe('ManagementListComponent', () => {
expect(reloadSpy).toHaveBeenCalled()
})
it('should use API count for pagination and nested ids for displayed total', fakeAsync(() => {
it('should use API count for pagination and nested ids for displayed total', () => {
jest.useFakeTimers()
jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce(
of({
count: 1,
@@ -240,11 +238,12 @@ describe('ManagementListComponent', () => {
)
component.reloadData()
tick(100)
jest.advanceTimersByTime(100)
expect(component.collectionSize()).toBe(1)
expect(component.displayCollectionSize()).toBe(3)
}))
jest.useRealTimers()
})
it('should support quick filter for objects', () => {
const expectedUrl = documentListViewService.getQuickFilterUrl([
@@ -525,7 +524,7 @@ describe('ManagementListComponent', () => {
expect(component.pageSize).toBe(25)
})
it('pageSize setter should update settings, reset page and reload data on success', fakeAsync(() => {
it('pageSize setter should update settings, reset page and reload data on success', () => {
const reloadSpy = jest.spyOn(component, 'reloadData')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
@@ -539,8 +538,6 @@ describe('ManagementListComponent', () => {
component.page.set(2)
component.pageSize = 100
tick()
expect(settingsService.set).toHaveBeenCalledWith(
SETTINGS_KEYS.OBJECT_LIST_SIZES,
{ tags: 100 }
@@ -548,9 +545,9 @@ describe('ManagementListComponent', () => {
expect(component.page()).toBe(1)
expect(reloadSpy).toHaveBeenCalled()
expect(toastErrorSpy).not.toHaveBeenCalled()
}))
})
it('pageSize setter should show error toast on settings store failure', fakeAsync(() => {
it('pageSize setter should show error toast on settings store failure', () => {
const reloadSpy = jest.spyOn(component, 'reloadData')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
@@ -563,12 +560,10 @@ describe('ManagementListComponent', () => {
component.typeNamePlural = 'tags'
component.pageSize = 50
tick()
expect(toastErrorSpy).toHaveBeenCalledWith(
'Error saving settings',
expect.any(Error)
)
expect(reloadSpy).not.toHaveBeenCalled()
}))
})
})
@@ -109,6 +109,7 @@ describe('DocumentListViewService', () => {
documentListViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
router = TestBed.inject(Router)
jest.spyOn(router, 'navigate').mockResolvedValue(true)
})
afterEach(() => {
@@ -3,7 +3,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import { fakeAsync, TestBed, tick } from '@angular/core/testing'
import { TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterTestingModule } from '@angular/router/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
@@ -122,7 +122,8 @@ describe('SettingsService', () => {
expect(req.request.method).toEqual('GET')
})
it('should catch error and show toast on retrieve ui_settings error', fakeAsync(() => {
it('should catch error and show toast on retrieve ui_settings error', () => {
jest.useFakeTimers()
const toastSpy = jest.spyOn(toastService, 'showError')
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
@@ -130,9 +131,10 @@ describe('SettingsService', () => {
{ detail: 'You do not have permission to perform this action.' },
{ status: 403, statusText: 'Forbidden' }
)
tick(500)
jest.advanceTimersByTime(500)
expect(toastSpy).toHaveBeenCalled()
}))
jest.useRealTimers()
})
it('calls ui_settings api endpoint with POST on store', () => {
let req = httpTestingController.expectOne(
-9
View File
@@ -13,12 +13,3 @@ export const environment = {
webSocketProtocol: 'ws:',
webSocketBaseUrl: '/ws/',
}
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
-33
View File
@@ -6,11 +6,6 @@ import '@angular/localize/init'
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
@@ -18,34 +13,6 @@ import '@angular/localize/init'
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* APPLICATION IMPORTS
*/