Skip to content

Commit a09c57e

Browse files
committed
sa: add draft-jobs.component tests
1 parent de136f1 commit a09c57e

File tree

4 files changed

+4074
-3
lines changed

4 files changed

+4074
-3
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/draft-jobs.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<div class="table-wrapper">
2828
<table mat-table id="draft-jobs-table" [dataSource]="rows">
2929
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
30-
<tr mat-row *matRowDef="let row; columns: columnsToDisplay"></tr>
30+
<tr class="job-row" mat-row *matRowDef="let row; columns: columnsToDisplay"></tr>
3131
<ng-container matColumnDef="status">
3232
<th mat-header-cell *matHeaderCellDef>Status</th>
3333
<td mat-cell *matCellDef="let row">
@@ -159,7 +159,7 @@
159159
</div>
160160

161161
@if (rows.length === 0) {
162-
<p>No draft jobs found for the selected time period.</p>
162+
<p id="no-draft-jobs-found-message">No draft jobs found for the selected time period.</p>
163163
}
164164
}
165165
} @else {
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
2+
import { provideHttpClientTesting } from '@angular/common/http/testing';
3+
import { DebugElement } from '@angular/core';
4+
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
5+
import { By } from '@angular/platform-browser';
6+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
7+
import { ActivatedRoute, RouterModule } from '@angular/router';
8+
import { BehaviorSubject } from 'rxjs';
9+
import { anything, mock, verify, when } from 'ts-mockito';
10+
import { AuthService } from 'xforge-common/auth.service';
11+
import { DialogService } from 'xforge-common/dialog.service';
12+
import { I18nService } from 'xforge-common/i18n.service';
13+
import { OnlineStatusService } from 'xforge-common/online-status.service';
14+
import { TestOnlineStatusModule } from 'xforge-common/test-online-status.module';
15+
import { TestOnlineStatusService } from 'xforge-common/test-online-status.service';
16+
import { TestRealtimeModule } from 'xforge-common/test-realtime.module';
17+
import { TestRealtimeService } from 'xforge-common/test-realtime.service';
18+
import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils';
19+
import { SF_TYPE_REGISTRY } from '../core/models/sf-type-registry';
20+
import { SFProjectService } from '../core/sf-project.service';
21+
import { EventMetric, EventScope } from '../event-metrics/event-metric';
22+
import { DraftJob, DraftJobsComponent } from './draft-jobs.component';
23+
import sampleEvents from './sample-events.json';
24+
import { ServalAdministrationService } from './serval-administration.service';
25+
26+
const mockedActivatedRoute = mock(ActivatedRoute);
27+
const mockedAuthService = mock(AuthService);
28+
const mockedDialogService = mock(DialogService);
29+
const mockedI18nService = mock(I18nService);
30+
const mockedProjectService = mock(SFProjectService);
31+
const mockedServalAdministrationService = mock(ServalAdministrationService);
32+
33+
describe('DraftJobsComponent', () => {
34+
configureTestingModule(() => ({
35+
imports: [
36+
NoopAnimationsModule,
37+
TestTranslocoModule,
38+
TestOnlineStatusModule.forRoot(),
39+
TestRealtimeModule.forRoot(SF_TYPE_REGISTRY),
40+
RouterModule.forRoot([])
41+
],
42+
providers: [
43+
provideHttpClient(withInterceptorsFromDi()),
44+
provideHttpClientTesting(),
45+
{ provide: ActivatedRoute, useMock: mockedActivatedRoute },
46+
{ provide: AuthService, useMock: mockedAuthService },
47+
{ provide: DialogService, useMock: mockedDialogService },
48+
{ provide: I18nService, useMock: mockedI18nService },
49+
{ provide: OnlineStatusService, useClass: TestOnlineStatusService },
50+
{ provide: SFProjectService, useMock: mockedProjectService },
51+
{ provide: ServalAdministrationService, useMock: mockedServalAdministrationService }
52+
]
53+
}));
54+
55+
it('should show jobs from event metrics', fakeAsync(() => {
56+
const env = new TestEnvironment();
57+
env.wait();
58+
expect(env.component.rows.length).toBeGreaterThan(0);
59+
expect(env.rows.length).toBeGreaterThan(0);
60+
expect(env.emptyMessage).toBeNull();
61+
}));
62+
63+
it('should display empty state when no jobs', fakeAsync(() => {
64+
const env = new TestEnvironment({ hasEvents: false });
65+
env.wait();
66+
expect(env.rows.length).toBe(0);
67+
expect(env.emptyMessage).not.toBeNull();
68+
expect(env.emptyMessage!.nativeElement.textContent).toContain('No draft jobs found');
69+
}));
70+
71+
it('should reload data when time period changes', fakeAsync(() => {
72+
const env = new TestEnvironment();
73+
env.wait();
74+
env.component.daysBack = 14;
75+
env.wait();
76+
verify(mockedProjectService.onlineAllEventMetricsForConstructingDraftJobs(anything(), anything(), 14)).once();
77+
}));
78+
79+
describe('associates events into jobs', () => {
80+
it('successful jobs', fakeAsync(() => {
81+
const env = new TestEnvironment();
82+
env.wait();
83+
// The Serval build id and event metric ids are written here from looking at the sample data.
84+
const servalBuildId = '68fbaaba6c00da700a86c979';
85+
const startEventId = '68fbaa9d6a411b337164c6ca';
86+
const buildEventId = '68fbaaba6a411b337164c6d0';
87+
// I would argue the finish event should be 68fbcb406a411b337164c877, the BuildCompletedAsync event.
88+
const finishEventId = '68fbcb3e6a411b337164c86f';
89+
90+
const draftJob: DraftJob = env.component.rows.filter(row => row.job.buildId === servalBuildId)[0].job;
91+
// Verify test setup.
92+
expect(draftJob.status).toEqual('success');
93+
94+
expect(draftJob!.startEvent!.id).toEqual(startEventId);
95+
expect(draftJob!.buildEvent!.id).toEqual(buildEventId);
96+
expect(draftJob!.finishEvent!.id).toEqual(finishEventId);
97+
}));
98+
99+
it('cancelled jobs', fakeAsync(() => {
100+
const env = new TestEnvironment();
101+
env.wait();
102+
const servalBuildId = '68fb7cfd6c00da700a863d21';
103+
const startEventId = '68fb7cc46a411b337164c38d';
104+
const buildEventId = '68fb7cfd6a411b337164c393';
105+
// I would think finish event id should be 68fb7d636a411b337164c39e BuildCompletedAsync (or perhaps
106+
// 68fb7d5a6a411b337164c39a CancelPreTranslationBuildAsync).
107+
const finishEventId = '68fb7d636a411b337164c39e';
108+
109+
const draftJob: DraftJob = env.component.rows.filter(row => row.job.buildId === servalBuildId)[0].job;
110+
// Verify test setup.
111+
expect(draftJob.status).toEqual('cancelled');
112+
113+
expect(draftJob!.startEvent!.id).toEqual(startEventId);
114+
expect(draftJob!.buildEvent!.id).toEqual(buildEventId);
115+
expect(draftJob!.finishEvent!.id).toEqual(finishEventId);
116+
}));
117+
118+
it('faulted jobs', fakeAsync(() => {
119+
const env = new TestEnvironment();
120+
env.wait();
121+
const servalBuildId = '68fa5b626c00da700a848a94';
122+
const startEventId = '68fa5b496a411b3371647957';
123+
const buildEventId = '68fa5b636a411b337164795d';
124+
const finishEventId = '68fa5b6d6a411b3371647961';
125+
126+
const draftJob: DraftJob = env.component.rows.filter(row => row.job.buildId === servalBuildId)[0].job;
127+
// Verify test setup.
128+
expect(draftJob.status).toEqual('failed');
129+
130+
expect(draftJob!.startEvent!.id).toEqual(startEventId);
131+
expect(draftJob!.buildEvent!.id).toEqual(buildEventId);
132+
expect(draftJob!.finishEvent!.id).toEqual(finishEventId);
133+
}));
134+
});
135+
136+
class TestEnvironment {
137+
readonly component: DraftJobsComponent;
138+
readonly fixture: ComponentFixture<DraftJobsComponent>;
139+
readonly realtimeService: TestRealtimeService = TestBed.inject<TestRealtimeService>(TestRealtimeService);
140+
readonly testOnlineStatusService: TestOnlineStatusService = TestBed.inject(
141+
OnlineStatusService
142+
) as TestOnlineStatusService;
143+
private readonly queryParams$ = new BehaviorSubject<any>({});
144+
145+
constructor({ hasEvents = true }: { hasEvents?: boolean } = {}) {
146+
when(mockedActivatedRoute.queryParams).thenReturn(this.queryParams$.asObservable());
147+
when(mockedAuthService.currentUserRoles).thenReturn([]);
148+
when(mockedI18nService.formatDate(anything(), anything())).thenCall((date: Date) => date.toISOString());
149+
if (hasEvents) this.setupDraftJobsData();
150+
this.fixture = TestBed.createComponent(DraftJobsComponent);
151+
this.component = this.fixture.componentInstance;
152+
this.fixture.detectChanges();
153+
}
154+
155+
get table(): DebugElement | null {
156+
return this.fixture.debugElement.query(By.css('#draft-jobs-table'));
157+
}
158+
159+
get rows(): DebugElement[] {
160+
if (!this.table) return [];
161+
return this.table.queryAll(By.css('.job-row'));
162+
}
163+
164+
get emptyMessage(): DebugElement | null {
165+
return this.fixture.debugElement.query(By.css('#no-draft-jobs-found-message'));
166+
}
167+
168+
wait(): void {
169+
flush();
170+
this.fixture.detectChanges();
171+
}
172+
173+
setupDraftJobsData(): void {
174+
const eventMetrics: EventMetric[] = this.transformJsonToEventMetrics(sampleEvents);
175+
176+
when(
177+
mockedProjectService.onlineAllEventMetricsForConstructingDraftJobs(anything(), anything(), anything())
178+
).thenCall((eventTypes: string[], projectId?: string, _daysBack?: number) => {
179+
let events: EventMetric[] = eventMetrics.filter(event => eventTypes.includes(event.eventType));
180+
if (projectId != null) events = events.filter(event => event.projectId === projectId);
181+
return { results: events, unpagedCount: events.length };
182+
});
183+
184+
// Mock project names via ServalAdministrationService for projects in JSON
185+
const projectIds = [...new Set(eventMetrics.map(e => e.projectId).filter((id): id is string => id != null))];
186+
projectIds.forEach(projectId => {
187+
when(mockedServalAdministrationService.get(projectId)).thenResolve({
188+
id: projectId,
189+
data: { name: `Project ${projectId.substring(0, 8)}`, shortName: projectId.substring(0, 4) }
190+
} as any);
191+
});
192+
}
193+
194+
/**
195+
* Transforms JSON event data to EventMetric objects.
196+
* Transforms "timeStamp":{"$date":"foo"} to just "timeStamp".
197+
*/
198+
private transformJsonToEventMetrics(jsonData: any[]): EventMetric[] {
199+
return jsonData.map(event => ({
200+
id: event._id,
201+
eventType: event.eventType,
202+
timeStamp: typeof event.timeStamp === 'string' ? event.timeStamp : (event.timeStamp?.$date ?? ''),
203+
scope: event.scope as EventScope,
204+
payload: event.payload ?? {},
205+
userId: event.userId,
206+
projectId: event.projectId,
207+
result: event.result,
208+
executionTime: event.executionTime,
209+
exception: event.exception
210+
}));
211+
}
212+
}
213+
});

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/draft-jobs.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ interface ProjectBooks {
2828
books: string[];
2929
}
3030

31-
interface DraftJob {
31+
export interface DraftJob {
32+
/** Serval build ID */
3233
buildId: string | null;
3334
projectId: string;
3435
startEvent?: EventMetric; // Made optional since incomplete jobs might not have a start event

0 commit comments

Comments
 (0)