Skip to content

Commit 4a85dd3

Browse files
authored
[ENG-10364] Part 4: Add unit tests for preprints (#902)
- Ticket: [ENG-10364] - Feature flag: n/a ## Summary of Changes 1. Updated unit tests for preprints components. 2. Updated coverage thresholds.
1 parent ee5d1bb commit 4a85dd3

20 files changed

Lines changed: 292 additions & 407 deletions

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: 39.5,
58-
functions: 41.1,
59-
lines: 68.0,
60-
statements: 68.4,
57+
branches: 43.3,
58+
functions: 42.7,
59+
lines: 69.3,
60+
statements: 69.8,
6161
},
6262
},
6363
watchPathIgnorePatterns: [

src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,84 +9,54 @@ describe('AdvisoryBoardComponent', () => {
99
const mockHtmlContent =
1010
'<div class="advisory-content"><h2>Advisory Board</h2><p>This is advisory board content.</p></div>';
1111

12-
beforeEach(async () => {
13-
await TestBed.configureTestingModule({
12+
beforeEach(() => {
13+
TestBed.configureTestingModule({
1414
imports: [AdvisoryBoardComponent],
15-
}).compileComponents();
15+
});
1616

1717
fixture = TestBed.createComponent(AdvisoryBoardComponent);
1818
component = fixture.componentInstance;
1919
fixture.detectChanges();
2020
});
2121

22-
it('should create', () => {
23-
expect(component).toBeTruthy();
24-
});
22+
function getSection(): HTMLElement | null {
23+
return fixture.nativeElement.querySelector('section');
24+
}
2525

2626
it('should have default input values', () => {
2727
expect(component.htmlContent()).toBeNull();
28-
expect(component.brand()).toBeUndefined();
2928
expect(component.isLandingPage()).toBe(false);
3029
});
3130

32-
it('should not render section when htmlContent is null', () => {
31+
it.each([null, undefined])('should not render section when htmlContent is %s', (htmlContent) => {
32+
fixture.componentRef.setInput('htmlContent', htmlContent);
3333
fixture.detectChanges();
3434

35-
const compiled = fixture.nativeElement;
36-
const section = compiled.querySelector('section');
37-
38-
expect(section).toBeNull();
39-
});
40-
41-
it('should not render section when htmlContent is undefined', () => {
42-
fixture.componentRef.setInput('htmlContent', undefined);
43-
fixture.detectChanges();
44-
45-
const compiled = fixture.nativeElement;
46-
const section = compiled.querySelector('section');
47-
48-
expect(section).toBeNull();
35+
expect(getSection()).toBeNull();
4936
});
5037

5138
it('should render section when htmlContent is provided', () => {
5239
fixture.componentRef.setInput('htmlContent', mockHtmlContent);
5340
fixture.detectChanges();
5441

55-
const compiled = fixture.nativeElement;
56-
const section = compiled.querySelector('section');
57-
58-
expect(section).toBeTruthy();
59-
expect(section.innerHTML).toBe(mockHtmlContent);
60-
});
61-
62-
it('should apply correct CSS classes when isLandingPage is false', () => {
63-
fixture.componentRef.setInput('htmlContent', mockHtmlContent);
64-
fixture.componentRef.setInput('isLandingPage', false);
65-
fixture.detectChanges();
66-
67-
const compiled = fixture.nativeElement;
68-
const section = compiled.querySelector('section');
42+
const section = getSection();
6943

7044
expect(section).toBeTruthy();
71-
expect(section.classList.contains('osf-preprint-service')).toBe(false);
72-
expect(section.classList.contains('preprints-advisory-board-section')).toBe(true);
73-
expect(section.classList.contains('pt-3')).toBe(true);
74-
expect(section.classList.contains('pb-5')).toBe(true);
75-
expect(section.classList.contains('px-3')).toBe(true);
76-
expect(section.classList.contains('flex')).toBe(true);
77-
expect(section.classList.contains('flex-column')).toBe(true);
45+
expect(section?.innerHTML).toContain('Advisory Board');
46+
expect(section?.innerHTML).toContain('This is advisory board content.');
7847
});
7948

80-
it('should apply correct CSS classes when isLandingPage is true', () => {
49+
it.each([
50+
{ isLandingPage: false, hasLandingClass: false },
51+
{ isLandingPage: true, hasLandingClass: true },
52+
])('should handle landing class when isLandingPage is $isLandingPage', ({ isLandingPage, hasLandingClass }) => {
8153
fixture.componentRef.setInput('htmlContent', mockHtmlContent);
82-
fixture.componentRef.setInput('isLandingPage', true);
54+
fixture.componentRef.setInput('isLandingPage', isLandingPage);
8355
fixture.detectChanges();
8456

85-
const compiled = fixture.nativeElement;
86-
const section = compiled.querySelector('section');
57+
const section = getSection();
8758

8859
expect(section).toBeTruthy();
89-
expect(section.classList.contains('osf-preprint-service')).toBe(true);
90-
expect(section.classList.contains('preprints-advisory-board-section')).toBe(true);
60+
expect(section?.classList.contains('osf-preprint-service')).toBe(hasLandingClass);
9161
});
9262
});

src/app/features/preprints/components/advisory-board/advisory-board.component.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { NgClass } from '@angular/common';
22
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
33

44
import { StringOrNullOrUndefined } from '@osf/shared/helpers/types.helper';
5-
import { BrandModel } from '@osf/shared/models/brand/brand.model';
65
import { SafeHtmlPipe } from '@osf/shared/pipes/safe-html.pipe';
76

87
@Component({
@@ -14,6 +13,5 @@ import { SafeHtmlPipe } from '@osf/shared/pipes/safe-html.pipe';
1413
})
1514
export class AdvisoryBoardComponent {
1615
htmlContent = input<StringOrNullOrUndefined>(null);
17-
brand = input<BrandModel>();
1816
isLandingPage = input<boolean>(false);
1917
}

src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ <h2 class="text-2xl">{{ 'preprints.browseBySubjects.title' | translate }}</h2>
66
<p-skeleton class="col-12 md:col-6" height="4rem" />
77
}
88
} @else {
9-
@for (subject of subjects(); track subject) {
9+
@for (subject of subjects(); track subject.id) {
1010
<p-button
1111
class="provider-subject col-12 md:col-6"
1212
styleClass="w-full p-4"
1313
[label]="subject.name"
1414
severity="secondary"
15-
[routerLink]="isLandingPage() ? '/search' : 'discover'"
16-
[queryParams]="linksToSearchPageForSubject()[$index]"
15+
[routerLink]="subjectRoute()"
16+
[queryParams]="getQueryParamsForSubject(subject)"
1717
/>
1818
}
1919
}
Lines changed: 53 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,94 @@
1+
import { MockProvider } from 'ng-mocks';
2+
13
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
import { ActivatedRoute } from '@angular/router';
25

36
import { ResourceType } from '@shared/enums/resource-type.enum';
47
import { SubjectModel } from '@shared/models/subject/subject.model';
58

69
import { BrowseBySubjectsComponent } from './browse-by-subjects.component';
710

811
import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock';
9-
import { OSFTestingModule } from '@testing/osf.testing.module';
12+
import { provideOSFCore } from '@testing/osf.testing.provider';
13+
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
1014

1115
describe('BrowseBySubjectsComponent', () => {
1216
let component: BrowseBySubjectsComponent;
1317
let fixture: ComponentFixture<BrowseBySubjectsComponent>;
1418

1519
const mockSubjects: SubjectModel[] = SUBJECTS_MOCK;
1620

17-
beforeEach(async () => {
18-
await TestBed.configureTestingModule({
19-
imports: [BrowseBySubjectsComponent, OSFTestingModule],
20-
}).compileComponents();
21+
function setup(overrides?: {
22+
subjects?: SubjectModel[];
23+
areSubjectsLoading?: boolean;
24+
isProviderLoading?: boolean;
25+
isLandingPage?: boolean;
26+
}) {
27+
TestBed.configureTestingModule({
28+
imports: [BrowseBySubjectsComponent],
29+
providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())],
30+
});
2131

2232
fixture = TestBed.createComponent(BrowseBySubjectsComponent);
2333
component = fixture.componentInstance;
24-
});
25-
26-
it('should create', () => {
27-
fixture.componentRef.setInput('subjects', []);
28-
fixture.componentRef.setInput('areSubjectsLoading', false);
29-
fixture.componentRef.setInput('isProviderLoading', false);
30-
34+
fixture.componentRef.setInput('subjects', overrides?.subjects ?? []);
35+
fixture.componentRef.setInput('areSubjectsLoading', overrides?.areSubjectsLoading ?? false);
36+
fixture.componentRef.setInput('isProviderLoading', overrides?.isProviderLoading ?? false);
37+
fixture.componentRef.setInput('isLandingPage', overrides?.isLandingPage ?? false);
3138
fixture.detectChanges();
32-
expect(component).toBeTruthy();
33-
});
39+
}
3440

35-
it('should have default input values', () => {
36-
fixture.componentRef.setInput('subjects', []);
37-
fixture.componentRef.setInput('areSubjectsLoading', false);
38-
fixture.componentRef.setInput('isProviderLoading', false);
39-
fixture.detectChanges();
41+
it('should keep default isLandingPage input as false', () => {
42+
setup();
4043

41-
expect(component.subjects()).toEqual([]);
42-
expect(component.areSubjectsLoading()).toBe(false);
43-
expect(component.isProviderLoading()).toBe(false);
4444
expect(component.isLandingPage()).toBe(false);
4545
});
4646

47-
it('should display title', () => {
48-
fixture.componentRef.setInput('subjects', []);
49-
fixture.componentRef.setInput('areSubjectsLoading', false);
50-
fixture.componentRef.setInput('isProviderLoading', false);
51-
fixture.detectChanges();
52-
53-
const compiled = fixture.nativeElement;
54-
const title = compiled.querySelector('h2');
47+
it('should render skeleton rows while loading', () => {
48+
setup({ areSubjectsLoading: true, subjects: mockSubjects });
5549

56-
expect(title).toBeTruthy();
57-
expect(title.textContent).toBe('preprints.browseBySubjects.title');
50+
expect(fixture.nativeElement.querySelectorAll('p-skeleton').length).toBe(6);
51+
expect(fixture.nativeElement.querySelectorAll('p-button').length).toBe(0);
5852
});
5953

60-
it('should display correct subject names in buttons', () => {
61-
fixture.componentRef.setInput('subjects', mockSubjects);
62-
fixture.componentRef.setInput('areSubjectsLoading', false);
63-
fixture.componentRef.setInput('isProviderLoading', false);
64-
fixture.detectChanges();
65-
66-
const compiled = fixture.nativeElement;
67-
const buttons = compiled.querySelectorAll('p-button');
54+
it('should render one button per subject when not loading', () => {
55+
setup({ subjects: mockSubjects });
6856

69-
expect(buttons[0].getAttribute('ng-reflect-label')).toBe('Mathematics');
70-
expect(buttons[1].getAttribute('ng-reflect-label')).toBe('Physics');
57+
expect(fixture.nativeElement.querySelectorAll('p-button').length).toBe(mockSubjects.length);
7158
});
7259

73-
it('should compute linksToSearchPageForSubject correctly', () => {
74-
fixture.componentRef.setInput('subjects', mockSubjects);
75-
fixture.componentRef.setInput('areSubjectsLoading', false);
76-
fixture.componentRef.setInput('isProviderLoading', false);
77-
fixture.detectChanges();
78-
79-
const links = component.linksToSearchPageForSubject();
60+
it('should build query params for subject with iri', () => {
61+
setup({ subjects: mockSubjects });
8062

81-
expect(links).toHaveLength(2);
82-
expect(links[0]).toEqual({
63+
expect(component.getQueryParamsForSubject(mockSubjects[0])).toEqual({
8364
tab: ResourceType.Preprint,
8465
filter_subject: '[{"label":"Mathematics","value":"https://example.com/subjects/mathematics"}]',
8566
});
86-
expect(links[1]).toEqual({
87-
tab: ResourceType.Preprint,
88-
filter_subject: '[{"label":"Physics","value":"https://example.com/subjects/physics"}]',
89-
});
9067
});
9168

92-
it('should set correct routerLink for non-landing page', () => {
93-
fixture.componentRef.setInput('subjects', mockSubjects);
94-
fixture.componentRef.setInput('areSubjectsLoading', false);
95-
fixture.componentRef.setInput('isProviderLoading', false);
96-
fixture.componentRef.setInput('isLandingPage', false);
97-
fixture.detectChanges();
98-
99-
const compiled = fixture.nativeElement;
100-
const buttons = compiled.querySelectorAll('p-button');
101-
102-
expect(buttons[0].getAttribute('ng-reflect-router-link')).toBe('discover');
103-
});
104-
105-
it('should set correct routerLink for landing page', () => {
106-
fixture.componentRef.setInput('subjects', mockSubjects);
107-
fixture.componentRef.setInput('areSubjectsLoading', false);
108-
fixture.componentRef.setInput('isProviderLoading', false);
109-
fixture.componentRef.setInput('isLandingPage', true);
110-
fixture.detectChanges();
111-
112-
const compiled = fixture.nativeElement;
113-
const buttons = compiled.querySelectorAll('p-button');
114-
115-
expect(buttons[0].getAttribute('ng-reflect-router-link')).toBe('/search');
116-
});
117-
118-
it('should handle subjects without iri', () => {
119-
const subjectsWithoutIri: SubjectModel[] = [
120-
{
121-
id: 'subject-1',
122-
name: 'Physics',
123-
iri: undefined,
124-
children: [],
125-
parent: null,
126-
expanded: false,
127-
},
128-
];
129-
130-
fixture.componentRef.setInput('subjects', subjectsWithoutIri);
131-
fixture.componentRef.setInput('areSubjectsLoading', false);
132-
fixture.componentRef.setInput('isProviderLoading', false);
133-
fixture.detectChanges();
134-
135-
const links = component.linksToSearchPageForSubject();
136-
137-
expect(links).toHaveLength(1);
138-
expect(links[0]).toEqual({
69+
it('should build query params for subject without iri', () => {
70+
setup();
71+
const subjectWithoutIri = {
72+
id: 'subject-1',
73+
name: 'Physics',
74+
iri: undefined,
75+
children: [],
76+
parent: null,
77+
expanded: false,
78+
} as SubjectModel;
79+
80+
expect(component.getQueryParamsForSubject(subjectWithoutIri)).toEqual({
13981
tab: ResourceType.Preprint,
14082
filter_subject: '[{"label":"Physics"}]',
14183
});
14284
});
85+
86+
it.each([
87+
{ isLandingPage: false, expected: 'discover' },
88+
{ isLandingPage: true, expected: '/search' },
89+
])('should resolve route for isLandingPage=$isLandingPage', ({ isLandingPage, expected }) => {
90+
setup({ isLandingPage });
91+
92+
expect(component.subjectRoute()).toBe(expected);
93+
});
14394
});

src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,35 @@ import { Skeleton } from 'primeng/skeleton';
66
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
77
import { RouterLink } from '@angular/router';
88

9-
import { ResourceType } from '@shared/enums/resource-type.enum';
10-
import { SubjectModel } from '@shared/models/subject/subject.model';
9+
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
10+
import { SubjectModel } from '@osf/shared/models/subject/subject.model';
1111

1212
@Component({
1313
selector: 'osf-browse-by-subjects',
14-
imports: [RouterLink, Skeleton, TranslatePipe, Button],
14+
imports: [Button, Skeleton, RouterLink, TranslatePipe],
1515
templateUrl: './browse-by-subjects.component.html',
1616
styleUrl: './browse-by-subjects.component.scss',
1717
changeDetection: ChangeDetectionStrategy.OnPush,
1818
})
1919
export class BrowseBySubjectsComponent {
20-
subjects = input.required<SubjectModel[]>();
21-
linksToSearchPageForSubject = computed(() => {
22-
return this.subjects().map((subject) => ({
20+
readonly subjects = input.required<SubjectModel[]>();
21+
readonly areSubjectsLoading = input.required<boolean>();
22+
readonly isProviderLoading = input.required<boolean>();
23+
readonly isLandingPage = input<boolean>(false);
24+
25+
readonly skeletonArray = new Array(6);
26+
27+
readonly subjectRoute = computed(() => (this.isLandingPage() ? '/search' : 'discover'));
28+
29+
getQueryParamsForSubject(subject: SubjectModel) {
30+
return {
2331
tab: ResourceType.Preprint,
2432
filter_subject: JSON.stringify([
2533
{
2634
label: subject.name,
2735
value: subject.iri,
2836
},
2937
]),
30-
}));
31-
});
32-
areSubjectsLoading = input.required<boolean>();
33-
isProviderLoading = input.required<boolean>();
34-
isLandingPage = input<boolean>(false);
35-
skeletonArray = Array.from({ length: 6 }, (_, i) => i + 1);
38+
};
39+
}
3640
}

0 commit comments

Comments
 (0)