From b7dcb3b3339bc619eb75fff2cb775423f7097cf9 Mon Sep 17 00:00:00 2001 From: Mykyta Maliarchuk <84377976+nikita-web-ua@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:02:14 +0200 Subject: [PATCH] [ACS-9758][ACS-9719] Refactor Folder Rules unit tests (#4709) * [ACS-9758][ACS-9719] Refactor Folder Rules unit tests * [ACS-9758] sonar issues * [ACS-9758] fix test naming * [ACS-9758] fix test naming * [ACS-9758] cr fixes --- .../src/folder-rules.rules.spec.ts | 42 +- .../manage-rules.smart-component.spec.ts | 368 ++++++++++++------ .../manage-rules.smart-component.ts | 3 +- .../folder-rules/src/model/rule.model.ts | 1 + .../rule-action-list.ui-component.spec.ts | 13 +- .../actions/rule-action.ui-component.spec.ts | 193 ++++++++- ...e-composite-condition.ui-component.spec.ts | 49 +-- ...rule-simple-condition.ui-component.spec.ts | 73 ++-- ... => edit-rule-dialog.ui-component.spec.ts} | 62 +-- .../options/rule-options.ui-component.spec.ts | 78 ++-- .../rule-details.ui-component.spec.ts | 38 +- .../rule-details/rule-details.ui-component.ts | 2 + .../rule-triggers.ui-component.spec.ts | 55 +-- ...rule-composite-condition.validator.spec.ts | 80 ++++ .../rule-list-grouping.ui-component.spec.ts | 79 +++- .../rule-list-item.ui-component.spec.ts | 116 ++++++ .../rule-list/rule-list.ui-component.spec.ts | 122 +++++- .../rule-set-picker.smart-component.spec.ts | 141 +++++-- .../services/folder-rule-sets.service.spec.ts | 367 +++++++++++++++-- .../src/services/folder-rule-sets.service.ts | 36 +- .../src/services/folder-rules.service.spec.ts | 264 ++++++++++++- 21 files changed, 1730 insertions(+), 452 deletions(-) rename projects/aca-content/folder-rules/src/rule-details/{edit-rule-dialog.smart-component.spec.ts => edit-rule-dialog.ui-component.spec.ts} (64%) create mode 100644 projects/aca-content/folder-rules/src/rule-details/validators/rule-composite-condition.validator.spec.ts create mode 100644 projects/aca-content/folder-rules/src/rule-list/rule-list-item/rule-list-item.ui-component.spec.ts diff --git a/projects/aca-content/folder-rules/src/folder-rules.rules.spec.ts b/projects/aca-content/folder-rules/src/folder-rules.rules.spec.ts index bab1a603d..2ab3bd837 100644 --- a/projects/aca-content/folder-rules/src/folder-rules.rules.spec.ts +++ b/projects/aca-content/folder-rules/src/folder-rules.rules.spec.ts @@ -23,29 +23,33 @@ */ import { isFolderRulesEnabled } from './folder-rules.rules'; +import { AcaRuleContext } from '@alfresco/aca-shared/rules'; +import { AppConfigService } from '@alfresco/adf-core'; describe('Folder Rules Visibility Rules', () => { - describe('isFolderRulesEnabled', () => { - it('should have the feature enabled', () => { - const context: any = { - appConfig: { - get: () => true - } - }; + let mockAppConfig: jasmine.SpyObj; + let context: Partial; - const result = isFolderRulesEnabled(context); - expect(result).toEqual(true); - }); + beforeEach(() => { + mockAppConfig = jasmine.createSpyObj('AppConfigService', ['get']); + context = { + appConfig: mockAppConfig + }; + }); - it('should have the feature disabled', () => { - const context: any = { - appConfig: { - get: () => false - } - }; + it('should return true when plugin is enabled in app config', () => { + mockAppConfig.get.and.returnValue(true); + const result = isFolderRulesEnabled(context as AcaRuleContext); - const result = isFolderRulesEnabled(context); - expect(result).toEqual(false); - }); + expect(context.appConfig.get).toHaveBeenCalledWith('plugins.folderRules', false); + expect(result).toBe(true); + }); + + it('should return false when plugin is disabled in app config', () => { + mockAppConfig.get.and.returnValue(false); + const result = isFolderRulesEnabled(context as AcaRuleContext); + + expect(context.appConfig.get).toHaveBeenCalledWith('plugins.folderRules', false); + expect(result).toBe(false); }); }); diff --git a/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.spec.ts b/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.spec.ts index 236800fce..676d2d31d 100644 --- a/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.spec.ts +++ b/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.spec.ts @@ -22,9 +22,8 @@ * from Hyland Software. If not, see . */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ManageRulesSmartComponent } from './manage-rules.smart-component'; -import { DebugElement, Predicate } from '@angular/core'; import { FolderRulesService } from '../services/folder-rules.service'; import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, of, Subject } from 'rxjs'; @@ -35,32 +34,69 @@ import { ownedRuleSetMock, ruleSetWithLinkMock } from '../mock/rule-sets.mock'; -import { By } from '@angular/platform-browser'; -import { getOwningFolderEntryMock, owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; -import { MatDialog } from '@angular/material/dialog'; +import { owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { ActionsService } from '../services/actions.service'; import { FolderRuleSetsService } from '../services/folder-rule-sets.service'; import { ruleMock, ruleSettingsMock } from '../mock/rules.mock'; -import { AppService } from '@alfresco/aca-shared'; +import { AppService, GenericErrorComponent } from '@alfresco/aca-shared'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatProgressBarHarness } from '@angular/material/progress-bar/testing'; import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; import { provideHttpClient } from '@angular/common/http'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { EmptyContentComponent, NoopTranslateModule, NotificationService, UnitTestingUtils } from '@alfresco/adf-core'; import { provideMockStore } from '@ngrx/store/testing'; +import { Location } from '@angular/common'; +import { DebugElement } from '@angular/core'; +import { EditRuleDialogUiComponent } from '../rule-details/edit-rule-dialog.ui-component'; +import { Rule } from '../model/rule.model'; +import { RuleDetailsUiComponent } from '../rule-details/rule-details.ui-component'; describe('ManageRulesSmartComponent', () => { let fixture: ComponentFixture; let component: ManageRulesSmartComponent; - let debugElement: DebugElement; let loader: HarnessLoader; + let dialog: MatDialog; + let unitTestingUtils: UnitTestingUtils; let folderRuleSetsService: FolderRuleSetsService; let folderRulesService: FolderRulesService; let actionsService: ActionsService; - let callApiSpy: jasmine.Spy; + + const setupBasicObservables = () => { + folderRuleSetsService.folderInfo$ = of(owningFolderMock); + folderRuleSetsService.isLoading$ = of(false); + actionsService.loading$ = of(false); + folderRulesService.deletedRuleId$ = of(null); + }; + + const setupWithMainRuleSet = (ruleSet = ownedRuleSetMock) => { + setupBasicObservables(); + folderRuleSetsService.mainRuleSet$ = of(ruleSet); + folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]); + folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); + }; + + const setupWithoutMainRuleSet = (inheritedRuleSets = [inheritedRuleSetWithEmptyRulesMock]) => { + setupBasicObservables(); + folderRuleSetsService.mainRuleSet$ = of(null); + folderRuleSetsService.inheritedRuleSets$ = of(inheritedRuleSets); + folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); + }; + + const setupLoadingState = () => { + folderRuleSetsService.folderInfo$ = of(null); + folderRuleSetsService.mainRuleSet$ = of(null); + folderRuleSetsService.inheritedRuleSets$ = of([]); + folderRuleSetsService.isLoading$ = of(true); + actionsService.loading$ = of(true); + }; + + const getRules = (): DebugElement[] => unitTestingUtils.getAllByCSS('.aca-rule-list-item'); + const getRuleSets = (): DebugElement[] => unitTestingUtils.getAllByCSS(`[data-automation-id="rule-set-list-item"]`); + const getRuleDetails = (): DebugElement => unitTestingUtils.getByDirective(RuleDetailsUiComponent); beforeEach(() => { TestBed.configureTestingModule({ @@ -81,9 +117,10 @@ describe('ManageRulesSmartComponent', () => { }); fixture = TestBed.createComponent(ManageRulesSmartComponent); + dialog = TestBed.inject(MatDialog); component = fixture.componentInstance; - debugElement = fixture.debugElement; loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); folderRuleSetsService = TestBed.inject(FolderRuleSetsService); folderRulesService = TestBed.inject(FolderRulesService); @@ -91,85 +128,84 @@ describe('ManageRulesSmartComponent', () => { spyOn(actionsService, 'loadActionDefinitions').and.stub(); spyOn(folderRulesService, 'getRuleSettings').and.returnValue(Promise.resolve(ruleSettingsMock)); - callApiSpy = spyOn(folderRuleSetsService, 'callApi'); - callApiSpy - .withArgs(`/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, 'GET') - .and.returnValue(Promise.resolve(ownedRuleSetMock)) - .withArgs(`/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET') - .and.returnValue(Promise.resolve(ownedRuleSetMock)) - .withArgs(`/nodes/${owningFolderIdMock}?include=path%2Cproperties%2CallowableOperations%2Cpermissions`, 'GET') - .and.returnValue(Promise.resolve(getOwningFolderEntryMock)); + spyOn(folderRuleSetsService, 'loadRuleSets'); }); afterEach(() => { fixture.destroy(); }); + it('should call location.back() when goBack is called', () => { + const locationService = TestBed.inject(Location); + spyOn(locationService, 'back'); + component.goBack(); + + expect(locationService.back).toHaveBeenCalled(); + }); + + it('should call folderRulesService.selectRule with provided rule on rule select', () => { + const testRule = ruleMock('test-rule-1'); + spyOn(folderRulesService, 'selectRule'); + + component.onSelectRule(testRule); + + expect(folderRulesService.selectRule).toHaveBeenCalledWith(testRule); + }); + + it('should call loadMoreInheritedRuleSets on load more rule sets', () => { + spyOn(folderRuleSetsService, 'loadMoreInheritedRuleSets'); + component.onLoadMoreRuleSets(); + expect(folderRuleSetsService.loadMoreInheritedRuleSets).toHaveBeenCalled(); + }); + + it('should load rules for the given rule set', () => { + spyOn(folderRulesService, 'loadRules'); + const ruleSet = inheritedRuleSetMock; + component.onLoadMoreRules(ruleSet); + expect(folderRulesService.loadRules).toHaveBeenCalledWith(ruleSet); + }); + it('should show a list of rule sets and rules', () => { - const loadRuleSetsSpy = spyOn(folderRuleSetsService, 'loadRuleSets').and.stub(); - - folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock); - folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]); - folderRuleSetsService.isLoading$ = of(false); - folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); - actionsService.loading$ = of(false); - + setupWithMainRuleSet(); fixture.detectChanges(); expect(component).toBeTruthy(); + expect(folderRuleSetsService.loadRuleSets).toHaveBeenCalledOnceWith(component.nodeId); - expect(loadRuleSetsSpy).toHaveBeenCalledOnceWith(component.nodeId); + const ruleGroupingSections = unitTestingUtils.getAllByCSS(`[data-automation-id="rule-list-item"]`); - const ruleGroupingSections = debugElement.queryAll(By.css(`[data-automation-id="rule-list-item"]`)); - const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); - const ruleDetails = debugElement.query(By.css('aca-rule-details')); - const deleteRuleBtn = debugElement.query(By.css('#delete-rule-btn')); + const deleteRuleBtn = unitTestingUtils.getByCSS('#delete-rule-btn'); expect(ruleGroupingSections.length).toBe(2, 'unexpected number of rule sections'); - expect(rules.length).toBe(4, 'unexpected number of aca-rule-list-item'); - expect(ruleDetails).toBeTruthy('aca-rule-details was not rendered'); + expect(getRules().length).toBe(4, 'unexpected number of aca-rule-list-item'); + expect(getRuleDetails()).toBeTruthy('aca-rule-details was not rendered'); expect(deleteRuleBtn).toBeTruthy('no delete rule button'); }); it('should show adf-empty-content if node has no rules defined yet', () => { - folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.mainRuleSet$ = of(null); - folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetWithEmptyRulesMock]); - folderRuleSetsService.isLoading$ = of(false); - actionsService.loading$ = of(false); - + setupWithoutMainRuleSet(); fixture.detectChanges(); expect(component).toBeTruthy(); - const adfEmptyContent = debugElement.query(By.css('adf-empty-content')); - const ruleSets = debugElement.queryAll(By.css(`[data-automation-id="rule-set-list-item"]`)); - const ruleDetails = debugElement.query(By.css('aca-rule-details')); + const adfEmptyContent = unitTestingUtils.getByDirective(EmptyContentComponent); expect(adfEmptyContent).toBeTruthy(); - expect(ruleSets.length).toBe(0); - expect(ruleDetails).toBeFalsy(); + expect(getRuleSets().length).toBe(0); + expect(getRuleDetails()).toBeFalsy(); }); it('should show adf-empty-content if there are only inherited disabled rules', () => { - folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.mainRuleSet$ = of(null); - folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetWithOnlyDisabledRulesMock]); - folderRuleSetsService.isLoading$ = of(false); - actionsService.loading$ = of(false); - + setupWithoutMainRuleSet([inheritedRuleSetWithOnlyDisabledRulesMock]); fixture.detectChanges(); expect(component).toBeTruthy(); - const adfEmptyContent = debugElement.query(By.css('adf-empty-content')); - const ruleSets = debugElement.queryAll(By.css(`[data-automation-id="rule-set-list-item"]`)); - const ruleDetails = debugElement.query(By.css('aca-rule-details')); + const adfEmptyContent = unitTestingUtils.getByDirective(EmptyContentComponent); expect(adfEmptyContent).toBeTruthy(); - expect(ruleSets.length).toBe(0); - expect(ruleDetails).toBeFalsy(); + expect(getRuleSets().length).toBe(0); + expect(getRuleDetails()).toBeFalsy(); }); it('should only show aca-generic-error if the non-existing node was provided', () => { @@ -183,129 +219,203 @@ describe('ManageRulesSmartComponent', () => { expect(component).toBeTruthy(); - const acaGenericError = debugElement.query(By.css('aca-generic-error')); - const rules = debugElement.query(By.css('.aca-rule-list-item')); - const ruleDetails = debugElement.query(By.css('aca-rule-details')); + const acaGenericError = unitTestingUtils.getByDirective(GenericErrorComponent); expect(acaGenericError).toBeTruthy(); - expect(rules).toBeFalsy(); - expect(ruleDetails).toBeFalsy(); + expect(getRules().length).toBeFalsy(); + expect(getRuleDetails()).toBeFalsy(); }); it('should only show progress bar while loading', async () => { - folderRuleSetsService.folderInfo$ = of(null); - folderRuleSetsService.mainRuleSet$ = of(null); - folderRuleSetsService.inheritedRuleSets$ = of([]); - folderRuleSetsService.isLoading$ = of(true); - actionsService.loading$ = of(true); - + setupLoadingState(); fixture.detectChanges(); expect(component).toBeTruthy(); const matProgressBar = loader.getHarness(MatProgressBarHarness); - const rules = debugElement.query(By.css('.aca-rule-list-item')); - const ruleDetails = debugElement.query(By.css('aca-rule-details')); expect(matProgressBar).toBeTruthy(); - expect(rules).toBeFalsy(); - expect(ruleDetails).toBeFalsy(); + expect(getRules().length).toBeFalsy(); + expect(getRuleDetails()).toBeFalsy(); }); - // TODO: [ACS-9719] flaky test that needs review - // eslint-disable-next-line ban/ban - xit('should call deleteRule() if confirmation dialog returns true', () => { - const dialog = TestBed.inject(MatDialog); - folderRuleSetsService.folderInfo$ = of(owningFolderMock); - folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock); - folderRuleSetsService.inheritedRuleSets$ = of([inheritedRuleSetMock]); - folderRuleSetsService.isLoading$ = of(false); - folderRulesService.selectedRule$ = of(ruleMock('owned-rule-1')); - folderRulesService.deletedRuleId$ = of(null); - actionsService.loading$ = of(false); + it('should call deleteRule() if confirmation dialog returns true', () => { + setupWithMainRuleSet(); + fixture.detectChanges(); - const onRuleDeleteButtonClickedSpy = spyOn(component, 'onRuleDeleteButtonClicked').and.callThrough(); - - const dialogResult: any = { + spyOn(dialog, 'open').and.returnValue({ afterClosed: () => of(true) - }; - const dialogOpenSpy = spyOn(dialog, 'open').and.returnValue(dialogResult); + } as MatDialogRef); const deleteRuleSpy = spyOn(folderRulesService, 'deleteRule'); - const onRuleDeleteSpy = spyOn(component, 'onRuleDelete').and.callThrough(); fixture.detectChanges(); expect(component).toBeTruthy('expected component'); - const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); - const ruleDetails = debugElement.query(By.css('aca-rule-details')); - const deleteRuleBtn = fixture.debugElement.nativeElement.querySelector('#delete-rule-btn'); - - deleteRuleBtn.click(); + component.onRuleDeleteButtonClicked(ruleMock('owned-rule-1')); fixture.detectChanges(); folderRulesService.deletedRuleId$ = of('owned-rule-1-id'); - expect(onRuleDeleteButtonClickedSpy).toHaveBeenCalled(); - expect(dialogOpenSpy).toHaveBeenCalled(); + expect(dialog.open).toHaveBeenCalled(); expect(deleteRuleSpy).toHaveBeenCalled(); - expect(onRuleDeleteSpy).toHaveBeenCalledTimes(1); - expect(rules).toBeTruthy('expected rules'); - expect(ruleDetails).toBeTruthy('expected ruleDetails'); - expect(deleteRuleBtn).toBeTruthy(); + expect(getRules()).toBeTruthy('expected rules'); + expect(getRuleDetails()).toBeTruthy('expected ruleDetails'); + }); + + it('should refresh main rule set when link rules dialog is closed', () => { + setupWithMainRuleSet(); + fixture.detectChanges(); + + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => of(true) + } as MatDialogRef); + const refreshMainRuleSetSpy = spyOn(folderRuleSetsService, 'refreshMainRuleSet'); + + fixture.detectChanges(); + expect(component).toBeTruthy('expected component'); + + component.openLinkRulesDialog(); + + fixture.detectChanges(); + folderRulesService.deletedRuleId$ = of('owned-rule-1-id'); + + expect(dialog.open).toHaveBeenCalled(); + expect(refreshMainRuleSetSpy).toHaveBeenCalled(); + expect(getRules()).toBeTruthy('expected rules'); + expect(getRuleDetails()).toBeTruthy('expected ruleDetails'); + }); + + it('should call deleteRuleSetLink when onRuleSetUnlinkClicked is called', () => { + setupWithMainRuleSet(); + fixture.detectChanges(); + + spyOn(dialog, 'open').and.returnValue({ + afterClosed: () => of(true) + } as MatDialogRef); + const deleteRuleSetLinkSpy = spyOn(folderRuleSetsService, 'deleteRuleSetLink'); + + fixture.detectChanges(); + expect(component).toBeTruthy('expected component'); + + component.onRuleSetUnlinkClicked(ruleSetWithLinkMock); + + fixture.detectChanges(); + folderRulesService.deletedRuleId$ = of('owned-rule-1-id'); + + expect(dialog.open).toHaveBeenCalled(); + expect(deleteRuleSetLinkSpy).toHaveBeenCalled(); + expect(getRules()).toBeTruthy('expected rules'); + expect(getRuleDetails()).toBeTruthy('expected ruleDetails'); + }); + + describe('EditRuleDialog', () => { + let submit$: Subject>; + let dialogRefMock: jasmine.SpyObj>; + + beforeEach(() => { + submit$ = new Subject>(); + dialogRefMock = jasmine.createSpyObj>('MatDialogRef', ['close', 'afterClosed']); + dialogRefMock.afterClosed.and.returnValue(of(true)); + dialogRefMock.componentInstance = { submitted: submit$ } as EditRuleDialogUiComponent; + spyOn(dialog, 'open').and.returnValue(dialogRefMock as MatDialogRef); + }); + + it('should open EditRuleDialogUiComponent with correct config', () => { + const model = { name: 'bar' } as Rule; + component.openCreateUpdateRuleDialog(model); + + expect(dialog.open).toHaveBeenCalled(); + }); + + it('should create a new rule and close dialog when submitted without id', async () => { + const newRuleParams = { name: 'NewRule' } as Rule; + const createdRule = ruleMock('new-id'); + spyOn(folderRulesService, 'createRule').and.returnValue(Promise.resolve(createdRule)); + const addOrUpdateSpy = spyOn(folderRuleSetsService, 'addOrUpdateRuleInMainRuleSet'); + + component.openCreateUpdateRuleDialog(); + submit$.next(newRuleParams); + await Promise.resolve(); + + expect(folderRulesService.createRule).toHaveBeenCalledWith(component.nodeId, newRuleParams); + expect(addOrUpdateSpy).toHaveBeenCalledWith(createdRule); + }); + + it('should update existing rule when submitted with id', async () => { + const updatedRuleParams = { id: '123', name: 'Test' } as Rule; + const updatedRule = ruleMock('123'); + spyOn(folderRulesService, 'updateRule').and.returnValue(Promise.resolve(updatedRule)); + const addOrUpdateSpy = spyOn(folderRuleSetsService, 'addOrUpdateRuleInMainRuleSet'); + + component.openCreateUpdateRuleDialog(); + submit$.next(updatedRuleParams); + await Promise.resolve(); + + expect(folderRulesService.updateRule).toHaveBeenCalled(); + expect(addOrUpdateSpy).toHaveBeenCalledWith(updatedRule); + }); + + it('should show error notification if submission fails', fakeAsync(() => { + const notificationService = TestBed.inject(NotificationService); + const params = { name: 'Bad' } as Rule; + const error = new Error('Test error'); + (error as any).response = { body: { error: { errorKey: 'ERROR_KEY' } } }; + spyOn(folderRulesService, 'createRule').and.returnValue(Promise.reject(error)); + spyOn(notificationService, 'showError'); + + component.openCreateUpdateRuleDialog(); + submit$.next(params); + tick(); + + expect(notificationService.showError).toHaveBeenCalledWith('ERROR_KEY'); + expect(dialogRefMock.close).not.toHaveBeenCalled(); + })); }); describe('Create rule & link rules buttons visibility', () => { - let createButtonPredicate: Predicate; - let linkButtonPredicate: Predicate; + const getCreateButton = (): DebugElement => unitTestingUtils.getByDataAutomationId('manage-rules-create-button'); + const getLinkButton = (): DebugElement => unitTestingUtils.getByDataAutomationId('manage-rules-link-button'); beforeEach(() => { folderRuleSetsService.folderInfo$ = of(owningFolderMock); folderRuleSetsService.inheritedRuleSets$ = of([]); folderRuleSetsService.isLoading$ = of(false); actionsService.loading$ = of(false); - - createButtonPredicate = By.css(`[data-automation-id="manage-rules-create-button"]`); - linkButtonPredicate = By.css(`[data-automation-id="manage-rules-link-button"]`); }); it('should show the create rule button if there is no main rule set', () => { folderRuleSetsService.mainRuleSet$ = of(null); fixture.detectChanges(); - const createButton = debugElement.query(createButtonPredicate); - expect(createButton).toBeTruthy(); + expect(getCreateButton()).toBeTruthy(); }); it('should show the link rules button if there is no main rule set', () => { folderRuleSetsService.mainRuleSet$ = of(null); fixture.detectChanges(); - const linkButton = debugElement.query(linkButtonPredicate); - expect(linkButton).toBeTruthy(); + expect(getLinkButton()).toBeTruthy(); }); it('should show the create rule button if the main rule set is owned', () => { folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock); fixture.detectChanges(); - const createButton = debugElement.query(createButtonPredicate); - expect(createButton).toBeTruthy(); + expect(getCreateButton()).toBeTruthy(); }); it('should not show the create rule button if the main rule set is linked', () => { folderRuleSetsService.mainRuleSet$ = of(ruleSetWithLinkMock); fixture.detectChanges(); - const createButton = debugElement.query(createButtonPredicate); - expect(createButton).toBeFalsy(); + expect(getCreateButton()).toBeFalsy(); }); it('should not show the link rules button if the folder has a main rule set', () => { folderRuleSetsService.mainRuleSet$ = of(ownedRuleSetMock); fixture.detectChanges(); - const linkButton = debugElement.query(linkButtonPredicate); - expect(linkButton).toBeFalsy(); + expect(getLinkButton()).toBeFalsy(); }); }); @@ -331,22 +441,36 @@ describe('ManageRulesSmartComponent', () => { expect(await createButton.isDisabled()).toBeTrue(); }); - it('should call onInheritanceToggleChange() on change', () => { - const onInheritanceToggleChangeSpy = spyOn(component, 'onInheritanceToggleChange').and.callThrough(); + it('should call onInheritanceToggleChange() on change', fakeAsync(() => { const updateRuleSettingsSpy = spyOn(folderRulesService, 'updateRuleSettings').and.returnValue(Promise.resolve(ruleSettingsMock)); - const loadRuleSetsSpy = spyOn(folderRuleSetsService, 'loadRuleSets').and.callThrough(); fixture.detectChanges(); - const inheritanceToggleBtn = fixture.debugElement.query(By.css(`[data-automation-id="manage-rules-inheritance-toggle-button"]`)); + const inheritanceToggleBtn = unitTestingUtils.getByDataAutomationId('manage-rules-inheritance-toggle-button'); inheritanceToggleBtn.nativeElement.dispatchEvent(new Event('change')); + tick(); fixture.detectChanges(); - expect(onInheritanceToggleChangeSpy).toHaveBeenCalled(); - expect(updateRuleSettingsSpy).toHaveBeenCalledTimes(1); - expect(loadRuleSetsSpy).toHaveBeenCalledTimes(1); - }); + expect(updateRuleSettingsSpy).toHaveBeenCalled(); + expect(folderRuleSetsService.loadRuleSets).toHaveBeenCalled(); + expect(folderRuleSetsService.loadRuleSets).toHaveBeenCalledWith(component.nodeId); + expect(component.isInheritanceToggleDisabled).toBeFalse(); + })); + + it('should update rule enabled state and add or update it in main rule set', fakeAsync(() => { + const original = ruleMock('test'); + const toggled: Rule = { ...original, isEnabled: !original.isEnabled }; + spyOn(folderRulesService, 'updateRule').and.returnValue(Promise.resolve(toggled)); + const addOrUpdateSpy = spyOn(folderRuleSetsService, 'addOrUpdateRuleInMainRuleSet'); + component.nodeId = 'node-123'; + + component.onRuleEnabledToggle(original, toggled.isEnabled); + tick(); + + expect(folderRulesService.updateRule).toHaveBeenCalledWith('node-123', original.id, { ...original, isEnabled: toggled.isEnabled }); + expect(addOrUpdateSpy).toHaveBeenCalledWith(toggled); + })); }); }); diff --git a/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.ts b/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.ts index 96206a613..6ad363cfd 100644 --- a/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.ts +++ b/projects/aca-content/folder-rules/src/manage-rules/manage-rules.smart-component.ts @@ -31,7 +31,7 @@ import { ActivatedRoute, RouterModule } from '@angular/router'; import { NodeInfo } from '@alfresco/aca-shared/store'; import { delay } from 'rxjs/operators'; import { EditRuleDialogUiComponent } from '../rule-details/edit-rule-dialog.ui-component'; -import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatDialog } from '@angular/material/dialog'; import { ConfirmDialogComponent, EmptyContentComponent, NotificationService, ToolbarComponent, ToolbarTitleComponent } from '@alfresco/adf-core'; import { ActionDefinitionTransformed } from '../model/rule-action.model'; import { ActionsService } from '../services/actions.service'; @@ -64,7 +64,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; RouterModule, GenericErrorComponent, RuleDetailsUiComponent, - MatDialogModule, EmptyContentComponent, ToolbarTitleComponent, ToolbarComponent diff --git a/projects/aca-content/folder-rules/src/model/rule.model.ts b/projects/aca-content/folder-rules/src/model/rule.model.ts index 2c298ea95..15da89c2c 100644 --- a/projects/aca-content/folder-rules/src/model/rule.model.ts +++ b/projects/aca-content/folder-rules/src/model/rule.model.ts @@ -52,6 +52,7 @@ export interface RuleForForm { id: string; name: string; description: string; + isShared: boolean; triggers: RuleTrigger[]; conditions: RuleCompositeCondition; actions: RuleAction[]; diff --git a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action-list.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action-list.ui-component.spec.ts index 11d0ffe7b..3b41df0a7 100644 --- a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action-list.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action-list.ui-component.spec.ts @@ -23,9 +23,8 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { RuleActionListUiComponent } from './rule-action-list.ui-component'; -import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { RuleActionUiComponent } from './rule-action.ui-component'; import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; @@ -33,9 +32,10 @@ import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-conten describe('RuleActionListUiComponent', () => { let fixture: ComponentFixture; let component: RuleActionListUiComponent; + let unitTestingUtils: UnitTestingUtils; const getByDataAutomationId = (dataAutomationId: string, index = 0): DebugElement => - fixture.debugElement.queryAll(By.css(`[data-automation-id="${dataAutomationId}"]`))[index]; + unitTestingUtils.getAllByCSS(`[data-automation-id="${dataAutomationId}"]`)[index]; beforeEach(() => { TestBed.configureTestingModule({ @@ -45,13 +45,14 @@ describe('RuleActionListUiComponent', () => { fixture = TestBed.createComponent(RuleActionListUiComponent); component = fixture.componentInstance; + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); component.writeValue([]); fixture.detectChanges(); }); it('should default to 1 empty action when an empty array of actions is written', () => { - const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent)); + const acaRuleActions = unitTestingUtils.getAllByDirective(RuleActionUiComponent); expect(acaRuleActions.length).toBe(1); }); @@ -60,7 +61,7 @@ describe('RuleActionListUiComponent', () => { addActionButton.click(); fixture.detectChanges(); - const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent)); + const acaRuleActions = unitTestingUtils.getAllByDirective(RuleActionUiComponent); expect(acaRuleActions.length).toBe(2); }); @@ -99,7 +100,7 @@ describe('RuleActionListUiComponent', () => { removeActionButton.click(); fixture.detectChanges(); - const acaRuleActions = fixture.debugElement.queryAll(By.directive(RuleActionUiComponent)); + const acaRuleActions = unitTestingUtils.getAllByDirective(RuleActionUiComponent); expect(acaRuleActions.length).toBe(1); }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts index 1ceeb01bd..4639280e5 100644 --- a/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/actions/rule-action.ui-component.spec.ts @@ -23,7 +23,14 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CardViewBoolItemModel, CardViewComponent, CardViewSelectItemModel, CardViewTextItemModel, NoopTranslateModule } from '@alfresco/adf-core'; +import { + CardViewBoolItemModel, + CardViewComponent, + CardViewSelectItemModel, + CardViewTextItemModel, + NoopTranslateModule, + UnitTestingUtils +} from '@alfresco/adf-core'; import { RuleActionUiComponent } from './rule-action.ui-component'; import { actionLinkToCategoryTransformedMock, @@ -31,20 +38,35 @@ import { actionsTransformedListMock, securityActionTransformedMock } from '../../mock/actions.mock'; -import { By } from '@angular/platform-browser'; import { dummyCategoriesConstraints, dummyConstraints, dummyTagsConstraints } from '../../mock/action-parameter-constraints.mock'; import { securityMarksResponseMock, updateNotificationMock } from '../../mock/security-marks.mock'; -import { AlfrescoApiService, AlfrescoApiServiceMock, CategoryService, NodeAction, TagService } from '@alfresco/adf-content-services'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { + AlfrescoApiService, + AlfrescoApiServiceMock, + CategoryService, + ContentNodeSelectorComponent, + NodeAction, + SecurityControlsService, + TagService +} from '@alfresco/adf-content-services'; +import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSelectHarness } from '@angular/material/select/testing'; import { of, Subject } from 'rxjs'; +import { Node } from '@alfresco/js-api'; +import { SimpleChanges } from '@angular/core'; describe('RuleActionUiComponent', () => { let fixture: ComponentFixture; let component: RuleActionUiComponent; let loader: HarnessLoader; + let unitTestingUtils: UnitTestingUtils; + let securityControlsService: SecurityControlsService; + + const clickActionItem = () => { + unitTestingUtils.getByCSS('.adf-textitem-action').nativeElement.click(); + }; const changeMatSelectValue = async (value: string) => { const matSelect = await loader.getHarness(MatSelectHarness); @@ -52,14 +74,7 @@ describe('RuleActionUiComponent', () => { fixture.detectChanges(); }; - const getPropertiesCardView = (): CardViewComponent => fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance; - - function setInputValue(value: string) { - const input = fixture.debugElement.query(By.css('input')).nativeElement; - input.value = value; - input.dispatchEvent(new Event('input')); - fixture.detectChanges(); - } + const getPropertiesCardView = (): CardViewComponent => unitTestingUtils.getByDirective(CardViewComponent).componentInstance; beforeEach(() => { TestBed.configureTestingModule({ @@ -69,7 +84,9 @@ describe('RuleActionUiComponent', () => { fixture = TestBed.createComponent(RuleActionUiComponent); component = fixture.componentInstance; + securityControlsService = TestBed.inject(SecurityControlsService); loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); }); it('should not accept empty parameters', async () => { @@ -79,12 +96,12 @@ describe('RuleActionUiComponent', () => { await changeMatSelectValue('mock-action-1-definition'); - setInputValue('test'); + await unitTestingUtils.fillMatInput('test'); await fixture.whenStable(); expect(component.parameters).toEqual({ 'mock-action-parameter-boolean': false, 'mock-action-parameter-text': 'test' }); - setInputValue(''); + await unitTestingUtils.fillMatInput(''); await fixture.whenStable(); expect(component.parameters).toEqual({ 'mock-action-parameter-boolean': false }); @@ -119,7 +136,7 @@ describe('RuleActionUiComponent', () => { expect(cardView.properties[4]).toBeInstanceOf(CardViewSelectItemModel); await changeMatSelectValue('mock-action-2-definition'); - expect(fixture.debugElement.query(By.directive(CardViewComponent))).toBeNull(); + expect(unitTestingUtils.getByDirective(CardViewComponent)).toBeNull(); }); it('should create category-value action parameter as a text box rather than node picker', async () => { @@ -138,21 +155,21 @@ describe('RuleActionUiComponent', () => { }); it('should open category selector dialog on category-value action parameter clicked', async () => { - const dialog = fixture.debugElement.injector.get(MatDialog); + const dialog = TestBed.inject(MatDialog); component.actionDefinitions = [actionLinkToCategoryTransformedMock]; component.parameterConstraints = dummyConstraints; spyOn(dialog, 'open'); fixture.detectChanges(); await changeMatSelectValue('mock-action-3-definition'); - fixture.debugElement.query(By.css('.adf-textitem-action')).nativeElement.click(); + clickActionItem(); expect(dialog.open).toHaveBeenCalledTimes(1); expect(dialog.open['calls'].argsFor(0)[0].name).toBe('CategorySelectorDialogComponent'); }); it('should open node selector dialog with correct parameters', async () => { - const dialog = fixture.debugElement.injector.get(MatDialog); + const dialog = TestBed.inject(MatDialog); component.actionDefinitions = [actionNodeTransformedMock]; const expectedData = { selectionMode: 'single', @@ -165,7 +182,7 @@ describe('RuleActionUiComponent', () => { fixture.detectChanges(); await changeMatSelectValue('mock-action-5-definition'); - fixture.debugElement.query(By.css('.adf-textitem-action')).nativeElement.click(); + clickActionItem(); expect(dialog.open).toHaveBeenCalledTimes(1); expect(dialogSpy.calls.mostRecent().args[1].data).toEqual(expectedData); @@ -248,6 +265,23 @@ describe('RuleActionUiComponent', () => { ]); }); }); + + it('should load security mark options when writeValue is called with securityGroupId parameter', async () => { + component.actionDefinitions = [securityActionTransformedMock]; + spyOn(securityControlsService, 'getSecurityMark').and.returnValue(Promise.resolve(securityMarksResponseMock)); + fixture.detectChanges(); + + await changeMatSelectValue('mock-action-4-definition'); + + component.writeValue({ + actionDefinitionId: 'mock-action-4-definition', + params: { securityGroupId: 'group-1' } + }); + + await fixture.whenStable(); + + expect(securityControlsService.getSecurityMark).toHaveBeenCalled(); + }); }); describe('Security mark actions', () => { @@ -280,4 +314,125 @@ describe('RuleActionUiComponent', () => { }); }); }); + + describe('ContentNodeSelectorComponent dialog', () => { + let mockDialogRef: jasmine.SpyObj>; + let dialogOpenSpy: jasmine.Spy< + (component: typeof ContentNodeSelectorComponent, config?: MatDialogConfig) => MatDialogRef + >; + + beforeEach(() => { + mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['afterClosed']); + dialogOpenSpy = spyOn(TestBed.inject(MatDialog), 'open').and.returnValue(mockDialogRef); + spyOn(TestBed.inject(MatDialog), 'closeAll'); + spyOn(console, 'error'); + }); + + it('should open dialog when node selector is clicked', async () => { + component.actionDefinitions = [actionNodeTransformedMock]; + component.nodeId = 'test-folder-id'; + fixture.detectChanges(); + + await changeMatSelectValue('mock-action-5-definition'); + clickActionItem(); + + expect(dialogOpenSpy).toHaveBeenCalledWith(ContentNodeSelectorComponent, { + data: { + selectionMode: 'single', + title: 'ACA_FOLDER_RULES.RULE_DETAILS.PLACEHOLDER.CHOOSE_FOLDER', + actionName: NodeAction.CHOOSE, + currentFolderId: 'test-folder-id', + select: jasmine.any(Subject) + }, + panelClass: 'adf-content-node-selector-dialog', + width: '630px' + }); + }); + + it('should update component when valid node is selected through dialog', async () => { + component.actionDefinitions = [actionNodeTransformedMock]; + fixture.detectChanges(); + + await changeMatSelectValue('mock-action-5-definition'); + + component.parameters = { existingParam: 'value' }; + + clickActionItem(); + + const dialogData = dialogOpenSpy.calls.mostRecent().args[1].data; + const selectedNode = { id: 'selected-node-123', name: 'Test Folder' } as Node; + dialogData.select.next([selectedNode]); + + expect(component.parameters).toEqual({ + existingParam: 'value', + 'aspect-name': 'selected-node-123' + }); + }); + + it('should log error when dialog selection encounters error', async () => { + component.actionDefinitions = [actionNodeTransformedMock]; + fixture.detectChanges(); + + await changeMatSelectValue('mock-action-5-definition'); + clickActionItem(); + + const dialogData = dialogOpenSpy.calls.mostRecent().args[1].data; + const testError = new Error('Selection failed'); + dialogData.select.error(testError); + + expect(console.error).toHaveBeenCalledWith(testError); + }); + + it('should close all dialogs when selection completes', async () => { + const dialogService = TestBed.inject(MatDialog); + component.actionDefinitions = [actionNodeTransformedMock]; + fixture.detectChanges(); + + await changeMatSelectValue('mock-action-5-definition'); + clickActionItem(); + + const dialogData = dialogOpenSpy.calls.mostRecent().args[1].data; + dialogData.select.complete(); + + expect(dialogService.closeAll).toHaveBeenCalled(); + }); + }); + + it('should enable form when readOnly changes from true to false', () => { + component.readOnly = true; + component.form.disable(); + + const changes: SimpleChanges = { + readOnly: { + currentValue: false, + previousValue: true, + firstChange: false, + isFirstChange: () => false + } + }; + + component.ngOnChanges(changes); + + expect(component.readOnly).toBe(false); + expect(component.form.enabled).toBe(true); + }); + + it('should disable form when readOnly changes from false to true', () => { + component.readOnly = false; + component.form.enable(); + + const changes: SimpleChanges = { + readOnly: { + currentValue: true, + previousValue: false, + firstChange: false, + isFirstChange: () => false + } + }; + + component.ngOnChanges(changes); + + expect(component.readOnly).toBe(true); + expect(component.form.disabled).toBe(true); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts index 82b467550..52876a273 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-composite-condition.ui-component.spec.ts @@ -24,8 +24,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RuleCompositeConditionUiComponent } from './rule-composite-condition.ui-component'; -import { NoopTranslateModule } from '@alfresco/adf-core'; -import { By } from '@angular/platform-browser'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { DebugElement } from '@angular/core'; import { compositeConditionWithNestedGroupsMock, @@ -37,6 +36,10 @@ import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-conten describe('RuleCompositeConditionUiComponent', () => { let fixture: ComponentFixture; + let unitTestingUtils: UnitTestingUtils; + + const getSimpleConditionComponents = (): DebugElement[] => unitTestingUtils.getAllByCSS(`.aca-rule-simple-condition`); + const getCompositeConditionComponents = (): DebugElement[] => unitTestingUtils.getAllByCSS(`.aca-rule-composite-condition`); beforeEach(() => { TestBed.configureTestingModule({ @@ -45,18 +48,18 @@ describe('RuleCompositeConditionUiComponent', () => { }); fixture = TestBed.createComponent(RuleCompositeConditionUiComponent); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); }); describe('No conditions', () => { - let noConditionsElement: DebugElement; + const getNoConditionsElementInnerText = (): string => unitTestingUtils.getInnerTextByDataAutomationId('no-conditions'); beforeEach(() => { fixture.detectChanges(); - noConditionsElement = fixture.debugElement.query(By.css(`[data-automation-id="no-conditions"]`)); }); it('should default to no conditions', () => { - const rowElements = fixture.debugElement.queryAll(By.css(`.aca-rule-composite-condition__form__row`)); + const rowElements = unitTestingUtils.getAllByCSS(`.aca-rule-composite-condition__form__row`); expect(rowElements.length).toBe(0); }); @@ -65,14 +68,14 @@ describe('RuleCompositeConditionUiComponent', () => { fixture.componentInstance.childCondition = false; fixture.detectChanges(); - expect((noConditionsElement.nativeElement as HTMLElement).innerText.trim()).toBe('ACA_FOLDER_RULES.RULE_DETAILS.NO_CONDITIONS'); + expect(getNoConditionsElementInnerText()).toBe('ACA_FOLDER_RULES.RULE_DETAILS.NO_CONDITIONS'); }); it('should show a different message if the component is not a root condition group', () => { fixture.componentInstance.childCondition = true; fixture.detectChanges(); - expect((noConditionsElement.nativeElement as HTMLElement).innerText.trim()).toBe('ACA_FOLDER_RULES.RULE_DETAILS.NO_CONDITIONS_IN_GROUP'); + expect(getNoConditionsElementInnerText()).toBe('ACA_FOLDER_RULES.RULE_DETAILS.NO_CONDITIONS_IN_GROUP'); }); }); @@ -80,64 +83,52 @@ describe('RuleCompositeConditionUiComponent', () => { it('should hide the add buttons in read only mode', () => { fixture.componentInstance.readOnly = true; fixture.detectChanges(); - const actionsElement = fixture.debugElement.query(By.css(`[data-automation-id="add-actions"]`)); - expect(actionsElement).toBeNull(); + expect(unitTestingUtils.getByDataAutomationId('add-actions')).toBeNull(); }); it('should hide the more actions button on the right side of the condition', () => { fixture.componentInstance.writeValue(compositeConditionWithOneGroupMock); fixture.componentInstance.readOnly = true; fixture.detectChanges(); - const actionsButtonElements = fixture.debugElement.queryAll(By.css(`[data-automation-id="condition-actions-button"]`)); - expect(actionsButtonElements.length).toBe(0); + expect(unitTestingUtils.getAllByCSS(`[data-automation-id="condition-actions-button"]`).length).toBe(0); }); }); it('should have as many simple condition components as defined in the simpleConditions array', () => { fixture.componentInstance.writeValue(compositeConditionWithThreeConditionMock); fixture.detectChanges(); - const simpleConditionComponents = fixture.debugElement.queryAll(By.css(`.aca-rule-simple-condition`)); - expect(simpleConditionComponents.length).toBe(3); + expect(getSimpleConditionComponents().length).toBe(3); }); it('should have as many composite condition components as defined in the compositeConditions array, including nested', () => { fixture.componentInstance.writeValue(compositeConditionWithNestedGroupsMock); fixture.detectChanges(); - const compositeConditionComponents = fixture.debugElement.queryAll(By.css(`.aca-rule-composite-condition`)); - expect(compositeConditionComponents.length).toBe(3); + expect(getCompositeConditionComponents().length).toBe(3); }); it('should add a simple condition component on click of add condition button', () => { fixture.detectChanges(); - const predicate = By.css(`.aca-rule-simple-condition`); - const simpleConditionComponentsBeforeClick = fixture.debugElement.queryAll(predicate); - expect(simpleConditionComponentsBeforeClick.length).toBe(0); + expect(getSimpleConditionComponents().length).toBe(0); - const addConditionButton = fixture.debugElement.query(By.css(`[data-automation-id="add-condition-button"]`)); - (addConditionButton.nativeElement as HTMLButtonElement).click(); + unitTestingUtils.clickByDataAutomationId('add-condition-button'); fixture.detectChanges(); - const simpleConditionComponentsAfterClick = fixture.debugElement.queryAll(predicate); - expect(simpleConditionComponentsAfterClick.length).toBe(1); + expect(getSimpleConditionComponents().length).toBe(1); }); it('should add a composite condition component on click of add group button', () => { fixture.detectChanges(); - const predicate = By.css(`.aca-rule-composite-condition`); - const compositeConditionComponentsBeforeClick = fixture.debugElement.queryAll(predicate); - expect(compositeConditionComponentsBeforeClick.length).toBe(0); + expect(getCompositeConditionComponents().length).toBe(0); - const addGroupButton = fixture.debugElement.query(By.css(`[data-automation-id="add-group-button"]`)); - (addGroupButton.nativeElement as HTMLButtonElement).click(); + unitTestingUtils.clickByDataAutomationId('add-group-button'); fixture.detectChanges(); - const compositeConditionComponentsAfterClick = fixture.debugElement.queryAll(predicate); - expect(compositeConditionComponentsAfterClick.length).toBe(1); + expect(getCompositeConditionComponents().length).toBe(1); }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts index e7bfb4fcf..8dd5da60a 100644 --- a/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/conditions/rule-simple-condition.ui-component.spec.ts @@ -24,8 +24,6 @@ import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RuleSimpleConditionUiComponent } from './rule-simple-condition.ui-component'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; import { tagMock, mimeTypeMock, simpleConditionUnknownFieldMock, categoriesListMock } from '../../mock/conditions.mock'; import { AlfrescoApiService, AlfrescoApiServiceMock, CategoryService, TagService } from '@alfresco/adf-content-services'; import { of } from 'rxjs'; @@ -37,19 +35,24 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSelectHarness } from '@angular/material/select/testing'; import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; import { AlfrescoMimeType } from '@alfresco/aca-shared'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; describe('RuleSimpleConditionUiComponent', () => { let fixture: ComponentFixture; let categoryService: CategoryService; let loader: HarnessLoader; + let unitTestingUtils: UnitTestingUtils; const fieldSelectAutomationId = 'field-select'; + const comparatorSelectAutomationId = 'comparator-select'; + const comparatorFormFieldAutomationId = 'comparator-form-field'; + const unknownFieldOptionAutomationId = 'unknown-field-option'; + const simpleConditionValueAutomationId = 'simple-condition-value-select'; + const autoCompleteInputFieldAutomationId = 'auto-complete-input-field'; + const autoCompleteSpinnerAutomationId = 'auto-complete-loading-spinner'; + const valueInputAutomationId = 'value-input'; const folderRulesBaseLabel = 'ACA_FOLDER_RULES.RULE_DETAILS.COMPARATORS'; - const getByDataAutomationId = (dataAutomationId: string): DebugElement => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)); - const changeMatSelectValue = async (dataAutomationId: string, value: string) => { const matSelect = await loader.getHarness(MatSelectHarness.with({ selector: `[data-automation-id="${dataAutomationId}"]` })); await matSelect.clickOptions({ selector: `[ng-reflect-value="${value}"]` }); @@ -62,13 +65,6 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.detectChanges(); }; - const setValueInInputField = (inputFieldDataAutomationId: string, value: string) => { - const inputField = fixture.debugElement.query(By.css(`[data-automation-id="${inputFieldDataAutomationId}"]`)).nativeElement; - inputField.value = value; - inputField.dispatchEvent(new Event('input')); - fixture.detectChanges(); - }; - const expectConditionFieldsDisplayedAsOptions = async (conditionFields: RuleConditionField[]): Promise => { loader = TestbedHarnessEnvironment.loader(fixture); const select = await loader.getHarness(MatSelectHarness); @@ -90,20 +86,21 @@ describe('RuleSimpleConditionUiComponent', () => { fixture = TestBed.createComponent(RuleSimpleConditionUiComponent); categoryService = TestBed.inject(CategoryService); loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); }); it('should default the field to the name, the comparator to equals and the value empty', () => { fixture.detectChanges(); - expect(getByDataAutomationId(fieldSelectAutomationId).componentInstance.value).toBe('cm:name'); - expect(getByDataAutomationId('comparator-select').componentInstance.value).toBe('equals'); - expect(getByDataAutomationId('value-input').nativeElement.value).toBe(''); + expect(unitTestingUtils.getByDataAutomationId(fieldSelectAutomationId).componentInstance.value).toBe('cm:name'); + expect(unitTestingUtils.getByDataAutomationId(comparatorSelectAutomationId).componentInstance.value).toBe('equals'); + expect(unitTestingUtils.getByDataAutomationId(valueInputAutomationId).nativeElement.value).toBe(''); }); it('should hide the comparator select box if the type of the field is mimeType', async () => { fixture.componentInstance.mimeTypes = [{ value: '', label: '' } as AlfrescoMimeType]; fixture.detectChanges(); - const comparatorFormField = getByDataAutomationId('comparator-form-field').nativeElement; + const comparatorFormField = unitTestingUtils.getByDataAutomationId(comparatorFormFieldAutomationId).nativeElement; expect(fixture.componentInstance.isComparatorHidden).toBeFalsy(); expect(getComputedStyle(comparatorFormField).display).not.toBe('none'); @@ -116,9 +113,9 @@ describe('RuleSimpleConditionUiComponent', () => { it('should set the comparator to equals if the field is set to a type with different comparators', async () => { spyOn(fixture.componentInstance, 'onChangeField').and.callThrough(); - const comparatorSelect = await loader.getHarness(MatSelectHarness.with({ selector: '[data-automation-id="comparator-select"]' })); + const comparatorSelect = await loader.getHarness(MatSelectHarness.with({ selector: `[data-automation-id="${comparatorSelectAutomationId}"]` })); - await changeMatSelectValue('comparator-select', 'contains'); + await changeMatSelectValue(comparatorSelectAutomationId, 'contains'); expect(await comparatorSelect.getValueText()).toBe(folderRulesBaseLabel + '.CONTAINS'); await changeMatSelectValue(fieldSelectAutomationId, 'size'); @@ -130,25 +127,23 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.componentInstance.writeValue(simpleConditionUnknownFieldMock); fixture.detectChanges(); - expect(getByDataAutomationId(fieldSelectAutomationId).componentInstance.value).toBe(simpleConditionUnknownFieldMock.field); - const matSelect = getByDataAutomationId(fieldSelectAutomationId).nativeElement; + expect(unitTestingUtils.getByDataAutomationId(fieldSelectAutomationId).componentInstance.value).toBe(simpleConditionUnknownFieldMock.field); + const matSelect = unitTestingUtils.getByDataAutomationId(fieldSelectAutomationId).nativeElement; matSelect.click(); fixture.detectChanges(); - const unknownOptionMatOption = getByDataAutomationId('unknown-field-option'); - expect(unknownOptionMatOption).not.toBeNull(); - expect((unknownOptionMatOption.nativeElement as HTMLElement).innerText.trim()).toBe(simpleConditionUnknownFieldMock.field); + expect(unitTestingUtils.getInnerTextByDataAutomationId(unknownFieldOptionAutomationId).trim()).toBe(simpleConditionUnknownFieldMock.field); }); it('should remove the option for the unknown field as soon as another option is selected', async () => { fixture.componentInstance.writeValue(simpleConditionUnknownFieldMock); fixture.detectChanges(); await changeMatSelectValue(fieldSelectAutomationId, 'cm:name'); - const matSelect = getByDataAutomationId(fieldSelectAutomationId).nativeElement; + const matSelect = unitTestingUtils.getByDataAutomationId(fieldSelectAutomationId).nativeElement; matSelect.click(); fixture.detectChanges(); - const unknownOptionMatOption = getByDataAutomationId('unknown-field-option'); + const unknownOptionMatOption = unitTestingUtils.getByDataAutomationId(unknownFieldOptionAutomationId); expect(unknownOptionMatOption).toBeNull(); }); @@ -178,7 +173,7 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.componentInstance.onChangeField(); fixture.detectChanges(); - expect(getByDataAutomationId('simple-condition-value-select')).toBeTruthy(); + expect(unitTestingUtils.getByDataAutomationId(simpleConditionValueAutomationId)).toBeTruthy(); expect(fixture.componentInstance.form.get('parameter').value).toEqual(mockMimeTypes[0].value); }); @@ -186,12 +181,12 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.componentInstance.writeValue(mimeTypeMock); fixture.detectChanges(); - expect(getByDataAutomationId('simple-condition-value-select')).toBeTruthy(); + expect(unitTestingUtils.getByDataAutomationId(simpleConditionValueAutomationId)).toBeTruthy(); fixture.componentInstance.writeValue(tagMock); fixture.detectChanges(); - expect(getByDataAutomationId('value-input').nativeElement.value).toBe(''); + expect(unitTestingUtils.getByDataAutomationId(valueInputAutomationId).nativeElement.value).toBe(''); }); it('should show loading spinner while auto-complete options are fetched, and then remove it once it is received', fakeAsync(async () => { @@ -199,12 +194,12 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.detectChanges(); await changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); - getByDataAutomationId('auto-complete-input-field')?.nativeElement?.click(); - let loadingSpinner = getByDataAutomationId('auto-complete-loading-spinner'); + unitTestingUtils.getByDataAutomationId(autoCompleteInputFieldAutomationId)?.nativeElement?.click(); + let loadingSpinner = unitTestingUtils.getByDataAutomationId(autoCompleteSpinnerAutomationId); expect(loadingSpinner).not.toBeNull(); tick(1000); fixture.detectChanges(); - loadingSpinner = getByDataAutomationId('auto-complete-loading-spinner'); + loadingSpinner = unitTestingUtils.getByDataAutomationId(autoCompleteSpinnerAutomationId); expect(loadingSpinner).toBeNull(); discardPeriodicTasks(); })); @@ -217,7 +212,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should hide the comparator select box if the type of the field is autoComplete', async () => { const autoCompleteField = 'category'; fixture.detectChanges(); - const comparatorFormField = getByDataAutomationId('comparator-form-field').nativeElement; + const comparatorFormField = unitTestingUtils.getByDataAutomationId(comparatorFormFieldAutomationId).nativeElement; expect(fixture.componentInstance.isComparatorHidden).toBeFalsy(); expect(getComputedStyle(comparatorFormField).display).not.toBe('none'); @@ -230,7 +225,7 @@ describe('RuleSimpleConditionUiComponent', () => { it('should hide the comparator select box if the type of the field is special', async () => { fixture.detectChanges(); - const comparatorFormField = getByDataAutomationId('comparator-form-field').nativeElement; + const comparatorFormField = unitTestingUtils.getByDataAutomationId(comparatorFormFieldAutomationId).nativeElement; expect(fixture.componentInstance.isComparatorHidden).toBeFalsy(); expect(getComputedStyle(comparatorFormField).display).not.toBe('none'); @@ -245,7 +240,7 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.detectChanges(); await changeMatSelectValue(fieldSelectAutomationId, 'category'); - expect(getByDataAutomationId('auto-complete-input-field')).toBeTruthy(); + expect(unitTestingUtils.getByDataAutomationId(autoCompleteInputFieldAutomationId)).toBeTruthy(); expect(fixture.componentInstance.form.get('parameter').value).toEqual(''); }); @@ -265,7 +260,7 @@ describe('RuleSimpleConditionUiComponent', () => { tick(500); expect(categoryService.searchCategories).toHaveBeenCalledWith(''); - setValueInInputField('auto-complete-input-field', categoryValue); + unitTestingUtils.fillInputByDataAutomationId(autoCompleteInputFieldAutomationId, categoryValue); tick(500); expect(categoryService.searchCategories).toHaveBeenCalledWith(categoryValue); })); @@ -274,9 +269,9 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.detectChanges(); await changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); - getByDataAutomationId('auto-complete-input-field')?.nativeElement?.click(); + unitTestingUtils.getByDataAutomationId(autoCompleteInputFieldAutomationId)?.nativeElement?.click(); await changeMatAutocompleteValue(categoriesListMock.list.entries[0].entry.id); - const displayValue = getByDataAutomationId('auto-complete-input-field')?.nativeElement?.value; + const displayValue = unitTestingUtils.getByDataAutomationId(autoCompleteInputFieldAutomationId)?.nativeElement?.value; expect(displayValue).toBe('category/path/1/FakeCategory1'); discardPeriodicTasks(); })); @@ -285,7 +280,7 @@ describe('RuleSimpleConditionUiComponent', () => { fixture.detectChanges(); await changeMatSelectValue(fieldSelectAutomationId, 'category'); tick(500); - const autoCompleteInputField = getByDataAutomationId('auto-complete-input-field')?.nativeElement; + const autoCompleteInputField = unitTestingUtils.getByDataAutomationId(autoCompleteInputFieldAutomationId)?.nativeElement; autoCompleteInputField.value = 'FakeCat'; autoCompleteInputField.dispatchEvent(new Event('focusout')); const parameterValue = fixture.componentInstance.form.get('parameter').value; diff --git a/projects/aca-content/folder-rules/src/rule-details/edit-rule-dialog.smart-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/edit-rule-dialog.ui-component.spec.ts similarity index 64% rename from projects/aca-content/folder-rules/src/rule-details/edit-rule-dialog.smart-component.spec.ts rename to projects/aca-content/folder-rules/src/rule-details/edit-rule-dialog.ui-component.spec.ts index 4472646a8..b8cde03d1 100644 --- a/projects/aca-content/folder-rules/src/rule-details/edit-rule-dialog.smart-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/edit-rule-dialog.ui-component.spec.ts @@ -24,21 +24,26 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { EditRuleDialogOptions, EditRuleDialogUiComponent } from './edit-rule-dialog.ui-component'; -import { By } from '@angular/platform-browser'; import { RuleDetailsUiComponent } from './rule-details.ui-component'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { of, timer } from 'rxjs'; import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { Rule } from '../model/rule.model'; describe('EditRuleDialogSmartComponent', () => { let fixture: ComponentFixture; + let component: EditRuleDialogUiComponent; + let unitTestingUtils: UnitTestingUtils; const dialogRef = { close: jasmine.createSpy('close'), open: jasmine.createSpy('open') }; + const getDialogSubmit = (): HTMLButtonElement => unitTestingUtils.getByDataAutomationId('edit-rule-dialog-submit').nativeElement; + const getDialogTitle = (): HTMLDivElement => unitTestingUtils.getByDataAutomationId('edit-rule-dialog-title').nativeElement; + const setupBeforeEach = (dialogOptions: EditRuleDialogOptions = { actionDefinitions$: of([]), parameterConstraints$: of([]) }) => { TestBed.configureTestingModule({ imports: [NoopTranslateModule, EditRuleDialogUiComponent], @@ -51,6 +56,8 @@ describe('EditRuleDialogSmartComponent', () => { fixture = TestBed.createComponent(EditRuleDialogUiComponent); fixture.detectChanges(); + component = fixture.componentInstance; + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); }; describe('No dialog options passed / indifferent', () => { @@ -59,38 +66,33 @@ describe('EditRuleDialogSmartComponent', () => { }); it('should activate the submit button only when a valid state is received', async () => { - const submitButton = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement; - const ruleDetails = fixture.debugElement.query(By.directive(RuleDetailsUiComponent)).componentInstance as RuleDetailsUiComponent; + const ruleDetails = unitTestingUtils.getByDirective(RuleDetailsUiComponent).componentInstance as RuleDetailsUiComponent; ruleDetails.formValidationChanged.emit(true); fixture.detectChanges(); // timer needed to wait for the next tick to avoid ExpressionChangedAfterItHasBeenCheckedError await timer(1).toPromise(); fixture.detectChanges(); - expect(submitButton.disabled).toBeFalsy(); + expect(getDialogSubmit().disabled).toBeFalsy(); ruleDetails.formValidationChanged.emit(false); fixture.detectChanges(); await timer(1).toPromise(); fixture.detectChanges(); - expect(submitButton.disabled).toBeTruthy(); + expect(getDialogSubmit().disabled).toBeTruthy(); }); it('should show a "create" label in the title', () => { - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement; - - expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE'); + expect(getDialogTitle().innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE'); }); it('should show a "create" label in the submit button', () => { - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement; - - expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE'); + expect(getDialogSubmit().innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE'); }); it('should set correct title and submitLabel for create mode', () => { - expect(fixture.componentInstance.title).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE'); - expect(fixture.componentInstance.submitLabel).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE'); + expect(component.title).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE_TITLE'); + expect(component.submitLabel).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.CREATE'); }); }); @@ -108,20 +110,36 @@ describe('EditRuleDialogSmartComponent', () => { }); it('should show an "update" label in the title', () => { - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-title"]')).nativeElement as HTMLDivElement; - - expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE'); + expect(getDialogTitle().innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE'); }); it('should show an "update" label in the submit button', () => { - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="edit-rule-dialog-submit"]')).nativeElement as HTMLButtonElement; - - expect(titleElement.innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE'); + expect(getDialogSubmit().innerText.trim()).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE'); }); it('should set correct title and submitLabel for update mode', () => { - expect(fixture.componentInstance.title).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE'); - expect(fixture.componentInstance.submitLabel).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE'); + expect(component.title).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE_TITLE'); + expect(component.submitLabel).toBe('ACA_FOLDER_RULES.EDIT_RULE_DIALOG.UPDATE'); }); }); + + it('should emit submitted event with form value when onSubmit is called', () => { + setupBeforeEach(); + let emittedValue: Partial | undefined; + + const testFormValue: Partial = { + id: 'test-id', + name: 'Test Rule', + description: 'Test description' + }; + + component.submitted.subscribe((value) => { + emittedValue = value; + }); + + component.formValue = testFormValue; + component.onSubmit(); + + expect(emittedValue).toEqual(testFormValue); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/options/rule-options.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/options/rule-options.ui-component.spec.ts index bbc4a48e1..4dc5f364e 100644 --- a/projects/aca-content/folder-rules/src/rule-details/options/rule-options.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/options/rule-options.ui-component.spec.ts @@ -25,8 +25,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DebugElement, SimpleChange } from '@angular/core'; import { RuleOptionsUiComponent } from './rule-options.ui-component'; -import { NoopTranslateModule } from '@alfresco/adf-core'; -import { By } from '@angular/platform-browser'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { errorScriptConstraintMock } from '../../mock/actions.mock'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; @@ -37,23 +36,27 @@ describe('RuleOptionsUiComponent', () => { let fixture: ComponentFixture; let component: RuleOptionsUiComponent; let loader: HarnessLoader; + let unitTestingUtils: UnitTestingUtils; - const getByDataAutomationId = (dataAutomationId: string): DebugElement => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)); + const errorScriptSelectAutomationId = 'rule-option-select-errorScript'; + const asynchronousCheckboxAutomationId = 'rule-option-checkbox-asynchronous'; + + const getErrorScriptSelect = (): DebugElement => unitTestingUtils.getByDataAutomationId(errorScriptSelectAutomationId); + const getAsynchronousCheckbox = (): DebugElement => unitTestingUtils.getByDataAutomationId(asynchronousCheckboxAutomationId); + const getErrorScriptFormField = (): DebugElement => unitTestingUtils.getByDataAutomationId('rule-option-form-field-errorScript'); + const getInheritableCheckbox = (): DebugElement => unitTestingUtils.getByDataAutomationId('rule-option-checkbox-inheritable'); + const getDisabledCheckbox = (): DebugElement => unitTestingUtils.getByDataAutomationId('rule-option-checkbox-disabled'); + const getEnabledCheckbox = (): DebugElement => unitTestingUtils.getByDataAutomationId('rule-option-checkbox-enabled'); const toggleMatCheckbox = (dataAutomationId: string) => { - (getByDataAutomationId(dataAutomationId).nativeElement as HTMLElement).querySelector('input').click(); + (unitTestingUtils.getByDataAutomationId(dataAutomationId).nativeElement as HTMLElement).querySelector('input').click(); }; const testErrorScriptFormFieldVisibility = (isVisible: boolean) => { if (isVisible) { - expect((getByDataAutomationId('rule-option-form-field-errorScript').nativeElement as HTMLElement).classList).not.toContain( - 'aca-hide-error-script-dropdown' - ); + expect((getErrorScriptFormField().nativeElement as HTMLElement).classList).not.toContain('aca-hide-error-script-dropdown'); } else { - expect((getByDataAutomationId('rule-option-form-field-errorScript').nativeElement as HTMLElement).classList).toContain( - 'aca-hide-error-script-dropdown' - ); + expect((getErrorScriptFormField().nativeElement as HTMLElement).classList).toContain('aca-hide-error-script-dropdown'); } }; @@ -66,6 +69,7 @@ describe('RuleOptionsUiComponent', () => { fixture = TestBed.createComponent(RuleOptionsUiComponent); component = fixture.componentInstance; loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); component.writeValue({ isEnabled: true, @@ -78,9 +82,9 @@ describe('RuleOptionsUiComponent', () => { it('should be able to write to the component', () => { fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeFalsy(); - expect(getByDataAutomationId('rule-option-checkbox-inheritable').componentInstance.checked).toBeFalsy(); - expect(getByDataAutomationId('rule-option-checkbox-disabled').componentInstance.checked).toBeFalsy(); + expect(getAsynchronousCheckbox().componentInstance.checked).toBeFalsy(); + expect(getInheritableCheckbox().componentInstance.checked).toBeFalsy(); + expect(getDisabledCheckbox().componentInstance.checked).toBeFalsy(); testErrorScriptFormFieldVisibility(false); component.writeValue({ @@ -91,29 +95,29 @@ describe('RuleOptionsUiComponent', () => { }); fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeTruthy(); - expect(getByDataAutomationId('rule-option-checkbox-inheritable').componentInstance.checked).toBeTruthy(); - expect(getByDataAutomationId('rule-option-checkbox-disabled').componentInstance.checked).toBeTruthy(); + expect(getAsynchronousCheckbox().componentInstance.checked).toBeTruthy(); + expect(getInheritableCheckbox().componentInstance.checked).toBeTruthy(); + expect(getDisabledCheckbox().componentInstance.checked).toBeTruthy(); testErrorScriptFormFieldVisibility(true); }); it('should enable selector when async checkbox is truthy', () => { fixture.detectChanges(); - toggleMatCheckbox('rule-option-checkbox-asynchronous'); + toggleMatCheckbox(asynchronousCheckboxAutomationId); fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeTruthy(); - expect(getByDataAutomationId('rule-option-select-errorScript').componentInstance.disabled).toBeFalsy(); + expect(getAsynchronousCheckbox().componentInstance.checked).toBeTruthy(); + expect(getErrorScriptSelect().componentInstance.disabled).toBeFalsy(); }); it('should hide disabled checkbox and unselected checkboxes in read-only mode', () => { component.readOnly = true; fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous')).toBeFalsy(); - expect(getByDataAutomationId('rule-option-checkbox-inheritable')).toBeFalsy(); - expect(getByDataAutomationId('rule-option-checkbox-enabled')).toBeFalsy(); - expect(getByDataAutomationId('rule-option-select-errorScript')).toBeFalsy(); + expect(getAsynchronousCheckbox()).toBeFalsy(); + expect(getInheritableCheckbox()).toBeFalsy(); + expect(getEnabledCheckbox()).toBeFalsy(); + expect(getErrorScriptSelect()).toBeFalsy(); component.writeValue({ isEnabled: false, @@ -123,10 +127,10 @@ describe('RuleOptionsUiComponent', () => { }); fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous')).toBeTruthy(); - expect(getByDataAutomationId('rule-option-checkbox-inheritable')).toBeTruthy(); - expect(getByDataAutomationId('rule-option-checkbox-enabled')).toBeFalsy(); - expect(getByDataAutomationId('rule-option-select-errorScript')).toBeTruthy(); + expect(getAsynchronousCheckbox()).toBeTruthy(); + expect(getInheritableCheckbox()).toBeTruthy(); + expect(getEnabledCheckbox()).toBeFalsy(); + expect(getErrorScriptSelect()).toBeTruthy(); }); it('should populate the error script dropdown with scripts', async () => { @@ -140,7 +144,7 @@ describe('RuleOptionsUiComponent', () => { component.ngOnChanges({ errorScriptConstraint: {} as SimpleChange }); fixture.detectChanges(); - (getByDataAutomationId('rule-option-select-errorScript').nativeElement as HTMLElement).click(); + unitTestingUtils.clickByDataAutomationId(errorScriptSelectAutomationId); fixture.detectChanges(); const selection = await loader.getHarness(MatSelectHarness); @@ -161,7 +165,7 @@ describe('RuleOptionsUiComponent', () => { component.errorScriptConstraint = errorScriptConstraintMock; fixture.detectChanges(); - const matFormField = fixture.debugElement.query(By.css('[data-automation-id="rule-option-form-field-errorScript"')); + const matFormField = getErrorScriptFormField(); fixture.detectChanges(); expect(matFormField).not.toBeNull(); expect(matFormField.componentInstance['floatLabel']).toBe('always'); @@ -177,10 +181,10 @@ describe('RuleOptionsUiComponent', () => { }); fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeTrue(); - expect(getByDataAutomationId('rule-option-checkbox-inheritable').componentInstance.checked).toBeTrue(); - expect(getByDataAutomationId('rule-option-checkbox-disabled').componentInstance.checked).toBeTrue(); - expect(getByDataAutomationId('rule-option-select-errorScript').componentInstance.value).toEqual('1234'); + expect(getAsynchronousCheckbox().componentInstance.checked).toBeTrue(); + expect(getInheritableCheckbox().componentInstance.checked).toBeTrue(); + expect(getDisabledCheckbox().componentInstance.checked).toBeTrue(); + expect(getErrorScriptSelect().componentInstance.value).toEqual('1234'); component.writeValue({ isEnabled: false, @@ -190,9 +194,9 @@ describe('RuleOptionsUiComponent', () => { }); fixture.detectChanges(); - expect(getByDataAutomationId('rule-option-checkbox-asynchronous').componentInstance.checked).toBeFalse(); - expect(getByDataAutomationId('rule-option-checkbox-inheritable').componentInstance.checked).toBeTrue(); - expect(getByDataAutomationId('rule-option-checkbox-disabled').componentInstance.checked).toBeTrue(); + expect(getAsynchronousCheckbox().componentInstance.checked).toBeFalse(); + expect(getInheritableCheckbox().componentInstance.checked).toBeTrue(); + expect(getDisabledCheckbox().componentInstance.checked).toBeTrue(); testErrorScriptFormFieldVisibility(false); }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts index 2633e3dc0..459d14645 100644 --- a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.spec.ts @@ -25,21 +25,23 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RuleDetailsUiComponent } from './rule-details.ui-component'; import { Rule } from '../model/rule.model'; -import { By } from '@angular/platform-browser'; import { RuleTriggersUiComponent } from './triggers/rule-triggers.ui-component'; import { RuleOptionsUiComponent } from './options/rule-options.ui-component'; import { RuleActionListUiComponent } from './actions/rule-action-list.ui-component'; import { AlfrescoApiService, AlfrescoApiServiceMock, CategoryService } from '@alfresco/adf-content-services'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; +import { ActionParameterConstraint } from '../model/action-parameter-constraint.model'; describe('RuleDetailsUiComponent', () => { let fixture: ComponentFixture; let component: RuleDetailsUiComponent; + let unitTestingUtils: UnitTestingUtils; const testValue: Partial = { id: 'rule-id', name: 'Rule name', description: 'This is the description of the rule', + isShared: false, triggers: ['update', 'outbound'], isAsynchronous: true, isInheritable: true, @@ -47,11 +49,8 @@ describe('RuleDetailsUiComponent', () => { errorScript: '' }; - const getHtmlElement = (dataAutomationId: string) => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`))?.nativeElement as T; - - const getComponentInstance = (dataAutomationId: string) => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`))?.componentInstance as T; + const getHtmlElement = (dataAutomationId: string): T => unitTestingUtils.getByDataAutomationId(dataAutomationId)?.nativeElement as T; + const getComponentInstance = (dataAutomationId: string): T => unitTestingUtils.getByDataAutomationId(dataAutomationId)?.componentInstance as T; beforeEach(() => { TestBed.configureTestingModule({ @@ -61,6 +60,7 @@ describe('RuleDetailsUiComponent', () => { fixture = TestBed.createComponent(RuleDetailsUiComponent); component = fixture.componentInstance; + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); }); it('should fill the form out with initial values', () => { @@ -147,8 +147,7 @@ describe('RuleDetailsUiComponent', () => { describe('RuleActionListUiComponent', () => { let categoryService: CategoryService; - const getRuleActionsListComponent = (): RuleActionListUiComponent => - fixture.debugElement.query(By.directive(RuleActionListUiComponent)).componentInstance; + const getRuleActionsListComponent = (): RuleActionListUiComponent => unitTestingUtils.getByDirective(RuleActionListUiComponent).componentInstance; beforeEach(() => { categoryService = TestBed.inject(CategoryService); @@ -193,4 +192,25 @@ describe('RuleDetailsUiComponent', () => { expect(getRuleActionsListComponent().actionDefinitions).toBe(component.actionDefinitions); }); }); + + it('should return description form control', () => { + component.value = testValue; + fixture.detectChanges(); + + const descriptionControl = component.description; + + expect(descriptionControl.value).toBe(testValue.description); + }); + + it('should set errorScriptConstraint when parameterConstraints contains script-ref', () => { + const mockConstraints: ActionParameterConstraint[] = [ + { name: 'script-ref', constraints: [] }, + { name: 'other-constraint', constraints: [] } + ]; + + component.parameterConstraints = mockConstraints; + component.ngOnInit(); + + expect(component.errorScriptConstraint).toBe(mockConstraints[0]); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts index 96dd4070e..cdc8c6d7b 100644 --- a/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts +++ b/projects/aca-content/folder-rules/src/rule-details/rule-details.ui-component.ts @@ -84,6 +84,7 @@ export class RuleDetailsUiComponent implements OnInit { id: newValue.id || FolderRulesService.emptyRule.id, name: newValue.name || FolderRulesService.emptyRule.name, description: newValue.description || FolderRulesService.emptyRule.description, + isShared: newValue.isShared || FolderRulesService.emptyRule.isShared, triggers: newValue.triggers || FolderRulesService.emptyRule.triggers, conditions: newValue.conditions || FolderRulesService.emptyRule.conditions, actions: newValue.actions || FolderRulesService.emptyRule.actions, @@ -146,6 +147,7 @@ export class RuleDetailsUiComponent implements OnInit { id: new UntypedFormControl(this.value.id), name: new UntypedFormControl(this.value.name || '', Validators.required), description: new UntypedFormControl(this.value.description || ''), + isShared: new UntypedFormControl(this.value.isShared || false), triggers: new UntypedFormControl(this.value.triggers || ['inbound'], Validators.required), conditions: new UntypedFormControl( this.value.conditions || { diff --git a/projects/aca-content/folder-rules/src/rule-details/triggers/rule-triggers.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-details/triggers/rule-triggers.ui-component.spec.ts index a15f5d1dc..cdd21724a 100644 --- a/projects/aca-content/folder-rules/src/rule-details/triggers/rule-triggers.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-details/triggers/rule-triggers.ui-component.spec.ts @@ -24,20 +24,27 @@ import { RuleTriggersUiComponent } from './rule-triggers.ui-component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopTranslateModule } from '@alfresco/adf-core'; -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { DebugElement } from '@angular/core'; describe('RuleTriggerUiComponent', () => { let fixture: ComponentFixture; let component: RuleTriggersUiComponent; + let unitTestingUtils: UnitTestingUtils; + let loader: HarnessLoader; - const getByDataAutomationId = (dataAutomationId: string): DebugElement => - fixture.debugElement.query(By.css(`[data-automation-id="${dataAutomationId}"]`)); + const checkboxUpdateAutomationId = 'rule-trigger-checkbox-update'; + const checkboxInboundAutomationId = 'rule-trigger-checkbox-inbound'; + const getCheckboxInbound = (): DebugElement => unitTestingUtils.getByDataAutomationId(checkboxInboundAutomationId); + const getCheckboxUpdate = (): DebugElement => unitTestingUtils.getByDataAutomationId(checkboxUpdateAutomationId); + const getCheckboxOutbound = (): DebugElement => unitTestingUtils.getByDataAutomationId('rule-trigger-checkbox-outbound'); - const toggleMatCheckbox = (dataAutomationId: string) => { - (getByDataAutomationId(dataAutomationId).nativeElement as HTMLElement).querySelector('input').click(); + const toggleMatCheckbox = async (dataAutomationId: string) => { + const checkbox = await unitTestingUtils.getMatCheckboxByDataAutomationId(dataAutomationId); + await checkbox.toggle(); }; beforeEach(() => { @@ -48,15 +55,17 @@ describe('RuleTriggerUiComponent', () => { fixture = TestBed.createComponent(RuleTriggersUiComponent); component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement, loader); }); it('should default to only the inbound checkbox', () => { fixture.detectChanges(); expect(component.value).toEqual(['inbound']); - expect(getByDataAutomationId('rule-trigger-checkbox-inbound').componentInstance.checked).toBeTruthy(); - expect(getByDataAutomationId('rule-trigger-checkbox-update').componentInstance.checked).toBeFalsy(); - expect(getByDataAutomationId('rule-trigger-checkbox-outbound').componentInstance.checked).toBeFalsy(); + expect(getCheckboxInbound().componentInstance.checked).toBeTruthy(); + expect(getCheckboxUpdate().componentInstance.checked).toBeFalsy(); + expect(getCheckboxOutbound().componentInstance.checked).toBeFalsy(); }); it('should change the checked boxes when the value is written to', () => { @@ -65,25 +74,25 @@ describe('RuleTriggerUiComponent', () => { fixture.detectChanges(); expect(component.value).toEqual(['update', 'outbound']); - expect(getByDataAutomationId('rule-trigger-checkbox-inbound').componentInstance.checked).toBeFalsy(); - expect(getByDataAutomationId('rule-trigger-checkbox-update').componentInstance.checked).toBeTruthy(); - expect(getByDataAutomationId('rule-trigger-checkbox-outbound').componentInstance.checked).toBeTruthy(); + expect(getCheckboxInbound().componentInstance.checked).toBeFalsy(); + expect(getCheckboxUpdate().componentInstance.checked).toBeTruthy(); + expect(getCheckboxOutbound().componentInstance.checked).toBeTruthy(); }); - it('should update the value when a checkbox is checked', () => { + it('should update the value when a checkbox is checked', async () => { const onChangeSpy = spyOn(component, 'onChange'); fixture.detectChanges(); - toggleMatCheckbox('rule-trigger-checkbox-update'); + await toggleMatCheckbox(checkboxUpdateAutomationId); fixture.detectChanges(); expect(component.value).toEqual(['inbound', 'update']); expect(onChangeSpy).toHaveBeenCalledWith(['inbound', 'update']); }); - it('should update the value when a checkbox is unchecked', () => { + it('should update the value when a checkbox is unchecked', async () => { const onChangeSpy = spyOn(component, 'onChange'); fixture.detectChanges(); - toggleMatCheckbox('rule-trigger-checkbox-inbound'); + await toggleMatCheckbox(checkboxInboundAutomationId); fixture.detectChanges(); expect(component.value).toEqual([]); @@ -95,12 +104,12 @@ describe('RuleTriggerUiComponent', () => { component.writeValue(['update', 'outbound']); fixture.detectChanges(); - expect(getByDataAutomationId('rule-trigger-checkbox-inbound')).toBeNull(); - expect(getByDataAutomationId('rule-trigger-checkbox-update')).toBeNull(); - expect(getByDataAutomationId('rule-trigger-checkbox-outbound')).toBeNull(); + expect(getCheckboxInbound()).toBeNull(); + expect(getCheckboxUpdate()).toBeNull(); + expect(getCheckboxOutbound()).toBeNull(); - expect(getByDataAutomationId('rule-trigger-value-inbound')).toBeNull(); - expect(getByDataAutomationId('rule-trigger-value-update')).not.toBeNull(); - expect(getByDataAutomationId('rule-trigger-value-outbound')).not.toBeNull(); + expect(unitTestingUtils.getByDataAutomationId('rule-trigger-value-inbound')).toBeNull(); + expect(unitTestingUtils.getByDataAutomationId('rule-trigger-value-update')).not.toBeNull(); + expect(unitTestingUtils.getByDataAutomationId('rule-trigger-value-outbound')).not.toBeNull(); }); }); diff --git a/projects/aca-content/folder-rules/src/rule-details/validators/rule-composite-condition.validator.spec.ts b/projects/aca-content/folder-rules/src/rule-details/validators/rule-composite-condition.validator.spec.ts new file mode 100644 index 000000000..73c374be2 --- /dev/null +++ b/projects/aca-content/folder-rules/src/rule-details/validators/rule-composite-condition.validator.spec.ts @@ -0,0 +1,80 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { FormControl, ValidatorFn } from '@angular/forms'; +import { ruleCompositeConditionValidator } from './rule-composite-condition.validator'; +import { RuleCompositeCondition } from '../../model/rule-composite-condition.model'; + +describe('ruleCompositeConditionValidator', () => { + let validatorFn: ValidatorFn; + + beforeEach(() => { + validatorFn = ruleCompositeConditionValidator(); + }); + + it('should return null for root condition with empty compositeConditions and empty simpleConditions', () => { + const mockCondition = { + compositeConditions: [], + simpleConditions: [] + } as RuleCompositeCondition; + + const control = new FormControl(mockCondition); + expect(validatorFn(control)).toBeNull(); + }); + + it('should return validation error for nested condition with empty simpleConditions and no nested compositeConditions', () => { + const mockCondition = { + compositeConditions: [ + { + compositeConditions: [], + simpleConditions: [] + } + ], + simpleConditions: [] + } as RuleCompositeCondition; + + const control = new FormControl(mockCondition); + expect(validatorFn(control)).toEqual({ ruleCompositeConditionInvalid: true }); + }); + + it('should return validation error for deeply nested invalid composite conditions', () => { + const mockCondition = { + compositeConditions: [ + { + compositeConditions: [ + { + compositeConditions: [], + simpleConditions: [] + } + ], + simpleConditions: [] + } + ], + simpleConditions: [] + } as RuleCompositeCondition; + + const control = new FormControl(mockCondition); + expect(validatorFn(control)).toEqual({ ruleCompositeConditionInvalid: true }); + }); +}); diff --git a/projects/aca-content/folder-rules/src/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts index 220cf43b8..b6914be1a 100644 --- a/projects/aca-content/folder-rules/src/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-list/rule-list-grouping/rule-list-grouping.ui-component.spec.ts @@ -25,14 +25,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RuleListGroupingUiComponent } from './rule-list-grouping.ui-component'; import { ruleListGroupingItemsMock, rulesMock } from '../../mock/rules.mock'; -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; +import { RuleSet } from '../../model/rule-set.model'; describe('RuleListGroupingUiComponent', () => { let component: RuleListGroupingUiComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; + let unitTestingUtils: UnitTestingUtils; beforeEach(() => { TestBed.configureTestingModule({ @@ -41,7 +40,7 @@ describe('RuleListGroupingUiComponent', () => { fixture = TestBed.createComponent(RuleListGroupingUiComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); }); it('should display the list of rules', () => { @@ -51,16 +50,78 @@ describe('RuleListGroupingUiComponent', () => { fixture.detectChanges(); - const rules = debugElement.queryAll(By.css('.aca-rule-list-item')); + const rules = unitTestingUtils.getAllByCSS('.aca-rule-list-item'); expect(rules).toBeTruthy('Could not find rules'); expect(rules.length).toBe(2, 'Unexpected number of rules'); - const rule = debugElement.query(By.css('.aca-rule-list-item:first-child')); - const name = rule.query(By.css('.aca-rule-list-item__header__name')); - const description = rule.query(By.css('.aca-rule-list-item__description')); + const name = unitTestingUtils.getByCSS('.aca-rule-list-item:first-child .aca-rule-list-item__header__name'); + const description = unitTestingUtils.getByCSS('.aca-rule-list-item:first-child .aca-rule-list-item__description'); expect(name.nativeElement.textContent).toBe(rulesMock[0].name); expect(description.nativeElement.textContent).toBe(rulesMock[0].description); }); + + it('should emit selectRule event with rule when onRuleClicked is called', () => { + spyOn(component.selectRule, 'emit'); + const mockRule = rulesMock[0]; + + component.onRuleClicked(mockRule); + + expect(component.selectRule.emit).toHaveBeenCalledWith(mockRule); + }); + + it('should return true when rule is selected', () => { + const mockRule = rulesMock[0]; + component.selectedRule = mockRule; + + const result = component.isSelected(mockRule); + + expect(result).toBe(true); + }); + + it('should return false when rule is not selected', () => { + const [mockRule, differentRule] = rulesMock; + component.selectedRule = mockRule; + + const result = component.isSelected(differentRule); + + expect(result).toBe(false); + }); + + it('should return false when no rule is selected', () => { + const mockRule = rulesMock[0]; + component.selectedRule = null; + + const result = component.isSelected(mockRule); + + expect(result).toBe(false); + }); + + it('should emit ruleEnabledChanged event with tuple when onEnabledChanged is called', () => { + spyOn(component.ruleEnabledChanged, 'emit'); + const mockRule = rulesMock[0]; + const isEnabled = true; + + component.onEnabledChanged(mockRule, isEnabled); + + expect(component.ruleEnabledChanged.emit).toHaveBeenCalledWith([mockRule, isEnabled]); + }); + + it('should emit loadMoreRules event with ruleSet when onClickLoadMoreRules is called', () => { + spyOn(component.loadMoreRules, 'emit'); + const mockRuleSet = { id: 'test-rule-set' } as RuleSet; + + component.onClickLoadMoreRules(mockRuleSet); + + expect(component.loadMoreRules.emit).toHaveBeenCalledWith(mockRuleSet); + }); + + it('should emit loadMoreRuleSets event when onClickLoadMoreRuleSets is called', () => { + spyOn(component.loadMoreRuleSets, 'emit'); + + component.onClickLoadMoreRuleSets(); + + expect(component.loadMoreRuleSets.emit).toHaveBeenCalled(); + }); }); diff --git a/projects/aca-content/folder-rules/src/rule-list/rule-list-item/rule-list-item.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-list/rule-list-item/rule-list-item.ui-component.spec.ts new file mode 100644 index 000000000..f6dc291b8 --- /dev/null +++ b/projects/aca-content/folder-rules/src/rule-list/rule-list-item/rule-list-item.ui-component.spec.ts @@ -0,0 +1,116 @@ +/*! + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RuleListItemUiComponent } from './rule-list-item.ui-component'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; +import { Rule } from '../../model/rule.model'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; + +describe('RuleListItemUiComponent', () => { + let component: RuleListItemUiComponent; + let fixture: ComponentFixture; + let loader: HarnessLoader; + let unitTestingUtils: UnitTestingUtils; + + const mockRule = { + id: 'test-rule-id', + name: 'Test Rule', + description: 'Test rule description', + isEnabled: true + } as Rule; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopTranslateModule, RuleListItemUiComponent] + }); + + fixture = TestBed.createComponent(RuleListItemUiComponent); + component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); + component.rule = mockRule; + }); + + it('should display rule name and description', () => { + fixture.detectChanges(); + + const nameElement = unitTestingUtils.getByCSS('.aca-rule-list-item__header__name'); + const descriptionElement = unitTestingUtils.getByCSS('.aca-rule-list-item__description'); + + expect(nameElement.nativeElement.textContent.trim()).toBe(mockRule.name); + expect(descriptionElement.nativeElement.textContent.trim()).toBe(mockRule.description); + }); + + it('should show slide toggle when showEnabledToggle is true', async () => { + component.showEnabledToggle = true; + fixture.detectChanges(); + + const toggleHarness = await loader.getHarnessOrNull(MatSlideToggleHarness); + expect(toggleHarness).toBeTruthy(); + }); + + it('should hide slide toggle when showEnabledToggle is false', async () => { + component.showEnabledToggle = false; + fixture.detectChanges(); + + const toggleHarness = await loader.getHarnessOrNull(MatSlideToggleHarness); + expect(toggleHarness).toBeFalsy(); + }); + + it('should set toggle checked state based on rule.isEnabled', async () => { + component.showEnabledToggle = true; + component.rule.isEnabled = true; + fixture.detectChanges(); + + const toggleHarness = await loader.getHarness(MatSlideToggleHarness); + expect(await toggleHarness.isChecked()).toBe(true); + }); + + describe('onToggleClick', () => { + it('should stop event propagation and emit enabledChanged with isEnabled value', () => { + spyOn(component.enabledChanged, 'emit'); + const mockEvent = jasmine.createSpyObj('Event', ['stopPropagation']); + const isEnabled = true; + + component.onToggleClick(isEnabled, mockEvent); + + expect(mockEvent.stopPropagation).toHaveBeenCalled(); + expect(component.enabledChanged.emit).toHaveBeenCalledWith(isEnabled); + }); + + it('should stop event propagation and emit enabledChanged with false value', () => { + spyOn(component.enabledChanged, 'emit'); + const mockEvent = jasmine.createSpyObj('Event', ['stopPropagation']); + const isEnabled = false; + + component.onToggleClick(isEnabled, mockEvent); + + expect(mockEvent.stopPropagation).toHaveBeenCalled(); + expect(component.enabledChanged.emit).toHaveBeenCalledWith(isEnabled); + }); + }); +}); diff --git a/projects/aca-content/folder-rules/src/rule-list/rule-list/rule-list.ui-component.spec.ts b/projects/aca-content/folder-rules/src/rule-list/rule-list/rule-list.ui-component.spec.ts index a44c73f63..dde86abcf 100644 --- a/projects/aca-content/folder-rules/src/rule-list/rule-list/rule-list.ui-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-list/rule-list/rule-list.ui-component.spec.ts @@ -24,18 +24,20 @@ import { RuleListUiComponent } from './rule-list.ui-component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopTranslateModule, UnitTestingUtils } from '@alfresco/adf-core'; import { ownedRuleSetMock, ruleSetsMock, ruleSetWithLinkMock } from '../../mock/rule-sets.mock'; -import { DebugElement } from '@angular/core'; -import { By } from '@angular/platform-browser'; import { owningFolderIdMock } from '../../mock/node.mock'; import { of } from 'rxjs'; import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { Rule } from '../../model/rule.model'; +import { RuleSet } from '../../model/rule-set.model'; describe('RuleListUiComponent', () => { let fixture: ComponentFixture; let component: RuleListUiComponent; - let debugElement: DebugElement; + let unitTestingUtils: UnitTestingUtils; + + const getMainRuleSetTitleText = (): string => unitTestingUtils.getInnerTextByDataAutomationId('main-rule-set-title'); beforeEach(() => { TestBed.configureTestingModule({ @@ -45,7 +47,7 @@ describe('RuleListUiComponent', () => { fixture = TestBed.createComponent(RuleListUiComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); component.folderId = owningFolderIdMock; component.inheritedRuleSets = ruleSetsMock; @@ -55,15 +57,117 @@ describe('RuleListUiComponent', () => { component.mainRuleSet$ = of(ownedRuleSetMock); fixture.detectChanges(); - const mainRuleSetTitleElement = debugElement.query(By.css(`[data-automation-id="main-rule-set-title"]`)); - expect((mainRuleSetTitleElement.nativeElement as HTMLDivElement).innerText.trim()).toBe('ACA_FOLDER_RULES.RULE_LIST.OWNED_RULES'); + expect(getMainRuleSetTitleText()).toBe('ACA_FOLDER_RULES.RULE_LIST.OWNED_RULES'); }); it('should show "Rules from linked folder" as a title if the main rule set is linked', () => { component.mainRuleSet$ = of(ruleSetWithLinkMock); fixture.detectChanges(); - const mainRuleSetTitleElement = debugElement.query(By.css(`[data-automation-id="main-rule-set-title"]`)); - expect((mainRuleSetTitleElement.nativeElement as HTMLDivElement).innerText.trim()).toBe('ACA_FOLDER_RULES.RULE_LIST.LINKED_RULES'); + expect(getMainRuleSetTitleText()).toBe('ACA_FOLDER_RULES.RULE_LIST.LINKED_RULES'); + }); + + it('should add loading item when both ruleSetsLoading and hasMoreRuleSets are true', () => { + component.inheritedRuleSets = ruleSetsMock; + component.mainRuleSet$ = of(ruleSetWithLinkMock); + component.ruleSetsLoading = true; + component.hasMoreRuleSets = true; + + component.ngOnInit(); + + const loadingItem = component.inheritedRuleSetGroupingItems.find((item) => item.type === 'loading'); + expect(loadingItem).toBeDefined(); + expect(loadingItem.type).toBe('loading'); + + const loadMoreItem = component.inheritedRuleSetGroupingItems.find((item) => item.type === 'load-more-rule-sets'); + expect(loadMoreItem).toBeUndefined(); + }); + + it('should add loading item when both loadingRules and hasMoreRules are true', () => { + const ruleSetWithBothFlags: RuleSet = { + ...ownedRuleSetMock, + loadingRules: true, + hasMoreRules: true + }; + + const result = component.getRuleSetGroupingItems(ruleSetWithBothFlags, false); + + const loadingItem = result.find((item) => item.type === 'loading'); + expect(loadingItem).toBeDefined(); + expect(loadingItem.type).toBe('loading'); + + const loadMoreItem = result.find((item) => item.type === 'load-more-rules'); + expect(loadMoreItem).toBeUndefined(); + }); + + it('should not add any special items when neither loadingRules nor hasMoreRules are true', () => { + const ruleSetWithoutFlags: RuleSet = { + ...ownedRuleSetMock, + loadingRules: false, + hasMoreRules: false + }; + + const result = component.getRuleSetGroupingItems(ruleSetWithoutFlags, false); + + const specialItems = result.filter((item) => item.type === 'loading' || item.type === 'load-more-rules'); + expect(specialItems.length).toBe(0); + }); + + it('should emit loadMoreRuleSets event when onLoadMoreRuleSets is called', () => { + spyOn(component.loadMoreRuleSets, 'emit'); + + component.onLoadMoreRuleSets(); + + expect(component.loadMoreRuleSets.emit).toHaveBeenCalledWith(); + }); + + it('should emit loadMoreRules event with ruleSet when onLoadMoreRules is called', () => { + spyOn(component.loadMoreRules, 'emit'); + const mockRuleSet = ownedRuleSetMock; + + component.onLoadMoreRules(mockRuleSet); + + expect(component.loadMoreRules.emit).toHaveBeenCalledWith(mockRuleSet); + }); + + it('should emit selectRule event with rule when onSelectRule is called', () => { + spyOn(component.selectRule, 'emit'); + const mockRule = ownedRuleSetMock.rules[0]; + + component.onSelectRule(mockRule); + + expect(component.selectRule.emit).toHaveBeenCalledWith(mockRule); + }); + + it('should stop event propagation and emit ruleSetEditLinkClicked with mainRuleSet when onRuleSetEditLinkClicked is called', () => { + spyOn(component.ruleSetEditLinkClicked, 'emit'); + const mockEvent = jasmine.createSpyObj('Event', ['stopPropagation']); + component.mainRuleSet = ownedRuleSetMock; + + component.onRuleSetEditLinkClicked(mockEvent); + + expect(mockEvent.stopPropagation).toHaveBeenCalled(); + expect(component.ruleSetEditLinkClicked.emit).toHaveBeenCalledWith(ownedRuleSetMock); + }); + + it('should emit ruleEnabledChanged event with tuple when onRuleEnabledChanged is called', () => { + spyOn(component.ruleEnabledChanged, 'emit'); + const mockRule = ownedRuleSetMock.rules[0]; + const event: [Rule, boolean] = [mockRule, true]; + + component.onRuleEnabledChanged(event); + + expect(component.ruleEnabledChanged.emit).toHaveBeenCalledWith(event); + }); + + it('should stop event propagation and emit ruleSetUnlinkClicked with mainRuleSet when onRuleSetUnlinkClicked is called', () => { + spyOn(component.ruleSetUnlinkClicked, 'emit'); + const mockEvent = jasmine.createSpyObj('Event', ['stopPropagation']); + component.mainRuleSet = ownedRuleSetMock; + + component.onRuleSetUnlinkClicked(mockEvent); + + expect(mockEvent.stopPropagation).toHaveBeenCalled(); + expect(component.ruleSetUnlinkClicked.emit).toHaveBeenCalledWith(ownedRuleSetMock); }); }); diff --git a/projects/aca-content/folder-rules/src/rule-set-picker/rule-set-picker.smart-component.spec.ts b/projects/aca-content/folder-rules/src/rule-set-picker/rule-set-picker.smart-component.spec.ts index 8afa1e71c..c165efeee 100644 --- a/projects/aca-content/folder-rules/src/rule-set-picker/rule-set-picker.smart-component.spec.ts +++ b/projects/aca-content/folder-rules/src/rule-set-picker/rule-set-picker.smart-component.spec.ts @@ -22,26 +22,44 @@ * from Hyland Software. If not, see . */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RuleSetPickerOptions, RuleSetPickerSmartComponent } from './rule-set-picker.smart-component'; -import { NoopAuthModule, NoopTranslateModule } from '@alfresco/adf-core'; +import { NoopAuthModule, NoopTranslateModule, NotificationService, UnitTestingUtils } from '@alfresco/adf-core'; import { folderToLinkMock, otherFolderMock } from '../mock/node.mock'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { FolderRuleSetsService } from '../services/folder-rule-sets.service'; import { of } from 'rxjs'; -import { ownedRuleSetMock, ruleSetWithLinkMock, ruleSetWithNoRulesToLinkMock, ruleSetWithOwnedRulesToLinkMock } from '../mock/rule-sets.mock'; +import { ruleSetWithLinkMock, ruleSetWithNoRulesToLinkMock, ruleSetWithOwnedRulesToLinkMock } from '../mock/rule-sets.mock'; import { ContentApiService } from '@alfresco/aca-shared'; -import { By } from '@angular/platform-browser'; -import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { + AlfrescoApiService, + AlfrescoApiServiceMock, + ContentNodeSelectorPanelComponent, + NodeEntryEvent, + SitesService +} from '@alfresco/adf-content-services'; import { provideRouter } from '@angular/router'; +import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; +import { RuleSet } from '../model/rule-set.model'; + +@Component({ + selector: 'adf-content-node-selector-panel', + template: '
', + standalone: true +}) +class MockContentNodeSelectorPanelComponent { + @Input() currentFolderId: string; + @Output() folderLoaded = new EventEmitter(); + @Output() navigationChange = new EventEmitter(); + @Output() siteChange = new EventEmitter(); +} describe('RuleSetPickerSmartComponent', () => { let fixture: ComponentFixture; let component: RuleSetPickerSmartComponent; - let folderRuleSetsService: FolderRuleSetsService; let loadRuleSetsSpy: jasmine.Spy; - let callApiSpy: jasmine.Spy; + let sitesService: SitesService; + let unitTestingUtils: UnitTestingUtils; const dialogRef = { close: jasmine.createSpy('close'), @@ -53,6 +71,9 @@ describe('RuleSetPickerSmartComponent', () => { defaultNodeId: 'folder-1-id' }; + const getItems = (): DebugElement[] => unitTestingUtils.getAllByCSS('.aca-rule-set-picker__content__rule-list aca-rule-list-item'); + const getEmptyList = (): DebugElement => unitTestingUtils.getByCSS('adf-empty-content'); + beforeEach(() => { TestBed.configureTestingModule({ imports: [NoopTranslateModule, NoopAuthModule, RuleSetPickerSmartComponent], @@ -73,27 +94,37 @@ describe('RuleSetPickerSmartComponent', () => { } } ] + }).overrideComponent(RuleSetPickerSmartComponent, { + remove: { + imports: [ContentNodeSelectorPanelComponent] + }, + add: { + imports: [MockContentNodeSelectorPanelComponent] + } }); - folderRuleSetsService = TestBed.inject(FolderRuleSetsService); fixture = TestBed.createComponent(RuleSetPickerSmartComponent); component = fixture.componentInstance; + sitesService = TestBed.inject(SitesService); + unitTestingUtils = new UnitTestingUtils(fixture.debugElement); - loadRuleSetsSpy = spyOn(component.folderRuleSetsService, 'loadRuleSets').and.callThrough(); - callApiSpy = spyOn(folderRuleSetsService, 'callApi'); - callApiSpy - .withArgs(`/nodes/${dialogOptions.nodeId}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, 'GET') - .and.returnValue(Promise.resolve(ownedRuleSetMock)) - .withArgs(`/nodes/${dialogOptions.nodeId}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET') - .and.returnValue(Promise.resolve(ownedRuleSetMock)) - .withArgs(`/nodes/${folderToLinkMock.id}?include=path%2Cproperties%2CallowableOperations%2Cpermissions`, 'GET') - .and.returnValue(Promise.resolve({ entry: folderToLinkMock })); + loadRuleSetsSpy = spyOn(component.folderRuleSetsService, 'loadRuleSets'); + spyOn(sitesService, 'getSites'); }); afterEach(() => { fixture.destroy(); }); + it('should set true to rulesLoading$ when rulesLoading or folderLoading is true', (done) => { + component.setFolderLoading(true); + + component.rulesLoading$.subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + it('should load the rule sets of a node once it has been selected', () => { expect(loadRuleSetsSpy).not.toHaveBeenCalled(); component.onNodeSelect([folderToLinkMock]); @@ -108,11 +139,8 @@ describe('RuleSetPickerSmartComponent', () => { component.onNodeSelect([folderToLinkMock]); fixture.detectChanges(); - const items = fixture.debugElement.queryAll(By.css('.aca-rule-set-picker__content__rule-list aca-rule-list-item')); - expect(items.length).toBe(0); - - const emptyList = fixture.debugElement.query(By.css('adf-empty-content')); - expect(emptyList).not.toBeNull(); + expect(getItems().length).toBe(0); + expect(getEmptyList()).not.toBeNull(); }); it('should show an empty list message if a selected folder has linked rules', () => { @@ -121,11 +149,8 @@ describe('RuleSetPickerSmartComponent', () => { component.onNodeSelect([folderToLinkMock]); fixture.detectChanges(); - const items = fixture.debugElement.queryAll(By.css('.aca-rule-set-picker__content__rule-list aca-rule-list-item')); - expect(items.length).toBe(0); - - const emptyList = fixture.debugElement.query(By.css('adf-empty-content')); - expect(emptyList).not.toBeNull(); + expect(getItems().length).toBe(0); + expect(getEmptyList()).not.toBeNull(); }); it('should show a list of items if a selected folder has owned rules', () => { @@ -134,10 +159,62 @@ describe('RuleSetPickerSmartComponent', () => { component.onNodeSelect([folderToLinkMock]); fixture.detectChanges(); - const items = fixture.debugElement.queryAll(By.css('.aca-rule-set-picker__content__rule-list aca-rule-list-item')); - expect(items.length).toBe(2); + expect(getItems().length).toBe(2); + expect(getEmptyList()).toBeNull(); + }); - const emptyList = fixture.debugElement.query(By.css('adf-empty-content')); - expect(emptyList).toBeNull(); + describe('onSubmit', () => { + let deleteRuleSetLinkSpy: jasmine.Spy<(nodeId: string, ruleSetId: string) => Promise>; + let createRuleSetLinkSpy: jasmine.Spy<(nodeId: string, linkedNodeId: string) => Promise>; + + beforeEach(() => { + deleteRuleSetLinkSpy = spyOn(component.folderRuleSetsService, 'deleteRuleSetLink').and.returnValue(Promise.resolve()); + createRuleSetLinkSpy = spyOn(component.folderRuleSetsService, 'createRuleSetLink').and.returnValue(Promise.resolve()); + component['selectedNodeId'] = 'selected-node-id'; + }); + + it('should set isBusy to true and create rule set link when no existing rule set', fakeAsync(() => { + component.existingRuleSet = null; + + component.onSubmit(); + + expect(component.isBusy).toBe(true); + expect(deleteRuleSetLinkSpy).not.toHaveBeenCalled(); + + tick(); + + expect(createRuleSetLinkSpy).toHaveBeenCalledWith(component.nodeId, 'selected-node-id'); + expect(dialogRef.close).toHaveBeenCalledWith(true); + expect(component.isBusy).toBe(false); + })); + + it('should delete existing rule set link then create new one when existing rule set provided', fakeAsync(() => { + component.existingRuleSet = { id: 'existing-rule-set-id' } as RuleSet; + + component.onSubmit(); + + expect(component.isBusy).toBe(true); + expect(deleteRuleSetLinkSpy).toHaveBeenCalledWith(component.nodeId, 'existing-rule-set-id'); + + tick(); + + expect(createRuleSetLinkSpy).toHaveBeenCalledWith(component.nodeId, 'selected-node-id'); + expect(dialogRef.close).toHaveBeenCalledWith(true); + expect(component.isBusy).toBe(false); + })); + + it('should handle error and call handleError when deleteRuleSetLink fails', fakeAsync(() => { + const notificationService = TestBed.inject(NotificationService); + spyOn(notificationService, 'showError'); + + deleteRuleSetLinkSpy.and.returnValue(Promise.reject(new Error('delete error'))); + component.existingRuleSet = { id: 'existing-rule-set-id' } as RuleSet; + + component.onSubmit(); + tick(); + + expect(component.isBusy).toBe(false); + expect(notificationService.showError).toHaveBeenCalledWith('ACA_FOLDER_RULES.LINK_RULES_DIALOG.ERRORS.REQUEST_FAILED'); + })); }); }); diff --git a/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.spec.ts b/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.spec.ts index 562c170fa..8ee39699d 100644 --- a/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.spec.ts +++ b/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.spec.ts @@ -27,38 +27,76 @@ import { TestBed } from '@angular/core/testing'; import { FolderRulesService } from './folder-rules.service'; import { ContentApiService } from '@alfresco/aca-shared'; import { getOtherFolderEntryMock, getOwningFolderEntryMock, otherFolderIdMock, owningFolderIdMock, owningFolderMock } from '../mock/node.mock'; -import { of } from 'rxjs'; +import { filter, firstValueFrom, lastValueFrom, of, skip, throwError } from 'rxjs'; import { getDefaultRuleSetResponseMock, getRuleSetsResponseMock, inheritedRuleSetMock, ownedRuleSetMock } from '../mock/rule-sets.mock'; import { take } from 'rxjs/operators'; import { inheritedRulesMock, linkedRulesMock, ownedRulesMock, ruleMock } from '../mock/rules.mock'; -import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { AlfrescoApiService } from '@alfresco/adf-content-services'; import { NoopTranslateModule } from '@alfresco/adf-core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Rule } from '../model/rule.model'; describe('FolderRuleSetsService', () => { let folderRuleSetsService: FolderRuleSetsService; let folderRulesService: FolderRulesService; let contentApiService: ContentApiService; - let callApiSpy: jasmine.Spy; + let apiClientSpy: jasmine.SpyObj<{ callApi: jasmine.Spy }>; let getRulesSpy: jasmine.Spy; let getNodeSpy: jasmine.Spy; beforeEach(() => { + apiClientSpy = jasmine.createSpyObj('contentPrivateClient', ['callApi']); + TestBed.configureTestingModule({ imports: [NoopTranslateModule], - providers: [FolderRuleSetsService, FolderRulesService, ContentApiService, { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }] + providers: [ + ContentApiService, + { + provide: AlfrescoApiService, + useValue: { + getInstance: () => ({ + contentPrivateClient: apiClientSpy + }) + } + } + ] }); folderRuleSetsService = TestBed.inject(FolderRuleSetsService); folderRulesService = TestBed.inject(FolderRulesService); contentApiService = TestBed.inject(ContentApiService); - callApiSpy = spyOn(folderRuleSetsService, 'callApi') - .withArgs(`/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET') + apiClientSpy.callApi + .withArgs( + `/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ) .and.returnValue(of(getDefaultRuleSetResponseMock)) - .withArgs(`/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, 'GET') + .withArgs( + `/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ) .and.returnValue(of(getRuleSetsResponseMock)) - .and.stub(); + .withArgs('/nodes/folder-1-id/rule-set-links', 'POST', {}, {}, {}, {}, { id: 'folder-2-id' }, ['application/json'], ['application/json']) + .and.returnValue(of({})) + .withArgs('/nodes/folder-1-id/rule-set-links/rule-set-1-id', 'DELETE', {}, {}, {}, {}, {}, ['application/json'], ['application/json']) + .and.returnValue(of({})); + getRulesSpy = spyOn(folderRulesService, 'getRules') .withArgs(jasmine.anything(), 'rule-set-no-links') .and.returnValue(of({ rules: ownedRulesMock, hasMoreRules: false })) @@ -72,17 +110,37 @@ describe('FolderRuleSetsService', () => { .and.returnValue(of(getOwningFolderEntryMock)) .withArgs(otherFolderIdMock) .and.returnValue(of(getOtherFolderEntryMock)); + + spyOn(folderRulesService, 'selectRule'); }); it('should have an initial value of null for selectedRuleSet$', async () => { - const selectedRuleSetPromise = folderRuleSetsService.selectedRuleSet$.pipe(take(1)).toPromise(); + const selectedRuleSetPromise = firstValueFrom(folderRuleSetsService.selectedRuleSet$.pipe(take(1))); const selectedRuleSet = await selectedRuleSetPromise; expect(selectedRuleSet).toBeNull(); }); + it('should select the first rule of the owned rule set of the folder', async () => { + // take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe + const mainRuleSetPromise = firstValueFrom(folderRuleSetsService.mainRuleSet$.pipe(skip(3), take(1))); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + folderRuleSetsService.removeRuleFromMainRuleSet('owned-rule-1-id'); + const mainRuleSet = await mainRuleSetPromise; + + expect(mainRuleSet.rules[0].id).toBe('owned-rule-2-id'); + expect(folderRulesService.selectRule).toHaveBeenCalledWith(ruleMock('owned-rule-1')); + }); + + it('should not call selectRule on removeRuleFromMainRuleSet if mainRuleSet not set', async () => { + folderRuleSetsService.removeRuleFromMainRuleSet('owned-rule-1-id'); + + expect(folderRulesService.selectRule).not.toHaveBeenCalled(); + }); + it(`should load node info when loading the node's rule sets`, async () => { // take(2), because: 1 = init of the BehaviourSubject, 2 = in subscribe - const folderInfoPromise = folderRuleSetsService.folderInfo$.pipe(take(2)).toPromise(); + const folderInfoPromise = lastValueFrom(folderRuleSetsService.folderInfo$.pipe(take(2))); folderRuleSetsService.loadRuleSets(owningFolderIdMock); const folderInfo = await folderInfoPromise; @@ -93,68 +151,303 @@ describe('FolderRuleSetsService', () => { it('should load the main rule set (main or linked)', async () => { // take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe - const mainRuleSetPromise = folderRuleSetsService.mainRuleSet$.pipe(take(3)).toPromise(); + const mainRuleSetPromise = lastValueFrom(folderRuleSetsService.mainRuleSet$.pipe(take(3))); folderRuleSetsService.loadRuleSets(owningFolderIdMock); const ruleSet = await mainRuleSetPromise; - expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, 'GET'); + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + `/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); expect(ruleSet).toEqual(ownedRuleSetMock); }); it('should load inherited rule sets of a node and filter out owned or inherited rule sets', async () => { // take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe - const inheritedRuleSetsPromise = folderRuleSetsService.inheritedRuleSets$.pipe(take(3)).toPromise(); - const hasMoreRuleSetsPromise = folderRuleSetsService.hasMoreRuleSets$.pipe(take(3)).toPromise(); + const inheritedRuleSetsPromise = lastValueFrom(folderRuleSetsService.inheritedRuleSets$.pipe(take(3))); + const hasMoreRuleSetsPromise = lastValueFrom(folderRuleSetsService.hasMoreRuleSets$.pipe(take(3))); folderRuleSetsService.loadRuleSets(owningFolderIdMock); const ruleSets = await inheritedRuleSetsPromise; const hasMoreRuleSets = await hasMoreRuleSetsPromise; - expect(callApiSpy).toHaveBeenCalledWith( + expect(apiClientSpy.callApi).toHaveBeenCalledWith( `/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=0&maxItems=100`, - 'GET' + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] ); expect(ruleSets).toEqual([inheritedRuleSetMock]); expect(getRulesSpy).toHaveBeenCalledWith(owningFolderIdMock, jasmine.anything()); expect(hasMoreRuleSets).toEqual(false); }); - it('should select the first rule of the owned rule set of the folder', async () => { - const selectRuleSpy = spyOn(folderRulesService, 'selectRule'); - // take(3), because: 1 = init of the BehaviourSubject, 2 = reinitialise at beginning of loadRuleSets, 3 = in subscribe - const ruleSetListingPromise = folderRuleSetsService.inheritedRuleSets$.pipe(take(3)).toPromise(); - + it('should append additional inherited rule sets on loadMoreInheritedRuleSets', (done) => { folderRuleSetsService.loadRuleSets(owningFolderIdMock); - await ruleSetListingPromise; - expect(selectRuleSpy).toHaveBeenCalledWith(ruleMock('owned-rule-1')); + folderRuleSetsService.inheritedRuleSets$.pipe(take(3)).subscribe(async () => { + const additionalRuleSet = { + ...inheritedRuleSetMock, + id: 'additional-inherited-rule-set', + owningFolder: otherFolderIdMock + }; + + apiClientSpy.callApi + .withArgs( + `/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=1&maxItems=100`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ) + .and.returnValue( + of({ + list: { + entries: [{ entry: additionalRuleSet }], + pagination: { hasMoreItems: false } + } + }) + ); + + getRulesSpy + .withArgs(jasmine.anything(), 'additional-inherited-rule-set') + .and.returnValue(of({ rules: inheritedRulesMock, hasMoreRules: false })); + + let updateCount = 0; + const subscription = folderRuleSetsService.inheritedRuleSets$.subscribe((ruleSets) => { + updateCount++; + if (updateCount === 2) { + subscription.unsubscribe(); + + expect(ruleSets[0]).toEqual(inheritedRuleSetMock); + expect(ruleSets[1].id).toBe('additional-inherited-rule-set'); + + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + `/nodes/${owningFolderIdMock}/rule-sets?include=isLinkedTo,owningFolder,linkedToBy&skipCount=1&maxItems=100`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); + + done(); + } + }); + + folderRuleSetsService.loadMoreInheritedRuleSets(); + }); }); - it('should select a different rule when removing a rule', () => { - const selectRuleSpy = spyOn(folderRulesService, 'selectRule'); - folderRuleSetsService['mainRuleSet'] = JSON.parse(JSON.stringify(ownedRuleSetMock)); - folderRuleSetsService['inheritedRuleSets'] = JSON.parse(JSON.stringify([inheritedRuleSetMock])); + describe('Error handling', () => { + function testRuleSetApiError(status: number, statusText: string, errorMessage: string, done: DoneFn) { + const httpError = new HttpErrorResponse({ + status, + statusText, + error: { message: errorMessage } + }); + + apiClientSpy.callApi + .withArgs( + `/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ) + .and.returnValue(throwError(() => httpError)); + + folderRuleSetsService.mainRuleSet$.pipe(skip(1), take(1)).subscribe((value) => { + expect(value).toBeNull(); + done(); + }); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock, false); + } + + function testNodeInfoError(service: FolderRuleSetsService, status: number, expectedValue: null | undefined, done: DoneFn) { + const httpError = new HttpErrorResponse({ status, statusText: status === 404 ? 'Not Found' : 'Failed' }); + getNodeSpy.withArgs(owningFolderIdMock).and.returnValue(throwError(() => httpError)); + + service.folderInfo$.pipe(skip(1), take(1)).subscribe((info) => { + expect(info).toEqual(expectedValue); + done(); + }); + + service.loadRuleSets(owningFolderIdMock, false); + } + + it('should set main rule set to null on 404 error', (done) => { + testRuleSetApiError(404, 'Not Found', 'Rule set not found', done); + }); + + it('should set mainRuleSet$ to null on non-404 error', (done) => { + testRuleSetApiError(400, 'Failed', 'Failed to fetch main rule set', done); + }); + + it('should emit null folderInfo when getNodeInfo fails with 404', (done) => { + testNodeInfoError(folderRuleSetsService, 404, null, done); + }); + + it('should emit undefined folderInfo on getNodeInfo non-404 error', (done) => { + testNodeInfoError(folderRuleSetsService, 400, undefined, done); + }); + }); + + it('should emit null folderInfo when nodeId is empty', (done) => { + apiClientSpy.callApi + .withArgs( + `/nodes//rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ) + .and.returnValue(throwError(() => new Error('Node ID is empty'))); + folderRuleSetsService.folderInfo$.pipe(skip(1), take(1)).subscribe((info) => { + expect(info).toBeNull(); + expect(getNodeSpy).not.toHaveBeenCalled(); + done(); + }); + + folderRuleSetsService.loadRuleSets('', false); + }); + + it('should add new rule to main rule set', async () => { + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + const newRule = ruleMock('new-rule'); + + await firstValueFrom( + folderRuleSetsService.isLoading$.pipe( + filter((loading) => !loading), + take(1) + ) + ); + + folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule); + const main = await firstValueFrom(folderRuleSetsService.mainRuleSet$.pipe(take(1))); + expect(main.rules[2]).toEqual(newRule); + }); + + it('should update existing rule in main rule set', async () => { + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + const newRule = { ...ownedRulesMock[0], description: 'new description' } as Rule; + folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule); + const main = await firstValueFrom(folderRuleSetsService.mainRuleSet$.pipe(take(1))); + + expect(main.rules[0].description).toEqual(newRule.description); + }); + + it('should set main rule set to null if last rules was removed', async () => { + getRulesSpy.withArgs(jasmine.anything(), 'rule-set-no-links').and.returnValue(of({ rules: [ownedRulesMock[0]], hasMoreRules: false })); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + + await firstValueFrom( + folderRuleSetsService.isLoading$.pipe( + filter((loading) => !loading), + take(1) + ) + ); folderRuleSetsService.removeRuleFromMainRuleSet('owned-rule-1-id'); - expect(selectRuleSpy).toHaveBeenCalledWith(ruleMock('owned-rule-2')); + const main = await lastValueFrom(folderRuleSetsService.mainRuleSet$.pipe(take(1))); - selectRuleSpy.calls.reset(); - folderRuleSetsService.removeRuleFromMainRuleSet('owned-rule-2-id'); - - expect(selectRuleSpy).toHaveBeenCalledWith(ruleMock('inherited-rule-1')); + expect(main).toBe(null); }); it('should send a POST request to create a new link between two folders', async () => { await folderRuleSetsService.createRuleSetLink('folder-1-id', 'folder-2-id'); - expect(callApiSpy).toHaveBeenCalledWith('/nodes/folder-1-id/rule-set-links', 'POST', { - id: 'folder-2-id' - }); + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + '/nodes/folder-1-id/rule-set-links', + 'POST', + {}, + {}, + {}, + {}, + { id: 'folder-2-id' }, + ['application/json'], + ['application/json'] + ); }); - it('should send a DELETE request to delete a link between two folders', async () => { + it('should call refreshMainRuleSet when main rule set is empty', (done) => { + const newRule = ruleMock('owned-rule-33'); + apiClientSpy.callApi + .withArgs( + `/nodes/${owningFolderIdMock}/rule-sets/-default-?include=isLinkedTo,owningFolder,linkedToBy`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ) + .and.returnValue(throwError(() => new Error('Main rule set not found'))); + + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + + folderRuleSetsService.mainRuleSet$.pipe(skip(0), take(1)).subscribe((ruleSet) => { + expect(ruleSet).toBeNull(); + done(); + }); + + folderRuleSetsService.addOrUpdateRuleInMainRuleSet(newRule); + }); + + it('should refreshMainRuleSet and select a rule when main rule set exists', () => { + folderRuleSetsService.loadRuleSets(owningFolderIdMock); + const newRule = ruleMock('new-rule'); + + folderRuleSetsService.refreshMainRuleSet(newRule); + + expect(folderRulesService.selectRule).toHaveBeenCalled(); + }); + + it('should send a DELETE request to remove a rule set link', async () => { await folderRuleSetsService.deleteRuleSetLink('folder-1-id', 'rule-set-1-id'); - expect(callApiSpy).toHaveBeenCalledWith('/nodes/folder-1-id/rule-set-links/rule-set-1-id', 'DELETE'); + + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + '/nodes/folder-1-id/rule-set-links/rule-set-1-id', + 'DELETE', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); }); }); diff --git a/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.ts b/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.ts index c1c88bf48..08bb88304 100644 --- a/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.ts +++ b/projects/aca-content/folder-rules/src/services/folder-rule-sets.service.ts @@ -194,7 +194,7 @@ export class FolderRuleSetsService { } return combineLatest( this.currentFolder?.id === entry.owningFolder ? of(this.currentFolder) : this.getNodeInfo(entry.owningFolder || ''), - this.folderRulesService.getRules(this.currentFolder.id || '', entry.id) + this.folderRulesService.getRules(this.currentFolder?.id || '', entry.id) ).pipe( map(([owningFolderNodeInfo, getRulesRes]) => ({ id: entry.id, @@ -209,28 +209,28 @@ export class FolderRuleSetsService { } removeRuleFromMainRuleSet(ruleId: string) { - if (this.mainRuleSet) { - const index = this.mainRuleSet.rules.findIndex((rule: Rule) => rule.id === ruleId); - if (index > -1) { - if (this.mainRuleSet.rules.length > 1) { - this.mainRuleSet.rules.splice(index, 1); - } else { - this.mainRuleSet = null; - } - this.mainRuleSetSource.next(this.mainRuleSet); - this.folderRulesService.selectRule(this.mainRuleSet?.rules[0] ?? this.inheritedRuleSets[0]?.rules[0] ?? null); - } + if (!this.mainRuleSet) { + return; } + + const updatedRules = this.mainRuleSet.rules.filter((rule) => rule.id !== ruleId); + const newMainRuleSet: RuleSet = updatedRules.length ? { ...this.mainRuleSet, rules: updatedRules } : null; + + this.mainRuleSet = newMainRuleSet; + this.mainRuleSetSource.next(newMainRuleSet); + + const nextRule = newMainRuleSet?.rules[0] ?? this.inheritedRuleSets[0]?.rules[0] ?? null; + + this.folderRulesService.selectRule(nextRule); } addOrUpdateRuleInMainRuleSet(newRule: Rule) { if (this.mainRuleSet) { - const index = this.mainRuleSet.rules.findIndex((rule: Rule) => rule.id === newRule.id); - if (index > -1) { - this.mainRuleSet.rules.splice(index, 1, newRule); - } else { - this.mainRuleSet.rules.push(newRule); - } + const updatedRules = this.mainRuleSet.rules.some((rule) => rule.id === newRule.id) + ? this.mainRuleSet.rules.map((rule) => (rule.id === newRule.id ? newRule : rule)) + : [...this.mainRuleSet.rules, newRule]; + + this.mainRuleSet = { ...this.mainRuleSet, rules: updatedRules }; this.mainRuleSetSource.next(this.mainRuleSet); this.folderRulesService.selectRule(newRule); } else { diff --git a/projects/aca-content/folder-rules/src/services/folder-rules.service.spec.ts b/projects/aca-content/folder-rules/src/services/folder-rules.service.spec.ts index a03283232..85767bf27 100644 --- a/projects/aca-content/folder-rules/src/services/folder-rules.service.spec.ts +++ b/projects/aca-content/folder-rules/src/services/folder-rules.service.spec.ts @@ -38,13 +38,13 @@ import { import { ruleSetMock } from '../mock/rule-sets.mock'; import { owningFolderIdMock } from '../mock/node.mock'; import { take } from 'rxjs/operators'; -import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-content-services'; +import { AlfrescoApiService } from '@alfresco/adf-content-services'; describe('FolderRulesService', () => { let folderRulesService: FolderRulesService; let notificationService: NotificationService; - let callApiSpy: jasmine.Spy; + let apiClientSpy: jasmine.SpyObj<{ callApi: jasmine.Spy }>; const nodeId = owningFolderIdMock; const ruleSetId = 'rule-set-id'; @@ -56,20 +56,30 @@ describe('FolderRulesService', () => { const key = ruleSettingsMock.key; beforeEach(() => { + apiClientSpy = jasmine.createSpyObj('contentPrivateClient', ['callApi']); + TestBed.configureTestingModule({ imports: [NoopTranslateModule], - providers: [FolderRulesService, { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }] + providers: [ + FolderRulesService, + { + provide: AlfrescoApiService, + useValue: { + getInstance: () => ({ + contentPrivateClient: apiClientSpy + }) + } + } + ] }); folderRulesService = TestBed.inject(FolderRulesService); notificationService = TestBed.inject(NotificationService); - - callApiSpy = spyOn(folderRulesService, 'callApi'); }); it('should load some rules into a rule set', () => { const ruleSet = ruleSetMock(); - callApiSpy.and.returnValue(of(getRulesResponseMock)); + apiClientSpy.callApi.and.returnValue(of(getRulesResponseMock)); expect(ruleSet.rules.length).toBe(0); expect(ruleSet.hasMoreRules).toBeTrue(); @@ -77,7 +87,17 @@ describe('FolderRulesService', () => { folderRulesService.loadRules(ruleSet); - expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${ruleSet.owningFolder.id}/rule-sets/${ruleSet.id}/rules?skipCount=0&maxItems=100`, 'GET'); + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + `/nodes/${ruleSet.owningFolder.id}/rule-sets/${ruleSet.id}/rules?skipCount=0&maxItems=100`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); expect(ruleSet.rules.length).toBe(2); expect(ruleSet.rules).toEqual(rulesMock); expect(ruleSet.hasMoreRules).toBeFalse(); @@ -85,14 +105,24 @@ describe('FolderRulesService', () => { it('should load more rules if it still has some more to load', () => { const ruleSet = ruleSetMock(rulesMock); - callApiSpy.and.returnValue(of(getMoreRulesResponseMock)); + apiClientSpy.callApi.and.returnValue(of(getMoreRulesResponseMock)); expect(ruleSet.rules.length).toBe(2); expect(ruleSet.hasMoreRules).toBeTrue(); folderRulesService.loadRules(ruleSet); - expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${ruleSet.owningFolder.id}/rule-sets/${ruleSet.id}/rules?skipCount=2&maxItems=100`, 'GET'); + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + `/nodes/${ruleSet.owningFolder.id}/rule-sets/${ruleSet.id}/rules?skipCount=2&maxItems=100`, + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); expect(ruleSet.rules.length).toBe(4); expect(ruleSet.rules).toEqual([...rulesMock, ...moreRulesMock]); expect(ruleSet.hasMoreRules).toBeFalse(); @@ -120,7 +150,9 @@ describe('FolderRulesService', () => { }); it('should delete a rule and return its id', async () => { - callApiSpy.withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE').and.returnValue(ruleId); + apiClientSpy.callApi + .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', {}, {}, {}, {}, {}, ['application/json'], ['application/json']) + .and.returnValue(ruleId); const deletedRulePromise = folderRulesService.deletedRuleId$.pipe(take(2)).toPromise(); folderRulesService.deleteRule(nodeId, ruleId, ruleSetId); @@ -128,13 +160,82 @@ describe('FolderRulesService', () => { expect(deletedRule).toBeTruthy('rule has not been deleted'); expect(deletedRule).toBe(ruleId, 'wrong id of deleted rule'); - expect(callApiSpy).toHaveBeenCalledTimes(1); - expect(callApiSpy).toHaveBeenCalledWith(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE'); + expect(apiClientSpy.callApi).toHaveBeenCalledTimes(1); + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + `/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, + 'DELETE', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); + }); + + it('should emit error when deleting rule fails', (done) => { + const errorMessage = 'Delete failed'; + const mockError = new Error(errorMessage); + + apiClientSpy.callApi + .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'DELETE', {}, {}, {}, {}, {}, ['application/json'], ['application/json']) + .and.returnValue(Promise.reject(mockError)); + + folderRulesService.deletedRuleId$.pipe(take(2)).subscribe((value) => { + if (value !== null) { + expect(value as unknown as Error).toEqual(mockError); + done(); + } + }); + + folderRulesService.deleteRule(nodeId, ruleId, ruleSetId); + }); + + it('should format simple conditions with default values when properties are missing', () => { + const mockResponse = { + list: { + entries: [ + { + entry: { + id: 'test-rule', + name: 'Test Rule', + conditions: { + simpleConditions: [{}] + } + } + } + ], + pagination: { hasMoreItems: false } + } + }; + + apiClientSpy.callApi.and.returnValue(of(mockResponse)); + + folderRulesService.getRules('folder-id', 'ruleset-id').subscribe((result) => { + const conditions = result.rules[0].conditions.simpleConditions; + + expect(conditions[0]).toEqual({ + field: 'cm:name', + comparator: 'equals', + parameter: '' + }); + }); }); it('should send correct POST request and return created rule', async () => { - callApiSpy - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, 'POST', mockedRuleWithoutId) + apiClientSpy.callApi + .withArgs( + `/nodes/${nodeId}/rule-sets/${ruleSetId}/rules`, + 'POST', + {}, + {}, + {}, + {}, + mockedRuleWithoutId, + ['application/json'], + ['application/json'] + ) .and.returnValue(Promise.resolve(mockedRuleEntry)); const result = await folderRulesService.createRule(nodeId, mockedRuleWithoutId, ruleSetId); @@ -142,8 +243,18 @@ describe('FolderRulesService', () => { }); it('should send correct PUT request to update rule and return it', async () => { - callApiSpy - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', mockedRule) + apiClientSpy.callApi + .withArgs( + `/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, + 'PUT', + {}, + {}, + {}, + {}, + mockedRule, + ['application/json'], + ['application/json'] + ) .and.returnValue(Promise.resolve(mockedRuleEntry)); const result = await folderRulesService.updateRule(nodeId, ruleId, mockedRule, ruleSetId); @@ -151,8 +262,18 @@ describe('FolderRulesService', () => { }); it('should display error message and revert enabled state when updating rule fails', async () => { - callApiSpy - .withArgs(`/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, 'PUT', mockedRule) + apiClientSpy.callApi + .withArgs( + `/nodes/${nodeId}/rule-sets/${ruleSetId}/rules/${ruleId}`, + 'PUT', + {}, + {}, + {}, + {}, + mockedRule, + ['application/json'], + ['application/json'] + ) .and.returnValue(Promise.reject(new Error(JSON.stringify({ error: { briefSummary: 'Error updating rule' } })))); spyOn(notificationService, 'showError'); @@ -162,16 +283,119 @@ describe('FolderRulesService', () => { }); it('should send correct GET request and return rule settings', async () => { - callApiSpy.withArgs(`/nodes/${nodeId}/rule-settings/${key}`, 'GET').and.returnValue(Promise.resolve(mockedRuleSettingsEntry)); + apiClientSpy.callApi + .withArgs(`/nodes/${nodeId}/rule-settings/${key}`, 'GET', {}, {}, {}, {}, {}, ['application/json'], ['application/json']) + .and.returnValue(Promise.resolve(mockedRuleSettingsEntry)); const result = await folderRulesService.getRuleSettings(nodeId, key); expect(result).toEqual(ruleSettingsMock); }); it('should send correct PUT request to update rule settings and return them', async () => { - callApiSpy.withArgs(`/nodes/${nodeId}/rule-settings/${key}`, 'PUT', ruleSettingsMock).and.returnValue(Promise.resolve(mockedRuleSettingsEntry)); + apiClientSpy.callApi + .withArgs(`/nodes/${nodeId}/rule-settings/${key}`, 'PUT', {}, {}, {}, {}, ruleSettingsMock, ['application/json'], ['application/json']) + .and.returnValue(Promise.resolve(mockedRuleSettingsEntry)); const result = await folderRulesService.updateRuleSettings(nodeId, key, ruleSettingsMock); expect(result).toEqual(ruleSettingsMock); }); + + it('should handle rule with null simpleConditions', () => { + const mockResponse = { + list: { + entries: [ + { + entry: { + id: 'test-rule', + name: 'Test Rule', + conditions: { + simpleConditions: null + } + } + } + ], + pagination: { hasMoreItems: false } + } + }; + + apiClientSpy.callApi.and.returnValue(of(mockResponse)); + + folderRulesService.getRules('folder-id', 'ruleset-id').subscribe((result) => { + expect(result.rules[0].conditions.simpleConditions).toEqual([]); + }); + + expect(apiClientSpy.callApi).toHaveBeenCalledWith( + '/nodes/folder-id/rule-sets/ruleset-id/rules?skipCount=0&maxItems=100', + 'GET', + {}, + {}, + {}, + {}, + {}, + ['application/json'], + ['application/json'] + ); + }); + + it('should handle nested composite conditions when getting rules', () => { + const mockResponse = { + list: { + entries: [ + { + entry: { + id: 'test-rule', + name: 'Test Rule', + conditions: { + inverted: false, + booleanMode: 'and', + simpleConditions: [], + compositeConditions: [ + { + inverted: true, + booleanMode: 'or', + simpleConditions: [{ field: 'cm:title', comparator: 'contains', parameter: 'test' }], + compositeConditions: [ + { + inverted: false, + booleanMode: 'and', + simpleConditions: [{ field: 'cm:description', comparator: 'equals', parameter: 'nested' }], + compositeConditions: [] + } + ] + } + ] + } + } + } + ], + pagination: { hasMoreItems: false } + } + }; + + apiClientSpy.callApi.and.returnValue(of(mockResponse)); + + folderRulesService.getRules('folder-id', 'ruleset-id').subscribe((result) => { + const conditions = result.rules[0].conditions; + + expect(conditions.compositeConditions[0].inverted).toBe(true); + expect(conditions.compositeConditions[0].booleanMode).toBe('or'); + expect(conditions.compositeConditions[0].compositeConditions[0].inverted).toBe(false); + expect(conditions.compositeConditions[0].compositeConditions[0].booleanMode).toBe('and'); + }); + }); + + it('should create empty rule for form with options properties removed', () => { + const result = FolderRulesService.emptyRuleForForm; + + expect(result).toEqual({ + id: '', + name: '', + description: '', + isShared: false, + triggers: ['inbound'], + conditions: FolderRulesService.emptyCompositeCondition, + actions: [], + options: FolderRulesService.emptyRuleOptions + }); + }); });