comic/odin/tests/phase7_drag_integration.odin
2026-05-22 03:51:50 +02:00

257 lines
9.9 KiB
Odin

package tests
import "core:fmt"
import "core:strings"
import "core:testing"
import "../src/core"
import "../src/adapters"
import "../src/gui"
import "../src/ui"
import "../src/shared"
// ─────────────────────────────────────────────────────────────
// Phase 7: Bubble Drag Positioning + Integration Tests
// ─────────────────────────────────────────────────────────────
// ── Bubble Drag Positioning Tests ──
@test
gui_action_reposition_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)
panel := core.Panel{
panel_id = "panel_1",
panel_number = 1,
dialogue = []core.Dialogue{
{speaker_id = "char_1", text = "Hello", emotion = .Neutral},
},
}
bubbles := core.auto_place_panel_bubbles(panel, 800, 600)
defer delete(bubbles)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles
if len(bubbles) > 0 {
bubble_id := bubbles[0].id
msg := gui.action_reposition_bubble(&controller, bubble_id, 0.5, 0.5)
testing.expect(t, strings.contains(msg, "repositioned"), fmt.tprintf("should reposition bubble, got %q", msg))
// Verify position was updated
slice := controller.state.speech_bubbles["panel_1"]
testing.expect(t, slice[0].position.x == 0.5, fmt.tprintf("x should be 0.5, got %f", slice[0].position.x))
testing.expect(t, slice[0].position.y == 0.5, fmt.tprintf("y should be 0.5, got %f", slice[0].position.y))
}
}
@test
gui_action_reset_bubble_position :: 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)
panel := core.Panel{
panel_id = "panel_1",
panel_number = 1,
dialogue = []core.Dialogue{
{speaker_id = "char_1", text = "Hello", emotion = .Neutral},
},
}
bubbles := core.auto_place_panel_bubbles(panel, 800, 600)
defer delete(bubbles)
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
controller.state.speech_bubbles["panel_1"] = bubbles
if len(bubbles) > 0 {
bubble_id := bubbles[0].id
// First reposition to non-zero
_ = gui.action_reposition_bubble(&controller, bubble_id, 0.8, 0.8)
// Then reset
msg := gui.action_reset_bubble_position(&controller, bubble_id)
testing.expect(t, strings.contains(msg, "reset"), fmt.tprintf("should reset bubble position, got %q", msg))
// Verify position was reset
slice := controller.state.speech_bubbles["panel_1"]
testing.expect(t, slice[0].position.x == 0.0, fmt.tprintf("x should be 0 after reset, got %f", slice[0].position.x))
testing.expect(t, slice[0].position.y == 0.0, fmt.tprintf("y should be 0 after reset, got %f", slice[0].position.y))
}
}
@test
gui_action_reposition_bubble_not_found :: 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)
// Empty speech_bubbles map (not nil) - returns "No bubbles to reposition"
controller.state.speech_bubbles = make(map[string][]core.Speech_Bubble)
msg := gui.action_reposition_bubble(&controller, "nonexistent", 0.5, 0.5)
// Empty map (non-nil) iterates zero times, returns "Bubble not found"
// But make() creates non-nil empty map, so it enters the loop and returns "Bubble not found"
testing.expect(t, msg == "Bubble not found" || msg == "No bubbles to reposition", fmt.tprintf("should report not found or no bubbles, got %q", msg))
}
@test
gui_action_reposition_bubble_empty_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)
msg := gui.action_reposition_bubble(&controller, "b1", 0.5, 0.5)
testing.expect(t, msg == "No bubbles to reposition", fmt.tprintf("should report no bubbles, got %q", msg))
}
// ── Integration: Full Pipeline Tests ──
@test
integration_local_full_pipeline :: 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.story_idea = "A hero's journey"
controller.state.story_genre = "action"
controller.state.art_style = "manga"
// Step 1: Generate local script
msg1 := gui.action_generate_local_script(&controller, 2)
testing.expect(t, strings.contains(msg1, "Generated"), fmt.tprintf("should generate script, got %q", msg1))
testing.expect(t, len(controller.state.script.pages) == 2, "should have 2 pages")
// Step 2: Generate local panels
msg2 := gui.action_generate_local_panels(&controller)
testing.expect(t, strings.contains(msg2, "Generated"), fmt.tprintf("should generate panels, got %q", msg2))
testing.expect(t, len(controller.state.panel_images) > 0, "should have panel images")
// Step 3: Auto layout
msg3 := gui.action_layout_auto(&controller)
testing.expect(t, strings.contains(msg3, "layout"), fmt.tprintf("should create layout, got %q", msg3))
testing.expect(t, len(controller.state.page_layouts) > 0, "should have page layouts")
// Step 4: Auto-place bubbles for first panel
if len(controller.state.page_layouts) > 0 {
layout := controller.state.page_layouts[0]
if len(layout.panels) > 0 {
panel_id := layout.panels[0].panel_id
msg4 := gui.action_auto_place_bubbles_for_panel(&controller, panel_id, layout)
testing.expect(t, strings.contains(msg4, "Auto-placed") || strings.contains(msg4, "No dialogue"), fmt.tprintf("should auto-place bubbles, got %q", msg4))
}
}
}
@test
integration_character_parser_workflow :: proc(t: ^testing.T) {
desc := "25-year-old female, black long hair, blue eyes, fair skin, slim build, wearing a red dress, glasses, scar on cheek"
tmpl := core.parse_description_to_template(desc)
// Verify template fields
testing.expect(t, tmpl.age == "25", "age should be 25")
testing.expect(t, tmpl.gender == "female", "gender should be female")
testing.expect(t, tmpl.hair_color == "black", "hair color should be black")
// Convert back to string
result := core.template_to_string(tmpl)
testing.expect(t, len(result) > 0, "template_to_string should produce output")
testing.expect(t, strings.contains(result, "25-year-old"), "should contain age")
testing.expect(t, strings.contains(result, "female"), "should contain gender")
}
// ── Adapter Edge Case Tests ──
@test
adapters_validate_script_options_empty_idea :: proc(t: ^testing.T) {
opts := adapters.Generate_Script_Options{
story_idea = "",
num_pages = 4,
}
err := adapters.validate_generate_script_options(opts)
testing.expect(t, !shared.is_ok(err), "should fail with empty story_idea")
testing.expect(t, strings.contains(err.message, "story_idea"), "error should mention story_idea")
}
@test
adapters_validate_script_options_zero_pages :: proc(t: ^testing.T) {
opts := adapters.Generate_Script_Options{
story_idea = "Test",
num_pages = 0,
}
err := adapters.validate_generate_script_options(opts)
testing.expect(t, !shared.is_ok(err), "should fail with zero pages")
testing.expect(t, strings.contains(err.message, "num_pages"), "error should mention num_pages")
}
@test
adapters_build_fallback_script :: proc(t: ^testing.T) {
opts := adapters.Generate_Script_Options{
story_idea = "A test story",
num_pages = 1,
}
script := adapters.build_fallback_script(opts)
testing.expect(t, len(script.title) > 0, "fallback should have title")
testing.expect(t, len(script.pages) > 0, "fallback should have pages")
testing.expect(t, len(script.characters) > 0, "fallback should have characters")
// Note: build_fallback_script uses array literals, no need to dispose
}
@test
adapters_extract_json_block :: proc(t: ^testing.T) {
// Plain JSON
result1 := adapters.extract_json_block("{\"key\":\"value\"}")
testing.expect(t, result1 == "{\"key\":\"value\"}", "should return plain JSON unchanged")
// JSON with markdown code block
result2 := adapters.extract_json_block("```json\n{\"key\":\"value\"}\n```")
testing.expect(t, strings.has_prefix(result2, "{") && strings.has_suffix(result2, "}"), "should extract JSON from code block")
// JSON with surrounding text
result3 := adapters.extract_json_block("Here is the script: {\"title\":\"Test\"} done.")
testing.expect(t, strings.has_prefix(result3, "{"), "should extract JSON from text")
}
@test
adapters_deepseek_json_escape :: proc(t: ^testing.T) {
// Escape quotes
result1 := adapters.deepseek_json_escape(`hello "world"`)
testing.expect(t, strings.contains(result1, `\"`), "should escape quotes")
// Escape backslashes
result2 := adapters.deepseek_json_escape(`path\to\file`)
testing.expect(t, strings.contains(result2, `\\`), "should escape backslashes")
// Escape newlines
result3 := adapters.deepseek_json_escape("line1\nline2")
testing.expect(t, strings.contains(result3, `\n`), "should escape newlines")
}
@test
adapters_fal_parse_response_body_typed :: proc(t: ^testing.T) {
body := "{\"images\":[{\"url\":\"https://example.com/test.png\",\"width\":1344,\"height\":768}]}"
resp, err := adapters.fal_parse_response_body(body)
defer adapters.dispose_fal_response(&resp)
testing.expect(t, shared.is_ok(err), "parse should succeed")
testing.expect(t, len(resp.images) == 1, "should have one image")
testing.expect(t, resp.images[0].width == 1344, "width should be 1344")
testing.expect(t, resp.images[0].height == 768, "height should be 768")
}
@test
adapters_fal_parse_response_body_empty :: proc(t: ^testing.T) {
body := "{\"images\":[]}"
resp, err := adapters.fal_parse_response_body(body)
// Empty images may or may not succeed depending on implementation
_ = resp
// Just verify the function doesn't crash
testing.expect(t, true, "parse function should not crash")
}