Skip to content

Commit 6b7bed7

Browse files
authored
PD-5538 (#2840)
* PD-5538 * PD-5538
1 parent ea36b4e commit 6b7bed7

8 files changed

Lines changed: 576 additions & 208 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { TestBed } from '@angular/core/testing'
2+
import { take } from 'rxjs/operators'
3+
import { getUserRecord } from '../record/record.service.spec'
4+
import { RecordHeaderStateService } from './record-header-state.service'
5+
6+
describe('RecordHeaderStateService', () => {
7+
let service: RecordHeaderStateService
8+
9+
beforeEach(() => {
10+
TestBed.configureTestingModule({})
11+
service = TestBed.inject(RecordHeaderStateService)
12+
service.reset()
13+
})
14+
15+
it('should emit header loading separately from full record loading', (done) => {
16+
service.setLoadingUserRecord(true)
17+
service.setLoadingRecordHeader(false)
18+
19+
service.loadingRecordHeader$.pipe(take(1)).subscribe((loading) => {
20+
expect(loading).toBeFalse()
21+
done()
22+
})
23+
})
24+
25+
it('should share the latest user record with header consumers', (done) => {
26+
const userRecord = getUserRecord()
27+
28+
service.setUserRecord(userRecord)
29+
30+
service.userRecord$.pipe(take(1)).subscribe((value) => {
31+
expect(value).toBe(userRecord)
32+
done()
33+
})
34+
})
35+
36+
it('should reset header-specific state', (done) => {
37+
service.setLoadingRecordHeader(false)
38+
service.setUserRecord(getUserRecord())
39+
40+
service.reset()
41+
42+
service.loadingRecordHeader$.pipe(take(1)).subscribe((loading) => {
43+
expect(loading).toBeTrue()
44+
service.userRecord$.pipe(take(1)).subscribe((userRecord) => {
45+
expect(userRecord).toBeNull()
46+
done()
47+
})
48+
})
49+
})
50+
})

src/app/core/record-header-state/record-header-state.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@angular/core'
22
import { BehaviorSubject } from 'rxjs'
3+
import { UserRecord } from 'src/app/types/record.local'
34

45
/**
56
* Shares state required by the Record Header between My ORCID and the Header.
@@ -8,7 +9,9 @@ import { BehaviorSubject } from 'rxjs'
89
@Injectable({ providedIn: 'root' })
910
export class RecordHeaderStateService {
1011
private readonly _loadingUserRecord = new BehaviorSubject<boolean>(true)
12+
private readonly _loadingRecordHeader = new BehaviorSubject<boolean>(true)
1113
private readonly _isPublicRecord = new BehaviorSubject<string | null>(null)
14+
private readonly _userRecord = new BehaviorSubject<UserRecord | null>(null)
1215
private readonly _affiliations = new BehaviorSubject<number>(0)
1316
private readonly _displaySideBar = new BehaviorSubject<boolean>(false)
1417
private readonly _displayBiography = new BehaviorSubject<boolean>(false)
@@ -17,7 +20,9 @@ export class RecordHeaderStateService {
1720
private readonly _hasCreditOrOtherNames = new BehaviorSubject<boolean>(false)
1821

1922
readonly loadingUserRecord$ = this._loadingUserRecord.asObservable()
23+
readonly loadingRecordHeader$ = this._loadingRecordHeader.asObservable()
2024
readonly isPublicRecord$ = this._isPublicRecord.asObservable()
25+
readonly userRecord$ = this._userRecord.asObservable()
2126
readonly affiliations$ = this._affiliations.asObservable()
2227
readonly displaySideBar$ = this._displaySideBar.asObservable()
2328
readonly displayBiography$ = this._displayBiography.asObservable()
@@ -29,9 +34,15 @@ export class RecordHeaderStateService {
2934
setLoadingUserRecord(val: boolean) {
3035
this._loadingUserRecord.next(val)
3136
}
37+
setLoadingRecordHeader(val: boolean) {
38+
this._loadingRecordHeader.next(val)
39+
}
3240
setIsPublicRecord(val: string | null) {
3341
this._isPublicRecord.next(val)
3442
}
43+
setUserRecord(val: UserRecord | null) {
44+
this._userRecord.next(val)
45+
}
3546
setAffiliations(val: number) {
3647
this._affiliations.next(val)
3748
}
@@ -53,7 +64,9 @@ export class RecordHeaderStateService {
5364

5465
reset() {
5566
this._loadingUserRecord.next(true)
67+
this._loadingRecordHeader.next(true)
5668
this._isPublicRecord.next(null)
69+
this._userRecord.next(null)
5770
this._affiliations.next(0)
5871
this._displaySideBar.next(false)
5972
this._displayBiography.next(false)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {
2+
Affiliation,
3+
AffiliationUIGroup,
4+
} from 'src/app/types/record-affiliation.endpoint'
5+
6+
export function getFeaturedEmploymentCaption(
7+
affiliations?: AffiliationUIGroup[]
8+
): string {
9+
if (!affiliations || affiliations.length === 0) {
10+
return ''
11+
}
12+
13+
const employmentGroup = affiliations.find((group) => group.type === 'EMPLOYMENT')
14+
15+
if (!employmentGroup || !employmentGroup.affiliationGroup) {
16+
return ''
17+
}
18+
19+
for (const group of employmentGroup.affiliationGroup) {
20+
if (!group.affiliations) {
21+
continue
22+
}
23+
24+
const featuredAffiliation = group.affiliations.find(
25+
(affiliation) =>
26+
affiliation.featured === true &&
27+
affiliation.affiliationType?.value === 'employment'
28+
)
29+
30+
if (featuredAffiliation) {
31+
return formatAffiliationCaption(featuredAffiliation)
32+
}
33+
}
34+
35+
return ''
36+
}
37+
38+
function formatAffiliationCaption(affiliation: Affiliation): string {
39+
const parts: string[] = []
40+
41+
const orgName = affiliation.affiliationName?.value
42+
if (orgName) {
43+
parts.push(orgName)
44+
}
45+
46+
const locationParts: string[] = []
47+
if (affiliation.city?.value) {
48+
locationParts.push(affiliation.city.value)
49+
}
50+
if (affiliation.region?.value) {
51+
locationParts.push(affiliation.region.value)
52+
}
53+
if (affiliation.countryForDisplay) {
54+
locationParts.push(affiliation.countryForDisplay)
55+
} else if (affiliation.country?.value) {
56+
locationParts.push(affiliation.country.value)
57+
}
58+
59+
if (orgName && locationParts.length > 0) {
60+
parts[0] = `${orgName}: ${locationParts.join(', ')}`
61+
} else if (locationParts.length > 0) {
62+
parts.push(locationParts.join(', '))
63+
}
64+
65+
const roleParts: string[] = []
66+
if (affiliation.roleTitle?.value) {
67+
roleParts.push(affiliation.roleTitle.value)
68+
}
69+
if (affiliation.departmentName?.value) {
70+
roleParts.push(affiliation.departmentName.value)
71+
}
72+
73+
if (roleParts.length > 0) {
74+
if (parts.length > 0) {
75+
parts.push(`- ${roleParts.join(', ')}`)
76+
} else {
77+
parts.push(roleParts.join(', '))
78+
}
79+
}
80+
81+
return parts.join(' ')
82+
}
Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,80 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing'
22

33
import { RecordHeaderComponent } from './record-header.component'
4-
import { HttpClientTestingModule } from '@angular/common/http/testing'
54
import { WINDOW_PROVIDERS } from 'src/app/cdk/window'
65
import { PlatformInfoService } from 'src/app/cdk/platform-info'
7-
import { ErrorHandlerService } from 'src/app/core/error-handler/error-handler.service'
8-
import { SnackbarService } from 'src/app/cdk/snackbar/snackbar.service'
9-
import { MatSnackBar } from '@angular/material/snack-bar'
10-
import { MatDialog } from '@angular/material/dialog'
11-
import { Overlay } from '@angular/cdk/overlay'
126
import { RouterTestingModule } from '@angular/router/testing'
137
import { RecordService } from 'src/app/core/record/record.service'
14-
15-
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
168
import { MatTooltipModule } from '@angular/material/tooltip'
9+
import { of } from 'rxjs'
10+
import { HeaderCompactService } from 'src/app/core/header-compact/header-compact.service'
11+
import { RecordHeaderStateService } from 'src/app/core/record-header-state/record-header-state.service'
12+
import { TogglzService } from 'src/app/core/togglz/togglz.service'
13+
import { UserService } from 'src/app/core'
14+
import { getUserRecord } from 'src/app/core/record/record.service.spec'
15+
import { getUserSession } from 'src/app/core/user/user.service.spec'
16+
import { NoopAnimationsModule } from '@angular/platform-browser/animations'
17+
import { RumJourneyEventService } from 'src/app/rum/service/customEvent.service'
18+
import { TogglzFlag } from 'src/app/types/config.endpoint'
19+
import { AffiliationType } from 'src/app/types/record-affiliation.endpoint'
1720

1821
describe('RecordHeaderComponent', () => {
1922
let component: RecordHeaderComponent
2023
let fixture: ComponentFixture<RecordHeaderComponent>
24+
let state: RecordHeaderStateService
25+
let recordService: jasmine.SpyObj<RecordService>
26+
let togglzService: { getStateOf: jasmine.Spy }
2127

2228
beforeEach(async () => {
29+
recordService = jasmine.createSpyObj<RecordService>('RecordService', [
30+
'getRecord',
31+
])
32+
togglzService = {
33+
getStateOf: jasmine.createSpy('getStateOf').and.callFake((flag: string) =>
34+
of(flag === TogglzFlag.FEATURED_AFFILIATIONS)
35+
),
36+
}
37+
2338
await TestBed.configureTestingModule({
2439
imports: [
25-
HttpClientTestingModule,
2640
RouterTestingModule,
41+
NoopAnimationsModule,
2742
MatTooltipModule,
2843
RecordHeaderComponent,
2944
],
3045
providers: [
3146
WINDOW_PROVIDERS,
32-
RecordService,
33-
PlatformInfoService,
34-
ErrorHandlerService,
35-
SnackbarService,
36-
MatSnackBar,
37-
MatDialog,
38-
Overlay,
47+
RecordHeaderStateService,
48+
{ provide: RecordService, useValue: recordService },
49+
{
50+
provide: PlatformInfoService,
51+
useValue: { get: () => of({ columns12: true }) },
52+
},
53+
{
54+
provide: HeaderCompactService,
55+
useValue: { compactActive$: of(false) },
56+
},
57+
{
58+
provide: TogglzService,
59+
useValue: togglzService,
60+
},
61+
{
62+
provide: UserService,
63+
useValue: { getUserSession: () => of(getUserSession()) },
64+
},
65+
{
66+
provide: RumJourneyEventService,
67+
useValue: {
68+
recordSimpleEvent: jasmine.createSpy('recordSimpleEvent'),
69+
},
70+
},
3971
],
40-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
4172
}).compileComponents()
4273
})
4374

4475
beforeEach(() => {
76+
state = TestBed.inject(RecordHeaderStateService)
77+
state.reset()
4578
fixture = TestBed.createComponent(RecordHeaderComponent)
4679
component = fixture.componentInstance
4780
fixture.detectChanges()
@@ -50,4 +83,61 @@ describe('RecordHeaderComponent', () => {
5083
it('should create', () => {
5184
expect(component).toBeTruthy()
5285
})
86+
87+
it('should render header data from shared record state', () => {
88+
const userRecord = getUserRecord()
89+
const orcid = userRecord.userInfo.REAL_USER_ORCID
90+
91+
state.setIsPublicRecord(orcid)
92+
state.setLoadingRecordHeader(false)
93+
state.setUserRecord(userRecord)
94+
fixture.detectChanges()
95+
96+
const text = fixture.nativeElement.textContent
97+
expect(component.bannerTitle).toBe('Published Name')
98+
expect(text).toContain('Published Name')
99+
expect(text).toContain(`https:${runtimeEnvironment.BASE_URL}${orcid}`)
100+
})
101+
102+
it('should not ask RecordService to load record data for the header', () => {
103+
state.setIsPublicRecord(getUserRecord().userInfo.REAL_USER_ORCID)
104+
state.setLoadingRecordHeader(false)
105+
state.setUserRecord(getUserRecord())
106+
fixture.detectChanges()
107+
108+
expect(recordService.getRecord).not.toHaveBeenCalled()
109+
})
110+
111+
it('should keep featured employment caption non-blocking', () => {
112+
const userRecord = getUserRecord()
113+
114+
state.setIsPublicRecord(userRecord.userInfo.REAL_USER_ORCID)
115+
state.setLoadingRecordHeader(false)
116+
state.setUserRecord({ ...userRecord, affiliations: undefined })
117+
fixture.detectChanges()
118+
119+
expect(component.loadingUserRecord).toBeFalse()
120+
expect(component.bannerTitle).toBe('Published Name')
121+
expect(component.bannerCaption).toBe('')
122+
})
123+
124+
it('should render the featured employment caption from shared state', () => {
125+
const userRecord = getUserRecord()
126+
const featuredAffiliation =
127+
userRecord.affiliations[0].affiliationGroup[0].affiliations[0]
128+
129+
featuredAffiliation.featured = true
130+
featuredAffiliation.affiliationType = { value: AffiliationType.employment }
131+
featuredAffiliation.roleTitle = { value: 'Engineer' }
132+
featuredAffiliation.departmentName = { value: 'Platform' }
133+
134+
state.setIsPublicRecord(userRecord.userInfo.REAL_USER_ORCID)
135+
state.setLoadingRecordHeader(false)
136+
state.setUserRecord(userRecord)
137+
fixture.detectChanges()
138+
139+
expect(component.bannerCaption).toBe(
140+
'ORCID: city, region, country - Engineer, Platform'
141+
)
142+
})
53143
})

0 commit comments

Comments
 (0)