Skip to content

Commit 4bec941

Browse files
committed
test(files): added unit tests
1 parent 589ecd6 commit 4bec941

23 files changed

Lines changed: 2089 additions & 386 deletions

src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { Store } from '@ngxs/store';
22

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

5-
import { DynamicDialogRef } from 'primeng/dynamicdialog';
6-
75
import { of, Subject, throwError } from 'rxjs';
86

97
import { Mock } from 'vitest';
@@ -35,6 +33,7 @@ import {
3533
import { MOCK_USER } from '@testing/mocks/data.mock';
3634
import { provideOSFCore } from '@testing/osf.testing.provider';
3735
import {
36+
CustomDialogServiceMock,
3837
CustomDialogServiceMockBuilder,
3938
CustomDialogServiceMockType,
4039
} from '@testing/providers/custom-dialog-provider.mock';
@@ -65,11 +64,8 @@ describe('InstitutionsProjectsComponent', () => {
6564

6665
const mockInstitution = { ...MOCK_ADMIN_INSTITUTIONS_INSTITUTION, id: 'inst-1' };
6766

68-
function createDialogRef<T>(onClose$: Subject<T>): DynamicDialogRef {
69-
return {
70-
onClose: onClose$.asObservable(),
71-
close: vi.fn(),
72-
} as unknown as DynamicDialogRef;
67+
function createDialogRef<T>(onClose$: Subject<T>) {
68+
return CustomDialogServiceMock.dialogRefWithClose(onClose$.asObservable());
7369
}
7470

7571
function createIconClickEvent(): TableIconClickEvent {

src/app/features/files/components/file-keywords/file-keywords.component.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,26 @@ describe('FileKeywordsComponent', () => {
126126

127127
expect(store.dispatch).not.toHaveBeenCalled();
128128
});
129+
130+
it('should not dispatch update when cannot edit tags', () => {
131+
setup({
132+
selectorOverrides: [{ selector: FilesSelectors.hasWriteAccess, value: false }],
133+
});
134+
(store.dispatch as Mock).mockClear();
135+
136+
component.deleteTag('tag1');
137+
138+
expect(store.dispatch).not.toHaveBeenCalled();
139+
});
140+
141+
it('should not dispatch update when file guid is missing', () => {
142+
setup({
143+
selectorOverrides: [{ selector: FilesSelectors.getOpenedFile, value: null }],
144+
});
145+
(store.dispatch as Mock).mockClear();
146+
147+
component.deleteTag('tag1');
148+
149+
expect(store.dispatch).not.toHaveBeenCalled();
150+
});
129151
});

src/app/features/files/components/file-metadata/file-metadata.component.spec.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Store } from '@ngxs/store';
22

33
import { MockProvider } from 'ng-mocks';
44

5+
import { Subject } from 'rxjs';
6+
57
import { Mock } from 'vitest';
68

79
import { ComponentFixture, TestBed } from '@angular/core/testing';
@@ -38,10 +40,11 @@ describe('FileMetadataComponent', () => {
3840

3941
interface SetupOverrides extends BaseSetupOverrides {
4042
url?: string;
43+
dialogServiceMock?: CustomDialogServiceMockType;
4144
}
4245

4346
function setup(options: SetupOverrides = {}) {
44-
customDialogService = CustomDialogServiceMock.simple();
47+
customDialogService = options.dialogServiceMock ?? CustomDialogServiceMock.simple();
4548
const defaultSignals = [
4649
{ selector: FilesSelectors.getFileCustomMetadata, value: mockFileMetadata },
4750
{ selector: FilesSelectors.isFileMetadataLoading, value: false },
@@ -127,4 +130,68 @@ describe('FileMetadataComponent', () => {
127130
setup({ url: '/test?view_only=abc' });
128131
expect(component.hasViewOnly).toBe(true);
129132
});
133+
134+
it('should open metadata url when file guid exists', () => {
135+
setup({ routeParams: { fileGuid: 'guid-123' } });
136+
const focus = vi.fn();
137+
const openSpy = vi.spyOn(window, 'open').mockReturnValue({ focus } as unknown as Window);
138+
139+
component.downloadFileMetadata();
140+
141+
expect(openSpy).toHaveBeenCalledWith(expect.stringMatching(/\/metadata\/guid-123$/));
142+
expect(focus).toHaveBeenCalled();
143+
});
144+
145+
it('should not open metadata url when file guid is missing', () => {
146+
setup({ routeParams: {} });
147+
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
148+
149+
component.downloadFileMetadata();
150+
151+
expect(openSpy).not.toHaveBeenCalled();
152+
});
153+
154+
it('should call setFileMetadata when edit dialog closes with metadata', () => {
155+
const metadataChange$ = new Subject<PatchFileMetadata>();
156+
customDialogService = CustomDialogServiceMock.create()
157+
.withOpen(
158+
vi.fn().mockReturnValue({
159+
onClose: metadataChange$,
160+
close: vi.fn(),
161+
})
162+
)
163+
.build();
164+
setup({ dialogServiceMock: customDialogService });
165+
const setFileMetadataSpy = vi.spyOn(component, 'setFileMetadata');
166+
const formValues: PatchFileMetadata = {
167+
title: 'Edited',
168+
description: 'Edited desc',
169+
resource_type_general: 'Dataset',
170+
language: 'en',
171+
};
172+
173+
component.openEditFileMetadataDialog();
174+
metadataChange$.next(formValues);
175+
176+
expect(setFileMetadataSpy).toHaveBeenCalledWith(formValues);
177+
});
178+
179+
it('should not call setFileMetadata when edit dialog closes with empty value', () => {
180+
const metadataChange$ = new Subject<PatchFileMetadata | null>();
181+
customDialogService = CustomDialogServiceMock.create()
182+
.withOpen(
183+
vi.fn().mockReturnValue({
184+
onClose: metadataChange$,
185+
close: vi.fn(),
186+
})
187+
)
188+
.build();
189+
setup({ dialogServiceMock: customDialogService });
190+
const setFileMetadataSpy = vi.spyOn(component, 'setFileMetadata');
191+
192+
component.openEditFileMetadataDialog();
193+
metadataChange$.next(null);
194+
195+
expect(setFileMetadataSpy).not.toHaveBeenCalled();
196+
});
130197
});
Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,158 @@
1+
import { Store } from '@ngxs/store';
2+
13
import { MockComponent } from 'ng-mocks';
24

5+
import { Mock } from 'vitest';
6+
37
import { ComponentFixture, TestBed } from '@angular/core/testing';
48

9+
import { SelectComponent } from '@osf/shared/components/select/select.component';
10+
import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum';
11+
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
12+
import { UserPermissions } from '@osf/shared/enums/user-permissions.enum';
13+
import { FileFolderModel } from '@osf/shared/models/files/file-folder.model';
14+
import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model';
15+
16+
import { MOCK_CONFIGURED_ADDON } from '@testing/mocks/configured-addon.mock';
17+
import { OSF_FILE_MOCK } from '@testing/mocks/osf-file.mock';
518
import { provideOSFCore } from '@testing/osf.testing.provider';
19+
import {
20+
BaseSetupOverrides,
21+
mergeSignalOverrides,
22+
provideMockStore,
23+
SignalOverride,
24+
} from '@testing/providers/store-provider.mock';
625

7-
import { SelectComponent } from '../../../../shared/components/select/select.component';
26+
import { FileProvider } from '../../constants/file-provider.constants';
27+
import {
28+
FilesSelectors,
29+
GetMoveDialogConfiguredStorageAddons,
30+
GetMoveDialogRootFolders,
31+
GetStorageSupportedFeatures,
32+
} from '../../store';
833

934
import { FileSelectDestinationComponent } from './file-select-destination.component';
1035

11-
describe.skip('FileSelectDestinationComponent', () => {
36+
interface SetupOverrides extends BaseSetupOverrides {
37+
projectId?: string;
38+
storageProvider?: string;
39+
components?: NodeShortInfoModel[];
40+
areComponentsLoading?: boolean;
41+
}
42+
43+
describe('FileSelectDestinationComponent', () => {
1244
let component: FileSelectDestinationComponent;
1345
let fixture: ComponentFixture<FileSelectDestinationComponent>;
46+
let store: Store;
47+
48+
const rootFolder: FileFolderModel = {
49+
...OSF_FILE_MOCK,
50+
id: 'root-1',
51+
name: 'OSF Storage',
52+
provider: FileProvider.OsfStorage,
53+
};
54+
55+
const components: NodeShortInfoModel[] = [
56+
{ id: 'project-1', title: 'Project 1', isPublic: true, permissions: [UserPermissions.Write] },
57+
{
58+
id: 'component-1',
59+
title: 'Component 1',
60+
isPublic: true,
61+
permissions: [UserPermissions.Write],
62+
parentId: 'project-1',
63+
},
64+
{ id: 'readonly-1', title: 'Readonly', isPublic: true, permissions: [UserPermissions.Read], parentId: 'project-1' },
65+
];
66+
67+
function setup(overrides: SetupOverrides = {}) {
68+
const defaultSignals: SignalOverride[] = [
69+
{ selector: FilesSelectors.getMoveDialogRootFolders, value: [rootFolder] },
70+
{ selector: FilesSelectors.isMoveDialogRootFoldersLoading, value: false },
71+
{
72+
selector: FilesSelectors.getMoveDialogConfiguredStorageAddons,
73+
value: [{ ...MOCK_CONFIGURED_ADDON, id: 'addon-1', externalServiceName: FileProvider.OsfStorage }],
74+
},
75+
{ selector: FilesSelectors.isMoveDialogConfiguredStorageAddonsLoading, value: false },
76+
{ selector: FilesSelectors.isMoveDialogFilesLoading, value: false },
77+
{
78+
selector: FilesSelectors.getStorageSupportedFeatures,
79+
value: { [FileProvider.OsfStorage]: [SupportedFeature.AddUpdateFiles] },
80+
},
81+
];
1482

15-
beforeEach(() => {
1683
TestBed.configureTestingModule({
1784
imports: [FileSelectDestinationComponent, MockComponent(SelectComponent)],
18-
providers: [provideOSFCore()],
85+
providers: [
86+
provideOSFCore(),
87+
provideMockStore({ signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides) }),
88+
],
1989
});
2090

91+
store = TestBed.inject(Store);
2192
fixture = TestBed.createComponent(FileSelectDestinationComponent);
2293
component = fixture.componentInstance;
94+
fixture.componentRef.setInput('projectId', overrides.projectId ?? 'project-1');
95+
fixture.componentRef.setInput('storageProvider', overrides.storageProvider ?? FileProvider.OsfStorage);
96+
fixture.componentRef.setInput('components', overrides.components ?? components);
97+
fixture.componentRef.setInput('areComponentsLoading', overrides.areComponentsLoading ?? false);
2398
fixture.detectChanges();
24-
});
99+
}
25100

26101
it('should create', () => {
102+
setup();
27103
expect(component).toBeTruthy();
28104
});
105+
106+
it('should load storage addons on init and request supported features', () => {
107+
setup();
108+
const calls = (store.dispatch as Mock).mock.calls.map((c) => c[0]);
109+
110+
expect(calls).toContainEqual(new GetMoveDialogRootFolders('project-1', ResourceType.Project));
111+
expect(calls).toContainEqual(new GetMoveDialogConfiguredStorageAddons('project-1'));
112+
expect(calls).toContainEqual(new GetStorageSupportedFeatures('addon-1', FileProvider.OsfStorage));
113+
});
114+
115+
it('should emit project selection and reload storage addons on project change', () => {
116+
setup();
117+
const emitSpy = vi.spyOn(component.selectProject, 'emit');
118+
(store.dispatch as Mock).mockClear();
119+
120+
component.onChangeProject('project-2');
121+
122+
expect(emitSpy).toHaveBeenCalledWith('project-2');
123+
expect(store.dispatch).toHaveBeenCalledWith(new GetMoveDialogRootFolders('project-2', ResourceType.Project));
124+
expect(store.dispatch).toHaveBeenCalledWith(new GetMoveDialogConfiguredStorageAddons('project-2'));
125+
});
126+
127+
it('should update current root folder and emit storage selection on storage change', () => {
128+
setup();
129+
const emitSpy = vi.spyOn(component.selectStorage, 'emit');
130+
131+
component.onStorageChange('root-1');
132+
133+
expect(component.currentRootFolder()?.folder.id).toBe('root-1');
134+
expect(emitSpy).toHaveBeenCalled();
135+
});
136+
137+
it('should include only write-access nodes in options', () => {
138+
setup();
139+
const values = component.options().map((o) => o.value);
140+
141+
expect(values).toContain('project-1');
142+
expect(values).toContain('component-1');
143+
expect(values).not.toContain('readonly-1');
144+
});
145+
146+
it('should detect add update feature by provider', () => {
147+
setup();
148+
149+
expect(component.hasAddUpdateFeature(FileProvider.OsfStorage)).toBe(true);
150+
expect(component.hasAddUpdateFeature('dropbox')).toBe(false);
151+
});
152+
153+
it('should expose loading state when component loading is true', () => {
154+
setup({ areComponentsLoading: true });
155+
156+
expect(component.isLoading()).toBe(true);
157+
});
29158
});

src/app/features/files/components/file-select-destination/file-select-destination.component.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ export class FileSelectDestinationComponent implements OnInit {
7777
});
7878

7979
initialSetup = true;
80-
private storageRequestId = 0;
8180
currentRootFolder = model<FileLabelModel | null>(null);
8281

8382
readonly selectedProject = computed(() => this.options().find((c) => c.value === this.projectId()) || null);
@@ -131,7 +130,6 @@ export class FileSelectDestinationComponent implements OnInit {
131130
}
132131

133132
private getStorageAddons(projectId: string) {
134-
const requestId = ++this.storageRequestId;
135133
forkJoin({
136134
rootFolders: this.actions.getRootFolders(projectId, ResourceType.Project),
137135
addons: this.actions.getConfiguredStorageAddons(projectId),
@@ -150,10 +148,6 @@ export class FileSelectDestinationComponent implements OnInit {
150148
})
151149
)
152150
.subscribe(() => {
153-
if (requestId !== this.storageRequestId) {
154-
return;
155-
}
156-
157151
const storages = this.storageAddons();
158152
const currentStorage = storages.find((s) => s.folder.provider === this.storageProvider()) || storages[0];
159153

0 commit comments

Comments
 (0)