fix issue where meta tags wouldn't be updated properly based on the route

This commit is contained in:
Art Lowel
2021-06-30 14:09:18 +02:00
parent 34b117efe3
commit 03e2e30510
5 changed files with 261 additions and 317 deletions

View File

@@ -1,6 +1,11 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs'; import {
BehaviorSubject,
combineLatest as observableCombineLatest,
Observable,
Subject
} from 'rxjs';
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators'; import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
@@ -8,8 +13,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list.model'; import { PaginatedList } from '../core/data/paginated-list.model';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { MetadataService } from '../core/metadata/metadata.service';
import { Bitstream } from '../core/shared/bitstream.model'; import { Bitstream } from '../core/shared/bitstream.model';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
@@ -65,7 +68,6 @@ export class CollectionPageComponent implements OnInit {
constructor( constructor(
private collectionDataService: CollectionDataService, private collectionDataService: CollectionDataService,
private searchService: SearchService, private searchService: SearchService,
private metadata: MetadataService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private authService: AuthService, private authService: AuthService,
@@ -122,10 +124,6 @@ export class CollectionPageComponent implements OnInit {
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
map((collection) => getCollectionPageRoute(collection.id)) map((collection) => getCollectionPageRoute(collection.id))
); );
this.route.queryParams.pipe(take(1)).subscribe((params) => {
this.metadata.processRemoteData(this.collectionRD$);
});
} }
isNotEmpty(object: any) { isNotEmpty(object: any) {

View File

@@ -1,8 +1,8 @@
import {filter, map} from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable , BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { ItemPageComponent } from '../simple/item-page.component'; import { ItemPageComponent } from '../simple/item-page.component';
import { MetadataMap } from '../../core/shared/metadata.models'; import { MetadataMap } from '../../core/shared/metadata.models';
@@ -11,8 +11,6 @@ import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade'; import { fadeInOut } from '../../shared/animations/fade';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
@@ -35,8 +33,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
metadata$: Observable<MetadataMap>; metadata$: Observable<MetadataMap>;
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService, authService: AuthService) { constructor(route: ActivatedRoute, router: Router, items: ItemDataService, authService: AuthService) {
super(route, router, items, metadataService, authService); super(route, router, items, authService);
} }
/*** AoT inheritance fix, will hopefully be resolved in the near future **/ /*** AoT inheritance fix, will hopefully be resolved in the near future **/

View File

@@ -8,8 +8,6 @@ import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade'; import { fadeInOut } from '../../shared/animations/fade';
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators'; import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
@@ -54,7 +52,6 @@ export class ItemPageComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private items: ItemDataService, private items: ItemDataService,
private metadataService: MetadataService,
private authService: AuthService, private authService: AuthService,
) { } ) { }
@@ -66,7 +63,6 @@ export class ItemPageComponent implements OnInit {
map((data) => data.dso as RemoteData<Item>), map((data) => data.dso as RemoteData<Item>),
redirectOn4xx(this.router, this.authService) redirectOn4xx(this.router, this.authService)
); );
this.metadataService.processRemoteData(this.itemRD$);
this.itemPageRoute$ = this.itemRD$.pipe( this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item)) map((item) => getItemPageRoute(item))

View File

@@ -1,82 +1,30 @@
import { CommonModule, Location } from '@angular/common'; import { fakeAsync, tick } from '@angular/core/testing';
import { HttpClient } from '@angular/common/http'; import { Meta, Title } from '@angular/platform-browser';
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Store, StoreModule } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { EmptyError, Observable, of } from 'rxjs';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { ItemMock, MockBitstream1, MockBitstream3, } from '../../shared/mocks/item.mock';
import { import {
ItemMock, MockBitstream1, MockBitstream2, MockBitstream3, MockBitstreamFormat1, MockBitstreamFormat2, createSuccessfulRemoteDataObject$,
MockBitstreamFormat3, createSuccessfulRemoteDataObject
MockOriginalBundle, } from '../../shared/remote-data.utils';
} from '../../shared/mocks/item.mock'; import { PaginatedList } from '../data/paginated-list.model';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { AuthService } from '../auth/auth.service';
import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { BitstreamDataService } from '../data/bitstream-data.service';
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
import { CommunityDataService } from '../data/community-data.service';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { ItemDataService } from '../data/item-data.service';
import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model';
import { FindListOptions } from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BitstreamFormat } from '../shared/bitstream-format.model';
import { Bitstream } from '../shared/bitstream.model'; import { Bitstream } from '../shared/bitstream.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { MetadataValue } from '../shared/metadata.models'; import { MetadataValue } from '../shared/metadata.models';
import { PageInfo } from '../shared/page-info.model';
import { UUIDService } from '../shared/uuid.service';
import { MetadataService } from './metadata.service'; import { MetadataService } from './metadata.service';
import { storeModuleConfig } from '../../app.reducer';
import { RootDataService } from '../data/root-data.service'; import { RootDataService } from '../data/root-data.service';
import { Root } from '../data/root.model';
import { Bundle } from '../shared/bundle.model'; import { Bundle } from '../shared/bundle.model';
import { BundleDataService } from '../data/bundle-data.service';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { DSONameService } from '../breadcrumbs/dso-name.service';
import { HardRedirectService } from '../services/hard-redirect.service'; import { HardRedirectService } from '../services/hard-redirect.service';
/* tslint:disable:max-classes-per-file */
@Component({
template: `
<router-outlet></router-outlet>`
})
class TestComponent {
constructor(private metadata: MetadataService) {
metadata.listenForRouteChange();
}
}
@Component({ template: '' })
class DummyItemComponent {
constructor(private route: ActivatedRoute, private items: ItemDataService, private metadata: MetadataService) {
this.route.params.subscribe((params) => {
this.metadata.processRemoteData(this.items.findById(params.id));
});
}
}
/* tslint:enable:max-classes-per-file */
describe('MetadataService', () => { describe('MetadataService', () => {
let metadataService: MetadataService; let metadataService: MetadataService;
@@ -84,281 +32,306 @@ describe('MetadataService', () => {
let title: Title; let title: Title;
let store: Store<CoreState>; let dsoNameService: DSONameService;
let objectCacheService: ObjectCacheService;
let requestService: RequestService;
let uuidService: UUIDService;
let remoteDataBuildService: RemoteDataBuildService;
let itemDataService: ItemDataService;
let bundleDataService; let bundleDataService;
let bitstreamDataService; let bitstreamDataService;
let authService: AuthService;
let rootService: RootDataService; let rootService: RootDataService;
let translateService: TranslateService; let translateService: TranslateService;
let hardRedirectService: HardRedirectService; let hardRedirectService: HardRedirectService;
let location: Location;
let router: Router; let router: Router;
let fixture: ComponentFixture<TestComponent>;
let tagStore: Map<string, MetaDefinition[]>;
beforeEach(() => { beforeEach(() => {
rootService = jasmine.createSpyObj({
store = new Store<CoreState>(undefined, undefined, undefined); findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' })
spyOn(store, 'dispatch'); });
bitstreamDataService = jasmine.createSpyObj({
objectCacheService = new ObjectCacheService(store, undefined); findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3]))
uuidService = new UUIDService(); });
requestService = new RequestService(objectCacheService, uuidService, store, undefined); bundleDataService = jasmine.createSpyObj({
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, undefined, requestService); findByItemAndName: mockBundleRD$([MockBitstream3])
bitstreamDataService = { });
findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> { translateService = getMockTranslateService();
if (item.equals(ItemMock)) { meta = jasmine.createSpyObj({
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [MockBitstream1, MockBitstream2])); addTag: {},
} else { removeTag: {}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); });
} title = jasmine.createSpyObj({
}, setTitle: {}
findAllByHref: jasmine.createSpy(), });
}; dsoNameService = jasmine.createSpyObj({
const mockBitstreamFormatDataService = { getName: ItemMock.firstMetadataValue('dc.title')
findByBitstream(bitstream: Bitstream): Observable<RemoteData<BitstreamFormat>> { });
switch (bitstream) { router = {
case MockBitstream1: url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
return createSuccessfulRemoteDataObject$(MockBitstreamFormat1); events: of(new NavigationEnd(1, '', '')),
break; routerState: {
case MockBitstream2: root: {}
return createSuccessfulRemoteDataObject$(MockBitstreamFormat2);
break;
case MockBitstream3:
return createSuccessfulRemoteDataObject$(MockBitstreamFormat3);
break;
default:
return createSuccessfulRemoteDataObject$(new BitstreamFormat());
}
} }
}; } as any as Router;
bundleDataService = jasmine.createSpyObj('bundleDataService', { hardRedirectService = jasmine.createSpyObj( {
findByItemAndName: createSuccessfulRemoteDataObject$(MockOriginalBundle),
});
rootService = jasmine.createSpyObj('rootService', {
findRoot: createSuccessfulRemoteDataObject$(Object.assign(new Root(), {
dspaceVersion: 'mock-dspace-version'
}))
});
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
getRequestOrigin: 'https://request.org', getRequestOrigin: 'https://request.org',
}); });
metadataService = new MetadataService(
TestBed.configureTestingModule({ router,
imports: [ translateService,
CommonModule, meta,
StoreModule.forRoot({}, storeModuleConfig), title,
TranslateModule.forRoot({ dsoNameService,
loader: { bundleDataService,
provide: TranslateLoader, bitstreamDataService,
useClass: TranslateLoaderMock undefined,
} rootService,
}), hardRedirectService
RouterTestingModule.withRoutes([ );
{ path: 'items/:id', component: DummyItemComponent, pathMatch: 'full' },
{
path: 'other',
component: DummyItemComponent,
pathMatch: 'full',
data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' }
}
])
],
declarations: [
TestComponent,
DummyItemComponent
],
providers: [
{ provide: ObjectCacheService, useValue: objectCacheService },
{ provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: {} },
{ provide: AuthService, useValue: {} },
{ provide: NotificationsService, useValue: {} },
{ provide: HttpClient, useValue: {} },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: CommunityDataService, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService },
{ provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: BundleDataService, useValue: bundleDataService },
{ provide: RootDataService, useValue: rootService },
{ provide: HardRedirectService, useValue: hardRedirectService },
Meta,
Title,
// tslint:disable-next-line:no-empty
{ provide: ItemDataService, useValue: { findById: () => {} } },
BrowseService,
MetadataService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
meta = TestBed.inject(Meta);
title = TestBed.inject(Title);
itemDataService = TestBed.inject(ItemDataService);
metadataService = TestBed.inject(MetadataService);
authService = TestBed.inject(AuthService);
translateService = TestBed.inject(TranslateService);
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture = TestBed.createComponent(TestComponent);
tagStore = metadataService.getTagStore();
}); });
it('items page should set meta tags', fakeAsync(() => { it('items page should set meta tags', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(ItemMock),
}
}
});
tick(); tick();
expect(title.getTitle()).toEqual('Test PowerPoint Document'); expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document');
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document'); expect(meta.addTag).toHaveBeenCalledWith({
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane'); property: 'citation_title',
expect(tagStore.get('citation_publication_date')[0].content).toEqual('1650-06-26'); content: 'Test PowerPoint Document'
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789'); });
expect(tagStore.get('citation_language')[0].content).toEqual('en'); expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_author', content: 'Doe, Jane' });
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_publication_date',
content: '1650-06-26'
});
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_issn', content: '123456789' });
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_language', content: 'en' });
expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_keywords',
content: 'keyword1; keyword2; keyword3'
});
})); }));
it('items page should set meta tags as published Thesis', fakeAsync(() => { it('items page should set meta tags as published Thesis', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Thesis')))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
}
}
});
tick(); tick();
expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document'); expect(meta.addTag).toHaveBeenCalledWith({
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download'); property: 'citation_dissertation_name',
content: 'Test PowerPoint Document'
});
expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_pdf_url',
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
});
})); }));
it('items page should set meta tags as published Technical Report', fakeAsync(() => { it('items page should set meta tags as published Technical Report', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Technical Report')))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
tick(); value: {
expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher'); dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
})); }
}
it('other navigation should add title, description and Generator', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
spyOn(translateService, 'get').and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
expect(tagStore.size).toBeGreaterThan(0);
router.navigate(['/other']);
tick();
expect(tagStore.size).toEqual(3);
expect(title.getTitle()).toEqual('DSpace :: Dummy Title');
expect(tagStore.get('title')[0].content).toEqual('DSpace :: Dummy Title');
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version');
}));
describe('when the item has no bitstreams', () => {
beforeEach(() => {
// this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL')
// spyOn(MockItem, 'getFiles').and.returnValue(observableOf([]));
}); });
tick();
expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_technical_report_institution',
content: 'Mock Publisher'
});
}));
it('processRemoteData should not produce an EmptyError', fakeAsync(() => { it('other navigation should add title and description', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); (translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
spyOn(metadataService, 'processRemoteData').and.callThrough(); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
title: 'Dummy Title',
description: 'This is a dummy item component for testing!'
}
}
});
tick();
expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title');
expect(meta.addTag).toHaveBeenCalledWith({
property: 'title',
content: 'DSpace :: Dummy Title'
});
expect(meta.addTag).toHaveBeenCalledWith({
property: 'description',
content: 'This is a dummy item component for testing!'
});
}));
describe(`listenForRouteChange`, () => {
it(`should call processRouteChange`, fakeAsync(() => {
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
metadataService.listenForRouteChange();
tick(); tick();
expect(metadataService.processRemoteData).not.toThrow(new EmptyError()); expect((metadataService as any).processRouteChange).toHaveBeenCalled();
}));
it(`should add Generator`, fakeAsync(() => {
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
metadataService.listenForRouteChange();
tick();
expect(meta.addTag).toHaveBeenCalledWith({
property: 'Generator',
content: 'mock-dspace-version'
});
})); }));
}); });
describe('citation_abstract_html_url', () => { describe('citation_abstract_html_url', () => {
it('should use dc.identifier.uri if available', fakeAsync(() => { it('should use dc.identifier.uri if available', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockUri(ItemMock, 'https://ddg.gg'))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')),
}
}
});
tick(); tick();
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual('https://ddg.gg'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_abstract_html_url',
content: 'https://ddg.gg'
});
})); }));
it('should use current route as fallback', fakeAsync(() => { it('should use current route as fallback', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockUri(ItemMock))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)),
}
}
});
tick(); tick();
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual('https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_abstract_html_url',
content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357'
});
})); }));
}); });
describe('citation_*_institution / citation_publisher', () => { describe('citation_*_institution / citation_publisher', () => {
it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => { it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Thesis')))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
}
}
});
tick(); tick();
expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher'); expect(meta.addTag).toHaveBeenCalledWith({
expect(tagStore.get('citation_technical_report_institution')).toBeFalsy(); property: 'citation_dissertation_institution',
expect(tagStore.get('citation_publisher')).toBeFalsy(); content: 'Mock Publisher'
});
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_technical_report_institution' }));
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_publisher' }));
})); }));
it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => { it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Technical Report')))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
}
}
});
tick(); tick();
expect(tagStore.get('citation_dissertation_institution')).toBeFalsy(); expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_dissertation_institution' }));
expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher'); expect(meta.addTag).toHaveBeenCalledWith({
expect(tagStore.get('citation_publisher')).toBeFalsy(); property: 'citation_technical_report_institution',
content: 'Mock Publisher'
});
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_publisher' }));
})); }));
it('should use citation_publisher for other item types', fakeAsync(() => { it('should use citation_publisher for other item types', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Some Other Type')))); (metadataService as any).processRouteChange({
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); data: {
value: {
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))),
}
}
});
tick(); tick();
expect(tagStore.get('citation_dissertation_institution')).toBeFalsy(); expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_dissertation_institution' }));
expect(tagStore.get('citation_technical_report_institution')).toBeFalsy(); expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_technical_report_institution' }));
expect(tagStore.get('citation_publisher')[0].content).toEqual('Mock Publisher'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_publisher',
content: 'Mock Publisher'
});
})); }));
}); });
describe('citation_pdf_url', () => { describe('citation_pdf_url', () => {
it('should link to primary Bitstream URL regardless of format', fakeAsync(() => { it('should link to primary Bitstream URL regardless of format', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3));
bundleDataService.findByItemAndName.and.returnValue(mockBundleRD$([], MockBitstream3));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); (metadataService as any).processRouteChange({
data: {
value: {
dso: createSuccessfulRemoteDataObject(ItemMock),
}
}
});
tick(); tick();
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_pdf_url',
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
});
})); }));
describe('no primary Bitstream', () => { describe('no primary Bitstream', () => {
it('should link to first and only Bitstream regardless of format', fakeAsync(() => { it('should link to first and only Bitstream regardless of format', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
bundleDataService.findByItemAndName.and.returnValue(mockBundleRD$([MockBitstream3]));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); (metadataService as any).processRouteChange({
data: {
value: {
dso: createSuccessfulRemoteDataObject(ItemMock),
}
}
});
tick(); tick();
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_pdf_url',
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
});
})); }));
it('should link to first Bitstream with allowed format', fakeAsync(() => { it('should link to first Bitstream with allowed format', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1]; const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1];
bundleDataService.findByItemAndName.and.returnValue(mockBundleRD$(bitstreams)); (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
bitstreamDataService.findAllByHref.and.returnValues( (bitstreamDataService.findAllByHref as jasmine.Spy).and.returnValues(
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)), ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
); );
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); (metadataService as any).processRouteChange({
data: {
value: {
dso: createSuccessfulRemoteDataObject(ItemMock),
}
}
});
tick(); tick();
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download'); expect(meta.addTag).toHaveBeenCalledWith({
property: 'citation_pdf_url',
content: 'https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download'
});
})); }));
}); });
}); });
const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => {
return createSuccessfulRemoteDataObject$(mockItem);
};
const mockType = (mockItem: Item, type: string): Item => { const mockType = (mockItem: Item, type: string): Item => {
const typedMockItem = Object.assign(new Item(), mockItem) as Item; const typedMockItem = Object.assign(new Item(), mockItem) as Item;
typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[]; typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[];

View File

@@ -6,11 +6,10 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable, of as observableOf, EMPTY } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, of as observableOf, EMPTY } from 'rxjs';
import { distinctUntilKeyChanged, filter, map, take, switchMap, expand } from 'rxjs/operators'; import { filter, map, take, switchMap, expand } from 'rxjs/operators';
import { hasValue, hasNoValue } from '../../shared/empty.util'; import { hasValue, hasNoValue } from '../../shared/empty.util';
import { DSONameService } from '../breadcrumbs/dso-name.service'; import { DSONameService } from '../breadcrumbs/dso-name.service';
import { CacheableObject } from '../cache/object-cache.reducer';
import { BitstreamDataService } from '../data/bitstream-data.service'; import { BitstreamDataService } from '../data/bitstream-data.service';
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
@@ -35,11 +34,9 @@ import { HardRedirectService } from '../services/hard-redirect.service';
@Injectable() @Injectable()
export class MetadataService { export class MetadataService {
private initialized: boolean;
private tagStore: Map<string, MetaDefinition[]>; private tagStore: Map<string, MetaDefinition[]>;
private currentObject: BehaviorSubject<DSpaceObject>; private currentObject: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined);
/** /**
* When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream), * When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream),
@@ -70,11 +67,13 @@ export class MetadataService {
) { ) {
// TODO: determine what open graph meta tags are needed and whether // TODO: determine what open graph meta tags are needed and whether
// the differ per route. potentially add image based on DSpaceObject // the differ per route. potentially add image based on DSpaceObject
this.initialized = false;
this.tagStore = new Map<string, MetaDefinition[]>(); this.tagStore = new Map<string, MetaDefinition[]>();
} }
public listenForRouteChange(): void { public listenForRouteChange(): void {
// This never changes, set it only once
this.setGenerator();
this.router.events.pipe( this.router.events.pipe(
filter((event) => event instanceof NavigationEnd), filter((event) => event instanceof NavigationEnd),
map(() => this.router.routerState.root), map(() => this.router.routerState.root),
@@ -86,22 +85,9 @@ export class MetadataService {
}); });
} }
public processRemoteData(remoteData: Observable<RemoteData<CacheableObject>>): void {
remoteData.pipe(map((rd: RemoteData<CacheableObject>) => rd.payload),
filter((co: CacheableObject) => hasValue(co)),
take(1))
.subscribe((dspaceObject: DSpaceObject) => {
if (!this.initialized) {
this.initialize(dspaceObject);
}
this.currentObject.next(dspaceObject);
});
}
private processRouteChange(routeInfo: any): void { private processRouteChange(routeInfo: any): void {
if (routeInfo.params.value.id === undefined) { this.clearMetaTags();
this.clearMetaTags();
}
if (routeInfo.data.value.title) { if (routeInfo.data.value.title) {
const titlePrefix = this.translate.get('repository.title.prefix'); const titlePrefix = this.translate.get('repository.title.prefix');
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value); const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
@@ -116,15 +102,10 @@ export class MetadataService {
}); });
} }
this.setGenerator(); if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) {
} this.currentObject.next(routeInfo.data.value.dso.payload);
this.setDSOMetaTags();
private initialize(dspaceObject: DSpaceObject): void { }
this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject);
this.currentObject.asObservable().pipe(distinctUntilKeyChanged('uuid')).subscribe(() => {
this.setMetaTags();
});
this.initialized = true;
} }
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
@@ -134,9 +115,7 @@ export class MetadataService {
return route; return route;
} }
private setMetaTags(): void { private setDSOMetaTags(): void {
this.clearMetaTags();
this.setTitleTag(); this.setTitleTag();
this.setDescriptionTag(); this.setDescriptionTag();
@@ -415,7 +394,7 @@ export class MetadataService {
*/ */
private setGenerator(): void { private setGenerator(): void {
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => { this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
this.addMetaTag('Generator', root.dspaceVersion); this.meta.addTag({ property: 'Generator', content: root.dspaceVersion });
}); });
} }