From 6d9f6d7cea1f19a98d95aeb0b6b022ef1e186b58 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Fri, 30 Jan 2026 10:46:56 +0100 Subject: [PATCH 1/6] Show 404 error for missing static pages Updated StaticPageComponent to display an inline 404 error message when a static page is not found, matching the PageNotFoundComponent design. Added translations for the new 404 messages in both English and Czech, and set the server response status to 404 for SSR. Removed legacy error page loading logic and updated tests to mock the new ServerResponseService dependency. --- .../static-page/static-page.component.html | 15 ++++++++++- .../static-page/static-page.component.spec.ts | 6 +++++ src/app/static-page/static-page.component.ts | 27 +++++++------------ src/assets/i18n/cs.json5 | 6 +++++ src/assets/i18n/en.json5 | 6 +++++ 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/app/static-page/static-page.component.html b/src/app/static-page/static-page.component.html index 99b85f71fb9..87056f08ded 100644 --- a/src/app/static-page/static-page.component.html +++ b/src/app/static-page/static-page.component.html @@ -1,3 +1,16 @@ -
+ +
+ + +
+

404

+

{{"static-page.404.page-not-found" | translate}}

+
+

{{"static-page.404.help" | translate}}

+
+

+ {{"static-page.404.link.home-page" | translate}} +

+
diff --git a/src/app/static-page/static-page.component.spec.ts b/src/app/static-page/static-page.component.spec.ts index 0c461042381..bf966a8f905 100644 --- a/src/app/static-page/static-page.component.spec.ts +++ b/src/app/static-page/static-page.component.spec.ts @@ -9,6 +9,7 @@ import { of } from 'rxjs'; import { APP_CONFIG } from '../../config/app-config.interface'; import { environment } from '../../environments/environment'; import { ClarinSafeHtmlPipe } from '../shared/utils/clarin-safehtml.pipe'; +import { ServerResponseService } from '../core/services/server-response.service'; describe('StaticPageComponent', () => { async function setupTest(html: string, restBase?: string) { @@ -17,6 +18,10 @@ describe('StaticPageComponent', () => { getHmtlContentByPathAndLocale: Promise.resolve(html) }); + const responseService = jasmine.createSpyObj('responseService', { + setNotFound: null + }); + const appConfig = { ...environment, ui: { @@ -37,6 +42,7 @@ describe('StaticPageComponent', () => { providers: [ { provide: HtmlContentService, useValue: htmlContentService }, { provide: Router, useValue: new RouterMock() }, + { provide: ServerResponseService, useValue: responseService }, { provide: APP_CONFIG, useValue: appConfig } ] }).compileComponents(); diff --git a/src/app/static-page/static-page.component.ts b/src/app/static-page/static-page.component.ts index aff3eed0aed..92883b9e156 100644 --- a/src/app/static-page/static-page.component.ts +++ b/src/app/static-page/static-page.component.ts @@ -1,10 +1,11 @@ import { Component, Inject, OnInit } from '@angular/core'; import { HtmlContentService } from '../shared/html-content.service'; -import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { Router } from '@angular/router'; import { isEmpty, isNotEmpty } from '../shared/empty.util'; -import { STATIC_FILES_DEFAULT_ERROR_PAGE_PATH, STATIC_PAGE_PATH } from './static-page-routing-paths'; +import { STATIC_PAGE_PATH } from './static-page-routing-paths'; import { APP_CONFIG, AppConfig } from '../../config/app-config.interface'; +import { ServerResponseService } from '../core/services/server-response.service'; /** * Component which load and show static files from the `static-files` folder. @@ -19,9 +20,11 @@ export class StaticPageComponent implements OnInit { static readonly no_static: string = 'no_static_'; htmlContent: BehaviorSubject = new BehaviorSubject(''); htmlFileName: string; + contentLoaded = false; constructor(private htmlContentService: HtmlContentService, private router: Router, + private responseService: ServerResponseService, @Inject(APP_CONFIG) protected appConfig?: AppConfig) { } async ngOnInit(): Promise { @@ -35,11 +38,13 @@ export class StaticPageComponent implements OnInit { htmlContent = htmlContent.replace(/href="\/server\/oai/gi, 'href="' + oaiUrl); this.htmlContent.next(htmlContent); + this.contentLoaded = true; return; } - // Show error page - await this.loadErrorPage(); + // Content not found - set 404 status for SSR and show inline error + this.responseService.setNotFound(); + this.contentLoaded = false; } /** @@ -123,24 +128,10 @@ export class StaticPageComponent implements OnInit { urlInList = urlInList.filter(n => n); // if length is 1 - html file name wasn't defined. if (isEmpty(urlInList) || urlInList.length === 1) { - void this.loadErrorPage(); return null; } // If the url is too long take just the first string after `/static` prefix. return urlInList[1]?.split('#')?.[0]; } - - /** - * Load `static-files/error.html` - * @private - */ - private async loadErrorPage() { - let errorPage = await firstValueFrom(this.htmlContentService.fetchHtmlContent(STATIC_FILES_DEFAULT_ERROR_PAGE_PATH)); - if (isEmpty(errorPage)) { - console.error('Cannot load error page from the path: ' + STATIC_FILES_DEFAULT_ERROR_PAGE_PATH); - return; - } - this.htmlContent.next(errorPage); - } } diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index cc326adb60f..d53d72ced03 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -23,6 +23,12 @@ "404.link.home-page": "Návrat na domovskou stránku", // "404.page-not-found": "Page not found", "404.page-not-found": "Stránka nebyla nalezena", + // "static-page.404.help": "The static page you requested does not exist. It may have been moved or deleted. You can use the button below to get back to the home page.", + "static-page.404.help": "Požadovaná statická stránka neexistuje. Mohla být přesunuta nebo smazána. Pomocí níže uvedeného tlačítka se můžete vrátit na domovskou stránku.", + // "static-page.404.link.home-page": "Take me to the home page", + "static-page.404.link.home-page": "Návrat na domovskou stránku", + // "static-page.404.page-not-found": "page not found", + "static-page.404.page-not-found": "stránka nebyla nalezena", // "error-page.description.401": "Unauthorized", "error-page.description.401": "Neautorizovaný přístup", // "error-page.description.403": "Forbidden", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b2a27c614a4..164dc0b5388 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -24,6 +24,12 @@ "404.page-not-found": "page not found", + "static-page.404.help": "The static page you requested does not exist. It may have been moved or deleted. You can use the button below to get back to the home page.", + + "static-page.404.link.home-page": "Take me to the home page", + + "static-page.404.page-not-found": "page not found", + "error-page.description.401": "unauthorized", "error-page.description.403": "forbidden", From 1ceae7f94c4eefdd2ca0212e37ead166b06df15f Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Fri, 30 Jan 2026 16:24:44 +0100 Subject: [PATCH 2/6] Improve static page loading and error handling Refactors static page loading to use a new contentState property ('loading', 'found', 'not-found') for clearer UI state management and error handling. Enhances HtmlContentService with cache-busting and robust fallback logic for localized HTML content. Updates tests and template to reflect new state handling and ensures change detection is triggered after content load or error. --- src/app/shared/html-content.service.spec.ts | 93 ++++++++++++++++++ src/app/shared/html-content.service.ts | 63 ++++++++++-- .../static-page/static-page.component.html | 6 +- .../static-page/static-page.component.spec.ts | 96 ++++++++++++++++++- src/app/static-page/static-page.component.ts | 47 +++++---- 5 files changed, 272 insertions(+), 33 deletions(-) create mode 100644 src/app/shared/html-content.service.spec.ts diff --git a/src/app/shared/html-content.service.spec.ts b/src/app/shared/html-content.service.spec.ts new file mode 100644 index 00000000000..00688e62d21 --- /dev/null +++ b/src/app/shared/html-content.service.spec.ts @@ -0,0 +1,93 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { of } from 'rxjs'; +import { HtmlContentService } from './html-content.service'; +import { LocaleService } from '../core/locale/locale.service'; + +describe('HtmlContentService', () => { + let service: HtmlContentService; + let httpClient: jasmine.SpyObj; + let localeService: jasmine.SpyObj; + + beforeEach(() => { + const httpSpy = jasmine.createSpyObj('HttpClient', ['get']); + const localeSpy = jasmine.createSpyObj('LocaleService', ['getCurrentLanguageCode']); + + TestBed.configureTestingModule({ + providers: [ + HtmlContentService, + { provide: HttpClient, useValue: httpSpy }, + { provide: LocaleService, useValue: localeSpy } + ] + }); + + service = TestBed.inject(HtmlContentService); + httpClient = TestBed.inject(HttpClient) as jasmine.SpyObj; + localeService = TestBed.inject(LocaleService) as jasmine.SpyObj; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getHmtlContentByPathAndLocale - fallback mechanism', () => { + it('should return English fallback when Czech translation not found (404)', async () => { + localeService.getCurrentLanguageCode.and.returnValue('cs'); + + const czechContent404 = new HttpResponse({ status: 404, body: '' }); + const englishContent200 = new HttpResponse({ status: 200, body: '
English Content
' }); + + httpClient.get.and.returnValues( + of(czechContent404), + of(czechContent404), + of(englishContent200) + ); + + const result = await service.getHmtlContentByPathAndLocale('license'); + + expect(result).toBe('
English Content
'); + expect(httpClient.get).toHaveBeenCalledTimes(3); + }); + + it('should return localized content when translation exists (200)', async () => { + localeService.getCurrentLanguageCode.and.returnValue('cs'); + + const czechContent200 = new HttpResponse({ status: 200, body: '
Czech Content
' }); + + httpClient.get.and.returnValue(of(czechContent200)); + + const result = await service.getHmtlContentByPathAndLocale('license'); + + expect(result).toBe('
Czech Content
'); + expect(httpClient.get).toHaveBeenCalledTimes(1); + }); + + it('should return English content directly when language is "en"', async () => { + localeService.getCurrentLanguageCode.and.returnValue('en'); + + const englishContent200 = new HttpResponse({ status: 200, body: '
English Content
' }); + + httpClient.get.and.returnValue(of(englishContent200)); + + const result = await service.getHmtlContentByPathAndLocale('license'); + + expect(result).toBe('
English Content
'); + expect(httpClient.get).toHaveBeenCalledTimes(1); + expect(httpClient.get.calls.mostRecent().args[0]).toContain('static-files/license.html'); + expect(httpClient.get.calls.mostRecent().args[0]).not.toContain('/cs/'); + }); + + it('should return undefined when both Czech and English files not found', async () => { + localeService.getCurrentLanguageCode.and.returnValue('cs'); + + const content404 = new HttpResponse({ status: 404, body: '' }); + + httpClient.get.and.returnValue(of(content404)); + + const result = await service.getHmtlContentByPathAndLocale('nonexistent'); + + expect(result).toBeUndefined(); + expect(httpClient.get).toHaveBeenCalledTimes(4); + }); + }); +}); diff --git a/src/app/shared/html-content.service.ts b/src/app/shared/html-content.service.ts index 2b31c7cfb81..ccc1245c6fc 100644 --- a/src/app/shared/html-content.service.ts +++ b/src/app/shared/html-content.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpResponse } from '@angular/common/http'; import { catchError } from 'rxjs/operators'; import { firstValueFrom, of as observableOf } from 'rxjs'; import { HTML_SUFFIX, STATIC_FILES_PROJECT_PATH } from '../static-page/static-page-routing-paths'; -import { isEmpty, isNotEmpty } from './empty.util'; +import { isEmpty } from './empty.util'; import { LocaleService } from '../core/locale/locale.service'; /** @@ -15,13 +15,54 @@ export class HtmlContentService { private localeService: LocaleService,) {} /** - * Load `.html` file content or return empty string if an error. + * Load `.html` file content and return the full response. * @param url file location */ fetchHtmlContent(url: string) { - // catchError -> return empty value. - return this.http.get(url, { responseType: 'text' }).pipe( - catchError(() => observableOf(''))); + return this.http.get(url, { responseType: 'text', observe: 'response' }).pipe( + catchError((error) => observableOf(new HttpResponse({ status: error.status || 0, body: '' })))); + } + + /** + * Append a cache-busting query parameter to force a fresh response. + * @param url file location + */ + private appendCacheBust(url: string): string { + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}cacheBust=${Date.now()}`; + } + + /** + * Load HTML content and handle cached 304 responses. + * @param url file location + */ + private async loadHtmlContent(url: string): Promise { + const response = await firstValueFrom(this.fetchHtmlContent(url)); + if (response.status === 404) { + const refreshed = await firstValueFrom(this.fetchHtmlContent(this.appendCacheBust(url))); + if (refreshed.status === 404) { + return undefined; + } + if (refreshed.status === 200) { + return refreshed.body ?? ''; + } + } + if (response.status === 200) { + if (isEmpty(response.body)) { + const refreshed = await firstValueFrom(this.fetchHtmlContent(this.appendCacheBust(url))); + if (refreshed.status === 404) { + return undefined; + } + if (refreshed.status === 200) { + return refreshed.body ?? ''; + } + } + return response.body ?? ''; + } + if (response.status === 304) { + return response.body ?? ''; + } + return undefined; } /** @@ -40,15 +81,17 @@ export class HtmlContentService { url += isEmpty(language) ? '/' + fileName : '/' + language + '/' + fileName; // Add `.html` suffix to get the current html file url = url.endsWith(HTML_SUFFIX) ? url : url + HTML_SUFFIX; - let potentialContent = await firstValueFrom(this.fetchHtmlContent(url)); - if (isNotEmpty(potentialContent)) { + let potentialContent = await this.loadHtmlContent(url); + if (potentialContent !== undefined) { return potentialContent; } // If the file wasn't find, get the non-translated file from the default package. url = STATIC_FILES_PROJECT_PATH + '/' + fileName; - potentialContent = await firstValueFrom(this.fetchHtmlContent(url)); - if (isNotEmpty(potentialContent)) { + // Add `.html` suffix to match localized request behavior + url = url.endsWith(HTML_SUFFIX) ? url : url + HTML_SUFFIX; + potentialContent = await this.loadHtmlContent(url); + if (potentialContent !== undefined) { return potentialContent; } } diff --git a/src/app/static-page/static-page.component.html b/src/app/static-page/static-page.component.html index 87056f08ded..9d4f2c002e9 100644 --- a/src/app/static-page/static-page.component.html +++ b/src/app/static-page/static-page.component.html @@ -1,10 +1,10 @@ - -
+ +
-
+

404

{{"static-page.404.page-not-found" | translate}}


diff --git a/src/app/static-page/static-page.component.spec.ts b/src/app/static-page/static-page.component.spec.ts index bf966a8f905..f6f161578a8 100644 --- a/src/app/static-page/static-page.component.spec.ts +++ b/src/app/static-page/static-page.component.spec.ts @@ -57,7 +57,6 @@ describe('StaticPageComponent', () => { expect(component).toBeTruthy(); }); - // Load `TEST MESSAGE` it('should load html file content', async () => { const { component } = await setupTest('
TEST MESSAGE
'); await component.ngOnInit(); @@ -107,4 +106,99 @@ describe('StaticPageComponent', () => { expect(component.htmlContent.value).toBe(otherHtml); }); + + describe('contentState behavior', () => { + it('should initialize contentState to "loading"', async () => { + const { component } = await setupTest('
test
'); + expect(component.contentState).toBe('loading'); + }); + + it('should set contentState to "found" when content loads successfully', async () => { + const { component } = await setupTest('
Test Content
'); + await component.ngOnInit(); + expect(component.contentState).toBe('found'); + }); + + it('should set contentState to "not-found" when content is undefined', async () => { + const htmlContentService = jasmine.createSpyObj('htmlContentService', { + getHmtlContentByPathAndLocale: Promise.resolve(undefined) + }); + + const responseService = jasmine.createSpyObj('responseService', { + setNotFound: null + }); + + const appConfig = { + ...environment, + ui: { ...(environment as any).ui, namespace: 'testNamespace' }, + rest: { ...(environment as any).rest } + }; + + await TestBed.configureTestingModule({ + declarations: [ StaticPageComponent, ClarinSafeHtmlPipe ], + imports: [ TranslateModule.forRoot() ], + providers: [ + { provide: HtmlContentService, useValue: htmlContentService }, + { provide: Router, useValue: new RouterMock() }, + { provide: ServerResponseService, useValue: responseService }, + { provide: APP_CONFIG, useValue: appConfig } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(StaticPageComponent); + const component = fixture.componentInstance; + + await component.ngOnInit(); + + expect(component.contentState).toBe('not-found'); + expect(responseService.setNotFound).toHaveBeenCalled(); + }); + }); + + describe('change detection', () => { + it('should call changeDetector.detectChanges() after successful content load', async () => { + const { component } = await setupTest('
test
'); + spyOn(component['changeDetector'], 'detectChanges'); + + await component.ngOnInit(); + + expect(component['changeDetector'].detectChanges).toHaveBeenCalled(); + }); + + it('should call changeDetector.detectChanges() when content not found', async () => { + const htmlContentService = jasmine.createSpyObj('htmlContentService', { + getHmtlContentByPathAndLocale: Promise.resolve(undefined) + }); + + const responseService = jasmine.createSpyObj('responseService', { + setNotFound: null + }); + + const appConfig = { + ...environment, + ui: { ...(environment as any).ui, namespace: 'testNamespace' }, + rest: { ...(environment as any).rest } + }; + + await TestBed.configureTestingModule({ + declarations: [ StaticPageComponent, ClarinSafeHtmlPipe ], + imports: [ TranslateModule.forRoot() ], + providers: [ + { provide: HtmlContentService, useValue: htmlContentService }, + { provide: Router, useValue: new RouterMock() }, + { provide: ServerResponseService, useValue: responseService }, + { provide: APP_CONFIG, useValue: appConfig } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(StaticPageComponent); + const component = fixture.componentInstance; + + spyOn(component['changeDetector'], 'detectChanges'); + + await component.ngOnInit(); + + expect(component['changeDetector'].detectChanges).toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/static-page/static-page.component.ts b/src/app/static-page/static-page.component.ts index 92883b9e156..a5b71151c34 100644 --- a/src/app/static-page/static-page.component.ts +++ b/src/app/static-page/static-page.component.ts @@ -1,8 +1,8 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; import { HtmlContentService } from '../shared/html-content.service'; import { BehaviorSubject } from 'rxjs'; import { Router } from '@angular/router'; -import { isEmpty, isNotEmpty } from '../shared/empty.util'; +import { isEmpty } from '../shared/empty.util'; import { STATIC_PAGE_PATH } from './static-page-routing-paths'; import { APP_CONFIG, AppConfig } from '../../config/app-config.interface'; import { ServerResponseService } from '../core/services/server-response.service'; @@ -20,31 +20,40 @@ export class StaticPageComponent implements OnInit { static readonly no_static: string = 'no_static_'; htmlContent: BehaviorSubject = new BehaviorSubject(''); htmlFileName: string; - contentLoaded = false; + contentState: 'loading' | 'found' | 'not-found' = 'loading'; constructor(private htmlContentService: HtmlContentService, private router: Router, private responseService: ServerResponseService, + private changeDetector: ChangeDetectorRef, @Inject(APP_CONFIG) protected appConfig?: AppConfig) { } async ngOnInit(): Promise { - // Fetch html file name from the url path. `static/some_file.html` - this.htmlFileName = this.getHtmlFileName(); - - let htmlContent = await this.htmlContentService.getHmtlContentByPathAndLocale(this.htmlFileName); - if (isNotEmpty(htmlContent)) { - const restBase = this.appConfig?.rest?.baseUrl; - const oaiUrl = restBase ? new URL('/server/oai', restBase).href : '/server/oai'; - htmlContent = htmlContent.replace(/href="\/server\/oai/gi, 'href="' + oaiUrl); - - this.htmlContent.next(htmlContent); - this.contentLoaded = true; - return; + try { + // Fetch html file name from the url path. `static/some_file.html` + this.htmlFileName = this.getHtmlFileName(); + + let htmlContent = await this.htmlContentService.getHmtlContentByPathAndLocale(this.htmlFileName); + if (htmlContent !== undefined) { + const restBase = this.appConfig?.rest?.baseUrl; + const oaiUrl = restBase ? new URL('/server/oai', restBase).href : '/server/oai'; + htmlContent = htmlContent.replace(/href="\/server\/oai/gi, 'href="' + oaiUrl); + + this.htmlContent.next(htmlContent); + this.contentState = 'found'; + this.changeDetector.detectChanges(); + return; + } + + // Content not found - set 404 status for SSR and show inline error + this.responseService.setNotFound(); + this.contentState = 'not-found'; + this.changeDetector.detectChanges(); + } catch { + this.responseService.setNotFound(); + this.contentState = 'not-found'; + this.changeDetector.detectChanges(); } - - // Content not found - set 404 status for SSR and show inline error - this.responseService.setNotFound(); - this.contentLoaded = false; } /** From 196a1252a68e1c8ee6bb8f91dd923e864f59a291 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Wed, 11 Feb 2026 13:32:06 +0100 Subject: [PATCH 3/6] Use any-cast for changeDetector spies Replace bracket-indexing (component['changeDetector']) with (component as any).changeDetector in static-page.component.spec.ts. This updates three occurrences in the change-detection tests so TypeScript accepts spying on detectChanges without index-signature errors. --- src/app/static-page/static-page.component.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/static-page/static-page.component.spec.ts b/src/app/static-page/static-page.component.spec.ts index f6f161578a8..21788b6eaeb 100644 --- a/src/app/static-page/static-page.component.spec.ts +++ b/src/app/static-page/static-page.component.spec.ts @@ -158,11 +158,11 @@ describe('StaticPageComponent', () => { describe('change detection', () => { it('should call changeDetector.detectChanges() after successful content load', async () => { const { component } = await setupTest('
test
'); - spyOn(component['changeDetector'], 'detectChanges'); + spyOn((component as any).changeDetector, 'detectChanges'); await component.ngOnInit(); - expect(component['changeDetector'].detectChanges).toHaveBeenCalled(); + expect((component as any).changeDetector.detectChanges).toHaveBeenCalled(); }); it('should call changeDetector.detectChanges() when content not found', async () => { @@ -194,11 +194,11 @@ describe('StaticPageComponent', () => { const fixture = TestBed.createComponent(StaticPageComponent); const component = fixture.componentInstance; - spyOn(component['changeDetector'], 'detectChanges'); + spyOn((component as any).changeDetector, 'detectChanges'); await component.ngOnInit(); - expect(component['changeDetector'].detectChanges).toHaveBeenCalled(); + expect((component as any).changeDetector.detectChanges).toHaveBeenCalled(); }); }); }); From 142804833a2f25cc997ec002d749d03b30be7fe0 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Thu, 12 Feb 2026 09:37:05 +0100 Subject: [PATCH 4/6] Make cache-bust deterministic & refactor tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a deterministic cache-bust parameter in HtmlContentService.appendCacheBust (avoid Date.now()) and avoid adding the param if it already exists — this keeps the behavior SSR-safe and cache-friendly. Refactor StaticPageComponent unit tests: allow setupTest to accept undefined HTML, return the mocked responseService, and simplify tests by reusing setupTest to remove duplicated test setup code. --- src/app/shared/html-content.service.ts | 8 ++- .../static-page/static-page.component.spec.ts | 60 ++----------------- 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/src/app/shared/html-content.service.ts b/src/app/shared/html-content.service.ts index ccc1245c6fc..670b0bb95c0 100644 --- a/src/app/shared/html-content.service.ts +++ b/src/app/shared/html-content.service.ts @@ -25,11 +25,17 @@ export class HtmlContentService { /** * Append a cache-busting query parameter to force a fresh response. + * Uses a deterministic value to remain SSR-safe and cache-friendly. * @param url file location */ private appendCacheBust(url: string): string { + const cacheBustParam = 'cacheBust='; + if (url.includes(cacheBustParam)) { + return url; + } const separator = url.includes('?') ? '&' : '?'; - return `${url}${separator}cacheBust=${Date.now()}`; + const cacheBustValue = '1'; + return `${url}${separator}${cacheBustParam}${cacheBustValue}`; } /** diff --git a/src/app/static-page/static-page.component.spec.ts b/src/app/static-page/static-page.component.spec.ts index 21788b6eaeb..a17f2c359a2 100644 --- a/src/app/static-page/static-page.component.spec.ts +++ b/src/app/static-page/static-page.component.spec.ts @@ -12,7 +12,7 @@ import { ClarinSafeHtmlPipe } from '../shared/utils/clarin-safehtml.pipe'; import { ServerResponseService } from '../core/services/server-response.service'; describe('StaticPageComponent', () => { - async function setupTest(html: string, restBase?: string) { + async function setupTest(html: string | undefined, restBase?: string) { const htmlContentService = jasmine.createSpyObj('htmlContentService', { fetchHtmlContent: of(html), getHmtlContentByPathAndLocale: Promise.resolve(html) @@ -49,7 +49,7 @@ describe('StaticPageComponent', () => { const fixture = TestBed.createComponent(StaticPageComponent); const component = fixture.componentInstance; - return { fixture, component, htmlContentService }; + return { fixture, component, htmlContentService, responseService }; } it('should create', async () => { @@ -120,33 +120,7 @@ describe('StaticPageComponent', () => { }); it('should set contentState to "not-found" when content is undefined', async () => { - const htmlContentService = jasmine.createSpyObj('htmlContentService', { - getHmtlContentByPathAndLocale: Promise.resolve(undefined) - }); - - const responseService = jasmine.createSpyObj('responseService', { - setNotFound: null - }); - - const appConfig = { - ...environment, - ui: { ...(environment as any).ui, namespace: 'testNamespace' }, - rest: { ...(environment as any).rest } - }; - - await TestBed.configureTestingModule({ - declarations: [ StaticPageComponent, ClarinSafeHtmlPipe ], - imports: [ TranslateModule.forRoot() ], - providers: [ - { provide: HtmlContentService, useValue: htmlContentService }, - { provide: Router, useValue: new RouterMock() }, - { provide: ServerResponseService, useValue: responseService }, - { provide: APP_CONFIG, useValue: appConfig } - ] - }).compileComponents(); - - const fixture = TestBed.createComponent(StaticPageComponent); - const component = fixture.componentInstance; + const { component, responseService } = await setupTest(undefined); await component.ngOnInit(); @@ -166,33 +140,7 @@ describe('StaticPageComponent', () => { }); it('should call changeDetector.detectChanges() when content not found', async () => { - const htmlContentService = jasmine.createSpyObj('htmlContentService', { - getHmtlContentByPathAndLocale: Promise.resolve(undefined) - }); - - const responseService = jasmine.createSpyObj('responseService', { - setNotFound: null - }); - - const appConfig = { - ...environment, - ui: { ...(environment as any).ui, namespace: 'testNamespace' }, - rest: { ...(environment as any).rest } - }; - - await TestBed.configureTestingModule({ - declarations: [ StaticPageComponent, ClarinSafeHtmlPipe ], - imports: [ TranslateModule.forRoot() ], - providers: [ - { provide: HtmlContentService, useValue: htmlContentService }, - { provide: Router, useValue: new RouterMock() }, - { provide: ServerResponseService, useValue: responseService }, - { provide: APP_CONFIG, useValue: appConfig } - ] - }).compileComponents(); - - const fixture = TestBed.createComponent(StaticPageComponent); - const component = fixture.componentInstance; + const { component } = await setupTest(undefined); spyOn((component as any).changeDetector, 'detectChanges'); From d1a76acbf2191daad2bd7b15fdc38fd733a29eb4 Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Thu, 12 Feb 2026 11:20:04 +0100 Subject: [PATCH 5/6] Add loading indicator and error logging Show a loading spinner while static page content is fetched and add console.error logging on load failures (includes fileName and URL) to aid debugging. Also simplify HtmlContentService by removing a redundant refetch-on-empty response path that caused an extra network request and special-case 404 handling; a 200 now consistently returns the response body (or empty string). UI behavior for not-found responses remains unchanged. --- src/app/shared/html-content.service.ts | 9 --------- src/app/static-page/static-page.component.html | 4 ++++ src/app/static-page/static-page.component.ts | 7 ++++++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/shared/html-content.service.ts b/src/app/shared/html-content.service.ts index 670b0bb95c0..1dfcd3285ac 100644 --- a/src/app/shared/html-content.service.ts +++ b/src/app/shared/html-content.service.ts @@ -54,15 +54,6 @@ export class HtmlContentService { } } if (response.status === 200) { - if (isEmpty(response.body)) { - const refreshed = await firstValueFrom(this.fetchHtmlContent(this.appendCacheBust(url))); - if (refreshed.status === 404) { - return undefined; - } - if (refreshed.status === 200) { - return refreshed.body ?? ''; - } - } return response.body ?? ''; } if (response.status === 304) { diff --git a/src/app/static-page/static-page.component.html b/src/app/static-page/static-page.component.html index 9d4f2c002e9..205dabb7f90 100644 --- a/src/app/static-page/static-page.component.html +++ b/src/app/static-page/static-page.component.html @@ -1,3 +1,7 @@ +
+ +
+
diff --git a/src/app/static-page/static-page.component.ts b/src/app/static-page/static-page.component.ts index a5b71151c34..d38313c18ee 100644 --- a/src/app/static-page/static-page.component.ts +++ b/src/app/static-page/static-page.component.ts @@ -49,7 +49,12 @@ export class StaticPageComponent implements OnInit { this.responseService.setNotFound(); this.contentState = 'not-found'; this.changeDetector.detectChanges(); - } catch { + } catch (error) { + console.error('Static page load error:', { + fileName: this.htmlFileName, + url: this.router.url, + error: error + }); this.responseService.setNotFound(); this.contentState = 'not-found'; this.changeDetector.detectChanges(); From 501500ee190d7c884cff31bb9d33147fa07ce61d Mon Sep 17 00:00:00 2001 From: amadulhaxxani Date: Thu, 12 Feb 2026 11:56:47 +0100 Subject: [PATCH 6/6] Use hourly cache-busting query param Replace the fixed cache-bust value ('1') with a dynamic hourly value computed from Date.now()/3600000. This appends a changing query parameter so responses are refreshed hourly (reducing stale content) while keeping the existing query-separator logic intact. Removed the prior comment about a deterministic SSR-safe value. --- src/app/shared/html-content.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/html-content.service.ts b/src/app/shared/html-content.service.ts index 1dfcd3285ac..1b9c2c4f795 100644 --- a/src/app/shared/html-content.service.ts +++ b/src/app/shared/html-content.service.ts @@ -25,7 +25,6 @@ export class HtmlContentService { /** * Append a cache-busting query parameter to force a fresh response. - * Uses a deterministic value to remain SSR-safe and cache-friendly. * @param url file location */ private appendCacheBust(url: string): string { @@ -34,7 +33,7 @@ export class HtmlContentService { return url; } const separator = url.includes('?') ? '&' : '?'; - const cacheBustValue = '1'; + const cacheBustValue = Math.floor(Date.now() / 3600000).toString(); return `${url}${separator}${cacheBustParam}${cacheBustValue}`; }