Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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: 39.5,
functions: 41.1,
lines: 68.0,
statements: 68.4,
branches: 43.3,
functions: 42.7,
lines: 69.3,
statements: 69.8,
},
},
watchPathIgnorePatterns: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,84 +9,54 @@ describe('AdvisoryBoardComponent', () => {
const mockHtmlContent =
'<div class="advisory-content"><h2>Advisory Board</h2><p>This is advisory board content.</p></div>';

beforeEach(async () => {
await TestBed.configureTestingModule({
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AdvisoryBoardComponent],
}).compileComponents();
});

fixture = TestBed.createComponent(AdvisoryBoardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

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

it('should have default input values', () => {
expect(component.htmlContent()).toBeNull();
expect(component.brand()).toBeUndefined();
expect(component.isLandingPage()).toBe(false);
});

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

const compiled = fixture.nativeElement;
const section = compiled.querySelector('section');

expect(section).toBeNull();
});

it('should not render section when htmlContent is undefined', () => {
fixture.componentRef.setInput('htmlContent', undefined);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const section = compiled.querySelector('section');

expect(section).toBeNull();
expect(getSection()).toBeNull();
});

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

const compiled = fixture.nativeElement;
const section = compiled.querySelector('section');

expect(section).toBeTruthy();
expect(section.innerHTML).toBe(mockHtmlContent);
});

it('should apply correct CSS classes when isLandingPage is false', () => {
fixture.componentRef.setInput('htmlContent', mockHtmlContent);
fixture.componentRef.setInput('isLandingPage', false);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const section = compiled.querySelector('section');
const section = getSection();

expect(section).toBeTruthy();
expect(section.classList.contains('osf-preprint-service')).toBe(false);
expect(section.classList.contains('preprints-advisory-board-section')).toBe(true);
expect(section.classList.contains('pt-3')).toBe(true);
expect(section.classList.contains('pb-5')).toBe(true);
expect(section.classList.contains('px-3')).toBe(true);
expect(section.classList.contains('flex')).toBe(true);
expect(section.classList.contains('flex-column')).toBe(true);
expect(section?.innerHTML).toContain('Advisory Board');
expect(section?.innerHTML).toContain('This is advisory board content.');
});

it('should apply correct CSS classes when isLandingPage is true', () => {
it.each([
{ isLandingPage: false, hasLandingClass: false },
{ isLandingPage: true, hasLandingClass: true },
])('should handle landing class when isLandingPage is $isLandingPage', ({ isLandingPage, hasLandingClass }) => {
fixture.componentRef.setInput('htmlContent', mockHtmlContent);
fixture.componentRef.setInput('isLandingPage', true);
fixture.componentRef.setInput('isLandingPage', isLandingPage);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const section = compiled.querySelector('section');
const section = getSection();

expect(section).toBeTruthy();
expect(section.classList.contains('osf-preprint-service')).toBe(true);
expect(section.classList.contains('preprints-advisory-board-section')).toBe(true);
expect(section?.classList.contains('osf-preprint-service')).toBe(hasLandingClass);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { NgClass } from '@angular/common';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

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

@Component({
Expand All @@ -14,6 +13,5 @@ import { SafeHtmlPipe } from '@osf/shared/pipes/safe-html.pipe';
})
export class AdvisoryBoardComponent {
htmlContent = input<StringOrNullOrUndefined>(null);
brand = input<BrandModel>();
isLandingPage = input<boolean>(false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ <h2 class="text-2xl">{{ 'preprints.browseBySubjects.title' | translate }}</h2>
<p-skeleton class="col-12 md:col-6" height="4rem" />
}
} @else {
@for (subject of subjects(); track subject) {
@for (subject of subjects(); track subject.id) {
<p-button
class="provider-subject col-12 md:col-6"
styleClass="w-full p-4"
[label]="subject.name"
severity="secondary"
[routerLink]="isLandingPage() ? '/search' : 'discover'"
[queryParams]="linksToSearchPageForSubject()[$index]"
[routerLink]="subjectRoute()"
[queryParams]="getQueryParamsForSubject(subject)"
/>
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,143 +1,94 @@
import { MockProvider } from 'ng-mocks';

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

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

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

import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock';
import { OSFTestingModule } from '@testing/osf.testing.module';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';

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

const mockSubjects: SubjectModel[] = SUBJECTS_MOCK;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BrowseBySubjectsComponent, OSFTestingModule],
}).compileComponents();
function setup(overrides?: {
subjects?: SubjectModel[];
areSubjectsLoading?: boolean;
isProviderLoading?: boolean;
isLandingPage?: boolean;
}) {
TestBed.configureTestingModule({
imports: [BrowseBySubjectsComponent],
providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())],
});

fixture = TestBed.createComponent(BrowseBySubjectsComponent);
component = fixture.componentInstance;
});

it('should create', () => {
fixture.componentRef.setInput('subjects', []);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);

fixture.componentRef.setInput('subjects', overrides?.subjects ?? []);
fixture.componentRef.setInput('areSubjectsLoading', overrides?.areSubjectsLoading ?? false);
fixture.componentRef.setInput('isProviderLoading', overrides?.isProviderLoading ?? false);
fixture.componentRef.setInput('isLandingPage', overrides?.isLandingPage ?? false);
fixture.detectChanges();
expect(component).toBeTruthy();
});
}

it('should have default input values', () => {
fixture.componentRef.setInput('subjects', []);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.detectChanges();
it('should keep default isLandingPage input as false', () => {
setup();

expect(component.subjects()).toEqual([]);
expect(component.areSubjectsLoading()).toBe(false);
expect(component.isProviderLoading()).toBe(false);
expect(component.isLandingPage()).toBe(false);
});

it('should display title', () => {
fixture.componentRef.setInput('subjects', []);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const title = compiled.querySelector('h2');
it('should render skeleton rows while loading', () => {
setup({ areSubjectsLoading: true, subjects: mockSubjects });

expect(title).toBeTruthy();
expect(title.textContent).toBe('preprints.browseBySubjects.title');
expect(fixture.nativeElement.querySelectorAll('p-skeleton').length).toBe(6);
expect(fixture.nativeElement.querySelectorAll('p-button').length).toBe(0);
});

it('should display correct subject names in buttons', () => {
fixture.componentRef.setInput('subjects', mockSubjects);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const buttons = compiled.querySelectorAll('p-button');
it('should render one button per subject when not loading', () => {
setup({ subjects: mockSubjects });

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

it('should compute linksToSearchPageForSubject correctly', () => {
fixture.componentRef.setInput('subjects', mockSubjects);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.detectChanges();

const links = component.linksToSearchPageForSubject();
it('should build query params for subject with iri', () => {
setup({ subjects: mockSubjects });

expect(links).toHaveLength(2);
expect(links[0]).toEqual({
expect(component.getQueryParamsForSubject(mockSubjects[0])).toEqual({
tab: ResourceType.Preprint,
filter_subject: '[{"label":"Mathematics","value":"https://example.com/subjects/mathematics"}]',
});
expect(links[1]).toEqual({
tab: ResourceType.Preprint,
filter_subject: '[{"label":"Physics","value":"https://example.com/subjects/physics"}]',
});
});

it('should set correct routerLink for non-landing page', () => {
fixture.componentRef.setInput('subjects', mockSubjects);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.componentRef.setInput('isLandingPage', false);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const buttons = compiled.querySelectorAll('p-button');

expect(buttons[0].getAttribute('ng-reflect-router-link')).toBe('discover');
});

it('should set correct routerLink for landing page', () => {
fixture.componentRef.setInput('subjects', mockSubjects);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.componentRef.setInput('isLandingPage', true);
fixture.detectChanges();

const compiled = fixture.nativeElement;
const buttons = compiled.querySelectorAll('p-button');

expect(buttons[0].getAttribute('ng-reflect-router-link')).toBe('/search');
});

it('should handle subjects without iri', () => {
const subjectsWithoutIri: SubjectModel[] = [
{
id: 'subject-1',
name: 'Physics',
iri: undefined,
children: [],
parent: null,
expanded: false,
},
];

fixture.componentRef.setInput('subjects', subjectsWithoutIri);
fixture.componentRef.setInput('areSubjectsLoading', false);
fixture.componentRef.setInput('isProviderLoading', false);
fixture.detectChanges();

const links = component.linksToSearchPageForSubject();

expect(links).toHaveLength(1);
expect(links[0]).toEqual({
it('should build query params for subject without iri', () => {
setup();
const subjectWithoutIri = {
id: 'subject-1',
name: 'Physics',
iri: undefined,
children: [],
parent: null,
expanded: false,
} as SubjectModel;

expect(component.getQueryParamsForSubject(subjectWithoutIri)).toEqual({
tab: ResourceType.Preprint,
filter_subject: '[{"label":"Physics"}]',
});
});

it.each([
{ isLandingPage: false, expected: 'discover' },
{ isLandingPage: true, expected: '/search' },
])('should resolve route for isLandingPage=$isLandingPage', ({ isLandingPage, expected }) => {
setup({ isLandingPage });

expect(component.subjectRoute()).toBe(expected);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,35 @@ import { Skeleton } from 'primeng/skeleton';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { RouterLink } from '@angular/router';

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

@Component({
selector: 'osf-browse-by-subjects',
imports: [RouterLink, Skeleton, TranslatePipe, Button],
imports: [Button, Skeleton, RouterLink, TranslatePipe],
templateUrl: './browse-by-subjects.component.html',
styleUrl: './browse-by-subjects.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BrowseBySubjectsComponent {
subjects = input.required<SubjectModel[]>();
linksToSearchPageForSubject = computed(() => {
return this.subjects().map((subject) => ({
readonly subjects = input.required<SubjectModel[]>();
readonly areSubjectsLoading = input.required<boolean>();
readonly isProviderLoading = input.required<boolean>();
readonly isLandingPage = input<boolean>(false);

readonly skeletonArray = new Array(6);

readonly subjectRoute = computed(() => (this.isLandingPage() ? '/search' : 'discover'));

getQueryParamsForSubject(subject: SubjectModel) {
return {
tab: ResourceType.Preprint,
filter_subject: JSON.stringify([
{
label: subject.name,
value: subject.iri,
},
]),
}));
});
areSubjectsLoading = input.required<boolean>();
isProviderLoading = input.required<boolean>();
isLandingPage = input<boolean>(false);
skeletonArray = Array.from({ length: 6 }, (_, i) => i + 1);
};
}
}
Loading