From 9ee8848bfe4dcc90d1b1429d655c2a5418958b25 Mon Sep 17 00:00:00 2001 From: Marsa Haoua Date: Fri, 24 Apr 2026 16:41:48 +0200 Subject: [PATCH] Fix #12303: Evaluating IIIF metadata values dspace.iiif.enabled and iiif.search.enabled correctly (cherry picked from commit 8007da9cbd152e2ee1db7fba00fb8ab597a68831) --- .../mirador-viewer.component.html | 14 +- .../mirador-viewer.component.spec.ts | 359 ++++++++++-------- .../mirador-viewer.component.ts | 9 + .../mirador-viewer/mirador-viewer.service.ts | 23 +- .../item-types/shared/item-iiif-utils.ts | 4 +- 5 files changed, 236 insertions(+), 173 deletions(-) diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.html b/src/app/item-page/mirador-viewer/mirador-viewer.component.html index 7f2d0c0aac3..93e8d1cd1ef 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.html +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.html @@ -1,8 +1,10 @@ -

{{'iiifviewer.fullscreen.notice' | translate}}

-@if (!isViewerAvailable) { -

{{viewerMessage}}

-} -@if (isViewerAvailable) { - +@if (isIiifEnabled$ | async) { +

{{'iiifviewer.fullscreen.notice' | translate}}

+ @if (!isViewerAvailable) { +

{{viewerMessage}}

+ } + @if (isViewerAvailable) { + + } } diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts index 93a9cbca462..4aef2a1a46f 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts @@ -4,14 +4,22 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule, } from '@ngx-translate/core'; import { of } from 'rxjs'; +import { + skip, + take, + toArray, +} from 'rxjs/operators'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BundleDataService } from '../../core/data/bundle-data.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { Item } from '../../core/shared/item.model'; import { MetadataMap } from '../../core/shared/metadata.models'; import { HostWindowService } from '../../shared/host-window.service'; @@ -23,14 +31,6 @@ import { MiradorViewerComponent } from './mirador-viewer.component'; import { MiradorViewerService } from './mirador-viewer.service'; -function getItem(metadata: MetadataMap) { - return Object.assign(new Item(), { - bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), - metadata: metadata, - relationships: createRelationshipsObservable(), - }); -} - const noMetadata = new MetadataMap(); const mockHostWindowService = { @@ -38,58 +38,100 @@ const mockHostWindowService = { widthCategory: of(true), }; -describe('MiradorViewerComponent with search', () => { - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); +let comp: MiradorViewerComponent; +let fixture: ComponentFixture; +let configurationDataService: jasmine.SpyObj; +let viewerService: jasmine.SpyObj; - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(true); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ +function getItem(metadata: MetadataMap) { + return Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: metadata, + relationships: createRelationshipsObservable(), + }); +} + +function setupTestBed(overrides?: { + showEmbedded?: boolean; + imageCount?: number; + iiifEnabled?: boolean; +}) { + configurationDataService = jasmine.createSpyObj('ConfigurationDataService', [ + 'findByPropertyName', + ]); + + viewerService = jasmine.createSpyObj('MiradorViewerService', [ + 'showEmbeddedViewer', + 'getImageCount', + 'isIiifEnabled', + ]); + + viewerService.showEmbeddedViewer.and.returnValue(overrides?.showEmbedded ?? true); + viewerService.getImageCount.and.returnValue(of(overrides?.imageCount ?? 1)); + viewerService.isIiifEnabled.and.returnValue(of(overrides?.iiifEnabled ?? true)); + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - ], - }, - }).compileComponents(); + }), + MiradorViewerComponent, + ], + providers: [ + { provide: BitstreamDataService, useValue: {} }, + { provide: BundleDataService, useValue: {} }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: HostWindowService, useValue: mockHostWindowService }, + { provide: MiradorViewerService, useValue: viewerService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }); +} + +function createComponent(options?: { + searchable?: boolean; + object?: any; +}) { + fixture = TestBed.createComponent(MiradorViewerComponent); + comp = fixture.componentInstance; + comp.object = options?.object ?? getItem(noMetadata); + comp.searchable = options?.searchable ?? false; + fixture.detectChanges(); +} + +function getViewerSrc(): string { + return fixture.debugElement.query(By.css('#mirador-viewer')) + .nativeElement.src; +} + +describe('MiradorViewerComponent with search', () => { + beforeEach(waitForAsync(() => { + setupTestBed(); + TestBed.compileComponents(); })); describe('searchable item', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - comp.searchable = true; - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent({ searchable: true }); + }); it('should set multi property to true', (() => { expect(comp.multi).toBe(true); })); - it('should set url "multi" param to true', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).toContain('multi=true'); - })); + it('should set url "multi" param to true', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).toContain('multi=true'); + }); - it('should set url "searchable" param to true', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).toContain('searchable=true'); - })); + it('should set url "searchable" param to true', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).toContain('searchable=true'); + }); it('should not call mirador service image count', () => { expect(viewerService.getImageCount).not.toHaveBeenCalled(); @@ -99,108 +141,52 @@ describe('MiradorViewerComponent with search', () => { }); describe('MiradorViewerComponent with multiple images', () => { - - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(true); - viewerService.getImageCount.and.returnValue(of(2)); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - ], - }, - }).compileComponents(); + setupTestBed({ imageCount: 2 }); + TestBed.compileComponents(); })); describe('non-searchable item with multiple images', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - comp.searchable = false; - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent(); + }); - it('should set url "multi" param to true', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).toContain('multi=true'); - })); + it('should set url "multi" param to true', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).toContain('multi=true'); + }); it('should call mirador service image count', () => { expect(viewerService.getImageCount).toHaveBeenCalled(); }); - it('should omit "searchable" param from url', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).not.toContain('searchable=true'); - })); + it('should omit "searchable" param from url', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).not.toContain('searchable=true'); + }); }); }); describe('MiradorViewerComponent with a single image', () => { - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(true); - viewerService.getImageCount.and.returnValue(of(1)); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - ], - }, - }).compileComponents(); + setupTestBed({ imageCount: 1 }); + TestBed.compileComponents(); })); describe('single image viewer', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent(); + }); - it('should omit "multi" param', (() => { - const value = fixture.debugElement - .nativeElement.querySelector('#mirador-viewer').src; - expect(value).not.toContain('multi=false'); - })); + it('should omit "multi" param', async () => { + await fixture.whenStable(); + fixture.detectChanges(); + expect(getViewerSrc()).not.toContain('multi=false'); + }); it('should call mirador service image count', () => { expect(viewerService.getImageCount).toHaveBeenCalled(); @@ -211,54 +197,99 @@ describe('MiradorViewerComponent with a single image', () => { }); describe('MiradorViewerComponent in development mode', () => { - let comp: MiradorViewerComponent; - let fixture: ComponentFixture; - const viewerService = jasmine.createSpyObj('MiradorViewerService', ['showEmbeddedViewer', 'getImageCount']); - beforeEach(waitForAsync(() => { - viewerService.showEmbeddedViewer.and.returnValue(false); - viewerService.getImageCount.and.returnValue(of(1)); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MiradorViewerComponent], - providers: [ - { provide: BitstreamDataService, useValue: {} }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MiradorViewerComponent, { - set: { - providers: [ - { provide: MiradorViewerService, useValue: viewerService }, - { provide: BundleDataService, useValue: {} }, - { provide: HostWindowService, useValue: mockHostWindowService }, - ], - }, - }).compileComponents(); + setupTestBed({ + showEmbedded: false, + imageCount: 1, + }); + TestBed.compileComponents(); })); describe('embedded viewer', () => { - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(MiradorViewerComponent); - comp = fixture.componentInstance; - comp.object = getItem(noMetadata); - fixture.detectChanges(); - })); + beforeEach(() => { + createComponent(); + }); - it('should not embed the viewer', (() => { + it('should not embed the viewer', async () => { + await fixture.whenStable(); + fixture.detectChanges(); const value = fixture.debugElement .nativeElement.querySelector('#mirador-viewer'); expect(value).toBeNull(); - })); + }); - it('should show message', (() => { + it('should show message', async () => { + await fixture.whenStable(); + fixture.detectChanges(); const value = fixture.debugElement .nativeElement.querySelector('#viewer-message'); expect(value).not.toBeNull(); - })); + }); }); }); + + +describe('MiradorViewerService whether IIIF is enabled in the repository', () => { + let miradorViewerService: MiradorViewerService; + function mockIiifEnabled(values: string[]): void { + configurationDataService.findByPropertyName.and.returnValue( + createSuccessfulRemoteDataObject$( + Object.assign(new ConfigurationProperty(), { + name: 'iiif.enabled', + values, + }), + ), + ); + } + beforeEach(() => { + configurationDataService = jasmine.createSpyObj('ConfigurationDataService', [ + 'findByPropertyName', + ]); + miradorViewerService = new MiradorViewerService(); + }); + describe('isIiifEnabled', () => { + it('should return false initially and then true', (done) => { + mockIiifEnabled(['true']); + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(take(2), toArray()) + .subscribe((results) => { + expect(results).toEqual([false, true]); + done(); + }); + }); + + it('should return true when iiif.enabled is true', (done) => { + mockIiifEnabled(['true']); + + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(skip(1)) + .subscribe((enabled) => { + expect(enabled).toBeTrue(); + expect(configurationDataService.findByPropertyName) + .toHaveBeenCalledWith('iiif.enabled'); + done(); + }); + }); + + it('should return false when iiif.enabled is false', (done) => { + mockIiifEnabled(['false']); + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(skip(1)) + .subscribe((enabled) => { + expect(enabled).toBeFalse(); + done(); + }); + }); + + it('should return false when configuration value is missing', (done) => { + mockIiifEnabled([]); + miradorViewerService.isIiifEnabled(configurationDataService) + .pipe(skip(1)) + .subscribe((enabled) => { + expect(enabled).toBeFalse(); + done(); + }); + }); + }); +}); diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts index 44284832e7d..06b784b0355 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts @@ -27,6 +27,7 @@ import { import { environment } from '../../../environments/environment'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BundleDataService } from '../../core/data/bundle-data.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { Item } from '../../core/shared/item.model'; import { HostWindowService, @@ -63,6 +64,11 @@ export class MiradorViewerComponent implements OnInit { */ isViewerAvailable = true; + /** + * Check if IIIF is enabled in the repository. + */ + isIiifEnabled$: Observable; + /** * The url for the iframe. */ @@ -85,6 +91,7 @@ export class MiradorViewerComponent implements OnInit { private bitstreamDataService: BitstreamDataService, private bundleDataService: BundleDataService, private hostWindowService: HostWindowService, + private configurationDataService: ConfigurationDataService, @Inject(PLATFORM_ID) private platformId: any) { } @@ -165,5 +172,7 @@ export class MiradorViewerComponent implements OnInit { ); } } + // Set the property whether IIIF is enabled in the repository + this.isIiifEnabled$ = this.viewerService.isIiifEnabled(this.configurationDataService); } } diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.service.ts b/src/app/item-page/mirador-viewer/mirador-viewer.service.ts index dc68075c4d9..77cfd2af7da 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.service.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.service.ts @@ -8,18 +8,23 @@ import { last, map, mergeMap, + startWith, switchMap, } from 'rxjs/operators'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BundleDataService } from '../../core/data/bundle-data.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { Bitstream } from '../../core/shared/bitstream.model'; import { BitstreamFormat } from '../../core/shared/bitstream-format.model'; import { Bundle } from '../../core/shared/bundle.model'; import { Item } from '../../core/shared/item.model'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../core/shared/operators'; import { followLink, FollowLinkConfig, @@ -40,6 +45,22 @@ export class MiradorViewerService { return !isDevMode(); } + /** + * Returns observable of boolean whether IIIF is enabled in the repository. + * The default value is false. + * @param configurationDataService + * @returns the configuration value of iiif.enabled + */ + isIiifEnabled (configurationDataService: ConfigurationDataService): Observable { + return configurationDataService.findByPropertyName('iiif.enabled') + .pipe(getFirstSucceededRemoteDataPayload(), + map((configurationProperty) => + configurationProperty.values?.[0] === 'true', + ), + startWith(false), + ); + } + /** * Returns observable of the number of images found in eligible IIIF bundles. Checks * the mimetype of the first 5 bitstreams in each bundle. diff --git a/src/app/item-page/simple/item-types/shared/item-iiif-utils.ts b/src/app/item-page/simple/item-types/shared/item-iiif-utils.ts index a39b345bf05..0c073272fe9 100644 --- a/src/app/item-page/simple/item-types/shared/item-iiif-utils.ts +++ b/src/app/item-page/simple/item-types/shared/item-iiif-utils.ts @@ -13,12 +13,12 @@ import { RouteService } from '../../../../core/services/route.service'; import { Item } from '../../../../core/shared/item.model'; export const isIiifEnabled = (item: Item) => { - return !!item.firstMetadataValue('dspace.iiif.enabled'); + return String(item.firstMetadataValue('dspace.iiif.enabled')?.valueOf?.() || '').trim().toLowerCase() === 'true'; }; export const isIiifSearchEnabled = (item: Item) => { - return !!item.firstMetadataValue('iiif.search.enabled'); + return String(item.firstMetadataValue('iiif.search.enabled')?.valueOf?.() || '').trim().toLowerCase() === 'true'; };