From 3b946c2bc556b20d7ac4f20db1a1d67e82c9e2c5 Mon Sep 17 00:00:00 2001 From: hz Date: Tue, 14 Jul 2026 21:38:03 -0500 Subject: [PATCH] Move simulation history into profile --- api/QuestionsApi.Tests/ExamSimulationTests.cs | 22 +++ .../Controllers/ExamSimulationsController.cs | 13 ++ api/QuestionsApi/Models/ExamSimulation.cs | 22 +++ .../Services/ExamSimulationStore.cs | 33 ++++ scripts/autoload/exam_simulation_service.gd | 16 +- scripts/ui/main.gd | 6 + scripts/ui/progress_profile.gd | 6 + scripts/ui/simulation_history.gd | 155 ++++++++++++++++++ scripts/ui/simulation_history.gd.uid | 1 + scripts/ui/simulation_results.gd | 30 +++- scripts/ui/simulation_review.gd | 7 +- scripts/ui/study_selector.gd | 49 ------ tests/simulation_history_test.gd | 19 +++ tests/simulation_history_test.gd.uid | 1 + 14 files changed, 312 insertions(+), 68 deletions(-) create mode 100644 scripts/ui/simulation_history.gd create mode 100644 scripts/ui/simulation_history.gd.uid create mode 100644 tests/simulation_history_test.gd create mode 100644 tests/simulation_history_test.gd.uid diff --git a/api/QuestionsApi.Tests/ExamSimulationTests.cs b/api/QuestionsApi.Tests/ExamSimulationTests.cs index 0385b60..1b71a17 100644 --- a/api/QuestionsApi.Tests/ExamSimulationTests.cs +++ b/api/QuestionsApi.Tests/ExamSimulationTests.cs @@ -71,6 +71,28 @@ public sealed class ExamSimulationTests Assert.Equal(expected, ExamSimulationStore.BlueprintCategory(new Question { System = system })); } + [Fact] + public void HistorySummarySupportsScoredAndUnscoredStates() + { + var active = ExamSimulationStore.ToHistorySummary(new ExamSimulation + { + State = ExamSimulationStates.BlockActive, + CurrentBlock = 3, + Items = BuildItems(20) + }); + var completed = ExamSimulationStore.ToHistorySummary(new ExamSimulation + { + State = ExamSimulationStates.Completed, + Items = BuildItems(20), + Score = new ExamSimulationScore { PredictedScore = 221, RangeLow = 204, RangeHigh = 238, Correct = 15, Total = 20 } + }); + + Assert.Null(active.PredictedScore); + Assert.Equal(3, active.CurrentBlock); + Assert.Equal(221, completed.PredictedScore); + Assert.Equal(15, completed.Correct); + } + private static List BuildItems(int count, bool wasSeen = false) => Enumerable.Range(0, count).Select(index => new ExamSimulationItem { diff --git a/api/QuestionsApi/Controllers/ExamSimulationsController.cs b/api/QuestionsApi/Controllers/ExamSimulationsController.cs index b4a75cb..6664c70 100644 --- a/api/QuestionsApi/Controllers/ExamSimulationsController.cs +++ b/api/QuestionsApi/Controllers/ExamSimulationsController.cs @@ -53,6 +53,19 @@ public class ExamSimulationsController(ExamSimulationStore simulations) : Contro return Ok(await simulations.GetCompletedAsync(UserId(), limit, cursor)); } + [HttpGet("history")] + public async Task History([FromQuery] int limit = 20, [FromQuery] string? before = null) + { + DateTime? cursor = null; + if (!string.IsNullOrWhiteSpace(before)) + { + if (!DateTime.TryParse(before, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed)) + return BadRequest("The simulation-history cursor is invalid."); + cursor = parsed; + } + return Ok(await simulations.GetHistoryAsync(UserId(), limit, cursor)); + } + [HttpGet("{id}")] public async Task Get(string id) { diff --git a/api/QuestionsApi/Models/ExamSimulation.cs b/api/QuestionsApi/Models/ExamSimulation.cs index a919783..62d02d6 100644 --- a/api/QuestionsApi/Models/ExamSimulation.cs +++ b/api/QuestionsApi/Models/ExamSimulation.cs @@ -137,6 +137,28 @@ public class CompletedExamSimulationPage public DateTime? NextBefore { get; set; } } +public class ExamSimulationHistorySummary +{ + public string Id { get; set; } = ""; + public string State { get; set; } = ""; + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } + public DateTime? CompletedUtc { get; set; } + public int CurrentBlock { get; set; } + public int TotalBlocks { get; set; } = 12; + public int? PredictedScore { get; set; } + public int? RangeLow { get; set; } + public int? RangeHigh { get; set; } + public int? Correct { get; set; } + public int Total { get; set; } +} + +public class ExamSimulationHistoryPage +{ + public List Items { get; set; } = []; + public DateTime? NextBefore { get; set; } +} + public class Step3Outcome { [BsonId] diff --git a/api/QuestionsApi/Services/ExamSimulationStore.cs b/api/QuestionsApi/Services/ExamSimulationStore.cs index 1b21664..4dd6f68 100644 --- a/api/QuestionsApi/Services/ExamSimulationStore.cs +++ b/api/QuestionsApi/Services/ExamSimulationStore.cs @@ -35,6 +35,8 @@ public class ExamSimulationStore Builders.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State))); _simulations.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State).Descending(item => item.CompletedUtc))); + _simulations.Indexes.CreateOne(new CreateIndexModel( + Builders.IndexKeys.Ascending(item => item.UserId).Descending(item => item.CreatedUtc))); _simulations.Indexes.CreateOne(new CreateIndexModel( Builders.IndexKeys.Ascending(item => item.UpdatedUtc))); _outcomes.Indexes.CreateOne(new CreateIndexModel( @@ -183,6 +185,37 @@ public class ExamSimulationStore }; } + public async Task GetHistoryAsync(string userId, int limit, DateTime? before) + { + limit = Math.Clamp(limit, 1, 50); + var filter = Builders.Filter.Eq(item => item.UserId, userId); + if (before is not null) + filter &= Builders.Filter.Lt(item => item.CreatedUtc, before.Value); + var simulations = await _simulations.Find(filter).SortByDescending(item => item.CreatedUtc).Limit(limit + 1).ToListAsync(); + var hasMore = simulations.Count > limit; + if (hasMore) simulations.RemoveAt(simulations.Count - 1); + return new ExamSimulationHistoryPage + { + Items = simulations.Select(ToHistorySummary).ToList(), + NextBefore = hasMore && simulations.Count > 0 ? simulations[^1].CreatedUtc : null + }; + } + + public static ExamSimulationHistorySummary ToHistorySummary(ExamSimulation item) => new() + { + Id = item.Id ?? "", + State = item.State, + CreatedUtc = item.CreatedUtc, + UpdatedUtc = item.UpdatedUtc, + CompletedUtc = item.CompletedUtc, + CurrentBlock = item.CurrentBlock, + PredictedScore = item.Score?.PredictedScore, + RangeLow = item.Score?.RangeLow, + RangeHigh = item.Score?.RangeHigh, + Correct = item.Score?.Correct, + Total = item.Score?.Total ?? item.Items.Count + }; + public async Task StartNextBlockAsync(ExamSimulation simulation) { if (simulation.State != ExamSimulationStates.BetweenBlocks) diff --git a/scripts/autoload/exam_simulation_service.gd b/scripts/autoload/exam_simulation_service.gd index d7c2e1c..568e42b 100644 --- a/scripts/autoload/exam_simulation_service.gd +++ b/scripts/autoload/exam_simulation_service.gd @@ -7,7 +7,7 @@ signal readiness_changed signal state_changed signal request_failed(message: String) signal outcome_changed -signal completed_history_changed +signal history_changed var api_url := DEFAULT_API_URL var outcome_api_url := "https://s3q.ranaze.com/api/step3-outcomes" @@ -15,8 +15,8 @@ var request: HTTPRequest var readiness := {} var simulation := {} var outcome := {} -var completed_history: Array = [] -var completed_history_next_before := "" +var history: Array = [] +var history_next_before := "" var is_loading := false var pending_action := "" var request_queue: Array = [] @@ -40,8 +40,8 @@ func load_active() -> void: _send("active", HTTPClient.METHOD_GET, "/active") -func load_completed_history(before: String = "") -> void: - var suffix := "/completed?limit=20" +func load_history(before: String = "") -> void: + var suffix := "/history?limit=20" if not before.is_empty(): suffix += "&before=%s" % before.uri_encode() _send("history_append" if not before.is_empty() else "history", HTTPClient.METHOD_GET, suffix) @@ -172,10 +172,10 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri return if action == "history" or action == "history_append": var items: Array = parsed.get("items", []) - completed_history = completed_history + items if action == "history_append" else items + history = history + items if action == "history_append" else items var next_before: Variant = parsed.get("nextBefore", null) - completed_history_next_before = "" if next_before == null else str(next_before) - completed_history_changed.emit() + history_next_before = "" if next_before == null else str(next_before) + history_changed.emit() return if action.begins_with("outcome_"): outcome = parsed diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index a2bcd54..9f362a1 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -173,6 +173,12 @@ func _build_ui() -> void: screens_container.add_child(simulation_review) screens["simulation_review"] = simulation_review + var simulation_history := preload("res://scripts/ui/simulation_history.gd").new() + simulation_history.name = "SimulationHistory" + simulation_history.main = self + screens_container.add_child(simulation_history) + screens["simulation_history"] = simulation_history + var profile := preload("res://scripts/ui/progress_profile.gd").new() profile.name = "ProgressProfile" profile.main = self diff --git a/scripts/ui/progress_profile.gd b/scripts/ui/progress_profile.gd index 3010e16..5c0159e 100644 --- a/scripts/ui/progress_profile.gd +++ b/scripts/ui/progress_profile.gd @@ -41,6 +41,12 @@ func _ready() -> void: ) row.add_child(reset) + var simulations := Button.new() + simulations.text = "Simulation History" + Style.style_chip(simulations, Style.CYAN) + simulations.pressed.connect(func(): main.show_screen("simulation_history")) + row.add_child(simulations) + reset_qbank_button = Button.new() reset_qbank_button.text = "Reset QBank" Style.style_chip(reset_qbank_button, Style.AMBER) diff --git a/scripts/ui/simulation_history.gd b/scripts/ui/simulation_history.gd new file mode 100644 index 0000000..52242e9 --- /dev/null +++ b/scripts/ui/simulation_history.gd @@ -0,0 +1,155 @@ +extends VBoxContainer + +const Style := preload("res://scripts/ui/ui_style.gd") + +var main: Control +var list: VBoxContainer +var status_label: Label +var load_more_button: Button +var pending_simulation_id := "" + + +func _ready() -> void: + set_anchors_preset(Control.PRESET_FULL_RECT) + add_theme_constant_override("separation", 12) + if not ExamSimulationService.history_changed.is_connected(_render_history): + ExamSimulationService.history_changed.connect(_render_history) + if not ExamSimulationService.state_changed.is_connected(_on_simulation_loaded): + ExamSimulationService.state_changed.connect(_on_simulation_loaded) + if not ExamSimulationService.request_failed.is_connected(_on_request_failed): + ExamSimulationService.request_failed.connect(_on_request_failed) + var top := HBoxContainer.new() + add_child(top) + var back := Button.new() + back.text = "< Profile" + Style.style_chip(back, Style.CYAN) + back.pressed.connect(func(): main.show_screen("profile")) + top.add_child(back) + var title := Label.new() + title.text = "Simulation History" + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + Style.set_label(title, 30, Style.TEXT) + top.add_child(title) + var balance := Control.new() + balance.custom_minimum_size = Vector2(100, 0) + top.add_child(balance) + status_label = Label.new() + status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + Style.set_label(status_label, 16, Style.MUTED) + add_child(status_label) + var scroll := ScrollContainer.new() + scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + add_child(scroll) + list = VBoxContainer.new() + list.size_flags_horizontal = Control.SIZE_EXPAND_FILL + list.add_theme_constant_override("separation", 8) + scroll.add_child(list) + load_more_button = Button.new() + load_more_button.text = "Load More" + load_more_button.visible = false + load_more_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + Style.style_chip(load_more_button, Style.CYAN) + load_more_button.pressed.connect(_load_more) + add_child(load_more_button) + + +func on_show(payload: Variant = null) -> void: + pending_simulation_id = "" + if str(payload) == "preserve" and not ExamSimulationService.history.is_empty(): + return + status_label.text = "Loading simulation history..." + load_more_button.disabled = true + ExamSimulationService.load_history() + + +func _render_history() -> void: + if not visible: + return + for child in list.get_children(): + child.queue_free() + if ExamSimulationService.history.is_empty(): + status_label.text = "No simulations yet." + else: + status_label.text = "Select a completed simulation to review it, or resume an active simulation." + for raw_summary in ExamSimulationService.history: + var summary: Dictionary = raw_summary + var state := str(summary.get("state", "")) + var action := _action_for_state(state) + var button := Button.new() + button.text = _summary_text(summary) + button.alignment = HORIZONTAL_ALIGNMENT_LEFT + button.custom_minimum_size = Vector2(0, 70) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.disabled = action == "none" + Style.style_chip(button, _state_color(state)) + if action != "none": + button.pressed.connect(_open_simulation.bind(str(summary.get("id", "")))) + list.add_child(button) + load_more_button.visible = not ExamSimulationService.history_next_before.is_empty() + load_more_button.disabled = false + + +func _summary_text(summary: Dictionary) -> String: + var state := str(summary.get("state", "")) + var date := str(summary.get("createdUtc", "")).left(10) + if state == "completed": + return "%s | Completed\nPredicted %d (%d-%d) | %d/%d correct" % [date, int(summary.get("predictedScore", 0)), int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)), int(summary.get("correct", 0)), int(summary.get("total", 0))] + if state == "block_active": + return "%s | Active\nResume Block %d of %d (timer continues)" % [date, int(summary.get("currentBlock", 0)) + 1, int(summary.get("totalBlocks", 12))] + if state == "between_blocks": + return "%s | In progress\n%d of %d blocks complete" % [date, int(summary.get("currentBlock", -1)) + 1, int(summary.get("totalBlocks", 12))] + return "%s | Abandoned\nThis simulation cannot be resumed." % date + + +func _open_simulation(simulation_id: String) -> void: + pending_simulation_id = simulation_id + status_label.text = "Opening simulation..." + ExamSimulationService.load_simulation(simulation_id) + + +func _on_simulation_loaded() -> void: + if not visible or pending_simulation_id.is_empty() or ExamSimulationService.simulation.is_empty(): + return + pending_simulation_id = "" + var state := str(ExamSimulationService.simulation.get("state", "")) + if state == "completed": + main.show_screen("simulation_results", {"simulation": ExamSimulationService.simulation, "return_screen": "simulation_history"}) + elif state == "block_active": + BlockSessionService.start_simulation_block( + ExamSimulationService.get_current_questions(), + int(ExamSimulationService.simulation.get("currentBlock", 0)), + int(ExamSimulationService.simulation.get("totalBlocks", 12)), + ExamSimulationService.get_block_deadline_unix()) + BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", [])) + main.show_screen("player") + elif state == "between_blocks": + main.show_screen("simulation_break") + else: + status_label.text = "This simulation can no longer be resumed." + + +func _load_more() -> void: + load_more_button.disabled = true + ExamSimulationService.load_history(ExamSimulationService.history_next_before) + + +func _on_request_failed(message: String) -> void: + if not visible: + return + pending_simulation_id = "" + status_label.text = message + load_more_button.disabled = false + + +static func _action_for_state(state: String) -> String: + if state == "completed": return "results" + if state == "block_active" or state == "between_blocks": return "resume" + return "none" + + +static func _state_color(state: String) -> Color: + if state == "completed": return Style.LIME + if state == "block_active" or state == "between_blocks": return Style.CYAN + return Style.MUTED diff --git a/scripts/ui/simulation_history.gd.uid b/scripts/ui/simulation_history.gd.uid new file mode 100644 index 0000000..8d4946d --- /dev/null +++ b/scripts/ui/simulation_history.gd.uid @@ -0,0 +1 @@ +uid://dg3a6hdg4w2pd diff --git a/scripts/ui/simulation_results.gd b/scripts/ui/simulation_results.gd index f0418da..3f26311 100644 --- a/scripts/ui/simulation_results.gd +++ b/scripts/ui/simulation_results.gd @@ -12,6 +12,8 @@ var outcome_consent: CheckBox var outcome_status: Label var outcome_delete_button: Button var current_simulation := {} +var return_screen := "selection" +var done_button: Button func _ready() -> void: @@ -47,7 +49,7 @@ func _ready() -> void: review_button.custom_minimum_size = Vector2(240, 50) review_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN Style.style_primary_button(review_button) - review_button.pressed.connect(func(): main.show_screen("simulation_review", current_simulation)) + review_button.pressed.connect(func(): main.show_screen("simulation_review", {"simulation": current_simulation, "return_screen": return_screen})) content.add_child(review_button) var outcome_heading := Label.new() @@ -91,17 +93,20 @@ func _ready() -> void: if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome): ExamSimulationService.outcome_changed.connect(_refresh_outcome) - var done := Button.new() - done.text = "Return to study modes" - done.custom_minimum_size = Vector2(240, 50) - done.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN - Style.style_primary_button(done) - done.pressed.connect(func(): main.show_screen("selection", "step3")) - add_child(done) + done_button = Button.new() + done_button.text = "Return to study modes" + done_button.custom_minimum_size = Vector2(240, 50) + done_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + Style.style_primary_button(done_button) + done_button.pressed.connect(_return_from_results) + add_child(done_button) func on_show(payload: Variant = null) -> void: - var simulation: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else ExamSimulationService.simulation + var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {} + var simulation: Dictionary = wrapper.get("simulation", wrapper) if not wrapper.is_empty() else ExamSimulationService.simulation + return_screen = str(wrapper.get("return_screen", "selection")) + done_button.text = "Return to simulation history" if return_screen == "simulation_history" else "Return to study modes" current_simulation = simulation var score: Dictionary = simulation.get("score", {}) var predicted := int(score.get("predictedScore", 0)) @@ -121,6 +126,13 @@ func on_show(payload: Variant = null) -> void: ExamSimulationService.load_outcome() +func _return_from_results() -> void: + if return_screen == "simulation_history": + main.show_screen("simulation_history", "preserve") + else: + main.show_screen("selection", "step3") + + func _clear(container: Control) -> void: for child in container.get_children(): child.queue_free() diff --git a/scripts/ui/simulation_review.gd b/scripts/ui/simulation_review.gd index a40e029..6636389 100644 --- a/scripts/ui/simulation_review.gd +++ b/scripts/ui/simulation_review.gd @@ -18,6 +18,7 @@ var empty_label: Label var media_requests: Array[HTTPRequest] = [] var image_overlay: Control var image_texture: TextureRect +var return_screen := "selection" func _ready() -> void: @@ -29,7 +30,7 @@ func _ready() -> void: var back := Button.new() back.text = "< Results" Style.style_chip(back, Style.CYAN) - back.pressed.connect(func(): main.show_screen("simulation_results", simulation)) + back.pressed.connect(func(): main.show_screen("simulation_results", {"simulation": simulation, "return_screen": return_screen})) toolbar.add_child(back) progress_label = Label.new() progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -90,7 +91,9 @@ func _ready() -> void: func on_show(payload: Variant = null) -> void: if typeof(payload) == TYPE_DICTIONARY: - simulation = payload + var wrapper: Dictionary = payload + simulation = wrapper.get("simulation", wrapper) + return_screen = str(wrapper.get("return_screen", "selection")) questions = simulation.get("reviewQuestions", []) responses.clear() for response in simulation.get("responses", []): diff --git a/scripts/ui/study_selector.gd b/scripts/ui/study_selector.gd index b2d29a9..6068297 100644 --- a/scripts/ui/study_selector.gd +++ b/scripts/ui/study_selector.gd @@ -14,8 +14,6 @@ var retry_button: Button var simulation_button: Button var simulation_status_label: Label var simulation_preview_link: LinkButton -var completed_history_list: VBoxContainer -var completed_history_more: Button var pending_simulation_launch := false var current_stage := "exam" var pending_question_bank_launch := false @@ -37,8 +35,6 @@ func _ready() -> void: ExamSimulationService.request_failed.connect(_on_simulation_failed) if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility): AuthState.auth_changed.connect(_refresh_simulation_preview_visibility) - if not ExamSimulationService.completed_history_changed.is_connected(_refresh_completed_history): - ExamSimulationService.completed_history_changed.connect(_refresh_completed_history) _build_exam_view() _build_mode_view() @@ -53,7 +49,6 @@ func on_show(payload: Variant = null) -> void: _refresh_bank_state() _refresh_simulation_preview_visibility() ExamSimulationService.load_readiness() - ExamSimulationService.load_completed_history() func on_hide() -> void: @@ -179,26 +174,6 @@ func _build_mode_view() -> void: mode_view.add_child(simulation_preview_link) _refresh_simulation_preview_visibility() - var history_heading := Label.new() - history_heading.text = "Completed simulations" - Style.set_label(history_heading, 20, Style.TEXT) - mode_view.add_child(history_heading) - completed_history_list = VBoxContainer.new() - completed_history_list.add_theme_constant_override("separation", 6) - completed_history_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL - var history_scroll := ScrollContainer.new() - history_scroll.custom_minimum_size = Vector2(0, 150) - history_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL - history_scroll.add_child(completed_history_list) - mode_view.add_child(history_scroll) - completed_history_more = Button.new() - completed_history_more.text = "Load more" - completed_history_more.visible = false - completed_history_more.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN - Style.style_chip(completed_history_more, Style.CYAN) - completed_history_more.pressed.connect(func(): ExamSimulationService.load_completed_history(ExamSimulationService.completed_history_next_before)) - mode_view.add_child(completed_history_more) - bank_status_label = Label.new() bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(bank_status_label, 16, Style.CYAN) @@ -294,30 +269,6 @@ func _refresh_simulation_preview_visibility() -> void: simulation_preview_link.visible = false -func _refresh_completed_history() -> void: - if completed_history_list == null: - return - for child in completed_history_list.get_children(): - child.queue_free() - for raw_summary in ExamSimulationService.completed_history: - var summary: Dictionary = raw_summary - var button := Button.new() - button.text = "%s | Predicted %d (%d-%d) | %d/%d correct" % [ - str(summary.get("completedUtc", "")).left(10), int(summary.get("predictedScore", 0)), - int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)), - int(summary.get("correct", 0)), int(summary.get("total", 0))] - button.alignment = HORIZONTAL_ALIGNMENT_LEFT - Style.style_chip(button, Style.CYAN) - button.pressed.connect(_open_completed_simulation.bind(str(summary.get("id", "")))) - completed_history_list.add_child(button) - completed_history_more.visible = not ExamSimulationService.completed_history_next_before.is_empty() - - -func _open_completed_simulation(simulation_id: String) -> void: - pending_simulation_launch = true - ExamSimulationService.load_simulation(simulation_id) - - func _on_simulation_state_changed() -> void: _refresh_simulation_state() if not pending_simulation_launch or ExamSimulationService.simulation.is_empty(): diff --git a/tests/simulation_history_test.gd b/tests/simulation_history_test.gd new file mode 100644 index 0000000..474e5ce --- /dev/null +++ b/tests/simulation_history_test.gd @@ -0,0 +1,19 @@ +extends SceneTree + +var failures := 0 + + +func _initialize() -> void: + var history = load("res://scripts/ui/simulation_history.gd") + _assert(history._action_for_state("completed") == "results", "completed simulations should open results") + _assert(history._action_for_state("block_active") == "resume", "active blocks should resume") + _assert(history._action_for_state("between_blocks") == "resume", "between-block simulations should resume") + _assert(history._action_for_state("abandoned") == "none", "abandoned simulations should be read-only") + quit(1 if failures > 0 else 0) + + +func _assert(condition: bool, message: String) -> void: + if condition: + return + failures += 1 + push_error(message) diff --git a/tests/simulation_history_test.gd.uid b/tests/simulation_history_test.gd.uid new file mode 100644 index 0000000..a60975e --- /dev/null +++ b/tests/simulation_history_test.gd.uid @@ -0,0 +1 @@ +uid://babu2x7xudmxc