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

245 lines
12 KiB
Odin

package tests
import "core:fmt"
import "core:strings"
import "core:testing"
import "../src/core"
import "../src/ui"
// ─────────────────────────────────────────────────────────────
// Phase 3: Character Parser + Progress Tracking + Genre Layouts
// ─────────────────────────────────────────────────────────────
// ── Character Parser Tests ──
@test
core_parse_age_numeric :: proc(t: ^testing.T) {
testing.expect(t, core.extract_age("25-year-old female") == "25", "should extract numeric age")
testing.expect(t, core.extract_age("30 years old man") == "30", "should extract age with 'years old'")
testing.expect(t, core.extract_age("A 42-year-old wizard") == "42", "should extract age from middle of string")
}
@test
core_parse_age_word :: proc(t: ^testing.T) {
testing.expect(t, core.extract_age("young boy") == "young", "should extract 'young'")
testing.expect(t, core.extract_age("elderly woman") == "elderly", "should extract 'elderly'")
testing.expect(t, core.extract_age("middle-aged detective") == "middle-aged", "should extract 'middle-aged'")
}
@test
core_parse_gender :: proc(t: ^testing.T) {
testing.expect(t, core.extract_gender("male warrior") == "male", "should extract male")
testing.expect(t, core.extract_gender("female knight") == "female", "should extract female")
testing.expect(t, core.extract_gender("non-binary hero") == "non-binary", "should extract non-binary")
testing.expect(t, core.extract_gender("she wears a dress") == "female", "should detect 'she' as female")
testing.expect(t, core.extract_gender("he carries a sword") == "male", "should detect 'he' as male")
}
@test
core_parse_hair :: proc(t: ^testing.T) {
testing.expect(t, core.extract_hair_color("black long hair") == "black", "should extract black hair color")
testing.expect(t, core.extract_hair_color("blonde ponytail") == "blonde", "should extract blonde")
testing.expect(t, core.extract_hair_style("long curly hair") == "long", "should extract long style")
testing.expect(t, core.extract_hair_style("short spiky hair") == "short", "should extract short style")
}
@test
core_parse_eyes :: proc(t: ^testing.T) {
testing.expect(t, core.extract_eye_color("blue eyes") == "blue", "should extract blue eyes")
testing.expect(t, core.extract_eye_color("piercing green gaze") == "green", "should extract green eyes")
}
@test
core_parse_skin_body :: proc(t: ^testing.T) {
testing.expect(t, core.extract_skin_tone("fair skin") == "fair", "should extract fair skin")
testing.expect(t, core.extract_skin_tone("olive complexion") == "olive", "should extract olive skin")
testing.expect(t, core.extract_body_type("slim build") == "slim", "should extract slim body type")
testing.expect(t, core.extract_body_type("muscular frame") == "muscular", "should extract muscular")
}
@test
core_parse_outfit :: proc(t: ^testing.T) {
testing.expect(t, core.extract_outfit("wearing a red dress") == "a red dress", "should extract outfit after 'wearing'")
testing.expect(t, core.extract_outfit("dressed in black armor, holding sword") == "black armor", "should extract outfit after 'dressed in'")
}
@test
core_parse_accessories :: proc(t: ^testing.T) {
testing.expect(t, strings.contains(core.extract_accessories("wearing glasses and a hat"), "glasses"), "should detect glasses")
testing.expect(t, strings.contains(core.extract_accessories("wearing glasses and a hat"), "hat"), "should detect hat")
testing.expect(t, core.extract_accessories("no accessories here") == "", "should return empty for no accessories")
}
@test
core_parse_distinguishing_features :: proc(t: ^testing.T) {
testing.expect(t, strings.contains(core.extract_distinguishing_features("scar on cheek"), "scar"), "should detect scar")
testing.expect(t, strings.contains(core.extract_distinguishing_features("tattoo on arm"), "tattoo"), "should detect tattoo")
testing.expect(t, core.extract_distinguishing_features("nothing special") == "", "should return empty for no features")
}
@test
core_parse_full_description_to_template :: 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)
testing.expect(t, tmpl.age == "25", fmt.tprintf("age should be '25', got %q", tmpl.age))
testing.expect(t, tmpl.gender == "female", fmt.tprintf("gender should be 'female', got %q", tmpl.gender))
testing.expect(t, tmpl.hair_color == "black", fmt.tprintf("hair_color should be 'black', got %q", tmpl.hair_color))
testing.expect(t, tmpl.hair_style == "long", fmt.tprintf("hair_style should be 'long', got %q", tmpl.hair_style))
testing.expect(t, tmpl.eye_color == "blue", fmt.tprintf("eye_color should be 'blue', got %q", tmpl.eye_color))
testing.expect(t, tmpl.skin_tone == "fair", fmt.tprintf("skin_tone should be 'fair', got %q", tmpl.skin_tone))
testing.expect(t, tmpl.body_type == "slim", fmt.tprintf("body_type should be 'slim', got %q", tmpl.body_type))
testing.expect(t, strings.contains(tmpl.outfit, "red dress"), fmt.tprintf("outfit should contain 'red dress', got %q", tmpl.outfit))
}
@test
core_extract_color_palette :: proc(t: ^testing.T) {
desc := "black hair, blue eyes, fair skin, wearing red"
palette := core.extract_color_palette(desc)
testing.expect(t, palette.hair == "black", "palette hair should be black")
testing.expect(t, palette.eyes == "blue", "palette eyes should be blue")
testing.expect(t, palette.skin == "fair", "palette skin should be fair")
}
@test
core_template_to_string_roundtrip :: proc(t: ^testing.T) {
tmpl := core.Character_Prompt_Template{
age = "30",
gender = "male",
hair_color = "brown",
hair_style = "short",
eye_color = "green",
skin_tone = "tan",
body_type = "athletic",
outfit = "leather jacket",
accessories = "sunglasses",
distinguishing_features = "scar",
}
result := core.template_to_string(tmpl)
testing.expect(t, strings.contains(result, "30-year-old"), "should contain age")
testing.expect(t, strings.contains(result, "male"), "should contain gender")
testing.expect(t, strings.contains(result, "brown short hair"), "should contain hair")
testing.expect(t, strings.contains(result, "green eyes"), "should contain eye color")
testing.expect(t, strings.contains(result, "tan skin"), "should contain skin tone")
testing.expect(t, strings.contains(result, "athletic build"), "should contain body type")
testing.expect(t, strings.contains(result, "leather jacket"), "should contain outfit")
testing.expect(t, strings.contains(result, "sunglasses"), "should contain accessories")
testing.expect(t, strings.contains(result, "scar"), "should contain features")
}
// ── Progress Tracking Tests ──
@test
ui_background_job_has_progress :: proc(t: ^testing.T) {
job := ui.Background_Job{
id = 1,
status = .Running,
progress = 50.0,
}
testing.expect(t, job.progress == 50.0, "progress should be 50.0")
}
@test
ui_update_job_progress :: 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)
job_id := ui.submit_job(&controller.jobs, .Generate_Panel, "Testing progress")
_ = ui.update_job_progress(&controller.jobs, job_id, 25.0)
idx := ui.job_index_by_id(&controller.jobs, job_id)
testing.expect(t, idx >= 0, "job should exist")
if idx >= 0 {
job := &controller.jobs.jobs[idx]
testing.expect(t, job.progress == 25.0, fmt.tprintf("progress should be 25.0, got %.1f", job.progress))
}
}
@test
ui_progress_percentage_calculation :: proc(t: ^testing.T) {
// progress is stored as f32 percentage (0-100)
pct_1 := f32(3) / f32(12) * 100.0
testing.expect(t, pct_1 == 25.0, fmt.tprintf("3/12 should be 25%%, got %.1f%%", pct_1))
pct_2 := f32(6) / f32(12) * 100.0
testing.expect(t, pct_2 == 50.0, fmt.tprintf("6/12 should be 50%%, got %.1f%%", pct_2))
}
// ── Genre-Based Layout Pattern Tests ──
@test
core_pattern_matches_genre :: proc(t: ^testing.T) {
testing.expect(t, core.pattern_matches_genre("grid-2x2", "any") == true, "grid should match any genre")
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "manga") == true, "manga pattern should match manga genre")
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "action") == true, "manga pattern should match action genre")
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "romance") == false, "manga pattern should not match romance")
testing.expect(t, core.pattern_matches_genre("action-dynamic", "superhero") == true, "action pattern should match superhero")
testing.expect(t, core.pattern_matches_genre("dialogue-heavy", "romance") == true, "dialogue should match romance")
testing.expect(t, core.pattern_matches_genre("cinematic-widescreen", "noir") == true, "cinematic should match noir")
testing.expect(t, core.pattern_matches_genre("webtoon-scroll", "slice-of-life") == true, "webtoon should match slice-of-life")
}
@test
core_pattern_matches_genre_empty :: proc(t: ^testing.T) {
testing.expect(t, core.pattern_matches_genre("manga-3-tier", "") == true, "empty genre should match all patterns")
testing.expect(t, core.pattern_matches_genre("action-dynamic", "") == true, "empty genre should match action pattern")
}
@test
core_select_best_pattern_genre_filtering :: proc(t: ^testing.T) {
// Action genre: grid-2x2 (4 panels) is tighter fit than action-dynamic (5 panels) for 4 panels
pattern := core.select_best_pattern(4, "action", "")
testing.expect(t, pattern.id == "grid-2x2", fmt.tprintf("action genre with 4 panels should pick grid-2x2 (tightest), got %s", pattern.id))
// For 5 panels action, action-dynamic (5) is tighter than manga-3-tier (6)
pattern2 := core.select_best_pattern(5, "action", "")
testing.expect(t, pattern2.id == "action-dynamic", fmt.tprintf("action genre with 5 panels should pick action-dynamic, got %s", pattern2.id))
// Manga genre with 5 panels should pick manga-3-tier
pattern3 := core.select_best_pattern(5, "manga", "")
testing.expect(t, pattern3.id == "manga-3-tier", fmt.tprintf("manga genre with 5 panels should pick manga-3-tier, got %s", pattern3.id))
// Romance should prefer dialogue-heavy for 6+ panels
pattern4 := core.select_best_pattern(6, "romance", "")
testing.expect(t, pattern4.id == "dialogue-heavy", fmt.tprintf("romance genre with 6 panels should pick dialogue-heavy, got %s", pattern4.id))
}
@test
core_select_best_pattern_tightest_fit :: proc(t: ^testing.T) {
// With no genre, should pick smallest pattern that fits
pattern := core.select_best_pattern(3, "", "")
testing.expect(t, pattern.max_panels >= 3, "pattern should fit 3 panels")
testing.expect(t, pattern.max_panels <= 4, "should pick tightest fit for 3 panels")
pattern2 := core.select_best_pattern(1, "", "")
testing.expect(t, pattern2.id == "splash-page", "1 panel should pick splash-page")
}
@test
core_select_best_pattern_preference_override :: proc(t: ^testing.T) {
// Preference should override genre filtering if it fits
pattern := core.select_best_pattern(3, "action", "grid-2x2")
testing.expect(t, pattern.id == "grid-2x2", fmt.tprintf("preference should override, got %s", pattern.id))
}
@test
core_auto_layout_pages_uses_genre :: proc(t: ^testing.T) {
panels: [dynamic]core.Panel
for i in 0..<5 {
append(&panels, core.Panel{
panel_id = fmt.tprintf("panel_%d", i),
panel_number = i + 1,
})
}
pages := core.auto_layout_pages(panels[:], .A4, "action", "")
testing.expect(t, len(pages) > 0, "should produce at least one page")
if len(pages) > 0 {
testing.expect(t, pages[0].pattern_id == "action-dynamic", fmt.tprintf("action genre should use action-dynamic pattern, got %s", pages[0].pattern_id))
}
defer delete(panels)
}