comic/odin/tests/gui_helpers_phase28.odin
2026-05-21 06:10:32 +02:00

696 lines
30 KiB
Odin

package tests
import "core:fmt"
import "core:os"
import "core:strings"
import "core:testing"
import "../src/adapters"
import "../src/core"
import "../src/gui"
import "../src/shared"
import "../src/ui"
@test
gui_parse_autosave_interval_bounds :: proc(t: ^testing.T) {
testing.expect(t, gui.parse_autosave_interval("42", 20) == 42, "valid interval should parse")
testing.expect(t, gui.parse_autosave_interval("", 20) == 20, "empty interval should fallback to default")
testing.expect(t, gui.parse_autosave_interval("1", 20) == 5, "interval should clamp to min")
testing.expect(t, gui.parse_autosave_interval("999", 20) == 300, "interval should clamp to max")
}
@test
gui_fit_text_for_width_truncates_with_ellipsis :: proc(t: ^testing.T) {
truncated := gui.fit_text_for_width("ABCDE", 1, 10)
testing.expect(t, truncated == "ABC…", "fit_text_for_width should enforce 4-char minimum width rule and truncate with ellipsis")
}
@test
gui_fit_text_for_width_passthrough_cases :: proc(t: ^testing.T) {
testing.expect(t, gui.fit_text_for_width("ABCD", 1, 10) == "ABCD", "text at min width should pass through")
testing.expect(t, gui.fit_text_for_width("ABCDE", 80, 0) == "ABCDE", "non-positive px_per_char should bypass truncation")
}
@test
gui_set_autosave_interval_text_clamps_and_formats :: proc(t: ^testing.T) {
interval_text := ""
msg := gui.set_autosave_interval_text(&interval_text, 3)
defer delete(interval_text)
defer delete(msg)
testing.expect(t, interval_text == "5", "text should clamp to minimum interval")
testing.expect(t, strings.contains(msg, "5s"), "message should include clamped seconds")
}
@test
gui_log_view_toggle_helpers :: proc(t: ^testing.T) {
lines: i32 = 6
msg1 := gui.toggle_log_lines_with_message(&lines)
defer delete(msg1)
testing.expect(t, lines == 4, "log lines should toggle from 6 to 4")
testing.expect(t, msg1 == "Log lines: 4", "log lines message should match")
msg2 := gui.toggle_log_lines_with_message(&lines)
defer delete(msg2)
testing.expect(t, lines == 6, "log lines should toggle back to 6")
testing.expect(t, msg2 == "Log lines: 6", "log lines message should match")
oldest_first := false
msg3 := gui.toggle_log_order_with_message(&oldest_first)
defer delete(msg3)
testing.expect(t, oldest_first, "log order should toggle to oldest-first")
testing.expect(t, strings.contains(msg3, "oldest"), "log order message should mention oldest")
msg4 := gui.toggle_log_order_with_message(&oldest_first)
defer delete(msg4)
testing.expect(t, !oldest_first, "log order should toggle back to newest-first")
testing.expect(t, strings.contains(msg4, "newest"), "log order message should mention newest")
}
@test
gui_export_format_helper_updates_path_and_dirty :: proc(t: ^testing.T) {
export_format: core.Export_Format = .PDF
export_path := "./comic.pdf"
is_dirty := false
msg := gui.set_export_format_with_message(&export_format, &export_path, .CBZ, &is_dirty)
defer delete(export_path)
defer delete(msg)
testing.expect(t, export_format == .CBZ, "format should update to CBZ")
testing.expect(t, is_dirty, "format switch should mark state dirty")
testing.expect(t, strings.has_suffix(export_path, ".cbz"), "export path should match selected format")
testing.expect(t, strings.has_prefix(msg, "Export format: CBZ"), "status should include format label")
}
@test
gui_diagnostics_context_builder_maps_fields :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
action_log: gui.Action_Log
ctx := gui.make_diagnostics_action_context(&controller, &action_log, true, false, true, false, 30, "./p.comic.json", "./e.cbz", 4, true)
testing.expect(t, ctx.controller == &controller, "context should hold controller pointer")
testing.expect(t, ctx.action_log == &action_log, "context should hold action-log pointer")
testing.expect(t, ctx.is_dirty, "context should map dirty flag")
testing.expect(t, !ctx.autosave_enabled, "context should map autosave flag")
testing.expect(t, ctx.project_ok && !ctx.export_ok, "context should map path health flags")
testing.expect(t, ctx.autosave_secs == 30, "context should map autosave seconds")
testing.expect(t, ctx.project_path == "./p.comic.json" && ctx.export_path == "./e.cbz", "context should map paths")
testing.expect(t, ctx.log_show_lines == 4 && ctx.log_oldest_first, "context should map log settings")
}
@test
gui_project_path_normalization_variants :: proc(t: ^testing.T) {
p1 := gui.normalize_project_path("")
testing.expect(t, p1 == "./gui_project.comic.json", "empty path should use default project path")
p2 := gui.normalize_project_path("my_story")
defer delete(p2)
testing.expect(t, p2 == "my_story.comic.json", "bare name should append .comic.json")
p3 := gui.normalize_project_path("my_story.json")
defer delete(p3)
testing.expect(t, p3 == "my_story.comic.json", "json path should be converted to .comic.json")
p4 := gui.normalize_project_path("already.comic.json")
testing.expect(t, p4 == "already.comic.json", "already-normalized path should remain unchanged")
}
@test
gui_fix_all_paths_normalizes_project_and_export :: proc(t: ^testing.T) {
project_path := "project"
export_path := "./out"
defer delete(project_path)
defer delete(export_path)
gui.fix_all_paths(&project_path, &export_path, .PNG)
testing.expect(t, strings.has_suffix(project_path, ".comic.json"), "fix_all_paths should normalize project suffix")
testing.expect(t, strings.has_suffix(export_path, ".zip"), "PNG export should normalize to .zip suffix")
}
@test
gui_summary_toggle_supported_screen_guard :: proc(t: ^testing.T) {
opts := gui.Summary_View_Options{}
msg1 := gui.toggle_summary_show_if_supported(.Story, &opts)
testing.expect(t, len(msg1) == 0, "show toggle should no-op on unsupported screen")
msg2 := gui.toggle_summary_sort_if_supported(.Story, &opts)
testing.expect(t, len(msg2) == 0, "sort toggle should no-op on unsupported screen")
msg3 := gui.toggle_summary_show_if_supported(.Script, &opts)
defer delete(msg3)
testing.expect(t, strings.has_prefix(msg3, "Script summary show-all"), "show toggle should produce script status message")
}
@test
gui_push_dirty_status_sets_dirty_and_logs :: proc(t: ^testing.T) {
is_dirty := false
status_msg := fmt.aprintf("")
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
defer delete(status_msg)
gui.push_dirty_status(&is_dirty, &status_msg, &action_log, "Changed value")
testing.expect(t, is_dirty, "push_dirty_status should set dirty flag")
testing.expect(t, status_msg == "Changed value", "push_dirty_status should set status message")
testing.expect(t, action_log.count == 1, "push_dirty_status should append one action-log entry")
}
@test
gui_push_status_if_nonempty_only_pushes_for_content :: proc(t: ^testing.T) {
status_msg := fmt.aprintf("initial")
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
defer delete(status_msg)
gui.push_status_if_nonempty(&status_msg, &action_log, "")
testing.expect(t, status_msg == "initial", "empty optional status should not overwrite status message")
testing.expect(t, action_log.count == 0, "empty optional status should not push to action log")
gui.push_status_if_nonempty(&status_msg, &action_log, "non-empty")
testing.expect(t, status_msg == "non-empty", "non-empty optional status should update status message")
testing.expect(t, action_log.count == 1, "non-empty optional status should append one action-log entry")
}
@test
gui_confirmation_request_sets_overlay_state :: proc(t: ^testing.T) {
show_confirm_overlay := false
show_help_overlay := true
pending_action: gui.Pending_Confirm_Action = .None
msg := gui.request_confirmation(&show_confirm_overlay, &show_help_overlay, &pending_action, .Open_Project, "Confirm open?")
testing.expect(t, msg == "Confirm open?", "request_confirmation should return prompt message")
testing.expect(t, show_confirm_overlay, "request_confirmation should enable confirm overlay")
testing.expect(t, !show_help_overlay, "request_confirmation should close help overlay")
testing.expect(t, pending_action == .Open_Project, "request_confirmation should set pending action")
}
@test
gui_autosave_tick_noop_when_disabled_or_clean :: proc(t: ^testing.T) {
state := core.new_initial_state()
project_path := "./tmp-test.comic.json"
is_dirty := true
last_autosave_at: f64 = 0
last_save_at: f64 = -1
msg1 := gui.autosave_tick_with_message(&project_path, state, false, &is_dirty, &last_autosave_at, &last_save_at, 1)
testing.expect(t, len(msg1) == 0, "autosave tick should no-op when autosave is disabled")
testing.expect(t, is_dirty, "autosave disabled should not mutate dirty flag")
is_dirty = false
msg2 := gui.autosave_tick_with_message(&project_path, state, true, &is_dirty, &last_autosave_at, &last_save_at, 1)
testing.expect(t, len(msg2) == 0, "autosave tick should no-op when state is clean")
}
@test
gui_log_clear_and_reset_helpers :: proc(t: ^testing.T) {
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
status_msg := fmt.aprintf("")
defer delete(status_msg)
gui.push_status(&status_msg, &action_log, "first")
gui.push_status(&status_msg, &action_log, "second")
testing.expect(t, action_log.count == 2, "setup should add log entries")
clear_msg := gui.clear_action_log_with_message(&action_log)
testing.expect(t, clear_msg == "Action log cleared", "clear helper should return expected message")
testing.expect(t, action_log.count == 0, "clear helper should reset action log count")
lines: i32 = 4
oldest_first := true
reset_msg := gui.reset_log_view_with_message(&lines, &oldest_first)
testing.expect(t, reset_msg == "Reset log view", "reset helper should return expected message")
testing.expect(t, lines == 6, "reset helper should restore default line count")
testing.expect(t, !oldest_first, "reset helper should restore newest-first ordering")
}
@test
gui_confirm_resolve_none_returns_message :: proc(t: ^testing.T) {
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
project_path := "./p.comic.json"
export_path := "./e.pdf"
is_dirty := false
last_autosave_at: f64 = 0
msg := gui.resolve_confirm_action_with_message(.None, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
testing.expect(t, msg == "No pending destructive action", "resolve helper should report none-pending message")
}
@test
gui_help_overlay_toggle_and_close_helpers :: proc(t: ^testing.T) {
show_help_overlay := false
gui.toggle_help_overlay(&show_help_overlay)
testing.expect(t, show_help_overlay, "toggle helper should enable help overlay")
gui.close_help_overlay_if_open(&show_help_overlay)
testing.expect(t, !show_help_overlay, "close helper should disable help overlay when open")
gui.close_help_overlay_if_open(&show_help_overlay)
testing.expect(t, !show_help_overlay, "close helper should be a no-op when already closed")
}
@test
gui_export_path_preset_and_derivation_helpers :: proc(t: ^testing.T) {
export_path := ""
msg1 := gui.set_export_preset_with_message(&export_path, .PNG)
defer delete(msg1)
testing.expect(t, strings.has_suffix(export_path, ".zip"), "preset helper should set PNG-compatible export path")
testing.expect(t, strings.has_prefix(msg1, "Preset export path:"), "preset helper should return status message")
delete(export_path)
project_path := "./work/my.comic.json"
msg2 := gui.set_export_path_from_project_with_message(&export_path, project_path, .CBZ)
defer delete(export_path)
defer delete(msg2)
testing.expect(t, strings.has_suffix(export_path, ".cbz"), "project-derivation helper should set CBZ-compatible export path")
testing.expect(t, strings.contains(msg2, "Export path from project dir:"), "project-derivation helper should return status message")
msg3 := gui.set_project_path_from_export_with_message(&project_path, export_path)
defer delete(project_path)
defer delete(msg3)
testing.expect(t, strings.has_suffix(project_path, "gui_project.comic.json"), "export-derivation helper should set project filename")
testing.expect(t, strings.contains(msg3, "Project path from export dir:"), "export-derivation helper should return status message")
}
@test
gui_reset_project_session_resets_dirty_and_screen :: proc(t: ^testing.T) {
state := core.new_initial_state()
state.story_idea = "changed"
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
controller.active_screen = .Export
is_dirty := true
last_autosave_at: f64 = 123
msg := gui.reset_project_session(&controller, &is_dirty, &last_autosave_at, false)
testing.expect(t, msg == "Reset project", "reset helper should return expected status")
testing.expect(t, !is_dirty, "reset helper should clear dirty flag")
testing.expect(t, controller.active_screen == .Story, "reset helper should sync active screen to initial workflow")
testing.expect(t, last_autosave_at == 123, "reset helper should not touch autosave timestamp when touch_time is false")
}
@test
gui_toggle_autosave_with_message_flips_state :: proc(t: ^testing.T) {
autosave_enabled := true
msg1 := gui.toggle_autosave_with_message(&autosave_enabled)
defer delete(msg1)
testing.expect(t, !autosave_enabled, "autosave toggle should flip from enabled to disabled")
testing.expect(t, strings.contains(msg1, "no"), "autosave toggle message should report disabled state")
msg2 := gui.toggle_autosave_with_message(&autosave_enabled)
defer delete(msg2)
testing.expect(t, autosave_enabled, "autosave toggle should flip back to enabled")
testing.expect(t, strings.contains(msg2, "yes"), "autosave toggle message should report enabled state")
}
@test
gui_reset_helper_fields_with_message_sets_defaults :: proc(t: ^testing.T) {
export_path := "./custom.cbz"
local_script_pages := "9"
autosave_interval_text := "90"
defer delete(export_path)
msg := gui.reset_helper_fields_with_message(&export_path, &local_script_pages, &autosave_interval_text, .PDF)
testing.expect(t, msg == "Reset helper fields to defaults", "reset helpers should return expected status")
testing.expect(t, export_path == "./comic.pdf", "reset helpers should restore format-based export default")
testing.expect(t, local_script_pages == "2", "reset helpers should restore default local script pages")
testing.expect(t, autosave_interval_text == "20", "reset helpers should restore default autosave interval text")
}
@test
gui_clear_selected_field_with_message_behaviors :: proc(t: ^testing.T) {
state := core.new_initial_state()
state.story_idea = "idea"
export_path := "./comic.pdf"
local_script_pages := "3"
project_path := "./p.comic.json"
autosave_interval_text := "30"
is_dirty := false
msg1 := gui.clear_selected_field_with_message(0, &state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty)
testing.expect(t, msg1 == "Cleared selected field", "clear helper should report clear when field had content")
testing.expect(t, len(state.story_idea) == 0, "clear helper should clear selected text field")
testing.expect(t, is_dirty, "clear helper should mark dirty when a field was cleared")
is_dirty = false
msg2 := gui.clear_selected_field_with_message(0, &state, &export_path, &local_script_pages, &project_path, &autosave_interval_text, &is_dirty)
testing.expect(t, msg2 == "Selected field already empty", "clear helper should report already-empty state")
testing.expect(t, !is_dirty, "clear helper should not mark dirty when nothing changed")
}
@test
gui_path_health_hint_variants :: proc(t: ^testing.T) {
testing.expect(t, gui.path_health_hint(true, true) == "", "path-health hint should be empty when both paths are healthy")
testing.expect(t, strings.contains(gui.path_health_hint(false, false), "Fix paths"), "path-health hint should mention fixing both paths")
testing.expect(t, strings.contains(gui.path_health_hint(false, true), "Fix project path"), "path-health hint should mention project path fix")
testing.expect(t, strings.contains(gui.path_health_hint(true, false), "Fix export path"), "path-health hint should mention export path fix")
}
@test
gui_path_health_predicates :: proc(t: ^testing.T) {
testing.expect(t, gui.project_path_is_normalized("./x.comic.json"), "project path predicate should accept normalized path")
testing.expect(t, !gui.project_path_is_normalized("./x.json"), "project path predicate should reject non-comic suffix")
testing.expect(t, gui.export_path_matches_format("./x.pdf", .PDF), "export path predicate should accept PDF suffix")
testing.expect(t, !gui.export_path_matches_format("./x.cbz", .PDF), "export path predicate should reject wrong format suffix")
}
@test
gui_screen_label_and_navigation_status :: proc(t: ^testing.T) {
testing.expect(t, gui.screen_status_label(.Panels) == "Panels", "screen label helper should map enum to name")
state := core.new_initial_state()
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
msg := gui.navigate_screen_with_status(&controller, .Story)
defer delete(msg)
testing.expect(t, msg == "Screen: Story", "navigate helper should return status message for successful navigation")
}
@test
gui_action_log_ring_buffer_retains_recent_entries :: proc(t: ^testing.T) {
status_msg := fmt.aprintf("")
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
defer delete(status_msg)
for i in 0..<10 {
msg := ""
switch i {
case 0: msg = "entry_00"
case 1: msg = "entry_01"
case 2: msg = "entry_02"
case 3: msg = "entry_03"
case 4: msg = "entry_04"
case 5: msg = "entry_05"
case 6: msg = "entry_06"
case 7: msg = "entry_07"
case 8: msg = "entry_08"
case 9: msg = "entry_09"
}
gui.push_status(&status_msg, &action_log, msg)
}
testing.expect(t, action_log.count == 10, "action log should track total pushes")
snapshot := gui.build_action_log_snapshot(action_log)
defer delete(snapshot)
testing.expect(t, strings.contains(snapshot, "entry_09"), "snapshot should include newest entry")
testing.expect(t, !strings.contains(snapshot, "entry_00"), "snapshot should drop entries outside ring capacity")
}
@test
gui_action_log_clear_resets_count_and_snapshot :: proc(t: ^testing.T) {
status_msg := fmt.aprintf("")
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
defer delete(status_msg)
gui.push_status(&status_msg, &action_log, "a")
gui.push_status(&status_msg, &action_log, "b")
testing.expect(t, action_log.count == 2, "setup should create action-log entries")
action_log.last_push_at = 42
_ = gui.clear_action_log_with_message(&action_log)
testing.expect(t, action_log.count == 0, "clear helper should reset count")
testing.expect(t, action_log.last_push_at == 0, "clear helper should reset last-push timestamp")
snapshot := gui.build_action_log_snapshot(action_log)
defer delete(snapshot)
testing.expect(t, snapshot == "(action log empty)", "snapshot should report empty log after clear")
}
@test
gui_set_status_replaces_owned_string :: proc(t: ^testing.T) {
status_msg := fmt.aprintf("start")
defer delete(status_msg)
gui.set_status(&status_msg, "updated")
testing.expect(t, status_msg == "updated", "set_status should replace existing status text")
}
@test
gui_open_project_session_missing_file_returns_error_and_keeps_state :: proc(t: ^testing.T) {
state := core.new_initial_state()
state.story_idea = "original"
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
project_path := "./definitely_missing_project"
export_path := "./comic.pdf"
is_dirty := true
last_autosave_at: f64 = 777
msg := gui.open_project_session(&controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
defer delete(project_path)
testing.expect(t, strings.contains(msg, "project file does not exist"), "open helper should surface missing-file error")
testing.expect(t, strings.has_suffix(project_path, ".comic.json"), "open helper should normalize missing-file project path")
testing.expect(t, controller.state.story_idea == "original", "failed open should keep existing controller state")
testing.expect(t, is_dirty, "failed open should preserve dirty flag")
testing.expect(t, last_autosave_at == 777, "failed open should preserve autosave timestamp")
}
@test
gui_resolve_confirm_action_reset_branch :: proc(t: ^testing.T) {
state := core.new_initial_state()
state.story_idea = "before"
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
project_path := "./unused.comic.json"
export_path := "./unused.pdf"
is_dirty := true
last_autosave_at: f64 = 42
msg := gui.resolve_confirm_action_with_message(.Reset_Project, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
testing.expect(t, msg == "Reset project", "resolve helper should dispatch reset action")
testing.expect(t, !is_dirty, "reset dispatch should clear dirty flag")
testing.expect(t, controller.active_screen == .Story, "reset dispatch should sync active screen")
testing.expect(t, last_autosave_at >= 0, "reset dispatch should set a non-negative autosave timestamp")
testing.expect(t, last_autosave_at != 42, "reset dispatch should refresh autosave timestamp")
}
@test
gui_open_project_session_success_updates_state_and_paths :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-gui-open-*", context.temp_allocator)
testing.expect(t, terr == nil, "temp dir should be created")
if terr != nil {
return
}
defer os.remove_all(tmp_dir)
project_path := fmt.aprintf("%s/session.comic.json", tmp_dir)
defer delete(project_path)
saved := core.new_initial_state()
saved.story_idea = "loaded-story"
err := adapters.save_project(project_path, saved)
testing.expect(t, shared.is_ok(err), "save_project should succeed for open-session test")
if !shared.is_ok(err) {
return
}
controller := ui.new_controller(core.new_initial_state())
defer ui.dispose_controller_owned(&controller)
controller.state.story_idea = "original-story"
export_path := "./placeholder.pdf"
is_dirty := true
last_autosave_at: f64 = 17
msg := gui.open_project_session(&controller, &project_path, &export_path, .CBZ, &is_dirty, &last_autosave_at)
defer delete(msg)
defer delete(export_path)
testing.expect(t, strings.has_prefix(msg, "Opened project:"), "open helper should return opened-project status")
testing.expect(t, controller.state.story_idea == "loaded-story", "open helper should replace controller state from loaded project")
testing.expect(t, !is_dirty, "open helper should clear dirty flag on success")
testing.expect(t, strings.has_suffix(export_path, ".cbz"), "open helper should sync export path suffix to selected format")
testing.expect(t, strings.contains(export_path, tmp_dir), "open helper should sync export path into the project directory")
testing.expect(t, last_autosave_at != 17, "open helper should refresh autosave timestamp on success")
}
@test
gui_resolve_confirm_action_open_branch_success :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-gui-confirm-open-*", context.temp_allocator)
testing.expect(t, terr == nil, "temp dir should be created")
if terr != nil {
return
}
defer os.remove_all(tmp_dir)
project_path := fmt.aprintf("%s/session.comic.json", tmp_dir)
defer delete(project_path)
saved := core.new_initial_state()
saved.story_idea = "confirm-open-loaded"
err := adapters.save_project(project_path, saved)
testing.expect(t, shared.is_ok(err), "save_project should succeed for resolve-open test")
if !shared.is_ok(err) {
return
}
controller := ui.new_controller(core.new_initial_state())
defer ui.dispose_controller_owned(&controller)
controller.state.story_idea = "before-open"
export_path := "./placeholder.pdf"
is_dirty := true
last_autosave_at: f64 = 99
msg := gui.resolve_confirm_action_with_message(.Open_Project, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
defer delete(msg)
defer delete(export_path)
testing.expect(t, strings.has_prefix(msg, "Opened project:"), "resolve helper should dispatch open action")
testing.expect(t, controller.state.story_idea == "confirm-open-loaded", "resolve open dispatch should load target state")
testing.expect(t, !is_dirty, "resolve open dispatch should clear dirty flag")
testing.expect(t, strings.has_suffix(export_path, ".pdf"), "resolve open dispatch should sync export path to PDF suffix")
testing.expect(t, strings.contains(export_path, tmp_dir), "resolve open dispatch should sync export path into project directory")
testing.expect(t, last_autosave_at != 99, "resolve open dispatch should refresh autosave timestamp")
}
@test
gui_resolve_confirm_action_open_branch_missing_file_preserves_state :: proc(t: ^testing.T) {
state := core.new_initial_state()
state.story_idea = "still-here"
controller := ui.new_controller(state)
defer ui.dispose_controller(&controller)
project_path := "./missing-confirm-open"
export_path := "./out.pdf"
is_dirty := true
last_autosave_at: f64 = 55
msg := gui.resolve_confirm_action_with_message(.Open_Project, &controller, &project_path, &export_path, .PDF, &is_dirty, &last_autosave_at)
defer delete(project_path)
testing.expect(t, strings.contains(msg, "project file does not exist"), "resolve open branch should surface missing-file error")
testing.expect(t, controller.state.story_idea == "still-here", "resolve open failure should preserve existing state")
testing.expect(t, is_dirty, "resolve open failure should preserve dirty flag")
testing.expect(t, last_autosave_at == 55, "resolve open failure should preserve autosave timestamp")
}
@test
gui_autosave_tick_success_writes_project_and_clears_dirty :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-gui-autosave-*", context.temp_allocator)
testing.expect(t, terr == nil, "temp dir should be created")
if terr != nil {
return
}
defer os.remove_all(tmp_dir)
project_path := fmt.aprintf("%s/autosave_target.comic.json", tmp_dir)
defer delete(project_path)
state := core.new_initial_state()
is_dirty := true
last_autosave_at: f64 = -1
last_save_at: f64 = -1
msg := gui.autosave_tick_with_message(&project_path, state, true, &is_dirty, &last_autosave_at, &last_save_at, 0)
defer delete(msg)
testing.expect(t, strings.has_prefix(msg, "Autosaved:"), "autosave tick should report success when project write works")
testing.expect(t, strings.has_suffix(project_path, ".comic.json"), "autosave tick should normalize project path suffix")
testing.expect(t, os.exists(project_path), "autosave tick should write project file")
testing.expect(t, !is_dirty, "autosave tick should clear dirty flag after successful save")
testing.expect(t, last_save_at >= 0, "autosave tick should set last-save timestamp on success")
}
@test
gui_write_diagnostics_with_message_creates_file :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-gui-diag-*", context.temp_allocator)
testing.expect(t, terr == nil, "temp dir should be created")
if terr != nil {
return
}
defer os.remove_all(tmp_dir)
project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
defer delete(project_path)
export_path := fmt.aprintf("%s/out.pdf", tmp_dir)
defer delete(export_path)
controller := ui.new_controller(core.new_initial_state())
defer ui.dispose_controller(&controller)
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
ctx := gui.make_diagnostics_action_context(&controller, &action_log, false, true, true, true, 20, project_path, export_path, 6, false)
msg := gui.write_diagnostics_with_message(ctx)
defer delete(msg)
testing.expect(t, strings.has_prefix(msg, "Wrote diagnostics file:"), "diagnostics write helper should report written file")
diag_path := fmt.aprintf("%s/gui_diagnostics.txt", tmp_dir)
defer delete(diag_path)
testing.expect(t, os.exists(diag_path), "diagnostics write helper should create diagnostics file")
}
@test
gui_write_session_report_with_message_creates_file :: proc(t: ^testing.T) {
tmp_dir, terr := os.make_directory_temp("", "comic-gui-report-*", context.temp_allocator)
testing.expect(t, terr == nil, "temp dir should be created")
if terr != nil {
return
}
defer os.remove_all(tmp_dir)
project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
defer delete(project_path)
export_path := fmt.aprintf("%s/out.cbz", tmp_dir)
defer delete(export_path)
controller := ui.new_controller(core.new_initial_state())
defer ui.dispose_controller(&controller)
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
status_msg := fmt.aprintf("")
defer delete(status_msg)
gui.push_status(&status_msg, &action_log, "seed log entry")
ctx := gui.make_diagnostics_action_context(&controller, &action_log, true, true, true, true, 30, project_path, export_path, 4, true)
msg := gui.write_session_report_with_message(ctx)
defer delete(msg)
testing.expect(t, strings.has_prefix(msg, "Wrote session report:"), "session report write helper should report written file")
report_path := fmt.aprintf("%s/gui_session_report.txt", tmp_dir)
defer delete(report_path)
testing.expect(t, os.exists(report_path), "session report helper should create report file")
}
@test
gui_write_diagnostics_with_message_failure_for_missing_dir :: proc(t: ^testing.T) {
controller := ui.new_controller(core.new_initial_state())
defer ui.dispose_controller(&controller)
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
project_path := "./missing-dir/sub/project.comic.json"
export_path := "./out.pdf"
ctx := gui.make_diagnostics_action_context(&controller, &action_log, false, true, true, true, 20, project_path, export_path, 6, false)
msg := gui.write_diagnostics_with_message(ctx)
defer delete(msg)
testing.expect(t, msg == "Failed writing diagnostics file", "diagnostics write helper should report failure for missing directory")
}
@test
gui_write_session_report_with_message_failure_for_missing_dir :: proc(t: ^testing.T) {
controller := ui.new_controller(core.new_initial_state())
defer ui.dispose_controller(&controller)
action_log: gui.Action_Log
defer gui.action_log_dispose(&action_log)
project_path := "./missing-dir/sub/project.comic.json"
export_path := "./out.pdf"
ctx := gui.make_diagnostics_action_context(&controller, &action_log, false, true, true, true, 20, project_path, export_path, 6, false)
msg := gui.write_session_report_with_message(ctx)
defer delete(msg)
testing.expect(t, msg == "Failed writing session report", "session report write helper should report failure for missing directory")
}