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
Original file line number Diff line number Diff line change
@@ -1,86 +1,92 @@
<section class="h-full" data-test-new-registration-form>
<osf-sub-header [title]="'registries.new.addNewRegistry' | translate" />
@if (canShowForm()) {
<section class="h-full" data-test-new-registration-form>
<osf-sub-header [title]="'registries.new.addNewRegistry' | translate" />

<section class="flex flex-column lg:flex-row flex-1 p-5 gap-4 bg-white w-full">
<p>
{{ 'registries.new.infoText1' | translate }}
<a class="font-bold" href="https://help.osf.io/"> {{ 'common.links.clickHere' | translate }}</a>
{{ 'registries.new.infoText2' | translate }}
</p>
</section>
<section class="flex flex-column lg:flex-row flex-1 p-5 gap-4 bg-white w-full">
<p>
{{ 'registries.new.infoText1' | translate }}
<a class="font-bold" href="https://help.osf.io/"> {{ 'common.links.clickHere' | translate }}</a>
{{ 'registries.new.infoText2' | translate }}
</p>
</section>

<section class="flex flex-column flex-1 p-5 gap-4 w-full bg-white">
<p-card class="w-full">
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 1</h2>
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step1' | translate }}</p>
<div class="flex gap-2">
<p-button
class="btn-full-width w-2 font-bold"
[class]="{ 'pointer-events-none': fromProject() }"
severity="info"
[label]="'common.buttons.yes' | translate"
[raised]="fromProject()"
(onClick)="toggleFromProject()"
/>
<p-button
class="btn-full-width w-2"
[class]="{ 'pointer-events-none': !fromProject() }"
severity="info"
[label]="'common.buttons.no' | translate"
[raised]="!fromProject()"
(onClick)="toggleFromProject()"
/>
</div>
</p-card>

<section class="flex flex-column flex-1 p-5 gap-4 w-full bg-white">
<p-card class="w-full">
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 1</h2>
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step1' | translate }}</p>
<div class="flex gap-2">
<p-button
class="btn-full-width w-2 font-bold"
[class]="{ 'pointer-events-none': fromProject() }"
severity="info"
[label]="'common.buttons.yes' | translate"
[raised]="fromProject()"
(onClick)="toggleFromProject()"
/>
<p-button
class="btn-full-width w-2"
[class]="{ 'pointer-events-none': !fromProject() }"
severity="info"
[label]="'common.buttons.no' | translate"
[raised]="!fromProject()"
(onClick)="toggleFromProject()"
/>
</div>
</p-card>
<form [formGroup]="draftForm" (ngSubmit)="createDraft()" class="flex flex-column gap-4">
@if (fromProject()) {
<p-card class="w-full">
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 2</h2>
<p class="mb-3 text-lg font-bold">{{ 'registries.new.steps.step2' | translate }}</p>
<p class="mb-4">{{ 'registries.new.steps.step2InfoText' | translate }}</p>
<div class="flex">
<p-select
data-test-project-select
formControlName="project"
[options]="projects()"
[placeholder]="'registries.new.selectProject' | translate"
optionLabel="title"
optionValue="id"
filter="true"
[loading]="isProjectsLoading()"
(onFilter)="onProjectFilter($event.filter)"
class="w-6"
/>
</div>
</p-card>
}

<form [formGroup]="draftForm" (ngSubmit)="createDraft()" class="flex flex-column gap-4">
@if (fromProject()) {
<p-card class="w-full">
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} 2</h2>
<p class="mb-3 text-lg font-bold">{{ 'registries.new.steps.step2' | translate }}</p>
<p class="mb-4">{{ 'registries.new.steps.step2InfoText' | translate }}</p>
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} {{ fromProject() ? '3' : '2' }}</h2>
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step3' | translate }}</p>
<div class="flex">
<p-select
data-test-project-select
formControlName="project"
[options]="projects()"
[placeholder]="'registries.new.selectProject' | translate"
optionLabel="title"
data-test-schema-select
formControlName="providerSchema"
[options]="providerSchemas()"
optionLabel="name"
optionValue="id"
filter="true"
[loading]="isProjectsLoading()"
(onFilter)="onProjectFilter($event.filter)"
[loading]="isProvidersLoading()"
class="w-6"
/>
</div>
</p-card>
}

<p-card class="w-full">
<h2 class="mb-4">{{ 'registries.new.steps.title' | translate }} {{ fromProject() ? '3' : '2' }}</h2>
<p class="mb-4 text-lg font-bold">{{ 'registries.new.steps.step3' | translate }}</p>
<div class="flex">
<p-select
data-test-schema-select
formControlName="providerSchema"
[options]="providerSchemas()"
optionLabel="name"
optionValue="id"
[loading]="isProvidersLoading()"
class="w-6"
<div class="flex justify-content-end">
<p-button
data-test-start-registration-button
[label]="'registries.new.createDraft' | translate"
[disabled]="draftForm.invalid"
type="submit"
[loading]="isDraftSubmitting()"
/>
</div>
</p-card>

<div class="flex justify-content-end">
<p-button
data-test-start-registration-button
[label]="'registries.new.createDraft' | translate"
[disabled]="draftForm.invalid"
type="submit"
[loading]="isDraftSubmitting()"
/>
</div>
</form>
</form>
</section>
</section>
</section>
} @else {
<div class="flex-1">
<osf-loading-spinner />
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,61 @@ import { UserSelectors } from '@core/store/user';
import { CreateDraft, GetProjects, GetProviderSchemas, RegistriesSelectors } from '@osf/features/registries/store';
import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component';
import { ToastService } from '@osf/shared/services/toast.service';
import { GetRegistryProvider } from '@shared/stores/registration-provider';
import { GetRegistryProvider, RegistrationProviderSelectors } from '@shared/stores/registration-provider';

import { NewRegistrationComponent } from './new-registration.component';

import { MOCK_PROVIDER_SCHEMAS } from '@testing/mocks/registries.mock';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import {
BaseSetupOverrides,
mergeSignalOverrides,
provideMockStore,
SignalOverride,
} from '@testing/providers/store-provider.mock';
import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock';

describe('NewRegistrationComponent', () => {
let component: NewRegistrationComponent;
let fixture: ComponentFixture<NewRegistrationComponent>;
let store: Store;
let mockRouter: RouterMockType;

beforeEach(() => {
let toastService: ToastServiceMockType;

interface SetupOverrides extends BaseSetupOverrides {
selectorOverrides?: SignalOverride[];
}

const defaultSignals: SignalOverride[] = [
{ selector: RegistriesSelectors.getProjects, value: [{ id: 'p1', title: 'P1' }] },
{ selector: RegistriesSelectors.getProviderSchemas, value: MOCK_PROVIDER_SCHEMAS },
{ selector: RegistriesSelectors.isDraftSubmitting, value: false },
{ selector: RegistriesSelectors.getDraftRegistration, value: { id: 'draft-1' } },
{ selector: RegistriesSelectors.isProvidersLoading, value: false },
{ selector: RegistriesSelectors.isProjectsLoading, value: false },
{ selector: UserSelectors.getCurrentUser, value: { id: 'user-1' } },
{ selector: RegistrationProviderSelectors.getBrandedProvider, value: { id: 'prov-1', allowSubmissions: true } },
];

const setup = (overrides?: SetupOverrides) => {
const mockActivatedRoute = ActivatedRouteMockBuilder.create()
.withParams({ providerId: 'prov-1' })
.withParams(overrides?.routeParams || { providerId: 'prov-1' })
.withQueryParams({ projectId: 'proj-1' })
.build();
mockRouter = RouterMockBuilder.create().withUrl('/x').build();
toastService = ToastServiceMock.simple();

TestBed.configureTestingModule({
imports: [NewRegistrationComponent, MockComponent(SubHeaderComponent)],
providers: [
provideOSFCore(),
MockProvider(ActivatedRoute, mockActivatedRoute),
MockProvider(ToastService),
MockProvider(ToastService, toastService),
MockProvider(Router, mockRouter),
provideMockStore({
signals: [
{ selector: RegistriesSelectors.getProjects, value: [{ id: 'p1', title: 'P1' }] },
{ selector: RegistriesSelectors.getProviderSchemas, value: MOCK_PROVIDER_SCHEMAS },
{ selector: RegistriesSelectors.isDraftSubmitting, value: false },
{ selector: RegistriesSelectors.getDraftRegistration, value: { id: 'draft-1' } },
{ selector: RegistriesSelectors.isProvidersLoading, value: false },
{ selector: RegistriesSelectors.isProjectsLoading, value: false },
{ selector: UserSelectors.getCurrentUser, value: { id: 'user-1' } },
],
signals: mergeSignalOverrides(defaultSignals, overrides?.selectorOverrides),
}),
],
});
Expand All @@ -57,31 +72,69 @@ describe('NewRegistrationComponent', () => {
fixture = TestBed.createComponent(NewRegistrationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
};

it('should create', () => {
setup();
expect(component).toBeTruthy();
});

it('should allow submissions when provider allows it', () => {
setup();
expect(component.canShowForm()).toBe(true);
expect(toastService.showError).not.toHaveBeenCalled();
expect(mockRouter.navigate).not.toHaveBeenCalled();
});

it('should redirect and show error when submissions are not allowed', () => {
setup({
selectorOverrides: [
{
selector: RegistrationProviderSelectors.getBrandedProvider,
value: { id: 'prov-1', allowSubmissions: false },
},
],
});

expect(component.canShowForm()).toBe(false);
expect(toastService.showError).toHaveBeenCalledWith('registries.new.registryClosedForSubmissions');
expect(mockRouter.navigate).toHaveBeenCalledWith(['/registries', 'prov-1']);
});

it('should redirect and show error when allowSubmissions is undefined', () => {
setup({
selectorOverrides: [{ selector: RegistrationProviderSelectors.getBrandedProvider, value: { id: 'prov-1' } }],
});

expect(component.canShowForm()).toBe(false);
expect(toastService.showError).toHaveBeenCalledWith('registries.new.registryClosedForSubmissions');
expect(mockRouter.navigate).toHaveBeenCalledWith(['/registries', 'prov-1']);
});

it('should dispatch initial data fetching on init', () => {
setup();
expect(store.dispatch).toHaveBeenCalledWith(new GetProjects('user-1', ''));
expect(store.dispatch).toHaveBeenCalledWith(new GetRegistryProvider('prov-1'));
expect(store.dispatch).toHaveBeenCalledWith(new GetProviderSchemas('prov-1'));
});

it('should init fromProject as true when projectId is present', () => {
setup();
expect(component.fromProject()).toBe(true);
});

it('should init form with project id from route', () => {
setup();
expect(component.draftForm.get('project')?.value).toBe('proj-1');
});

it('should default providerSchema when schemas are available', () => {
setup();
expect(component.draftForm.get('providerSchema')?.value).toBe('schema-1');
});

it('should toggle fromProject and add/remove validator', () => {
setup();
component.fromProject.set(false);
component.toggleFromProject();
expect(component.fromProject()).toBe(true);
Expand All @@ -93,6 +146,7 @@ describe('NewRegistrationComponent', () => {
});

it('should dispatch createDraft and navigate when form is valid', () => {
setup();
component.draftForm.patchValue({ providerSchema: 'schema-1', project: 'proj-1' });
component.fromProject.set(true);
(store.dispatch as jest.Mock).mockClear();
Expand All @@ -106,6 +160,7 @@ describe('NewRegistrationComponent', () => {
});

it('should not dispatch createDraft when form is invalid', () => {
setup();
component.draftForm.patchValue({ providerSchema: '' });
(store.dispatch as jest.Mock).mockClear();

Expand All @@ -115,6 +170,7 @@ describe('NewRegistrationComponent', () => {
});

it('should dispatch getProjects after debounced filter', fakeAsync(() => {
setup();
(store.dispatch as jest.Mock).mockClear();

component.onProjectFilter('abc');
Expand All @@ -124,6 +180,7 @@ describe('NewRegistrationComponent', () => {
}));

it('should not dispatch duplicate getProjects for same filter value', fakeAsync(() => {
setup();
(store.dispatch as jest.Mock).mockClear();

component.onProjectFilter('abc');
Expand All @@ -132,12 +189,13 @@ describe('NewRegistrationComponent', () => {
tick(300);

const getProjectsCalls = (store.dispatch as jest.Mock).mock.calls.filter(
([action]: [any]) => action instanceof GetProjects
([action]: [unknown]) => action instanceof GetProjects
);
expect(getProjectsCalls.length).toBe(1);
}));

it('should debounce rapid filter calls and dispatch only the last value', fakeAsync(() => {
setup();
(store.dispatch as jest.Mock).mockClear();

component.onProjectFilter('a');
Expand All @@ -146,7 +204,7 @@ describe('NewRegistrationComponent', () => {
tick(300);

const getProjectsCalls = (store.dispatch as jest.Mock).mock.calls.filter(
([action]: [any]) => action instanceof GetProjects
([action]: [unknown]) => action instanceof GetProjects
);
expect(getProjectsCalls.length).toBe(1);
expect(getProjectsCalls[0][0]).toEqual(new GetProjects('user-1', 'abc'));
Expand Down
Loading