Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,66 @@
</div>
</div>
</div>

<div class="labels-section mt-4">
<h3 class="border-bottom pb-2">{{'clarin.license.label.section.title' | translate}}</h3>

<div *ngVar="(labelsRD$ | async)?.payload as cLicenseLabels">
<ds-pagination
[paginationOptions]="labelPaginationOptions"
[collectionSize]="cLicenseLabels?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[retainScrollPosition]="true">
<div class="table-responsive">
<table class="table table-striped table-bordered align-middle my-2">
<thead>
<tr>
<th>{{'clarin.license.label.table.header.label' | translate}}</th>
<th>{{'clarin.license.label.table.header.title' | translate}}</th>
<th>{{'clarin.license.label.table.header.extended' | translate}}</th>
<th>{{'clarin.license.label.table.header.icon' | translate}}</th>
<th class="labels-actions-column">{{'clarin.license.label.table.header.actions' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let label of cLicenseLabels?.page">
<td>{{label?.label}}</td>
<td>{{label?.title}}</td>
<td>{{label?.extended ? ('clarin.license.label.table.boolean.yes' | translate) : ('clarin.license.label.table.boolean.no' | translate)}}</td>
<td>
<i class="fas" [ngClass]="label?.icon?.length > 0 ? 'fa-check text-success' : 'fa-times text-muted'" aria-hidden="true"></i>
<span class="sr-only">{{label?.icon?.length > 0 ? ('clarin.license.label.table.icon.available' | translate) : ('clarin.license.label.table.icon.none' | translate)}}</span>
</td>
<td>
<button
type="button"
class="btn btn-warning btn-sm mr-1"
(click)="editLabel(label)"
[attr.aria-label]="('clarin.license.label.table.edit.aria' | translate:{label: (label?.label || '')})">
<i class="fas fa-pen" aria-hidden="true"></i>
{{'clarin.license.label.table.edit' | translate}}
</button>
<button
type="button"
class="btn btn-danger btn-sm"
(click)="confirmDeleteLabel(label)"
[attr.aria-label]="('clarin.license.label.table.delete.aria' | translate:{label: (label?.label || '')})">
<i class="fas fa-trash" aria-hidden="true"></i>
{{'clarin.license.label.table.delete' | translate}}
</button>
</td>
</tr>
<tr *ngIf="!(loading$ | async) && !(cLicenseLabels?.page?.length > 0)">
<td colspan="5" class="text-center text-muted">{{'clarin.license.label.table.empty' | translate}}</td>
</tr>
</tbody>
</table>
</div>

<ds-loading *ngIf="loading$ | async" class="text-center"></ds-loading>
<span *ngIf="loading$ | async" class="sr-only">{{'clarin.license.label.table.loading' | translate}}</span>
</ds-pagination>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
width: 3.5%;
max-width: 3.5%;
}

.labels-actions-column {
width: 11rem;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ClarinLicenseTableComponent } from './clarin-license-table.component';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { ClarinLicenseDataService } from '../../core/data/clarin/clarin-license-data.service';
import { RequestService } from '../../core/data/request.service';
import { of as observableOf } from 'rxjs';
import { EventEmitter } from '@angular/core';
import { of as observableOf, throwError } from 'rxjs';
import { SharedModule } from '../../shared/shared.module';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
Expand All @@ -15,22 +17,27 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { defaultPagination } from '../clarin-license-table-pagination';
import { ClarinLicenseLabelDataService } from '../../core/data/clarin/clarin-license-label-data.service';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import {
createdLicenseLabelRD$,
createdLicenseRD$,
mockExtendedLicenseLabel,
mockLicenseLabelListRD$,
mockLicense, mockLicenseRD$,
mockNonExtendedLicenseLabel, successfulResponse
} from '../../shared/testing/clarin-license-mock';
import {GroupDataService} from '../../core/eperson/group-data.service';
import {createSuccessfulRemoteDataObject$} from '../../shared/remote-data.utils';
import { createFailedRemoteDataObject$, createNoContentRemoteDataObject$ } from '../../shared/remote-data.utils';
import {createPaginatedList} from '../../shared/testing/utils.test';
import {LinkHeadService} from '../../core/services/link-head.service';
import {ConfigurationDataService} from '../../core/data/configuration-data.service';
import {ConfigurationProperty} from '../../core/shared/configuration-property.model';
import {SearchConfigurationService} from '../../core/shared/search/search-configuration.service';
import { DefineLicenseLabelFormComponent } from './modal/define-license-label-form/define-license-label-form.component';
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';

describe('ClarinLicenseTableComponent', () => {
let component: ClarinLicenseTableComponent;
Expand All @@ -40,10 +47,13 @@ describe('ClarinLicenseTableComponent', () => {
let clarinLicenseLabelDataService: ClarinLicenseLabelDataService;
let requestService: RequestService;
let notificationService: NotificationsServiceStub;
let modalStub: NgbActiveModal;
let activeModalStub: NgbActiveModal;
let modalServiceStub: jasmine.SpyObj<NgbModal>;
let groupsDataService: GroupDataService;
let service: ConfigurationDataService;
let searchConfigurationServiceStub: SearchConfigurationService;
let labelEditModalRef: any;
let labelDeleteModalRef: any;

beforeEach(async () => {
notificationService = new NotificationsServiceStub();
Expand All @@ -55,14 +65,36 @@ describe('ClarinLicenseTableComponent', () => {
getLinkPath: observableOf('')
});
clarinLicenseLabelDataService = jasmine.createSpyObj('clarinLicenseLabelService', {
create: createdLicenseLabelRD$
create: createdLicenseLabelRD$,
findAll: mockLicenseLabelListRD$,
put: createdLicenseLabelRD$,
delete: observableOf({ hasSucceeded: true })
});
requestService = jasmine.createSpyObj('requestService', {
send: observableOf('response'),
getByUUID: observableOf(successfulResponse),
generateRequestId: observableOf('123456'),
});
modalStub = jasmine.createSpyObj('modalService', ['close', 'open']);
activeModalStub = jasmine.createSpyObj('activeModal', ['close', 'open']);
modalServiceStub = jasmine.createSpyObj('modalService', ['open']);
labelEditModalRef = {
componentInstance: {},
result: Promise.resolve(null)
};
labelDeleteModalRef = {
componentInstance: {
response: new EventEmitter<boolean>()
}
};
modalServiceStub.open.and.callFake((modalComponent) => {
if (modalComponent === DefineLicenseLabelFormComponent) {
return labelEditModalRef;
}
if (modalComponent === ConfirmationModalComponent) {
return labelDeleteModalRef;
}
return { componentInstance: {}, result: Promise.resolve(null) } as any;
});
groupsDataService = jasmine.createSpyObj('groupsDataService', {
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: ''
Expand Down Expand Up @@ -101,7 +133,8 @@ describe('ClarinLicenseTableComponent', () => {
{ provide: ClarinLicenseLabelDataService, useValue: clarinLicenseLabelDataService },
{ provide: PaginationService, useValue: new PaginationServiceStub() },
{ provide: NotificationsService, useValue: notificationService },
{ provide: NgbActiveModal, useValue: modalStub },
{ provide: NgbActiveModal, useValue: activeModalStub },
{ provide: NgbModal, useValue: modalServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: GroupDataService, useValue: groupsDataService },
{ provide: LinkHeadService, useValue: linkHeadService },
Expand Down Expand Up @@ -181,4 +214,120 @@ describe('ClarinLicenseTableComponent', () => {
expect((component as any).clarinLicenseService.searchBy).toHaveBeenCalled();
expect((component as ClarinLicenseTableComponent).licensesRD$).not.toBeNull();
});

describe('label edit flow', () => {
beforeEach(() => {
notificationService.success.calls.reset();
notificationService.error.calls.reset();
(clarinLicenseLabelDataService.put as jasmine.Spy).calls.reset();
});

it('should open edit modal with the selected label when editLabel is called', () => {
component.editLabel(mockExtendedLicenseLabel);

expect(modalServiceStub.open).toHaveBeenCalledWith(DefineLicenseLabelFormComponent);
expect(labelEditModalRef.componentInstance.clarinLicenseLabel).toBe(mockExtendedLicenseLabel);
});

it('should call clarinLicenseLabelService.put with updated label on modal submit', fakeAsync(() => {
const refreshSpy = spyOn(component, 'refreshLabels').and.stub();
const reloadLicensesSpy = spyOn(component, 'loadAllLicenses').and.stub();
labelEditModalRef.result = Promise.resolve({
label: 'EDIT',
title: 'Edited title',
extended: false
});

component.editLabel(mockExtendedLicenseLabel);
tick();

expect((clarinLicenseLabelDataService.put as jasmine.Spy)).toHaveBeenCalled();
const putArgument = (clarinLicenseLabelDataService.put as jasmine.Spy).calls.mostRecent().args[0];
expect(putArgument.id).toBe(mockExtendedLicenseLabel.id);
expect(putArgument._links).toEqual(mockExtendedLicenseLabel._links);
expect(putArgument.label).toBe('EDIT');
expect(putArgument.title).toBe('Edited title');
expect(putArgument.extended).toBeFalse();
expect(notificationService.success).toHaveBeenCalled();
expect(refreshSpy).toHaveBeenCalled();
expect(reloadLicensesSpy).toHaveBeenCalled();
}));

it('should show error notification on failed edit', fakeAsync(() => {
spyOn(component, 'refreshLabels').and.stub();
(clarinLicenseLabelDataService.put as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('put failed', 500));

component.editLicenseLabel({
label: 'ERR',
title: 'Failed title',
extended: true
}, mockExtendedLicenseLabel);
tick();

expect(notificationService.error).toHaveBeenCalled();
}));
});

describe('label delete flow', () => {
beforeEach(() => {
notificationService.success.calls.reset();
notificationService.error.calls.reset();
(clarinLicenseLabelDataService.delete as jasmine.Spy).calls.reset();
labelDeleteModalRef.componentInstance.response = new EventEmitter<boolean>();
});

it('should open confirmation modal when confirmDeleteLabel is called', () => {
component.confirmDeleteLabel(mockNonExtendedLicenseLabel);

expect(modalServiceStub.open).toHaveBeenCalledWith(ConfirmationModalComponent);
expect(labelDeleteModalRef.componentInstance.headerLabel).toBe('clarin.license.label.delete.confirm.title');
expect(labelDeleteModalRef.componentInstance.infoLabel).toBe('clarin.license.label.delete.confirm.message');
expect(labelDeleteModalRef.componentInstance.dso.name).toBe(mockNonExtendedLicenseLabel.label);
});

it('should call clarinLicenseLabelService.delete with correct id on confirmation', fakeAsync(() => {
const refreshSpy = spyOn(component, 'refreshLabels').and.stub();
const reloadLicensesSpy = spyOn(component, 'loadAllLicenses').and.stub();
(clarinLicenseLabelDataService.delete as jasmine.Spy).and.returnValue(createNoContentRemoteDataObject$());

component.confirmDeleteLabel(mockNonExtendedLicenseLabel);
labelDeleteModalRef.componentInstance.response.emit(true);
tick();

expect((clarinLicenseLabelDataService.delete as jasmine.Spy)).toHaveBeenCalledWith(String(mockNonExtendedLicenseLabel.id));
expect(notificationService.success).toHaveBeenCalled();
expect(refreshSpy).toHaveBeenCalled();
expect(reloadLicensesSpy).toHaveBeenCalled();
}));

it('should show error notification on failed delete', () => {
spyOn(component, 'refreshLabels').and.stub();
(clarinLicenseLabelDataService.delete as jasmine.Spy).and.returnValue(throwError(() => new Error('delete failed')));

component.confirmDeleteLabel(mockNonExtendedLicenseLabel);
labelDeleteModalRef.componentInstance.response.emit(true);

expect(notificationService.error).toHaveBeenCalled();
});

it('should not call delete service when confirmation is cancelled', () => {
component.confirmDeleteLabel(mockNonExtendedLicenseLabel);
labelDeleteModalRef.componentInstance.response.emit(false);

expect((clarinLicenseLabelDataService.delete as jasmine.Spy)).not.toHaveBeenCalled();
});
});

describe('label row actions', () => {
it('should render edit and delete buttons for each label row', () => {
fixture.detectChanges();

const firstRowButtons = fixture.debugElement.queryAll(By.css('.labels-section tbody tr'))[0]
.queryAll(By.css('button'));

expect(firstRowButtons.length).toBe(2);
expect((firstRowButtons[0].nativeElement as HTMLButtonElement).disabled).toBeFalse();
expect((firstRowButtons[1].nativeElement as HTMLButtonElement).disabled).toBeFalse();
});
});
});
Loading
Loading