feat: Challenges Page and Challenge Solve Modal 1.03

This commit is contained in:
OpenVelo Agent
2026-07-23 03:11:16 +00:00
parent c434e5d3a2
commit 1c64f057f9
10 changed files with 229 additions and 63 deletions
+107
View File
@@ -0,0 +1,107 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injector, runInInjectionContext } from '@angular/core';
import { Observable } from 'rxjs';
import { ChallengesApiService } from '../../frontend/src/app/features/challenges/challenges.service';
import { ChallengeDetail } from '../../frontend/src/app/features/challenges/challenges.pure';
interface CapturedRequest {
url: string;
params: HttpParams;
withCredentials: boolean;
}
function makeDetail(): ChallengeDetail {
return {
challenge: {
id: 'c1',
name: 'alpha',
descriptionMd: '# Alpha',
categoryId: 'cat1',
categoryAbbreviation: 'CRY',
categoryIconPath: '',
difficulty: 'LOW',
livePoints: 90,
solveCount: 1,
solvedByMe: false,
},
solvers: [
{
position: 1,
playerId: 'p1',
playerName: 'alice',
solvedAtUtc: '2025-01-01T00:00:00.000Z',
awardedPoints: 100,
basePoints: 100,
rankBonus: 0,
isFirst: true,
isSecond: false,
isThird: false,
},
],
};
}
class StubHttpClient {
public captured: CapturedRequest | null = null;
get<T>(url: string, options: { withCredentials?: boolean; params?: HttpParams | Record<string, string> }): Observable<T> {
this.captured = {
url,
params:
options.params instanceof HttpParams
? options.params
: new HttpParams({ fromObject: (options.params ?? {}) as Record<string, string> }),
withCredentials: !!options.withCredentials,
};
return new Observable<T>((subscriber) => {
subscriber.next(makeDetail() as unknown as T);
subscriber.complete();
});
}
post<T>(_url: string, _body: unknown, _options: unknown): Observable<T> {
return new Observable<T>();
}
}
describe('ChallengesApiService.getDetail', () => {
let http: StubHttpClient;
let service: ChallengesApiService;
beforeEach(() => {
http = new StubHttpClient();
const injector = Injector.create({
providers: [
{ provide: HttpClient, useValue: http },
ChallengesApiService,
],
});
service = runInInjectionContext(injector, () => injector.get(ChallengesApiService));
});
it('requests the challenge detail with include=solvers and withCredentials', (done) => {
service.getDetail('c1').subscribe(() => {
try {
expect(http.captured).not.toBeNull();
expect(http.captured!.url).toBe('/api/v1/challenges/c1');
expect(http.captured!.params.get('include')).toBe('solvers');
expect(http.captured!.withCredentials).toBe(true);
done();
} catch (err) {
done(err);
}
});
});
it('encodes the challenge id in the URL path', (done) => {
service.getDetail('c/with spaces').subscribe(() => {
try {
expect(http.captured!.url).toBe('/api/v1/challenges/c%2Fwith%20spaces');
expect(http.captured!.params.get('include')).toBe('solvers');
done();
} catch (err) {
done(err);
}
});
});
});