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 79d55eb1d..7893a6e4a 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 @@ -28,7 +28,10 @@ import { DjangoMessagesService, } from 'src/app/services/django-messages.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' -import { PermissionsService } from 'src/app/services/permissions.service' +import { + PermissionType, + PermissionsService, +} from 'src/app/services/permissions.service' import { RemoteVersionService } from 'src/app/services/rest/remote-version.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SearchService } from 'src/app/services/rest/search.service' @@ -258,7 +261,7 @@ describe('AppFrameComponent', () => { const toastSpy = jest.spyOn(toastService, 'showError') component.toggleSlimSidebar() httpTestingController - .expectOne(`${environment.apiBaseUrl}ui_settings/`) + .match(`${environment.apiBaseUrl}ui_settings/`)[0] .flush('error', { status: 500, statusText: 'error', @@ -373,4 +376,36 @@ describe('AppFrameComponent', () => { it('should call maybeRefreshDocumentCounts after saved views reload', () => { expect(maybeRefreshSpy).toHaveBeenCalled() }) + + it('should indicate attributes management availability when any permission is granted', () => { + jest + .spyOn(permissionsService, 'currentUserCan') + .mockImplementation((action, type) => { + return type === PermissionType.Tag + }) + + expect(component.canManageAttributes).toBe(true) + }) + + it('should persist attributes section collapse state', () => { + const setSpy = jest.spyOn(settingsService, 'set') + jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true)) + + component.attributesSectionsCollapsed = true + + expect(setSpy).toHaveBeenCalledWith( + SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, + ['attributes'] + ) + }) + + it('should collapse attributes sections when enabling slim sidebar', () => { + jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true)) + settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, []) + settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false) + + component.toggleSlimSidebar() + + expect(component.attributesSectionsCollapsed).toBe(true) + }) }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index aeb34cc81..a063c5095 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -223,7 +223,7 @@ export class AppFrameComponent get attributesSectionsCollapsed(): boolean { return this.settingsService .get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED) - .includes(CollapsibleSection.ATTRIBUTES) + ?.includes(CollapsibleSection.ATTRIBUTES) } set attributesSectionsCollapsed(collapsed: boolean) { diff --git a/src-ui/src/app/components/manage/document-attributes/document-attributes.component.spec.ts b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.spec.ts new file mode 100644 index 000000000..5705c0483 --- /dev/null +++ b/src-ui/src/app/components/manage/document-attributes/document-attributes.component.spec.ts @@ -0,0 +1,143 @@ +import { Component } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { + ActivatedRoute, + convertToParamMap, + ParamMap, + Router, +} from '@angular/router' +import { RouterTestingModule } from '@angular/router/testing' +import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subject } from 'rxjs' +import { + PermissionAction, + PermissionsService, + PermissionType, +} from 'src/app/services/permissions.service' +import { DocumentAttributesComponent } from './document-attributes.component' + +@Component({ + selector: 'pngx-dummy-section', + template: '', + standalone: true, +}) +class DummySectionComponent {} + +describe('DocumentAttributesComponent', () => { + let component: DocumentAttributesComponent + let fixture: ComponentFixture + let router: Router + let paramMapSubject: Subject + let permissionsService: PermissionsService + + beforeEach(async () => { + paramMapSubject = new Subject() + + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + NgxBootstrapIconsModule.pick(allIcons), + DocumentAttributesComponent, + DummySectionComponent, + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + paramMap: paramMapSubject.asObservable(), + }, + }, + { + provide: PermissionsService, + useValue: { + currentUserCan: jest.fn(), + }, + }, + ], + }).compileComponents() + + fixture = TestBed.createComponent(DocumentAttributesComponent) + component = fixture.componentInstance + router = TestBed.inject(Router) + permissionsService = TestBed.inject(PermissionsService) + + jest.spyOn(router, 'navigate').mockResolvedValue(true) + ;(component as any).sections = [ + { + id: 1, + path: 'tags', + label: 'Tags', + icon: 'tags', + permissionType: PermissionType.Tag, + kind: 'attributeList', + component: DummySectionComponent, + }, + { + id: 2, + path: 'customfields', + label: 'Custom fields', + icon: 'ui-radios', + permissionType: PermissionType.CustomField, + kind: 'customFields', + component: DummySectionComponent, + }, + ] + }) + + it('should navigate to default section when no section is provided', () => { + ;(permissionsService.currentUserCan as jest.Mock).mockImplementation( + (action, type) => { + return action === PermissionAction.View && type === PermissionType.Tag + } + ) + + fixture.detectChanges() + paramMapSubject.next(convertToParamMap({})) + + expect(router.navigate).toHaveBeenCalledWith(['attributes', 'tags'], { + replaceUrl: true, + }) + expect(component.activeNavID).toBe(1) + }) + + it('should set active section from route param when valid', () => { + ;(permissionsService.currentUserCan as jest.Mock).mockImplementation( + (action, type) => { + return ( + action === PermissionAction.View && + type === PermissionType.CustomField + ) + } + ) + + fixture.detectChanges() + paramMapSubject.next(convertToParamMap({ section: 'customfields' })) + + expect(component.activeNavID).toBe(2) + expect(router.navigate).not.toHaveBeenCalled() + }) + + it('should redirect to dashboard when no sections are visible', () => { + ;(permissionsService.currentUserCan as jest.Mock).mockReturnValue(false) + + fixture.detectChanges() + paramMapSubject.next(convertToParamMap({})) + + expect(router.navigate).toHaveBeenCalledWith(['/dashboard'], { + replaceUrl: true, + }) + }) + + it('should navigate when a nav change occurs', () => { + ;(permissionsService.currentUserCan as jest.Mock).mockImplementation( + () => true + ) + + fixture.detectChanges() + paramMapSubject.next(convertToParamMap({ section: 'tags' })) + + component.onNavChange({ nextId: 2 } as any) + + expect(router.navigate).toHaveBeenCalledWith(['attributes', 'customfields']) + }) +})