Skip to content

Commit 282b387

Browse files
committed
feat(registration-recent-activity): second round of code fixes regarding review comments
1 parent e4eb2c1 commit 282b387

24 files changed

+529
-138
lines changed
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { InjectionToken } from '@angular/core';
22

33
import { environment } from 'src/environments/environment';
4+
import { AppEnvironment } from '@shared/models/environment.model';
45

5-
export const ENVIRONMENT = new InjectionToken<typeof environment>('App Environment', {
6+
export const ENVIRONMENT = new InjectionToken<AppEnvironment>('App Environment', {
67
providedIn: 'root',
7-
factory: () => environment,
8+
factory: () => environment as AppEnvironment,
89
});

src/app/core/guards/require-registration-id.guard.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,98 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { provideStore, Store } from '@ngxs/store';
3+
import { ActivatedRoute } from '@angular/router';
4+
5+
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
6+
import { provideHttpClientTesting } from '@angular/common/http/testing';
7+
8+
import { TranslateService } from '@ngx-translate/core';
9+
import { of } from 'rxjs';
210

311
import { RecentActivityComponent } from './recent-activity.component';
12+
import { ActivityLogDisplayService } from '@shared/services';
13+
import { GetActivityLogs } from '@shared/stores/activity-logs';
14+
import { ActivityLogsState } from '@shared/stores/activity-logs/activity-logs.state';
415

516
describe('RecentActivityComponent', () => {
6-
let component: RecentActivityComponent;
717
let fixture: ComponentFixture<RecentActivityComponent>;
18+
let store: Store;
819

920
beforeEach(async () => {
1021
await TestBed.configureTestingModule({
1122
imports: [RecentActivityComponent],
23+
providers: [
24+
provideStore([ActivityLogsState]),
25+
26+
provideHttpClient(withInterceptorsFromDi()),
27+
provideHttpClientTesting(),
28+
29+
{
30+
provide: TranslateService,
31+
useValue: {
32+
instant: (k: string) => k,
33+
get: () => of(''),
34+
stream: () => of(''),
35+
onLangChange: of({}),
36+
onDefaultLangChange: of({}),
37+
onTranslationChange: of({}),
38+
},
39+
},
40+
41+
{ provide: ActivatedRoute, useValue: { snapshot: { params: { id: 'proj123' } }, parent: null } },
42+
{ provide: ActivityLogDisplayService, useValue: { getActivityDisplay: jest.fn().mockReturnValue('FMT') } },
43+
],
1244
}).compileComponents();
1345

46+
store = TestBed.inject(Store);
47+
store.reset({
48+
activityLogs: {
49+
activityLogs: { data: [], isLoading: false, error: null, totalCount: 0 },
50+
},
51+
} as any);
52+
1453
fixture = TestBed.createComponent(RecentActivityComponent);
15-
component = fixture.componentInstance;
54+
fixture.componentRef.setInput('pageSize', 10);
1655
fixture.detectChanges();
1756
});
1857

1958
it('should create', () => {
20-
expect(component).toBeTruthy();
59+
expect(fixture.componentInstance).toBeTruthy();
60+
});
61+
62+
it('formats activity logs using ActivityLogDisplayService', () => {
63+
store.reset({
64+
activityLogs: {
65+
activityLogs: {
66+
data: [{ id: 'log1', date: '2024-01-01T00:00:00Z' }],
67+
isLoading: false,
68+
error: null,
69+
totalCount: 1,
70+
},
71+
},
72+
} as any);
73+
74+
fixture.detectChanges();
75+
76+
const formatted = fixture.componentInstance.formattedActivityLogs();
77+
expect(formatted.length).toBe(1);
78+
expect(formatted[0].formattedActivity).toBe('FMT');
79+
});
80+
81+
it('dispatches GetActivityLogs with numeric page and pageSize on page change', () => {
82+
const dispatchSpy = jest.spyOn(store, 'dispatch');
83+
fixture.componentInstance.onPageChange({ page: 2 } as any);
84+
85+
expect(dispatchSpy).toHaveBeenCalled();
86+
const action = dispatchSpy.mock.calls.at(-1)?.[0] as GetActivityLogs;
87+
88+
expect(action).toBeInstanceOf(GetActivityLogs);
89+
expect(action.projectId).toBe('proj123');
90+
expect(action.page).toBe(3);
91+
expect(action.pageSize).toBe(10);
92+
});
93+
94+
it('computes firstIndex correctly', () => {
95+
fixture.componentInstance['currentPage'].set(3);
96+
expect(fixture.componentInstance['firstIndex']()).toBe(20);
2197
});
2298
});
Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,69 @@
1-
import { MockComponent } from 'ng-mocks';
2-
31
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { RouterTestingModule } from '@angular/router/testing';
3+
import { provideStore, Store } from '@ngxs/store';
4+
import { ActivatedRoute } from '@angular/router';
5+
import { of } from 'rxjs';
46

5-
import { SubHeaderComponent } from '@osf/shared/components';
7+
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
8+
import { provideHttpClientTesting } from '@angular/common/http/testing';
69

710
import { ProjectOverviewComponent } from './project-overview.component';
11+
import { GetActivityLogs } from '@shared/stores/activity-logs';
12+
13+
import { DataciteService } from '@osf/shared/services';
14+
import { DialogService } from 'primeng/dynamicdialog';
15+
import { TranslateService } from '@ngx-translate/core';
16+
import { ToastService } from '@osf/shared/services';
817

918
describe('ProjectOverviewComponent', () => {
10-
let component: ProjectOverviewComponent;
1119
let fixture: ComponentFixture<ProjectOverviewComponent>;
20+
let component: ProjectOverviewComponent;
21+
let store: Store;
1222

1323
beforeEach(async () => {
24+
TestBed.overrideComponent(ProjectOverviewComponent, { set: { template: '' } });
25+
1426
await TestBed.configureTestingModule({
15-
imports: [ProjectOverviewComponent, MockComponent(SubHeaderComponent)],
27+
imports: [ProjectOverviewComponent, RouterTestingModule],
28+
providers: [
29+
provideStore([]),
30+
31+
{ provide: ActivatedRoute, useValue: { snapshot: { params: { id: 'proj123' } }, parent: null } },
32+
33+
provideHttpClient(withInterceptorsFromDi()),
34+
provideHttpClientTesting(),
35+
36+
{ provide: DataciteService, useValue: {} },
37+
{ provide: DialogService, useValue: { open: () => ({ onClose: of(null) }) } },
38+
{ provide: TranslateService, useValue: { instant: (k: string) => k } },
39+
{ provide: ToastService, useValue: { showSuccess: jest.fn() } },
40+
],
1641
}).compileComponents();
1742

43+
store = TestBed.inject(Store);
1844
fixture = TestBed.createComponent(ProjectOverviewComponent);
1945
component = fixture.componentInstance;
20-
fixture.detectChanges();
2146
});
2247

2348
it('should create', () => {
2449
expect(component).toBeTruthy();
2550
});
51+
52+
it('dispatches GetActivityLogs with numeric page and pageSize on init', () => {
53+
const dispatchSpy = jest.spyOn(store, 'dispatch');
54+
55+
jest.spyOn(component as any, 'setupDataciteViewTrackerEffect').mockReturnValue(of(null));
56+
57+
component.ngOnInit();
58+
59+
const actions = dispatchSpy.mock.calls.map((c) => c[0]);
60+
const activityAction = actions.find((a) => a instanceof GetActivityLogs) as GetActivityLogs;
61+
62+
expect(activityAction).toBeDefined();
63+
expect(activityAction.projectId).toBe('proj123');
64+
expect(activityAction.page).toBe(1);
65+
expect(activityAction.pageSize).toBe(5);
66+
expect(typeof activityAction.page).toBe('number');
67+
expect(typeof activityAction.pageSize).toBe('number');
68+
});
2669
});

src/app/features/registry/pages/recent-activity/registration-recent-activity.component.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ <h2 id="recent-activity-title" class="mb-2" data-test="recent-activity-title">
77
{{ 'project.overview.recentActivity.title' | translate }}
88
</h2>
99

10-
@defer (when !isLoading()) {
10+
@if (!isLoading()) {
1111
<div role="list" data-test="recent-activity-list">
1212
@for (activityLog of formattedActivityLogs(); track activityLog.id) {
1313
<div
@@ -38,5 +38,13 @@ <h2 id="recent-activity-title" class="mb-2" data-test="recent-activity-title">
3838
(pageChanged)="onPageChange($event)"
3939
/>
4040
}
41+
} @else {
42+
<div class="flex flex-column gap-2" data-test="recent-activity-skeleton">
43+
<p-skeleton width="100%" height="2rem" />
44+
<p-skeleton width="100%" height="2rem" />
45+
<p-skeleton width="100%" height="2rem" />
46+
<p-skeleton width="100%" height="2rem" />
47+
<p-skeleton width="100%" height="2rem" />
48+
</div>
4149
}
4250
</div>

src/app/features/registry/pages/recent-activity/registration-recent-activity.component.scss

Whitespace-only changes.

src/app/features/registry/pages/recent-activity/registration-recent-activity.component.spec.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
2+
import { provideHttpClientTesting } from '@angular/common/http/testing';
3+
import { TranslateService } from '@ngx-translate/core';
4+
import { of } from 'rxjs';
5+
import { ActivityLogDisplayService } from '@shared/services';
6+
17
import { provideStore, Store } from '@ngxs/store';
28

39
import { ComponentFixture, TestBed } from '@angular/core/testing';
@@ -17,6 +23,27 @@ describe('RegistrationRecentActivityComponent', () => {
1723
imports: [RegistrationRecentActivityComponent],
1824
providers: [
1925
provideStore([ActivityLogsState]),
26+
27+
provideHttpClient(withInterceptorsFromDi()),
28+
provideHttpClientTesting(),
29+
30+
{
31+
provide: TranslateService,
32+
useValue: {
33+
instant: (k: string) => k,
34+
get: () => of(''),
35+
stream: () => of(''),
36+
onLangChange: of({}),
37+
onDefaultLangChange: of({}),
38+
onTranslationChange: of({}),
39+
},
40+
},
41+
42+
{
43+
provide: ActivityLogDisplayService,
44+
useValue: { getActivityDisplay: jest.fn(() => '<b>formatted</b>') },
45+
},
46+
2047
{ provide: ActivatedRoute, useValue: { snapshot: { params: { id: 'reg123' } }, parent: null } },
2148
],
2249
}).compileComponents();
@@ -55,7 +82,7 @@ describe('RegistrationRecentActivityComponent', () => {
5582
data: [
5683
{
5784
id: 'log1',
58-
date: '2024-01-01T00:00:00Z',
85+
date: '2024-01-01T12:34:00Z',
5986
formattedActivity: '<b>formatted</b>',
6087
},
6188
],
@@ -70,10 +97,29 @@ describe('RegistrationRecentActivityComponent', () => {
7097
const item = fixture.nativeElement.querySelector('[data-test="recent-activity-item"]');
7198
const content = fixture.nativeElement.querySelector('[data-test="recent-activity-item-content"]');
7299
const paginator = fixture.nativeElement.querySelector('[data-test="recent-activity-paginator"]');
100+
const dateText = fixture.nativeElement.querySelector('[data-test="recent-activity-item-date"]')?.textContent ?? '';
73101

74102
expect(item).toBeTruthy();
75103
expect(content?.innerHTML).toContain('formatted');
76104
expect(paginator).toBeTruthy();
105+
expect(dateText).toMatch(/\w{3} \d{1,2}, \d{4} \d{1,2}:\d{2} [AP]M/);
106+
});
107+
108+
it('does not render paginator when totalCount <= pageSize', () => {
109+
store.reset({
110+
activityLogs: {
111+
activityLogs: {
112+
data: [{ id: 'log1', date: '2024-01-01T12:34:00Z', formattedActivity: '<b>formatted</b>' }],
113+
isLoading: false,
114+
error: null,
115+
totalCount: 10,
116+
},
117+
},
118+
} as any);
119+
fixture.detectChanges();
120+
121+
const paginator = fixture.nativeElement.querySelector('[data-test="recent-activity-paginator"]');
122+
expect(paginator).toBeFalsy();
77123
});
78124

79125
it('dispatches on page change', () => {
@@ -87,11 +133,64 @@ describe('RegistrationRecentActivityComponent', () => {
87133
expect(action.page).toBe(3);
88134
});
89135

136+
it('does not dispatch when page change event has undefined page', () => {
137+
const dispatchSpy = store.dispatch as jest.Mock;
138+
dispatchSpy.mockClear();
139+
140+
fixture.componentInstance.onPageChange({} as any);
141+
expect(dispatchSpy).not.toHaveBeenCalled();
142+
});
143+
144+
it('computes firstIndex correctly after page change', () => {
145+
fixture.componentInstance.onPageChange({ page: 1 } as any);
146+
const firstIndex = (fixture.componentInstance as any)['firstIndex']();
147+
expect(firstIndex).toBe(10);
148+
});
149+
90150
it('clears store on destroy', () => {
91151
const dispatchSpy = store.dispatch as jest.Mock;
92152
dispatchSpy.mockClear();
93153

94154
fixture.destroy();
95155
expect(dispatchSpy).toHaveBeenCalledWith(expect.any(ClearActivityLogsStore));
96156
});
157+
158+
it('shows skeleton while loading', () => {
159+
store.reset({
160+
activityLogs: {
161+
activityLogs: { data: [], isLoading: true, error: null, totalCount: 0 },
162+
},
163+
} as any);
164+
165+
fixture.detectChanges();
166+
167+
expect(fixture.nativeElement.querySelector('[data-test="recent-activity-skeleton"]')).toBeTruthy();
168+
expect(fixture.nativeElement.querySelector('[data-test="recent-activity-list"]')).toBeFalsy();
169+
expect(fixture.nativeElement.querySelector('[data-test="recent-activity-paginator"]')).toBeFalsy();
170+
});
171+
172+
it('renders expected ARIA roles/labels', () => {
173+
store.reset({
174+
activityLogs: {
175+
activityLogs: {
176+
data: [{ id: 'log1', date: '2024-01-01T12:34:00Z', formattedActivity: '<b>formatted</b>' }],
177+
isLoading: false,
178+
error: null,
179+
totalCount: 1,
180+
},
181+
},
182+
} as any);
183+
fixture.detectChanges();
184+
185+
const region = fixture.nativeElement.querySelector('[role="region"]');
186+
const heading = fixture.nativeElement.querySelector('#recent-activity-title');
187+
const list = fixture.nativeElement.querySelector('[role="list"]');
188+
const listitem = fixture.nativeElement.querySelector('[role="listitem"]');
189+
190+
expect(region).toBeTruthy();
191+
expect(region.getAttribute('aria-labelledby')).toBe('recent-activity-title');
192+
expect(heading).toBeTruthy();
193+
expect(list).toBeTruthy();
194+
expect(listitem).toBeTruthy();
195+
});
97196
});

0 commit comments

Comments
 (0)