diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index cc1f1679b..686f083da 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -173,11 +173,13 @@ describe('AppComponent', () => { expect(fixture.nativeElement.querySelector('.maintenance-overlay')).toBeFalsy(); }); - it('should not subscribe to router events on server', () => { + it('should not dispatch user session or subscribe to router events on server', () => { setup({ isBrowser: false }); routerBuilder.emit(new ResolveStart(1, '/next', '/next', {} as any)); routerBuilder.emit(new NavigationEnd(5, '/x', '/x')); fixture.detectChanges(); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(maintenanceModeServiceMock.checkOnce).not.toHaveBeenCalled(); expect(loaderServiceMock.show).not.toHaveBeenCalled(); expect(loaderServiceMock.hide).not.toHaveBeenCalled(); expect(gtmServiceMock.pushTag).not.toHaveBeenCalled(); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 932d7bd31..2d9c888fa 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -53,16 +53,15 @@ export class AppComponent implements OnInit { } ngOnInit(): void { - this.actions - .getCurrentUser() - .pipe( - takeUntilDestroyed(this.destroyRef), - switchMap(() => this.actions.getEmails()) - ) - .subscribe(); - if (this.isBrowser) { - // `GetCurrentUser` short-circuits on a cached user, so it cannot be relied on to surface an outage. + this.actions + .getCurrentUser() + .pipe( + takeUntilDestroyed(this.destroyRef), + switchMap(() => this.actions.getEmails()) + ) + .subscribe(); + this.maintenanceModeService.checkOnce(); this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { diff --git a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts index 4a83b2f36..c597ed8a0 100644 --- a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts @@ -73,11 +73,11 @@ describe('MaintenanceBannerComponent', () => { expect(component.maintenance()).toEqual(activeMaintenance); }); - it('should fetch maintenance on server without cookie check', () => { + it('should not check cookie or fetch maintenance on server', () => { setup({ isBrowser: false, maintenance: activeMaintenance }); expect(cookieServiceMock.check).not.toHaveBeenCalled(); - expect(maintenanceServiceMock.fetchMaintenanceStatus).toHaveBeenCalledTimes(1); - expect(component.maintenance()).toEqual(activeMaintenance); + expect(maintenanceServiceMock.fetchMaintenanceStatus).not.toHaveBeenCalled(); + expect(component.maintenance()).toBeNull(); }); it('should dismiss and persist cookie in browser', () => { @@ -89,7 +89,7 @@ describe('MaintenanceBannerComponent', () => { }); it('should dismiss without setting cookie on server', () => { - setup({ isBrowser: false, maintenance: activeMaintenance }); + setup({ isBrowser: false }); component.dismiss(); expect(cookieServiceMock.set).not.toHaveBeenCalled(); expect(component.dismissed()).toBe(true); diff --git a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts index b2980488b..70cf4b293 100644 --- a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts +++ b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts @@ -65,10 +65,12 @@ export class MaintenanceBannerComponent implements OnInit { * - If not dismissed, triggers a fetch of current maintenance status */ ngOnInit(): void { - if (isPlatformBrowser(this.platformId)) { - this.dismissed.set(this.cookies.check(this.cookieName)); + if (!isPlatformBrowser(this.platformId)) { + return; } + this.dismissed.set(this.cookies.check(this.cookieName)); + if (!this.dismissed()) { this.fetchMaintenanceStatus(); } diff --git a/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts b/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts index 0d54a92f3..ff421ca22 100644 --- a/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts @@ -4,7 +4,7 @@ import { MockProvider } from 'ng-mocks'; import { BehaviorSubject } from 'rxjs'; -import { signal, WritableSignal } from '@angular/core'; +import { PLATFORM_ID, signal, WritableSignal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BannerModel } from '@core/components/osf-banners/models/banner.model'; @@ -24,43 +24,51 @@ describe('Component: Scheduled Banner', () => { const currentBannerSignal: WritableSignal = signal(null); let isMobile$: BehaviorSubject; - beforeEach(() => { + function setup(overrides: { isBrowser?: boolean } = {}) { isMobile$ = new BehaviorSubject(false); + currentBannerSignal.set(null); TestBed.configureTestingModule({ imports: [ScheduledBannerComponent], providers: [ provideOSFCore(), MockProvider(IS_XSMALL, isMobile$), + MockProvider(PLATFORM_ID, overrides.isBrowser === false ? 'server' : 'browser'), provideMockStore({ signals: [{ selector: BannersSelector.getCurrentBanner, value: currentBannerSignal }], }), ], }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(ScheduledBannerComponent); component = fixture.componentInstance; fixture.detectChanges(); - - store = TestBed.inject(Store); - }); + } it('should create the component', () => { + setup(); expect(component).toBeInstanceOf(ScheduledBannerComponent); }); - it('should dispatch FetchCurrentScheduledBanner on init', () => { - const dispatchSpy = vi.spyOn(store, 'dispatch'); - component.ngOnInit(); - expect(dispatchSpy).toHaveBeenCalledWith(new GetCurrentScheduledBanner()); + it('should dispatch GetCurrentScheduledBanner on init in browser', () => { + setup(); + expect(store.dispatch).toHaveBeenCalledWith(new GetCurrentScheduledBanner()); + }); + + it('should not dispatch GetCurrentScheduledBanner on server', () => { + setup({ isBrowser: false }); + expect(store.dispatch).not.toHaveBeenCalled(); }); it('should return false if no banner is set', () => { + setup(); currentBannerSignal.set(null); expect(component.shouldShowBanner()).toBe(false); }); it('should return true if current time is within banner window', () => { + setup(); const showBanner = component.shouldShowBanner; expect(showBanner()).toBeFalsy(); @@ -87,6 +95,7 @@ describe('Component: Scheduled Banner', () => { }); it('should return false if current time is outside the banner window', () => { + setup(); const now = new Date(); currentBannerSignal.set( Object({ @@ -104,6 +113,7 @@ describe('Component: Scheduled Banner', () => { }); it('should reflect the isMobile signal', () => { + setup(); const now = new Date(); currentBannerSignal.set( Object({ diff --git a/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.ts b/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.ts index c2761b236..742359b6d 100644 --- a/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.ts +++ b/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.ts @@ -1,6 +1,7 @@ import { Store } from '@ngxs/store'; -import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, OnInit, PLATFORM_ID } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { IS_XSMALL } from '@osf/shared/helpers/breakpoints.tokens'; @@ -27,6 +28,7 @@ export class ScheduledBannerComponent implements OnInit { * Injected NGXS Store instance used to dispatch actions and select state. */ private readonly store = inject(Store); + private readonly platformId = inject(PLATFORM_ID); /** * Reactive signal of the currently scheduled banner fetched from the NGXS store. @@ -44,7 +46,9 @@ export class ScheduledBannerComponent implements OnInit { * when the component initializes. */ ngOnInit(): void { - this.store.dispatch(new GetCurrentScheduledBanner()); + if (isPlatformBrowser(this.platformId)) { + this.store.dispatch(new GetCurrentScheduledBanner()); + } } /** diff --git a/src/app/core/interceptors/error.interceptor.spec.ts b/src/app/core/interceptors/error.interceptor.spec.ts index a082c289d..74a78140d 100644 --- a/src/app/core/interceptors/error.interceptor.spec.ts +++ b/src/app/core/interceptors/error.interceptor.spec.ts @@ -113,6 +113,18 @@ describe('errorInterceptor', () => { expect(toastServiceMock.showError).toHaveBeenCalledWith('server failed'); }); + it('should hide loader without toast on server platform', async () => { + setup('server'); + const request = createRequest('/api/v2/nodes/abc'); + const error = new HttpErrorResponse({ status: 500, error: { message: 'server failed' }, url: request.url }); + + const caught = await runInterceptor(request, error); + + expect(caught?.status).toBe(500); + expect(loaderServiceMock.hide).toHaveBeenCalled(); + expect(toastServiceMock.showError).not.toHaveBeenCalled(); + }); + it('should rethrow 409 without toast or loader handling', async () => { setup(); const request = createRequest('/api/v2/nodes/abc'); diff --git a/src/app/core/interceptors/error.interceptor.ts b/src/app/core/interceptors/error.interceptor.ts index 4a620df46..127f18219 100644 --- a/src/app/core/interceptors/error.interceptor.ts +++ b/src/app/core/interceptors/error.interceptor.ts @@ -105,7 +105,9 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { loaderService.hide(); - toastService.showError(errorMessage); + if (isPlatformBrowser(platformId)) { + toastService.showError(errorMessage); + } return throwError(() => error); })