Files
step3/tests/web_responsive_test.gd
hz 46953ee71a Reach Settings/Profile from the pause menu, with a way back
The pause menu (Escape during a question/CCS/flashcards run) only had
Resume/UI Sounds/Exit Run/Log Out. Settings already has an equivalent
"Interface sounds" toggle, so drop the duplicate from the pause menu.
More importantly, "player" and "ccs_player" hide the entire app shell
(header + sidebar), so there was no way to reach Settings or Profile
at all mid-session - add Profile/Settings buttons to the pause menu to
close that gap.

Navigating there follows the existing return_screen payload
convention (already used by simulation_history/results/review) rather
than inventing new navigation infrastructure: _navigate_from_pause()
remembers current_screen_name and passes it through show_screen's
payload. Both screens show a "Back to session" button when that's
set, and hide it otherwise (including on ordinary non-paused visits).

show_screen() unconditionally resumes the question timer via
_close_app_menu() as a side effect of any navigation, so
_navigate_from_pause() re-pauses it immediately when leaving "player" -
the timer then resumes for real, for free, when the user actually
returns via show_screen(return_screen).

progress_profile.gd rebuilds its whole child tree on every theme
toggle (_rebuild -> _build), which would otherwise wipe the button's
visibility - added _refresh_back_to_session_visibility(), called both
after _build() and from on_show(), so it survives a theme change while
paused.

Flipped tests/web_responsive_test.gd's assertion, which previously
required the pause menu NOT to have a Profile button.

Verified end-to-end headlessly: opening the pause menu from a live
"player" session, navigating to Settings and Profile, confirming
current_question_pause_started_at is correctly paused while away and
resumed on return, confirming the back-to-session button appears only
when reached via pause (not on ordinary nav), and confirming it
survives a dark/light theme toggle on the Profile screen.
2026-07-25 22:30:03 -05:00

112 lines
7.3 KiB
GDScript

extends SceneTree
var failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var host := Control.new()
host.size = Vector2(240, 720)
root.add_child(host)
var selector = load("res://scripts/ui/study_selector.gd").new()
selector.size = host.size
host.add_child(selector)
await process_frame
selector._show_stage("step3")
selector._refresh_grid_columns()
await process_frame
_assert(selector.stage_scroll != null, "study selection should scroll vertically on short web viewports")
_assert(selector.stage_scroll.horizontal_scroll_mode == ScrollContainer.SCROLL_MODE_DISABLED, "study selection should never introduce horizontal scrolling")
_assert(selector.mode_grid.columns == 1, "phone-width mode cards should use one column")
_assert(selector.stage_content.get_combined_minimum_size().x <= host.size.x, "phone-width study content should fit inside the web canvas (minimum %.1f px)" % selector.stage_content.get_combined_minimum_size().x)
for card in selector.responsive_cards:
_assert(card.custom_minimum_size.x == 0.0, "phone-width cards should release their desktop minimum width")
for row in selector.responsive_card_rows:
_assert(row.vertical, "phone-width card content should stack instead of clipping")
_assert(selector.mode_view.find_children("*", "RichTextLabel", true, false).is_empty(), "study modes should not render the legacy rich-text simulation note")
var question_bank_icon := selector.mode_view.find_child("QuestionBankIcon", true, false) as TextureRect
_assert(question_bank_icon != null, "question bank should expose its mode icon")
if question_bank_icon != null:
_assert(question_bank_icon.texture.resource_path == "res://assets/icons/lucide/clipboard-list.svg", "question bank should use a web-safe vector icon")
selector._show_stage("question_bank")
await process_frame
_assert(selector.question_bank_launch_view.visible and not selector.mode_view.visible, "direct question bank navigation should show only the focused launch state")
_assert(selector.launch_retry_button != null and selector.launch_error_label != null, "question bank launch should retain retry and error controls")
var main_screen = load("res://scripts/ui/main.gd").new()
main_screen.size = host.size
host.add_child(main_screen)
await process_frame
main_screen._update_responsive_layout()
_assert(main_screen.screens.has("dashboard"), "the authenticated shell should include the dashboard screen")
_assert(main_screen.theme_button.custom_minimum_size == Vector2(44, 44), "the theme toggle should keep a stable square footprint")
_assert(main_screen.theme_button.size.x >= 44.0 and main_screen.theme_button.size.y >= 44.0, "the theme toggle should not collapse into an oval")
_assert(main_screen.theme_button.icon_alignment == HORIZONTAL_ALIGNMENT_CENTER, "the theme toggle icon should remain centered")
_assert(main_screen.theme_button.get_index() < main_screen.account_button.get_index(), "the theme toggle should appear before the account dropdown")
_assert(main_screen.account_chevron_button.get_index() == main_screen.account_button.get_index() + 1, "the account avatar and chevron should remain adjacent")
_assert(main_screen.header_panel.find_child("HeaderStats", true, false) == null, "the application header should not display rating or role labels")
var pause_menu_has_profile := false
var pause_menu_has_settings := false
for menu_button in main_screen.app_menu.find_children("*", "Button", true, false):
if menu_button.text == "Profile":
pause_menu_has_profile = true
if menu_button.text == "Settings":
pause_menu_has_settings = true
_assert(pause_menu_has_profile and pause_menu_has_settings, "the paused-run menu should offer Profile and Settings navigation")
_assert(main_screen.session_expiry_dialog != null, "the application shell should construct a global session-expiry prompt")
main_screen.session_expiry_dialog._show_warning(120)
_assert(main_screen.session_expiry_dialog.visible, "the session-expiry prompt should cover every authenticated screen")
_assert(main_screen.session_expiry_dialog.message_label.text.contains("02:00"), "the session-expiry prompt should display a live countdown")
main_screen.session_expiry_dialog._refresh_started()
_assert(main_screen.session_expiry_dialog.stay_button.disabled, "the renewal action should lock while a refresh request is active")
main_screen.session_expiry_dialog._refresh_failed("Connection interrupted.", false)
_assert(not main_screen.session_expiry_dialog.stay_button.disabled, "a transient renewal failure should permit retry before expiry")
var cancel_event := InputEventAction.new()
cancel_event.action = "ui_cancel"
cancel_event.pressed = true
main_screen.session_expiry_dialog._unhandled_input(cancel_event)
_assert(main_screen.session_expiry_dialog.visible, "the session-expiry prompt should not be dismissible with Escape")
main_screen.session_expiry_dialog.dismiss_for_logout()
_assert(not main_screen.account_chevron_button.visible, "nonessential account controls should collapse on phone-width headers")
var question_bank_nav := main_screen.sidebar.find_child("NavQuestionBank", true, false) as Button
_assert(question_bank_nav != null and question_bank_nav.text == "Question Bank", "the Practice navigation item should be replaced by Question Bank")
_assert(main_screen.sidebar.find_child("NavPractice", true, false) == null, "the sidebar should not retain a Practice navigation item")
var profile_nav := main_screen.sidebar.find_child("NavPerformance", true, false) as Button
var settings_nav := main_screen.sidebar.find_child("NavSettings", true, false) as Button
var donation_nav := main_screen.sidebar.find_child("NavDonations", true, false) as Button
_assert(donation_nav != null and donation_nav.text == "Support RanaZe", "the shell should expose donation navigation")
_assert(profile_nav != null and settings_nav != null and profile_nav.get_index() + 1 == settings_nav.get_index(), "Profile should sit directly above Settings in the lower navigation group")
var theme_service = root.get_node("UIThemeService")
var prior_dark_mode: bool = theme_service.is_dark_mode()
theme_service.set_dark_mode(true)
await process_frame
theme_service.set_dark_mode(false)
await process_frame
var dashboard = main_screen.screens.get("dashboard")
var dashboard_card := dashboard.metric_grid.get_parent() as PanelContainer
var dashboard_box := dashboard_card.get_theme_stylebox("panel") as StyleBoxFlat
_assert(dashboard_box.bg_color.is_equal_approx(Color.WHITE), "dashboard cards should remain light after the shell refresh")
var simulations = main_screen.screens.get("simulation_history")
var simulation_box := simulations.active_section.get_theme_stylebox("panel") as StyleBoxFlat
_assert(simulation_box.bg_color.is_equal_approx(simulations._section_box(false).bg_color), "exam simulation panels should remain light after the shell refresh")
var notes_screen = main_screen.screens.get("notes")
var notes_box := notes_screen.notes_editor.editor.get_theme_stylebox("normal") as StyleBoxFlat
_assert(notes_box.bg_color.is_equal_approx(Color.WHITE), "the Notes editor should remain light after the shell refresh")
theme_service.set_dark_mode(prior_dark_mode)
await process_frame
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)