Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export class PreprintProvidersMapper {
): PreprintProviderShortInfo[] {
return response.map((item) => ({
id: item.id,
descriptionHtml: item.attributes.description,
name: item.attributes.name,
name: replaceBadEncodedChars(item.attributes.name),
descriptionHtml: replaceBadEncodedChars(item.attributes.description),
whiteWideImageUrl: item.attributes.assets?.wide_white,
squareColorNoTransparentImageUrl: item.attributes.assets?.square_color_no_transparent,
}));
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/preprints/mappers/preprints.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class PreprintsMapper {
datePublished: response.attributes.date_published,
dateLastTransitioned: response.attributes.date_last_transitioned,
title: replaceBadEncodedChars(response.attributes.title),
description: response.attributes.description,
description: replaceBadEncodedChars(response.attributes.description),
reviewsState: response.attributes.reviews_state,
preprintDoiCreated: response.attributes.preprint_doi_created,
currentUserPermissions: response.attributes.current_user_permissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
} @else {
<img
class="preprint-provider-hero-logo"
alt="Provider Logo"
height="40"
[src]="preprintProvider()!.brand.heroLogoImageUrl"
[alt]="'preprints.selectService.providerLogoImageAlt' | translate"
/>
<h1 class="preprint-provider-name">
{{ 'preprints.createNewVersionTitle' | translate }}
Expand All @@ -30,17 +30,17 @@ <h1 class="preprint-provider-name">
}
</section>

<section class="flex-1 bg-white px-3 py-4 md:py-4 md:px-4">
@switch (currentStep().value) {
@case (PreprintSteps.File) {
<osf-file-step
[provider]="preprintProvider()"
(nextClicked)="moveToNextStep()"
(backClicked)="moveToPreviousStep()"
/>
}
@case (PreprintSteps.Review) {
<osf-review-step [provider]="preprintProvider()" />
@let provider = preprintProvider();

@if (provider) {
<section class="flex-1 bg-white px-3 py-4 md:py-4 md:px-4">
@switch (currentStep().value) {
@case (PreprintSteps.File) {
<osf-file-step [provider]="provider" (nextClicked)="moveToNextStep()" (backClicked)="navigateBack()" />
}
@case (PreprintSteps.Review) {
<osf-review-step [provider]="preprintProvider()" />
}
}
}
</section>
</section>
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Store } from '@ngxs/store';

import { MockComponents, MockProvider } from 'ng-mocks';

import { of } from 'rxjs';
Expand All @@ -7,7 +9,6 @@ import { ActivatedRoute, Router } from '@angular/router';

import { StepperComponent } from '@osf/shared/components/stepper/stepper.component';
import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens';
import { StepOption } from '@osf/shared/models/step-option.model';
import { BrandService } from '@osf/shared/services/brand.service';
import { BrowserTabService } from '@osf/shared/services/browser-tab.service';
import { HeaderStyleService } from '@osf/shared/services/header-style.service';
Expand All @@ -16,181 +17,185 @@ import { FileStepComponent, ReviewStepComponent } from '../../components';
import { createNewVersionStepsConst } from '../../constants';
import { PreprintSteps } from '../../enums';
import { PreprintProviderDetails } from '../../models';
import { PreprintProvidersSelectors } from '../../store/preprint-providers';
import { PreprintStepperSelectors } from '../../store/preprint-stepper';
import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers';
import { FetchPreprintById, PreprintStepperSelectors, ResetPreprintStepperState } from '../../store/preprint-stepper';

import { CreateNewVersionComponent } from './create-new-version.component';

import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock';
import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details';
import { TranslationServiceMock } from '@testing/mocks/translation.service.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { BrandServiceMock, BrandServiceMockType } from '@testing/providers/brand-service.mock';
import { BrowserTabServiceMock, BrowserTabServiceMockType } from '@testing/providers/browser-tab-service.mock';
import { HeaderStyleServiceMock, HeaderStyleServiceMockType } from '@testing/providers/header-style-service.mock';
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
import { RouterMockBuilder } from '@testing/providers/router-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock';

describe('CreateNewVersionComponent', () => {
let component: CreateNewVersionComponent;
let fixture: ComponentFixture<CreateNewVersionComponent>;
let routerMock: ReturnType<RouterMockBuilder['build']>;
let routeMock: ReturnType<ActivatedRouteMockBuilder['build']>;
let store: Store;
let routerMock: RouterMockType;
let brandServiceMock: BrandServiceMockType;
let headerStyleMock: HeaderStyleServiceMockType;
let browserTabMock: BrowserTabServiceMockType;

const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK;
const mockPreprint = PREPRINT_MOCK;
const mockProviderId = 'osf';
const mockPreprintId = 'test_preprint_123';

beforeEach(async () => {
const defaultSignals: SignalOverride[] = [
{ selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId), value: mockProvider },
{ selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, value: false },
{ selector: PreprintStepperSelectors.hasBeenSubmitted, value: false },
];

function setup(overrides?: { selectorOverrides?: SignalOverride[] }) {
const signals = mergeSignalOverrides(defaultSignals, overrides?.selectorOverrides);

routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build();
routeMock = ActivatedRouteMockBuilder.create()
const routeMock = ActivatedRouteMockBuilder.create()
.withParams({ providerId: mockProviderId, preprintId: mockPreprintId })
.withQueryParams({})
.build();

await TestBed.configureTestingModule({
imports: [
CreateNewVersionComponent,
OSFTestingModule,
...MockComponents(StepperComponent, FileStepComponent, ReviewStepComponent),
],
brandServiceMock = BrandServiceMock.simple();
headerStyleMock = HeaderStyleServiceMock.simple();
browserTabMock = BrowserTabServiceMock.simple();

TestBed.configureTestingModule({
imports: [CreateNewVersionComponent, ...MockComponents(StepperComponent, FileStepComponent, ReviewStepComponent)],
providers: [
TranslationServiceMock,
MockProvider(BrandService),
MockProvider(BrowserTabService),
MockProvider(HeaderStyleService),
MockProvider(Router, routerMock),
provideOSFCore(),
MockProvider(ActivatedRoute, routeMock),
MockProvider(Router, routerMock),
MockProvider(BrandService, brandServiceMock),
MockProvider(HeaderStyleService, headerStyleMock),
MockProvider(BrowserTabService, browserTabMock),
MockProvider(IS_WEB, of(true)),
provideMockStore({
signals: [
{
selector: PreprintStepperSelectors.getPreprint,
value: mockPreprint,
},
{
selector: PreprintProvidersSelectors.getPreprintProviderDetails(mockProviderId),
value: mockProvider,
},
{
selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading,
value: false,
},
{
selector: PreprintStepperSelectors.hasBeenSubmitted,
value: false,
},
],
}),
provideMockStore({ signals }),
],
}).compileComponents();
});

store = TestBed.inject(Store);
fixture = TestBed.createComponent(CreateNewVersionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
}

afterEach(() => {
if (fixture) {
fixture.destroy();
}
fixture?.destroy();
jest.restoreAllMocks();
});

it('should initialize with correct default values', () => {
setup();

expect(component.PreprintSteps).toBe(PreprintSteps);
expect(component.newVersionSteps).toBe(createNewVersionStepsConst);
expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]);
expect(component.classes).toBe('flex-1 flex flex-column w-full');
});

it('should return preprint from store', () => {
const preprint = component.preprint();
expect(preprint).toBe(mockPreprint);
});
it('should dispatch initial actions on creation', () => {
setup();

it('should return preprint provider from store', () => {
const provider = component.preprintProvider();
expect(provider).toBe(mockProvider);
expect(store.dispatch).toHaveBeenCalledWith(new GetPreprintProviderById(mockProviderId));
expect(store.dispatch).toHaveBeenCalledWith(new FetchPreprintById(mockPreprintId));
});

it('should return loading state from store', () => {
const loading = component.isPreprintProviderLoading();
expect(loading).toBe(false);
});
it('should apply branding when provider is available', () => {
setup();

it('should return submission state from store', () => {
const submitted = component.hasBeenSubmitted();
expect(submitted).toBe(false);
expect(brandServiceMock.applyBranding).toHaveBeenCalledWith(mockProvider.brand);
expect(headerStyleMock.applyHeaderStyles).toHaveBeenCalledWith(
mockProvider.brand.primaryColor,
mockProvider.brand.secondaryColor,
mockProvider.brand.heroBackgroundImageUrl
);
expect(browserTabMock.updateTabStyles).toHaveBeenCalledWith(mockProvider.faviconUrl, mockProvider.name);
});

it('should return web environment state', () => {
const isWeb = component.isWeb();
expect(typeof isWeb).toBe('boolean');
});
it('should reset services on destroy', () => {
setup();

it('should initialize with first step as current step', () => {
expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]);
component.ngOnDestroy();

expect(headerStyleMock.resetToDefaults).toHaveBeenCalled();
expect(brandServiceMock.resetBranding).toHaveBeenCalled();
expect(browserTabMock.resetToDefaults).toHaveBeenCalled();
expect(store.dispatch).toHaveBeenCalledWith(new ResetPreprintStepperState());
});

it('should handle step change when moving to previous step', () => {
const previousStep = createNewVersionStepsConst[0];
it('should prevent beforeunload when not submitted', () => {
setup();
const event = { preventDefault: jest.fn() } as unknown as BeforeUnloadEvent;

component.stepChange(previousStep);
component.onBeforeUnload(event);

expect(component.currentStep()).toEqual(previousStep);
expect(event.preventDefault).toHaveBeenCalled();
});

it('should not change step when moving to next step', () => {
const currentStep = component.currentStep();
const nextStep = createNewVersionStepsConst[1];
it('should not prevent beforeunload when submitted', () => {
setup({ selectorOverrides: [{ selector: PreprintStepperSelectors.hasBeenSubmitted, value: true }] });
const event = { preventDefault: jest.fn() } as unknown as BeforeUnloadEvent;

component.stepChange(nextStep);
component.onBeforeUnload(event);

expect(component.currentStep()).toEqual(currentStep);
expect(event.preventDefault).not.toHaveBeenCalled();
});

it('should move to next step', () => {
const currentIndex = component.currentStep()?.index ?? 0;
const nextStep = createNewVersionStepsConst[currentIndex + 1];

component.moveToNextStep();
it('should prevent deactivation when not submitted', () => {
setup();

expect(component.currentStep()).toEqual(nextStep);
expect(component.canDeactivate()).toBe(false);
});

it('should navigate to previous step (preprint page)', () => {
component.moveToPreviousStep();
it('should allow deactivation when submitted', () => {
setup({ selectorOverrides: [{ selector: PreprintStepperSelectors.hasBeenSubmitted, value: true }] });

expect(routerMock.navigate).toHaveBeenCalledWith([mockPreprintId.split('_')[0]]);
expect(component.canDeactivate()).toBe(true);
});

it('should return canDeactivate state', () => {
const canDeactivate = component.canDeactivate();
expect(canDeactivate).toBe(false);
it('should ignore stepping forward via stepper', () => {
setup();

component.stepChange(createNewVersionStepsConst[1]);

expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]);
});

it('should handle beforeunload event', () => {
const event = {
preventDefault: jest.fn(),
} as unknown as BeforeUnloadEvent;
it('should allow stepping back via stepper', () => {
setup();
component.moveToNextStep();

const result = component.onBeforeUnload(event);
component.stepChange(createNewVersionStepsConst[0]);

expect(event.preventDefault).toHaveBeenCalled();
expect(result).toBe(false);
expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]);
});

it('should handle step navigation correctly', () => {
it('should move to next step', () => {
setup();

component.moveToNextStep();

expect(component.currentStep()).toEqual(createNewVersionStepsConst[1]);
});

component.stepChange(createNewVersionStepsConst[0]);
expect(component.currentStep()).toEqual(createNewVersionStepsConst[0]);
it('should not move past the last step', () => {
setup();
component.currentStep.set(createNewVersionStepsConst[createNewVersionStepsConst.length - 1]);

component.moveToNextStep();

expect(component.currentStep()).toEqual(createNewVersionStepsConst[createNewVersionStepsConst.length - 1]);
});

it('should handle edge case when moving to next step with undefined current step', () => {
component.currentStep.set({} as StepOption);
it('should navigate back to preprint page', () => {
setup();

expect(() => component.moveToNextStep()).not.toThrow();
component.navigateBack();

expect(routerMock.navigate).toHaveBeenCalledWith([mockPreprintId.split('_')[0]]);
});
});
Loading