Skip to content

Commit 7eaa041

Browse files
authored
Test/565 my projects (#334)
* test(my-projects): added unit tests * test(create-project-dialog): fixed errors * test(my-projects): fixed errors * test(create-project-dialog): fixed
1 parent 1729d0e commit 7eaa041

File tree

6 files changed

+242
-35
lines changed

6 files changed

+242
-35
lines changed

jest.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ module.exports = {
7070
'<rootDir>/src/app/features/files/components',
7171
'<rootDir>/src/app/features/files/pages/community-metadata',
7272
'<rootDir>/src/app/features/files/pages/file-detail',
73-
'<rootDir>/src/app/features/my-projects/',
7473
'<rootDir>/src/app/features/preprints/',
7574
'<rootDir>/src/app/features/project/contributors/',
7675
'<rootDir>/src/app/features/project/overview/',
Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,89 @@
1-
import { provideStore } from '@ngxs/store';
1+
import { Store } from '@ngxs/store';
22

3-
import { TranslatePipe } from '@ngx-translate/core';
4-
import { MockPipe, MockProvider } from 'ng-mocks';
3+
import { MockComponent, MockProvider } from 'ng-mocks';
54

65
import { DynamicDialogRef } from 'primeng/dynamicdialog';
76

8-
import { provideHttpClient } from '@angular/common/http';
9-
import { provideHttpClientTesting } from '@angular/common/http/testing';
7+
import { of } from 'rxjs';
8+
109
import { ComponentFixture, TestBed } from '@angular/core/testing';
1110

12-
import { TranslateServiceMock } from '@shared/mocks';
13-
import { MyResourcesState } from '@shared/stores';
14-
import { InstitutionsState } from '@shared/stores/institutions';
15-
import { RegionsState } from '@shared/stores/regions';
11+
import { MY_PROJECTS_TABLE_PARAMS } from '@osf/shared/constants';
12+
import { ProjectFormControls } from '@osf/shared/enums';
13+
import { MOCK_STORE } from '@osf/shared/mocks';
14+
import { CreateProject, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores';
15+
import { AddProjectFormComponent } from '@shared/components';
1616

1717
import { CreateProjectDialogComponent } from './create-project-dialog.component';
1818

19+
import { OSFTestingModule } from '@testing/osf.testing.module';
20+
1921
describe('CreateProjectDialogComponent', () => {
2022
let component: CreateProjectDialogComponent;
2123
let fixture: ComponentFixture<CreateProjectDialogComponent>;
24+
let store: Store;
25+
let dialogRef: DynamicDialogRef;
26+
27+
const fillValidForm = (
28+
title = 'My Project',
29+
description = 'Some description',
30+
template = 'tmpl-1',
31+
storageLocation = 'osfstorage',
32+
affiliations: string[] = ['aff-1', 'aff-2']
33+
) => {
34+
component.projectForm.patchValue({
35+
[ProjectFormControls.Title]: title,
36+
[ProjectFormControls.Description]: description,
37+
[ProjectFormControls.Template]: template,
38+
[ProjectFormControls.StorageLocation]: storageLocation,
39+
[ProjectFormControls.Affiliations]: affiliations,
40+
});
41+
};
2242

2343
beforeEach(async () => {
44+
(MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => {
45+
if (selector === MyResourcesSelectors.isProjectSubmitting) return () => false;
46+
return () => undefined;
47+
});
48+
2449
await TestBed.configureTestingModule({
25-
imports: [CreateProjectDialogComponent, MockPipe(TranslatePipe)],
26-
providers: [
27-
provideStore([MyResourcesState, InstitutionsState, RegionsState]),
28-
provideHttpClient(),
29-
provideHttpClientTesting(),
30-
TranslateServiceMock,
31-
MockProvider(DynamicDialogRef),
32-
],
50+
imports: [CreateProjectDialogComponent, OSFTestingModule, MockComponent(AddProjectFormComponent)],
51+
providers: [MockProvider(Store, MOCK_STORE)],
3352
}).compileComponents();
3453

3554
fixture = TestBed.createComponent(CreateProjectDialogComponent);
3655
component = fixture.componentInstance;
56+
57+
store = TestBed.inject(Store);
58+
dialogRef = TestBed.inject(DynamicDialogRef);
59+
3760
fixture.detectChanges();
3861
});
3962

4063
it('should create', () => {
4164
expect(component).toBeTruthy();
4265
});
66+
67+
it('should mark all controls touched and not dispatch when form is invalid', () => {
68+
const markAllSpy = jest.spyOn(component.projectForm, 'markAllAsTouched');
69+
70+
(store.dispatch as unknown as jest.Mock).mockClear();
71+
72+
component.submitForm();
73+
74+
expect(markAllSpy).toHaveBeenCalled();
75+
expect(store.dispatch).not.toHaveBeenCalled();
76+
});
77+
78+
it('should submit, refresh list and close dialog when form is valid', () => {
79+
fillValidForm('Title', 'Desc', 'Tpl', 'Storage', ['a1']);
80+
81+
(MOCK_STORE.dispatch as jest.Mock).mockReturnValue(of(undefined));
82+
83+
component.submitForm();
84+
85+
expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new CreateProject('Title', 'Desc', 'Tpl', 'Storage', ['a1']));
86+
expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new GetMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {}));
87+
expect((dialogRef as any).close).toHaveBeenCalled();
88+
});
4389
});
Lines changed: 168 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,202 @@
1-
import { provideStore } from '@ngxs/store';
1+
import { Store } from '@ngxs/store';
22

3-
import { TranslateModule } from '@ngx-translate/core';
4-
import { MockProvider } from 'ng-mocks';
3+
import { MockComponents, MockProvider } from 'ng-mocks';
54

65
import { DialogService } from 'primeng/dynamicdialog';
76

87
import { BehaviorSubject, of } from 'rxjs';
98

10-
import { provideHttpClient } from '@angular/common/http';
11-
import { provideHttpClientTesting } from '@angular/common/http/testing';
129
import { ComponentFixture, TestBed } from '@angular/core/testing';
13-
import { ActivatedRoute } from '@angular/router';
10+
import { ActivatedRoute, Router } from '@angular/router';
1411

12+
import { MyProjectsTab } from '@osf/features/my-projects/enums';
13+
import { SortOrder } from '@osf/shared/enums';
1514
import { IS_MEDIUM } from '@osf/shared/helpers';
16-
import { MyResourcesState } from '@shared/stores/my-resources/my-resources.state';
17-
18-
import { InstitutionsState } from '../../shared/stores/institutions';
15+
import { MOCK_STORE } from '@osf/shared/mocks';
16+
import { BookmarksSelectors, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores';
17+
import { MyProjectsTableComponent, SelectComponent, SubHeaderComponent } from '@shared/components';
1918

2019
import { MyProjectsComponent } from './my-projects.component';
2120

22-
describe.skip('MyProjectsComponent', () => {
21+
import { OSFTestingModule } from '@testing/osf.testing.module';
22+
23+
describe('MyProjectsComponent', () => {
2324
let component: MyProjectsComponent;
2425
let fixture: ComponentFixture<MyProjectsComponent>;
2526
let isMediumSubject: BehaviorSubject<boolean>;
27+
let queryParamsSubject: BehaviorSubject<Record<string, string>>;
28+
let store: jest.Mocked<Store>;
29+
let router: jest.Mocked<Router>;
2630

2731
beforeEach(async () => {
2832
isMediumSubject = new BehaviorSubject<boolean>(false);
33+
queryParamsSubject = new BehaviorSubject<Record<string, string>>({});
34+
35+
(MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => {
36+
if (
37+
selector === MyResourcesSelectors.getTotalProjects ||
38+
selector === MyResourcesSelectors.getTotalRegistrations ||
39+
selector === MyResourcesSelectors.getTotalPreprints ||
40+
selector === MyResourcesSelectors.getTotalBookmarks
41+
)
42+
return () => 0;
43+
if (selector === BookmarksSelectors.getBookmarksCollectionId) return () => null;
44+
if (
45+
selector === MyResourcesSelectors.getProjects ||
46+
selector === MyResourcesSelectors.getRegistrations ||
47+
selector === MyResourcesSelectors.getPreprints ||
48+
selector === MyResourcesSelectors.getBookmarks
49+
)
50+
return () => [];
51+
return () => undefined;
52+
});
2953

3054
await TestBed.configureTestingModule({
31-
imports: [MyProjectsComponent, TranslateModule.forRoot()],
55+
imports: [
56+
MyProjectsComponent,
57+
OSFTestingModule,
58+
...MockComponents(SubHeaderComponent, MyProjectsTableComponent, SelectComponent),
59+
],
3260
providers: [
33-
provideStore([MyResourcesState, InstitutionsState]),
34-
provideHttpClient(),
35-
provideHttpClientTesting(),
36-
MockProvider(DialogService),
37-
MockProvider(ActivatedRoute, { queryParams: of({}) }),
61+
MockProvider(Store, MOCK_STORE),
62+
MockProvider(DialogService, { open: jest.fn() }),
63+
MockProvider(ActivatedRoute, { queryParams: queryParamsSubject.asObservable() }),
64+
MockProvider(Router, { navigate: jest.fn() }),
3865
MockProvider(IS_MEDIUM, isMediumSubject),
3966
],
4067
}).compileComponents();
4168

4269
fixture = TestBed.createComponent(MyProjectsComponent);
4370
component = fixture.componentInstance;
71+
store = TestBed.inject(Store) as jest.Mocked<Store>;
72+
router = TestBed.inject(Router) as jest.Mocked<Router>;
73+
74+
store.dispatch.mockReturnValue(of(undefined));
75+
4476
fixture.detectChanges();
4577
});
4678

4779
it('should create', () => {
4880
expect(component).toBeTruthy();
4981
});
82+
83+
it('should update component state from query params', () => {
84+
component.updateComponentState({ page: 2, size: 20, search: 'q', sortColumn: 'name', sortOrder: SortOrder.Desc });
85+
86+
expect(component.currentPage()).toBe(2);
87+
expect(component.currentPageSize()).toBe(20);
88+
expect(component.searchControl.value).toBe('q');
89+
expect(component.sortColumn()).toBe('name');
90+
expect(component.sortOrder()).toBe(SortOrder.Desc);
91+
expect(component.tableParams().firstRowIndex).toBe(20);
92+
expect(component.tableParams().rows).toBe(20);
93+
});
94+
95+
it('should create filters depending on tab', () => {
96+
const filtersProjects = component.createFilters({
97+
page: 1,
98+
size: 10,
99+
search: 's',
100+
sortColumn: 'name',
101+
sortOrder: SortOrder.Asc,
102+
});
103+
expect(filtersProjects.searchValue).toBe('s');
104+
expect(filtersProjects.searchFields).toEqual(['title', 'tags', 'description']);
105+
106+
component.selectedTab.set(MyProjectsTab.Preprints);
107+
const filtersPreprints = component.createFilters({
108+
page: 2,
109+
size: 25,
110+
search: 's2',
111+
sortColumn: 'date',
112+
sortOrder: SortOrder.Desc,
113+
});
114+
expect(filtersPreprints.searchFields).toEqual(['title', 'tags']);
115+
});
116+
117+
it('should fetch data for projects tab and stop loading', () => {
118+
jest.clearAllMocks();
119+
store.dispatch.mockReturnValue(of(undefined));
120+
121+
component.fetchDataForCurrentTab({ page: 1, size: 10, search: 's', sortColumn: 'name', sortOrder: SortOrder.Asc });
122+
123+
expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetMyProjects));
124+
expect(component.isLoading()).toBe(false);
125+
});
126+
127+
it('should handle search and update query params', () => {
128+
jest.clearAllMocks();
129+
queryParamsSubject.next({ sortColumn: 'name', sortOrder: 'desc', size: '25' });
130+
131+
component.handleSearch('query');
132+
133+
expect(router.navigate).toHaveBeenCalledWith([], {
134+
relativeTo: expect.anything(),
135+
queryParams: { page: '1', size: '25', search: 'query', sortColumn: 'name', sortOrder: 'desc' },
136+
});
137+
});
138+
139+
it('should paginate and update query params', () => {
140+
jest.clearAllMocks();
141+
queryParamsSubject.next({ sortColumn: 'title', sortOrder: 'asc' });
142+
143+
component.onPageChange({ first: 30, rows: 15 } as any);
144+
145+
expect(router.navigate).toHaveBeenCalledWith([], {
146+
relativeTo: expect.anything(),
147+
queryParams: { page: '3', size: '15', sortColumn: 'title', sortOrder: 'asc' },
148+
});
149+
});
150+
151+
it('should sort and update query params', () => {
152+
jest.clearAllMocks();
153+
154+
component.onSort({ field: 'updated', order: SortOrder.Desc } as any);
155+
156+
expect(router.navigate).toHaveBeenCalledWith([], {
157+
relativeTo: expect.anything(),
158+
queryParams: { sortColumn: 'updated', sortOrder: 'desc' },
159+
});
160+
});
161+
162+
it('should clear and reset on tab change', () => {
163+
jest.clearAllMocks();
164+
queryParamsSubject.next({ size: '50' });
165+
166+
component.onTabChange(1);
167+
168+
expect(router.navigate).toHaveBeenCalledWith([], {
169+
relativeTo: expect.anything(),
170+
queryParams: { page: '1', size: '50' },
171+
});
172+
173+
expect(store.dispatch).toHaveBeenCalled();
174+
});
175+
176+
it('should open create project dialog with responsive width', () => {
177+
const openSpy = jest.spyOn(component.dialogService, 'open');
178+
179+
isMediumSubject.next(false);
180+
component.createProject();
181+
expect(openSpy).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ width: '95vw' }));
182+
183+
openSpy.mockClear();
184+
isMediumSubject.next(true);
185+
component.createProject();
186+
expect(openSpy).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ width: '850px' }));
187+
});
188+
189+
it('should navigate to project and set active project', () => {
190+
const project = { id: 'p1' } as any;
191+
component.navigateToProject(project);
192+
expect(component.activeProject()).toEqual(project);
193+
expect(router.navigate).toHaveBeenCalledWith(['p1']);
194+
});
195+
196+
it('should navigate to registry and set active project', () => {
197+
const reg = { id: 'r1' } as any;
198+
component.navigateToRegistry(reg);
199+
expect(component.activeProject()).toEqual(reg);
200+
expect(router.navigate).toHaveBeenCalledWith(['r1']);
201+
});
50202
});

src/app/shared/models/view-only-links/view-only-link-response.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MetaJsonApi } from '../common';
2-
import { UserDataJsonApi } from '../user';
32
import { BaseNodeDataJsonApi } from '../nodes';
3+
import { UserDataJsonApi } from '../user';
44

55
export interface ViewOnlyLinksResponseJsonApi {
66
data: ViewOnlyLinkJsonApi[];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { DynamicDialogRef } from 'primeng/dynamicdialog';
2+
3+
export const DynamicDialogRefMock = {
4+
provide: DynamicDialogRef,
5+
useValue: {
6+
close: jest.fn(),
7+
},
8+
};

src/testing/osf.testing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { BrowserModule } from '@angular/platform-browser';
88
import { NoopAnimationsModule, provideNoopAnimations } from '@angular/platform-browser/animations';
99
import { provideRouter } from '@angular/router';
1010

11+
import { DynamicDialogRefMock } from './mocks/dynamic-dialog-ref.mock';
1112
import { EnvironmentTokenMock } from './mocks/environment.token.mock';
1213
import { StoreMock } from './mocks/store.mock';
1314
import { ToastServiceMock } from './mocks/toast.service.mock';
@@ -31,6 +32,7 @@ import { TranslationServiceMock } from './mocks/translation.service.mock';
3132
provideHttpClient(withInterceptorsFromDi()),
3233
provideHttpClientTesting(),
3334
TranslationServiceMock,
35+
DynamicDialogRefMock,
3436
EnvironmentTokenMock,
3537
ToastServiceMock,
3638
],

0 commit comments

Comments
 (0)