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 @@ -42,7 +42,7 @@
}
@if (type === 'search') {
<span class="d-inline-flex pe-1">
<a [href]="getLinkToSearch(i)">{{mdValue.value | dsReplace: this.replaceCharacter}}</a>
<a [href]="getLinkToSearch(i)">{{mdValue.value | dsReplace: this.replaceCharacter}} @if (mdValue.authority && (fields?.includes('dc.publisher') || fields?.includes('creativework.publisher'))) {<img src="assets/images/ror-icon.svg" alt="ROR ID" height="16" class="ror-icon ms-1 align-middle">}</a>
@if (!last) {
<span [innerHTML]="separator"></span>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';

import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
import { Item } from '../../../../core/shared/item.model';
import { ClarinGenericItemFieldComponent } from './clarin-generic-item-field.component';

describe('ClarinGenericItemFieldComponent', () => {
let component: ClarinGenericItemFieldComponent;
let fixture: ComponentFixture<ClarinGenericItemFieldComponent>;

const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
findByPropertyName: of(true),
});
const dsoNameServiceSpy = jasmine.createSpyObj('dsoNameService', ['getName']);

/** Build an Item carrying a single metadata value (dc.publisher by default) with the given authority. */
function itemWith(authority: string | null, field = 'dc.publisher', value = 'ACME Press'): Item {
const item = new Item();
item.metadata = {
[field]: [
{
value,
authority,
confidence: authority ? 600 : -1,
place: 0,
language: null,
uuid: 'mock-uuid',
isVirtual: false,
virtualValue: null,
} as any,
],
};
return item;
}

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
ClarinGenericItemFieldComponent,
],
providers: [
{ provide: ConfigurationDataService, useValue: configurationServiceSpy },
{ provide: DSONameService, useValue: dsoNameServiceSpy },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ClarinGenericItemFieldComponent);
component = fixture.componentInstance;
// Avoid ngOnInit (detectChanges) for the isolation tests so we can exercise getLinkToSearch directly.
component.baseUrl = 'http://localhost:4000';
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('getLinkToSearch', () => {
it('uses the authority operator and key when the metadata value has an authority', () => {
component.item = itemWith('02mhbdp94');
component.fields = ['dc.publisher'];
expect(component.getLinkToSearch(0))
.toBe('http://localhost:4000/search?f.publisher=02mhbdp94,authority');
});

it('uses the equals operator and plain value when the metadata value has no authority', () => {
component.item = itemWith(null);
component.fields = ['dc.publisher'];
expect(component.getLinkToSearch(0))
.toBe('http://localhost:4000/search?f.publisher=ACME%20Press,equals');
});

it('uses the explicitly provided value (e.g. a split subject) with the equals operator', () => {
const item = new Item();
item.metadata = {
'dc.subject': [
{ value: 'history;art', authority: null, confidence: -1, place: 0, language: null } as any,
],
};
component.item = item;
component.fields = ['dc.subject'];
expect(component.getLinkToSearch(-1, 'history'))
.toBe('http://localhost:4000/search?f.subject=history,equals');
});

it('falls back to the bare search endpoint when the index is out of range', () => {
component.item = itemWith(null);
component.fields = ['dc.publisher'];
expect(component.getLinkToSearch(5)).toBe('http://localhost:4000/search');
});
});

describe('ROR icon rendering (guard scope)', () => {
it('renders the ROR icon for an authority-bearing dc.publisher search field', () => {
component.item = itemWith('02mhbdp94', 'dc.publisher');
component.fields = ['dc.publisher'];
component.type = 'search';
fixture.detectChanges();
const img = fixture.debugElement.query(By.css('img.ror-icon'));
expect(img).not.toBeNull();
expect(img.nativeElement.getAttribute('src')).toContain('ror-icon.svg');
});

it('does NOT render the ROR icon for an authority-bearing NON-publisher field (dc.subject)', () => {
component.item = itemWith('some-authority', 'dc.subject', 'History');
component.fields = ['dc.subject'];
component.type = 'search';
fixture.detectChanges();
const imgs = fixture.debugElement.queryAll(By.css('img'))
.filter((de) => (de.nativeElement.getAttribute('src') || '').includes('ror-icon.svg'));
expect(imgs.length).toBe(0);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Item } from '../../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { ClarinItemAuthorPreviewComponent } from '../../../../shared/clarin-item-author-preview/clarin-item-author-preview.component';
import {
buildAuthoritySearchFilter,
convertMetadataFieldIntoSearchType,
getBaseUrl,
} from '../../../../shared/clarin-shared-util';
Expand Down Expand Up @@ -172,38 +173,17 @@ export class ClarinGenericItemFieldComponent implements OnInit {
* @param index
*/
public getLinkToSearch(index, value = '') {
let metadataValue = 'Error: value is empty';
if (isEmpty(value)) {
// Get metadata value from the Item's metadata field
metadataValue = this.getMetadataValue(index);
} else {
// The metadata value is passed from the parameter.
metadataValue = value;
}

const searchType = convertMetadataFieldIntoSearchType(this.fields);
return this.baseUrl + '/search?f.' + encodeURIComponent(searchType) + '=' +
encodeURIComponent(metadataValue) + ',equals';
}

/**
* If the metadata field has more than 1 value return the value based on the index.
* @param index of the metadata value
*/
public getMetadataValue(index) {
let metadataValue = '';
if (index === 0) {
// Return first metadata value.
return this.item.firstMetadataValue(this.fields);
// If a value is explicitly provided (e.g. a single subject from a split list), search by that plain value.
// Otherwise resolve the full MetadataValue for this index so an authority (e.g. ROR) can be used.
const mdValue = !isEmpty(value) ? { value } : this.item.allMetadata(this.fields)?.[index];
if (!mdValue) {
// ultimate fallback (should not happen)
return this.baseUrl + '/search';
}
// The metadata field has more metadata values - get the actual one
this.item.allMetadataValues(this.fields)?.forEach((metadataValueArray, arrayIndex) => {
if (index !== arrayIndex) {
return metadataValue;
}
metadataValue = metadataValueArray;
});
return metadataValue;

return this.baseUrl + '/search?' + buildAuthoritySearchFilter(searchType, mdValue);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<!-- truthiness check: a missing dc.publisher is undefined, and an empty <a>
has no accessible name (axe link-name) -->
<span>(@if (itemPublisher) {
<span><a [href]="publisherRedirectLink">{{itemPublisher}}</a> / </span>
<span><a [attr.href]="publisherRedirectLink">{{itemPublisher}} @if (hasPublisherRorAuthority) {<img src="assets/images/ror-icon.svg" alt="ROR ID" height="16" class="ror-icon ms-1 align-middle">}</a> / </span>
}{{itemDate}})</span>
</div>
}
Expand Down
Loading
Loading