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
Expand Up @@ -35,7 +35,7 @@ <h2 class="flex align-items-center gap-2">
}
</div>

<div class="flex flex-wrap gap-1">
<div class="flex gap-1">
<p class="font-bold">{{ 'common.labels.contributors' | translate }}:</p>

<osf-contributors-list
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,104 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Store } from '@ngxs/store';

import { MockProvider } from 'ng-mocks';

import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { of } from 'rxjs';

import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';

import { ToastService } from '@osf/shared/services/toast.service';
import { DeleteNodeLink, NodeLinksSelectors } from '@osf/shared/stores/node-links';

import { ProjectOverviewSelectors } from '../../store';

import { DeleteNodeLinkDialogComponent } from './delete-node-link-dialog.component';

describe.skip('DeleteNodeLinkDialogComponent', () => {
import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock';
import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock';
import { ToastServiceMock } from '@testing/mocks/toast.service.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';
import { provideMockStore } from '@testing/providers/store-provider.mock';

describe('DeleteNodeLinkDialogComponent', () => {
let component: DeleteNodeLinkDialogComponent;
let fixture: ComponentFixture<DeleteNodeLinkDialogComponent>;
let store: jest.Mocked<Store>;
let dialogRef: jest.Mocked<DynamicDialogRef>;
let dialogConfig: jest.Mocked<DynamicDialogConfig>;
let toastService: jest.Mocked<ToastService>;

const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'test-project-id' };
const mockCurrentLink = { ...MOCK_NODE_WITH_ADMIN, id: 'linked-resource-id', title: 'Linked Resource' };

beforeEach(async () => {
dialogConfig = {
data: { currentLink: mockCurrentLink },
} as jest.Mocked<DynamicDialogConfig>;

await TestBed.configureTestingModule({
imports: [DeleteNodeLinkDialogComponent],
imports: [DeleteNodeLinkDialogComponent, OSFTestingModule],
providers: [
DynamicDialogRefMock,
ToastServiceMock,
MockProvider(DynamicDialogConfig, dialogConfig),
provideMockStore({
signals: [
{ selector: ProjectOverviewSelectors.getProject, value: mockProject },
{ selector: NodeLinksSelectors.getNodeLinksSubmitting, value: false },
],
}),
],
}).compileComponents();

store = TestBed.inject(Store) as jest.Mocked<Store>;
store.dispatch = jest.fn().mockReturnValue(of(true));
fixture = TestBed.createComponent(DeleteNodeLinkDialogComponent);
component = fixture.componentInstance;
dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked<DynamicDialogRef>;
toastService = TestBed.inject(ToastService) as jest.Mocked<ToastService>;
fixture.detectChanges();
});

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

it('should initialize currentProject selector', () => {
expect(component.currentProject()).toEqual(mockProject);
});

it('should initialize isSubmitting selector', () => {
expect(component.isSubmitting()).toBe(false);
});

it('should initialize actions with deleteNodeLink mapping', () => {
expect(component.actions.deleteNodeLink).toBeDefined();
});

it('should dispatch DeleteNodeLink action with correct parameters on successful deletion', () => {
component.handleDeleteNodeLink();

expect(store.dispatch).toHaveBeenCalledWith(expect.any(DeleteNodeLink));
const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof DeleteNodeLink);
expect(call).toBeDefined();
const action = call[0] as DeleteNodeLink;
expect(action.projectId).toBe('test-project-id');
expect(action.linkedResource).toEqual(mockCurrentLink);
});

it('should show success toast on successful deletion', fakeAsync(() => {
component.handleDeleteNodeLink();
tick();

expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.deleteNodeLink.success');
}));

it('should close dialog with hasChanges true on successful deletion', fakeAsync(() => {
component.handleDeleteNodeLink();
tick();

expect(dialogRef.close).toHaveBeenCalledWith({ hasChanges: true });
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core';

import { ToastService } from '@osf/shared/services/toast.service';
import { DeleteNodeLink, GetLinkedResources, NodeLinksSelectors } from '@osf/shared/stores/node-links';
import { DeleteNodeLink, NodeLinksSelectors } from '@osf/shared/stores/node-links';

import { ProjectOverviewSelectors } from '../../store';

Expand All @@ -28,7 +28,7 @@ export class DeleteNodeLinkDialogComponent {
currentProject = select(ProjectOverviewSelectors.getProject);
isSubmitting = select(NodeLinksSelectors.getNodeLinksSubmitting);

actions = createDispatchMap({ deleteNodeLink: DeleteNodeLink, getLinkedResources: GetLinkedResources });
actions = createDispatchMap({ deleteNodeLink: DeleteNodeLink });

handleDeleteNodeLink(): void {
const project = this.currentProject();
Expand All @@ -38,9 +38,8 @@ export class DeleteNodeLinkDialogComponent {

this.actions.deleteNodeLink(project.id, currentLink).subscribe({
next: () => {
this.dialogRef.close();
this.actions.getLinkedResources(project.id);
this.toastService.showSuccess('project.overview.dialog.toast.deleteNodeLink.success');
this.dialogRef.close({ hasChanges: true });
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,14 @@
<p-table
class="link-project-table"
[value]="isCurrentTableLoading() ? skeletonData : currentTableItems()"
[rows]="tableRows"
[first]="(currentPage() - 1) * 10"
[paginator]="currentTotalCount() > tableRows"
[totalRecords]="currentTotalCount()"
[rows]="tableParams().rows"
[first]="tableParams().firstRowIndex"
[paginator]="tableParams().paginator"
[totalRecords]="tableParams().totalRecords"
paginatorDropdownAppendTo="body"
[resizableColumns]="true"
[autoLayout]="true"
[scrollable]="true"
[sortMode]="'single'"
[scrollable]="tableParams().scrollable"
[lazy]="true"
[lazyLoadOnInit]="true"
(onPage)="onPageChange($event)"
Expand All @@ -74,7 +73,7 @@
<p-checkbox
[disabled]="isNodeLinksSubmitting()"
[inputId]="item.id"
[ngModel]="isItemLinked()(item.id)"
[ngModel]="isItemLinked(item.id)"
[binary]="true"
(onChange)="handleToggleNodeLink(item)"
/>
Expand All @@ -84,15 +83,19 @@
<td>{{ item.dateCreated | date: 'MMM d, y' }}</td>
<td>{{ item.dateModified | date: 'MMM d, y' }}</td>
<td>
@for (contributor of item.contributors; track contributor.id) {
@for (contributor of item.contributors.slice(0, 3); track contributor.id) {
{{ contributor.fullName }}{{ $last ? '' : ', ' }}
}
@if (item.contributors.length > 3) {
, {{ 'common.labels.and' | translate }} {{ item.contributors.length - 3 }}
{{ 'common.labels.more' | translate }}
}
</td>
</tr>
} @else {
<tr class="loading-row">
<td colspan="4">
<p-skeleton width="100%" height="3.3rem" borderRadius="0" />
<p-skeleton width="100%" height="2.75rem" borderRadius="0" />
</td>
</tr>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,185 @@
import { Store } from '@ngxs/store';

import { MockComponents } from 'ng-mocks';

import { TablePageEvent } from 'primeng/table';

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
import { ResourceSearchMode } from '@osf/shared/enums/resource-search-mode.enum';
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
import { MyResourcesSelectors } from '@osf/shared/stores/my-resources';
import { NodeLinksSelectors } from '@osf/shared/stores/node-links';
import { MyResourcesItem } from '@shared/models/my-resources/my-resources.models';

import { ProjectOverviewSelectors } from '../../store';

import { LinkResourceDialogComponent } from './link-resource-dialog.component';

describe.skip('LinkProjectDialogComponent', () => {
import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock';
import {
MOCK_MY_RESOURCES_ITEM_PROJECT,
MOCK_MY_RESOURCES_ITEM_PROJECT_PRIVATE,
MOCK_MY_RESOURCES_ITEM_REGISTRATION,
} from '@testing/mocks/my-resources.mock';
import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';
import { provideMockStore } from '@testing/providers/store-provider.mock';

describe('LinkResourceDialogComponent', () => {
let component: LinkResourceDialogComponent;
let fixture: ComponentFixture<LinkResourceDialogComponent>;
let store: Store;
let dialogRef: { close: jest.Mock };

const mockProjects: MyResourcesItem[] = [MOCK_MY_RESOURCES_ITEM_PROJECT, MOCK_MY_RESOURCES_ITEM_PROJECT_PRIVATE];

const mockRegistrations: MyResourcesItem[] = [MOCK_MY_RESOURCES_ITEM_REGISTRATION];

const mockLinkedResources = [{ ...MOCK_NODE_WITH_ADMIN, id: 'project-1', title: 'Linked Project 1' }];

const mockCurrentProject = { ...MOCK_NODE_WITH_ADMIN, id: 'current-project-id' };

beforeEach(async () => {
dialogRef = { close: jest.fn() };

await TestBed.configureTestingModule({
imports: [LinkResourceDialogComponent, ...MockComponents(SearchInputComponent)],
imports: [LinkResourceDialogComponent, OSFTestingModule, ...MockComponents(SearchInputComponent)],
providers: [
provideMockStore({
signals: [
{ selector: MyResourcesSelectors.getProjects, value: mockProjects },
{ selector: MyResourcesSelectors.getProjectsLoading, value: false },
{ selector: MyResourcesSelectors.getRegistrations, value: mockRegistrations },
{ selector: MyResourcesSelectors.getRegistrationsLoading, value: false },
{ selector: MyResourcesSelectors.getTotalProjects, value: 2 },
{ selector: MyResourcesSelectors.getTotalRegistrations, value: 1 },
{ selector: NodeLinksSelectors.getNodeLinksSubmitting, value: false },
{ selector: NodeLinksSelectors.getLinkedResources, value: mockLinkedResources },
{ selector: ProjectOverviewSelectors.getProject, value: mockCurrentProject },
],
}),
{ provide: DynamicDialogRefMock.provide, useValue: dialogRef },
],
}).compileComponents();

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

it('should initialize with default values', () => {
expect(component.currentPage()).toBe(1);
expect(component.searchMode()).toBe(ResourceSearchMode.User);
expect(component.resourceType()).toBe(ResourceType.Project);
expect(component.searchControl.value).toBe('');
});

it('should initialize skeleton data with correct length', () => {
expect(component.skeletonData.length).toBe(component.tableRows);
});

it('should compute currentResourceId from currentProject', () => {
expect(component.currentResourceId()).toBe('current-project-id');
});

it('should compute isCurrentTableLoading for registrations', () => {
component.resourceType.set(ResourceType.Registration);
fixture.detectChanges();
expect(component.isCurrentTableLoading()).toBe(false);
});

it('should create', () => {
expect(component).toBeTruthy();
it('should compute currentTotalCount for registrations', () => {
component.resourceType.set(ResourceType.Registration);
fixture.detectChanges();
expect(component.currentTotalCount()).toBe(1);
});

it('should compute tableParams correctly', () => {
const params = component.tableParams();
expect(params.rows).toBe(component.tableRows);
expect(params.firstRowIndex).toBe(0);
expect(params.paginator).toBe(false);
expect(params.totalRecords).toBe(2);
});

it('should compute linkedResourceIds as a Set', () => {
const linkedIds = component.linkedResourceIds();
expect(linkedIds).toBeInstanceOf(Set);
expect(linkedIds.has('project-1')).toBe(true);
expect(linkedIds.has('project-2')).toBe(false);
});

it('should update searchMode and reset to first page', () => {
component.currentPage.set(2);
component.onSearchModeChange(ResourceSearchMode.All);

expect(component.searchMode()).toBe(ResourceSearchMode.All);
expect(component.currentPage()).toBe(1);
});

it('should update resourceType and reset to first page', () => {
component.currentPage.set(2);
component.onObjectTypeChange(ResourceType.Registration);

expect(component.resourceType()).toBe(ResourceType.Registration);
expect(component.currentPage()).toBe(1);
});

it('should update currentPage and trigger search', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch');
const event: TablePageEvent = {
first: 6,
rows: 6,
};

component.onPageChange(event);

expect(component.currentPage()).toBe(2);
expect(dispatchSpy).toHaveBeenCalled();
});

it('should return true for linked items', () => {
expect(component.isItemLinked('project-1')).toBe(true);
});

it('should return false for non-linked items', () => {
expect(component.isItemLinked('project-2')).toBe(false);
});

it('should close the dialog', () => {
component.handleCloseDialog();
expect(dialogRef.close).toHaveBeenCalled();
});

it('should trigger search when searchMode changes', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch');

component.onSearchModeChange(ResourceSearchMode.All);

expect(dispatchSpy).toHaveBeenCalled();
});

it('should trigger search when resourceType changes', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch');

component.onObjectTypeChange(ResourceType.Registration);

expect(dispatchSpy).toHaveBeenCalled();
});

it('should handle page change with zero first value', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch');
const event: TablePageEvent = {
first: 0,
rows: 6,
};

component.onPageChange(event);

expect(component.currentPage()).toBe(1);
expect(dispatchSpy).toHaveBeenCalled();
});
});
Loading