diff --git a/src-ui/setup-jest.ts b/src-ui/setup-jest.ts index 86e447b59..b90d97b2f 100644 --- a/src-ui/setup-jest.ts +++ b/src-ui/setup-jest.ts @@ -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 diff --git a/src-ui/src/app/app.component.spec.ts b/src-ui/src/app/app.component.spec.ts index 0d23d4feb..721aa37b3 100644 --- a/src-ui/src/app/app.component.spec.ts +++ b/src-ui/src/app/app.component.spec.ts @@ -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') diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index 5ee0b47fd..3f48e7f56 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -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() }) diff --git a/src-ui/src/app/components/admin/trash/trash.component.spec.ts b/src-ui/src/app/components/admin/trash/trash.component.spec.ts index 65f9fd4af..7edb7121e 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.spec.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.spec.ts @@ -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') diff --git a/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts b/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts index 78b08efba..a06852994 100644 --- a/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts +++ b/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts @@ -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() diff --git a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts index b533b9f12..71b22bef1 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts @@ -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(() => {}) diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts index 95ec55d24..a497f3e50 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts @@ -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 diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts index babebb6ed..42a891642 100644 --- a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts +++ b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts @@ -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() - })) + }) }) diff --git a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts index ec3cb4c35..a0d954a03 100644 --- a/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.spec.ts @@ -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() - })) + }) }) diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts index 37f56aa54..4ac087ea1 100644 --- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts @@ -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 diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts index ee99a726b..91e06c428 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts @@ -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() + }) }) diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts index e16e8a3d8..6dd1e11c9 100644 --- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts @@ -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) diff --git a/src-ui/src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.spec.ts index c4925881f..57ff40ed0 100644 --- a/src-ui/src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.spec.ts @@ -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() + }) }) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index c8a536eab..e517fa7ac 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -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 diff --git a/src-ui/src/app/components/common/input/select/select.component.spec.ts b/src-ui/src/app/components/common/input/select/select.component.spec.ts index 9104faf45..a71f37afb 100644 --- a/src-ui/src/app/components/common/input/select/select.component.spec.ts +++ b/src-ui/src/app/components/common/input/select/select.component.spec.ts @@ -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 diff --git a/src-ui/src/app/components/common/input/tags/tags.component.spec.ts b/src-ui/src/app/components/common/input/tags/tags.component.spec.ts index 31676e25b..054a79394 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.spec.ts +++ b/src-ui/src/app/components/common/input/tags/tags.component.spec.ts @@ -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', () => { diff --git a/src-ui/src/app/components/common/page-header/page-header.component.spec.ts b/src-ui/src/app/components/common/page-header/page-header.component.spec.ts index 6cc3d8e1b..1bdcd4279 100644 --- a/src-ui/src/app/components/common/page-header/page-header.component.spec.ts +++ b/src-ui/src/app/components/common/page-header/page-header.component.spec.ts @@ -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') diff --git a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts index e97cfd37a..6dcae6c6b 100644 --- a/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.spec.ts @@ -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() + }) }) diff --git a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts index e45d2d82f..d7c695131 100644 --- a/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.spec.ts @@ -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/' diff --git a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts index 99076edfc..5c943b190 100644 --- a/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.spec.ts @@ -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 = {}) => @@ -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() - })) + }) }) diff --git a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts index 5b1f6d7da..3e6be2e94 100644 --- a/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.spec.ts @@ -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) diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts index f0c802015..df65f7678 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts @@ -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() diff --git a/src-ui/src/app/components/common/toast/toast.component.spec.ts b/src-ui/src/app/components/common/toast/toast.component.spec.ts index c5d52a28f..2d0eb8341 100644 --- a/src-ui/src/app/components/common/toast/toast.component.spec.ts +++ b/src-ui/src/app/components/common/toast/toast.component.spec.ts @@ -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( diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts index 6afab7980..39480d373 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts @@ -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') diff --git a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts index c27cadbda..9506c81a8 100644 --- a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts @@ -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) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index c0fd97c23..d84b71587 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -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() + }) }) }) diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts index 855c637fc..4efd23afa 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts @@ -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() diff --git a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.spec.ts b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.spec.ts index 65749d1da..26f33adaf 100644 --- a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.spec.ts +++ b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.spec.ts @@ -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 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' diff --git a/src-ui/src/app/components/file-drop/file-drop.component.spec.ts b/src-ui/src/app/components/file-drop/file-drop.component.spec.ts index 75a19a912..f6ea9572a 100644 --- a/src-ui/src/app/components/file-drop/file-drop.component.spec.ts +++ b/src-ui/src/app/components/file-drop/file-drop.component.spec.ts @@ -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() - })) + }) }) diff --git a/src-ui/src/app/components/manage/document-attributes/management-list/management-list.component.spec.ts b/src-ui/src/app/components/manage/document-attributes/management-list/management-list.component.spec.ts index 1e8fb3f7c..7f4ff1dc1 100644 --- a/src-ui/src/app/components/manage/document-attributes/management-list/management-list.component.spec.ts +++ b/src-ui/src/app/components/manage/document-attributes/management-list/management-list.component.spec.ts @@ -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() - })) + }) }) diff --git a/src-ui/src/app/services/document-list-view.service.spec.ts b/src-ui/src/app/services/document-list-view.service.spec.ts index 85c66268f..9447101ad 100644 --- a/src-ui/src/app/services/document-list-view.service.spec.ts +++ b/src-ui/src/app/services/document-list-view.service.spec.ts @@ -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(() => { diff --git a/src-ui/src/app/services/settings.service.spec.ts b/src-ui/src/app/services/settings.service.spec.ts index b4966f8c0..c91296537 100644 --- a/src-ui/src/app/services/settings.service.spec.ts +++ b/src-ui/src/app/services/settings.service.spec.ts @@ -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( diff --git a/src-ui/src/environments/environment.ts b/src-ui/src/environments/environment.ts index e5823e27e..83567bc40 100644 --- a/src-ui/src/environments/environment.ts +++ b/src-ui/src/environments/environment.ts @@ -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. diff --git a/src-ui/src/polyfills.ts b/src-ui/src/polyfills.ts index aa8ce82f4..b2125ee9d 100644 --- a/src-ui/src/polyfills.ts +++ b/src-ui/src/polyfills.ts @@ -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 */