Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
17 changes: 8 additions & 9 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,43 +24,51 @@ describe('Component: Scheduled Banner', () => {
const currentBannerSignal: WritableSignal<BannerModel | null> = signal<BannerModel | null>(null);
let isMobile$: BehaviorSubject<boolean>;

beforeEach(() => {
function setup(overrides: { isBrowser?: boolean } = {}) {
isMobile$ = new BehaviorSubject<boolean>(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();

Expand All @@ -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({
Expand All @@ -104,6 +113,7 @@ describe('Component: Scheduled Banner', () => {
});

it('should reflect the isMobile signal', () => {
setup();
const now = new Date();
currentBannerSignal.set(
Object({
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
Expand All @@ -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());
}
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/app/core/interceptors/error.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/interceptors/error.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => {

loaderService.hide();

toastService.showError(errorMessage);
if (isPlatformBrowser(platformId)) {
toastService.showError(errorMessage);
}

return throwError(() => error);
})
Expand Down
Loading