mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-11 20:13:07 +00:00
finished tests for item metadata updates
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
import {NgModule} from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {SharedModule} from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import {EditItemPageRoutingModule} from './edit-item-page.routing.module';
|
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
||||||
import {EditItemPageComponent} from './edit-item-page.component';
|
import { EditItemPageComponent } from './edit-item-page.component';
|
||||||
import {ItemStatusComponent} from './item-status/item-status.component';
|
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||||
import {ItemOperationComponent} from './item-operation/item-operation.component';
|
import { ItemOperationComponent } from './item-operation/item-operation.component';
|
||||||
import {ModifyItemOverviewComponent} from './modify-item-overview/modify-item-overview.component';
|
import { ModifyItemOverviewComponent } from './modify-item-overview/modify-item-overview.component';
|
||||||
import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
|
import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component';
|
||||||
import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
|
import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component';
|
||||||
import {AbstractSimpleItemActionComponent} from './simple-item-action/abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component';
|
||||||
import {ItemPrivateComponent} from './item-private/item-private.component';
|
import { ItemPrivateComponent } from './item-private/item-private.component';
|
||||||
import {ItemPublicComponent} from './item-public/item-public.component';
|
import { ItemPublicComponent } from './item-public/item-public.component';
|
||||||
import {ItemDeleteComponent} from './item-delete/item-delete.component';
|
import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||||
|
|
||||||
|
@@ -105,6 +105,36 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('changeType is UPDATE', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.fieldUpdate.changeType = FieldChangeType.UPDATE;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should have class table-warning', () => {
|
||||||
|
expect(el.classList).toContain('table-warning');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changeType is ADD', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.fieldUpdate.changeType = FieldChangeType.ADD;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should have class table-success', () => {
|
||||||
|
expect(el.classList).toContain('table-success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changeType is REMOVE', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.fieldUpdate.changeType = FieldChangeType.REMOVE;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should have class table-danger', () => {
|
||||||
|
expect(el.classList).toContain('table-danger');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('setEditable', () => {
|
describe('setEditable', () => {
|
||||||
const editable = false;
|
const editable = false;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -116,6 +146,30 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('editable is true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.editable = observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should contain input fields or textareas', () => {
|
||||||
|
const inputField = de.queryAll(By.css('input'));
|
||||||
|
const textAreas = de.queryAll(By.css('textarea'));
|
||||||
|
expect(inputField.length + textAreas.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('editable is false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.editable = observableOf(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should contain no input fields or textareas', () => {
|
||||||
|
const inputField = de.queryAll(By.css('input'));
|
||||||
|
const textAreas = de.queryAll(By.css('textarea'));
|
||||||
|
expect(inputField.length + textAreas.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.remove();
|
comp.remove();
|
||||||
@@ -225,10 +279,98 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when canSetEditable emits true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(true));
|
||||||
|
});
|
||||||
|
it('the div should contain a edit icon', () => {
|
||||||
|
const editIcon = de.query(By.css('i.fa-edit'));
|
||||||
|
expect(editIcon).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canSetEditable emits false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canSetEditable').and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should not contain a edit icon', () => {
|
||||||
|
const editIcon = de.query(By.css('i.fa-edit'));
|
||||||
|
expect(editIcon).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canSetUneditable emits true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should contain a check icon', () => {
|
||||||
|
const checkIcon = de.query(By.css('i.fa-check'));
|
||||||
|
expect(checkIcon).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canSetUneditable emits false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canSetUneditable').and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should not contain a check icon', () => {
|
||||||
|
const checkIcon = de.query(By.css('i.fa-check'));
|
||||||
|
expect(checkIcon).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canRemove emits true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canRemove').and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should contain a trash icon', () => {
|
||||||
|
const trashIcon = de.query(By.css('i.fa-trash-alt'));
|
||||||
|
expect(trashIcon).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canRemove emits false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canRemove').and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should not contain a trash icon', () => {
|
||||||
|
const trashIcon = de.query(By.css('i.fa-trash-alt'));
|
||||||
|
expect(trashIcon).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canUndo emits true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canUndo').and.returnValue(observableOf(true));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should contain a undo icon', () => {
|
||||||
|
const undoIcon = de.query(By.css('i.fa-undo-alt'));
|
||||||
|
expect(undoIcon).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when canUndo emits false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(comp, 'canUndo').and.returnValue(observableOf(false));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should not contain a undo icon', () => {
|
||||||
|
const undoIcon = de.query(By.css('i.fa-undo-alt'));
|
||||||
|
expect(undoIcon).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('canRemove', () => {
|
describe('canRemove', () => {
|
||||||
describe('when editable is currently true', () => {
|
describe('when editable is currently true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.editable = observableOf(true);
|
comp.editable = observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('canRemove should return an observable emitting false', () => {
|
it('canRemove should return an observable emitting false', () => {
|
||||||
const expected = '(a|)';
|
const expected = '(a|)';
|
||||||
|
@@ -3,14 +3,14 @@ import { isNotEmpty } from '../../../../shared/empty.util';
|
|||||||
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
import { Metadatum } from '../../../../core/shared/metadatum.model';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { map, take } from 'rxjs/operators';
|
import { map, take } from 'rxjs/operators';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
import { MetadataField } from '../../../../core/metadata/metadatafield.model';
|
||||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-in-place-field',
|
selector: 'ds-edit-in-place-field',
|
||||||
|
@@ -9,7 +9,7 @@ import { SharedModule } from '../../../shared/shared.module';
|
|||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {
|
import {
|
||||||
@@ -81,7 +81,8 @@ describe('ItemMetadataComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
item = Object.assign(new Item(), { metadata: [metadatum1, metadatum2, metadatum3] }, { lastModified: date });
|
item = Object.assign(new Item(), { metadata: [metadatum1, metadatum2, metadatum3] }, { lastModified: date });
|
||||||
itemService = jasmine.createSpyObj('itemService', {
|
itemService = jasmine.createSpyObj('itemService', {
|
||||||
update: observableOf(new RemoteData(false, false, true, undefined, item))
|
update: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
|
commitUpdates: {}
|
||||||
});
|
});
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
|
@@ -11,7 +11,7 @@ import {
|
|||||||
Identifiable
|
Identifiable
|
||||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Metadatum } from '../../../core/shared/metadatum.model';
|
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||||
import { first, switchMap } from 'rxjs/operators';
|
import { first, switchMap, tap } from 'rxjs/operators';
|
||||||
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
@@ -121,6 +121,7 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
|
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata });
|
||||||
return this.itemService.update(updatedItem);
|
return this.itemService.update(updatedItem);
|
||||||
}),
|
}),
|
||||||
|
tap(() => this.itemService.commitUpdates()),
|
||||||
getSucceededRemoteData()
|
getSucceededRemoteData()
|
||||||
).subscribe(
|
).subscribe(
|
||||||
(rd: RemoteData<Item>) => {
|
(rd: RemoteData<Item>) => {
|
||||||
|
@@ -41,6 +41,7 @@ import { CacheableObject } from '../cache/object-cache.reducer';
|
|||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { ChangeAnalyzer } from './change-analyzer';
|
import { ChangeAnalyzer } from './change-analyzer';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
|
||||||
export abstract class DataService<T extends CacheableObject> {
|
export abstract class DataService<T extends CacheableObject> {
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
@@ -228,4 +229,12 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
* @param method The RestRequestMethod for which de server sync buffer should be committed
|
||||||
|
*/
|
||||||
|
commitUpdates(method?: RestRequestMethod) {
|
||||||
|
this.requestService.commit(method);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,9 @@ import { type } from '../../../shared/ngrx/type';
|
|||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { Identifiable } from './object-updates.reducer';
|
import { Identifiable } from './object-updates.reducer';
|
||||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||||
|
/**
|
||||||
|
* The list of ObjectUpdatesAction type definitions
|
||||||
|
*/
|
||||||
|
|
||||||
export const ObjectUpdatesActionTypes = {
|
export const ObjectUpdatesActionTypes = {
|
||||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||||
@@ -14,12 +17,19 @@ export const ObjectUpdatesActionTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum that represents the different types of updates that can be performed on a field in the ObjectUpdates store
|
||||||
|
*/
|
||||||
export enum FieldChangeType {
|
export enum FieldChangeType {
|
||||||
UPDATE = 0,
|
UPDATE = 0,
|
||||||
ADD = 1,
|
ADD = 1,
|
||||||
REMOVE = 2
|
REMOVE = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to initialize a new page's fields in the ObjectUpdates state
|
||||||
|
*/
|
||||||
export class InitializeFieldsAction implements Action {
|
export class InitializeFieldsAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.INITIALIZE_FIELDS;
|
type = ObjectUpdatesActionTypes.INITIALIZE_FIELDS;
|
||||||
payload: {
|
payload: {
|
||||||
@@ -28,6 +38,14 @@ export class InitializeFieldsAction implements Action {
|
|||||||
lastModified: Date
|
lastModified: Date
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new InitializeFieldsAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which the fields are being initialized
|
||||||
|
* @param fields The identifiable fields of which the updates are kept track of
|
||||||
|
* @param lastModified The last modified date of the object that belongs to the page
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
fields: Identifiable[],
|
fields: Identifiable[],
|
||||||
@@ -37,6 +55,9 @@ export class InitializeFieldsAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to add a new field update in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
export class AddFieldUpdateAction implements Action {
|
export class AddFieldUpdateAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.ADD_FIELD;
|
type = ObjectUpdatesActionTypes.ADD_FIELD;
|
||||||
payload: {
|
payload: {
|
||||||
@@ -45,6 +66,14 @@ export class AddFieldUpdateAction implements Action {
|
|||||||
changeType: FieldChangeType,
|
changeType: FieldChangeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddFieldUpdateAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which a field update is added
|
||||||
|
* @param field The identifiable field of which a new update is added
|
||||||
|
* @param changeType The update's change type
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
field: Identifiable,
|
field: Identifiable,
|
||||||
@@ -53,6 +82,9 @@ export class AddFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to set the editable state of an existing field in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
export class SetEditableFieldUpdateAction implements Action {
|
export class SetEditableFieldUpdateAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.SET_EDITABLE_FIELD;
|
type = ObjectUpdatesActionTypes.SET_EDITABLE_FIELD;
|
||||||
payload: {
|
payload: {
|
||||||
@@ -61,6 +93,14 @@ export class SetEditableFieldUpdateAction implements Action {
|
|||||||
editable: boolean,
|
editable: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SetEditableFieldUpdateAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page
|
||||||
|
* @param fieldUUID The UUID of the field of which
|
||||||
|
* @param editable The new editable value for the field
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
fieldUUID: string,
|
fieldUUID: string,
|
||||||
@@ -69,13 +109,23 @@ export class SetEditableFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to discard all existing updates in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
export class DiscardObjectUpdatesAction implements Action {
|
export class DiscardObjectUpdatesAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.DISCARD;
|
type = ObjectUpdatesActionTypes.DISCARD;
|
||||||
payload: {
|
payload: {
|
||||||
url: string,
|
url: string,
|
||||||
notification
|
notification: INotification
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DiscardObjectUpdatesAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which the changes should be discarded
|
||||||
|
* @param notification The notification that is raised when changes are discarded
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
notification: INotification
|
notification: INotification
|
||||||
@@ -84,12 +134,21 @@ export class DiscardObjectUpdatesAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to reinstate all previously discarded updates in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
export class ReinstateObjectUpdatesAction implements Action {
|
export class ReinstateObjectUpdatesAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.REINSTATE;
|
type = ObjectUpdatesActionTypes.REINSTATE;
|
||||||
payload: {
|
payload: {
|
||||||
url: string
|
url: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ReinstateObjectUpdatesAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which the changes should be reinstated
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
@@ -97,12 +156,21 @@ export class ReinstateObjectUpdatesAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to remove all previously discarded updates in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
export class RemoveObjectUpdatesAction implements Action {
|
export class RemoveObjectUpdatesAction implements Action {
|
||||||
type = ObjectUpdatesActionTypes.REMOVE;
|
type = ObjectUpdatesActionTypes.REMOVE;
|
||||||
payload: {
|
payload: {
|
||||||
url: string
|
url: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new RemoveObjectUpdatesAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which the changes should be removed
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string
|
url: string
|
||||||
) {
|
) {
|
||||||
@@ -117,6 +185,13 @@ export class RemoveFieldUpdateAction implements Action {
|
|||||||
uuid: string
|
uuid: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new RemoveObjectUpdatesAction
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* the unique url of the page for which a field's change should be removed
|
||||||
|
* @param uuid The UUID of the field for which the change should be removed
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
uuid: string
|
uuid: string
|
||||||
|
@@ -1,55 +1,122 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { provideMockActions } from '@ngrx/effects/testing';
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
import { cold, hot } from 'jasmine-marbles';
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { ObjectUpdatesEffects } from './object-updates.effects';
|
import { ObjectUpdatesEffects } from './object-updates.effects';
|
||||||
import { RemoveObjectUpdatesAction } from './object-updates.actions';
|
import {
|
||||||
|
DiscardObjectUpdatesAction,
|
||||||
|
ObjectUpdatesAction,
|
||||||
|
ReinstateObjectUpdatesAction,
|
||||||
|
RemoveFieldUpdateAction,
|
||||||
|
RemoveObjectUpdatesAction
|
||||||
|
} from './object-updates.actions';
|
||||||
|
import {
|
||||||
|
INotification,
|
||||||
|
Notification
|
||||||
|
} from '../../../shared/notifications/models/notification.model';
|
||||||
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
fdescribe('ObjectUpdatesEffects', () => {
|
describe('ObjectUpdatesEffects', () => {
|
||||||
let updatesEffects: ObjectUpdatesEffects;
|
let updatesEffects: ObjectUpdatesEffects;
|
||||||
let actions: Observable<any>;
|
let actions: Observable<any>;
|
||||||
const testURL = 'www.dspace.org/dspace7';
|
let testURL = 'www.dspace.org/dspace7';
|
||||||
beforeEach(() => {
|
let testUUID = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
|
||||||
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
ObjectUpdatesEffects,
|
ObjectUpdatesEffects,
|
||||||
provideMockActions(() => actions),
|
provideMockActions(() => actions),
|
||||||
{
|
{
|
||||||
provide: NotificationsService, useClass: {
|
provide: NotificationsService,
|
||||||
|
useValue: {
|
||||||
remove: (notification) => { /* empty */
|
remove: (notification) => { /* empty */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// other providers
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testURL = 'www.dspace.org/dspace7';
|
||||||
|
testUUID = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
|
||||||
updatesEffects = TestBed.get(ObjectUpdatesEffects);
|
updatesEffects = TestBed.get(ObjectUpdatesEffects);
|
||||||
|
(updatesEffects as any).actionMap[testURL] = new Subject<ObjectUpdatesAction>();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapLastActions$', () => {
|
describe('mapLastActions$', () => {
|
||||||
describe('When any ObjectUpdatesAction is triggered', () => {
|
describe('When any ObjectUpdatesAction is triggered', () => {
|
||||||
const action = new RemoveObjectUpdatesAction(testURL);
|
let action;
|
||||||
|
let emittedAction;
|
||||||
|
beforeEach(() => {
|
||||||
|
action = new RemoveObjectUpdatesAction(testURL);
|
||||||
|
});
|
||||||
it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => {
|
it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => {
|
||||||
|
action = new RemoveObjectUpdatesAction(testURL);
|
||||||
actions = hot('--a-', { a: action });
|
actions = hot('--a-', { a: action });
|
||||||
|
(updatesEffects as any).actionMap[testURL].subscribe((act) => emittedAction = act);
|
||||||
|
const expected = cold('--b-', { b: undefined });
|
||||||
|
|
||||||
const expected = cold('--b-', { b: action });
|
expect(updatesEffects.mapLastActions$).toBeObservable(expected);
|
||||||
|
expect(emittedAction).toBe(action);
|
||||||
expect((updatesEffects as any).actionMap[testURL]).toBeObservable(expected);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// describe('removeAfterDiscardOrReinstateOnUndo$', () => {
|
describe('removeAfterDiscardOrReinstateOnUndo$', () => {
|
||||||
//
|
describe('When an ObjectUpdatesActionTypes.DISCARD action is triggered', () => {
|
||||||
// it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
|
let infoNotification: INotification;
|
||||||
// actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } });
|
let removeAction;
|
||||||
//
|
describe('When there is no user interactions before the timeout is finished', () => {
|
||||||
// const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) });
|
beforeEach(() => {
|
||||||
//
|
infoNotification = new Notification('id', NotificationType.Info, 'info');
|
||||||
// expect(updatesEffects.routeChange$).toBeObservable(expected);
|
infoNotification.options.timeOut = 0;
|
||||||
// });
|
removeAction = new RemoveObjectUpdatesAction(testURL)
|
||||||
//
|
});
|
||||||
// });
|
it('should return a RemoveObjectUpdatesAction', () => {
|
||||||
|
actions = hot('a|', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||||
|
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe(
|
||||||
|
filter(((action) => hasValue(action))))
|
||||||
|
.subscribe((t) => {
|
||||||
|
expect(t).toEqual(removeAction);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When there a REINSTATE action is fired before the timeout is finished', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
infoNotification = new Notification('id', NotificationType.Info, 'info');
|
||||||
|
infoNotification.options.timeOut = 10;
|
||||||
|
});
|
||||||
|
it('should return an action with type NO_ACTION', () => {
|
||||||
|
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||||
|
actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) });
|
||||||
|
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => {
|
||||||
|
expect(t).toEqual({ type: 'NO_ACTION' });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When there any ObjectUpdates action - other than REINSTATE - is fired before the timeout is finished', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
infoNotification = new Notification('id', NotificationType.Info, 'info');
|
||||||
|
infoNotification.options.timeOut = 10;
|
||||||
|
});
|
||||||
|
it('should return a RemoveObjectUpdatesAction', () => {
|
||||||
|
actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) });
|
||||||
|
actions = hot('b', { b: new RemoveFieldUpdateAction(testURL, testUUID) });
|
||||||
|
|
||||||
|
updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) =>
|
||||||
|
expect(t).toEqual(new RemoveObjectUpdatesAction(testURL))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,15 +3,23 @@ import { Actions, Effect, ofType } from '@ngrx/effects';
|
|||||||
import {
|
import {
|
||||||
DiscardObjectUpdatesAction,
|
DiscardObjectUpdatesAction,
|
||||||
ObjectUpdatesAction,
|
ObjectUpdatesAction,
|
||||||
ObjectUpdatesActionTypes
|
ObjectUpdatesActionTypes,
|
||||||
|
RemoveObjectUpdatesAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import { map } from 'rxjs/operators';
|
import { delay, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { Subject } from 'rxjs';
|
import { of as observableOf, race as observableRace, Subject } from 'rxjs';
|
||||||
import { hasNoValue } from '../../../shared/empty.util';
|
import { hasNoValue } from '../../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NGRX effects for ObjectUpdatesActions
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectUpdatesEffects {
|
export class ObjectUpdatesEffects {
|
||||||
|
/**
|
||||||
|
* Map that keeps track of the latest ObjectUpdatesAction for each page's url
|
||||||
|
*/
|
||||||
private actionMap: {
|
private actionMap: {
|
||||||
/* Use Subject instead of BehaviorSubject:
|
/* Use Subject instead of BehaviorSubject:
|
||||||
we only want Actions that are fired while we're listening
|
we only want Actions that are fired while we're listening
|
||||||
@@ -20,6 +28,9 @@ export class ObjectUpdatesEffects {
|
|||||||
[url: string]: Subject<ObjectUpdatesAction>
|
[url: string]: Subject<ObjectUpdatesAction>
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effect that makes sure all last fired ObjectUpdatesActions are stored in the map of this service, with the url as their key
|
||||||
|
*/
|
||||||
@Effect({ dispatch: false }) mapLastActions$ = this.actions$
|
@Effect({ dispatch: false }) mapLastActions$ = this.actions$
|
||||||
.pipe(
|
.pipe(
|
||||||
ofType(...Object.values(ObjectUpdatesActionTypes)),
|
ofType(...Object.values(ObjectUpdatesActionTypes)),
|
||||||
@@ -33,31 +44,44 @@ export class ObjectUpdatesEffects {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// @Effect() removeAfterDiscardOrReinstateOnUndo$ = this.actions$
|
/**
|
||||||
// .pipe(
|
* Effect that checks whether the removeAction's notification timeout ends before a user triggers another ObjectUpdatesAction
|
||||||
// ofType(ObjectUpdatesActionTypes.DISCARD),
|
* When no ObjectUpdatesAction is fired during the timeout, a RemoteObjectUpdatesAction will be returned
|
||||||
// switchMap((action: DiscardObjectUpdatesAction) => {
|
* When a REINSTATE action is fired during the timeout, a NO_ACTION action will be returned
|
||||||
// const url: string = action.payload.url;
|
* When any other ObjectUpdatesAction is fired during the timeout, a RemoteObjectUpdatesAction will be returned
|
||||||
// const notification: INotification = action.payload.notification;
|
*/
|
||||||
// const timeOut = notification.options.timeOut;
|
@Effect() removeAfterDiscardOrReinstateOnUndo$ = this.actions$
|
||||||
// return observableRace(
|
.pipe(
|
||||||
// // Either wait for the delay and perform a remove action
|
ofType(ObjectUpdatesActionTypes.DISCARD),
|
||||||
// observableOf(new RemoveObjectUpdatesAction(action.payload.url)).pipe(delay(timeOut)),
|
switchMap((action: DiscardObjectUpdatesAction) => {
|
||||||
// // Or wait for a reinstate action and perform no action
|
const url: string = action.payload.url;
|
||||||
// this.actionMap[url].pipe(
|
const notification: INotification = action.payload.notification;
|
||||||
// // filter((updateAction: ObjectUpdatesAction) => updateAction.type === ObjectUpdatesActionTypes.REINSTATE),
|
const timeOut = notification.options.timeOut;
|
||||||
// tap(() => this.notificationsService.remove(notification)),
|
return observableRace(
|
||||||
// map(() => {
|
// Either wait for the delay and perform a remove action
|
||||||
// return { type: 'NO_ACTION' }
|
observableOf(new RemoveObjectUpdatesAction(action.payload.url)).pipe(delay(timeOut)),
|
||||||
// }
|
// Or wait for a a user action
|
||||||
// )
|
this.actionMap[url].pipe(
|
||||||
// )
|
take(1),
|
||||||
// )
|
tap(() => this.notificationsService.remove(notification)),
|
||||||
// }
|
map((updateAction: ObjectUpdatesAction) => {
|
||||||
// )
|
if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) {
|
||||||
// );
|
// If someone reinstated, do nothing, just let the reinstating happen
|
||||||
|
return { type: 'NO_ACTION' }
|
||||||
|
} else {
|
||||||
|
// If someone performed another action, assume the user does not want to reinstate and remove all changes
|
||||||
|
return new RemoveObjectUpdatesAction(action.payload.url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private notificationsService: NotificationsService) {
|
||||||
|
|
||||||
constructor(private actions$: Actions, private notificationsService: NotificationsService) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
256
src/app/core/data/object-updates/object-updates.reducer.spec.ts
Normal file
256
src/app/core/data/object-updates/object-updates.reducer.spec.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
import {
|
||||||
|
AddFieldUpdateAction,
|
||||||
|
DiscardObjectUpdatesAction,
|
||||||
|
FieldChangeType,
|
||||||
|
InitializeFieldsAction,
|
||||||
|
ReinstateObjectUpdatesAction,
|
||||||
|
RemoveFieldUpdateAction, RemoveObjectUpdatesAction,
|
||||||
|
SetEditableFieldUpdateAction
|
||||||
|
} from './object-updates.actions';
|
||||||
|
import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer';
|
||||||
|
|
||||||
|
class NullAction extends RemoveFieldUpdateAction {
|
||||||
|
type = null;
|
||||||
|
payload = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifiable1 = {
|
||||||
|
uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320',
|
||||||
|
key: 'dc.contributor.author',
|
||||||
|
language: null,
|
||||||
|
value: 'Smith, John'
|
||||||
|
};
|
||||||
|
|
||||||
|
const identifiable1update = {
|
||||||
|
uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320',
|
||||||
|
key: 'dc.contributor.author',
|
||||||
|
language: null,
|
||||||
|
value: 'Smith, James'
|
||||||
|
};
|
||||||
|
const identifiable2 = {
|
||||||
|
uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241',
|
||||||
|
key: 'dc.title',
|
||||||
|
language: null,
|
||||||
|
value: 'New title'
|
||||||
|
};
|
||||||
|
const identifiable3 = {
|
||||||
|
uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e',
|
||||||
|
key: 'dc.description.abstract',
|
||||||
|
language: null,
|
||||||
|
value: 'Unchanged value'
|
||||||
|
};
|
||||||
|
|
||||||
|
const modDate = new Date(2010, 2, 11);
|
||||||
|
const uuid = identifiable1.uuid;
|
||||||
|
const url = 'test-object.url/edit';
|
||||||
|
describe('objectUpdatesReducer', () => {
|
||||||
|
const testState = {
|
||||||
|
[url]: {
|
||||||
|
fieldStates: {
|
||||||
|
[identifiable1.uuid]: {
|
||||||
|
editable: true,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
[identifiable2.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: true
|
||||||
|
},
|
||||||
|
[identifiable3.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fieldUpdates: {
|
||||||
|
[identifiable2.uuid]: {
|
||||||
|
field: {
|
||||||
|
uuid: identifiable2.uuid,
|
||||||
|
key: 'dc.title',
|
||||||
|
language: null,
|
||||||
|
value: 'New title'
|
||||||
|
},
|
||||||
|
changeType: FieldChangeType.ADD
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lastModified: modDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const discardedTestState = {
|
||||||
|
[url]: {
|
||||||
|
fieldStates: {
|
||||||
|
[identifiable1.uuid]: {
|
||||||
|
editable: true,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
[identifiable2.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: true
|
||||||
|
},
|
||||||
|
[identifiable3.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lastModified: modDate
|
||||||
|
},
|
||||||
|
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
||||||
|
fieldStates: {
|
||||||
|
[identifiable1.uuid]: {
|
||||||
|
editable: true,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
[identifiable2.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: true
|
||||||
|
},
|
||||||
|
[identifiable3.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fieldUpdates: {
|
||||||
|
[identifiable2.uuid]: {
|
||||||
|
field: {
|
||||||
|
uuid: identifiable2.uuid,
|
||||||
|
key: 'dc.title',
|
||||||
|
language: null,
|
||||||
|
value: 'New title'
|
||||||
|
},
|
||||||
|
changeType: FieldChangeType.ADD
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lastModified: modDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deepFreeze(testState);
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(testState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty object', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const initialState = objectUpdatesReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(initialState).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the INITIALIZE_FIELDS action without affecting the previous state', () => {
|
||||||
|
const action = new InitializeFieldsAction(url, [identifiable1, identifiable2], modDate);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the SET_EDITABLE_FIELD action without affecting the previous state', () => {
|
||||||
|
const action = new SetEditableFieldUpdateAction(url, uuid, false);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the ADD_FIELD action without affecting the previous state', () => {
|
||||||
|
const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the DISCARD action without affecting the previous state', () => {
|
||||||
|
const action = new DiscardObjectUpdatesAction(url, null);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the REINSTATE action without affecting the previous state', () => {
|
||||||
|
const action = new ReinstateObjectUpdatesAction(url);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the REMOVE action without affecting the previous state', () => {
|
||||||
|
const action = new RemoveFieldUpdateAction(url, uuid);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the REMOVE_FIELD action without affecting the previous state', () => {
|
||||||
|
const action = new RemoveFieldUpdateAction(url, uuid);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectUpdatesReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
||||||
|
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate);
|
||||||
|
|
||||||
|
const expectedState = {
|
||||||
|
[url]: {
|
||||||
|
fieldStates: {
|
||||||
|
[identifiable1.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
[identifiable3.uuid]: {
|
||||||
|
editable: false,
|
||||||
|
isNew: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fieldUpdates: {},
|
||||||
|
lastModified: modDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
|
expect(newState).toEqual(expectedState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the given field\'s fieldStates when the SET_EDITABLE_FIELD action is dispatched, based on the payload', () => {
|
||||||
|
const action = new SetEditableFieldUpdateAction(url, identifiable3.uuid, true);
|
||||||
|
|
||||||
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
|
expect(newState[url].fieldStates[identifiable3.uuid].editable).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a given field\'s update to the state when the ADD_FIELD action is dispatched, based on the payload', () => {
|
||||||
|
const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE);
|
||||||
|
|
||||||
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
|
expect(newState[url].fieldUpdates[identifiable1.uuid].field).toEqual(identifiable1update);
|
||||||
|
expect(newState[url].fieldUpdates[identifiable1.uuid].changeType).toEqual(FieldChangeType.UPDATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should discard a given url\'s updates from the state when the DISCARD action is dispatched, based on the payload', () => {
|
||||||
|
const action = new DiscardObjectUpdatesAction(url, null);
|
||||||
|
|
||||||
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
|
expect(newState[url].fieldUpdates).toEqual({});
|
||||||
|
expect(newState[url + OBJECT_UPDATES_TRASH_PATH]).toEqual(testState[url]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reinstate a given url\'s updates from the state when the REINSTATE action is dispatched, based on the payload', () => {
|
||||||
|
const action = new ReinstateObjectUpdatesAction(url);
|
||||||
|
|
||||||
|
const newState = objectUpdatesReducer(discardedTestState, action);
|
||||||
|
expect(newState).toEqual(testState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a given url\'s updates from the state when the REMOVE action is dispatched, based on the payload', () => {
|
||||||
|
const action = new RemoveObjectUpdatesAction(url);
|
||||||
|
|
||||||
|
const newState = objectUpdatesReducer(discardedTestState, action);
|
||||||
|
expect(newState[url].fieldUpdates).toBeUndefined();
|
||||||
|
expect(newState[url + OBJECT_UPDATES_TRASH_PATH]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a given field\'s update from the state when the REMOVE_FIELD action is dispatched, based on the payload', () => {
|
||||||
|
const action = new RemoveFieldUpdateAction(url, uuid);
|
||||||
|
|
||||||
|
const newState = objectUpdatesReducer(testState, action);
|
||||||
|
expect(newState[url].fieldUpdates[uuid]).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@@ -52,45 +52,45 @@ const initialNewFieldState = { editable: true, isNew: true };
|
|||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
const initialState = Object.create(null);
|
const initialState = Object.create(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer method to calculate the next ObjectUpdates state, based on the current state and the ObjectUpdatesAction
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
export function objectUpdatesReducer(state = initialState, action: ObjectUpdatesAction): ObjectUpdatesState {
|
export function objectUpdatesReducer(state = initialState, action: ObjectUpdatesAction): ObjectUpdatesState {
|
||||||
let newState = state;
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||||
newState = initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||||
newState = addFieldUpdate(state, action as AddFieldUpdateAction);
|
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.DISCARD: {
|
case ObjectUpdatesActionTypes.DISCARD: {
|
||||||
newState = discardObjectUpdates(state, action as DiscardObjectUpdatesAction);
|
return discardObjectUpdates(state, action as DiscardObjectUpdatesAction);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.REINSTATE: {
|
case ObjectUpdatesActionTypes.REINSTATE: {
|
||||||
newState = reinstateObjectUpdates(state, action as ReinstateObjectUpdatesAction);
|
return reinstateObjectUpdates(state, action as ReinstateObjectUpdatesAction);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.REMOVE: {
|
case ObjectUpdatesActionTypes.REMOVE: {
|
||||||
newState = removeObjectUpdates(state, action as RemoveObjectUpdatesAction);
|
return removeObjectUpdates(state, action as RemoveObjectUpdatesAction);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.REMOVE_FIELD: {
|
case ObjectUpdatesActionTypes.REMOVE_FIELD: {
|
||||||
newState = removeFieldUpdate(state, action as RemoveFieldUpdateAction);
|
return removeFieldUpdate(state, action as RemoveFieldUpdateAction);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
|
case ObjectUpdatesActionTypes.SET_EDITABLE_FIELD: {
|
||||||
// return directly, no need to change the lastModified date
|
|
||||||
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
|
return setEditableFieldUpdate(state, action as SetEditableFieldUpdateAction);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// return setUpdated(newState, action.payload.url);
|
|
||||||
return newState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the state for a specific url and store all its fields in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const fields: Identifiable[] = action.payload.fields;
|
const fields: Identifiable[] = action.payload.fields;
|
||||||
@@ -101,11 +101,16 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
|||||||
state[url],
|
state[url],
|
||||||
{ fieldStates: fieldStates },
|
{ fieldStates: fieldStates },
|
||||||
{ fieldUpdates: {} },
|
{ fieldUpdates: {} },
|
||||||
{ lastServerUpdate: lastModifiedServer }
|
{ lastModified: lastModifiedServer }
|
||||||
);
|
);
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new update for a specific field to the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const field: Identifiable = action.payload.field;
|
const field: Identifiable = action.payload.field;
|
||||||
@@ -130,6 +135,11 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
|||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discard all updates for a specific action's url in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
|
function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const pageState: ObjectUpdatesEntry = state[url];
|
const pageState: ObjectUpdatesEntry = state[url];
|
||||||
@@ -149,6 +159,11 @@ function discardObjectUpdates(state: any, action: DiscardObjectUpdatesAction) {
|
|||||||
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reinstate all updates for a specific action's url in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function reinstateObjectUpdates(state: any, action: ReinstateObjectUpdatesAction) {
|
function reinstateObjectUpdates(state: any, action: ReinstateObjectUpdatesAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const trashState = state[url + OBJECT_UPDATES_TRASH_PATH];
|
const trashState = state[url + OBJECT_UPDATES_TRASH_PATH];
|
||||||
@@ -158,17 +173,32 @@ function reinstateObjectUpdates(state: any, action: ReinstateObjectUpdatesAction
|
|||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all updates for a specific action's url in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function removeObjectUpdates(state: any, action: RemoveObjectUpdatesAction) {
|
function removeObjectUpdates(state: any, action: RemoveObjectUpdatesAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
return removeObjectUpdatesByURL(state, url);
|
return removeObjectUpdatesByURL(state, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all updates for a specific url in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function removeObjectUpdatesByURL(state: any, url: string) {
|
function removeObjectUpdatesByURL(state: any, url: string) {
|
||||||
const newState = Object.assign({}, state);
|
const newState = Object.assign({}, state);
|
||||||
delete newState[url + OBJECT_UPDATES_TRASH_PATH];
|
delete newState[url + OBJECT_UPDATES_TRASH_PATH];
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discard the update for a specific action's url and field UUID in the store
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
|
function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const uuid: string = action.payload.uuid;
|
const uuid: string = action.payload.uuid;
|
||||||
@@ -196,11 +226,12 @@ function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) {
|
|||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpdated(state: any, url: string) {
|
/**
|
||||||
const newPageState = Object.assign({}, state[url] || {}, { lastUpdated: Date.now() });
|
* Determine the most prominent FieldChangeType, ordered as follows:
|
||||||
return Object.assign({}, state, { [url]: newPageState });
|
* undefined < UPDATE < ADD < REMOVE
|
||||||
}
|
* @param oldType The current type
|
||||||
|
* @param newType The new type that should possibly override the new type
|
||||||
|
*/
|
||||||
function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType): FieldChangeType {
|
function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType): FieldChangeType {
|
||||||
if (hasNoValue(newType)) {
|
if (hasNoValue(newType)) {
|
||||||
return oldType;
|
return oldType;
|
||||||
@@ -211,6 +242,11 @@ function determineChangeType(oldType: FieldChangeType, newType: FieldChangeType)
|
|||||||
return oldType.valueOf() > newType.valueOf() ? oldType : newType;
|
return oldType.valueOf() > newType.valueOf() ? oldType : newType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the state of a specific action's url and uuid to false or true
|
||||||
|
* @param state The current state
|
||||||
|
* @param action The action to perform on the current state
|
||||||
|
*/
|
||||||
function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction) {
|
function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction) {
|
||||||
const url: string = action.payload.url;
|
const url: string = action.payload.url;
|
||||||
const uuid: string = action.payload.uuid;
|
const uuid: string = action.payload.uuid;
|
||||||
@@ -228,6 +264,10 @@ function setEditableFieldUpdate(state: any, action: SetEditableFieldUpdateAction
|
|||||||
return Object.assign({}, state, { [url]: newPageState });
|
return Object.assign({}, state, { [url]: newPageState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to create an initial FieldStates object based on a list of Identifiable objects
|
||||||
|
* @param fields Identifiable objects
|
||||||
|
*/
|
||||||
function createInitialFieldStates(fields: Identifiable[]) {
|
function createInitialFieldStates(fields: Identifiable[]) {
|
||||||
const uuids = fields.map((field: Identifiable) => field.uuid);
|
const uuids = fields.map((field: Identifiable) => field.uuid);
|
||||||
const fieldStates = {};
|
const fieldStates = {};
|
||||||
|
233
src/app/core/data/object-updates/object-updates.service.spec.ts
Normal file
233
src/app/core/data/object-updates/object-updates.service.spec.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectUpdatesService } from './object-updates.service';
|
||||||
|
import {
|
||||||
|
DiscardObjectUpdatesAction,
|
||||||
|
FieldChangeType,
|
||||||
|
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction,
|
||||||
|
SetEditableFieldUpdateAction
|
||||||
|
} from './object-updates.actions';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Notification } from '../../../shared/notifications/models/notification.model';
|
||||||
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
|
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
||||||
|
|
||||||
|
describe('ObjectUpdatesService', () => {
|
||||||
|
let service: ObjectUpdatesService;
|
||||||
|
let store: Store<CoreState>;
|
||||||
|
const value = 'test value';
|
||||||
|
const url = 'test-url.com/dspace';
|
||||||
|
const identifiable1 = { uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320' };
|
||||||
|
const identifiable1Updated = { uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320', value: value };
|
||||||
|
const identifiable2 = { uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241' };
|
||||||
|
const identifiable3 = { uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e' };
|
||||||
|
const identifiables = [identifiable1, identifiable2];
|
||||||
|
|
||||||
|
const fieldUpdates = {
|
||||||
|
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE },
|
||||||
|
[identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }
|
||||||
|
};
|
||||||
|
|
||||||
|
const modDate = new Date(2010, 2, 11);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const fieldStates = {
|
||||||
|
[identifiable1.uuid]: { editable: false, isNew: false },
|
||||||
|
[identifiable2.uuid]: { editable: true, isNew: false },
|
||||||
|
[identifiable3.uuid]: { editable: true, isNew: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectEntry = {
|
||||||
|
fieldStates, fieldUpdates, lastModified: modDate
|
||||||
|
};
|
||||||
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
|
spyOn(store, 'dispatch');
|
||||||
|
service = new ObjectUpdatesService(store);
|
||||||
|
|
||||||
|
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
||||||
|
spyOn(service as any, 'saveFieldUpdate');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialize', () => {
|
||||||
|
it('should dispatch an INITIALIZE action with the correct URL, initial identifiables and the last modified date', () => {
|
||||||
|
service.initialize(url, identifiables, modDate);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new InitializeFieldsAction(url, identifiables, modDate));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFieldUpdates', () => {
|
||||||
|
it('should return the list of all fields, including their update if there is one', () => {
|
||||||
|
const result$ = service.getFieldUpdates(url, identifiables);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE },
|
||||||
|
[identifiable2.uuid]: { field: identifiable2, changeType: undefined },
|
||||||
|
[identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }
|
||||||
|
};
|
||||||
|
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEditable', () => {
|
||||||
|
it('should return false if this identifiable is currently not editable in the store', () => {
|
||||||
|
const result$ = service.isEditable(url, identifiable1.uuid);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if this identifiable is currently editable in the store', () => {
|
||||||
|
const result$ = service.isEditable(url, identifiable2.uuid);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveAddFieldUpdate', () => {
|
||||||
|
it('should call saveFieldUpdate on the service with FieldChangeType.ADD', () => {
|
||||||
|
service.saveAddFieldUpdate(url, identifiable1);
|
||||||
|
expect((service as any).saveFieldUpdate).toHaveBeenCalledWith(url, identifiable1, FieldChangeType.ADD);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveRemoveFieldUpdate', () => {
|
||||||
|
it('should call saveFieldUpdate on the service with FieldChangeType.REMOVE', () => {
|
||||||
|
service.saveRemoveFieldUpdate(url, identifiable1);
|
||||||
|
expect((service as any).saveFieldUpdate).toHaveBeenCalledWith(url, identifiable1, FieldChangeType.REMOVE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveChangeFieldUpdate', () => {
|
||||||
|
it('should call saveFieldUpdate on the service with FieldChangeType.UPDATE', () => {
|
||||||
|
service.saveChangeFieldUpdate(url, identifiable1);
|
||||||
|
expect((service as any).saveFieldUpdate).toHaveBeenCalledWith(url, identifiable1, FieldChangeType.UPDATE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setEditableFieldUpdate', () => {
|
||||||
|
it('should dispatch a SetEditableFieldUpdateAction action with the correct URL, uuid and true when true was set', () => {
|
||||||
|
service.setEditableFieldUpdate(url, identifiable1.uuid, true);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SetEditableFieldUpdateAction(url, identifiable1.uuid, true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch an SetEditableFieldUpdateAction action with the correct URL, uuid and false when false was set', () => {
|
||||||
|
service.setEditableFieldUpdate(url, identifiable1.uuid, false);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SetEditableFieldUpdateAction(url, identifiable1.uuid, false));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('discardFieldUpdates', () => {
|
||||||
|
it('should dispatch a DiscardObjectUpdatesAction action with the correct URL and passed notification ', () => {
|
||||||
|
const undoNotification = new Notification('id', NotificationType.Info, 'undo');
|
||||||
|
service.discardFieldUpdates(url, undoNotification);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new DiscardObjectUpdatesAction(url, undoNotification));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reinstateFieldUpdates', () => {
|
||||||
|
it('should dispatch a ReinstateObjectUpdatesAction action with the correct URL ', () => {
|
||||||
|
service.reinstateFieldUpdates(url);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new ReinstateObjectUpdatesAction(url));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSingleFieldUpdate', () => {
|
||||||
|
it('should dispatch a RemoveFieldUpdateAction action with the correct URL and uuid', () => {
|
||||||
|
service.removeSingleFieldUpdate(url, identifiable1.uuid);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFieldUpdateAction(url, identifiable1.uuid));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUpdatedFields', () => {
|
||||||
|
it('should return the list of all metadata fields with their new values', () => {
|
||||||
|
const result$ = service.getUpdatedFields(url, identifiables);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
|
||||||
|
const expectedResult = [identifiable1Updated, identifiable2, identifiable3];
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasUpdates', () => {
|
||||||
|
it('should return true when there are updates', () => {
|
||||||
|
const result$ = service.hasUpdates(url);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
|
||||||
|
const expectedResult = true;
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('when updates are emtpy', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(service as any).getObjectEntry.and.returnValue(observableOf({}))
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there are no updates', () => {
|
||||||
|
const result$ = service.hasUpdates(url);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
|
||||||
|
const expectedResult = false;
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isReinstatable', () => {
|
||||||
|
|
||||||
|
describe('when updates are not emtpy', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'hasUpdates').and.returnValue(observableOf(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', () => {
|
||||||
|
const result$ = service.isReinstatable(url);
|
||||||
|
expect(service.hasUpdates).toHaveBeenCalledWith(url + OBJECT_UPDATES_TRASH_PATH);
|
||||||
|
|
||||||
|
const expectedResult = true;
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when updates are emtpy', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'hasUpdates').and.returnValue(observableOf(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
const result$ = service.isReinstatable(url);
|
||||||
|
expect(service.hasUpdates).toHaveBeenCalledWith(url + OBJECT_UPDATES_TRASH_PATH);
|
||||||
|
const expectedResult = false;
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLastModified', () => {
|
||||||
|
it('should return true when hasUpdates returns true', () => {
|
||||||
|
const result$ = service.getLastModified(url);
|
||||||
|
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||||
|
|
||||||
|
const expectedResult = modDate;
|
||||||
|
result$.subscribe((result) => {
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -29,24 +29,49 @@ function filterByUrlObjectUpdatesStateSelector(url: string): MemoizedSelector<Co
|
|||||||
return createSelector(objectUpdatesStateSelector(), (state: ObjectUpdatesState) => state[url]);
|
return createSelector(objectUpdatesStateSelector(), (state: ObjectUpdatesState) => state[url]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that dispatches and reads from the ObjectUpdates' state in the store
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectUpdatesService {
|
export class ObjectUpdatesService {
|
||||||
constructor(private store: Store<CoreState>) {
|
constructor(private store: Store<CoreState>) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to dispatch an InitializeFieldsAction to the store
|
||||||
|
* @param url The page's URL for which the changes are being mapped
|
||||||
|
* @param fields The initial fields for the page's object
|
||||||
|
* @param lastModified The date the object was last modified
|
||||||
|
*/
|
||||||
initialize(url, fields: Identifiable[], lastModified: Date): void {
|
initialize(url, fields: Identifiable[], lastModified: Date): void {
|
||||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to dispatch an AddFieldUpdateAction to the store
|
||||||
|
* @param url The page's URL for which the changes are saved
|
||||||
|
* @param field An updated field for the page's object
|
||||||
|
* @param changeType The last type of change applied to this field
|
||||||
|
*/
|
||||||
private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType) {
|
private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType) {
|
||||||
this.store.dispatch(new AddFieldUpdateAction(url, field, changeType))
|
this.store.dispatch(new AddFieldUpdateAction(url, field, changeType))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the ObjectUpdatesEntry state for a specific URL
|
||||||
|
* @param url The URL to filter by
|
||||||
|
*/
|
||||||
private getObjectEntry(url: string): Observable<ObjectUpdatesEntry> {
|
private getObjectEntry(url: string): Observable<ObjectUpdatesEntry> {
|
||||||
return this.store.pipe(select(filterByUrlObjectUpdatesStateSelector(url)));
|
return this.store.pipe(select(filterByUrlObjectUpdatesStateSelector(url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that combines the state's updates with the initial values (when there's no update) to create
|
||||||
|
* a FieldUpdates object
|
||||||
|
* @param url The URL of the page for which the FieldUpdates should be requested
|
||||||
|
* @param initialFields The initial values of the fields
|
||||||
|
*/
|
||||||
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
return objectUpdates.pipe(map((objectEntry) => {
|
||||||
@@ -63,6 +88,11 @@ export class ObjectUpdatesService {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to check if a specific field is currently editable in the store
|
||||||
|
* @param url The URL of the page on which the field resides
|
||||||
|
* @param uuid The UUID of the field
|
||||||
|
*/
|
||||||
isEditable(url: string, uuid: string): Observable<boolean> {
|
isEditable(url: string, uuid: string): Observable<boolean> {
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(
|
return objectUpdates.pipe(
|
||||||
@@ -72,34 +102,74 @@ export class ObjectUpdatesService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the saveFieldUpdate method with FieldChangeType.ADD
|
||||||
|
* @param url The page's URL for which the changes are saved
|
||||||
|
* @param field An updated field for the page's object
|
||||||
|
*/
|
||||||
saveAddFieldUpdate(url: string, field: Identifiable) {
|
saveAddFieldUpdate(url: string, field: Identifiable) {
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the saveFieldUpdate method with FieldChangeType.REMOVE
|
||||||
|
* @param url The page's URL for which the changes are saved
|
||||||
|
* @param field An updated field for the page's object
|
||||||
|
*/
|
||||||
saveRemoveFieldUpdate(url: string, field: Identifiable) {
|
saveRemoveFieldUpdate(url: string, field: Identifiable) {
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the saveFieldUpdate method with FieldChangeType.UPDATE
|
||||||
|
* @param url The page's URL for which the changes are saved
|
||||||
|
* @param field An updated field for the page's object
|
||||||
|
*/
|
||||||
saveChangeFieldUpdate(url: string, field: Identifiable) {
|
saveChangeFieldUpdate(url: string, field: Identifiable) {
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a SetEditableFieldUpdateAction to the store to set a field's editable state
|
||||||
|
* @param url The URL of the page on which the field resides
|
||||||
|
* @param uuid The UUID of the field that should be set
|
||||||
|
* @param editable The new value of editable in the store for this field
|
||||||
|
*/
|
||||||
setEditableFieldUpdate(url: string, uuid: string, editable: boolean) {
|
setEditableFieldUpdate(url: string, uuid: string, editable: boolean) {
|
||||||
this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable));
|
this.store.dispatch(new SetEditableFieldUpdateAction(url, uuid, editable));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to dispatch an DiscardObjectUpdatesAction to the store
|
||||||
|
* @param url The page's URL for which the changes should be discarded
|
||||||
|
* @param undoNotification The notification which is should possibly be canceled
|
||||||
|
*/
|
||||||
discardFieldUpdates(url: string, undoNotification: INotification) {
|
discardFieldUpdates(url: string, undoNotification: INotification) {
|
||||||
this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification));
|
this.store.dispatch(new DiscardObjectUpdatesAction(url, undoNotification));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to dispatch an ReinstateObjectUpdatesAction to the store
|
||||||
|
* @param url The page's URL for which the changes should be reinstated
|
||||||
|
*/
|
||||||
reinstateFieldUpdates(url: string) {
|
reinstateFieldUpdates(url: string) {
|
||||||
this.store.dispatch(new ReinstateObjectUpdatesAction(url));
|
this.store.dispatch(new ReinstateObjectUpdatesAction(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to dispatch an RemoveFieldUpdateAction to the store
|
||||||
|
* @param url The page's URL for which the changes should be removed
|
||||||
|
*/
|
||||||
removeSingleFieldUpdate(url: string, uuid) {
|
removeSingleFieldUpdate(url: string, uuid) {
|
||||||
this.store.dispatch(new RemoveFieldUpdateAction(url, uuid));
|
this.store.dispatch(new RemoveFieldUpdateAction(url, uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that combines the state's updates with the initial values (when there's no update) to create
|
||||||
|
* a list of updates fields
|
||||||
|
* @param url The URL of the page for which the updated fields should be requested
|
||||||
|
* @param initialFields The initial values of the fields
|
||||||
|
*/
|
||||||
getUpdatedFields(url: string, initialFields: Identifiable[]): Observable<Identifiable[]> {
|
getUpdatedFields(url: string, initialFields: Identifiable[]): Observable<Identifiable[]> {
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
return objectUpdates.pipe(map((objectEntry) => {
|
||||||
@@ -120,14 +190,26 @@ export class ObjectUpdatesService {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the page currently has updates in the store or not
|
||||||
|
* @param url The page's url to check for in the store
|
||||||
|
*/
|
||||||
hasUpdates(url: string): Observable<boolean> {
|
hasUpdates(url: string): Observable<boolean> {
|
||||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
||||||
}
|
}
|
||||||
|
|
||||||
isReinstatable(route: string): Observable<boolean> {
|
/**
|
||||||
return this.hasUpdates(route + OBJECT_UPDATES_TRASH_PATH)
|
* Checks if the page currently is reinstatable in the store or not
|
||||||
|
* @param url The page's url to check for in the store
|
||||||
|
*/
|
||||||
|
isReinstatable(url: string): Observable<boolean> {
|
||||||
|
return this.hasUpdates(url + OBJECT_UPDATES_TRASH_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the current lastModified date stored for the updates in the store
|
||||||
|
* @param url The page's url to check for in the store
|
||||||
|
*/
|
||||||
getLastModified(url: string): Observable<Date> {
|
getLastModified(url: string): Observable<Date> {
|
||||||
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||||
<input type="submit" class="d-none"/>
|
<input type="submit" class="d-none"/>
|
||||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||||
<div>
|
<div class="dropdown-list">
|
||||||
<div *ngFor="let suggestionOption of suggestions">
|
<div *ngFor="let suggestionOption of suggestions">
|
||||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
||||||
<span [innerHTML]="suggestionOption.displayValue"></span>
|
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||||
|
@@ -77,7 +77,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the last element ', () => {
|
it('should put the focus on the last element ', () => {
|
||||||
const lastLink = de.query(By.css('.list-unstyled > li:last-child a'));
|
const lastLink = de.query(By.css('.dropdown-list > div:last-child a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(lastLink.nativeElement);
|
expect(activeElement).toEqual(lastLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -103,7 +103,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the first element ', () => {
|
it('should put the focus on the first element ', () => {
|
||||||
const firstLink = de.query(By.css('.list-unstyled > li:first-child a'));
|
const firstLink = de.query(By.css('.dropdown-list > div:first-child a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(firstLink.nativeElement);
|
expect(activeElement).toEqual(firstLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -117,7 +117,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the second element', () => {
|
it('should put the focus on the second element', () => {
|
||||||
const secondLink = de.query(By.css('.list-unstyled > li:nth-child(2) a'));
|
const secondLink = de.query(By.css('.dropdown-list > div:nth-child(2) a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(secondLink.nativeElement);
|
expect(activeElement).toEqual(secondLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -126,7 +126,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
|
|
||||||
describe('when the first element is in focus', () => {
|
describe('when the first element is in focus', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const firstLink = de.query(By.css('.list-unstyled > li:first-child a'));
|
const firstLink = de.query(By.css('.dropdown-list > div:first-child a'));
|
||||||
firstLink.nativeElement.focus();
|
firstLink.nativeElement.focus();
|
||||||
comp.selectedIndex = 0;
|
comp.selectedIndex = 0;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -140,7 +140,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the last element ', () => {
|
it('should put the focus on the last element ', () => {
|
||||||
const lastLink = de.query(By.css('.list-unstyled > li:last-child a'));
|
const lastLink = de.query(By.css('.dropdown-list > div:last-child a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(lastLink.nativeElement);
|
expect(activeElement).toEqual(lastLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -153,7 +153,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the second element ', () => {
|
it('should put the focus on the second element ', () => {
|
||||||
const secondLink = de.query(By.css('.list-unstyled > li:nth-child(2) a'));
|
const secondLink = de.query(By.css('.dropdown-list > div:nth-child(2) a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(secondLink.nativeElement);
|
expect(activeElement).toEqual(secondLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -162,7 +162,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
|
|
||||||
describe('when the last element is in focus', () => {
|
describe('when the last element is in focus', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const lastLink = de.query(By.css('.list-unstyled > li:last-child a'));
|
const lastLink = de.query(By.css('.dropdown-list > div:last-child a'));
|
||||||
lastLink.nativeElement.focus();
|
lastLink.nativeElement.focus();
|
||||||
comp.selectedIndex = suggestions.length - 1;
|
comp.selectedIndex = suggestions.length - 1;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -176,7 +176,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the second last element ', () => {
|
it('should put the focus on the second last element ', () => {
|
||||||
const secondLastLink = de.query(By.css('.list-unstyled > li:nth-last-child(2) a'));
|
const secondLastLink = de.query(By.css('.dropdown-list > div:nth-last-child(2) a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(secondLastLink.nativeElement);
|
expect(activeElement).toEqual(secondLastLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -189,7 +189,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should put the focus on the first element ', () => {
|
it('should put the focus on the first element ', () => {
|
||||||
const firstLink = de.query(By.css('.list-unstyled > li:first-child a'));
|
const firstLink = de.query(By.css('.dropdown-list > div:first-child a'));
|
||||||
const activeElement = el.ownerDocument.activeElement;
|
const activeElement = el.ownerDocument.activeElement;
|
||||||
expect(activeElement).toEqual(firstLink.nativeElement);
|
expect(activeElement).toEqual(firstLink.nativeElement);
|
||||||
});
|
});
|
||||||
@@ -294,7 +294,7 @@ describe('InputSuggestionsComponent', () => {
|
|||||||
const clickedIndex = 0;
|
const clickedIndex = 0;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(comp, 'onClickSuggestion');
|
spyOn(comp, 'onClickSuggestion');
|
||||||
const clickedLink = de.query(By.css('.list-unstyled > li:nth-child(' + (clickedIndex + 1) + ') a'));
|
const clickedLink = de.query(By.css('.dropdown-list > div:nth-child(' + (clickedIndex + 1) + ') a'));
|
||||||
clickedLink.triggerEventHandler('click', {} );
|
clickedLink.triggerEventHandler('click', {} );
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -207,9 +207,11 @@ export class InputSuggestionsComponent implements ControlValueAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerOnTouched(fn: any): void {
|
registerOnTouched(fn: any): void {
|
||||||
|
/* no implementation */
|
||||||
}
|
}
|
||||||
|
|
||||||
setDisabledState(isDisabled: boolean): void {
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
/* no implementation */
|
||||||
}
|
}
|
||||||
|
|
||||||
writeValue(value: any): void {
|
writeValue(value: any): void {
|
||||||
|
Reference in New Issue
Block a user