Skip to content

Commit 5a3adbf

Browse files
authored
[ENG-10255] Part 4: Add unit test coverage for registries (#892)
- Ticket: [ENG-10255] - Feature flag: n/a ## Summary of Changes 1. Updated tests for registry.
1 parent 70339af commit 5a3adbf

57 files changed

Lines changed: 3188 additions & 3373 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/testing.md

Lines changed: 762 additions & 213 deletions
Large diffs are not rendered by default.

jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ module.exports = {
5454
extensionsToTreatAsEsm: ['.ts'],
5555
coverageThreshold: {
5656
global: {
57-
branches: 34.8,
58-
functions: 38.0,
59-
lines: 65.5,
60-
statements: 66.0,
57+
branches: 39.5,
58+
functions: 41.1,
59+
lines: 68.0,
60+
statements: 68.4,
6161
},
6262
},
6363
watchPathIgnorePatterns: [

src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
@let resourceType = currentResource()?.type;
88
@let iconName = resourceType === 'analytic_code' ? 'code' : resourceType;
99
@let icon = `custom-icon-${iconName} icon-resource-size`;
10-
@let resourceName = resourceType === RegistryResourceType.Code ? 'Analytic Code' : resourceType;
1110

1211
<div class="flex align-content-end gap-3 content">
1312
<osf-icon class="align-self-start" [iconClass]="icon"></osf-icon>
1413
<div class="flex flex-column gap-3 mt-1">
1514
<div class="flex flex-column gap-2">
16-
<h2>{{ resourceName }}</h2>
15+
<h2>{{ resourceTypeTranslationKey() | translate }}</h2>
1716
<a class="font-bold" [href]="doiLink()" target="_blank" rel="noopener noreferrer">
1817
{{ doiLink() }}
1918
</a>
Lines changed: 152 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,92 @@
11
import { Store } from '@ngxs/store';
22

3-
import { MockComponent, MockProvider } from 'ng-mocks';
3+
import { MockComponents, MockProvider } from 'ng-mocks';
44

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

7-
import { signal } from '@angular/core';
8-
import { ComponentFixture, TestBed } from '@angular/core/testing';
7+
import { TestBed } from '@angular/core/testing';
98

109
import { IconComponent } from '@osf/shared/components/icon/icon.component';
1110
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
11+
import { RegistryResourceType } from '@osf/shared/enums/registry-resource.enum';
1212

13+
import { RegistryResource } from '../../models';
1314
import { RegistryResourcesSelectors } from '../../store/registry-resources';
1415
import { ResourceFormComponent } from '../resource-form/resource-form.component';
1516

1617
import { AddResourceDialogComponent } from './add-resource-dialog.component';
1718

18-
import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock';
19-
import { TranslateServiceMock } from '@testing/mocks/translate.service.mock';
20-
import { OSFTestingModule } from '@testing/osf.testing.module';
21-
import { provideMockStore } from '@testing/providers/store-provider.mock';
19+
import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock';
20+
import { provideOSFCore } from '@testing/osf.testing.provider';
21+
import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock';
22+
23+
const MOCK_RESOURCE: RegistryResource = {
24+
id: 'res-1',
25+
description: 'Test',
26+
finalized: false,
27+
type: RegistryResourceType.Data,
28+
pid: '10.1234/test',
29+
};
30+
31+
interface SetupOverrides {
32+
registryId?: string;
33+
selectorOverrides?: SignalOverride[];
34+
}
35+
36+
function setup(overrides: SetupOverrides = {}) {
37+
const mockDialogConfig = { data: { id: overrides.registryId ?? 'registry-123' } };
38+
39+
const defaultSignals = [
40+
{ selector: RegistryResourcesSelectors.getCurrentResource, value: null },
41+
{ selector: RegistryResourcesSelectors.isCurrentResourceLoading, value: false },
42+
];
43+
44+
const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides);
45+
46+
TestBed.configureTestingModule({
47+
imports: [
48+
AddResourceDialogComponent,
49+
...MockComponents(LoadingSpinnerComponent, ResourceFormComponent, IconComponent),
50+
],
51+
providers: [
52+
provideOSFCore(),
53+
provideDynamicDialogRefMock(),
54+
MockProvider(DynamicDialogConfig, mockDialogConfig),
55+
provideMockStore({ signals }),
56+
],
57+
});
58+
59+
const store = TestBed.inject(Store);
60+
const dialogRef = TestBed.inject(DynamicDialogRef);
61+
const fixture = TestBed.createComponent(AddResourceDialogComponent);
62+
const component = fixture.componentInstance;
63+
fixture.detectChanges();
64+
65+
return { fixture, component, store, dialogRef };
66+
}
2267

2368
describe('AddResourceDialogComponent', () => {
24-
let component: AddResourceDialogComponent;
25-
let fixture: ComponentFixture<AddResourceDialogComponent>;
26-
let store: Store;
27-
let dialogRef: jest.Mocked<DynamicDialogRef>;
28-
let mockDialogConfig: jest.Mocked<DynamicDialogConfig>;
29-
30-
const mockRegistryId = 'registry-123';
31-
32-
beforeEach(async () => {
33-
mockDialogConfig = {
34-
data: {
35-
id: mockRegistryId,
36-
},
37-
} as jest.Mocked<DynamicDialogConfig>;
38-
39-
await TestBed.configureTestingModule({
40-
imports: [
41-
AddResourceDialogComponent,
42-
OSFTestingModule,
43-
MockComponent(LoadingSpinnerComponent),
44-
MockComponent(ResourceFormComponent),
45-
MockComponent(IconComponent),
46-
],
47-
providers: [
48-
DynamicDialogRefMock,
49-
TranslateServiceMock,
50-
MockProvider(DynamicDialogConfig, mockDialogConfig),
51-
provideMockStore({
52-
signals: [
53-
{ selector: RegistryResourcesSelectors.getCurrentResource, value: signal(null) },
54-
{ selector: RegistryResourcesSelectors.isCurrentResourceLoading, value: signal(false) },
55-
],
56-
}),
57-
],
58-
}).compileComponents();
59-
60-
fixture = TestBed.createComponent(AddResourceDialogComponent);
61-
component = fixture.componentInstance;
62-
store = TestBed.inject(Store);
63-
dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked<DynamicDialogRef>;
64-
fixture.detectChanges();
65-
});
66-
67-
it('should create', () => {
68-
expect(component).toBeTruthy();
69-
});
69+
it('should create with default values', () => {
70+
const { component } = setup();
7071

71-
it('should initialize with default values', () => {
72+
expect(component).toBeTruthy();
7273
expect(component.doiDomain).toBe('https://doi.org/');
73-
expect(component.inputLimits).toBeDefined();
7474
expect(component.isResourceConfirming()).toBe(false);
7575
expect(component.isPreviewMode()).toBe(false);
76-
expect(component.resourceOptions()).toBeDefined();
7776
});
7877

7978
it('should initialize form with empty values', () => {
79+
const { component } = setup();
80+
8081
expect(component.form.get('pid')?.value).toBe('');
8182
expect(component.form.get('resourceType')?.value).toBe('');
8283
expect(component.form.get('description')?.value).toBe('');
8384
});
8485

8586
it('should validate pid with DOI validator', () => {
87+
const { component } = setup();
8688
const pidControl = component.form.get('pid');
89+
8790
pidControl?.setValue('invalid-doi');
8891
pidControl?.updateValueAndValidity();
8992

@@ -92,53 +95,130 @@ describe('AddResourceDialogComponent', () => {
9295
});
9396

9497
it('should accept valid DOI format', () => {
98+
const { component } = setup();
9599
const pidControl = component.form.get('pid');
100+
96101
pidControl?.setValue('10.1234/valid.doi');
97102

98103
expect(pidControl?.hasError('doi')).toBe(false);
99104
});
100105

101106
it('should not preview resource when form is invalid', () => {
102-
const dispatchSpy = jest.spyOn(store, 'dispatch');
103-
component.form.get('pid')?.setValue('');
107+
const { component, store } = setup();
108+
109+
(store.dispatch as jest.Mock).mockClear();
110+
component.previewResource();
111+
112+
expect(store.dispatch).not.toHaveBeenCalled();
113+
expect(component.isPreviewMode()).toBe(false);
114+
});
115+
116+
it('should not preview resource when currentResource is null', () => {
117+
const { component, store } = setup();
104118

119+
component.form.patchValue({ pid: '10.1234/test', resourceType: 'data' });
120+
(store.dispatch as jest.Mock).mockClear();
105121
component.previewResource();
106122

107-
expect(dispatchSpy).not.toHaveBeenCalled();
123+
expect(store.dispatch).not.toHaveBeenCalled();
108124
expect(component.isPreviewMode()).toBe(false);
109125
});
110126

111-
it('should throw error when previewing resource without current resource', () => {
112-
component.form.patchValue({
113-
pid: '10.1234/test',
114-
resourceType: 'dataset',
127+
it('should preview resource and set preview mode on success', () => {
128+
const { component, store } = setup({
129+
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
115130
});
116131

117-
expect(() => component.previewResource()).toThrow();
132+
component.form.patchValue({ pid: '10.1234/test', resourceType: 'data', description: 'desc' });
133+
(store.dispatch as jest.Mock).mockClear();
134+
component.previewResource();
135+
136+
expect(store.dispatch).toHaveBeenCalled();
137+
expect(component.isPreviewMode()).toBe(true);
118138
});
119139

120140
it('should set isPreviewMode to false when backToEdit is called', () => {
121-
component.isPreviewMode.set(true);
141+
const { component } = setup();
122142

143+
component.isPreviewMode.set(true);
123144
component.backToEdit();
124145

125146
expect(component.isPreviewMode()).toBe(false);
126147
});
127148

128-
it('should throw error when adding resource without current resource', () => {
129-
expect(() => component.onAddResource()).toThrow();
149+
it('should not add resource when currentResource is null', () => {
150+
const { component, store, dialogRef } = setup();
151+
152+
(store.dispatch as jest.Mock).mockClear();
153+
component.onAddResource();
154+
155+
expect(store.dispatch).not.toHaveBeenCalled();
156+
expect(dialogRef.close).not.toHaveBeenCalled();
130157
});
131158

132-
it('should close dialog without deleting when closeDialog is called without current resource', () => {
133-
const dispatchSpy = jest.spyOn(store, 'dispatch');
159+
it('should confirm add resource and close dialog on success', () => {
160+
const { component, store, dialogRef } = setup({
161+
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
162+
});
163+
164+
(store.dispatch as jest.Mock).mockClear();
165+
component.onAddResource();
134166

167+
expect(component.isResourceConfirming()).toBe(false);
168+
expect(store.dispatch).toHaveBeenCalled();
169+
expect(dialogRef.close).toHaveBeenCalledWith(true);
170+
});
171+
172+
it('should close dialog without deleting when currentResource is null', () => {
173+
const { component, store, dialogRef } = setup();
174+
175+
(store.dispatch as jest.Mock).mockClear();
176+
component.closeDialog();
177+
178+
expect(dialogRef.close).toHaveBeenCalled();
179+
expect(store.dispatch).not.toHaveBeenCalled();
180+
});
181+
182+
it('should delete resource and close dialog when currentResource exists', () => {
183+
const { component, store, dialogRef } = setup({
184+
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
185+
});
186+
187+
(store.dispatch as jest.Mock).mockClear();
135188
component.closeDialog();
136189

190+
expect(store.dispatch).toHaveBeenCalled();
137191
expect(dialogRef.close).toHaveBeenCalled();
138-
expect(dispatchSpy).not.toHaveBeenCalled();
139192
});
140193

141-
it('should compute doiLink as undefined when current resource does not exist', () => {
142-
expect(component.doiLink()).toBe('https://doi.org/undefined');
194+
it('should compute doiLink from currentResource pid', () => {
195+
const { component } = setup({
196+
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
197+
});
198+
199+
expect(component.doiLink()).toBe('https://doi.org/10.1234/test');
200+
});
201+
202+
it('should return empty string for resourceTypeTranslationKey when currentResource is null', () => {
203+
const { component } = setup();
204+
205+
expect(component.resourceTypeTranslationKey()).toBe('');
206+
});
207+
208+
it('should return translation key for resourceTypeTranslationKey when resource type matches', () => {
209+
const { component } = setup({
210+
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
211+
});
212+
213+
expect(component.resourceTypeTranslationKey()).toBe('resources.typeOptions.data');
214+
});
215+
216+
it('should return empty string for resourceTypeTranslationKey when type is unknown', () => {
217+
const unknownResource = { ...MOCK_RESOURCE, type: 'unknown_type' as RegistryResourceType };
218+
const { component } = setup({
219+
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: unknownResource }],
220+
});
221+
222+
expect(component.resourceTypeTranslationKey()).toBe('');
143223
});
144224
});

0 commit comments

Comments
 (0)