feat: Challenges Page and Challenge Solve Modal 1.05

This commit is contained in:
OpenVelo Agent
2026-07-23 04:06:47 +00:00
parent c874e96879
commit 8abb633a64
4 changed files with 105 additions and 122 deletions
@@ -0,0 +1,64 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const sourcePath = resolve(
__dirname,
'../../frontend/src/app/features/challenges/challenge-card.component.ts',
);
const templatePath = resolve(
__dirname,
'../../frontend/src/app/features/challenges/challenge-card.component.ts',
);
const source = readFileSync(sourcePath, 'utf8');
describe('ChallengeCardComponent — per-card solved accessibility contract', () => {
function extractTemplate(src: string): string {
const match = src.match(/template:\s*`([\s\S]*?)`,?\s*\n\s*\}/);
return match ? match[1] : '';
}
const template = extractTemplate(source);
it('wraps the card in a native <button type="button"> so it is keyboard-activatable', () => {
expect(template).toMatch(/<button[^>]*type="button"[^>]*>/);
expect(template).not.toMatch(/class="card"[^>]*role=/);
});
it('binds the click output on the button root and preserves open.emit semantics', () => {
expect(template).toMatch(/\(click\)\s*=\s*"open\.emit\(card\.id\)"/);
});
it('preserves the challenge-card-<uuid> data-testid on the new button root', () => {
expect(template).toMatch(/\[attr\.data-testid\]="'challenge-card-' \+ card\.id"/);
});
it('exposes a player-specific solved state on the accessibility tree via aria-label', () => {
expect(template).toMatch(
/\[attr\.aria-label\]="'Challenge ' \+ card\.name \+ \(card\.solvedByMe \? ', solved' : ', not solved'\)"/,
);
});
it('exposes an aria-pressed value derived solely from card.solvedByMe', () => {
const matches = template.match(/\[attr\.aria-pressed\]="card\.solvedByMe"/g) ?? [];
expect(matches.length).toBe(1);
});
it('keeps the conditional checkmark glyph tied to card.solvedByMe and hidden from assistive tech', () => {
expect(template).toMatch(/\*ngIf="card\.solvedByMe"/);
expect(template).toMatch(/class="check"/);
expect(template).toMatch(/aria-hidden="true"/);
});
it('does not derive solved state from solve count, points, or any global field', () => {
expect(template).not.toMatch(/solvedByMe\s*\|\|/);
expect(template).not.toMatch(/solvedByMe\s*&&/);
expect(template).not.toMatch(/solveCount\s*>\s*0/);
expect(template).not.toMatch(/livePoints\s*[<>]=?\s*0/);
expect(template).toMatch(/card\.solvedByMe\s*\?\s*'true'\s*:\s*'false'/);
expect(template).toMatch(/card\.solvedByMe\s*\?\s*', solved'\s*:\s*', not solved'/);
});
it('still binds the solved CSS class solely to card.solvedByMe for visual feedback', () => {
expect(template).toMatch(/\[class\.solved\]="card\.solvedByMe"/);
});
});