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" // ───────────────────────────────────────────────────────────── // Milestone 34E: Layout constant validation at multiple resolutions // ───────────────────────────────────────────────────────────── @test layout_constants_match_hardcoded_values :: proc(t: ^testing.T) { testing.expect(t, shared.LAYOUT.sidebar_width == 260, "sidebar_width should match 260") testing.expect(t, shared.LAYOUT.right_margin == 16, "right_margin should match 16") testing.expect(t, shared.LAYOUT.min_main_width == 960, "min_main_width should match historical 960") testing.expect(t, shared.LAYOUT.top_reserved_height == 252, "top_reserved_height should match historical 252") testing.expect(t, shared.LAYOUT.min_lower_y == 450, "min_lower_y should match historical 450") testing.expect(t, shared.LAYOUT.compact_height == 720, "compact_height should match 720") } @test compute_main_width_formula :: proc(t: ^testing.T) { w := shared.compute_main_width(1920) testing.expect(t, w == 1644, fmt.tprintf("1920px screen: expected 1644, got %d", w)) w2 := shared.compute_main_width(1366) testing.expect(t, w2 == 1090, fmt.tprintf("1366px screen: expected 1090, got %d", w2)) w3 := shared.compute_main_width(2560) testing.expect(t, w3 == 2284, fmt.tprintf("2560px screen: expected 2284, got %d", w3)) w4 := shared.compute_main_width(1200) testing.expect(t, w4 == 960, fmt.tprintf("narrow screen: expected 960 floor, got %d", w4)) } @test compute_lower_y_formula :: proc(t: ^testing.T) { // 1920x1080 standard y := shared.compute_lower_y(1080) testing.expect(t, y == 828, fmt.tprintf("1080px height: expected 828, got %d", y)) // 1366x768 compact y2 := shared.compute_lower_y(768) testing.expect(t, y2 == 516, fmt.tprintf("768px height: expected 516, got %d", y2)) // Minimum floor y3 := shared.compute_lower_y(600) testing.expect(t, y3 == 450, fmt.tprintf("short screen: expected 450 floor, got %d", y3)) } @test screen_profile_classification :: proc(t: ^testing.T) { p1 := shared.breakpoint(1366, 768) testing.expect(t, p1 == .Standard, "1366x768 should be Standard") p2 := shared.breakpoint(1440, 900) testing.expect(t, p2 == .Standard, "1440x900 should be Standard") p3 := shared.breakpoint(1920, 1080) testing.expect(t, p3 == .Wide, "1920x1080 should be Wide") p4 := shared.breakpoint(2560, 1440) testing.expect(t, p4 == .Wide, "2560x1440 should be Wide") p5 := shared.breakpoint(1280, 600) testing.expect(t, p5 == .Compact, "1280x600 should be Compact") } @test is_compact_helper :: proc(t: ^testing.T) { testing.expect(t, shared.is_compact(600), "600px should be compact") testing.expect(t, shared.is_compact(719), "719px should be compact") testing.expect(t, !shared.is_compact(720), "720px should not be compact") testing.expect(t, !shared.is_compact(1080), "1080px should not be compact") } // ───────────────────────────────────────────────────────────── // Milestone 39A: GUI integration smoke tests for full local flow // ───────────────────────────────────────────────────────────── @test gui_bubble_type_name_roundtrip :: proc(t: ^testing.T) { types := []core.Bubble_Type{.Normal, .Thought, .Shout, .Whisper, .Narration, .Sound_Effect} names := []string{"Normal", "Thought", "Shout", "Whisper", "Narration", "SFX"} for name, i in names { nt := gui.bubble_type_from_name(name) testing.expect(t, nt == types[i], fmt.tprintf("bubble_type_from_name(%q) should map to correct type", name)) testing.expect(t, gui.bubble_type_name(types[i]) == name, fmt.tprintf("bubble_type_name should return %q", name)) } } @test gui_clamp_bubble_cursor :: proc(t: ^testing.T) { testing.expect(t, gui.clamp_bubble_cursor(0, 5) == 0, "zero count should return 0") testing.expect(t, gui.clamp_bubble_cursor(3, -1) == 0, "negative cursor should clamp to 0") testing.expect(t, gui.clamp_bubble_cursor(3, 3) == 2, "cursor at count should clamp to last index") testing.expect(t, gui.clamp_bubble_cursor(3, 10) == 2, "cursor beyond count should clamp to last index") testing.expect(t, gui.clamp_bubble_cursor(5, 2) == 2, "valid cursor should pass through") } @test gui_layout_validation_coverage :: proc(t: ^testing.T) { panels: [dynamic]core.Page_Layout_Panel defer delete(panels) // 4 panels filling the page exactly for i in 0..<4 { panel := core.Page_Layout_Panel{ panel_id = fmt.tprintf("panel_%d", i), panel_number = i + 1, layout_cell = core.Layout_Cell{ x = f32(i%2) * 0.5, y = f32(i/2) * 0.5, w = 0.5, h = 0.5, }, } append(&panels, panel) } layout := core.Page_Layout{ page_number = 1, pattern_id = "grid-2x2", width = 800, height = 1200, panels = panels[:], } val := gui.validate_layout_page(layout, nil) testing.expect(t, val.total_panels == 4, "should count 4 panels") testing.expect(t, val.coverage_pct == 100.0, fmt.tprintf("coverage should be 100%%, got %.1f%%", val.coverage_pct)) testing.expect(t, val.missing_bindings == 4, "all 4 panels should be missing images") testing.expect(t, val.bounds_violations == 0, "no bounds violations for valid cells") } @test gui_layout_validation_bounds_violations :: proc(t: ^testing.T) { panels: [dynamic]core.Page_Layout_Panel defer delete(panels) // Panel extending beyond bounds panel := core.Page_Layout_Panel{ panel_id = "panel_1", panel_number = 1, layout_cell = core.Layout_Cell{ x = -0.1, y = 0.0, w = 1.2, h = 1.0, }, } append(&panels, panel) layout := core.Page_Layout{ page_number = 1, pattern_id = "single-splash", width = 800, height = 1200, panels = panels[:], } val := gui.validate_layout_page(layout, nil) testing.expect(t, val.bounds_violations == 1, "should detect bounds violation for negative x and width > 1") } @test gui_layout_validation_missing_bindings :: proc(t: ^testing.T) { panels: [dynamic]core.Page_Layout_Panel defer delete(panels) panel := core.Page_Layout_Panel{ panel_id = "panel_1", panel_number = 1, layout_cell = core.Layout_Cell{x = 0, y = 0, w = 1, h = 1}, } append(&panels, panel) layout := core.Page_Layout{ page_number = 1, pattern_id = "single-splash", width = 800, height = 1200, panels = panels[:], } // No panel images at all val := gui.validate_layout_page(layout, nil) testing.expect(t, val.missing_bindings == 1, "should detect missing binding for panel without image") // Panel image present images := make(map[string]core.Panel_Image) images["panel_1"] = core.Panel_Image{url = "test.png", width = 512, height = 512, seed = 42, prompt = "test"} val2 := gui.validate_layout_page(layout, images) testing.expect(t, val2.missing_bindings == 0, "should have no missing bindings when image exists") delete(images) } // ───────────────────────────────────────────────────────────── // Milestone 39B: Error-path tests // ───────────────────────────────────────────────────────────── @test gui_action_regenerate_page_layout_invalid_page :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) msg := gui.action_regenerate_page_layout(&controller, -1) testing.expect(t, msg == "Invalid layout page", "negative page index should return error") msg2 := gui.action_regenerate_page_layout(&controller, 0) testing.expect(t, msg2 == "Invalid layout page", "page index beyond layouts should return error") } @test gui_action_regenerate_page_layout_no_panels :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) // Add empty layout layouts: [dynamic]core.Page_Layout defer delete(layouts) append(&layouts, core.Page_Layout{ page_number = 1, pattern_id = "single-splash", width = 800, height = 1200, }) controller.state.page_layouts = layouts[:] defer delete(controller.state.page_layouts) msg := gui.action_regenerate_page_layout(&controller, 0) testing.expect(t, msg == "Page has no panels", "empty layout should return error") } @test gui_action_add_bubble_creates_map :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) testing.expect(t, controller.state.speech_bubbles == nil, "speech_bubbles should start nil") msg := gui.action_add_bubble(&controller, "test_panel") testing.expect(t, msg == "Added bubble", "add bubble should succeed") testing.expect(t, controller.state.speech_bubbles != nil, "speech_bubbles map should be created") slice, ok := controller.state.speech_bubbles["test_panel"] testing.expect(t, ok, "panel key should exist in map") testing.expect(t, len(slice) == 1, "should have one bubble") testing.expect(t, slice[0].type == .Normal, "new bubble should be Normal type") testing.expect(t, slice[0].text == "New bubble", "new bubble should have default text") // Clean up owned strings core.dispose_speech_bubbles(&controller.state.speech_bubbles) } @test gui_action_delete_bubble_edge_cases :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) // Delete from nil map msg := gui.action_delete_bubble(&controller, "panel_1", 0) testing.expect(t, strings.has_prefix(msg, "No bubbles"), "delete from nil map should return error") // Delete with invalid index on populated map bubbles: [dynamic]core.Speech_Bubble defer delete(bubbles) bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"} append(&bubbles, bubble) controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) controller.state.speech_bubbles["panel_1"] = bubbles[:] msg3 := gui.action_delete_bubble(&controller, "panel_1", -1) testing.expect(t, strings.has_prefix(msg3, "Bubble not found"), "negative index should return error") msg4 := gui.action_delete_bubble(&controller, "panel_1", 5) testing.expect(t, strings.has_prefix(msg4, "Bubble not found"), "out-of-range index should return error") } @test gui_action_delete_bubble_removes_last_bubble :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) bubbles: [dynamic]core.Speech_Bubble bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"} append(&bubbles, bubble) controller.state.speech_bubbles["panel_1"] = bubbles[:] msg := gui.action_delete_bubble(&controller, "panel_1", 0) testing.expect(t, msg == "Deleted bubble", "delete should succeed") _, ok := controller.state.speech_bubbles["panel_1"] testing.expect(t, !ok, "panel key should be removed when last bubble deleted") // Clean up core.dispose_speech_bubbles(&controller.state.speech_bubbles) } @test gui_action_update_bubble_edge_cases :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) // Update from nil map msg := gui.action_update_bubble(&controller, "panel_1", 0, .Thought, "") testing.expect(t, strings.has_prefix(msg, "No bubbles"), "update from nil map should return error") // Update with invalid index on populated map bubbles: [dynamic]core.Speech_Bubble defer delete(bubbles) bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"} append(&bubbles, bubble) controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) controller.state.speech_bubbles["panel_1"] = bubbles[:] msg2 := gui.action_update_bubble(&controller, "panel_1", -1, .Thought, "") testing.expect(t, strings.has_prefix(msg2, "Bubble not found"), "negative index should return error") msg3 := gui.action_update_bubble(&controller, "panel_1", 5, .Thought, "") testing.expect(t, strings.has_prefix(msg3, "Bubble not found"), "out-of-range index should return error") } @test gui_action_update_bubble_changes_type :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) bubbles: [dynamic]core.Speech_Bubble bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "hello"} append(&bubbles, bubble) controller.state.speech_bubbles["panel_1"] = bubbles[:] msg := gui.action_update_bubble(&controller, "panel_1", 0, .Thought, "") testing.expect(t, msg == "Bubble updated", "update should succeed") testing.expect(t, controller.state.speech_bubbles["panel_1"][0].type == .Thought, "bubble type should change") core.dispose_speech_bubbles(&controller.state.speech_bubbles) } @test gui_action_update_bubble_changes_text :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) controller := ui.new_controller(state) defer ui.dispose_job_manager(&controller.jobs) controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble) bubbles: [dynamic]core.Speech_Bubble bubble := core.Speech_Bubble{id = "b1", panel_id = "panel_1", type = .Normal, text = "old text"} append(&bubbles, bubble) controller.state.speech_bubbles["panel_1"] = bubbles[:] msg := gui.action_update_bubble(&controller, "panel_1", 0, .Normal, "new text") testing.expect(t, msg == "Bubble updated", "update should succeed") testing.expect(t, controller.state.speech_bubbles["panel_1"][0].text == "new text", "bubble text should change") core.dispose_speech_bubbles(&controller.state.speech_bubbles) } // ───────────────────────────────────────────────────────────── // Milestone 39C: Ownership/lifecycle audits for new GUI surfaces // ───────────────────────────────────────────────────────────── @test gui_bubble_map_disposal_cleans_strings :: proc(t: ^testing.T) { state := core.new_initial_state() defer core.dispose_state(&state) // Create bubbles with owned strings state.speech_bubbles = make(map[string][]core.Speech_Bubble) bubbles: [dynamic]core.Speech_Bubble bubble := core.Speech_Bubble{ id = fmt.aprintf("bubble_%d", 1), panel_id = fmt.aprintf("panel_%d", 1), type = .Normal, text = fmt.aprintf("Test dialogue %d", 1), speaker_id = fmt.aprintf("char_%d", 1), tail_direction = "bottom", style = core.DEFAULT_BUBBLE_STYLE, } append(&bubbles, bubble) state.speech_bubbles["panel_1"] = bubbles[:] // Verify bubbles exist before dispose testing.expect(t, len(state.speech_bubbles) == 1, "should have one panel entry") testing.expect(t, len(state.speech_bubbles["panel_1"]) == 1, "should have one bubble") // Dispose should clean up owned strings without errors core.dispose_speech_bubbles(&state.speech_bubbles) // Note: dispose_speech_bubbles deletes the map shell, so we don't check state after testing.expect(t, true, "dispose completed without crash") } @test gui_layout_page_cursor_clamp_stability :: proc(t: ^testing.T) { testing.expect(t, gui.clamp_layout_cursor(0, 5) == 0, "zero layouts should return 0") testing.expect(t, gui.clamp_layout_cursor(3, -1) == 0, "negative cursor should clamp to 0") testing.expect(t, gui.clamp_layout_cursor(3, 3) == 2, "cursor at count should clamp to last") testing.expect(t, gui.clamp_layout_cursor(5, 2) == 2, "valid cursor should pass through") } @test gui_collect_layout_panels_for_page_edge_cases :: proc(t: ^testing.T) { layouts: [dynamic]core.Page_Layout defer delete(layouts) // Empty layouts result := gui.collect_layout_panels_for_page(layouts[:], 0) testing.expect(t, result == nil, "empty layouts should return nil") // Invalid page cursor append(&layouts, core.Page_Layout{page_number = 1, pattern_id = "single-splash"}) result2 := gui.collect_layout_panels_for_page(layouts[:], -1) testing.expect(t, result2 == nil, "negative cursor should return nil") result3 := gui.collect_layout_panels_for_page(layouts[:], 5) testing.expect(t, result3 == nil, "out-of-range cursor should return nil") } @test gui_count_bubbles_for_panel_nil_map :: proc(t: ^testing.T) { count := gui.count_bubbles_for_panel(nil, "panel_1") testing.expect(t, count == 0, "nil map should return 0") // Non-nil map with no entry images := make(map[string][]core.Speech_Bubble) defer core.dispose_speech_bubbles(&images) count2 := gui.count_bubbles_for_panel(images, "panel_1") testing.expect(t, count2 == 0, "missing key should return 0") }