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 @@ -5,7 +5,7 @@
<div class="flex align-items-center flex-wrap gap-2 w-full">
<osf-icon [iconClass]="registrationData().public ? 'fas fa-lock-open' : 'fas fa-lock'"></osf-icon>
<h2 class="align-self-center word-break-word">
{{ (registrationData().title | fixSpecialChar) || ('project.registrations.card.noTitle' | translate) }}
{{ registrationData().title || ('project.registrations.card.noTitle' | translate) }}
</h2>
@if (!isDraft()) {
<osf-status-badge [status]="registrationData().status" />
Expand Down Expand Up @@ -49,20 +49,19 @@ <h2 class="align-self-center word-break-word">
</div>
<div class="flex gap-2 mt-1">
@if (isDraft()) {
@if (hasWriteAccess) {
@if (hasWriteAccess()) {
<p-button
severity="primary"
[label]="'common.buttons.review' | translate"
[routerLink]="['/registries/drafts/', registrationData().id, 'review']"
routerLinkActive="router-link-active"
/>
<p-button
severity="secondary"
[label]="'common.buttons.edit' | translate"
[routerLink]="['/registries/drafts/', registrationData().id, 'metadata']"
/>
}
@if (hasAdminAccess) {
@if (hasAdminAccess()) {
<p-button
severity="danger"
[label]="'common.buttons.delete' | translate"
Expand All @@ -77,15 +76,15 @@ <h2 class="align-self-center word-break-word">
routerLinkActive="router-link-active"
[label]="'common.buttons.view' | translate"
></p-button>
@if (showButtons) {
@if (isApproved) {
@if (showButtons()) {
@if (isApproved()) {
<p-button
severity="secondary"
(onClick)="updateRegistration(registrationData().id)"
[label]="'common.buttons.update' | translate"
></p-button>
}
@if (isInProgress || isUnapproved) {
@if (isInProgress() || isUnapproved()) {
<p-button
severity="secondary"
(onClick)="continueUpdateRegistration(registrationData().id)"
Expand All @@ -97,7 +96,7 @@ <h2 class="align-self-center word-break-word">
</div>
</div>

@if (isApproved) {
@if (isApproved()) {
<div class="flex flex-column min-w-max">
<h3 class="mb-3">{{ 'shared.resources.title' | translate }}</h3>
<osf-data-resources
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { MockComponents } from 'ng-mocks';
import { Store } from '@ngxs/store';

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

import { Mock } from 'vitest';

import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { Router } from '@angular/router';

import { RegistriesSelectors } from '@osf/features/registries/store';
import { CreateSchemaResponse, FetchAllSchemaResponses, RegistriesSelectors } from '@osf/features/registries/store';
import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum';
import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum';
import { UserPermissions } from '@osf/shared/enums/user-permissions.enum';
import { RegistrationCard } from '@shared/models/registration/registration-card.model';

import { MOCK_REGISTRATION } from '@testing/mocks/registration.mock';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
import {
BaseSetupOverrides,
mergeSignalOverrides,
provideMockStore,
SignalOverride,
} from '@testing/providers/store-provider.mock';

import { ContributorsListComponent } from '../contributors-list/contributors-list.component';
import { DataResourcesComponent } from '../data-resources/data-resources.component';
Expand All @@ -23,144 +34,189 @@ import { RegistrationCardComponent } from './registration-card.component';
describe('RegistrationCardComponent', () => {
let component: RegistrationCardComponent;
let fixture: ComponentFixture<RegistrationCardComponent>;
let store: Store;
let routerMock: RouterMockType;

const mockRegistrationData: RegistrationCard = MOCK_REGISTRATION;

beforeEach(() => {
const defaultSignals: SignalOverride[] = [
{ selector: RegistriesSelectors.getSchemaResponse, value: signal({ id: 'revision-id' }) },
];

type SetupOverrides = BaseSetupOverrides & {
registrationData?: RegistrationCard;
isDraft?: boolean;
};

function setup(overrides: SetupOverrides = {}): void {
routerMock = RouterMockBuilder.create().build();

TestBed.configureTestingModule({
imports: [
RegistrationCardComponent,
...MockComponents(StatusBadgeComponent, DataResourcesComponent, IconComponent, ContributorsListComponent),
],
providers: [
provideOSFCore(),
provideRouter([]),
provideMockStore({
signals: [{ selector: RegistriesSelectors.getSchemaResponse, value: signal(null) }],
}),
MockProvider(Router, routerMock),
provideMockStore({ signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides) }),
],
});

store = TestBed.inject(Store);
fixture = TestBed.createComponent(RegistrationCardComponent);
component = fixture.componentInstance;
});
fixture.componentRef.setInput('registrationData', overrides.registrationData ?? mockRegistrationData);
fixture.componentRef.setInput('isDraft', overrides.isDraft ?? false);
fixture.detectChanges();
}

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

it('should have registrationData as required input', () => {
fixture.componentRef.setInput('registrationData', mockRegistrationData);
expect(component.registrationData()).toEqual(mockRegistrationData);
expect(component).toBeTruthy();
});

it('should compute isAccepted correctly when reviewsState is Accepted', () => {
const testData = {
...mockRegistrationData,
reviewsState: RegistrationReviewStates.Accepted,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();
it.each([
[[UserPermissions.Read, UserPermissions.Write, UserPermissions.Admin], true, true],
[[UserPermissions.Write], false, true],
[[UserPermissions.Admin], true, false],
] as [UserPermissions[], boolean, boolean][])(
'should identify user permissions',
(currentUserPermissions, hasAdminAccess, hasWriteAccess) => {
setup({
registrationData: {
...mockRegistrationData,
currentUserPermissions,
},
});

expect(component.hasAdminAccess()).toBe(hasAdminAccess);
expect(component.hasWriteAccess()).toBe(hasWriteAccess);
}
);

it.each([
[RegistrationReviewStates.Accepted, true],
[RegistrationReviewStates.Pending, true],
[RegistrationReviewStates.Embargo, true],
[RegistrationReviewStates.Withdrawn, false],
] as [RegistrationReviewStates, boolean][])(
'should identify update-eligible review states',
(reviewsState, eligible) => {
setup({
registrationData: {
...mockRegistrationData,
reviewsState,
},
});

expect(component.isAccepted()).toBe(reviewsState === RegistrationReviewStates.Accepted);
expect(component.isPending()).toBe(reviewsState === RegistrationReviewStates.Pending);
expect(component.isEmbargo()).toBe(reviewsState === RegistrationReviewStates.Embargo);
expect(component.showButtons()).toBe(eligible);
}
);

it.each([
[RevisionReviewStates.Approved, true, false, false],
[RevisionReviewStates.Unapproved, false, true, false],
[RevisionReviewStates.RevisionInProgress, false, false, true],
] as [RevisionReviewStates, boolean, boolean, boolean][])(
'should identify revision states',
(revisionState, isApproved, isUnapproved, isInProgress) => {
setup({
registrationData: {
...mockRegistrationData,
revisionState,
},
});

expect(component.isApproved()).toBe(isApproved);
expect(component.isUnapproved()).toBe(isUnapproved);
expect(component.isInProgress()).toBe(isInProgress);
}
);

it.each([
[null, true],
[mockRegistrationData.id, true],
['different-root-id', false],
] as [string | null, boolean][])('should identify root registrations', (rootParentId, isRootRegistration) => {
setup({
registrationData: {
...mockRegistrationData,
rootParentId,
},
});

expect(component.isAccepted).toBe(true);
expect(component.isRootRegistration()).toBe(isRootRegistration);
});

it('should compute isPending correctly when reviewsState is Pending', () => {
const testData = {
...mockRegistrationData,
reviewsState: RegistrationReviewStates.Pending,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();
it.each([
['updates are disabled', { allowUpdates: false }],
['user lacks admin access', { currentUserPermissions: [UserPermissions.Write] }],
['registration is not root', { rootParentId: 'different-root-id' }],
] as [string, Partial<RegistrationCard>][])('should hide update buttons when %s', (_label, registrationData) => {
setup({
registrationData: {
...mockRegistrationData,
...registrationData,
},
});

expect(component.isPending).toBe(true);
expect(component.showButtons()).toBe(false);
});

it('should compute isApproved correctly when revisionState is Approved', () => {
const testData = {
...mockRegistrationData,
revisionState: RevisionReviewStates.Approved,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();
it('should dispatch create schema response and navigate to justification page on updateRegistration', () => {
setup();
(store.dispatch as Mock).mockClear();

expect(component.isApproved).toBe(true);
});
component.updateRegistration(mockRegistrationData.id);

it('should compute isUnapproved correctly when revisionState is Unapproved', () => {
const testData = {
...mockRegistrationData,
revisionState: RevisionReviewStates.Unapproved,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();

expect(component.isUnapproved).toBe(true);
expect(store.dispatch).toHaveBeenCalledWith(new CreateSchemaResponse(mockRegistrationData.id));
expect(routerMock.navigate).toHaveBeenCalledWith(['/registries/revisions/revision-id/justification']);
});

it('should compute isInProgress correctly when revisionState is RevisionInProgress', () => {
const testData = {
...mockRegistrationData,
revisionState: RevisionReviewStates.RevisionInProgress,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();

expect(component.isInProgress).toBe(true);
});
it('should dispatch fetch schema responses and navigate to review page for unapproved revision', () => {
setup({
registrationData: {
...mockRegistrationData,
revisionState: RevisionReviewStates.Unapproved,
},
});
(store.dispatch as Mock).mockClear();

it('should compute isAccepted as false when reviewsState is not Accepted', () => {
const testData = {
...mockRegistrationData,
reviewsState: RegistrationReviewStates.Pending,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();
component.continueUpdateRegistration(mockRegistrationData.id);

expect(component.isAccepted).toBe(false);
expect(store.dispatch).toHaveBeenCalledWith(new FetchAllSchemaResponses(mockRegistrationData.id));
expect(routerMock.navigate).toHaveBeenCalledWith(['/registries/revisions/revision-id/review']);
});

it('should compute isPending as false when reviewsState is not Pending', () => {
const testData = {
...mockRegistrationData,
reviewsState: RegistrationReviewStates.Accepted,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();

expect(component.isPending).toBe(false);
});
it('should dispatch fetch schema responses and navigate to justification page for non-unapproved revision', () => {
setup({
registrationData: {
...mockRegistrationData,
revisionState: RevisionReviewStates.RevisionInProgress,
},
});
(store.dispatch as Mock).mockClear();

it('should compute isApproved as false when revisionState is not Approved', () => {
const testData = {
...mockRegistrationData,
revisionState: RevisionReviewStates.Unapproved,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();
component.continueUpdateRegistration(mockRegistrationData.id);

expect(component.isApproved).toBe(false);
expect(store.dispatch).toHaveBeenCalledWith(new FetchAllSchemaResponses(mockRegistrationData.id));
expect(routerMock.navigate).toHaveBeenCalledWith(['/registries/revisions/revision-id/justification']);
});

it('should compute isUnapproved as false when revisionState is not Unapproved', () => {
const testData = {
...mockRegistrationData,
revisionState: RevisionReviewStates.Approved,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();

expect(component.isUnapproved).toBe(false);
});
it('should not navigate when schema response is not present', () => {
setup({
selectorOverrides: [{ selector: RegistriesSelectors.getSchemaResponse, value: signal(null) }],
});
(store.dispatch as Mock).mockClear();

it('should compute isInProgress as false when revisionState is not RevisionInProgress', () => {
const testData = {
...mockRegistrationData,
revisionState: RevisionReviewStates.Approved,
};
fixture.componentRef.setInput('registrationData', testData);
fixture.detectChanges();
component.updateRegistration(mockRegistrationData.id);

expect(component.isInProgress).toBe(false);
expect(store.dispatch).toHaveBeenCalledWith(new CreateSchemaResponse(mockRegistrationData.id));
expect(routerMock.navigate).not.toHaveBeenCalled();
});
});
Loading
Loading