extends SceneTree # Exercises the study screen's session-history navigation (skip / Previous / Next / undo-in- # history) and the layout/footer-state fixes added alongside it. The screen is added to the # tree (mirrors flashcard_theme_responsive_test.gd) so _input()'s tree-visibility guard behaves # realistically; every FlashcardService call this drives (request_next_card/bury_cards/ # suspend_cards) short-circuits before touching the network because AuthState reports no # logged-in session in this headless context, matching the "no network" style of the other # flashcard tests. var failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: var host := Control.new() host.size = Vector2(1280, 800) root.add_child(host) var Study := load("res://scripts/ui/flashcards_study.gd") var screen = Study.new() host.add_child(screen) await process_frame # ---------- Layout: side panels must hug their content, not stretch full height ---------- _assert(screen.deck_panel.size_flags_vertical == Control.SIZE_SHRINK_BEGIN, "the current-deck panel should not stretch to the row's full height") _assert(screen.stats_panel.size_flags_vertical == Control.SIZE_SHRINK_BEGIN, "the spaced-repetition panel should not stretch to the row's full height") # ---------- Footer button states at session start (no history, no live card) ---------- _assert(screen.previous_button.disabled, "Previous should be disabled with no history to step back into") _assert(screen.next_button.disabled, "Next should be disabled with no live card to skip and no history to page through") screen.current_note = {"type": "basic", "prompt": "P3(live)", "answer": "A3", "extra": ""} screen.current_card = {"id": "card-3", "variant": 0, "revision": 1} screen.rating_options = [] screen.due_remaining = 5 screen.new_remaining = 3 screen.session_total = 10 screen._update_footer_button_states() _assert(not screen.next_button.disabled, "Next should enable once a live card is present, to allow skipping it") _assert(screen.previous_button.disabled, "Previous should still be disabled before any history exists") # ---------- Options menu opens even with no live card, and disables Bury/Suspend ---------- screen.current_note = {} screen.current_card = {} screen._toggle_options_menu() _assert(screen.options_menu.visible, "Options should open even with no live card (e.g. deck exhausted)") _assert(screen.options_bury_button.disabled and screen.options_suspend_button.disabled, "Bury/Suspend should disable when there is no live card to act on") screen._close_options_menu() screen.current_note = {"type": "basic", "prompt": "P3(live)", "answer": "A3", "extra": ""} screen.current_card = {"id": "card-3", "variant": 0, "revision": 1} screen._toggle_options_menu() _assert(not screen.options_bury_button.disabled and not screen.options_suspend_button.disabled, "Bury/Suspend should enable once a live card is present") screen._close_options_menu() # ---------- Undo-in-history only surfaces for the matching most-recently-graded entry ---------- screen.session_history = [{ "note": {"type": "basic", "prompt": "P1", "answer": "A1", "extra": ""}, "card": {"id": "card-1", "variant": 0, "revision": 1}, "rating_options": [], "action": "graded", "card_id": "card-1", "rating": "good", "client_review_id": "review-1" }] screen.last_card_id = "card-99" screen.last_client_review_id = "review-1" screen._render_history_entry(0) _assert(not screen.undo_button.visible, "undo should not surface when the browsed entry's card id doesn't match the last graded card") screen.last_card_id = "" screen.last_client_review_id = "" screen._render_history_entry(0) _assert(not screen.undo_button.visible, "undo should not surface when there is no outstanding undoable review at all") # ---------- Skip: pushes a "skipped" history entry, excludes the card, no grading occurs ---------- screen.seen_card_ids = [] screen.session_history = [] screen.history_cursor = 0 screen.current_note = {"type": "basic", "prompt": "P3(live)", "answer": "A3", "extra": ""} screen.current_card = {"id": "card-3", "variant": 0, "revision": 1} screen.awaiting_next = false screen._skip_current_card() _assert(screen.session_history.size() == 1 and screen.session_history[0]["action"] == "skipped", "skipping the live card should push a 'skipped' history entry") _assert(screen.seen_card_ids.has("card-3"), "skipping should still exclude the card from reappearing this session") # ---------- Previous/Next paging through a hand-seeded history, back to live ---------- screen.seen_card_ids = [] screen.last_card_id = "card-1" screen.last_client_review_id = "review-1" screen.session_history = [ { "note": {"type": "basic", "prompt": "P1", "answer": "A1", "extra": ""}, "card": {"id": "card-1", "variant": 0, "revision": 1}, "rating_options": [], "action": "graded", "card_id": "card-1", "rating": "good", "client_review_id": "review-1" }, { "note": {"type": "basic", "prompt": "P2", "answer": "A2", "extra": ""}, "card": {"id": "card-2", "variant": 0, "revision": 1}, "rating_options": [], "action": "skipped", "card_id": "card-2" } ] screen.history_cursor = screen.session_history.size() screen.current_note = {"type": "basic", "prompt": "P3(live)", "answer": "A3", "extra": ""} screen.current_card = {"id": "card-3", "variant": 0, "revision": 1} screen.rating_options = [] screen.due_remaining = 5 screen.new_remaining = 3 screen.revealed = false screen._go_previous() _assert(screen.front_label.text == "P2", "the first Previous press should show the most recently added history entry") _assert(not screen.grade_row.visible, "grading buttons should stay hidden while browsing history") _assert(not screen.undo_button.visible, "a skipped history entry should never offer 'undo this rating'") screen._go_previous() _assert(screen.front_label.text == "P1", "a second Previous press should step further back in history") _assert(screen.undo_button.visible, "the specific most-recently-graded entry should surface 'undo this rating'") screen._go_previous() _assert(screen.history_cursor == 0, "Previous should refuse to step before the start of history") screen._go_next() _assert(screen.front_label.text == "P2", "Next while browsing history should step forward, not skip the live card") screen._go_next() _assert(screen.front_label.text == "P3(live)" and screen.history_cursor == screen.session_history.size(), "stepping past the last history entry should return to the live card") _assert(screen.reveal_button.visible and not screen.grade_row.visible, "returning to a live, unrevealed card should restore the reveal prompt") screen._go_next() _assert(screen.session_history.size() == 3 and screen.session_history[2]["action"] == "skipped" and screen.session_history[2]["card_id"] == "card-3", "Next while already at the live card should skip it (session-only, dual-meaning behavior)") _assert(screen.seen_card_ids.has("card-3"), "skipping via Next should still exclude the card this session") # ---------- Keyboard shortcuts are ignored while browsing history ---------- screen.session_history = [{ "note": {"type": "basic", "prompt": "P1", "answer": "A1", "extra": ""}, "card": {"id": "card-1", "variant": 0, "revision": 1}, "rating_options": [], "action": "graded", "card_id": "card-1" }] screen.history_cursor = 0 screen.current_note = {"type": "basic", "prompt": "P1", "answer": "A1", "extra": ""} screen.current_card = {"id": "card-1", "variant": 0, "revision": 1} screen.revealed = true var key_event := InputEventKey.new() key_event.keycode = KEY_1 key_event.pressed = true screen._input(key_event) _assert(screen.session_history.size() == 1, "a grading keystroke while browsing history should be ignored instead of grading the historical card") screen.free() host.queue_free() await process_frame quit(1 if failures > 0 else 0) func _assert(condition: bool, message: String) -> void: if condition: return failures += 1 push_error(message)