Add flashcards feature with spaced repetition scheduling
Adds a full flashcards system: FSRS-based scheduling, cloze/basic card types, CSV import/export, media storage, and study UI, wired into navigation and the dashboard alongside existing question flows.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
using System.Text.Json;
|
||||
using QuestionsApi.Models;
|
||||
using QuestionsApi.Services.Flashcards;
|
||||
using Xunit;
|
||||
|
||||
namespace QuestionsApi.Tests;
|
||||
|
||||
public sealed class FlashcardImportExportModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void JsonExport_RoundTrips_NotesCardsAndReviewHistory()
|
||||
{
|
||||
var export = new FlashcardJsonExport
|
||||
{
|
||||
SchemaVersion = 1,
|
||||
Settings = new FlashcardSettings { UserId = "user-1", NewCardsPerDay = 20, ReviewsPerDay = 200 },
|
||||
Decks = [new FlashcardDeck { Id = "deck-1", UserId = "user-1", Name = "Cardio", IsDefault = true }],
|
||||
Notes =
|
||||
[
|
||||
new FlashcardJsonExportNote
|
||||
{
|
||||
Note = new FlashcardNote
|
||||
{
|
||||
Id = "note-1", UserId = "user-1", DeckId = "deck-1", StableId = "stable-1",
|
||||
Type = FlashcardTypes.Cloze, Prompt = "{{c1::Paris}} is the capital of France",
|
||||
Tags = ["geography"]
|
||||
},
|
||||
Cards =
|
||||
[
|
||||
new FlashcardSchedule
|
||||
{
|
||||
Id = "card-1", UserId = "user-1", NoteId = "note-1", Variant = 1,
|
||||
State = FlashcardScheduleStates.Review, Stability = 12.3, Difficulty = 4.5,
|
||||
DueUtc = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
],
|
||||
ReviewLogs =
|
||||
[
|
||||
new FlashcardReviewLog
|
||||
{
|
||||
Id = "log-1", UserId = "user-1", CardId = "card-1", Rating = FlashcardRatings.Good,
|
||||
ReviewedUtc = new DateTime(2025, 12, 31, 0, 0, 0, DateTimeKind.Utc)
|
||||
}
|
||||
],
|
||||
Images = [new FlashcardJsonExportImage { AssetId = "asset-1", FileName = "x.png", ContentType = "image/png", Base64Bytes = Convert.ToBase64String([1, 2, 3]) }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(export);
|
||||
var roundTripped = JsonSerializer.Deserialize<FlashcardJsonExport>(json);
|
||||
|
||||
Assert.NotNull(roundTripped);
|
||||
var note = Assert.Single(roundTripped!.Notes);
|
||||
Assert.Equal("stable-1", note.Note.StableId);
|
||||
Assert.Equal(FlashcardTypes.Cloze, note.Note.Type);
|
||||
var card = Assert.Single(note.Cards);
|
||||
Assert.Equal(1, card.Variant);
|
||||
Assert.Equal(12.3, card.Stability);
|
||||
var log = Assert.Single(note.ReviewLogs);
|
||||
Assert.Equal(FlashcardRatings.Good, log.Rating);
|
||||
var image = Assert.Single(note.Images);
|
||||
Assert.Equal([1, 2, 3], Convert.FromBase64String(image.Base64Bytes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fingerprint_IsStable_ForSameTypePromptAndAnswer_IgnoringSurroundingWhitespace()
|
||||
{
|
||||
var a = FlashcardNoteStore.Fingerprint(FlashcardTypes.Basic, " What is 2+2? ", "4");
|
||||
var b = FlashcardNoteStore.Fingerprint(FlashcardTypes.Basic, "What is 2+2?", "4");
|
||||
|
||||
Assert.Equal(a, b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fingerprint_DiffersWhenAnswerDiffers()
|
||||
{
|
||||
var a = FlashcardNoteStore.Fingerprint(FlashcardTypes.Basic, "What is 2+2?", "4");
|
||||
var b = FlashcardNoteStore.Fingerprint(FlashcardTypes.Basic, "What is 2+2?", "five");
|
||||
|
||||
Assert.NotEqual(a, b);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fingerprint_DiffersByType_ForSamePromptAndAnswer()
|
||||
{
|
||||
var basic = FlashcardNoteStore.Fingerprint(FlashcardTypes.Basic, "text", "text");
|
||||
var cloze = FlashcardNoteStore.Fingerprint(FlashcardTypes.Cloze, "text", "text");
|
||||
|
||||
Assert.NotEqual(basic, cloze);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user