diff --git a/src/app/features/analytics/analytics.component.spec.ts b/src/app/features/analytics/analytics.component.spec.ts index d71856472..d349d6611 100644 --- a/src/app/features/analytics/analytics.component.spec.ts +++ b/src/app/features/analytics/analytics.component.spec.ts @@ -1,28 +1,105 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; -import { SubHeaderComponent } from '@osf/shared/components'; +import { AnalyticsComponent } from '@osf/features/analytics/analytics.component'; +import { AnalyticsKpiComponent } from '@osf/features/analytics/components'; +import { AnalyticsSelectors } from '@osf/features/analytics/store'; +import { + BarChartComponent, + LineChartComponent, + PieChartComponent, + SelectComponent, + SubHeaderComponent, + ViewOnlyLinkMessageComponent, +} from '@shared/components'; +import { IS_WEB } from '@shared/helpers'; +import { MOCK_ANALYTICS_METRICS, MOCK_RELATED_COUNTS, MOCK_RESOURCE_OVERVIEW } from '@shared/mocks'; -import { AnalyticsComponent } from './analytics.component'; +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; -describe.skip('AnalyticsComponent', () => { +describe('AnalyticsComponent', () => { let component: AnalyticsComponent; let fixture: ComponentFixture; + const resourceId = MOCK_RESOURCE_OVERVIEW.id; + const metrics = { ...MOCK_ANALYTICS_METRICS, id: resourceId }; + const relatedCounts = { ...MOCK_RELATED_COUNTS, id: resourceId }; + + const metricsSelector = AnalyticsSelectors.getMetrics(resourceId); + const relatedCountsSelector = AnalyticsSelectors.getRelatedCounts(resourceId); + beforeEach(async () => { + jest.clearAllMocks(); await TestBed.configureTestingModule({ - imports: [AnalyticsComponent, MockComponent(SubHeaderComponent), MockPipe(TranslatePipe)], - providers: [MockProvider(TranslateService)], + imports: [ + AnalyticsComponent, + ...MockComponents( + SubHeaderComponent, + AnalyticsKpiComponent, + LineChartComponent, + BarChartComponent, + PieChartComponent, + ViewOnlyLinkMessageComponent, + SelectComponent + ), + OSFTestingModule, + ], + providers: [ + provideMockStore({ + selectors: [ + { selector: metricsSelector, value: metrics }, + { selector: relatedCountsSelector, value: relatedCounts }, + { selector: AnalyticsSelectors.isMetricsLoading, value: false }, + { selector: AnalyticsSelectors.isRelatedCountsLoading, value: false }, + { selector: AnalyticsSelectors.isMetricsError, value: false }, + ], + signals: [ + { selector: metricsSelector, value: metrics }, + { selector: relatedCountsSelector, value: relatedCounts }, + { selector: AnalyticsSelectors.isMetricsLoading, value: false }, + { selector: AnalyticsSelectors.isRelatedCountsLoading, value: false }, + { selector: AnalyticsSelectors.isMetricsError, value: false }, + ], + }), + { provide: IS_WEB, useValue: of(true) }, + MockProvider(Router, { navigate: jest.fn(), url: '/' }), + { + provide: ActivatedRoute, + useValue: { + parent: { params: of({ id: resourceId }) }, + data: of({ resourceType: undefined }), + }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(AnalyticsComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should set selectedRange via onRangeChange', () => { + fixture.detectChanges(); + component.onRangeChange('month'); + expect(component.selectedRange()).toBe('month'); + }); + + it('should navigate to duplicates with correct relative route', () => { + const router = TestBed.inject(Router); + const navigateSpy = jest.spyOn(router, 'navigate'); + + fixture.detectChanges(); + component.navigateToDuplicates(); + + expect(navigateSpy).toHaveBeenCalledWith(['duplicates'], { relativeTo: expect.any(Object) }); + }); }); diff --git a/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts b/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts index 0d18d624e..52f23acf8 100644 --- a/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts +++ b/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts @@ -1,22 +1,85 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { AnalyticsKpiComponent } from './analytics-kpi.component'; -describe.skip('AnalyticsKpiComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; + +describe('AnalyticsKpiComponent', () => { let component: AnalyticsKpiComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AnalyticsKpiComponent], + imports: [AnalyticsKpiComponent, OSFTestingModule], }).compileComponents(); fixture = TestBed.createComponent(AnalyticsKpiComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have default input values', () => { + expect(component.isLoading()).toBe(false); + expect(component.showButton()).toBe(false); + expect(component.buttonLabel()).toBe(''); + expect(component.title()).toBe(''); + expect(component.value()).toBe(0); + }); + + it('should update inputs via setInput', () => { + fixture.componentRef.setInput('isLoading', true); + fixture.componentRef.setInput('showButton', true); + fixture.componentRef.setInput('buttonLabel', 'CLICK_ME'); + fixture.componentRef.setInput('title', 'T'); + fixture.componentRef.setInput('value', 7); + + expect(component.isLoading()).toBe(true); + expect(component.showButton()).toBe(true); + expect(component.buttonLabel()).toBe('CLICK_ME'); + expect(component.title()).toBe('T'); + expect(component.value()).toBe(7); + }); + + it('should render title set via setInput', () => { + fixture.componentRef.setInput('title', 'SOME_TITLE'); + fixture.detectChanges(); + + const titleEl = fixture.debugElement.query(By.css('p.title')); + expect(titleEl).toBeTruthy(); + expect(titleEl.nativeElement.textContent.trim()).toBe('SOME_TITLE'); + }); + + it('should show button with label and emit on click', () => { + const clickSpy = jest.fn(); + component.buttonClick.subscribe(() => clickSpy()); + + fixture.componentRef.setInput('showButton', true); + fixture.componentRef.setInput('buttonLabel', 'CLICK_ME'); + fixture.detectChanges(); + + const nativeButton = fixture.debugElement.query(By.css('button.p-button')); + expect(nativeButton).toBeTruthy(); + expect(nativeButton.nativeElement.textContent.trim()).toBe('CLICK_ME'); + + nativeButton.nativeElement.click(); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('should toggle button visibility via setInput', () => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('button.p-button'))).toBeNull(); + + fixture.componentRef.setInput('showButton', true); + fixture.componentRef.setInput('buttonLabel', 'LBL'); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('button.p-button'))).toBeTruthy(); + + fixture.componentRef.setInput('showButton', false); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('button.p-button'))).toBeNull(); + }); }); diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts index 1cde4d324..cf06a6ea6 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts @@ -1,22 +1,132 @@ +import { MockComponents, MockProvider } from 'ng-mocks'; + +import { DialogService } from 'primeng/dynamicdialog'; +import { PaginatorState } from 'primeng/paginator'; + +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { ProjectOverviewSelectors } from '@osf/features/project/overview/store'; +import { RegistryOverviewSelectors } from '@osf/features/registry/store/registry-overview'; +import { ResourceType } from '@osf/shared/enums'; +import { IS_SMALL } from '@osf/shared/helpers'; +import { DuplicatesSelectors } from '@osf/shared/stores'; +import { + CustomPaginatorComponent, + IconComponent, + LoadingSpinnerComponent, + SubHeaderComponent, + TruncatedTextComponent, +} from '@shared/components'; +import { MOCK_PROJECT_OVERVIEW } from '@shared/mocks'; import { ViewDuplicatesComponent } from './view-duplicates.component'; -describe.skip('ViewForksComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('ViewDuplicatesComponent', () => { let component: ViewDuplicatesComponent; let fixture: ComponentFixture; + let dialogService: DialogService; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ViewDuplicatesComponent], + imports: [ + ViewDuplicatesComponent, + OSFTestingModule, + ...MockComponents( + SubHeaderComponent, + TruncatedTextComponent, + LoadingSpinnerComponent, + CustomPaginatorComponent, + IconComponent + ), + ], + providers: [ + provideMockStore({ + signals: [ + { selector: DuplicatesSelectors.getDuplicates, value: [] }, + { selector: DuplicatesSelectors.getDuplicatesLoading, value: false }, + { selector: DuplicatesSelectors.getDuplicatesTotalCount, value: 0 }, + { selector: ProjectOverviewSelectors.getProject, value: MOCK_PROJECT_OVERVIEW }, + { selector: ProjectOverviewSelectors.isProjectAnonymous, value: false }, + { selector: RegistryOverviewSelectors.getRegistry, value: undefined }, + { selector: RegistryOverviewSelectors.isRegistryAnonymous, value: false }, + ], + }), + MockProvider(Router, { navigate: jest.fn() }), + { provide: IS_SMALL, useValue: of(false) }, + { + provide: ActivatedRoute, + useValue: { + parent: { params: of({ id: 'rid' }) }, + data: of({ resourceType: ResourceType.Project }), + }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ViewDuplicatesComponent); component = fixture.componentInstance; + + dialogService = fixture.debugElement.injector.get(DialogService); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should open ForkDialog with width 95vw and refresh on success', () => { + const openSpy = jest.spyOn(dialogService, 'open').mockReturnValue({ onClose: of({ success: true }) } as any); + (component as any).actions = { ...component.actions, getDuplicates: jest.fn() }; + + component.handleForkResource(); + + expect(openSpy).toHaveBeenCalledWith( + expect.any(Function), + expect.objectContaining({ + width: '95vw', + focusOnShow: false, + header: 'project.overview.dialog.fork.headerProject', + closeOnEscape: true, + modal: true, + closable: true, + data: expect.objectContaining({ + resource: expect.any(Object), + resourceType: ResourceType.Project, + }), + }) + ); + + expect((component as any).actions.getDuplicates).toHaveBeenCalled(); + }); + + it('should open ForkDialog with width 450px when small and not refresh on failure', () => { + (component as any).isSmall = () => true; + (component as any).actions = { ...component.actions, getDuplicates: jest.fn() }; + + const openSpy = jest.spyOn(dialogService, 'open').mockReturnValue({ onClose: of({ success: false }) } as any); + + component.handleForkResource(); + + expect(openSpy).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ width: '450px' })); + expect((component as any).actions.getDuplicates).not.toHaveBeenCalled(); + }); + + it('should update currentPage when page is defined', () => { + const event: PaginatorState = { page: 1 } as PaginatorState; + component.onPageChange(event); + expect(component.currentPage()).toBe('2'); + }); + + it('should not update currentPage when page is undefined', () => { + component.currentPage.set('5'); + const event: PaginatorState = { page: undefined } as PaginatorState; + component.onPageChange(event); + expect(component.currentPage()).toBe('5'); + }); }); diff --git a/src/app/features/files/pages/files/files.component.spec.ts b/src/app/features/files/pages/files/files.component.spec.ts index 2b0748949..58e9e79bb 100644 --- a/src/app/features/files/pages/files/files.component.spec.ts +++ b/src/app/features/files/pages/files/files.component.spec.ts @@ -98,6 +98,8 @@ describe('Component: Files', () => { 'viewOnlyDownloadable', 'resourceId', 'provider', + 'storage', + 'totalCount', ]), ], }, diff --git a/src/app/features/settings/tokens/services/tokens.service.ts b/src/app/features/settings/tokens/services/tokens.service.ts index 90ff66a49..901318634 100644 --- a/src/app/features/settings/tokens/services/tokens.service.ts +++ b/src/app/features/settings/tokens/services/tokens.service.ts @@ -40,7 +40,7 @@ export class TokensService { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService - .post>(environment.apiDomainUrl + '/tokens/', request) + .post>(`${this.apiUrl}/tokens/`, request) .pipe(map((response) => TokenMapper.fromGetResponse(response.data))); } diff --git a/src/app/shared/mocks/analytics.mock.ts b/src/app/shared/mocks/analytics.mock.ts new file mode 100644 index 000000000..75fddfd5a --- /dev/null +++ b/src/app/shared/mocks/analytics.mock.ts @@ -0,0 +1,30 @@ +import { AnalyticsMetricsModel, RelatedCountsModel } from '@osf/features/analytics/models'; + +export const MOCK_ANALYTICS_METRICS: AnalyticsMetricsModel = { + id: 'rid', + type: 'analytics', + uniqueVisits: [ + { date: '2023-01-01', count: 1 }, + { date: '2023-01-02', count: 2 }, + ], + timeOfDay: [ + { hour: 0, count: 5 }, + { hour: 1, count: 3 }, + ], + refererDomain: [ + { refererDomain: 'example.com', count: 4 }, + { refererDomain: 'osf.io', count: 6 }, + ], + popularPages: [ + { path: '/', route: '/', title: 'Home', count: 7 }, + { path: '/about', route: '/about', title: 'About', count: 2 }, + ], +}; + +export const MOCK_RELATED_COUNTS: RelatedCountsModel = { + id: 'rid', + isPublic: true, + forksCount: 1, + linksToCount: 2, + templateCount: 3, +}; diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index 6658112d7..c079f9343 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -1,4 +1,5 @@ export { MOCK_ADDON } from './addon.mock'; +export * from './analytics.mock'; export { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from './cedar-metadata-data-template-json-api.mock'; export * from './contributors.mock'; export * from './custom-сonfirmation.service.mock';