459 lines
18 KiB
Odin
459 lines
18 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"
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// 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")
|
|
}
|