mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Replaces the notify-me quiz stub with a full loop: quiz sessions per couple, answer-and-guess flow, dual-direction scoring, and a score-only share card (no question or answer content) captured via ViewShot with Sauci branding. API: migration 0019 (quiz_questions, quiz_sessions, quiz_answers, 12 seeded questions), new quiz domain with session start / current / answers / result routes, couple isolation, route + integration + scoring unit tests. Mobile: src/features/quiz feature (intro, per-question self+guess flow, waiting-for-partner nudge, results with breakdown), quizStore with sign-out clearing, QuizShareModal following SharePreviewModal, discovery QuizTile, analytics events, and tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { computeQuizScore, type QuizAnswerPair } from '../src/domains/quiz/service.js';
|
|
|
|
function pair(overrides: Partial<QuizAnswerPair> = {}): QuizAnswerPair {
|
|
return {
|
|
questionId: 'q1',
|
|
aSelfIndex: 0,
|
|
aGuessIndex: 0,
|
|
bSelfIndex: 0,
|
|
bGuessIndex: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('computeQuizScore', () => {
|
|
it('scores 100% when every guess matches the partner self-answer in both directions', () => {
|
|
const pairs: QuizAnswerPair[] = [
|
|
pair({ questionId: 'q1', aSelfIndex: 1, aGuessIndex: 2, bSelfIndex: 2, bGuessIndex: 1 }),
|
|
pair({ questionId: 'q2', aSelfIndex: 0, aGuessIndex: 3, bSelfIndex: 3, bGuessIndex: 0 }),
|
|
];
|
|
const { scorePercent } = computeQuizScore(pairs);
|
|
expect(scorePercent).toBe(100);
|
|
});
|
|
|
|
it('scores 0% when nobody guesses their partner correctly', () => {
|
|
const pairs: QuizAnswerPair[] = [
|
|
pair({ questionId: 'q1', aSelfIndex: 1, aGuessIndex: 0, bSelfIndex: 2, bGuessIndex: 3 }),
|
|
];
|
|
expect(computeQuizScore(pairs).scorePercent).toBe(0);
|
|
});
|
|
|
|
it('counts each direction independently, so a half-right couple lands at 50%', () => {
|
|
// A guesses B correctly, but B guesses A incorrectly: one hit out of two possible.
|
|
const pairs: QuizAnswerPair[] = [
|
|
pair({ questionId: 'q1', aSelfIndex: 1, aGuessIndex: 2, bSelfIndex: 2, bGuessIndex: 0 }),
|
|
];
|
|
const result = computeQuizScore(pairs);
|
|
expect(result.scorePercent).toBe(50);
|
|
expect(result.perQuestion[0]).toMatchObject({ aGuessedRight: true, bGuessedRight: false });
|
|
});
|
|
|
|
it('rounds to the nearest whole percent', () => {
|
|
// 1 hit out of 6 possible (3 questions * 2 directions) = 16.67%, rounds to 17.
|
|
const pairs: QuizAnswerPair[] = [
|
|
// aGuess (0) matches bSelf (0): one hit. bGuess (3) does not match aSelf (1).
|
|
pair({ questionId: 'q1', aSelfIndex: 1, aGuessIndex: 0, bSelfIndex: 0, bGuessIndex: 3 }),
|
|
pair({ questionId: 'q2', aSelfIndex: 0, aGuessIndex: 3, bSelfIndex: 0, bGuessIndex: 3 }),
|
|
pair({ questionId: 'q3', aSelfIndex: 0, aGuessIndex: 3, bSelfIndex: 0, bGuessIndex: 3 }),
|
|
];
|
|
expect(computeQuizScore(pairs).scorePercent).toBe(17);
|
|
});
|
|
|
|
it('returns 0% for an empty question set instead of dividing by zero', () => {
|
|
expect(computeQuizScore([]).scorePercent).toBe(0);
|
|
});
|
|
});
|