Files
step3/api/QuestionsApi.Tests/FlashcardSchedulingTests.cs
T
hz 70aa425fde Rebuild flashcard study screen from mockup, with real backend-backed stats
Study screen now matches the provided mockup's layout: a "Current
Deck" panel with a circular mastery-progress ring, and a "Spaced
Repetition" panel (New/Due/Learning/Review counts, streak, daily
goal) on either side of the card. Kept the app's real global sidebar
rather than the mockup's own nav, per direction.

Mastery/Streak/Daily Goal weren't tracked anywhere, so this adds real
backend support rather than showing fabricated numbers:
- Mastered = graduated to review state AND FSRS stability >= 21 days
  (FlashcardScheduleStore.MasteryStabilityDaysThreshold), stricter
  than the plain Review-state count
- CurrentStreakDays = consecutive UTC days with a review, computed
  from review-log timestamps in the new pure FlashcardScheduling
  .ComputeStreakDays (unit tested, habit-streak semantics: doesn't
  reset to 0 the instant a new day starts before today's first review)
- DailyGoal/ReviewsToday reuse the account's existing ReviewsPerDay
  setting rather than inventing a new one
Also removes the exit "X" button in favor of the same Escape/pause
menu the question-player flow uses (main.gd's shared app_menu, with
an "Exit Study Session" action), and adds a new circular_progress.gd
reusable ring control plus a real "Options" menu (bury/suspend the
current card) wired to schedule endpoints that had no UI before.
2026-07-21 23:20:14 -05:00

106 lines
3.5 KiB
C#

using QuestionsApi.Services.Flashcards;
using Xunit;
namespace QuestionsApi.Tests;
public sealed class FlashcardSchedulingTests
{
[Fact]
public void NextStudyDayStart_IsMidnightOfTheFollowingDay()
{
var studyDayStart = new DateTime(2026, 3, 5, 0, 0, 0, DateTimeKind.Utc);
var next = FlashcardScheduling.NextStudyDayStart(studyDayStart);
Assert.Equal(new DateTime(2026, 3, 6, 0, 0, 0, DateTimeKind.Utc), next);
}
[Fact]
public void NextStudyDayStart_TruncatesTimeOfDayBeforeAdvancing()
{
// A session that started mid-day still buries siblings until the *next calendar day*,
// not exactly 24h later.
var studyDayStart = new DateTime(2026, 3, 5, 14, 30, 0, DateTimeKind.Utc);
var next = FlashcardScheduling.NextStudyDayStart(studyDayStart);
Assert.Equal(new DateTime(2026, 3, 6, 0, 0, 0, DateTimeKind.Utc), next);
}
[Theory]
[InlineData(20, null, 0, 20)]
[InlineData(20, null, 15, 5)]
[InlineData(20, null, 25, 0)]
[InlineData(20, 5, 0, 5)]
[InlineData(20, 0, 0, 0)]
public void ResolveDailyBudget_DeckOverrideWinsOverGlobal_ClampedAtZero(
int globalLimit, int? deckOverride, int consumedToday, int expected)
{
Assert.Equal(expected, FlashcardScheduling.ResolveDailyBudget(globalLimit, deckOverride, consumedToday));
}
private static readonly DateTime Today = new(2026, 3, 10, 0, 0, 0, DateTimeKind.Utc);
[Fact]
public void ComputeStreakDays_NoReviews_IsZero()
{
Assert.Equal(0, FlashcardScheduling.ComputeStreakDays([], Today));
}
[Fact]
public void ComputeStreakDays_ReviewedTodayOnly_IsOne()
{
var timestamps = new List<DateTime> { Today.AddHours(9) };
Assert.Equal(1, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
[Fact]
public void ComputeStreakDays_ThreeConsecutiveDaysEndingToday_IsThree()
{
var timestamps = new List<DateTime>
{
Today.AddDays(-2).AddHours(8), Today.AddDays(-1).AddHours(20), Today.AddHours(7)
};
Assert.Equal(3, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
[Fact]
public void ComputeStreakDays_NotYetReviewedToday_StillCountsThroughYesterday()
{
// The streak shouldn't visibly reset to 0 the instant a new calendar day begins, before
// the user has had a chance to study today.
var timestamps = new List<DateTime> { Today.AddDays(-2).AddHours(8), Today.AddDays(-1).AddHours(20) };
Assert.Equal(2, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
[Fact]
public void ComputeStreakDays_GapBeforeYesterday_BreaksTheStreak()
{
var timestamps = new List<DateTime>
{
Today.AddDays(-10).AddHours(8), Today.AddDays(-1).AddHours(20), Today.AddHours(7)
};
Assert.Equal(2, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
[Fact]
public void ComputeStreakDays_MissedYesterdayAndToday_IsZero()
{
var timestamps = new List<DateTime> { Today.AddDays(-3).AddHours(8) };
Assert.Equal(0, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
[Fact]
public void ComputeStreakDays_MultipleReviewsSameDay_CountOnceTowardTheStreak()
{
var timestamps = new List<DateTime> { Today.AddHours(7), Today.AddHours(8), Today.AddHours(9) };
Assert.Equal(1, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
}