Files
step3/tests/flashcard_theme_responsive_test.gd
T
hz 09b670d5d4 Close the gap between the flashcards study screen and the mockup
Add real session-history navigation (Previous/Next/Shuffle), compact
side panels, and a segmented progress row to match the mockup layout.
Flashcards now opens straight into Study, with library management
moved into the Options menu ("Manage Flashcards") and Export hidden
from the UI (Import stays). Shuffle is backed by a real, due-before-new
randomized pick on the backend, not a decorative toggle.
2026-07-22 00:43:52 -05:00

153 lines
7.4 KiB
GDScript

extends SceneTree
# Flashcards previously had no coverage for theme switching or narrow-viewport layout: the
# library screen's `_refresh_status()` and the study screen's `refresh_theme_controls()` were
# both no-op stubs, and neither screen reacted to its own resize. This drives both screens
# through a dark/light toggle and a phone-width resize with no network calls (mirrors
# flashcard_study_render_test.gd: content is injected directly rather than fetched).
const Brand := preload("res://scripts/ui/medmind_style.gd")
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 theme_service = root.get_node("UIThemeService")
var prior_dark: bool = theme_service.is_dark_mode()
var Hub := load("res://scripts/ui/flashcards_hub.gd")
var hub = Hub.new()
hub.size = host.size
host.add_child(hub)
await process_frame
hub.notes = [{"id": "note-1", "type": "basic", "prompt": "2 + 2", "answer": "4", "tags": []}]
theme_service.set_dark_mode(true)
hub._render_notes()
await process_frame
var dark_list_box := hub.list_card.get_theme_stylebox("panel") as StyleBoxFlat
_assert(dark_list_box.bg_color.is_equal_approx(Color("081426")),
"the flashcards list card should use the dark surface while dark mode is active")
var dark_row := hub.list_container.get_child(0) as BoxContainer
var dark_icon_box := (dark_row.get_child(0) as PanelContainer).get_theme_stylebox("panel") as StyleBoxFlat
_assert(dark_icon_box.bg_color.is_equal_approx(Brand.icon_circle_box(true, Brand.ROYAL_BLUE, 20).bg_color),
"a basic card's type icon should use its dark accent styling")
theme_service.set_dark_mode(false)
await process_frame
var light_list_box := hub.list_card.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_list_box.bg_color.is_equal_approx(Color.WHITE),
"toggling the app theme should rebuild the flashcards list card with the light surface (the library screen's theme_changed handler was previously a no-op)")
var light_row := hub.list_container.get_child(0) as BoxContainer
var light_icon_box := (light_row.get_child(0) as PanelContainer).get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_icon_box.bg_color.is_equal_approx(Brand.icon_circle_box(false, Brand.ROYAL_BLUE, 20).bg_color),
"a basic card's type icon should use its light accent styling")
var light_menu_box := hub.import_menu.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_menu_box.bg_color.is_equal_approx(Brand.panel_box(false).bg_color),
"the import menu panel should also restyle to the light surface after a theme toggle")
theme_service.set_dark_mode(prior_dark)
hub.size = Vector2(360, 800)
hub._update_responsive_layout()
await process_frame
_assert(hub.compact_layout, "the flashcards library should detect a phone-width viewport")
var compact_row := hub.list_container.get_child(0) as BoxContainer
_assert(compact_row.vertical, "phone-width flashcard rows should stack their content above the edit/archive actions instead of clipping")
_assert(compact_row.get_combined_minimum_size().x <= hub.size.x,
"a phone-width flashcard row should fit inside the viewport (minimum %.1f px)" % compact_row.get_combined_minimum_size().x)
hub.size = Vector2(1280, 800)
hub._update_responsive_layout()
await process_frame
_assert(not hub.compact_layout, "a wide viewport should restore the flashcards library's desktop row layout")
var wide_row := hub.list_container.get_child(0) as BoxContainer
_assert(not wide_row.vertical, "a wide viewport should lay flashcard row content out horizontally again")
hub.free()
var Study := load("res://scripts/ui/flashcards_study.gd")
var study = Study.new()
study._build()
theme_service.set_dark_mode(true)
study.refresh_theme_controls()
var dark_study_box := study.card_panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(dark_study_box.bg_color.is_equal_approx(Brand.content_card_box(true, true, Brand.ROYAL_BLUE).bg_color),
"the study card should use the dark active-card surface while dark mode is active")
theme_service.set_dark_mode(false)
study.refresh_theme_controls()
var light_study_box := study.card_panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_study_box.bg_color.is_equal_approx(Brand.content_card_box(false, true, Brand.ROYAL_BLUE).bg_color),
"toggling the app theme should restyle the study card to the light surface (refresh_theme_controls was previously a no-op)")
_assert(light_study_box.border_color.is_equal_approx(Brand.ROYAL_BLUE),
"the study card should keep its accent border after a theme refresh")
theme_service.set_dark_mode(prior_dark)
study.size = Vector2(360, 720)
study._update_responsive_layout()
_assert(study.compact_layout, "the study screen should detect a phone-width viewport")
_assert(study.card_panel.custom_minimum_size.x <= study.size.x,
"the study card should shrink to fit a phone-width viewport instead of overflowing (minimum %.1f px on a %.1f px viewport)" % [study.card_panel.custom_minimum_size.x, study.size.x])
study.size = Vector2(1280, 720)
study._update_responsive_layout()
_assert(not study.compact_layout, "a wide viewport should restore the study screen's desktop layout")
_assert(is_equal_approx(study.card_panel.custom_minimum_size.x, 720.0),
"a wide viewport should restore the study card's full 720px width")
_assert(study.deck_panel.visible and study.stats_panel.visible,
"a wide viewport should show the current-deck and spaced-repetition side panels")
study.size = Vector2(360, 720)
study._update_responsive_layout()
_assert(not study.deck_panel.visible and not study.stats_panel.visible,
"a phone-width viewport should hide the side panels rather than squeeze a 3-column layout into no room")
study.deck_id = "deck-1"
study.decks = [
{"deck": {"id": "deck-1", "name": "Nephrology"}, "total": 150, "mastered": 48},
{"deck": {"id": "deck-2", "name": "Cardiology"}, "total": 40, "mastered": 40},
]
study._refresh_deck_panel()
_assert(study.deck_name_label.text == "Nephrology", "the deck panel should show the currently-studied deck's name, not just the first one")
_assert(study.deck_mastered_label.text == "48 / 150 cards mastered", "the deck panel should report mastered/total for the current deck only")
study.deck_id = ""
study._refresh_deck_panel()
_assert(study.deck_name_label.text == "All decks", "studying with no deck filter should label the panel \"All decks\"")
_assert(study.deck_mastered_label.text == "88 / 190 cards mastered", "\"All decks\" should sum mastered/total across every deck")
study.latest_counts = {
"due": 5, "newAvailable": 15, "learning": 48, "review": 132,
"currentStreakDays": 1, "reviewsToday": 25, "dailyGoal": 50
}
study._refresh_stats_panel()
_assert(study.stats_due_label.text == "5" and study.stats_learning_label.text == "48" and study.stats_review_label.text == "132",
"the spaced-repetition panel should reflect the latest due/learning/review counts")
_assert(study.streak_label.text == "1 day", "a streak of exactly one day should use the singular form")
_assert(study.daily_goal_label.text == "25 / 50" and is_equal_approx(study.daily_goal_bar.value, 25.0),
"the daily goal panel should show today's review count against the account's configured goal")
study.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)