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") }