Compare commits

..
Author SHA1 Message Date
Dawid L Jablonski d2cf66cb5d [ci:force] 2023-03-31 12:42:00 +02:00
Dawid L Jablonski 0a4a0679c9 Corrected condition 2023-03-31 12:28:31 +02:00
Dawid L Jablonski c26ee4f1aa [ci:force] 2023-03-31 10:17:08 +02:00
Dawid L Jablonski 6c55cc50bd [ci:force] 2023-03-31 10:14:27 +02:00
Dawid L Jablonski 37e067cc56 [ci:force] 2023-03-30 17:32:19 +02:00
Dawid L Jablonski 9755933b46 [ci:force] 2023-03-30 16:52:09 +02:00
Dawid L Jablonski e701daad77 [ci:force] 2023-03-30 15:25:13 +02:00
Dawid L Jablonski 720280d612 [ci:force] 2023-03-30 15:01:38 +02:00
Dawid L Jablonski 1d555dfcd1 [ci:force] 2023-03-30 14:34:15 +02:00
Dawid L Jablonski 1440455eb9 Corrected one line 2023-03-30 12:52:56 +02:00
Dawid L Jablonski a3019b1602 Corrected conditions on ng2' 2023-03-30 12:07:29 +02:00
41 changed files with 619 additions and 2682 deletions
+20 -19
View File
@@ -93,6 +93,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Check package-lock.json version
run: |
if [[ $(jq '.lockfileVersion == 2' package-lock.json) == "true" ]] ; then
@@ -107,26 +108,24 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get branch name
uses: Alfresco/alfresco-build-tools/.github/actions/get-branch-name@v1.36.0
- name: Save commit message
uses: Alfresco/alfresco-build-tools/.github/actions/get-commit-message@v1.36.0
- name: ci:force flag parser
shell: bash
run: |
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.actor }}" == "dependabot[bot]" ]; then
echo -e "\033[32mci:force check can be skipped\033[0m"
exit 0
else
git fetch --all
message=$(git log --format=%B -n 1 origin/$BRANCH_NAME)
echo "COMMIT_MESSAGE=$message" >> $GITHUB_ENV
export COMMIT_MESSAGE=$message
if [[ "$COMMIT_MESSAGE" == *"[ci:force]"* ]]; then
echo "Forcing CI to run with commit flag [ci:force]."
echo CI_FORCE_RUN=true >> $GITHUB_ENV
echo "BREAK_ACTION=true" >> $GITHUB_ENV
fi
echo -e "\033[32mci:force check can be skipped\033[0m"
skip_check="true"
elif [[ "$COMMIT_MESSAGE" == *"[ci:force]"* ]]; then
echo -e "\033[32m[ci:force] flag detected. No need for approval.\033[0m"
skip_check="true"
fi
- name: Get PR number
@@ -134,7 +133,7 @@ jobs:
uses: kamatama41/get-pr-number-action@5c77b38a1b4974ebf8a9521a689f38a5898eadd6
id: action
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: show pr number
shell: bash
@@ -147,10 +146,12 @@ jobs:
GH_TOKEN: ${{ github.token }}
skip_check: "false"
run: |
echo "ci force run: $CI_FORCE_RUN"
if [ "${CI_FORCE_RUN}" == "true" ]; then
echo -e "\033[32m[ci:force] flag detected. No need for approval.\033[0m"
skip_check="true"
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.actor }}" == "dependabot[bot]" ]; then
echo -e "\033[32mci:force check can be skipped\033[0m"
skip_check="true"
elif [[ "$COMMIT_MESSAGE" == *"[ci:force]"* ]]; then
echo -e "\033[32m[ci:force] flag detected. No need for approval.\033[0m"
skip_check="true"
fi
if [ "${{ github.actor }}" == "dependabot[bot]" ] || [ "${{ github.actor }}" == "alfresco-build" ]; then
@@ -190,7 +191,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
with:
fetch-depth: 0 # Fetch all history for all tags and branches
- uses: ./.github/actions/setup
- name: install
View File
@@ -1,40 +0,0 @@
---
Title: Tags Creator component
Added: v6.0.0-A.3
Status: Active
Last reviewed: 2023-03-27
---
# [Tags Creator component](../../../lib/content-services/src/lib/tag/tags-creator/tags-creator.component.ts "Defined in tags-creator.component.ts")
Allows to create multiple tags. That component contains input and two lists. Top list is all created tags, bottom list is searched tags based on input's value.
## Basic Usage
```html
<adf-tags-creator
[(tagNameControlVisible)]="tagNameControlVisible"
(tagsChange)="tags = $event"
[mode]="tagsCreatorMode">
</adf-tags-creator>
```
## Class members
### Properties
| Name | Type | Default value | Description |
|-----------------------|-------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------|
| mode | `TagsCreatorMode` | | Create mode if only new tags can be created or Create And Assign mode if new tags can be created and existing tags can be selected. |
| disabledTagsRemoving | `boolean` | false | False if tags can be removed from top list, true otherwise. |
| tags | `string[]` | | Default top list. |
| tagNameControlVisible | `boolean` | false | True if input should be visible, false otherwise. | |
### Events
| Name | Type | Description |
|-----------------------------------|------------------------------------------------------------------------|--------------------------------------------------|
| existingTagsPanelVisibilityChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when bottom list is showing or hiding. |
| tagsChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string[]>` | Emitted when tags in top list are changed. |
| tagNameControlVisibleChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<boolean>` | Emitted when input is showing or hiding. |
@@ -61,11 +61,6 @@ Manages tags in Content Services.
Find tag which name matches exactly to passed name.
- _name:_ `string` - Value for name which should be used during finding exact tag.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<TagEntry>` - Found tag which name matches exactly to passed name.
- **assignTagsToNode**(nodeId: `string`, tags: `TagBody[]`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagPaging`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagPaging.md)`|`[`TagEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagEntry.md)`>`<br/>
Assign tags to node. If tag is new then tag is also created additionally, if tag already exists then it is just assigned.
- _nodeId:_ `string` - Id of node to which tags should be assigned.
- _tags:_ `TagBody[]` - List of tags to create and assign or just assign if they already exist.
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TagPaging`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagPaging.md)`|`[`TagEntry`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/TagEntry.md)`>` - Just linked tags to node or single tag if linked only one tag.
## Details
@@ -7,7 +7,6 @@ Title: Upgrade guides
Below are links to the upgrade guides notes for all released versions of ADF back to v2.6.0
- [Upgrading from ADF v5.0 to v6.0](upgrade50-60.md)
- [upgrading from ADF v4.11 to v5.0](upgrade411-50.md)
- [Upgrading from ADF v4.6 to v4.7](upgrade46-47.md)
- [Upgrading from ADF v4.5 to v4.6](upgrade45-46.md)
- [Upgrading from ADF v4.4 to v4.5](upgrade44-45.md)
@@ -25,7 +25,6 @@ import { ContentTestingModule } from '../../testing/content.testing.module';
import { NodeAspectService } from './node-aspect.service';
import { DialogAspectListService } from './dialog-aspect-list.service';
import { CardViewContentUpdateService } from '../../common/services/card-view-content-update.service';
import { TagService } from '@alfresco/adf-content-services';
describe('NodeAspectService', () => {
@@ -100,13 +99,4 @@ describe('NodeAspectService', () => {
nodeAspectService.updateNodeAspects('fake-node-id');
});
it('should call emit on refresh from TagService', () => {
const tagService = TestBed.inject(TagService);
spyOn(dialogAspectListService, 'openAspectListDialog').and.returnValue(of([]));
const node = new MinimalNode({ id: 'fake-node-id', aspectNames: ['a', 'b', 'c'] });
spyOn(nodeApiService, 'updateNode').and.returnValue(of(node));
spyOn(tagService.refresh, 'emit');
nodeAspectService.updateNodeAspects('some node id', 'some-selector');
expect(tagService.refresh.emit).toHaveBeenCalled();
});
});
@@ -19,7 +19,6 @@ import { Injectable } from '@angular/core';
import { DialogAspectListService } from './dialog-aspect-list.service';
import { CardViewContentUpdateService } from '../../common/services/card-view-content-update.service';
import { NodesApiService } from '../../common/services/nodes-api.service';
import { TagService } from '../../tag/services/tag.service';
@Injectable({
providedIn: 'root'
@@ -28,8 +27,7 @@ export class NodeAspectService {
constructor(private nodesApiService: NodesApiService,
private dialogAspectListService: DialogAspectListService,
private cardViewContentUpdateService: CardViewContentUpdateService,
private tagService: TagService) {
private cardViewContentUpdateService: CardViewContentUpdateService) {
}
updateNodeAspects(nodeId: string, selectorAutoFocusedOnClose?: string) {
@@ -37,7 +35,6 @@ export class NodeAspectService {
this.nodesApiService.updateNode(nodeId, { aspectNames: [...aspectList] }).subscribe((updatedNode) => {
this.nodesApiService.nodeUpdated.next(updatedNode);
this.cardViewContentUpdateService.updateNodeAspect(updatedNode);
this.tagService.refresh.emit();
});
});
}
@@ -8,8 +8,7 @@
[editable]="editable"
[multi]="multi"
[displayAspect]="displayAspect"
[preset]="preset"
[displayTags]="true">
[preset]="preset">
</adf-content-metadata>
</mat-card-content>
<mat-card-footer class="adf-content-metadata-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
@@ -55,8 +55,7 @@ describe('ContentMetadataCardComponent', () => {
content: {},
properties: {},
createdByUser: {},
modifiedByUser: {},
id: 'some-id'
modifiedByUser: {}
} as Node;
component.node = node;
@@ -92,10 +91,6 @@ describe('ContentMetadataCardComponent', () => {
expect(contentMetadataComponent.node).toBe(node);
});
it('should assign true to displayTags for ContentMetadataComponent', () => {
expect(fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance.displayTags).toBeTrue();
});
it('should pass through the preset to the underlying component', () => {
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
@@ -19,36 +19,7 @@
[multiValueSeparator]="multiValueSeparator">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="displayTags">
<mat-expansion-panel *ngIf="!editable">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'METADATA.BASIC.TAGS' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<p *ngFor="let tag of tags" class="adf-metadata-properties-tag">{{ tag }}</p>
</mat-expansion-panel>
<div
*ngIf="editable"
class="adf-metadata-properties-tags">
<div class="adf-metadata-properties-tags-title">
<p>{{ 'METADATA.BASIC.TAGS' | translate }}</p>
<button
data-automation-id="showing-tag-input-button"
mat-icon-button
[attr.title]="'METADATA.BASIC.ADD_TAG_TOOLTIP' | translate"
(click)="tagNameControlVisible = true"
[hidden]="tagNameControlVisible || saving">
<mat-icon>add</mat-icon>
</button>
</div>
<adf-tags-creator
[(tagNameControlVisible)]="tagNameControlVisible"
(tagsChange)="storeTagsToAssign($event)"
[mode]="tagsCreatorMode"
[tags]="assignedTags"
[disabledTagsRemoving]="saving">
</adf-tags-creator>
</div>
</ng-container>
<ng-container *ngIf="expanded">
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;"
@@ -17,45 +17,6 @@
.mat-expansion-panel:not([class*='mat-elevation-z']) {
box-shadow: none;
}
.adf-metadata-properties-tag {
height: 40px;
display: flex;
align-items: center;
margin-top: -14px;
margin-bottom: 1em;
}
&-tags {
padding: 0 10px 0 26px;
&-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 15px;
height: 68px;
}
adf-tags-creator {
.adf-tags-creation {
padding-right: 0;
padding-left: 12px;
.adf-tag {
margin-top: -14px;
}
}
&.adf-creator-with-existing-tags-panel {
background: var(--adf-theme-background-dialog-color);
}
}
[hidden] {
visibility: hidden;
}
}
}
&-metadata-action-buttons {
@@ -16,24 +16,23 @@
*/
import { ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { DebugElement, SimpleChange } from '@angular/core';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ClassesApi, MinimalNode, Node, Tag, TagBody, TagEntry, TagPaging, TagPagingList } from '@alfresco/js-api';
import { ClassesApi, MinimalNode, Node } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import {
CardViewBaseItemModel, CardViewComponent,
LogService, setupTestBed, AppConfigService, UpdateNotification
LogService, setupTestBed, AppConfigService
} from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { throwError, of, EMPTY } from 'rxjs';
import { throwError, of } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { mockGroupProperties } from './mock-data';
import { TranslateModule } from '@ngx-translate/core';
import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
import { PropertyGroup } from '../../interfaces/property-group.interface';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { TagsCreatorComponent, TagsCreatorMode, TagService } from '@alfresco/adf-content-services';
describe('ContentMetadataComponent', () => {
let component: ContentMetadataComponent;
@@ -43,64 +42,14 @@ describe('ContentMetadataComponent', () => {
let nodesApiService: NodesApiService;
let node: Node;
let folderNode: Node;
let tagService: TagService;
const preset = 'custom-preset';
const mockTagPaging = (): TagPaging => {
const tagPaging = new TagPaging();
tagPaging.list = new TagPagingList();
const tagEntry1 = new TagEntry();
tagEntry1.entry = new Tag();
tagEntry1.entry.tag = 'Tag 1';
tagEntry1.entry.id = 'some id 1';
const tagEntry2 = new TagEntry();
tagEntry2.entry = new Tag();
tagEntry2.entry.tag = 'Tag 2';
tagEntry2.entry.id = 'some id 2';
tagPaging.list.entries = [tagEntry1, tagEntry2];
return tagPaging;
};
const findTagElements = (): DebugElement[] => fixture.debugElement.queryAll(By.css('.adf-metadata-properties-tag'));
const findCancelButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=reset-metadata]')).nativeElement;
const clickOnCancel = () => {
findCancelButton().click();
fixture.detectChanges();
};
const findSaveButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('[data-automation-id=save-metadata]')).nativeElement;
const clickOnSave = () => {
findSaveButton().click();
fixture.detectChanges();
};
const findTagsCreator = (): TagsCreatorComponent => fixture.debugElement.query(By.directive(TagsCreatorComponent))?.componentInstance;
const findShowingTagInputButton = (): HTMLButtonElement =>
fixture.debugElement.query(By.css('[data-automation-id=showing-tag-input-button]')).nativeElement;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
],
providers: [{
provide: LogService,
useValue: {
error: jasmine.createSpy('error')
}
}, {
provide: TagService,
useValue: {
getTagsByNodeId: () => EMPTY,
removeTag: () => EMPTY,
assignTagsToNode: () => EMPTY
}
}]
providers: [{ provide: LogService, useValue: { error: jasmine.createSpy('error') } }]
});
beforeEach(() => {
@@ -109,7 +58,6 @@ describe('ContentMetadataComponent', () => {
contentMetadataService = TestBed.inject(ContentMetadataService);
updateService = TestBed.inject(CardViewContentUpdateService);
nodesApiService = TestBed.inject(NodesApiService);
tagService = TestBed.inject(TagService);
node = {
id: 'node-id',
@@ -203,42 +151,14 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
clickOnSave();
const saveButton = fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]'));
saveButton.nativeElement.click();
await fixture.whenStable();
expect(component.node).toEqual(expectedNode);
expect(nodesApiService.updateNode).toHaveBeenCalled();
}));
it('should call removeTag and assignTagsToNode on TagService on save click', fakeAsync( () => {
component.editable = true;
component.displayTags = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
const tagPaging = mockTagPaging();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
spyOn(tagService, 'removeTag').and.returnValue(EMPTY);
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
findTagsCreator().tagsChange.emit([tagName1, tagName2]);
clickOnSave();
const tag1 = new TagBody();
tag1.tag = tagName1;
const tag2 = new TagBody();
tag2.tag = tagName2;
expect(tagService.removeTag).toHaveBeenCalledWith(node.id, tagPaging.list.entries[1].entry.id);
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
}));
it('should throw error on unsuccessful save', fakeAsync((done) => {
const logService: LogService = TestBed.inject(LogService);
component.editable = true;
@@ -257,7 +177,11 @@ describe('ContentMetadataComponent', () => {
spyOn(nodesApiService, 'updateNode').and.returnValue(throwError(new Error('My bad')));
fixture.detectChanges();
fixture.whenStable().then(() => clickOnSave());
fixture.whenStable().then(() => {
const saveButton = fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]'));
saveButton.nativeElement.click();
fixture.detectChanges();
});
}));
it('should open the confirm dialog when content type is changed', fakeAsync(() => {
@@ -272,7 +196,8 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
tick(100);
clickOnSave();
const saveButton = fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]'));
saveButton.nativeElement.click();
tick(100);
expect(component.node).toEqual(expectedNode);
@@ -280,39 +205,6 @@ describe('ContentMetadataComponent', () => {
expect(nodesApiService.updateNode).toHaveBeenCalled();
}));
it('should call removeTag and assignTagsToNode on TagService after confirming confirmation dialog when content type is changed', fakeAsync(() => {
component.editable = true;
component.displayTags = true;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
const expectedNode = { ...node, nodeType: 'ft:sbiruli' };
spyOn(contentMetadataService, 'openConfirmDialog').and.returnValue(of(true));
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
const tagPaging = mockTagPaging();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
spyOn(tagService, 'removeTag').and.returnValue(EMPTY);
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
updateService.update(property, 'ft:poppoli');
tick(600);
fixture.detectChanges();
findTagsCreator().tagsChange.emit([tagName1, tagName2]);
tick(100);
fixture.detectChanges();
clickOnSave();
tick(100);
const tag1 = new TagBody();
tag1.tag = tagName1;
const tag2 = new TagBody();
tag2.tag = tagName2;
expect(tagService.removeTag).toHaveBeenCalledWith(node.id, tagPaging.list.entries[1].entry.id);
expect(tagService.assignTagsToNode).toHaveBeenCalledWith(node.id, [tag1, tag2]);
}));
it('should retrigger the load of the properties when the content type has changed', fakeAsync(() => {
component.editable = true;
const property = { key: 'nodeType', value: 'ft:sbiruli' } as CardViewBaseItemModel;
@@ -326,7 +218,8 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
tick(100);
clickOnSave();
const saveButton = fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]'));
saveButton.nativeElement.click();
tick(100);
expect(component.node).toEqual(expectedNode);
@@ -882,244 +775,6 @@ describe('ContentMetadataComponent', () => {
expect(event.stopPropagation).not.toHaveBeenCalled();
});
});
describe('Tags list', () => {
let tagPaging: TagPaging;
beforeEach(() => {
tagPaging = mockTagPaging();
component.displayTags = true;
});
it('should render tags after loading tags in ngOnInit', () => {
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(2);
expect(tagElements[0].nativeElement.textContent).toBe(tagPaging.list.entries[0].entry.tag);
expect(tagElements[1].nativeElement.textContent).toBe(tagPaging.list.entries[1].entry.tag);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
});
it('should not render tags after loading tags in ngOnInit if displayTags is false', () => {
component.displayTags = false;
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(0);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
});
it('should render tags after loading tags in ngOnChanges', () => {
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnChanges({
node: new SimpleChange(undefined, node, false)
});
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(2);
expect(tagElements[0].nativeElement.textContent).toBe(tagPaging.list.entries[0].entry.tag);
expect(tagElements[1].nativeElement.textContent).toBe(tagPaging.list.entries[1].entry.tag);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
});
it('should not render tags after loading tags in ngOnChanges if displayTags is false', () => {
component.displayTags = false;
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnChanges({
node: new SimpleChange(undefined, node, false)
});
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(0);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
});
it('should not render tags after loading tags in ngOnChanges if node is not changed', () => {
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnChanges({});
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(0);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
});
it('should not render tags after loading tags in ngOnChanges if node is changed first time', () => {
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnChanges({
node: new SimpleChange(undefined, node, true)
});
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(0);
expect(tagService.getTagsByNodeId).not.toHaveBeenCalled();
});
it('should render tags after loading tags after clicking on Cancel button', fakeAsync(() => {
component.editable = true;
fixture.detectChanges();
TestBed.inject(CardViewContentUpdateService).itemUpdated$.next({
changed: {}
} as UpdateNotification);
tick(500);
fixture.detectChanges();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
clickOnCancel();
component.editable = false;
fixture.detectChanges();
const tagElements = findTagElements();
expect(tagElements).toHaveSize(2);
expect(tagElements[0].nativeElement.textContent).toBe(tagPaging.list.entries[0].entry.tag);
expect(tagElements[1].nativeElement.textContent).toBe(tagPaging.list.entries[1].entry.tag);
expect(tagService.getTagsByNodeId).toHaveBeenCalledOnceWith(node.id);
}));
it('should be hidden when editable is true', () => {
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
fixture.detectChanges();
component.editable = true;
fixture.detectChanges();
expect(findTagElements()).toHaveSize(0);
});
});
describe('Tags creator', () => {
let tagsCreator: TagsCreatorComponent;
beforeEach(() => {
component.editable = true;
component.displayTags = true;
fixture.detectChanges();
tagsCreator = findTagsCreator();
});
it('should have assigned false to tagNameControlVisible initially', () => {
expect(tagsCreator.tagNameControlVisible).toBeFalse();
});
it('should hide showing tag input button after emitting tagNameControlVisibleChange event with true', () => {
tagsCreator.tagNameControlVisibleChange.emit(true);
fixture.detectChanges();
expect(findShowingTagInputButton().hasAttribute('hidden')).toBeTrue();
});
it('should show showing tag input button after emitting tagNameControlVisibleChange event with false', fakeAsync(() => {
tagsCreator.tagNameControlVisibleChange.emit(true);
fixture.detectChanges();
tick();
tagsCreator.tagNameControlVisibleChange.emit(false);
fixture.detectChanges();
tick(100);
expect(findShowingTagInputButton().hasAttribute('hidden')).toBeFalse();
}));
it('should have assigned correct mode', () => {
expect(tagsCreator.mode).toBe(TagsCreatorMode.CREATE_AND_ASSIGN);
});
it('should enable cancel button after emitting tagsChange event', () => {
tagsCreator.tagsChange.emit(['New tag 1', 'New tag 2', 'New tag 3']);
fixture.detectChanges();
expect(findCancelButton().disabled).toBeFalse();
});
it('should enable save button after emitting tagsChange event', () => {
tagsCreator.tagsChange.emit(['New tag 1', 'New tag 2', 'New tag 3']);
fixture.detectChanges();
expect(findSaveButton().disabled).toBeFalse();
});
it('should have assigned false to disabledTagsRemoving', () => {
expect(tagsCreator.disabledTagsRemoving).toBeFalse();
});
it('should have assigned true to disabledTagsRemoving after clicking on update button', () => {
tagsCreator.tagsChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(tagsCreator.disabledTagsRemoving).toBeTrue();
});
it('should have assigned false to disabledTagsRemoving if forkJoin fails', fakeAsync( () => {
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
const expectedNode = { ...node, name: 'some-modified-value' };
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
const tagPaging = mockTagPaging();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
component.ngOnInit();
spyOn(tagService, 'removeTag').and.returnValue(throwError({}));
spyOn(tagService, 'assignTagsToNode').and.returnValue(EMPTY);
const tagName1 = tagPaging.list.entries[0].entry.tag;
const tagName2 = 'New tag 3';
updateService.update(property, 'updated-value');
tick(600);
fixture.detectChanges();
tagsCreator.tagsChange.emit([tagName1, tagName2]);
clickOnSave();
expect(tagsCreator.disabledTagsRemoving).toBeFalse();
}));
it('should have assigned false to tagNameControlVisible after clicking on update button', () => {
tagsCreator.tagNameControlVisibleChange.emit(true);
tagsCreator.tagsChange.emit([]);
fixture.detectChanges();
clickOnSave();
expect(tagsCreator.tagNameControlVisible).toBeFalse();
});
describe('Setting tags', () => {
let tagPaging: TagPaging;
beforeEach(() => {
tagPaging = mockTagPaging();
spyOn(tagService, 'getTagsByNodeId').and.returnValue(of(tagPaging));
});
it('should assign correct tags after ngOnInit', () => {
component.ngOnInit();
fixture.detectChanges();
expect(tagsCreator.tags).toEqual([tagPaging.list.entries[0].entry.tag, tagPaging.list.entries[1].entry.tag]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
});
it('should assign correct tags after ngOnChanges', () => {
component.ngOnInit();
fixture.detectChanges();
expect(tagsCreator.tags).toEqual([tagPaging.list.entries[0].entry.tag, tagPaging.list.entries[1].entry.tag]);
expect(tagService.getTagsByNodeId).toHaveBeenCalledWith(node.id);
});
});
});
it('should show tags creator if editable is true and displayTags is true', () => {
component.editable = true;
component.displayTags = true;
fixture.detectChanges();
expect(findTagsCreator()).toBeDefined();
});
it('should not show tags creator if editable is true and displayTags is false', () => {
component.editable = true;
component.displayTags = false;
fixture.detectChanges();
expect(findTagsCreator()).toBeUndefined();
});
});
const queryDom = (fixture: ComponentFixture<ContentMetadataComponent>, properties: string = 'properties') =>
@@ -16,8 +16,8 @@
*/
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node, TagBody, TagEntry, TagPaging } from '@alfresco/js-api';
import { Observable, Subject, of, zip, forkJoin } from 'rxjs';
import { Node } from '@alfresco/js-api';
import { Observable, Subject, of, zip } from 'rxjs';
import {
CardViewItem,
LogService,
@@ -31,8 +31,6 @@ import { CardViewGroup, PresetConfig } from '../../interfaces/content-metadata.i
import { takeUntil, debounceTime, catchError, map } from 'rxjs/operators';
import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { TagsCreatorMode } from '../../../tag/tags-creator/tags-creator-mode';
import { TagService } from '../../../tag/services/tag.service';
const DEFAULT_SEPARATOR = ', ';
@@ -52,14 +50,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
/** Toggles whether the edit button should be shown */
@Input()
set editable(editable: boolean) {
this._editable = editable;
this._assignedTags = [...this.tags];
}
get editable(): boolean {
return this._editable;
}
editable: boolean = false;
/** Toggles whether to display empty values in the card view */
@Input()
@@ -95,25 +86,13 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
@Input()
useChipsForMultiValueProperty: boolean = true;
/** True if tags should be displayed, false otherwise */
@Input()
displayTags = false;
multiValueSeparator: string;
basicProperties$: Observable<CardViewItem[]>;
groupedProperties$: Observable<CardViewGroup[]>;
changedProperties = {};
hasMetadataChanged = false;
tagNameControlVisible = false;
private _assignedTags: string[] = [];
private assignedTagsEntries: TagEntry[] = [];
private _editable = false;
private _tagsCreatorMode = TagsCreatorMode.CREATE_AND_ASSIGN;
private _tags: string[] = [];
private targetProperty: CardViewBaseItemModel;
private _saving = false;
constructor(
private contentMetadataService: ContentMetadataService,
@@ -121,8 +100,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
private nodesApiService: NodesApiService,
private logService: LogService,
private translationService: TranslationService,
private appConfig: AppConfigService,
private tagService: TagService
private appConfig: AppConfigService
) {
this.copyToClipboardAction = this.appConfig.get<boolean>('content-metadata.copy-to-clipboard-action');
this.multiValueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator') || DEFAULT_SEPARATOR;
@@ -150,22 +128,6 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.loadProperties(this.node);
}
get assignedTags(): string[] {
return this._assignedTags;
}
get tags(): string[] {
return this._tags;
}
get tagsCreatorMode(): TagsCreatorMode {
return this._tagsCreatorMode;
}
get saving(): boolean {
return this._saving;
}
protected handleUpdateError(error: Error) {
this.logService.error(error);
@@ -198,9 +160,6 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
if (node) {
this.basicProperties$ = this.getProperties(node);
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
if (this.displayTags) {
this.loadTagsForNode(node.id);
}
}
}
@@ -227,12 +186,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
});
}
/**
* Called after clicking save button. It confirms all changes done for metadata. Before clicking on that button they are not saved.
*/
saveChanges() {
this._saving = true;
this.tagNameControlVisible = false;
if (this.hasContentTypeChanged(this.changedProperties)) {
this.contentMetadataService.openConfirmDialog(this.changedProperties).subscribe(() => {
this.updateNode();
@@ -242,36 +196,22 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
}
/**
* Register all tags which should be assigned to node. Please note that they are just in "register" state and are not yet saved
* until button for saving data is clicked. Calling that function causes that save button is enabled.
* @param tags array of tags to register, they are not saved yet until we click save button.
*/
storeTagsToAssign(tags: string[]) {
this._tags = tags;
this.hasMetadataChanged = true;
}
private updateNode() {
forkJoin({
updatedNode: this.nodesApiService.updateNode(this.node.id, this.changedProperties),
...(this.displayTags ? this.saveTags() : {})
}).pipe(
this.nodesApiService.updateNode(this.node.id, this.changedProperties).pipe(
catchError((err) => {
this.cardViewContentUpdateService.updateElement(this.targetProperty);
this.handleUpdateError(err);
return of(null);
}))
.subscribe((result) => {
if (result) {
.subscribe((updatedNode) => {
if (updatedNode) {
if (this.hasContentTypeChanged(this.changedProperties)) {
this.cardViewContentUpdateService.updateNodeAspect(this.node);
}
this.revertChanges();
Object.assign(this.node, result.updatedNode);
Object.assign(this.node, updatedNode);
this.nodesApiService.nodeUpdated.next(this.node);
}
this._saving = false;
});
}
@@ -317,31 +257,4 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
private isEmpty(value: any): boolean {
return value === undefined || value === null || value === '';
}
private loadTagsForNode(id: string) {
this.tagService.getTagsByNodeId(id).subscribe((tagPaging) => {
this.assignedTagsEntries = tagPaging.list.entries;
this._tags = tagPaging.list.entries.map((tagEntry) => tagEntry.entry.tag);
this._assignedTags = [...this._tags];
});
}
private saveTags(): { [key: string]: Observable<TagPaging | TagEntry | void> } {
const observables: { [key: string]: Observable<TagPaging | TagEntry | void> } = {};
if (this.tags) {
this.assignedTagsEntries.forEach((tagEntry) => {
if (!this.tags.some((tag) => tagEntry.entry.tag === tag)) {
observables[`${tagEntry.entry.id}Removing`] = this.tagService.removeTag(this.node.id, tagEntry.entry.id);
}
});
if (this.tags.length) {
observables.tagsAssigning = this.tagService.assignTagsToNode(this.node.id, this.tags.map((tag) => {
const tagBody = new TagBody();
tagBody.tag = tag;
return tagBody;
}));
}
}
return observables;
}
}
@@ -22,15 +22,13 @@ import { MaterialModule } from '../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { ContentMetadataComponent } from './components/content-metadata/content-metadata.component';
import { ContentMetadataCardComponent } from './components/content-metadata-card/content-metadata-card.component';
import { TagModule } from '../tag/tag.module';
@NgModule({
imports: [
CommonModule,
MaterialModule,
FlexLayoutModule,
CoreModule,
TagModule
CoreModule
],
exports: [
ContentMetadataComponent,
+1 -26
View File
@@ -132,29 +132,6 @@
},
"BUTTON": {
"ADD": "Add Tag"
},
"TAGS_CREATOR": {
"EXISTING_TAGS": "Existing tags:",
"EXISTING_TAGS_SELECTION": "Select an existing tag:",
"NO_TAGS_CREATED": "No Tags Created",
"NO_EXISTING_TAGS": "No Existing Tags",
"TITLE": "Create Tags",
"CREATE_TAG": "Create: {{tag}}",
"NAME": "Name",
"ERRORS": {
"EXISTING_TAG": "Tag already exists",
"ALREADY_ADDED_TAG": "Tag is already added",
"EMPTY_TAG": "Tag name can't contain only spaces",
"REQUIRED": "Tag name is required",
"FETCH_TAGS": "Error while fetching the tags",
"CREATE_TAGS": "Error while creating the tags"
},
"TOOLTIPS": {
"DELETE_TAG": "Delete tag",
"HIDE_INPUT": "Hide input"
},
"TAGS_LOADING": "Tags loading",
"CREATE_TAGS_SUCCESS": "Tags created successfully"
}
},
"ADF_FILE_UPLOAD": {
@@ -382,9 +359,7 @@
"CREATED_DATE": "Created Date",
"MODIFIER": "Modifier",
"MODIFIED_DATE": "Modified Date",
"CONTENT_TYPE": "Content Type",
"TAGS": "Tags",
"ADD_TAG_TOOLTIP": "Add tag"
"CONTENT_TYPE": "Content Type"
},
"CONTENT_TYPE": {
"DIALOG" :{
@@ -22,7 +22,3 @@ export * from './tag-node-list.component';
export * from './services/tag.service';
export * from './tag.module';
export * from './tags-creator/tags-creator-mode';
export * from './tags-creator/tags-creator.component';
@@ -345,50 +345,5 @@ describe('TagService', () => {
tick();
}));
});
describe('assignTagsToNode', () => {
let singleResult: TagEntry;
let tags: TagBody[];
const nodeId = 'some node id';
beforeEach(() => {
singleResult = new TagEntry();
singleResult.entry = new Tag();
singleResult.entry.tag = 'some name';
const tag = new TagBody();
tag.tag = 'some name';
tags = [tag];
});
it('should call assignTagsToNode on TagsApi with correct parameters', () => {
spyOn(service.tagsApi, 'assignTagsToNode').and.returnValue(Promise.resolve(singleResult));
service.assignTagsToNode(nodeId, tags);
expect(service.tagsApi.assignTagsToNode).toHaveBeenCalledWith(nodeId, tags);
});
it('should return observable which emits paging object for tags', fakeAsync(() => {
const pagingResult = mockTagPaging();
const tag2 = new TagBody();
tag2.tag = 'some other tag';
tags.push(tag2);
spyOn(service.tagsApi, 'assignTagsToNode').and.returnValue(Promise.resolve(pagingResult));
service.assignTagsToNode(nodeId, tags).subscribe((tagsResult) => {
expect(tagsResult).toEqual(pagingResult);
});
tick();
}));
it('should return observable which emits single tag', fakeAsync(() => {
spyOn(service.tagsApi, 'assignTagsToNode').and.returnValue(Promise.resolve(singleResult));
service.assignTagsToNode(nodeId, tags).subscribe((tagsResult) => {
expect(tagsResult).toEqual(singleResult);
});
tick();
}));
});
});
});
@@ -96,7 +96,7 @@ export class TagService {
* @param tag Name of the tag to remove
* @returns Null object when the operation completes
*/
removeTag(nodeId: string, tag: string): Observable<void> {
removeTag(nodeId: string, tag: string): Observable<any> {
const observableRemove = from(this.tagsApi.deleteTagFromNode(nodeId, tag));
observableRemove.subscribe((data) => {
@@ -191,20 +191,6 @@ export class TagService {
);
}
/**
* Assign tags to node. If tag is new then tag is also created additionally, if tag already exists then it is just assigned.
*
* @param nodeId Id of node to which tags should be assigned.
* @param tags List of tags to create and assign or just assign if they already exist.
*
* @return Just linked tags to node or single tag if linked only one tag.
*/
assignTagsToNode(nodeId: string, tags: TagBody[]): Observable<TagPaging | TagEntry> {
return from(this.tagsApi.assignTagsToNode(nodeId, tags)).pipe(
tap((data) => this.refresh.emit(data))
);
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
@@ -90,7 +90,7 @@ describe('TagActionsComponent', () => {
it('Tag list click on delete button should delete the tag', async () => {
component.nodeId = 'fake-node-id';
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
spyOn(tagService, 'removeTag').and.returnValue(of(true));
component.ngOnChanges();
fixture.detectChanges();
@@ -98,7 +98,7 @@ describe('TagNodeList', () => {
});
it('Tag list click on delete button should delete the tag', async () => {
spyOn(tagService, 'removeTag').and.returnValue(of(undefined));
spyOn(tagService, 'removeTag').and.returnValue(of(true));
component.ngOnChanges();
fixture.detectChanges();
@@ -24,13 +24,10 @@ import { TagActionsComponent } from './tag-actions.component';
import { TagListComponent } from './tag-list.component';
import { TagNodeListComponent } from './tag-node-list.component';
import { CoreModule } from '@alfresco/adf-core';
import { TagsCreatorComponent } from './tags-creator/tags-creator.component';
import { ContentDirectiveModule } from '../directives/content-directive.module';
@NgModule({
imports: [
CommonModule,
ContentDirectiveModule,
MaterialModule,
FormsModule,
ReactiveFormsModule,
@@ -39,14 +36,12 @@ import { ContentDirectiveModule } from '../directives/content-directive.module';
exports: [
TagActionsComponent,
TagListComponent,
TagNodeListComponent,
TagsCreatorComponent
TagNodeListComponent
],
declarations: [
TagActionsComponent,
TagListComponent,
TagNodeListComponent,
TagsCreatorComponent
TagNodeListComponent
]
})
export class TagModule {}
@@ -1,26 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Available modes for tags creator.
* Create mode allows only for creating completely new tags.
* Create and Assign mode allows for both - creation of new tags and selection of existing tags.
*/
export enum TagsCreatorMode {
CREATE,
CREATE_AND_ASSIGN
}
@@ -1,87 +0,0 @@
<div class="adf-tags-creation">
<p
class="adf-no-tags-message"
*ngIf="!tags.length && !tagNameControlVisible">
{{ 'TAG.TAGS_CREATOR.NO_TAGS_CREATED' | translate }}
</p>
<div
class="adf-tags-list"
[class.adf-tags-list-with-scrollbar]="tagsListScrollbarVisible"
#tagsList>
<p
*ngFor="let tag of tags"
class="adf-tag adf-label-with-icon-button">
{{ tag }}
<button
data-automation-id="remove-tag-button"
mat-icon-button
(click)="removeTag(tag)"
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.DELETE_TAG' | translate"
[hidden]="disabledTagsRemoving">
<mat-icon>remove</mat-icon>
</button>
</p>
</div>
<div
class="adf-tag-name-field"
*ngIf="(!tagNameControlVisible && tags.length) || tagNameControlVisible"
[hidden]="!tagNameControlVisible">
<mat-form-field *ngIf="tagNameControlVisible">
<mat-icon matPrefix>search</mat-icon>
<mat-label id="adf-tag-name-input-label">
{{ 'TAG.TAGS_CREATOR.NAME' | translate }}
</mat-label>
<input
#tagNameInput
matInput
autocomplete="off"
[formControl]="tagNameControl"
(keyup.enter)="addTag()"
aria-labelledby="adf-tag-name-input-label"
adf-auto-focus
/>
<mat-error [hidden]="!tagNameControl.invalid">{{ tagNameErrorMessageKey | translate }}</mat-error>
</mat-form-field>
<button
data-automation-id="hide-tag-name-input-button"
mat-icon-button
(click)="hideNameInput()"
[attr.title]="'TAG.TAGS_CREATOR.TOOLTIPS.HIDE_INPUT' | translate">
<mat-icon>remove</mat-icon>
</button>
</div>
</div>
<div
class="adf-existing-tags-panel"
*ngIf="existingTagsPanelVisible">
<ng-container *ngIf="!spinnerVisible || existingTags">
<span
class="adf-create-tag-label"
(click)="addTag()"
[hidden]="tagNameControl.invalid || typing">
{{ 'TAG.TAGS_CREATOR.CREATE_TAG' | translate : { tag: tagNameControl.value } }}
</span>
<p class="adf-existing-tags-label">
{{ existingTagsLabelKey | translate }}
</p>
</ng-container>
<div class="adf-tags-list">
<mat-selection-list
*ngIf="!spinnerVisible || existingTags"
(selectionChange)="addExistingTagToTagsToAssign($event)"
[disabled]="isOnlyCreateMode()">
<mat-list-option
*ngFor="let tagRow of existingTags"
class="adf-tag"
[value]="tagRow">
{{ tagRow.entry.tag }}
</mat-list-option>
<p *ngIf="!existingTags?.length">{{ 'TAG.TAGS_CREATOR.NO_EXISTING_TAGS' | translate }}</p>
</mat-selection-list>
<mat-spinner
*ngIf="spinnerVisible"
[diameter]="50"
[attr.aria-label]="'TAG.TAGS_CREATOR.TAGS_LOADING' | translate">
</mat-spinner>
</div>
</div>
@@ -1,144 +0,0 @@
adf-tags-creator {
display: block;
margin-left: -24px;
&.adf-creator-with-existing-tags-panel {
background-color: var(--theme-background-color);
}
.adf-label-with-icon-button {
display: flex;
justify-content: space-between;
align-items: center;
}
.adf-no-tags-message {
margin-left: 9px;
margin-top: 28.5px;
margin-bottom: 0;
height: 30px;
}
.adf-tag-name-field,
.adf-tag-name-field[hidden] {
display: flex;
align-items: center;
min-height: 57px;
mat-form-field {
width: 100%;
box-sizing: border-box;
padding-right: 3px;
font-size: 14px;
}
}
.adf-create-tag-label {
color: var(--theme-primary-color);
cursor: pointer;
margin-top: -1px;
padding-left: 27px;
overflow-wrap: anywhere;
display: inline-block;
padding-right: 12px;
overflow: auto;
max-height: 7vh;
}
.adf-tags-list {
padding-left: 10px;
padding-right: 0;
overflow: auto;
&.adf-tags-list-with-scrollbar {
padding-right: 7px;
margin-right: -22px;
}
}
.adf-tag {
margin-top: 0;
overflow-wrap: anywhere;
& + .adf-tag {
margin-top: -14px;
}
}
.adf-tags-creation {
padding-left: 27px;
padding-right: 22px;
}
.adf-existing-tags-panel {
background-color: var(--theme-card-background-color);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
padding-top: 12px;
position: relative;
width: 100%;
z-index: 100;
.adf-existing-tags-label {
font-size: 10px;
color: var(--theme-secondary-text-color);
padding-left: 12px;
margin-bottom: 2px;
}
.adf-tags-list {
padding-left: 18px;
margin-top: -2px;
padding-right: 0;
display: flex;
flex-direction: column;
min-height: 75px;
.adf-tag {
margin-bottom: 18px;
margin-top: 0;
padding-right: 12px;
font-size: 14px;
height: auto;
width: unset;
& + .adf-tag {
margin-top: -10px;
}
.mat-list-item-content-reverse {
padding: 0 6px;
.mat-pseudo-checkbox {
display: none;
}
}
}
mat-spinner {
margin: auto;
overflow: unset;
min-height: 25px;
}
.mat-list-item-disabled {
background-color: inherit;
color: inherit;
}
}
}
mat-dialog-actions {
margin-right: -24px;
background-color: inherit;
.adf-save-button {
margin-right: 24px;
}
}
[hidden] {
visibility: hidden;
display: unset;
}
}
@@ -1,689 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { TagsCreatorComponent } from './tags-creator.component';
import { NotificationService } from '@alfresco/adf-core';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { MatIconModule } from '@angular/material/icon';
import { MatError, MatFormField, MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { ContentDirectiveModule, TagsCreatorMode, TagService } from '@alfresco/adf-content-services';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { EMPTY, of, throwError } from 'rxjs';
import { DebugElement } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatListModule, MatSelectionList, MatSelectionListChange } from '@angular/material/list';
describe('TagsCreatorComponent', () => {
let fixture: ComponentFixture<TagsCreatorComponent>;
let component: TagsCreatorComponent;
let tagService: TagService;
let notificationService: NotificationService;
const tagNameFieldSelector = '.adf-tag-name-field';
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TagsCreatorComponent],
imports: [
ContentDirectiveModule,
MatButtonModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatListModule,
NoopAnimationsModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
providers: [
{
provide: TagService,
useValue: {
findTagByName: () => of(null),
searchTags: () =>
of({
list: {
entries: []
}
})
}
},
{
provide: NotificationService,
useValue: {
showError: () => ({})
}
}
]
});
fixture = TestBed.createComponent(TagsCreatorComponent);
component = fixture.componentInstance;
tagService = TestBed.inject(TagService);
notificationService = TestBed.inject(NotificationService);
fixture.detectChanges();
});
function getNameInput(): HTMLInputElement {
return fixture.debugElement.query(By.css(`.adf-tag-name-field input`))?.nativeElement;
}
function getCreateTagLabel(): HTMLSpanElement {
return fixture.debugElement.query(By.css('.adf-create-tag-label'))?.nativeElement;
}
function getRemoveTagButtons(): HTMLButtonElement[] {
const elements = fixture.debugElement.queryAll(By.css(`[data-automation-id="remove-tag-button"]`));
return elements.map(el => el.nativeElement);
}
function clickAtHideNameInputButton() {
fixture.debugElement.query(By.css(`[data-automation-id="hide-tag-name-input-button"]`)).nativeElement.click();
fixture.detectChanges();
}
function getAddedTags(): string[] {
const tagElements = fixture.debugElement.queryAll(By.css(`.adf-tags-creation .adf-tag`));
return tagElements.map(el => el.nativeElement.firstChild.nodeValue.trim());
}
function addTagToAddedList(tagName: string, addUsingEnter?: boolean, typingTimeout = 300): void {
typeTag(tagName, typingTimeout);
if (addUsingEnter) {
getNameInput().dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
} else {
getCreateTagLabel().click();
}
tick(300);
fixture.detectChanges();
}
function typeTag(tagName: string, timeout = 300): void {
component.tagNameControlVisible = true;
fixture.detectChanges();
const tagNameInput = getNameInput();
tagNameInput.value = tagName;
tagNameInput.dispatchEvent(new InputEvent('input'));
tick(timeout);
fixture.detectChanges();
}
function findSelectionList(): MatSelectionList {
return fixture.debugElement.query(By.directive(MatSelectionList)).componentInstance;
}
describe('Created tags list', () => {
it('should display no tags created message after initialization', () => {
const message = fixture.debugElement.query(By.css('.adf-no-tags-message')).nativeElement.textContent.trim();
expect(message).toBe('TAG.TAGS_CREATOR.NO_TAGS_CREATED');
});
it('should display all tags which have been typed in input and accepted using enter', fakeAsync(() => {
const tag1 = 'Tag 1';
const tag2 = 'Tag 2';
addTagToAddedList(tag1, true);
addTagToAddedList(tag2, true);
const tagElements = getAddedTags();
expect(tagElements.length).toBe(2);
expect(tagElements[0]).toBe(tag1);
expect(tagElements[1]).toBe(tag2);
}));
it('should display all tags which have been typed in input and accepted by clicking at create label', fakeAsync(() => {
const tag1 = 'Tag 1';
const tag2 = 'Tag 2';
addTagToAddedList(tag1);
addTagToAddedList(tag2);
const tagElements = getAddedTags();
expect(tagElements).toEqual([tag1, tag2]);
}));
it('should not add tag if contains only spaces', fakeAsync(() => {
addTagToAddedList(' ', true);
expect(getAddedTags().length).toBe(0);
}));
it('should not add tag if field is empty', fakeAsync(() => {
addTagToAddedList('', true);
expect(getAddedTags().length).toBe(0);
}));
it('should not duplicate already added tag', fakeAsync(() => {
const tag = 'Some tag';
addTagToAddedList(tag, true);
addTagToAddedList(tag, true);
expect(getAddedTags().length).toBe(1);
}));
it('should not duplicate already existing tag', fakeAsync(() => {
const tag = 'Tag';
spyOn(tagService, 'findTagByName').and.returnValue(of({
entry: {
tag,
id: 'tag-1'
}
}));
addTagToAddedList(tag, true);
expect(getAddedTags().length).toBe(0);
}));
it('should not add tag if hit enter during tags loading', fakeAsync(() => {
addTagToAddedList('Tag', true, 0);
expect(getAddedTags().length).toBe(0);
}));
it('should remove specific tag after clicking at remove icon', fakeAsync(() => {
const tag1 = 'Tag 1';
const tag2 = 'Tag 2';
addTagToAddedList(tag1);
addTagToAddedList(tag2);
getRemoveTagButtons()[0].click();
tick();
fixture.detectChanges();
const tagElements = getAddedTags();
expect(tagElements).toEqual([tag2]);
}));
it('should hide button for removing tag if disabledTagsRemoving is true', fakeAsync(() => {
const tag1 = 'Tag 1';
component.disabledTagsRemoving = true;
addTagToAddedList(tag1);
tick();
expect(getRemoveTagButtons()[0].hasAttribute('hidden')).toBeTrue();
}));
it('should show button for removing tag if disabledTagsRemoving is false', fakeAsync(() => {
const tag1 = 'Tag 1';
component.disabledTagsRemoving = false;
addTagToAddedList(tag1);
tick();
expect(getRemoveTagButtons()[0].hasAttribute('hidden')).toBeFalse();
}));
it('should display tags passed by tags input', () => {
component.tags = ['Passed tag 1', 'Passed tag 2'];
fixture.detectChanges();
expect(getAddedTags()).toEqual(component.tags);
});
});
describe('Tag name field', () => {
it('should be visible if tagNameControlVisible is true', () => {
component.tagNameControlVisible = true;
fixture.detectChanges();
const tagNameField = fixture.debugElement.query(By.css(tagNameFieldSelector));
expect(tagNameField).toBeTruthy();
expect(tagNameField.nativeElement.hasAttribute('hidden')).toBeFalsy();
expect(tagNameField.query(By.directive(MatFormField))).toBeTruthy();
});
it('should be hidden after clicking button for hiding input', fakeAsync(() => {
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
clickAtHideNameInputButton();
const tagNameField = fixture.debugElement.query(By.css(tagNameFieldSelector));
expect(tagNameField).toBeFalsy();
}));
it('should input be autofocused', fakeAsync(() => {
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getNameInput()).toBe(document.activeElement as HTMLInputElement);
}));
it('should input be autofocused after showing input second time', fakeAsync(() => {
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
clickAtHideNameInputButton();
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
expect(getNameInput()).toBe(document.activeElement as HTMLInputElement);
}));
describe('Errors', () => {
function getFirstError(): string {
const error = fixture.debugElement.query(By.directive(MatError));
return error.nativeElement.textContent;
}
it('should show error for only spaces', fakeAsync(() => {
typeTag(' ');
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.EMPTY_TAG');
}));
it('should show error for required', fakeAsync(() => {
typeTag('');
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.REQUIRED');
}));
it('should show error when duplicated already added tag', fakeAsync(() => {
const tag = 'Some tag';
addTagToAddedList(tag);
typeTag(tag);
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.ALREADY_ADDED_TAG');
}));
it('should show error when duplicated already existing tag', fakeAsync(() => {
const tag = 'Some tag';
spyOn(tagService, 'findTagByName').and.returnValue(of({
entry: {
tag,
id: 'tag-1'
}
}));
typeTag(tag);
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.EXISTING_TAG');
}));
it('should error for required when not typed anything and blur input', fakeAsync(() => {
component.tagNameControlVisible = true;
fixture.detectChanges();
tick(100);
getNameInput().blur();
fixture.detectChanges();
const error = getFirstError();
expect(error).toBe('TAG.TAGS_CREATOR.ERRORS.REQUIRED');
flush();
}));
});
});
describe('Existing tags panel', () => {
function getPanel(): DebugElement {
return fixture.debugElement.query(By.css(`.adf-existing-tags-panel`));
}
it('should be visible when input is visible and something is typed in input', fakeAsync(() => {
typeTag('some tag');
expect(getPanel()).toBeTruthy();
}));
it('should not be visible initially when input is hidden and nothing was typed', () => {
expect(getPanel()).toBeFalsy();
});
it('should not be visible when input is visible and empty string is typed in input', fakeAsync(() => {
typeTag(' ');
expect(getPanel()).toBeFalsy();
}));
it('should not be visible when input is visible and nothing has been typed', () => {
component.tagNameControlVisible = true;
fixture.detectChanges();
expect(getPanel()).toBeFalsy();
});
it('should not be visible when something has been typed and input has been hidden', fakeAsync(() => {
typeTag('some tag');
clickAtHideNameInputButton();
expect(getPanel()).toBeFalsy();
}));
describe('Label for tag creation', () => {
it('should be visible', fakeAsync(() => {
typeTag('some tag');
expect(getCreateTagLabel()).toBeTruthy();
}));
it('should not be visible if typed only spaces', fakeAsync(() => {
typeTag(' ');
expect(getCreateTagLabel()).toBeFalsy();
}));
it('should not be visible if required error occurs', fakeAsync(() => {
typeTag('');
expect(getCreateTagLabel()).toBeFalsy();
}));
it('should not be visible when trying to duplicate already added tag', fakeAsync(() => {
const tag = 'Some tag';
addTagToAddedList(tag);
typeTag(tag);
expect(getCreateTagLabel().hasAttribute('hidden')).toBeTruthy();
}));
it('should not be visible when trying to duplicate already existing tag', fakeAsync(() => {
const tag = 'Tag';
spyOn(tagService, 'findTagByName').and.returnValue(of({
entry: {
tag,
id: 'tag-1'
}
}));
typeTag(tag);
expect(getCreateTagLabel().hasAttribute('hidden')).toBeTruthy();
}));
it('should not be visible if typed nothing', () => {
component.tagNameControlVisible = true;
fixture.detectChanges();
expect(getCreateTagLabel()).toBeFalsy();
});
it('should not be visible during typing', fakeAsync(() => {
typeTag('some tag', 0);
expect(getCreateTagLabel()).toBeFalsy();
discardPeriodicTasks();
flush();
}));
});
describe('Existing tags', () => {
function getExistingTags(): string[] {
const tagElements = fixture.debugElement.queryAll(By.css(`.adf-existing-tags-panel .adf-tag .mat-list-text`));
return tagElements.map(el => el.nativeElement.firstChild.nodeValue.trim());
}
it('should call findTagByName on tagService using name set in input', fakeAsync(() => {
spyOn(tagService, 'findTagByName').and.returnValue(EMPTY);
const name = 'Tag';
typeTag(name);
expect(tagService.findTagByName).toHaveBeenCalledWith(name);
}));
it('should call searchTags on tagService using name set in input and correct params', fakeAsync(() => {
spyOn(tagService, 'searchTags').and.returnValue(EMPTY);
const name = 'Tag';
typeTag(name);
expect(tagService.searchTags).toHaveBeenCalledWith(name, { orderBy: 'tag', direction: 'asc' },
false, 0, 15 );
}));
it('should display loaded existing tags', fakeAsync(() => {
const tag1 = 'Tag 1';
const tag2 = 'Tag 2';
spyOn(tagService, 'searchTags').and.returnValue(
of({
list: {
entries: [
{ entry: { tag: tag1 } as any },
{ entry: { tag: tag2 } as any }
],
pagination: {}
}
})
);
typeTag('Tag');
const tagElements = getExistingTags();
expect(tagElements).toEqual([tag1, tag2]);
}));
it('should exclude tags passed through tags input from loaded existing tags', fakeAsync(() => {
const tag1 = 'Tag 1';
const tag2 = 'Tag 2';
component.tags = [tag1];
spyOn(tagService, 'searchTags').and.returnValue(
of({
list: {
entries: [
{ entry: { tag: tag1 } as any },
{ entry: { tag: tag2 } as any }
],
pagination: {}
}
})
);
typeTag('Tag');
expect(getExistingTags()).toEqual([tag2]);
}));
it('should not display existing tags if searching fails', fakeAsync(() => {
spyOn(notificationService, 'showError');
spyOn(tagService, 'searchTags').and.returnValue(throwError({}));
typeTag('Tag');
expect(getExistingTags()).toEqual([]);
expect(notificationService.showError).toHaveBeenCalledWith('TAG.TAGS_CREATOR.ERRORS.FETCH_TAGS');
}));
it('should display exact tag', fakeAsync(() => {
const tag = 'Tag';
spyOn(tagService, 'findTagByName').and.returnValue(of({
entry: {
tag,
id: 'tag-1'
}
}));
typeTag(tag);
const tagElements = getExistingTags();
expect(tagElements).toEqual([tag]);
}));
it('should not display exact tag if that tag was passed through tags input', fakeAsync(() => {
const tag = 'Tag';
component.tags = [tag];
spyOn(tagService, 'findTagByName').and.returnValue(of({
entry: {
tag,
id: 'tag-1'
}
}));
typeTag(tag);
expect(getExistingTags()).toEqual([]);
}));
it('should not display exact tag if exact tag loading fails', fakeAsync(() => {
spyOn(notificationService, 'showError');
spyOn(tagService, 'findTagByName').and.returnValue(throwError({}));
typeTag('Tag');
expect(getExistingTags()).toEqual([]);
expect(notificationService.showError).toHaveBeenCalledWith('TAG.TAGS_CREATOR.ERRORS.FETCH_TAGS');
}));
it('should exact tag be above others existing tags when there are some different existing tags than exact tag', fakeAsync(() => {
const tag = 'Tag';
const tag1 = 'Tag 1';
const tag2 = 'Tag 2';
spyOn(tagService, 'findTagByName').and.returnValue(of({
entry: {
tag,
id: 'tag-1'
}
}));
spyOn(tagService, 'searchTags').and.returnValue(
of({
list: {
entries: [
{ entry: { tag: tag1 } as any },
{ entry: { tag: tag2 } as any }
],
pagination: {}
}
})
);
typeTag(tag);
const tagElements = getExistingTags();
expect(tagElements).toEqual([tag, tag1, tag2]);
}));
it('should selection be disabled if mode is Create', fakeAsync(() => {
component.mode = TagsCreatorMode.CREATE;
spyOn(tagService, 'searchTags').and.returnValue(
of({
list: {
entries: [
{ entry: { tag: 'tag' } as any }
],
pagination: {}
}
})
);
typeTag('Tag');
expect(findSelectionList().disabled).toBeTrue();
}));
it('should selection be enabled if mode is Create And Assign', fakeAsync(() => {
component.mode = TagsCreatorMode.CREATE_AND_ASSIGN;
spyOn(tagService, 'searchTags').and.returnValue(
of({
list: {
entries: [
{ entry: { tag: 'tag' } as any }
],
pagination: {}
}
})
);
typeTag('Tag');
expect(findSelectionList().disabled).toBeFalse();
}));
it('should select existing tag when selectionChange event emits', fakeAsync(() => {
const selectedTag = { entry: { tag: 'tag1' } as any };
const leftTag = 'tag2';
component.mode = TagsCreatorMode.CREATE_AND_ASSIGN;
spyOn(tagService, 'searchTags').and.returnValue(
of({
list: {
entries: [
selectedTag,
{ entry: { tag: leftTag } as any }
],
pagination: {}
}
})
);
typeTag('Tag');
findSelectionList().selectionChange.emit({
options: [{
value: selectedTag
}]
} as MatSelectionListChange);
fixture.detectChanges();
expect(getAddedTags()).toEqual([selectedTag.entry.tag]);
expect(getExistingTags()).toEqual([leftTag]);
}));
});
describe('Spinner', () => {
function getSpinner(): DebugElement {
return fixture.debugElement.query(By.css(`.mat-progress-spinner`));
}
it('should be displayed when existing tags are loading', fakeAsync(() => {
typeTag('tag', 0);
const spinner = getSpinner();
expect(spinner).toBeTruthy();
discardPeriodicTasks();
flush();
}));
it('should not be displayed when existing tags stopped loading', fakeAsync(() => {
typeTag('tag');
const spinner = getSpinner();
expect(spinner).toBeFalsy();
}));
it('should have correct diameter', fakeAsync(() => {
typeTag('tag', 0);
const spinner = getSpinner();
expect(spinner.componentInstance.diameter).toBe(50);
discardPeriodicTasks();
flush();
}));
});
});
});
@@ -1,427 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TagEntry, TagPaging } from '@alfresco/js-api';
import {
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { debounce, distinctUntilChanged, finalize, first, map, takeUntil, tap } from 'rxjs/operators';
import { EMPTY, forkJoin, Observable, Subject, timer } from 'rxjs';
import { NotificationService } from '@alfresco/adf-core';
import { TagsCreatorMode } from './tags-creator-mode';
import { MatSelectionListChange } from '@angular/material/list';
import { TagService } from '../services/tag.service';
interface TagNameControlErrors {
duplicatedExistingTag?: boolean;
duplicatedAddedTag?: boolean;
emptyTag?: boolean;
required?: boolean;
}
const DEFAULT_TAGS_SORTING = {
orderBy: 'tag',
direction: 'asc'
};
/**
* Allows to create multiple tags. That component contains input and two lists. Top list is all created tags, bottom list is searched tags based on input's value.
*/
@Component({
selector: 'adf-tags-creator',
templateUrl: './tags-creator.component.html',
styleUrls: ['./tags-creator.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TagsCreatorComponent implements OnInit, OnDestroy {
/**
* Mode for component.
* In Create mode we can't select existing tags, we can only create them.
* In Create and Assign mode we can both - create tags and select existing tags.
*/
@Input()
mode: TagsCreatorMode;
/**
* False if tags can be removed from top list, true otherwise.
*/
@Input()
disabledTagsRemoving = false;
/**
* Default top list.
* @param tags tags which should be displayed as default tags for top list.
*/
@Input()
set tags(tags: string[]) {
this._tags = [...tags];
this._spinnerVisible = true;
this._initialExistingTags = null;
this._existingTags = null;
this.loadTags(this.tagNameControl.value);
this.tagNameControl.updateValueAndValidity();
}
get tags(): string[] {
return this._tags;
}
/**
* Decides if input for tags creation/searching should be visible. When input is hidden then panel of existing tags is hidden as well.
* @param tagNameControlVisible true if input should be visible, false otherwise.
*/
@Input()
set tagNameControlVisible(tagNameControlVisible: boolean) {
this._tagNameControlVisible = tagNameControlVisible;
if (tagNameControlVisible) {
this._existingTagsPanelVisible = !!this.tagNameControl.value.trim();
setTimeout(() => {
this.tagNameInputElement.nativeElement.scrollIntoView();
});
} else {
this._existingTagsPanelVisible = false;
}
this.existingTagsPanelVisibilityChange.emit(this.existingTagsPanelVisible);
}
get tagNameControlVisible(): boolean {
return this._tagNameControlVisible;
}
/**
* Emitted when bottom list is showing or hiding.
*/
@Output()
existingTagsPanelVisibilityChange = new EventEmitter<boolean>();
@Output()
/**
* Emitted when tags in top list are changed.
*/
tagsChange = new EventEmitter<string[]>();
/**
* Emitted when input is showing or hiding.
*/
@Output()
tagNameControlVisibleChange = new EventEmitter<boolean>();
readonly nameErrorMessagesByErrors = new Map<keyof TagNameControlErrors, string>([
['duplicatedExistingTag', 'EXISTING_TAG'],
['duplicatedAddedTag', 'ALREADY_ADDED_TAG'],
['emptyTag', 'EMPTY_TAG'],
['required', 'REQUIRED']
]);
private readonly existingTagsListLimit = 15;
private exactTagSet$ = new Subject<void>();
private _tags: string[] = [];
private _tagNameControl = new FormControl<string>(
'',
[
this.validateIfNotAlreadyAdded.bind(this),
Validators.required,
this.validateEmptyTag
],
this.validateIfNotExistingTag.bind(this)
);
private _tagNameControlVisible = false;
private _existingTags: TagEntry[];
private _initialExistingTags: TagEntry[];
private onDestroy$ = new Subject<void>();
private _tagNameErrorMessageKey = '';
private _spinnerVisible = false;
private _typing = false;
private _tagsListScrollbarVisible = false;
private cancelExistingTagsLoading$ = new Subject<void>();
private existingExactTag: TagEntry;
private _existingTagsPanelVisible: boolean;
private _existingTagsLabelKey: string;
@ViewChild('tagsList')
private tagsListElement: ElementRef;
@ViewChild('tagNameInput')
private tagNameInputElement: ElementRef;
constructor(
private tagService: TagService,
private notificationService: NotificationService
) {}
ngOnInit(): void {
this.tagNameControl.valueChanges
.pipe(
map((name: string) => name.trim()),
distinctUntilChanged(),
tap((name: string) => {
this._typing = true;
if (name) {
this._spinnerVisible = true;
this._existingTagsPanelVisible = true;
} else {
this._existingTagsPanelVisible = false;
}
this.existingTagsPanelVisibilityChange.emit(this.existingTagsPanelVisible);
this.cancelExistingTagsLoading$.next();
this._initialExistingTags = null;
this._existingTags = null;
}),
debounce((name: string) => (name ? timer(300) : EMPTY)),
takeUntil(this.onDestroy$)
)
.subscribe((name: string) => this.onTagNameControlValueChange(name));
this.tagNameControl.statusChanges
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => this.setTagNameControlErrorMessageKey());
this.setTagNameControlErrorMessageKey();
}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();
this.cancelExistingTagsLoading$.next();
this.cancelExistingTagsLoading$.complete();
}
@HostBinding('class.adf-creator-with-existing-tags-panel')
get hostClass(): boolean {
return this.existingTagsPanelVisible;
}
get tagNameControl(): FormControl<string> {
return this._tagNameControl;
}
get existingTags(): TagEntry[] {
return this._existingTags;
}
get tagNameErrorMessageKey(): string {
return this._tagNameErrorMessageKey;
}
get spinnerVisible(): boolean {
return this._spinnerVisible;
}
get typing(): boolean {
return this._typing;
}
get tagsListScrollbarVisible(): boolean {
return this._tagsListScrollbarVisible;
}
get existingTagsPanelVisible(): boolean {
return this._existingTagsPanelVisible;
}
get existingTagsLabelKey(): string {
return this._existingTagsLabelKey;
}
/**
* Hide input for typing name for new tag or for searching. When input is hidden then panel of existing tags is hidden as well.
*/
hideNameInput(): void {
this.tagNameControlVisible = false;
this._existingTagsPanelVisible = false;
this.existingTagsPanelVisibilityChange.emit(this.existingTagsPanelVisible);
this.tagNameControlVisibleChange.emit(this.tagNameControlVisible);
}
/**
* Add tags to top list using value which is set in input. Adding tag is not allowed when value in input is invalid
* or if user is still typing what means that validation for input is not called yet.
*/
addTag(): void {
if (!this._typing && !this.tagNameControl.invalid) {
this.tags.push(this.tagNameControl.value.trim());
this.hideNameInput();
this.tagNameControl.setValue('');
this.checkScrollbarVisibility();
this.tagNameControl.markAsUntouched();
this.tagsChange.emit(this.tags);
}
}
/**
* Remove tag from top list. In case that tag was part of search result then that tag is moved to bottom list
* (list of existing tags) after removing so user can reselect it again later.
* @param tag tag's name which should be removed from top list.
*/
removeTag(tag: string): void {
this.removeTagFromArray(this.tags, tag);
this.tagNameControl.updateValueAndValidity();
this.updateExistingTagsListOnRemoveFromTagsToConfirm(tag);
this.checkScrollbarVisibility();
this.tagsChange.emit(this.tags);
}
/**
* Called when user selects any tag from list of existing tags. It moves tag from existing tags list to top list.
* @param change
*/
addExistingTagToTagsToAssign(change: MatSelectionListChange): void {
const selectedTag: TagEntry = change.options[0].value;
this.tags.push(selectedTag.entry.tag);
this.removeTagFromArray(this.existingTags, selectedTag);
this.tagNameControl.updateValueAndValidity();
this.exactTagSet$.next();
this.tagsChange.emit(this.tags);
}
/**
* Checks if component is in Create mode.
* @return true if Create mode, false otherwise.
*/
isOnlyCreateMode(): boolean {
return this.mode === TagsCreatorMode.CREATE;
}
private onTagNameControlValueChange(name: string): void {
this.tagNameControl.markAsTouched();
this.loadTags(name);
}
private loadTags(name: string) {
if (name) {
forkJoin({
exactResult: this.tagService.findTagByName(name),
searchedResult: this.tagService.searchTags(name, DEFAULT_TAGS_SORTING, false, 0, this.existingTagsListLimit)
}).pipe(
takeUntil(this.cancelExistingTagsLoading$),
finalize(() => (this._typing = false))
).subscribe(({ exactResult, searchedResult }: {
exactResult: TagEntry;
searchedResult: TagPaging;
}) => {
if (exactResult) {
this.existingExactTag = exactResult;
this.removeExactTagFromSearchedResult(searchedResult);
searchedResult.list.entries.unshift(exactResult);
} else {
this.existingExactTag = null;
}
this._initialExistingTags = searchedResult.list.entries;
this.excludeAlreadyAddedTags(this._initialExistingTags);
this.exactTagSet$.next();
this._spinnerVisible = false;
}, () => {
this.notificationService.showError('TAG.TAGS_CREATOR.ERRORS.FETCH_TAGS');
this._spinnerVisible = false;
});
} else {
this.existingExactTag = null;
}
}
private removeExactTagFromSearchedResult(searchedResult: TagPaging): void {
const exactTagIndex = searchedResult.list.entries.findIndex(
(row) => this.compareTags(row.entry.tag, this.existingExactTag.entry.tag)
);
if (exactTagIndex > -1) {
searchedResult.list.entries.splice(exactTagIndex, 1);
}
}
private validateIfNotExistingTag(tagNameControl: FormControl<string>): Observable<TagNameControlErrors | null> {
return this.exactTagSet$.pipe(
map<void, TagNameControlErrors | null>(() =>
this.compareTags(tagNameControl.value, this.existingExactTag?.entry?.tag) ? { duplicatedExistingTag: true }
: null
), first()
);
}
private validateIfNotAlreadyAdded(tagNameControl: FormControl<string>): TagNameControlErrors | null {
return this.tags.some((tag) => this.compareTags(tag, tagNameControl.value))
? { duplicatedAddedTag: true }
: null;
}
private compareTags(tagName1?: string, tagName2?: string): boolean {
return tagName1?.trim().toUpperCase() === tagName2?.trim().toUpperCase();
}
private validateEmptyTag(tagNameControl: FormControl<string>): TagNameControlErrors | null {
return tagNameControl.value.length && !tagNameControl.value.trim()
? { emptyTag: true }
: null;
}
private setTagNameControlErrorMessageKey(): void {
this._tagNameErrorMessageKey = this.tagNameControl.invalid
? `TAG.TAGS_CREATOR.ERRORS.${this.nameErrorMessagesByErrors.get(
Object.keys(this.tagNameControl.errors)[0] as keyof TagNameControlErrors
)}`
: '';
}
private checkScrollbarVisibility(): void {
setTimeout(() => {
this._tagsListScrollbarVisible =
this.tagsListElement.nativeElement.scrollHeight >
this.tagsListElement.nativeElement.clientHeight;
});
}
private removeTagFromArray<T>(tags: T[], tag: T) {
tags.splice(tags.indexOf(tag), 1);
}
private updateExistingTagsListOnRemoveFromTagsToConfirm(tag: string) {
const entryForTagAddedToExistingTags = this._initialExistingTags?.find(
(tagEntry) => tagEntry.entry.tag === tag
);
if (entryForTagAddedToExistingTags) {
this.existingTags.unshift(entryForTagAddedToExistingTags);
if (this.existingExactTag) {
if (tag !== this.existingExactTag.entry.tag) {
this.removeTagFromArray(this.existingTags, this.existingExactTag);
this.sortExistingTags();
this.existingTags.unshift(this.existingExactTag);
}
} else {
this.sortExistingTags();
}
this.exactTagSet$.next();
}
}
private sortExistingTags() {
this.existingTags.sort((tagEntry1, tagEntry2) =>
tagEntry1.entry.tag.localeCompare(tagEntry2.entry.tag));
}
private excludeAlreadyAddedTags(tags: TagEntry[]) {
this._existingTags = tags.filter((tag) => !this.tags.includes(tag.entry.tag));
}
}
@@ -962,8 +962,10 @@ describe('FormCloudComponent', () => {
expect(formComponent.isOutcomeButtonEnabled(startProcessOutcome)).toBeTruthy();
});
it('should raise [executeOutcome] event for formService', async () => {
spyOn(formComponent.executeOutcome, 'emit');
it('should raise [executeOutcome] event for formService', (done) => {
formComponent.executeOutcome.subscribe(() => {
done();
});
const outcome = new FormOutcomeModel(new FormModel(), {
id: FormCloudComponent.CUSTOM_OUTCOME_ID,
@@ -972,10 +974,6 @@ describe('FormCloudComponent', () => {
formComponent.form = new FormModel();
formComponent.onOutcomeClicked(outcome);
fixture.detectChanges();
await fixture.whenStable();
expect(formComponent.executeOutcome.emit).toHaveBeenCalledTimes(1);
});
it('should refresh form values when data is changed', (done) => {
@@ -47,29 +47,31 @@ describe('FormDefinitionCloudComponent', () => {
getFormsSpy = spyOn(service, 'getStandAloneTaskForms').and.returnValue(of([{ id: 'fake-form', name: 'fakeForm' } as any]));
});
it('should load the forms by default', async () => {
it('should load the forms by default', () => {
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const clickMatSelect = fixture.debugElement.query(By.css(('.mat-select-trigger')));
clickMatSelect.triggerEventHandler('click', null);
fixture.detectChanges();
const options: any = fixture.debugElement.queryAll(By.css('mat-option'));
expect(options[0].nativeElement.innerText.trim()).toBe('ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.NONE');
expect(options[1].nativeElement.innerText.trim()).toBe('fakeForm');
expect(getFormsSpy).toHaveBeenCalled();
fixture.whenStable().then(() => {
fixture.detectChanges();
const clickMatSelect = fixture.debugElement.query(By.css(('.mat-select-trigger')));
clickMatSelect.triggerEventHandler('click', null);
fixture.detectChanges();
const options: any = fixture.debugElement.queryAll(By.css('mat-option'));
expect(options[0].nativeElement.innerText.trim()).toBe('ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.NONE');
expect(options[1].nativeElement.innerText.trim()).toBe('fakeForm');
expect(getFormsSpy).toHaveBeenCalled();
});
});
it('should load only None option when no forms exist', async () => {
it('should load only None option when no forms exist', () => {
getFormsSpy.and.returnValue(of([]));
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const clickMatSelect = fixture.debugElement.query(By.css(('.mat-select-trigger')));
clickMatSelect.triggerEventHandler('click', null);
fixture.detectChanges();
const options: any = fixture.debugElement.queryAll(By.css('mat-option'));
expect((options).length).toBe(1);
fixture.whenStable().then(() => {
fixture.detectChanges();
const clickMatSelect = fixture.debugElement.query(By.css(('.mat-select-trigger')));
clickMatSelect.triggerEventHandler('click', null);
fixture.detectChanges();
const options: any = fixture.debugElement.queryAll(By.css('mat-option'));
expect((options).length).toBe(1);
});
});
it('should not preselect any form by default', () => {
@@ -79,17 +81,18 @@ describe('FormDefinitionCloudComponent', () => {
expect(formInput.nodeValue).toBeNull();
});
it('should display the name of the form that is selected', async () => {
it('should display the name of the form that is selected', () => {
fixture.detectChanges();
await fixture.whenStable();
const clickMatSelect = fixture.debugElement.query(By.css(('.mat-select-trigger')));
clickMatSelect.triggerEventHandler('click', null);
fixture.detectChanges();
const options: any = fixture.debugElement.queryAll(By.css('mat-option'));
options[1].triggerEventHandler('click', {});
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('mat-select'));
const selectedValue = ((selected).nativeElement.innerText);
expect(selectedValue.trim()).toBe('fakeForm');
fixture.whenStable().then(() => {
const clickMatSelect = fixture.debugElement.query(By.css(('.mat-select-trigger')));
clickMatSelect.triggerEventHandler('click', null);
fixture.detectChanges();
const options: any = fixture.debugElement.queryAll(By.css('mat-option'));
options[1].triggerEventHandler('click', {});
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('mat-select'));
const selectedValue = ((selected).nativeElement.innerText);
expect(selectedValue.trim()).toBe('fakeForm');
});
});
});
@@ -57,8 +57,7 @@ import {
allSourceParamsWithRelativePath,
fakeLocalPhysicalRecordResponse,
displayableCMParams,
fakeLocalPngHavingCMProperties,
mockMyNodeId
fakeLocalPngHavingCMProperties
} from '../../../mocks/attach-file-cloud-widget.mock';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@@ -183,7 +182,6 @@ describe('AttachFileCloudWidgetComponent', () => {
it('should be able to attach files coming from content selector', async () => {
createUploadWidgetField(new FormModel(), 'attach-file-alfresco', [], contentSourceParam);
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockNodeId);
fixture.detectChanges();
await fixture.whenStable();
clickOnAttachFileWidget('attach-file-alfresco');
@@ -220,12 +218,12 @@ describe('AttachFileCloudWidgetComponent', () => {
expect(element.querySelector('#file-1155-icon')).not.toBeNull();
});
it('should be able to set label property for Attach File widget', async () => {
it('should be able to set label property for Attach File widget', () => {
createUploadWidgetField(new FormModel(), 'attach-file', [], onlyLocalParams, false, 'Label', true);
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('label').innerText).toEqual('Label');
fixture.whenStable().then(() => {
expect(element.querySelector('label').innerText).toEqual('Label');
});
});
it('should reset the custom models when the component gets destroyed', () => {
@@ -377,7 +375,6 @@ describe('AttachFileCloudWidgetComponent', () => {
});
it('Should set default user alias (-my-) as rootNodeId if destinationFolderPath contains wrong alias and single upload for Alfresco Content + Locale', async () => {
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockMyNodeId);
createUploadWidgetField(new FormModel(), 'attach-file-alfresco', [], allSourceWithWrongAliasParams, false);
fixture.detectChanges();
await fixture.whenStable();
@@ -390,7 +387,7 @@ describe('AttachFileCloudWidgetComponent', () => {
});
it('Should set default user alias (-my-) as rootNodeId if destinationFolderPath contains wrong alias and multiple upload for Alfresco Content + Locale', async () => {
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockMyNodeId);
createUploadWidgetField(new FormModel(), 'attach-file-alfresco', [], allSourceWithWrongAliasParams, true);
fixture.detectChanges();
await fixture.whenStable();
@@ -403,7 +400,6 @@ describe('AttachFileCloudWidgetComponent', () => {
});
it('Should set default user alias (-my-) as rootNodeId if destinationFolderPath does not have alias for Alfresco Content + Locale', async () => {
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockMyNodeId);
createUploadWidgetField(new FormModel(), 'attach-file-alfresco', [], allSourceWithNoAliasParams, true);
fixture.detectChanges();
await fixture.whenStable();
@@ -466,7 +462,6 @@ describe('AttachFileCloudWidgetComponent', () => {
});
it('Should be able to set default user alias (-my-) as rootNodeId if the nodeId of the alias is not fetched from the api', async () => {
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockMyNodeId);
createUploadWidgetField(new FormModel(), 'attach-file-alfresco', [], contentSourceParam, false);
fixture.detectChanges();
await fixture.whenStable();
@@ -548,11 +543,7 @@ describe('AttachFileCloudWidgetComponent', () => {
await fixture.whenStable();
});
afterEach(() => {
fixture.destroy();
});
it('should remove file when remove is clicked', async () => {
it('should remove file when remove is clicked', (done) => {
fixture.detectChanges();
const menuButton = fixture.debugElement.query(By.css('#file-fake-properties-option-menu')).nativeElement as HTMLButtonElement;
menuButton.click();
@@ -560,8 +551,10 @@ describe('AttachFileCloudWidgetComponent', () => {
const removeOption = fixture.debugElement.query(By.css('#file-fake-properties-remove')).nativeElement as HTMLButtonElement;
removeOption.click();
fixture.detectChanges();
await fixture.whenRenderingDone();
expect(element.querySelector('#file-fake-properties-icon')).toBeNull();
fixture.whenRenderingDone().then(() => {
expect(element.querySelector('#file-fake-properties-icon')).toBeNull();
done();
});
});
it('should download file when download is clicked', (done) => {
@@ -619,31 +612,31 @@ describe('AttachFileCloudWidgetComponent', () => {
expect(updateFormSpy).toHaveBeenCalledWith(expectedValues);
});
it('should display the default menu options if no options are provided', async () => {
it('should display the default menu options if no options are provided', () => {
widget.field.params = onlyLocalParams;
const inputDebugElement = fixture.debugElement.query(
By.css('#attach-file-alfresco')
);
inputDebugElement.triggerEventHandler('change', {
target: { files: [fakeLocalPngAnswer] }
fixture.detectChanges();
fixture.whenStable().then(() => {
const inputDebugElement = fixture.debugElement.query(
By.css('#attach-file-attach')
);
inputDebugElement.triggerEventHandler('change', {
target: { files: [fakeLocalPngAnswer] }
});
fixture.detectChanges();
const menuButton = fixture.debugElement.query(By.css('#file-1155-option-menu')).nativeElement as HTMLButtonElement;
menuButton.click();
fixture.detectChanges();
const showOption = fixture.debugElement.query(By.css('#file-1155-show-file')).nativeElement as HTMLButtonElement;
const downloadOption = fixture.debugElement.query(By.css('#file-1155-download-file')).nativeElement as HTMLButtonElement;
const retrieveMetadataOption = fixture.debugElement.query(By.css('#file-1155-retrieve-file-metadata')).nativeElement as HTMLButtonElement;
const removeOption = fixture.debugElement.query(By.css('#file-1155-remove')).nativeElement as HTMLButtonElement;
expect(showOption).not.toBeNull();
expect(downloadOption).not.toBeNull();
expect(retrieveMetadataOption).toBeNull();
expect(removeOption).not.toBeNull();
});
fixture.detectChanges();
await fixture.whenStable();
const menuButton = fixture.debugElement.query(By.css('#file-fake-properties-option-menu')).nativeElement as HTMLButtonElement;
menuButton.click();
fixture.detectChanges();
await fixture.whenStable();
const showOption = fixture.debugElement.query(By.css('#file-fake-properties-show-file'));
const downloadOption = fixture.debugElement.query(By.css('#file-fake-properties-download-file'));
const retrieveMetadataOption = fixture.debugElement.query(By.css('#file-fake-properties-retrieve-file-metadata'));
const removeOption = fixture.debugElement.query(By.css('#file-fake-properties-remove'));
expect(showOption).not.toBeNull();
expect(downloadOption).not.toBeNull();
expect(retrieveMetadataOption).toBeNull();
expect(removeOption).not.toBeNull();
});
});
@@ -718,7 +711,6 @@ describe('AttachFileCloudWidgetComponent', () => {
});
it('should have been called on attach file when value was empty', async () => {
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockNodeId);
clickOnAttachFileWidget('attach-file-alfresco');
fixture.detectChanges();
await fixture.whenStable();
@@ -729,7 +721,6 @@ describe('AttachFileCloudWidgetComponent', () => {
});
it('should not be called on attach file when has a file previously', async () => {
spyOn(contentCloudNodeSelectorService, 'getNodeIdFromPath').and.returnValue(mockNodeId);
widget.field.value = [fakeMinimalNode];
clickOnAttachFileWidget('attach-file-alfresco');
@@ -19,7 +19,6 @@ import { TranslateModule } from '@ngx-translate/core';
import { FileViewerWidgetComponent } from './file-viewer.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormModel, FormService, FormFieldModel } from '@alfresco/adf-core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
describe('FileViewerWidgetComponent', () => {
const fakeForm = new FormModel();
@@ -48,8 +47,7 @@ describe('FileViewerWidgetComponent', () => {
TranslateModule.forRoot()
],
declarations: [ FileViewerWidgetComponent ],
providers: [ { provide: FormService, useValue: formServiceStub } ],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
providers: [ { provide: FormService, useValue: formServiceStub } ]
});
formServiceStub = TestBed.inject(FormService);
@@ -292,8 +292,6 @@ export const mockNodeId = new Promise<string>((resolve) => {
resolve('mock-node-id');
});
export const mockMyNodeId = Promise.resolve('-my-');
export const mockNodeIdBasedOnStringVariableValue = new Promise<string>((resolve) => {
resolve('mock-string-value-node-id');
});
@@ -443,6 +443,16 @@ describe('EditProcessFilterCloudComponent', () => {
});
});
it('should have floating labels when values are present', async () => {
fixture.detectChanges();
await fixture.whenStable();
const inputLabelsNodes = document.querySelectorAll('mat-form-field');
inputLabelsNodes.forEach(labelNode => {
expect(labelNode.getAttribute('ng-reflect-float-label')).toBe('auto');
});
});
it('should be able to filter filterProperties when input is defined', async () => {
fixture.detectChanges();
await fixture.whenStable();
@@ -16,15 +16,13 @@
*/
import { Component, SimpleChange, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import {
AppConfigService,
ColumnsSelectorComponent,
CustomEmptyContentTemplateDirective,
DataColumn,
DataRowEvent,
DataTableModule,
getDataColumnMock,
ObjectDataRow,
setupTestBed
@@ -41,9 +39,6 @@ import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud
import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service';
import { ProcessListCloudPreferences } from '../models/process-cloud-preferences';
import { PROCESS_LIST_CUSTOM_VARIABLE_COLUMN } from '../../../models/data-column-custom-data';
import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@Component({
template: `
@@ -64,6 +59,19 @@ class CustomTaskListComponent {
processListCloud: ProcessListCloudComponent;
}
@Component({
template: `
<adf-cloud-process-list>
<adf-custom-empty-content-template>
<p id="custom-id">TEST</p>
</adf-custom-empty-content-template>
</adf-cloud-process-list>
`
})
class EmptyTemplateComponent {
}
describe('ProcessListCloudComponent', () => {
let component: ProcessListCloudComponent;
let fixture: ComponentFixture<ProcessListCloudComponent>;
@@ -550,33 +558,14 @@ describe('ProcessListCloudComponent', () => {
describe('Creating an empty custom template - EmptyTemplateComponent', () => {
@Component({
template: `
<adf-cloud-process-list #processListCloud>
<adf-custom-empty-content-template>
<p id="custom-id">TEST</p>
</adf-custom-empty-content-template>
</adf-cloud-process-list>
`
})
class EmptyTemplateComponent {
@ViewChild(ProcessListCloudComponent)
processListCloud: ProcessListCloudComponent;
}
let fixtureEmpty: ComponentFixture<EmptyTemplateComponent>;
setupTestBed({
imports: [
TranslateModule.forRoot(),
HttpClientModule,
NoopAnimationsModule,
DataTableModule,
MatProgressSpinnerModule
ProcessServiceCloudTestingModule
],
providers: [{ provide: PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN, useExisting: LocalPreferenceCloudService }],
declarations: [EmptyTemplateComponent, ProcessListCloudComponent, CustomEmptyContentTemplateDirective]
declarations: [EmptyTemplateComponent]
});
beforeEach(() => {
@@ -588,16 +577,14 @@ describe('ProcessListCloudComponent', () => {
fixtureEmpty.destroy();
});
it('should render the custom template', async () => {
it('should render the custom template', fakeAsync((done) => {
const emptyList = {list: {entries: []}};
spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(emptyList));
fixtureEmpty.componentInstance.processListCloud.isLoading = false;
fixtureEmpty.detectChanges();
await fixtureEmpty.whenStable();
expect(fixtureEmpty.debugElement.query(By.css('#custom-id'))).not.toBeNull();
expect(fixtureEmpty.debugElement.query(By.css('.adf-empty-content'))).toBeNull();
});
component.success.subscribe(() => {
expect(fixtureEmpty.debugElement.query(By.css('#custom-id'))).not.toBeNull();
expect(fixtureEmpty.debugElement.query(By.css('.adf-empty-content'))).toBeNull();
done();
});
}));
});
});
@@ -61,14 +61,13 @@ describe('StartProcessCloudComponent', () => {
const firstChange = new SimpleChange(undefined, 'myApp', true);
const selectOptionByName = async (name: string) => {
const selectOptionByName = (name: string) => {
const selectElement = fixture.nativeElement.querySelector('button#adf-select-process-dropdown');
selectElement.click();
fixture.detectChanges();
await fixture.whenStable();
const options: any = fixture.debugElement.queryAll(By.css('.mat-autocomplete-panel .mat-option'));
const currentOption: DebugElement = options.find((option: DebugElement) => option.nativeElement.querySelector('.mat-option-text').innerHTML.trim() === name);
const options: any = fixture.debugElement.queryAll(By.css('.mat-option-text'));
const currentOption = options.find((option: DebugElement) => option.nativeElement.innerHTML.trim() === name);
if (currentOption) {
currentOption.nativeElement.click();
@@ -105,8 +104,7 @@ describe('StartProcessCloudComponent', () => {
fixture = TestBed.createComponent(StartProcessCloudComponent);
component = fixture.componentInstance;
getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions');
formDefinitionSpy = spyOn(formCloudService, 'getForm');
getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions').and.returnValue(of(fakeProcessDefinitions));
spyOn(processService, 'updateProcess').and.returnValue(of());
startProcessSpy = spyOn(processService, 'startProcess').and.returnValue(of(fakeProcessInstance));
getStartEventFormStaticValuesMappingSpy = spyOn(processService, 'getStartEventFormStaticValuesMapping').and.returnValue(of([]));
@@ -120,9 +118,8 @@ describe('StartProcessCloudComponent', () => {
describe('start a process without start form', () => {
beforeEach(() => {
component.name = 'My formless new process';
component.name = 'My new process';
component.appName = 'myApp';
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
fixture.detectChanges();
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
@@ -140,25 +137,28 @@ describe('StartProcessCloudComponent', () => {
fixture.detectChanges();
tick(550);
fixture.detectChanges();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.isProcessFormValid()).toBe(true);
expect(startBtn.disabled).toBe(false);
fixture.whenStable().then(() => {
fixture.detectChanges();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.isProcessFormValid()).toBe(true);
expect(startBtn.disabled).toBe(false);
});
}));
it('should create a process instance if the selection is valid', async () => {
it('should create a process instance if the selection is valid', fakeAsync(() => {
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.name = 'My new process';
component.processDefinitionName = 'process';
await selectOptionByName('processwithoutform2');
fixture.detectChanges();
await fixture.whenStable();
selectOptionByName('process');
fixture.whenStable().then(() => {
expect(component.processDefinitionCurrent.name).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[1])).name);
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.isProcessFormValid()).toBe(true);
expect(startBtn.disabled).toBe(false);
});
}));
expect(component.processDefinitionCurrent.name).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[1])).name);
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.isProcessFormValid()).toBe(true);
expect(startBtn.disabled).toBe(false);
});
it('should have start button disabled when no process is selected', async () => {
component.name = '';
@@ -206,7 +206,10 @@ describe('StartProcessCloudComponent', () => {
component.ngOnChanges({ appName: change });
fixture.detectChanges();
tick(550);
expect(component.resolvedValues).toEqual(staticInputs.concat(values));
fixture.whenStable().then(() => {
expect(component.resolvedValues).toEqual(staticInputs.concat(values));
});
}));
});
@@ -214,11 +217,6 @@ describe('StartProcessCloudComponent', () => {
beforeEach(() => {
component.name = 'My new process with form';
component.appName = 'startformwithoutupload';
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
fixture.detectChanges();
const change = new SimpleChange(null, 'startformwithoutupload', true);
component.ngOnChanges({ appName: change });
component.values = [{
id: '1',
type: 'string',
@@ -241,113 +239,162 @@ describe('StartProcessCloudComponent', () => {
this['value'] = value;
}
}];
fixture.detectChanges();
});
it('should be able to start a process with a valid form', async () => {
formDefinitionSpy.and.returnValue(of(fakeStartForm));
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform')));
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
it('should be able to start a process with a valid form', fakeAsync(() => {
component.processDefinitionName = 'processwithform';
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition(component.processDefinitionName)));
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(true);
expect(startBtn.disabled).toBe(false);
});
it('should NOT be able to start a process with a form NOT valid', async () => {
formDefinitionSpy.and.returnValue(of(fakeStartFormNotValid));
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform')));
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(false);
expect(startBtn.disabled).toBe(true);
});
it('should be able to start a process with a prefilled valid form', async () => {
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform')));
formDefinitionSpy.and.returnValue(of(fakeStartForm));
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
await fixture.whenStable();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
expect(firstNameEl.value).toEqual('FakeName');
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
expect(lastNameEl.value).toEqual('FakeLastName');
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(true);
expect(startBtn.disabled).toBe(false);
});
it('should NOT be able to start a process with a prefilled NOT valid form', async () => {
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithform')));
formDefinitionSpy.and.returnValue(of(fakeStartFormNotValid));
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
await fixture.whenStable();
expect(formDefinitionSpy).toHaveBeenCalled();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
expect(firstNameEl.value).toEqual('FakeName');
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
expect(lastNameEl.value).toEqual('FakeLastName');
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(false);
expect(startBtn.disabled).toBe(true);
});
it('should display enabled start process button if the selection is valid', async () => {
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithoutform2')));
formDefinitionSpy.and.returnValue(of(fakeStartForm));
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithoutform2');
fixture.detectChanges();
await fixture.whenStable();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(startBtn.disabled).toBe(false);
expect(component.isProcessFormValid()).toBe(true);
});
it('should have start button enabled when default values are set', async () => {
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition('processwithoutform2')));
formDefinitionSpy.and.returnValue(of(fakeStartForm));
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartForm));
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
await fixture.whenStable();
tick();
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
tick(550);
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(startBtn.disabled).toBe(false);
});
fixture.whenStable().then(() => {
fixture.detectChanges();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(true);
expect(startBtn.disabled).toBe(false);
});
}));
it('should NOT be able to start a process with a form NOT valid', fakeAsync(() => {
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition(component.processDefinitionName)));
fixture.detectChanges();
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartFormNotValid));
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
tick();
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
tick(550);
fixture.whenStable().then(() => {
fixture.detectChanges();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(false);
expect(startBtn.disabled).toBe(true);
});
}));
it('should be able to start a process with a prefilled valid form', fakeAsync(() => {
component.processDefinitionName = 'processwithform';
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition(component.processDefinitionName)));
fixture.detectChanges();
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartForm));
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
tick();
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
tick(550);
fixture.whenStable().then(() => {
fixture.detectChanges();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
expect(firstNameEl.value).toEqual('FakeName');
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
expect(lastNameEl.value).toEqual('FakeLastName');
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(true);
expect(startBtn.disabled).toBe(false);
});
}));
it('should NOT be able to start a process with a prefilled NOT valid form', fakeAsync(() => {
component.processDefinitionName = 'processwithform';
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition(component.processDefinitionName)));
fixture.detectChanges();
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartFormNotValid));
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
tick();
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
typeValueInto('#processDefinitionName', 'processwithform');
fixture.detectChanges();
tick(4500);
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(formDefinitionSpy).toHaveBeenCalled();
const firstNameEl = fixture.nativeElement.querySelector('#firstName');
expect(firstNameEl).toBeDefined();
expect(firstNameEl.value).toEqual('FakeName');
const lastNameEl = fixture.nativeElement.querySelector('#lastName');
expect(lastNameEl).toBeDefined();
expect(lastNameEl.value).toEqual('FakeLastName');
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(component.formCloud.isValid).toBe(false);
expect(startBtn.disabled).toBe(true);
});
}));
it('should display enabled start process button if the selection is valid', fakeAsync(() => {
component.name = 'testFormWithProcess';
component.processDefinitionName = 'processwithoutform2';
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition(component.processDefinitionName)));
fixture.detectChanges();
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartForm));
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
tick(550);
fixture.whenStable().then(() => {
fixture.detectChanges();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(startBtn.disabled).toBe(false);
expect(component.formCloud.isValid).toBe(true);
expect(component.isProcessFormValid()).toBe(true);
});
}));
it('should have start button enabled when default values are set', fakeAsync(() => {
component.name = 'testFormWithProcess';
component.processDefinitionName = 'processwithoutform2';
getDefinitionsSpy.and.returnValue(of(fakeSingleProcessDefinition(component.processDefinitionName)));
fixture.detectChanges();
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartForm));
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
tick(550);
fixture.whenStable().then(() => {
fixture.detectChanges();
const startBtn = fixture.nativeElement.querySelector('#button-start');
expect(startBtn.disabled).toBe(false);
});
}));
});
describe('process definitions list', () => {
@@ -355,60 +402,55 @@ describe('StartProcessCloudComponent', () => {
beforeEach(() => {
component.name = 'My new process';
component.appName = 'myApp';
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
fixture.detectChanges();
const change = new SimpleChange(null, 'MyApp', true);
component.ngOnChanges({ appName: change });
fixture.detectChanges();
});
it('should call service to fetch process definitions with appId', async () => {
await fixture.whenStable();
expect(getDefinitionsSpy).toHaveBeenCalledWith('MyApp');
it('should call service to fetch process definitions with appId', () => {
fixture.whenStable().then(() => {
expect(getDefinitionsSpy).toHaveBeenCalledWith('myApp');
});
});
it('should display the correct number of processes in the select list', async () => {
await fixture.whenStable();
const arrowButton = fixture.nativeElement.querySelector('button#adf-select-process-dropdown');
arrowButton.click();
fixture.detectChanges();
const processLists = fixture.debugElement.query(By.css('.mat-autocomplete-panel'));
expect(processLists.children.length).toBe(4);
it('should display the correct number of processes in the select list', () => {
fixture.whenStable().then(() => {
const selectElement = fixture.nativeElement.querySelector('mat-select');
expect(selectElement.children.length).toBe(1);
});
});
it('should display the option def details', async () => {
it('should display the option def details', () => {
component.processDefinitionList = fakeProcessDefinitions;
fixture.detectChanges();
await fixture.whenStable();
const selectElement = fixture.nativeElement.querySelector('button#adf-select-process-dropdown');
selectElement.click();
fixture.detectChanges();
const optionElement = fixture.debugElement.queryAll(By.css('.mat-autocomplete-panel .mat-option'));
expect(selectElement).not.toBeNull();
expect(selectElement).toBeDefined();
expect(optionElement).not.toBeNull();
expect(optionElement).toBeDefined();
fixture.whenStable().then(() => {
const selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger');
const optionElement = fixture.nativeElement.querySelectorAll('mat-option');
selectElement.click();
expect(selectElement).not.toBeNull();
expect(selectElement).toBeDefined();
expect(optionElement).not.toBeNull();
expect(optionElement).toBeDefined();
});
});
it('should display the key when the processDefinition name is empty or null', async () => {
it('should display the key when the processDefinition name is empty or null', () => {
component.processDefinitionList = fakeNoNameProcessDefinitions;
fixture.detectChanges();
await fixture.whenStable();
const selectElement = fixture.nativeElement.querySelector('button#adf-select-process-dropdown');
selectElement.click();
fixture.detectChanges();
const optionElement = fixture.debugElement.queryAll(By.css('.mat-autocomplete-panel .mat-option'));
expect(selectElement).not.toBeNull();
expect(selectElement).toBeDefined();
expect(optionElement).not.toBeNull();
expect(optionElement[0].nativeElement.textContent.trim()).toBe('NewProcess 1');
fixture.whenStable().then(() => {
const selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger');
const optionElement = fixture.nativeElement.querySelectorAll('mat-option');
selectElement.click();
expect(selectElement).not.toBeNull();
expect(selectElement).toBeDefined();
expect(optionElement).not.toBeNull();
expect(optionElement[0].textContent).toBe('NewProcess 1');
});
});
it('should indicate an error to the user if process defs cannot be loaded', async () => {
getDefinitionsSpy.and.returnValue(throwError({}));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(throwError({}));
const change = new SimpleChange('myApp', 'myApp1', true);
component.ngOnChanges({ appName: change });
@@ -420,7 +462,7 @@ describe('StartProcessCloudComponent', () => {
});
it('should show no process available message when no process definition is loaded', async () => {
getDefinitionsSpy.and.returnValue(of([]));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of([]));
const change = new SimpleChange('myApp', 'myApp1', true);
component.ngOnChanges({ appName: change });
@@ -433,7 +475,7 @@ describe('StartProcessCloudComponent', () => {
});
it('should select automatically the processDefinition if the app contain only one', async () => {
getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]]));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]]));
const change = new SimpleChange('myApp', 'myApp1', true);
component.ngOnChanges({ appName: change });
@@ -443,30 +485,29 @@ describe('StartProcessCloudComponent', () => {
expect(component.processForm.controls['processDefinition'].value).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[0])).name);
});
it('should select automatically the form when processDefinition is selected as default', async () => {
getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[2]]));
formDefinitionSpy.and.returnValue(of(fakeStartForm));
it('should select automatically the form when processDefinition is selected as default', fakeAsync(() => {
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]]));
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartForm));
const change = new SimpleChange('myApp', 'myApp1', true);
component.ngOnInit();
component.ngOnChanges({ appName: change });
component.processForm.controls['processDefinition'].setValue('process');
fixture.detectChanges();
await fixture.whenStable();
await selectOptionByName('processwithform');
tick(3000);
component.processDefinitionName = fakeProcessDefinitions[0].name;
component.setProcessDefinitionOnForm(fakeProcessDefinitions[0].name);
fixture.detectChanges();
await fixture.whenStable();
tick(3000);
component.processDefinitionName = fakeProcessDefinitions[2].name;
component.setProcessDefinitionOnForm(fakeProcessDefinitions[2].name);
fixture.detectChanges();
await fixture.whenStable();
const processForm = fixture.nativeElement.querySelector('adf-cloud-form');
expect(component.hasForm()).toBeTruthy();
expect(processForm).not.toBeNull();
});
fixture.whenStable().then(() => {
const processForm = fixture.nativeElement.querySelector('adf-cloud-form');
expect(component.hasForm()).toBeTruthy();
expect(processForm).not.toBeNull();
});
}));
it('should not select automatically any processDefinition if the app contain multiple process and does not have any processDefinition as input', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.appName = 'myApp';
component.ngOnChanges({});
@@ -476,27 +517,23 @@ describe('StartProcessCloudComponent', () => {
expect(component.processPayloadCloud.name).toBeNull();
});
it('should select the right process when the processKey begins with the name', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
formDefinitionSpy.and.returnValue(of(fakeStartForm));
it('should select the right process when the processKey begins with the name', fakeAsync(() => {
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.name = 'My new process';
component.processDefinitionName = 'process';
component.appName = 'myApp';
component.ngOnChanges({});
selectOptionByName('process');
fixture.detectChanges();
await fixture.whenStable();
expect(component.processDefinitionCurrent.name).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[3])).name);
expect(component.processDefinitionCurrent.key).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[3])).key);
});
fixture.whenStable().then(() => {
expect(component.processDefinitionCurrent.name).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[3])).name);
expect(component.processDefinitionCurrent.key).toBe(JSON.parse(JSON.stringify(fakeProcessDefinitions[3])).key);
});
}));
describe('dropdown', () => {
it('should hide the process dropdown button if showSelectProcessDropdown is false', async () => {
fixture.detectChanges();
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.appName = 'myApp';
component.showSelectProcessDropdown = false;
component.ngOnChanges({});
@@ -510,7 +547,7 @@ describe('StartProcessCloudComponent', () => {
it('should show the process dropdown button if showSelectProcessDropdown is false', async () => {
fixture.detectChanges();
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.appName = 'myApp';
component.processDefinitionName = 'NewProcess 2';
component.showSelectProcessDropdown = true;
@@ -525,7 +562,7 @@ describe('StartProcessCloudComponent', () => {
it('should show the process dropdown button by default', async () => {
fixture.detectChanges();
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.appName = 'myApp';
component.processDefinitionName = 'NewProcess 2';
component.ngOnChanges({});
@@ -544,27 +581,25 @@ describe('StartProcessCloudComponent', () => {
const change = new SimpleChange('myApp', 'myApp1', false);
beforeEach(() => {
component.name = 'My new process';
component.appName = 'myApp';
fixture.detectChanges();
});
it('should have floating labels for process name and type', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.ngOnChanges({ appName: change });
component.appName = 'myApp';
component.processDefinitionName = 'NewProcess 2';
component.ngOnChanges({});
fixture.detectChanges();
await fixture.whenStable();
component.processForm.controls.processInstanceName.setValue('My sharona');
component.processForm.controls.processDefinition.setValue('process');
fixture.detectChanges();
const inputLabelsNodes = document.querySelectorAll('.mat-form-field-label');
expect(inputLabelsNodes.length).toBe(2);
const inputLabelsNodes = document.querySelectorAll('.adf-start-process .adf-process-input-container');
inputLabelsNodes.forEach(labelNode => {
expect(labelNode.getAttribute('ng-reflect-float-label')).toBe('always');
});
});
it('should reload processes when appName input changed', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.ngOnChanges({ appName: firstChange });
component.ngOnChanges({ appName: change });
@@ -575,7 +610,6 @@ describe('StartProcessCloudComponent', () => {
});
it('should reload processes ONLY when appName input changed', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.ngOnChanges({ appName: firstChange });
fixture.detectChanges();
@@ -587,30 +621,28 @@ describe('StartProcessCloudComponent', () => {
});
it('should get current processDef', () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.ngOnChanges({ appName: change });
fixture.detectChanges();
expect(getDefinitionsSpy).toHaveBeenCalled();
expect(component.processDefinitionList).toBe(fakeProcessDefinitions);
});
it('should display the matching results in the dropdown as the user types down', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
it('should display the matching results in the dropdown as the user types down', fakeAsync(() => {
component.processDefinitionList = fakeProcessDefinitions;
component.ngOnInit();
component.ngOnChanges({ appName: change });
fixture.detectChanges();
typeValueInto('#processDefinitionName', 'process');
component.processForm.controls['processDefinition'].setValue('process');
fixture.detectChanges();
await fixture.whenStable();
tick(3000);
expect(component.filteredProcesses.length).toEqual(4);
typeValueInto('#processDefinitionName', 'processwithfo');
component.processForm.controls['processDefinition'].setValue('processwithfo');
fixture.detectChanges();
await fixture.whenStable();
tick(3000);
expect(component.filteredProcesses.length).toEqual(1);
});
}));
it('should display the process definion field as empty if are more than one process definition in the list', async () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
@@ -630,8 +662,7 @@ describe('StartProcessCloudComponent', () => {
fixture.detectChanges();
component.name = 'NewProcess 1';
component.appName = 'myApp';
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
fixture.detectChanges();
component.ngOnChanges({});
});
it('should see start button', async () => {
@@ -715,22 +746,19 @@ describe('StartProcessCloudComponent', () => {
expect(startProcessSpy).toHaveBeenCalledWith(component.appName, payload);
});
it('should call service with the correct parameters when variables are undefined and formCloud is defined', async () => {
getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[2]]));
formDefinitionSpy.and.returnValue(of(fakeStartForm));
it('should call service with the correct parameters when variables are undefined and formCloud is defined', fakeAsync(() => {
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]]));
formDefinitionSpy = spyOn(formCloudService, 'getForm').and.returnValue(of(fakeStartForm));
const change = new SimpleChange('myApp', 'myApp1', true);
component.ngOnInit();
component.ngOnChanges({ appName: change });
component.processForm.controls['processDefinition'].setValue('process');
fixture.detectChanges();
await fixture.whenStable();
await selectOptionByName('processwithform');
tick(3000);
component.processDefinitionName = fakeProcessDefinitions[0].name;
component.setProcessDefinitionOnForm(fakeProcessDefinitions[0].name);
fixture.detectChanges();
await fixture.whenStable();
component.processDefinitionName = fakeProcessDefinitions[2].name;
component.setProcessDefinitionOnForm(fakeProcessDefinitions[2].name);
fixture.detectChanges();
await fixture.whenStable();
tick(3000);
const processForm = fixture.nativeElement.querySelector('adf-cloud-form');
expect(component.hasForm()).toBeTruthy();
@@ -738,7 +766,7 @@ describe('StartProcessCloudComponent', () => {
const payload: ProcessPayloadCloud = new ProcessPayloadCloud({
name: component.processInstanceName.value,
processDefinitionKey: fakeProcessDefinitions[2].key,
processDefinitionKey: fakeProcessDefinitions[0].key,
variables: Object.assign({}, component.formCloud.values)
});
const startButton = fixture.debugElement.query(By.css('#button-start'));
@@ -746,7 +774,7 @@ describe('StartProcessCloudComponent', () => {
startButton.triggerEventHandler('click', null);
expect(startProcessSpy).toHaveBeenCalledWith(component.appName, payload);
});
}));
it('should output start event when process started successfully', () => {
const emitSpy = spyOn(component.success, 'emit');
@@ -830,18 +858,16 @@ describe('StartProcessCloudComponent', () => {
expect(startBtn.disabled).toBe(true);
});
it('should emit processDefinitionSelection event when a process definition is selected', async () => {
const emitSpy = spyOn(component.processDefinitionSelection, 'emit');
it('should emit processDefinitionSelection event when a process definition is selected', (done) => {
component.appName = 'myApp';
formDefinitionSpy.and.returnValue(of(fakeStartForm));
component.ngOnChanges({ appName: firstChange });
component.processDefinitionSelection.subscribe((processDefinition) => {
expect(processDefinition).toEqual(fakeProcessDefinitions[0]);
done();
});
fixture.detectChanges();
await fixture.whenStable();
component.processDefinitionName = 'processwithoutform1';
selectOptionByName(fakeProcessDefinitions[0].name);
fixture.detectChanges();
await fixture.whenStable();
expect(emitSpy).toHaveBeenCalledOnceWith(fakeProcessDefinitions[0]);
});
it('should wait for process definition to be loaded before showing the empty process definition message', () => {
@@ -857,18 +883,17 @@ describe('StartProcessCloudComponent', () => {
expect(noProcessElement).not.toBeNull();
});
it('should set the process name using the processName cloud pipe when a process definition gets selected', async () => {
it('should set the process name using the processName cloud pipe when a process definition gets selected', () => {
const processNameCloudPipe = TestBed.inject(ProcessNameCloudPipe);
const processNamePipeTransformSpy = spyOn(processNameCloudPipe, 'transform').and.returnValue('fake-transformed-name');
const expectedProcessInstanceDetails: ProcessInstanceCloud = { processDefinitionName: fakeProcessDefinitions[0].name };
getDefinitionsSpy.and.returnValue(of([fakeProcessDefinitions[0]]));
formDefinitionSpy.and.stub();
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.appName = 'myApp';
component.ngOnChanges({ appName: firstChange });
fixture.detectChanges();
await fixture.whenStable();
selectOptionByName(fakeProcessDefinitions[0].name);
expect(processNamePipeTransformSpy).toHaveBeenCalledWith(component.name, expectedProcessInstanceDetails);
expect(component.processInstanceName.dirty).toBe(true);
@@ -977,7 +1002,6 @@ describe('StartProcessCloudComponent', () => {
beforeEach(() => {
component.name = 'NewProcess 1';
component.appName = 'myApp';
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.ngOnChanges({ appName: firstChange });
component.processDefinitionList = fakeProcessDefinitions;
component.processDefinitionName = fakeProcessDefinitions[0].name;
@@ -1016,7 +1040,6 @@ describe('StartProcessCloudComponent', () => {
beforeEach(() => {
component.name = 'NewProcess 1';
component.appName = 'myApp';
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
component.ngOnChanges({ appName: firstChange });
fixture.detectChanges();
});
@@ -80,7 +80,7 @@ describe('PreferenceService', () => {
});
});
it('Should not fetch preferences if error occurred', (done) => {
it('Should not fetch preferences if error occurred', () => {
getInstanceSpy.and.returnValue(apiErrorMock);
service.getPreferences('mock-app-name')
.subscribe(
@@ -89,7 +89,6 @@ describe('PreferenceService', () => {
expect(error.state).toEqual(404);
expect(error.stateText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
@@ -108,7 +107,7 @@ describe('PreferenceService', () => {
});
});
it('Should not fetch preference by key if error occurred', (done) => {
it('Should not fetch preference by key if error occurred', () => {
getInstanceSpy.and.returnValue(apiErrorMock);
service.getPreferenceByKey('mock-app-name', 'mock-preference-key')
.subscribe(
@@ -117,7 +116,6 @@ describe('PreferenceService', () => {
expect(error.state).toEqual(404);
expect(error.stateText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
@@ -134,7 +132,7 @@ describe('PreferenceService', () => {
});
});
it('Should not create preference if error occurred', (done) => {
it('Should not create preference if error occurred', () => {
getInstanceSpy.and.returnValue(apiErrorMock);
service.createPreference('mock-app-name', 'mock-preference-key', createMockPreference)
.subscribe(
@@ -143,7 +141,6 @@ describe('PreferenceService', () => {
expect(error.state).toEqual(404);
expect(error.stateText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
@@ -160,7 +157,7 @@ describe('PreferenceService', () => {
});
});
it('Should not update preference if error occurred', (done) => {
it('Should not update preference if error occurred', () => {
getInstanceSpy.and.returnValue(apiErrorMock);
service.createPreference('mock-app-name', 'mock-preference-key', updateMockPreference)
.subscribe(
@@ -169,7 +166,6 @@ describe('PreferenceService', () => {
expect(error.state).toEqual(404);
expect(error.stateText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
@@ -182,7 +178,7 @@ describe('PreferenceService', () => {
});
});
it('Should not delete preference if error occurred', (done) => {
it('Should not delete preference if error occurred', () => {
getInstanceSpy.and.returnValue(apiErrorMock);
service.deletePreference('mock-app-name', 'mock-preference-key')
.subscribe(
@@ -191,7 +187,6 @@ describe('PreferenceService', () => {
expect(error.state).toEqual(404);
expect(error.stateText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
@@ -15,12 +15,12 @@
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { setupTestBed } from '@alfresco/adf-core';
import { MatDialog } from '@angular/material/dialog';
import { of, Subject } from 'rxjs';
import { of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
@@ -35,7 +35,6 @@ import { TranslateModule } from '@ngx-translate/core';
import { EditServiceTaskFilterCloudComponent } from './edit-service-task-filter-cloud.component';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { ProcessDefinitionCloud } from '../../../../models/process-definition-cloud.model';
import { TaskFilterDialogCloudComponent } from '../task-filter-dialog/task-filter-dialog-cloud.component';
describe('EditServiceTaskFilterCloudComponent', () => {
let component: EditServiceTaskFilterCloudComponent;
@@ -46,7 +45,6 @@ describe('EditServiceTaskFilterCloudComponent', () => {
let getTaskFilterSpy: jasmine.Spy;
let getRunningApplicationsSpy: jasmine.Spy;
let taskService: TaskCloudService;
const afterClosedSubject = new Subject<any>();
setupTestBed({
imports: [
@@ -68,10 +66,13 @@ describe('EditServiceTaskFilterCloudComponent', () => {
appsService = TestBed.inject(AppsProcessCloudService);
taskService = TestBed.inject(TaskCloudService);
dialog = TestBed.inject(MatDialog);
const dialogRefMock: any = {
afterClosed: () => afterClosedSubject
};
spyOn(dialog, 'open').and.returnValue(dialogRefMock);
spyOn(dialog, 'open').and.returnValue({
afterClosed: of({
action: EditServiceTaskFilterCloudComponent.ACTION_SAVE,
icon: 'icon',
name: 'fake-name'
})
} as any);
getTaskFilterSpy = spyOn(service, 'getTaskFilterById').and.returnValue(of(fakeServiceFilter));
getRunningApplicationsSpy = spyOn(appsService, 'getDeployedApplicationsByStatus').and.returnValue(of(fakeApplicationInstance));
fixture.detectChanges();
@@ -79,17 +80,18 @@ describe('EditServiceTaskFilterCloudComponent', () => {
afterEach(() => fixture.destroy());
it('should fetch task filter by taskId', async () => {
it('should fetch task filter by taskId', () => {
const taskFilterIdChange = new SimpleChange(undefined, 'mock-task-filter-id', true);
component.ngOnChanges({ id: taskFilterIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(getTaskFilterSpy).toHaveBeenCalled();
expect(component.taskFilter.name).toEqual('FakeInvolvedTasks');
expect(component.taskFilter.icon).toEqual('adjust');
expect(component.taskFilter.status).toEqual('COMPLETED');
expect(component.taskFilter.order).toEqual('ASC');
expect(component.taskFilter.sort).toEqual('id');
fixture.whenStable().then(() => {
expect(getTaskFilterSpy).toHaveBeenCalled();
expect(component.taskFilter.name).toEqual('FakeInvolvedTasks');
expect(component.taskFilter.icon).toEqual('adjust');
expect(component.taskFilter.status).toEqual('CREATED');
expect(component.taskFilter.order).toEqual('ASC');
expect(component.taskFilter.sort).toEqual('id');
});
});
it('should fetch process definitions when processDefinitionName filter property is set', async () => {
@@ -634,7 +636,7 @@ describe('EditServiceTaskFilterCloudComponent', () => {
spyOn(component.action, 'emit').and.callThrough();
});
it('should emit save event and save the filter on click save button', async () => {
it('should emit save event and save the filter on click save button', fakeAsync(() => {
component.toggleFilterActions = true;
spyOn(service, 'updateFilter').and.returnValue(of(null));
fixture.detectChanges();
@@ -648,15 +650,16 @@ describe('EditServiceTaskFilterCloudComponent', () => {
sortOptions[3].nativeElement.click();
fixture.detectChanges();
const saveButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-save"]');
await fixture.whenStable();
fixture.detectChanges();
expect(saveButton.disabled).toBe(false);
saveButton.click();
expect(service.updateFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(saveButton.disabled).toBe(false);
saveButton.click();
expect(service.updateFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
});
}));
it('should emit delete event and delete the filter on click of delete button', async () => {
it('should emit delete event and delete the filter on click of delete button', fakeAsync(() => {
component.toggleFilterActions = true;
spyOn(service, 'deleteFilter').and.returnValue(of(null));
fixture.detectChanges();
@@ -667,48 +670,40 @@ describe('EditServiceTaskFilterCloudComponent', () => {
stateElement.click();
fixture.detectChanges();
const deleteButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-delete"]');
await fixture.whenStable();
fixture.detectChanges();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
});
}));
it('should emit saveAs event and add filter on click saveAs button', async () => {
it('should emit saveAs event and add filter on click saveAs button', fakeAsync(() => {
component.toggleFilterActions = true;
spyOn(service, 'addFilter').and.returnValue(of(null));
fixture.detectChanges();
const expansionPanel = fixture.debugElement.nativeElement.querySelector('mat-expansion-panel-header');
expansionPanel.click();
fixture.detectChanges();
const stateElement = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-cloud-edit-task-property-sort"] .mat-select-trigger');
stateElement.click();
const sortElement = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-cloud-edit-task-property-sort"] .mat-select-trigger');
sortElement.click();
fixture.detectChanges();
const sortOptions = fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-edit-task-property-options-sort"] .mat-option-ripple'));
sortOptions[3].nativeElement.click();
const sortOptions = fixture.debugElement.queryAll(By.css('.mat-option-text'));
sortOptions[2].nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
const saveButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-saveAs"]');
saveButton.dispatchEvent(new Event('click'));
fixture.detectChanges();
afterClosedSubject.next({
action: TaskFilterDialogCloudComponent.ACTION_SAVE,
icon: 'icon',
name: 'fake-name'
const saveAsButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-saveAs"]');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(saveAsButton.disabled).toBe(false);
saveAsButton.click();
expect(service.addFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(dialog.open).toHaveBeenCalled();
});
await fixture.whenStable();
}));
expect(service.addFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(dialog.open).toHaveBeenCalled();
});
it('should call restore default filters service on deletion of last filter', async () => {
it('should call restore default filters service on deletion of last filter', fakeAsync(() => {
component.toggleFilterActions = true;
spyOn(service, 'deleteFilter').and.returnValue(of([]));
const restoreDefaultFiltersSpy = spyOn(component, 'restoreDefaultTaskFilters').and.returnValue(of([]));
@@ -720,16 +715,17 @@ describe('EditServiceTaskFilterCloudComponent', () => {
stateElement.click();
fixture.detectChanges();
const deleteButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-delete"]');
await fixture.whenStable();
fixture.detectChanges();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(restoreDefaultFiltersSpy).toHaveBeenCalled();
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(restoreDefaultFiltersSpy).toHaveBeenCalled();
});
}));
it('should not call restore default filters service on deletion of first filter', async () => {
it('should not call restore default filters service on deletion of first filter', fakeAsync(() => {
component.toggleFilterActions = true;
spyOn(service, 'deleteFilter').and.returnValue(of([{ name: 'mock-filter-name' }]));
const restoreDefaultFiltersSpy = spyOn(component, 'restoreDefaultTaskFilters').and.returnValue(of([]));
@@ -741,13 +737,14 @@ describe('EditServiceTaskFilterCloudComponent', () => {
stateElement.click();
fixture.detectChanges();
const deleteButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-delete"]');
await fixture.whenStable();
fixture.detectChanges();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(restoreDefaultFiltersSpy).not.toHaveBeenCalled();
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(restoreDefaultFiltersSpy).not.toHaveBeenCalled();
});
}));
});
});
@@ -15,12 +15,12 @@
* limitations under the License.
*/
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AlfrescoApiService, setupTestBed } from '@alfresco/adf-core';
import { MatDialog } from '@angular/material/dialog';
import { of, Subject } from 'rxjs';
import { of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../../services/local-preference-cloud.service';
@@ -54,8 +54,6 @@ import {
} from '../../mock/edit-task-filter-cloud.mock';
import { mockFoodUsers } from '../../../../people/mock/people-cloud.mock';
import { mockFoodGroups } from '../../../../group/mock/group-cloud.mock';
import { SimpleChanges } from '@angular/core';
import { TaskFilterDialogCloudComponent } from '../task-filter-dialog/task-filter-dialog-cloud.component';
describe('EditTaskFilterCloudComponent', () => {
let component: EditTaskFilterCloudComponent;
@@ -67,7 +65,6 @@ describe('EditTaskFilterCloudComponent', () => {
let getTaskFilterSpy: jasmine.Spy;
let getRunningApplicationsSpy: jasmine.Spy;
let taskService: TaskCloudService;
const afterClosedSubject = new Subject<any>();
setupTestBed({
imports: [
@@ -91,10 +88,13 @@ describe('EditTaskFilterCloudComponent', () => {
taskService = TestBed.inject(TaskCloudService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
dialog = TestBed.inject(MatDialog);
const dialogRefMock: any = {
afterClosed: () => afterClosedSubject
};
spyOn(dialog, 'open').and.returnValue(dialogRefMock);
spyOn(dialog, 'open').and.returnValue({
afterClosed: of({
action: EditTaskFilterCloudComponent.ACTION_SAVE,
icon: 'icon',
name: 'fake-name'
})
} as any);
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mockAlfrescoApi);
getTaskFilterSpy = spyOn(service, 'getTaskFilterById').and.returnValue(of(fakeFilter));
getRunningApplicationsSpy = spyOn(appsService, 'getDeployedApplicationsByStatus').and.returnValue(of(fakeApplicationInstance));
@@ -103,17 +103,17 @@ describe('EditTaskFilterCloudComponent', () => {
afterEach(() => fixture.destroy());
it('should fetch task filter by taskId', async () => {
it('should fetch task filter by taskId', () => {
component.ngOnChanges({ id: mockTaskFilterIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(getTaskFilterSpy).toHaveBeenCalled();
expect(component.taskFilter.name).toEqual('FakeInvolvedTasks');
expect(component.taskFilter.icon).toEqual('adjust');
expect(component.taskFilter.status).toEqual(TaskStatusFilter.CREATED);
expect(component.taskFilter.order).toEqual('ASC');
expect(component.taskFilter.sort).toEqual('id');
fixture.whenStable().then(() => {
expect(getTaskFilterSpy).toHaveBeenCalled();
expect(component.taskFilter.name).toEqual('FakeInvolvedTasks');
expect(component.taskFilter.icon).toEqual('adjust');
expect(component.taskFilter.status).toEqual(TaskStatusFilter.CREATED);
expect(component.taskFilter.order).toEqual('ASC');
expect(component.taskFilter.sort).toEqual('id');
});
});
describe('processInstanceId filter', () => {
@@ -192,6 +192,7 @@ describe('EditTaskFilterCloudComponent', () => {
expect(getProcessInstanceIdInputElement().value).toEqual('fakeProcessInstanceIdFromResponse');
});
});
it('should fetch process definitions when processDefinitionName filter property is set', async () => {
@@ -955,39 +956,36 @@ describe('EditTaskFilterCloudComponent', () => {
describe('edit filter actions', () => {
beforeEach(() => {
component.changedTaskFilter = { name: 'mock-filter-name' } as TaskFilterCloudModel;
component.ngOnChanges({ id: mockTaskFilterIdChange } as SimpleChanges);
spyOn(component.action, 'emit').and.stub();
component.toggleFilterActions = true;
component.ngOnChanges({ id: mockTaskFilterIdChange });
fixture.detectChanges();
spyOn(component.action, 'emit').and.callThrough();
});
it('should emit save event and save the filter on click save button', fakeAsync(() => {
spyOn(service, 'updateFilter').and.returnValue(of([new TaskFilterCloudModel({ name: 'mock-filter-name' })]));
component.toggleFilterActions = true;
spyOn(service, 'updateFilter').and.returnValue(of(null));
fixture.detectChanges();
const expansionPanel = fixture.debugElement.nativeElement.querySelector('mat-expansion-panel-header');
expansionPanel.click();
fixture.detectChanges();
tick();
const stateElement = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-cloud-edit-task-property-sort"] .mat-select-trigger');
stateElement.click();
fixture.detectChanges();
tick();
const sortOptions = fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-edit-task-property-options-sort"] .mat-option-ripple'));
const sortOptions = fixture.debugElement.queryAll(By.css('.mat-option-text'));
sortOptions[3].nativeElement.click();
fixture.detectChanges();
tick(550);
const saveButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-save"]');
saveButton.dispatchEvent(new Event('click'));
fixture.detectChanges();
tick();
expect(service.updateFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
fixture.whenStable().then(() => {
const saveButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-save"]');
expect(saveButton.disabled).toBe(false);
saveButton.click();
expect(service.updateFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
});
}));
it('should emit delete event and delete the filter on click of delete button', async () => {
component.toggleFilterActions = true;
spyOn(service, 'deleteFilter').and.returnValue(of(null));
fixture.detectChanges();
const expansionPanel = fixture.debugElement.nativeElement.querySelector('mat-expansion-panel-header');
@@ -1000,45 +998,38 @@ describe('EditTaskFilterCloudComponent', () => {
await fixture.whenStable();
const deleteButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-delete"]');
expect(deleteButton.getAttribute('disabled')).toBeNull();
expect(deleteButton.disabled).toBe(false);
deleteButton.click();
expect(service.deleteFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
});
it('should emit saveAs event and add filter on click saveAs button', async () => {
it('should emit saveAs event and add filter on click saveAs button', fakeAsync(() => {
component.toggleFilterActions = true;
spyOn(service, 'addFilter').and.returnValue(of(null));
fixture.detectChanges();
const expansionPanel = fixture.debugElement.nativeElement.querySelector('mat-expansion-panel-header');
expansionPanel.click();
fixture.detectChanges();
const stateElement = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-cloud-edit-task-property-sort"] .mat-select-trigger');
stateElement.click();
const sortElement = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-cloud-edit-task-property-sort"] .mat-select-trigger');
sortElement.click();
fixture.detectChanges();
const sortOptions = fixture.debugElement.queryAll(By.css('.mat-option-text'));
sortOptions[2].nativeElement.click();
const sortOptions = fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-edit-task-property-options-sort"] .mat-option-ripple'));
sortOptions[3].nativeElement.click();
fixture.detectChanges();
await fixture.whenStable();
const saveButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-saveAs"]');
saveButton.dispatchEvent(new Event('click'));
fixture.detectChanges();
afterClosedSubject.next({
action: TaskFilterDialogCloudComponent.ACTION_SAVE,
icon: 'icon',
name: 'fake-name'
fixture.whenStable().then(() => {
const saveAsButton = fixture.debugElement.nativeElement.querySelector('[data-automation-id="adf-filter-action-saveAs"]');
expect(saveAsButton.disabled).toBe(false);
saveAsButton.click();
expect(service.addFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(dialog.open).toHaveBeenCalled();
});
await fixture.whenStable();
expect(service.addFilter).toHaveBeenCalled();
expect(component.action.emit).toHaveBeenCalled();
expect(dialog.open).toHaveBeenCalled();
});
}));
it('should call restore default filters service on deletion of last filter', async () => {
component.toggleFilterActions = true;
spyOn(service, 'deleteFilter').and.returnValue(of([]));
const restoreDefaultFiltersSpy = spyOn(component, 'restoreDefaultTaskFilters').and.returnValue(of([]));
fixture.detectChanges();
@@ -1060,6 +1051,7 @@ describe('EditTaskFilterCloudComponent', () => {
});
it('should not call restore default filters service on deletion of first filter', async () => {
component.toggleFilterActions = true;
spyOn(service, 'deleteFilter').and.returnValue(of([new TaskFilterCloudModel({ name: 'mock-filter-name' })]));
const restoreDefaultFiltersSpy = spyOn(component, 'restoreDefaultTaskFilters').and.returnValue(of([]));
fixture.detectChanges();
@@ -98,7 +98,7 @@ export class EditTaskFilterCloudComponent extends BaseEditTaskFilterCloudCompone
}
}
protected updateFilter(filterToUpdate: TaskFilterCloudModel): Observable<TaskFilterCloudModel[]> {
protected updateFilter(filterToUpdate: TaskFilterCloudModel): Observable<any> {
return this.taskFilterCloudService.updateFilter(filterToUpdate);
}
@@ -85,10 +85,6 @@ describe('TaskFormCloudComponent', () => {
component = fixture.componentInstance;
});
afterEach(() => {
fixture.destroy();
});
describe('Complete button', () => {
beforeEach(() => {
@@ -289,62 +285,57 @@ describe('TaskFormCloudComponent', () => {
beforeEach(() => {
component.appName = 'app1';
component.taskId = 'task1';
fixture.detectChanges();
});
it('should emit cancelClick when cancel button is clicked', async () => {
spyOn(component.cancelClick,'emit').and.stub();
it('should emit cancelClick when cancel button is clicked', (done) => {
component.cancelClick.subscribe(() => {
done();
});
fixture.detectChanges();
const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task'));
cancelBtn.triggerEventHandler('click', {});
fixture.detectChanges();
await fixture.whenStable();
expect(component.cancelClick.emit).toHaveBeenCalledOnceWith('task1');
cancelBtn.nativeElement.click();
});
it('should emit taskCompleted when task is completed', async () => {
it('should emit taskCompleted when task is completed', (done) => {
spyOn(taskCloudService, 'completeTask').and.returnValue(of({}));
spyOn(component.taskCompleted, 'emit').and.stub();
component.taskCompleted.subscribe(() => {
done();
});
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
completeBtn.triggerEventHandler('click', {});
fixture.detectChanges();
await fixture.whenStable();
expect(component.taskCompleted.emit).toHaveBeenCalledOnceWith('task1');
completeBtn.nativeElement.click();
});
it('should emit taskClaimed when task is claimed', async () => {
it('should emit taskClaimed when task is claimed', (done) => {
spyOn(taskCloudService, 'claimTask').and.returnValue(of({}));
spyOn(component, 'hasCandidateUsers').and.returnValue(true);
spyOn(component.taskClaimed, 'emit').and.stub();
taskDetails.status = TASK_CREATED_STATE;
taskDetails.permissions = [TASK_CLAIM_PERMISSION];
getTaskSpy.and.returnValue(of(taskDetails));
component.taskClaimed.subscribe(() => {
done();
});
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]'));
claimBtn.triggerEventHandler('click', {});
fixture.detectChanges();
await fixture.whenStable();
expect(component.taskClaimed.emit).toHaveBeenCalledOnceWith('task1');
claimBtn.nativeElement.click();
});
it('should emit error when error occurs', async () => {
spyOn(component.error, 'emit').and.stub();
it('should emit error when error occurs', (done) => {
component.error.subscribe(() => {
done();
});
component.onError({});
fixture.detectChanges();
await fixture.whenStable();
expect(component.error.emit).toHaveBeenCalled();
});
it('should reload when task is completed', () => {
@@ -16,20 +16,9 @@
*/
import { Component, SimpleChange, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppConfigService,
setupTestBed,
DataRowEvent,
ObjectDataRow,
User,
DataColumn,
ColumnsSelectorComponent,
AlfrescoApiService,
AlfrescoApiServiceMock,
AppConfigServiceMock,
TranslationService,
TranslationMock } from '@alfresco/adf-core';
import { AppConfigService, setupTestBed, DataRowEvent, ObjectDataRow, User, DataColumn, ColumnsSelectorComponent } from '@alfresco/adf-core';
import { TaskListCloudService } from '../services/task-list-cloud.service';
import { TaskListCloudComponent } from './task-list-cloud.component';
import { fakeGlobalTasks, fakeCustomSchema, fakeGlobalTask } from '../mock/fake-task-response.mock';
@@ -40,9 +29,6 @@ import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.mod
import { shareReplay, skip } from 'rxjs/operators';
import { TaskListCloudServiceInterface } from '../../../services/task-list-cloud.service.interface';
import { TASK_LIST_CLOUD_TOKEN } from '../../../services/cloud-token.service';
import { TaskListCloudModule } from '../task-list-cloud.module';
import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
template: `
@@ -102,6 +88,9 @@ describe('TaskListCloudComponent', () => {
TranslateModule.forRoot(),
ProcessServiceCloudTestingModule
],
declarations: [
EmptyTemplateComponent
],
providers: [
{
provide: TASK_LIST_CLOUD_TOKEN,
@@ -534,20 +523,6 @@ describe('TaskListCloudComponent', () => {
describe('Creating an empty custom template - EmptyTemplateComponent', () => {
let fixtureEmpty: ComponentFixture<EmptyTemplateComponent>;
setupTestBed({
imports: [
HttpClientModule,
NoopAnimationsModule,
TranslateModule.forRoot(),
TaskListCloudModule
],
providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
{ provide: AppConfigService, useClass: AppConfigServiceMock },
{ provide: TranslationService, useClass: TranslationMock }
]
});
beforeEach(() => {
const emptyList = { list: { entries: [] } };
spyOn(taskListCloudService, 'getTaskByRequest').and.returnValue(of(emptyList));
@@ -560,12 +535,16 @@ describe('TaskListCloudComponent', () => {
fixtureEmpty.destroy();
});
it('should render the custom template', async () => {
// TODO still not working because of the Loading Spinner
// eslint-disable-next-line
xit('should render the custom template', (done) => {
fixtureEmpty.detectChanges();
await fixtureEmpty.whenStable();
fixtureEmpty.detectChanges();
expect(fixtureEmpty.debugElement.query(By.css('#custom-id'))).not.toBeNull();
expect(fixtureEmpty.debugElement.query(By.css('.adf-empty-content'))).toBeNull();
fixtureEmpty.whenStable().then(() => {
fixtureEmpty.detectChanges();
expect(fixtureEmpty.debugElement.query(By.css('#custom-id'))).not.toBeNull();
expect(fixtureEmpty.debugElement.query(By.css('.adf-empty-content'))).toBeNull();
done();
});
});
});
@@ -602,7 +581,7 @@ describe('TaskListCloudComponent', () => {
sortable: true
},
{
key: 'priority',
key: 'entry.priority',
type: 'text',
title: 'ADF_TASK_LIST.PROPERTIES.PRIORITY',
sortable: true
@@ -623,34 +602,45 @@ describe('TaskListCloudComponent', () => {
fixture.destroy();
});
it('should show tooltip if config copyContent flag is true', fakeAsync(() => {
// TODO: highly unstable test
// eslint-disable-next-line
xit('should show tooltip if config copyContent flag is true', fakeAsync(() => {
taskSpy.and.returnValue(of(fakeGlobalTasks));
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
component.success.subscribe(() => {
fixture.whenStable().then(() => {
fixture.detectChanges();
const spanHTMLElement = element.querySelector<HTMLInputElement>('span[title="11fe013d-c263-11e8-b75b-0a5864600540"]');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-copy-tooltip')).not.toBeNull();
});
});
component.presetColumn = 'fakeCustomSchema';
component.appName = appName.currentValue;
component.ngOnChanges({ appName });
component.ngAfterContentInit();
tick();
fixture.detectChanges();
const spanHTMLElement = element.querySelector<HTMLInputElement>('span[title="11fe013d-c263-11e8-b75b-0a5864600540"]');
spanHTMLElement.dispatchEvent(new Event('mouseenter'));
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-copy-tooltip')).not.toBeNull();
}));
it('should replace priority values', fakeAsync(() => {
// TODO: highly unstable test
// eslint-disable-next-line
xit('should replace priority values', (done) => {
taskSpy.and.returnValue(of(fakeGlobalTasks));
component.presetColumn = 'fakeCustomSchema';
const appName = new SimpleChange(null, 'FAKE-APP-NAME', true);
component.ngOnChanges({ appName });
component.success.subscribe(() => {
const cell = fixture.nativeElement.querySelector('[data-automation-id="text_ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NONE"]');
expect(cell.textContent).toEqual('ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NONE');
done();
});
fixture.detectChanges();
component.reload();
tick();
const cell = fixture.nativeElement.querySelector('[data-automation-id="text_ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NONE"]');
expect(cell.textContent).toEqual('ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NONE');
}));
});
it('replacePriorityValues should return undefined when no rows defined', () => {
const emptyList = { list: { entries: [] } };
@@ -132,43 +132,39 @@ describe('Activiti ServiceTaskList Cloud Service', () => {
});
it('should throw an exeption and execute logService error if appName is null', (done) => {
it('should throw an exeption and execute logService error if appName is null', () => {
const expectedErrorMessage = 'Appname/executionId/flowNodeId not configured';
const params = [null, 'executionId_1', 'flowNodeId_1'] as const;
service.replayServiceTaskRequest(...params).toPromise().catch((error) => {
expect(error).toEqual(expectedErrorMessage);
expect(logServiceErrorSpy).toHaveBeenCalled();
done();
});
});
it('should throw an exeption and execute logService error if executionId is null', (done) => {
it('should throw an exeption and execute logService error if executionId is null', () => {
const expectedErrorMessage = 'Appname/executionId/flowNodeId not configured';
const params = ['fakeName', null, 'flowNodeId_1'] as const;
service.replayServiceTaskRequest(...params).toPromise().catch((error) => {
expect(error).toEqual(expectedErrorMessage);
expect(logServiceErrorSpy).toHaveBeenCalled();
done();
});
});
it('should throw an exeption and execute logService error if flowNodeId is null', (done) => {
it('should throw an exeption and execute logService error if flowNodeId is null', () => {
const expectedErrorMessage = 'Appname/executionId/flowNodeId not configured';
const params = ['fakeName', 'executionId_1', null] as const;
service.replayServiceTaskRequest(...params).toPromise().catch((error) => {
expect(error).toEqual(expectedErrorMessage);
expect(logServiceErrorSpy).toHaveBeenCalled();
done();
});
});
it('should throw an exeption and execute logService error if appName, executionId and flowNodeId are null', (done) => {
it('should throw an exeption and execute logService error if appName, executionId and flowNodeId are null', () => {
const expectedErrorMessage = 'Appname/executionId/flowNodeId not configured';
const params = [null, null, null] as const;
service.replayServiceTaskRequest(...params).toPromise().catch((error) => {
expect(error).toEqual(expectedErrorMessage);
expect(logServiceErrorSpy).toHaveBeenCalled();
done();
});
});
});