Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6a7b557
test(registries): added unit tests for pages components in registries
nsemets Feb 10, 2026
ff3e9e8
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into tests/r…
nsemets Feb 12, 2026
eef7ace
test(registries-pages): updated unit tests and code for some pages
nsemets Feb 12, 2026
81f14ff
test(setup): added providers mocks
nsemets Feb 13, 2026
0320401
test(registries-pages): updated tests and coverage
nsemets Feb 13, 2026
59e4214
test(registries-metadata-step): improved code and updated unit tests
nsemets Feb 13, 2026
0669fb7
test(registries): updated model imports and added selector
nsemets Feb 13, 2026
065ddd8
test(registries-license): add test to cover all lines
nsemets Feb 13, 2026
af84d0f
fix(registries-components): refactored components and removed dead code
nsemets Feb 16, 2026
24fa9cd
test(registries-components): added unit tests for registries components
nsemets Feb 17, 2026
5273f80
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into tests/r…
nsemets Feb 17, 2026
c7721cf
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into test/re…
nsemets Feb 18, 2026
70addd0
test(registry): updated tests for registry
nsemets Feb 20, 2026
ae4eb81
test(coverage): updated threshold numbers
nsemets Feb 20, 2026
4073805
test(docs): updated testing docs
nsemets Feb 20, 2026
8f5d7de
Merge remote-tracking branch 'upstream/feature/pbs-26-2' into test/re…
nsemets Feb 20, 2026
0da0840
fix(docs): fixed formatting
nsemets Feb 20, 2026
cdea365
fix(add-resource-dialog): updated logic for resource type
nsemets Feb 20, 2026
2f0fb41
fix(registry-resources): updated resource type and name logic
nsemets Feb 20, 2026
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
975 changes: 762 additions & 213 deletions docs/testing.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ module.exports = {
extensionsToTreatAsEsm: ['.ts'],
coverageThreshold: {
global: {
branches: 34.8,
functions: 38.0,
lines: 65.5,
statements: 66.0,
branches: 39.5,
functions: 41.1,
lines: 68.0,
statements: 68.4,
},
},
watchPathIgnorePatterns: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
@let resourceType = currentResource()?.type;
@let iconName = resourceType === 'analytic_code' ? 'code' : resourceType;
@let icon = `custom-icon-${iconName} icon-resource-size`;
@let resourceName = resourceType === RegistryResourceType.Code ? 'Analytic Code' : resourceType;

<div class="flex align-content-end gap-3 content">
<osf-icon class="align-self-start" [iconClass]="icon"></osf-icon>
<div class="flex flex-column gap-3 mt-1">
<div class="flex flex-column gap-2">
<h2>{{ resourceName }}</h2>
<h2>{{ resourceTypeTranslationKey() | translate }}</h2>
<a class="font-bold" [href]="doiLink()" target="_blank" rel="noopener noreferrer">
{{ doiLink() }}
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,89 +1,92 @@
import { Store } from '@ngxs/store';

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

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

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

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

import { RegistryResource } from '../../models';
import { RegistryResourcesSelectors } from '../../store/registry-resources';
import { ResourceFormComponent } from '../resource-form/resource-form.component';

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

import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock';
import { TranslateServiceMock } from '@testing/mocks/translate.service.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock';

const MOCK_RESOURCE: RegistryResource = {
id: 'res-1',
description: 'Test',
finalized: false,
type: RegistryResourceType.Data,
pid: '10.1234/test',
};

interface SetupOverrides {
registryId?: string;
selectorOverrides?: SignalOverride[];
}

function setup(overrides: SetupOverrides = {}) {
const mockDialogConfig = { data: { id: overrides.registryId ?? 'registry-123' } };

const defaultSignals = [
{ selector: RegistryResourcesSelectors.getCurrentResource, value: null },
{ selector: RegistryResourcesSelectors.isCurrentResourceLoading, value: false },
];

const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides);

TestBed.configureTestingModule({
imports: [
AddResourceDialogComponent,
...MockComponents(LoadingSpinnerComponent, ResourceFormComponent, IconComponent),
],
providers: [
provideOSFCore(),
provideDynamicDialogRefMock(),
MockProvider(DynamicDialogConfig, mockDialogConfig),
provideMockStore({ signals }),
],
});

const store = TestBed.inject(Store);
const dialogRef = TestBed.inject(DynamicDialogRef);
const fixture = TestBed.createComponent(AddResourceDialogComponent);
const component = fixture.componentInstance;
fixture.detectChanges();

return { fixture, component, store, dialogRef };
}

describe('AddResourceDialogComponent', () => {
let component: AddResourceDialogComponent;
let fixture: ComponentFixture<AddResourceDialogComponent>;
let store: Store;
let dialogRef: jest.Mocked<DynamicDialogRef>;
let mockDialogConfig: jest.Mocked<DynamicDialogConfig>;

const mockRegistryId = 'registry-123';

beforeEach(async () => {
mockDialogConfig = {
data: {
id: mockRegistryId,
},
} as jest.Mocked<DynamicDialogConfig>;

await TestBed.configureTestingModule({
imports: [
AddResourceDialogComponent,
OSFTestingModule,
MockComponent(LoadingSpinnerComponent),
MockComponent(ResourceFormComponent),
MockComponent(IconComponent),
],
providers: [
DynamicDialogRefMock,
TranslateServiceMock,
MockProvider(DynamicDialogConfig, mockDialogConfig),
provideMockStore({
signals: [
{ selector: RegistryResourcesSelectors.getCurrentResource, value: signal(null) },
{ selector: RegistryResourcesSelectors.isCurrentResourceLoading, value: signal(false) },
],
}),
],
}).compileComponents();

fixture = TestBed.createComponent(AddResourceDialogComponent);
component = fixture.componentInstance;
store = TestBed.inject(Store);
dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked<DynamicDialogRef>;
fixture.detectChanges();
});

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

it('should initialize with default values', () => {
expect(component).toBeTruthy();
expect(component.doiDomain).toBe('https://doi.org/');
expect(component.inputLimits).toBeDefined();
expect(component.isResourceConfirming()).toBe(false);
expect(component.isPreviewMode()).toBe(false);
expect(component.resourceOptions()).toBeDefined();
});

it('should initialize form with empty values', () => {
const { component } = setup();

expect(component.form.get('pid')?.value).toBe('');
expect(component.form.get('resourceType')?.value).toBe('');
expect(component.form.get('description')?.value).toBe('');
});

it('should validate pid with DOI validator', () => {
const { component } = setup();
const pidControl = component.form.get('pid');

pidControl?.setValue('invalid-doi');
pidControl?.updateValueAndValidity();

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

it('should accept valid DOI format', () => {
const { component } = setup();
const pidControl = component.form.get('pid');

pidControl?.setValue('10.1234/valid.doi');

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

it('should not preview resource when form is invalid', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch');
component.form.get('pid')?.setValue('');
const { component, store } = setup();

(store.dispatch as jest.Mock).mockClear();
component.previewResource();

expect(store.dispatch).not.toHaveBeenCalled();
expect(component.isPreviewMode()).toBe(false);
});

it('should not preview resource when currentResource is null', () => {
const { component, store } = setup();

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

expect(dispatchSpy).not.toHaveBeenCalled();
expect(store.dispatch).not.toHaveBeenCalled();
expect(component.isPreviewMode()).toBe(false);
});

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

expect(() => component.previewResource()).toThrow();
component.form.patchValue({ pid: '10.1234/test', resourceType: 'data', description: 'desc' });
(store.dispatch as jest.Mock).mockClear();
component.previewResource();

expect(store.dispatch).toHaveBeenCalled();
expect(component.isPreviewMode()).toBe(true);
});

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

component.isPreviewMode.set(true);
component.backToEdit();

expect(component.isPreviewMode()).toBe(false);
});

it('should throw error when adding resource without current resource', () => {
expect(() => component.onAddResource()).toThrow();
it('should not add resource when currentResource is null', () => {
const { component, store, dialogRef } = setup();

(store.dispatch as jest.Mock).mockClear();
component.onAddResource();

expect(store.dispatch).not.toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});

it('should close dialog without deleting when closeDialog is called without current resource', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch');
it('should confirm add resource and close dialog on success', () => {
const { component, store, dialogRef } = setup({
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
});

(store.dispatch as jest.Mock).mockClear();
component.onAddResource();

expect(component.isResourceConfirming()).toBe(false);
expect(store.dispatch).toHaveBeenCalled();
expect(dialogRef.close).toHaveBeenCalledWith(true);
});

it('should close dialog without deleting when currentResource is null', () => {
const { component, store, dialogRef } = setup();

(store.dispatch as jest.Mock).mockClear();
component.closeDialog();

expect(dialogRef.close).toHaveBeenCalled();
expect(store.dispatch).not.toHaveBeenCalled();
});

it('should delete resource and close dialog when currentResource exists', () => {
const { component, store, dialogRef } = setup({
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
});

(store.dispatch as jest.Mock).mockClear();
component.closeDialog();

expect(store.dispatch).toHaveBeenCalled();
expect(dialogRef.close).toHaveBeenCalled();
expect(dispatchSpy).not.toHaveBeenCalled();
});

it('should compute doiLink as undefined when current resource does not exist', () => {
expect(component.doiLink()).toBe('https://doi.org/undefined');
it('should compute doiLink from currentResource pid', () => {
const { component } = setup({
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
});

expect(component.doiLink()).toBe('https://doi.org/10.1234/test');
});

it('should return empty string for resourceTypeTranslationKey when currentResource is null', () => {
const { component } = setup();

expect(component.resourceTypeTranslationKey()).toBe('');
});

it('should return translation key for resourceTypeTranslationKey when resource type matches', () => {
const { component } = setup({
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: MOCK_RESOURCE }],
});

expect(component.resourceTypeTranslationKey()).toBe('resources.typeOptions.data');
});

it('should return empty string for resourceTypeTranslationKey when type is unknown', () => {
const unknownResource = { ...MOCK_RESOURCE, type: 'unknown_type' as RegistryResourceType };
const { component } = setup({
selectorOverrides: [{ selector: RegistryResourcesSelectors.getCurrentResource, value: unknownResource }],
});

expect(component.resourceTypeTranslationKey()).toBe('');
});
});
Loading