Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #763 (2023-10-17 02:38:47)

Fix bugs regarding bury sibling cards
Hi  @st3v3nmw 

This fixes bugs:
https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/759
https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/760

I've included a new test case, and updated a few that were based on an incorrect understanding of the existing 1.10.1 functionality.

Apologies for the extra work!

Cheers
Ronny


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bed66b..1cc39a6 120000
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,1 +1,1 @@
-docs/changelog.md
\ No newline at end of file
+docs/changelog.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 651dc17..9815d5b 120000
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,1 +1,1 @@
-docs/en/contributing.md
\ No newline at end of file
+docs/en/contributing.md
diff --git a/docs/changelog.md b/docs/changelog.md
index d555381..91f73df 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. Dates are d
 
 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
 
+#### Unreleased
+
+-   Fix: Bury Siblings Is Not Honored [`#759`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/759)
+-   Fix: Skipping a card seems to permanently bury it and make it impossible to review again in the same day [`#760`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/760)
+
 #### [1.10.2](https://github.com/st3v3nmw/obsidian-spaced-repetition/compare/1.10.1...1.10.2)
 
 -   refactor: Separate business logic from user interface code [`#751`](https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/751)
diff --git a/src/CardSchedule.ts b/src/CardSchedule.ts
index 12403ab..151940b 100644
--- a/src/CardSchedule.ts
+++ b/src/CardSchedule.ts
@@ -16,6 +16,15 @@ export class CardScheduleInfo {
     ease: number;
     delayBeforeReviewTicks: number;
 
+    // A question can have multiple cards. The schedule info for all sibling cards are formatted together
+    // in a single <!--SR: --> comment, such as:
+    // <!--SR:!2023-09-02,4,270!2023-09-02,5,270!2023-09-02,6,270!2023-09-02,7,270-->
+    //
+    // However, not all sibling cards may have been reviewed. Therefore we need a method of indicating that a particular card
+    // has not been reviewed, and should be considered "new"
+    // This is done by using this magic value for the date
+    private static dummyDueDateForNewCard: string = "2000-01-01";
+
     constructor(dueDate: Moment, interval: number, ease: number, delayBeforeReviewTicks: number) {
         this.dueDate = dueDate;
         this.interval = interval;
@@ -31,9 +40,13 @@ export class CardScheduleInfo {
         return this.dueDate.isSameOrBefore(globalDateProvider.today);
     }
 
-    static getDummySchedule(settings: SRSettings): CardScheduleInfo {
+    isDummyScheduleForNewCard(): boolean {
+        return this.formatDueDate() == CardScheduleInfo.dummyDueDateForNewCard;
+    }
+
+    static getDummyScheduleForNewCard(settings: SRSettings): CardScheduleInfo {
         return CardScheduleInfo.fromDueDateStr(
-            "2000-01-01",
+            CardScheduleInfo.dummyDueDateForNewCard,
             CardScheduleInfo.initialInterval,
             settings.baseEase,
             0,
diff --git a/src/DeckTreeIterator.ts b/src/DeckTreeIterator.ts
index 8f5b39d..0da88f7 100644
--- a/src/DeckTreeIterator.ts
+++ b/src/DeckTreeIterator.ts
@@ -120,13 +120,21 @@ class SingleDeckIterator {
     deleteCurrentQuestion(): void {
         this.ensureCurrentCard();
         const q: Question = this.currentCard.question;
-        const cards: Card[] = this.deck.getCardListForCardType(this.cardListType);
-        do {
-            this.deck.deleteCardAtIndex(this.cardIdx, this.cardListType);
-        } while (this.cardIdx < cards.length && Object.is(q, cards[this.cardIdx].question));
+
+        // A question could have some cards in the new list and some in the due list
+        this.deleteQuestionFromList(q, CardListType.NewCard);
+        this.deleteQuestionFromList(q, CardListType.DueCard);
+
         this.setNoCurrentCard();
     }
 
+    private deleteQuestionFromList(q: Question, cardListType: CardListType): void {
+        const cards: Card[] = this.deck.getCardListForCardType(cardListType);
+        for (let i = cards.length - 1; i >= 0; i--) {
+            if (Object.is(q, cards[i].question)) this.deck.deleteCardAtIndex(i, cardListType);
+        }
+    }
+
     deleteCurrentCard(): void {
         this.ensureCurrentCard();
         this.deck.deleteCardAtIndex(this.cardIdx, this.cardListType);
diff --git a/src/FlashcardReviewSequencer.ts b/src/FlashcardReviewSequencer.ts
index af522bb..871c72b 100644
--- a/src/FlashcardReviewSequencer.ts
+++ b/src/FlashcardReviewSequencer.ts
@@ -113,7 +113,6 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
     }
 
     skipCurrentCard(): void {
-        this.questionPostponementList.addIfRequired(this.currentQuestion);
         this.cardSequencer.deleteCurrentQuestion();
     }
 
@@ -143,7 +142,15 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
         if (response == ReviewResponse.Reset) {
             this.cardSequencer.moveCurrentCardToEndOfList();
             this.cardSequencer.nextCard();
-        } else this.deleteCurrentCard();
+        } else {
+            if (this.settings.burySiblingCards) {
+                this.questionPostponementList.add(this.currentQuestion);
+                await this.questionPostponementList.write();
+                this.cardSequencer.deleteCurrentQuestion();
+            } else {
+                this.deleteCurrentCard();
+            }
+        }
     }
 
     async processReview_CramMode(response: ReviewResponse): Promise<void> {
diff --git a/src/NoteQuestionParser.ts b/src/NoteQuestionParser.ts
index 1f8c01b..fdd63bc 100644
--- a/src/NoteQuestionParser.ts
+++ b/src/NoteQuestionParser.ts
@@ -120,12 +120,15 @@ export class NoteQuestionParser {
             const { front, back } = cardFrontBackList[i];
 
             const hasScheduleInfo: boolean = i < cardScheduleInfoList.length;
+            const schedule: CardScheduleInfo = cardScheduleInfoList[i];
+
             const cardObj: Card = new Card({
                 front,
                 back,
                 cardIdx: i,
             });
-            cardObj.scheduleInfo = hasScheduleInfo ? cardScheduleInfoList[i] : null;
+            cardObj.scheduleInfo =
+                hasScheduleInfo && !schedule.isDummyScheduleForNewCard() ? schedule : null;
 
             siblings.push(cardObj);
         }
diff --git a/src/Question.ts b/src/Question.ts
index 82948c1..c94d879 100644
--- a/src/Question.ts
+++ b/src/Question.ts
@@ -143,7 +143,7 @@ export class Question {
             const card: Card = this.cards[i];
             const schedule: CardScheduleInfo = card.hasSchedule
                 ? card.scheduleInfo
-                : CardScheduleInfo.getDummySchedule(settings);
+                : CardScheduleInfo.getDummyScheduleForNewCard(settings);
             result += schedule.formatSchedule();
         }
         result += SR_HTML_COMMENT_END;
diff --git a/src/QuestionPostponementList.ts b/src/QuestionPostponementList.ts
index 07bcd19..5438616 100644
--- a/src/QuestionPostponementList.ts
+++ b/src/QuestionPostponementList.ts
@@ -4,7 +4,7 @@ import { SRSettings } from "./settings";
 
 export interface IQuestionPostponementList {
     clear(): void;
-    addIfRequired(question: Question): void;
+    add(question: Question): void;
     includes(question: Question): boolean;
     write(): Promise<void>;
 }
@@ -24,8 +24,8 @@ export class QuestionPostponementList implements IQuestionPostponementList {
         this.list = [];
     }
 
-    addIfRequired(question: Question): void {
-        if (this.settings.burySiblingCards) this.list.push(question.questionText.textHash);
+    add(question: Question): void {
+        if (!this.includes(question)) this.list.push(question.questionText.textHash);
     }
 
     includes(question: Question): boolean {
@@ -33,6 +33,9 @@ export class QuestionPostponementList implements IQuestionPostponementList {
     }
 
     async write(): Promise<void> {
+        // This is null only whilst unit testing is being performed
+        if (this.plugin == null) return;
+
         await this.plugin.savePluginData();
     }
 }
diff --git a/src/util/DateProvider.ts b/src/util/DateProvider.ts
index 1674c60..b995818 100644
--- a/src/util/DateProvider.ts
+++ b/src/util/DateProvider.ts
@@ -36,6 +36,21 @@ export class DateUtil {
 
 export let globalDateProvider: IDateProvider = new LiveDateProvider();
 
+const originDate: string = "2023-09-06";
+
+export function setupStaticDateProvider(dateStr: string) {
+    globalDateProvider = StaticDateProvider.fromDateStr(dateStr);
+}
+
+function getOriginDateAsMoment(): Moment {
+    return DateUtil.dateStrToMoment(originDate);
+}
+
+export function setupStaticDateProvider_OriginDatePlusDays(days: number) {
+    const simulatedDate: Moment = getOriginDateAsMoment().add(days, "d");
+    globalDateProvider = new StaticDateProvider(simulatedDate);
+}
+
 export function setupStaticDateProvider_20230906() {
-    globalDateProvider = StaticDateProvider.fromDateStr("2023-09-06");
+    setupStaticDateProvider(originDate);
 }


Test patch:
diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/FlashcardReviewSequencer.test.ts
index 86c1a76..923afdf 100644
--- a/tests/unit/FlashcardReviewSequencer.test.ts
+++ b/tests/unit/FlashcardReviewSequencer.test.ts
@@ -11,18 +11,30 @@ import {
     IFlashcardReviewSequencer,
 } from "src/FlashcardReviewSequencer";
 import { TopicPath } from "src/TopicPath";
-import { CardListType, Deck } from "src/Deck";
+import { CardListType, Deck, DeckTreeFilter } from "src/Deck";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
 import { SampleItemDecks } from "./SampleItems";
 import { UnitTestSRFile } from "src/SRFile";
 import { ReviewResponse } from "src/scheduling";
-import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import {
+    setupStaticDateProvider,
+    setupStaticDateProvider_20230906,
+    setupStaticDateProvider_OriginDatePlusDays,
+} from "src/util/DateProvider";
 import moment from "moment";
 import { INoteEaseList, NoteEaseList } from "src/NoteEaseList";
 import { QuestionPostponementList, IQuestionPostponementList } from "src/QuestionPostponementList";
 import { order_DueFirst_Sequential } from "./DeckTreeIterator.test";
 
+let clozeQuestion1: string = "This single ==question== turns into ==3 separate== ==cards==";
+let clozeQuestion1Card1: RegExp = /This single.+\.\.\..+turns into 3 separate cards/;
+let clozeQuestion1Card2: RegExp = /This single question turns into.+\.\.\..+cards/;
+let clozeQuestion1Card3: RegExp = /This single question turns into 3 separate.+\.\.\./;
+
 class TestContext {
+    settings: SRSettings;
+    reviewMode: FlashcardReviewMode;
+    iteratorOrder: IIteratorOrder;
     cardSequencer: IDeckTreeIterator;
     noteEaseList: INoteEaseList;
     cardScheduleCalculator: CardScheduleCalculator;
@@ -36,13 +48,43 @@ class TestContext {
         Object.assign(this, init);
     }
 
+    async resetContext(text: string, daysAfterOrigin: number): Promise<void> {
+        this.originalText = text;
+        this.file.content = text;
+        let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(
+            this.iteratorOrder,
+            IteratorDeckSource.UpdatedByIterator,
+        );
+        let reviewSequencer: IFlashcardReviewSequencer = new FlashcardReviewSequencer(
+            this.reviewMode,
+            cardSequencer,
+            this.settings,
+            this.cardScheduleCalculator,
+            this.questionPostponementList,
+        );
+        setupStaticDateProvider_OriginDatePlusDays(daysAfterOrigin);
+
+        await this.setSequencerDeckTreeFromOriginalText();
+    }
+
+    // Within the actual application, clearing the postponement list is done in main.ts, and therefore not
+    // unit testable. Within the unit tests, this is used instead.
+    clearQuestionPostponementList(): void {
+        this.questionPostponementList.clear();
+    }
+
     async setSequencerDeckTreeFromOriginalText(): Promise<Deck> {
-        let deck: Deck = await SampleItemDecks.createDeckFromFile(
+        const deckTree: Deck = await SampleItemDecks.createDeckFromFile(
             this.file,
             new TopicPath(["Root"]),
         );
-        this.reviewSequencer.setDeckTree(deck, deck);
-        return deck;
+        const remainingDeckTree = DeckTreeFilter.filterForRemainingCards(
+            this.questionPostponementList,
+            deckTree,
+            this.reviewMode,
+        );
+        this.reviewSequencer.setDeckTree(deckTree, remainingDeckTree);
+        return deckTree;
     }
 
     static Create(
@@ -76,6 +118,9 @@ class TestContext {
         var file: UnitTestSRFile = new UnitTestSRFile(text, fakeFilePath);
 
         let result: TestContext = new TestContext({
+            settings,
+            reviewMode,
+            iteratorOrder,
             cardSequencer,
             noteEaseList,
             cardScheduleCalculator,
@@ -227,6 +272,24 @@ async function setupSample2(reviewMode: FlashcardReviewMode): Promise<TestContex
     return c;
 }
 
+async function checkEmptyPostponementList(
+    burySiblingCards: boolean,
+    flashcardReviewMode: FlashcardReviewMode,
+): Promise<void> {
+    let settings: SRSettings = { ...DEFAULT_SETTINGS };
+    settings.burySiblingCards = burySiblingCards;
+
+    let c: TestContext = await setupSample1(flashcardReviewMode, settings);
+    expect(c.questionPostponementList.list.length).toEqual(0);
+    expect(c.reviewSequencer.currentCard.front).toEqual("Q2");
+
+    // Skip over these 2 questions
+    skipThenCheckCardFront(c.reviewSequencer, "Q1");
+    skipThenCheckCardFront(c.reviewSequencer, "Q3");
+
+    expect(c.questionPostponementList.list.length).toEqual(0);
+}
+
 function skipThenCheckCardFront(sequencer: IFlashcardReviewSequencer, expectedFront: string): void {
     sequencer.skipCurrentCard();
     expect(sequencer.currentCard.front).toEqual(expectedFront);
@@ -234,7 +297,8 @@ function skipThenCheckCardFront(sequencer: IFlashcardReviewSequencer, expectedFr
 
 //////////////////////////////////////////////////////////////////////
 
-beforeAll(() => {
+// Do this before each test, as some tests change the "current" date
+beforeEach(() => {
     setupStaticDateProvider_20230906();
 });
 
@@ -297,8 +361,7 @@ describe("skipCurrentCard", () => {
         skipThenCheckCardFront(c.reviewSequencer, "Q5");
         skipThenCheckCardFront(c.reviewSequencer, "Q6");
 
-        c.reviewSequencer.skipCurrentCard();
-        expect(c.reviewSequencer.hasCurrentCard).toEqual(false);
+        skipAndCheckNoRemainingCards(c);
     });
 
     test("Skipping a card skips all sibling cards", async () => {
@@ -343,57 +406,24 @@ describe("skipCurrentCard", () => {
         expect(c.reviewSequencer.hasCurrentCard).toEqual(false);
     });
 
-    describe("Checking postponement list", () => {
+    describe("Checking postponement list (skipped cards)", () => {
         describe("FlashcardReviewMode.Review", () => {
             test("burySiblingCards=false - skipped question not added to postponement list", async () => {
-                let settings: SRSettings = { ...DEFAULT_SETTINGS };
-                settings.burySiblingCards = false;
-
-                let c: TestContext = await setupSample1(FlashcardReviewMode.Review, settings);
-                expect(c.questionPostponementList.list.length).toEqual(0);
-                expect(c.reviewSequencer.currentCard.front).toEqual("Q2");
-
-                // Skip over these 2 questions
-                skipThenCheckCardFront(c.reviewSequencer, "Q1");
-                skipThenCheckCardFront(c.reviewSequencer, "Q3");
-
-                expect(c.questionPostponementList.list.length).toEqual(0);
+                checkEmptyPostponementList(false, FlashcardReviewMode.Review);
             });
 
-            test("burySiblingCards=true - skipped question added to postponement list", async () => {
-                let settings: SRSettings = { ...DEFAULT_SETTINGS };
-                settings.burySiblingCards = true;
-
-                let c: TestContext = await setupSample1(FlashcardReviewMode.Review, settings);
-                expect(c.questionPostponementList.list.length).toEqual(0);
-                expect(c.reviewSequencer.currentCard.front).toEqual("Q2");
-
-                // Skip over 2 questions
-                skipThenCheckCardFront(c.reviewSequencer, "Q1");
-                skipThenCheckCardFront(c.reviewSequencer, "Q3");
-
-                expect(c.questionPostponementList.list.length).toEqual(2);
+            // https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/760
+            test("burySiblingCards=true - skipped question not added to postponement list", async () => {
+                checkEmptyPostponementList(true, FlashcardReviewMode.Review);
             });
         });
 
         describe("FlashcardReviewMode.Cram", () => {
             test("Cram mode - skipped question not added to postponement list", async () => {
-                let c: TestContext = await setupSample1(FlashcardReviewMode.Cram, DEFAULT_SETTINGS);
-                expect(c.questionPostponementList.list.length).toEqual(0);
-                expect(c.reviewSequencer.currentCard.front).toEqual("Q2");
-
-                // Skip over these questions
-                skipThenCheckCardFront(c.reviewSequencer, "Q1");
-                skipThenCheckCardFront(c.reviewSequencer, "Q3");
-
-                expect(c.questionPostponementList.list.length).toEqual(0);
+                checkEmptyPostponementList(false, FlashcardReviewMode.Cram);
             });
         });
     });
-    // No postponement during cramming
-    // Deletion of sibling cards after text modification
-    // Deletion of sibling cards after card skip
-    // Delete+postpone
 });
 
 describe("processReview", () => {
@@ -463,6 +493,146 @@ describe("processReview", () => {
                 await checkReviewResponse_ReviewMode(ReviewResponse.Easy, expected);
             });
         });
+
+        describe("Checking postponement list (after card reviewed, burySiblingCards=false)", () => {
+            test("reviewed question not added to postponement list; sibling cards are sequenced (not deleted)", async () => {
+                let settings: SRSettings = { ...DEFAULT_SETTINGS };
+                settings.burySiblingCards = false;
+
+                let text: string = `
+#flashcards This single ==question== turns into ==3 separate== ==cards==
+
+Q1::A1
+    `;
+
+                let c: TestContext = TestContext.Create(
+                    order_DueFirst_Sequential,
+                    FlashcardReviewMode.Review,
+                    settings,
+                    text,
+                );
+                await c.setSequencerDeckTreeFromOriginalText();
+                expect(c.cardSequencer.currentDeck.getCardCount(CardListType.All, false)).toEqual(
+                    4,
+                );
+
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card1);
+
+                // After reviewing, sibling cards still present
+                await c.reviewSequencer.processReview(ReviewResponse.Easy);
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card2);
+                await c.reviewSequencer.processReview(ReviewResponse.Good);
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card3);
+
+                // After reviewing last sibling, move to next card
+                await c.reviewSequencer.processReview(ReviewResponse.Hard);
+                expect(c.reviewSequencer.currentCard.front).toEqual("Q1");
+
+                skipAndCheckNoRemainingCards(c);
+                checkQuestionPostponementListCount(c, 0);
+            });
+        });
+
+        describe("Checking postponement list (after card reviewed, burySiblingCards=true)", () => {
+            test("reviewed question added to postponement list; sibling cards are buried", async () => {
+                let settings: SRSettings = { ...DEFAULT_SETTINGS };
+                settings.burySiblingCards = true;
+
+                let text: string = `
+#flashcards ${clozeQuestion1}
+
+Q1::A1
+    `;
+
+                let c: TestContext = TestContext.Create(
+                    order_DueFirst_Sequential,
+                    FlashcardReviewMode.Review,
+                    settings,
+                    text,
+                );
+                await c.setSequencerDeckTreeFromOriginalText();
+                expect(c.cardSequencer.currentDeck.getCardCount(CardListType.All, false)).toEqual(
+                    4,
+                );
+
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card1);
+
+                // After reviewing, sibling cards skipped
+                await c.reviewSequencer.processReview(ReviewResponse.Easy);
+                expect(c.reviewSequencer.currentCard.front).toEqual("Q1");
+
+                skipAndCheckNoRemainingCards(c);
+
+                // Single question on the list ()
+                checkQuestionPostponementListCount(c, 1);
+            });
+
+            test("card reviewed as hard, after restarting the review process, that question skipped and next question is shown", async () => {
+                let settings: SRSettings = { ...DEFAULT_SETTINGS };
+                settings.burySiblingCards = true;
+
+                let text: string = `
+#flashcards ${clozeQuestion1}
+
+Q1::A1
+    `;
+
+                // Simulate performing the review on 2023-09-06
+                // Check that the reviewed card, scheduled for following day; 2 buried cards have schedule dates with magic number indicating unreviewed card ("2000-01-01")
+                setupStaticDateProvider_OriginDatePlusDays(0);
+                const c: TestContext = TestContext.Create(
+                    order_DueFirst_Sequential,
+                    FlashcardReviewMode.Review,
+                    settings,
+                    text,
+                );
+                await c.setSequencerDeckTreeFromOriginalText();
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card1);
+                await c.reviewSequencer.processReview(ReviewResponse.Hard);
+                text = c.file.content;
+                let expectedCard1Review: string = "2023-09-07,1,230";
+                expect(text).toContain(
+                    `<!--SR:!${expectedCard1Review}!2000-01-01,1,250!2000-01-01,1,250-->`,
+                );
+                checkQuestionPostponementListCount(c, 1);
+
+                // Reset the context to the new content (that now includes the schedule info); simulate same day
+                // First question not shown (as all of its cards have been "buried"); second question shown
+                let daysAfterOrigin: number = 0;
+                await c.resetContext(text, daysAfterOrigin);
+                expect(c.reviewSequencer.currentCard.front).toEqual("Q1");
+
+                // Simulate next day 2023-09-07
+                // First card (rated as hard the previous day) is reshown; now reviewed as Good
+                c.clearQuestionPostponementList();
+                daysAfterOrigin = 1;
+                await c.resetContext(text, daysAfterOrigin);
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card1);
+                await c.reviewSequencer.processReview(ReviewResponse.Good);
+                text = c.file.content;
+                expectedCard1Review = "2023-09-09,2,230";
+                expect(text).toContain(
+                    `<!--SR:!${expectedCard1Review}!2000-01-01,1,250!2000-01-01,1,250-->`,
+                );
+                expect(c.reviewSequencer.currentCard.front).toEqual("Q1");
+
+                // Simulate next day 2023-09-08
+                // First card (rated as Good the previous day) is skipped; second sibling shown
+                // Post review of second sibling, third sibling skipped and subsequent question Q1 shown
+                c.clearQuestionPostponementList();
+                daysAfterOrigin = 2;
+                await c.resetContext(text, daysAfterOrigin);
+                expect(c.reviewSequencer.currentCard.front).toMatch(clozeQuestion1Card2);
+                await c.reviewSequencer.processReview(ReviewResponse.Easy);
+                text = c.file.content;
+                expectedCard1Review = "2023-09-09,2,230";
+                let expectedCard2Review: string = "2023-09-12,4,270";
+                expect(text).toContain(
+                    `<!--SR:!${expectedCard1Review}!${expectedCard2Review}!2000-01-01,1,250-->`,
+                );
+                expect(c.reviewSequencer.currentCard.front).toEqual("Q1");
+            });
+        });
     });
 
     describe("FlashcardReviewMode.Cram", () => {
@@ -799,6 +969,15 @@ ${updatedStr}
     });
 });
 
+function checkQuestionPostponementListCount(c: TestContext, expectedListLength: number) {
+    expect(c.questionPostponementList.list.length).toEqual(expectedListLength);
+}
+
+function skipAndCheckNoRemainingCards(c: TestContext) {
+    c.reviewSequencer.skipCurrentCard();
+    expect(c.reviewSequencer.hasCurrentCard).toEqual(false);
+}
+
 async function checkUpdateCurrentQuestionText(
     noteText: string,
     updatedQ: string,