From 66b3888374ed13b3b279d74912b97ac9ec49bb47 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Wed, 10 Sep 2025 16:45:07 -0500 Subject: [PATCH 1/5] feat(eng-8768): add the help-scout to preprints --- src/@types/global.d.ts | 41 ++++++++++ src/app/app.config.ts | 14 +++- src/app/core/factory/window.factory.spec.ts | 32 ++++++++ src/app/core/factory/window.factory.ts | 26 ++++++ .../core/services/help-scout.service.spec.ts | 65 +++++++++++++++ src/app/core/services/help-scout.service.ts | 79 +++++++++++++++++++ .../preprints/preprints.component.spec.ts | 27 +++++-- .../features/preprints/preprints.component.ts | 13 ++- src/testing/osf.testing.module.ts | 9 ++- 9 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 src/app/core/factory/window.factory.spec.ts create mode 100644 src/app/core/factory/window.factory.ts create mode 100644 src/app/core/services/help-scout.service.spec.ts create mode 100644 src/app/core/services/help-scout.service.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 582a45a49..1d6eba8ad 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -1,10 +1,51 @@ import type gapi from 'gapi-script'; // or just `import gapi from 'gapi-script';` +/** + * Extends the global `Window` interface to include additional properties used by the application, + * such as Google APIs (`gapi`, `google.picker`) and the `dataLayer` for analytics or GTM integration. + */ declare global { interface Window { + /** + * Represents the Google API client library (`gapi`) attached to the global window. + * Used for OAuth, Picker API, Drive API, etc. + * + * @see https://developers.google.com/api-client-library/javascript/ + */ gapi: typeof gapi; + + /** + * Contains Google-specific UI services attached to the global window, + * such as the `google.picker` API. + * + * @see https://developers.google.com/picker/docs/ + */ google: { + /** + * Reference to the Google Picker API used for file selection and Drive integration. + */ picker: typeof google.picker; }; + + /** + * Global analytics `dataLayer` object used by Google Tag Manager (GTM). + * Can store custom application metadata for tracking and event push. + * + * @property resourceType - The type of resource currently being viewed (e.g., 'project', 'file', etc.) + * @property loggedIn - Indicates whether the user is currently authenticated. + */ + dataLayer: { + /** + * The type of content or context being viewed (e.g., "project", "node", etc.). + * Optional — may be undefined depending on when or where GTM initializes. + */ + resourceType: string | undefined; + + /** + * Indicates if the current user is authenticated. + * Used for segmenting analytics based on login state. + */ + loggedIn: boolean; + }; } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a606ce22b..c93c66678 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -7,12 +7,19 @@ import { ConfirmationService, MessageService } from 'primeng/api'; import { providePrimeNG } from 'primeng/config'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; +import { + ApplicationConfig, + ErrorHandler, + importProvidersFrom, + PLATFORM_ID, + provideZoneChangeDetection, +} from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; import { STATES } from '@core/constants'; import { APPLICATION_INITIALIZATION_PROVIDER } from '@core/factory/application.initialization.factory'; +import { WINDOW, windowFactory } from '@core/factory/window.factory'; import { provideTranslation } from '@core/helpers'; import { authInterceptor, errorInterceptor, viewOnlyInterceptor } from './core/interceptors'; @@ -49,5 +56,10 @@ export const appConfig: ApplicationConfig = { provide: ErrorHandler, useFactory: () => Sentry.createErrorHandler({ showDialog: false }), }, + { + provide: WINDOW, + useFactory: windowFactory, + deps: [PLATFORM_ID], + }, ], }; diff --git a/src/app/core/factory/window.factory.spec.ts b/src/app/core/factory/window.factory.spec.ts new file mode 100644 index 000000000..0211cf905 --- /dev/null +++ b/src/app/core/factory/window.factory.spec.ts @@ -0,0 +1,32 @@ +// src/app/window.spec.ts +import { isPlatformBrowser } from '@angular/common'; + +import { windowFactory } from './window.factory'; + +jest.mock('@angular/common', () => ({ + isPlatformBrowser: jest.fn(), +})); + +describe('windowFactory', () => { + const mockWindow = globalThis.window; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return window object if platform is browser', () => { + (isPlatformBrowser as jest.Mock).mockReturnValue(true); + + const result = windowFactory('browser'); + expect(isPlatformBrowser).toHaveBeenCalledWith('browser'); + expect(result).toBe(mockWindow); + }); + + it('should return empty object if platform is not browser', () => { + (isPlatformBrowser as jest.Mock).mockReturnValue(false); + + const result = windowFactory('server'); + expect(isPlatformBrowser).toHaveBeenCalledWith('server'); + expect(result).toEqual({}); + }); +}); diff --git a/src/app/core/factory/window.factory.ts b/src/app/core/factory/window.factory.ts new file mode 100644 index 000000000..395524e44 --- /dev/null +++ b/src/app/core/factory/window.factory.ts @@ -0,0 +1,26 @@ +import { isPlatformBrowser } from '@angular/common'; +import { InjectionToken } from '@angular/core'; + +export const WINDOW = new InjectionToken('Global Window Object'); + +/** + * A factory function to provide the global `window` object in Angular. + * + * This is useful for making Angular applications **Universal-compatible** (i.e., supporting server-side rendering). + * It conditionally returns the real `window` only when the code is running in the **browser**, not on the server. + * + * @param platformId - The Angular platform ID token (injected by Angular) that helps detect the execution environment. + * @returns The actual `window` object if running in the browser, otherwise a mock object `{}` for SSR environments. + * + * @see https://angular.io/api/core/PLATFORM_ID + * @see https://angular.io/guide/universal + */ +export function windowFactory(platformId: string): Window | object { + // Check if we're running in the browser (vs server-side) + if (isPlatformBrowser(platformId)) { + return window; + } + + // Return an empty object as a safe fallback during server-side rendering + return {}; +} diff --git a/src/app/core/services/help-scout.service.spec.ts b/src/app/core/services/help-scout.service.spec.ts new file mode 100644 index 000000000..3bb9469dc --- /dev/null +++ b/src/app/core/services/help-scout.service.spec.ts @@ -0,0 +1,65 @@ +import { Store } from '@ngxs/store'; + +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { WINDOW } from '@core/factory/window.factory'; +import { UserSelectors } from '@core/store/user/user.selectors'; + +import { HelpScoutService } from './help-scout.service'; + +describe('HelpScoutService', () => { + let storeMock: Partial; + let service: HelpScoutService; + let mockWindow: any; + const authSignal = signal(false); + + beforeEach(() => { + mockWindow = { + dataLayer: {}, + }; + + storeMock = { + selectSignal: jest.fn().mockImplementation((selector) => { + if (selector === UserSelectors.isAuthenticated) { + return authSignal; + } + return signal(null); // fallback + }), + }; + + TestBed.configureTestingModule({ + providers: [{ provide: WINDOW, useValue: mockWindow }, HelpScoutService, { provide: Store, useValue: storeMock }], + }); + + service = TestBed.inject(HelpScoutService); + }); + + it('should initialize dataLayer with default values', () => { + expect(mockWindow.dataLayer).toEqual({ + loggedIn: false, + resourceType: undefined, + }); + }); + + it('should set the resourceType', () => { + service.setResourceType('project'); + expect(mockWindow.dataLayer.resourceType).toBe('project'); + }); + + it('should unset the resourceType', () => { + service.setResourceType('node'); + service.unsetResourceType(); + expect(mockWindow.dataLayer.resourceType).toBeUndefined(); + }); + + it('should set loggedIn to true or false', () => { + authSignal.set(true); + TestBed.flushEffects(); + expect(mockWindow.dataLayer.loggedIn).toBeTruthy(); + + authSignal.set(false); + TestBed.flushEffects(); + expect(mockWindow.dataLayer.loggedIn).toBeFalsy(); + }); +}); diff --git a/src/app/core/services/help-scout.service.ts b/src/app/core/services/help-scout.service.ts new file mode 100644 index 000000000..1be92f992 --- /dev/null +++ b/src/app/core/services/help-scout.service.ts @@ -0,0 +1,79 @@ +import { Store } from '@ngxs/store'; + +import { effect, inject, Injectable } from '@angular/core'; + +import { WINDOW } from '@core/factory/window.factory'; +import { UserSelectors } from '@osf/core/store/user'; + +/** + * HelpScoutService manages GTM-compatible `dataLayer` state + * related to user authentication and resource type. + * + * This service ensures that specific fields in the global + * `window.dataLayer` object are set correctly for downstream + * tools like Google Tag Manager or HelpScout integrations. + */ +@Injectable({ + providedIn: 'root', +}) +export class HelpScoutService { + /** + * Reference to the global window object, injected via a factory. + * Used to access and manipulate `dataLayer` for tracking. + */ + private window = inject(WINDOW); + + /** + * Angular Store instance used to access application state via NgRx. + * Injected using Angular's `inject()` function. + * + * @private + * @type {Store} + */ + private store = inject(Store); + + /** + * Signal that represents the current authentication state of the user. + * Derived from the NgRx selector `UserSelectors.isAuthenticated`. + * + * Can be used reactively in effects or template bindings to update UI or behavior + * based on whether the user is logged in. + * + * @private + * @type {Signal} + */ + private isAuthenticated = this.store.selectSignal(UserSelectors.isAuthenticated); + + /** + * Initializes the `dataLayer` with default values. + * + * - `loggedIn`: false + * - `resourceType`: undefined + */ + constructor() { + this.window.dataLayer = { + loggedIn: false, + resourceType: undefined, + }; + + effect(() => { + this.window.dataLayer.loggedIn = this.isAuthenticated(); + }); + } + + /** + * Sets the current resource type in the `dataLayer`. + * + * @param resourceType - The name of the resource (e.g., 'project', 'node') + */ + setResourceType(resourceType: string): void { + this.window.dataLayer.resourceType = resourceType; + } + + /** + * Clears the `resourceType` from the `dataLayer`, setting it to `undefined`. + */ + unsetResourceType(): void { + this.window.dataLayer.resourceType = undefined; + } +} diff --git a/src/app/features/preprints/preprints.component.spec.ts b/src/app/features/preprints/preprints.component.spec.ts index f24fc1a9f..a60891d4f 100644 --- a/src/app/features/preprints/preprints.component.spec.ts +++ b/src/app/features/preprints/preprints.component.spec.ts @@ -3,31 +3,48 @@ import { MockProvider } from 'ng-mocks'; import { BehaviorSubject } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideRouter } from '@angular/router'; +import { HelpScoutService } from '@core/services/help-scout.service'; import { IS_WEB } from '@osf/shared/helpers'; import { PreprintsComponent } from './preprints.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; + describe('PreprintsComponent', () => { let component: PreprintsComponent; let fixture: ComponentFixture; let isWebSubject: BehaviorSubject; + let helpScountService: HelpScoutService; beforeEach(async () => { isWebSubject = new BehaviorSubject(true); await TestBed.configureTestingModule({ - imports: [PreprintsComponent], - providers: [provideRouter([]), MockProvider(IS_WEB, isWebSubject)], + imports: [PreprintsComponent, OSFTestingModule], + providers: [ + MockProvider(IS_WEB, isWebSubject), + { + provide: HelpScoutService, + useValue: { + setResourceType: jest.fn(), + unsetResourceType: jest.fn(), + }, + }, + ], }).compileComponents(); + helpScountService = TestBed.inject(HelpScoutService); fixture = TestBed.createComponent(PreprintsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have a default value', () => { + expect(component.isDesktop()).toBeTruthy(); + }); + + it('should called the helpScoutService', () => { + expect(helpScountService.setResourceType).toHaveBeenCalledWith('preprint'); }); }); diff --git a/src/app/features/preprints/preprints.component.ts b/src/app/features/preprints/preprints.component.ts index 86cc62761..8306a32f4 100644 --- a/src/app/features/preprints/preprints.component.ts +++ b/src/app/features/preprints/preprints.component.ts @@ -1,7 +1,8 @@ -import { ChangeDetectionStrategy, Component, HostBinding, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostBinding, inject, OnDestroy } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { RouterOutlet } from '@angular/router'; +import { HelpScoutService } from '@core/services/help-scout.service'; import { IS_WEB } from '@osf/shared/helpers'; @Component({ @@ -11,7 +12,15 @@ import { IS_WEB } from '@osf/shared/helpers'; styleUrl: './preprints.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PreprintsComponent { +export class PreprintsComponent implements OnDestroy { readonly isDesktop = toSignal(inject(IS_WEB)); @HostBinding('class') classes = 'flex flex-1 flex-column w-full'; + + constructor(private helpScoutService: HelpScoutService) { + this.helpScoutService.setResourceType('preprint'); + } + + ngOnDestroy(): void { + this.helpScoutService.unsetResourceType(); + } } diff --git a/src/testing/osf.testing.module.ts b/src/testing/osf.testing.module.ts index a4e376233..c7412daf6 100644 --- a/src/testing/osf.testing.module.ts +++ b/src/testing/osf.testing.module.ts @@ -3,11 +3,13 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { NgModule } from '@angular/core'; +import { NgModule, PLATFORM_ID } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule, provideNoopAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; +import { WINDOW, windowFactory } from '@core/factory/window.factory'; + import { DynamicDialogRefMock } from './mocks/dynamic-dialog-ref.mock'; import { EnvironmentTokenMock } from './mocks/environment.token.mock'; import { StoreMock } from './mocks/store.mock'; @@ -35,6 +37,11 @@ import { TranslationServiceMock } from './mocks/translation.service.mock'; DynamicDialogRefMock, EnvironmentTokenMock, ToastServiceMock, + { + provide: WINDOW, + useFactory: windowFactory, + deps: [PLATFORM_ID], + }, ], }) export class OSFTestingModule {} From 2c55e68309dcccfdd582e489157dea47770acd7f Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Wed, 10 Sep 2025 16:48:17 -0500 Subject: [PATCH 2/5] feat(eng-8768): add the help-scout to projects --- .../preprints/preprints.component.spec.ts | 2 +- .../project/project.component.spec.ts | 36 ++++++++++++++++--- src/app/features/project/project.component.ts | 14 ++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/app/features/preprints/preprints.component.spec.ts b/src/app/features/preprints/preprints.component.spec.ts index a60891d4f..005f147a5 100644 --- a/src/app/features/preprints/preprints.component.spec.ts +++ b/src/app/features/preprints/preprints.component.spec.ts @@ -11,7 +11,7 @@ import { PreprintsComponent } from './preprints.component'; import { OSFTestingModule } from '@testing/osf.testing.module'; -describe('PreprintsComponent', () => { +describe('Component: Preprint', () => { let component: PreprintsComponent; let fixture: ComponentFixture; let isWebSubject: BehaviorSubject; diff --git a/src/app/features/project/project.component.spec.ts b/src/app/features/project/project.component.spec.ts index 34a2dde82..a79bb91bb 100644 --- a/src/app/features/project/project.component.spec.ts +++ b/src/app/features/project/project.component.spec.ts @@ -1,22 +1,50 @@ +import { MockProvider } from 'ng-mocks'; + +import { BehaviorSubject } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HelpScoutService } from '@core/services/help-scout.service'; +import { IS_WEB } from '@osf/shared/helpers'; + import { ProjectComponent } from './project.component'; -describe('ProjectComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('Component: Project', () => { let component: ProjectComponent; let fixture: ComponentFixture; + let isWebSubject: BehaviorSubject; + let helpScountService: HelpScoutService; beforeEach(async () => { + isWebSubject = new BehaviorSubject(true); + await TestBed.configureTestingModule({ - imports: [ProjectComponent], + imports: [ProjectComponent, OSFTestingModule], + providers: [ + MockProvider(IS_WEB, isWebSubject), + { + provide: HelpScoutService, + useValue: { + setResourceType: jest.fn(), + unsetResourceType: jest.fn(), + }, + }, + ], }).compileComponents(); + helpScountService = TestBed.inject(HelpScoutService); fixture = TestBed.createComponent(ProjectComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have a default value', () => { + expect(component.classes).toBe('flex flex-1 flex-column w-full'); + }); + + it('should called the helpScoutService', () => { + expect(helpScountService.setResourceType).toHaveBeenCalledWith('project'); }); }); diff --git a/src/app/features/project/project.component.ts b/src/app/features/project/project.component.ts index e6191eac5..2d48cf3fa 100644 --- a/src/app/features/project/project.component.ts +++ b/src/app/features/project/project.component.ts @@ -1,7 +1,9 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostBinding, OnDestroy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { HelpScoutService } from '@core/services/help-scout.service'; + @Component({ selector: 'osf-project', imports: [RouterOutlet, CommonModule], @@ -9,6 +11,14 @@ import { RouterOutlet } from '@angular/router'; styleUrl: './project.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ProjectComponent { +export class ProjectComponent implements OnDestroy { @HostBinding('class') classes = 'flex flex-1 flex-column w-full'; + + constructor(private helpScoutService: HelpScoutService) { + this.helpScoutService.setResourceType('project'); + } + + ngOnDestroy(): void { + this.helpScoutService.unsetResourceType(); + } } From 9f691ba9fdd4f46a94f090c95ef419aff45e30a6 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Wed, 10 Sep 2025 16:50:03 -0500 Subject: [PATCH 3/5] feat(eng-8768): add the help-scout to registration --- .../registries/registries.component.spec.ts | 34 +++++++++++++++---- .../registries/registries.component.ts | 14 ++++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/app/features/registries/registries.component.spec.ts b/src/app/features/registries/registries.component.spec.ts index 01003ff5e..83e0af5f4 100644 --- a/src/app/features/registries/registries.component.spec.ts +++ b/src/app/features/registries/registries.component.spec.ts @@ -1,22 +1,44 @@ +import { MockProvider } from 'ng-mocks'; + +import { BehaviorSubject } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HelpScoutService } from '@core/services/help-scout.service'; +import { IS_WEB } from '@osf/shared/helpers'; + import { RegistriesComponent } from './registries.component'; -describe('RegistriesComponent', () => { - let component: RegistriesComponent; +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('Component: Registries', () => { let fixture: ComponentFixture; + let isWebSubject: BehaviorSubject; + let helpScountService: HelpScoutService; beforeEach(async () => { + isWebSubject = new BehaviorSubject(true); + await TestBed.configureTestingModule({ - imports: [RegistriesComponent], + imports: [RegistriesComponent, OSFTestingModule], + providers: [ + MockProvider(IS_WEB, isWebSubject), + { + provide: HelpScoutService, + useValue: { + setResourceType: jest.fn(), + unsetResourceType: jest.fn(), + }, + }, + ], }).compileComponents(); + helpScountService = TestBed.inject(HelpScoutService); fixture = TestBed.createComponent(RegistriesComponent); - component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should called the helpScoutService', () => { + expect(helpScountService.setResourceType).toHaveBeenCalledWith('registration'); }); }); diff --git a/src/app/features/registries/registries.component.ts b/src/app/features/registries/registries.component.ts index 78716bee1..173ddc379 100644 --- a/src/app/features/registries/registries.component.ts +++ b/src/app/features/registries/registries.component.ts @@ -1,6 +1,8 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { HelpScoutService } from '@core/services/help-scout.service'; + @Component({ selector: 'osf-registries', imports: [RouterOutlet], @@ -8,4 +10,12 @@ import { RouterOutlet } from '@angular/router'; styleUrl: './registries.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RegistriesComponent {} +export class RegistriesComponent implements OnDestroy { + constructor(private helpScoutService: HelpScoutService) { + this.helpScoutService.setResourceType('registration'); + } + + ngOnDestroy(): void { + this.helpScoutService.unsetResourceType(); + } +} From f3bb589c87ea730da2b50013307582e83ca9b57a Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Wed, 10 Sep 2025 16:56:00 -0500 Subject: [PATCH 4/5] feat(eng-8768): add the help-scout to settings and registries files --- .../files-control.component.spec.ts | 37 ++++++++++++++++-- .../files-control/files-control.component.ts | 22 +++++++++-- .../settings-container.component.spec.ts | 38 +++++++++++++++---- .../settings/settings-container.component.ts | 14 ++++++- 4 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/app/features/registries/components/files-control/files-control.component.spec.ts b/src/app/features/registries/components/files-control/files-control.component.spec.ts index 004f5e814..876d103c8 100644 --- a/src/app/features/registries/components/files-control/files-control.component.spec.ts +++ b/src/app/features/registries/components/files-control/files-control.component.spec.ts @@ -1,22 +1,51 @@ +import { MockProvider } from 'ng-mocks'; + +import { BehaviorSubject } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HelpScoutService } from '@core/services/help-scout.service'; +import { IS_WEB } from '@osf/shared/helpers'; + import { FilesControlComponent } from './files-control.component'; -describe('FilesControlComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('Component: File Control', () => { let component: FilesControlComponent; let fixture: ComponentFixture; + let isWebSubject: BehaviorSubject; + let helpScountService: HelpScoutService; beforeEach(async () => { + isWebSubject = new BehaviorSubject(true); + await TestBed.configureTestingModule({ - imports: [FilesControlComponent], + imports: [FilesControlComponent, OSFTestingModule], + providers: [ + MockProvider(IS_WEB, isWebSubject), + { + provide: HelpScoutService, + useValue: { + setResourceType: jest.fn(), + unsetResourceType: jest.fn(), + }, + }, + ], }).compileComponents(); + helpScountService = TestBed.inject(HelpScoutService); fixture = TestBed.createComponent(FilesControlComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should have a default value', () => { + expect(component.fileIsUploading()).toBeFalsy(); + expect(component.isFolderOpening()).toBeFalsy(); + }); + + it('should called the helpScoutService', () => { + expect(helpScountService.setResourceType).toHaveBeenCalledWith('project'); }); }); diff --git a/src/app/features/registries/components/files-control/files-control.component.ts b/src/app/features/registries/components/files-control/files-control.component.ts index 980a2763e..1809cf6ef 100644 --- a/src/app/features/registries/components/files-control/files-control.component.ts +++ b/src/app/features/registries/components/files-control/files-control.component.ts @@ -10,10 +10,21 @@ import { DialogService } from 'primeng/dynamicdialog'; import { EMPTY, filter, finalize, Observable, shareReplay, take } from 'rxjs'; import { HttpEventType } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + inject, + input, + OnDestroy, + output, + signal, +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HelpScoutService } from '@core/services/help-scout.service'; import { CreateFolderDialogComponent } from '@osf/features/files/components'; import { FilesTreeComponent, LoadingSpinnerComponent } from '@osf/shared/components'; import { FilesTreeActions, OsfFile } from '@osf/shared/models'; @@ -45,7 +56,7 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, providers: [DialogService, TreeDragDropService], }) -export class FilesControlComponent { +export class FilesControlComponent implements OnDestroy { attachedFiles = input.required[]>(); attachFile = output(); filesLink = input.required(); @@ -86,7 +97,8 @@ export class FilesControlComponent { setMoveFileCurrentFolder: (folder) => this.actions.setMoveFileCurrentFolder(folder), }; - constructor() { + constructor(private helpScoutService: HelpScoutService) { + this.helpScoutService.setResourceType('preprint'); effect(() => { const filesLink = this.filesLink(); if (filesLink) { @@ -194,4 +206,8 @@ export class FilesControlComponent { folderIsOpening(value: boolean): void { this.isFolderOpening.set(value); } + + ngOnDestroy(): void { + this.helpScoutService.unsetResourceType(); + } } diff --git a/src/app/features/settings/settings-container.component.spec.ts b/src/app/features/settings/settings-container.component.spec.ts index 8b56cb825..7fd061b5c 100644 --- a/src/app/features/settings/settings-container.component.spec.ts +++ b/src/app/features/settings/settings-container.component.spec.ts @@ -1,28 +1,50 @@ +import { MockProvider } from 'ng-mocks'; + +import { BehaviorSubject } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { HelpScoutService } from '@core/services/help-scout.service'; +import { IS_WEB } from '@osf/shared/helpers'; + import { SettingsContainerComponent } from './settings-container.component'; -describe('SettingsContainerComponent', () => { - let component: SettingsContainerComponent; +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('Component: Settings', () => { let fixture: ComponentFixture; + let isWebSubject: BehaviorSubject; + let helpScountService: HelpScoutService; beforeEach(async () => { + isWebSubject = new BehaviorSubject(true); + await TestBed.configureTestingModule({ - imports: [SettingsContainerComponent], + imports: [SettingsContainerComponent, OSFTestingModule], + providers: [ + MockProvider(IS_WEB, isWebSubject), + { + provide: HelpScoutService, + useValue: { + setResourceType: jest.fn(), + unsetResourceType: jest.fn(), + }, + }, + ], }).compileComponents(); + helpScountService = TestBed.inject(HelpScoutService); fixture = TestBed.createComponent(SettingsContainerComponent); - component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should render router outlet', () => { const routerOutlet = fixture.debugElement.query(By.css('router-outlet')); expect(routerOutlet).toBeTruthy(); }); + + it('should called the helpScoutService', () => { + expect(helpScountService.setResourceType).toHaveBeenCalledWith('user'); + }); }); diff --git a/src/app/features/settings/settings-container.component.ts b/src/app/features/settings/settings-container.component.ts index ee731c455..a076371db 100644 --- a/src/app/features/settings/settings-container.component.ts +++ b/src/app/features/settings/settings-container.component.ts @@ -1,6 +1,8 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { HelpScoutService } from '@core/services/help-scout.service'; + @Component({ selector: 'osf-settings-container', imports: [RouterOutlet], @@ -8,4 +10,12 @@ import { RouterOutlet } from '@angular/router'; styleUrl: './settings-container.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SettingsContainerComponent {} +export class SettingsContainerComponent implements OnDestroy { + constructor(private helpScoutService: HelpScoutService) { + this.helpScoutService.setResourceType('user'); + } + + ngOnDestroy(): void { + this.helpScoutService.unsetResourceType(); + } +} From 535525a788090f4887c2c6ce49d4196a18dfd7a6 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Fri, 12 Sep 2025 07:57:28 -0500 Subject: [PATCH 5/5] feat(pr-updates): updated code based on the pr --- src/app/features/preprints/preprints.component.ts | 3 ++- src/app/features/project/project.component.ts | 5 +++-- .../files-control/files-control.component.spec.ts | 2 +- .../components/files-control/files-control.component.ts | 5 +++-- src/app/features/registries/registries.component.ts | 5 +++-- src/app/features/settings/settings-container.component.ts | 6 ++++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/app/features/preprints/preprints.component.ts b/src/app/features/preprints/preprints.component.ts index 8306a32f4..128a8b949 100644 --- a/src/app/features/preprints/preprints.component.ts +++ b/src/app/features/preprints/preprints.component.ts @@ -13,10 +13,11 @@ import { IS_WEB } from '@osf/shared/helpers'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PreprintsComponent implements OnDestroy { + private readonly helpScoutService = inject(HelpScoutService); readonly isDesktop = toSignal(inject(IS_WEB)); @HostBinding('class') classes = 'flex flex-1 flex-column w-full'; - constructor(private helpScoutService: HelpScoutService) { + constructor() { this.helpScoutService.setResourceType('preprint'); } diff --git a/src/app/features/project/project.component.ts b/src/app/features/project/project.component.ts index 2d48cf3fa..b331f2ac3 100644 --- a/src/app/features/project/project.component.ts +++ b/src/app/features/project/project.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, HostBinding, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostBinding, inject, OnDestroy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { HelpScoutService } from '@core/services/help-scout.service'; @@ -12,9 +12,10 @@ import { HelpScoutService } from '@core/services/help-scout.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectComponent implements OnDestroy { + private readonly helpScoutService = inject(HelpScoutService); @HostBinding('class') classes = 'flex flex-1 flex-column w-full'; - constructor(private helpScoutService: HelpScoutService) { + constructor() { this.helpScoutService.setResourceType('project'); } diff --git a/src/app/features/registries/components/files-control/files-control.component.spec.ts b/src/app/features/registries/components/files-control/files-control.component.spec.ts index 876d103c8..25094a789 100644 --- a/src/app/features/registries/components/files-control/files-control.component.spec.ts +++ b/src/app/features/registries/components/files-control/files-control.component.spec.ts @@ -46,6 +46,6 @@ describe('Component: File Control', () => { }); it('should called the helpScoutService', () => { - expect(helpScountService.setResourceType).toHaveBeenCalledWith('project'); + expect(helpScountService.setResourceType).toHaveBeenCalledWith('files'); }); }); diff --git a/src/app/features/registries/components/files-control/files-control.component.ts b/src/app/features/registries/components/files-control/files-control.component.ts index 1809cf6ef..2729252d4 100644 --- a/src/app/features/registries/components/files-control/files-control.component.ts +++ b/src/app/features/registries/components/files-control/files-control.component.ts @@ -68,6 +68,7 @@ export class FilesControlComponent implements OnDestroy { private readonly dialogService = inject(DialogService); private readonly translateService = inject(TranslateService); private readonly destroyRef = inject(DestroyRef); + private readonly helpScoutService = inject(HelpScoutService); readonly files = select(RegistriesSelectors.getFiles); readonly filesTotalCount = select(RegistriesSelectors.getFilesTotalCount); @@ -97,8 +98,8 @@ export class FilesControlComponent implements OnDestroy { setMoveFileCurrentFolder: (folder) => this.actions.setMoveFileCurrentFolder(folder), }; - constructor(private helpScoutService: HelpScoutService) { - this.helpScoutService.setResourceType('preprint'); + constructor() { + this.helpScoutService.setResourceType('files'); effect(() => { const filesLink = this.filesLink(); if (filesLink) { diff --git a/src/app/features/registries/registries.component.ts b/src/app/features/registries/registries.component.ts index 173ddc379..841cff891 100644 --- a/src/app/features/registries/registries.component.ts +++ b/src/app/features/registries/registries.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { HelpScoutService } from '@core/services/help-scout.service'; @@ -11,7 +11,8 @@ import { HelpScoutService } from '@core/services/help-scout.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistriesComponent implements OnDestroy { - constructor(private helpScoutService: HelpScoutService) { + private readonly helpScoutService = inject(HelpScoutService); + constructor() { this.helpScoutService.setResourceType('registration'); } diff --git a/src/app/features/settings/settings-container.component.ts b/src/app/features/settings/settings-container.component.ts index a076371db..12c980d7e 100644 --- a/src/app/features/settings/settings-container.component.ts +++ b/src/app/features/settings/settings-container.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { HelpScoutService } from '@core/services/help-scout.service'; @@ -11,7 +11,9 @@ import { HelpScoutService } from '@core/services/help-scout.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsContainerComponent implements OnDestroy { - constructor(private helpScoutService: HelpScoutService) { + private readonly helpScoutService = inject(HelpScoutService); + + constructor() { this.helpScoutService.setResourceType('user'); }