check point
This commit is contained in:
1 parent
ae1cae967b
commit
1e85df5193
72 files changed
+11452
No files matched your search
@@ -0,0 +1,103 @@
|
||||
package tests
|
||||
|
||||
import "core:testing"
|
||||
import "../src/adapters"
|
||||
import "../src/core"
|
||||
import "../src/shared"
|
||||
|
||||
deepseek_calls: int
|
||||
|
||||
phase2_deepseek_transport :: proc(cfg: shared.Config, request_json: string) -> (string, int, shared.App_Error) {
|
||||
_ = cfg
|
||||
_ = request_json
|
||||
deepseek_calls += 1
|
||||
if deepseek_calls == 1 {
|
||||
return "", 429, shared.ok()
|
||||
}
|
||||
return "{\"choices\":[{\"message\":{\"content\":\"{\\\"title\\\":\\\"Test\\\",\\\"synopsis\\\":\\\"A hero rises\\\",\\\"characters\\\":[{\\\"id\\\":\\\"char_001\\\",\\\"name\\\":\\\"Hero\\\",\\\"role\\\":\\\"protagonist\\\",\\\"description\\\":\\\"Determined\\\",\\\"firstAppearancePanel\\\":\\\"panel_001_001\\\"}],\\\"pages\\\":[{\\\"pageNumber\\\":1,\\\"layoutType\\\":\\\"grid\\\",\\\"panels\\\":[{\\\"panelId\\\":\\\"panel_001_001\\\",\\\"panelNumber\\\":1,\\\"shotType\\\":\\\"medium\\\",\\\"description\\\":\\\"Hero stands\\\",\\\"charactersPresent\\\":[\\\"char_001\\\"],\\\"dialogue\\\":[{\\\"speakerId\\\":\\\"char_001\\\",\\\"text\\\":\\\"Let's go!\\\",\\\"bubbleType\\\":\\\"normal\\\",\\\"emotion\\\":\\\"determined\\\"}],\\\"caption\\\":\\\"\\\",\\\"soundEffects\\\":[],\\\"transitionFromPrevious\\\":\\\"none\\\"}]}]}\"}}]}", 200, shared.ok()
|
||||
}
|
||||
|
||||
fal_calls: int
|
||||
|
||||
phase2_fal_transport :: proc(cfg: shared.Config, endpoint, prompt: string, seed: i64) -> (string, int, shared.App_Error) {
|
||||
_ = cfg
|
||||
_ = endpoint
|
||||
_ = prompt
|
||||
_ = seed
|
||||
fal_calls += 1
|
||||
if fal_calls == 1 {
|
||||
return "", 0, shared.network_error("temporary network issue")
|
||||
}
|
||||
return "https://example.com/image.png", 200, shared.ok()
|
||||
}
|
||||
|
||||
@test
|
||||
deepseek_retries_then_succeeds :: proc(t: ^testing.T) {
|
||||
deepseek_calls = 0
|
||||
|
||||
cfg := shared.Config{
|
||||
deepseek_api_key = "test-key",
|
||||
deepseek_base_url = "https://api.deepseek.com",
|
||||
}
|
||||
client := adapters.new_deepseek_client()
|
||||
client.transport = phase2_deepseek_transport
|
||||
client.max_retries = 3
|
||||
|
||||
opts := adapters.Generate_Script_Options{
|
||||
story_idea = "A hero rises",
|
||||
genre = "action",
|
||||
art_style = "manga",
|
||||
num_pages = 4,
|
||||
audience = "general",
|
||||
}
|
||||
|
||||
script, err := adapters.generate_comic_script(client, cfg, opts)
|
||||
defer core.dispose_script_owned(&script)
|
||||
testing.expect(t, shared.is_ok(err), "deepseek request should eventually succeed")
|
||||
testing.expect(t, deepseek_calls == 2, "expected one retry before success")
|
||||
testing.expect(t, len(script.pages) > 0, "generated script should contain pages")
|
||||
}
|
||||
|
||||
@test
|
||||
fal_queue_enforces_cap :: proc(t: ^testing.T) {
|
||||
q := adapters.new_fal_queue(1)
|
||||
|
||||
first := adapters.try_acquire_slot(&q)
|
||||
second := adapters.try_acquire_slot(&q)
|
||||
adapters.release_slot(&q)
|
||||
third := adapters.try_acquire_slot(&q)
|
||||
|
||||
testing.expect(t, first, "first slot acquisition should succeed")
|
||||
testing.expect(t, !second, "second slot acquisition should fail when saturated")
|
||||
testing.expect(t, third, "slot acquisition should succeed after release")
|
||||
}
|
||||
|
||||
@test
|
||||
fal_panel_generation_retries_network_error :: proc(t: ^testing.T) {
|
||||
fal_calls = 0
|
||||
|
||||
cfg := shared.Config{fal_api_key = "test-key"}
|
||||
q := adapters.new_fal_queue(2)
|
||||
client := adapters.new_fal_client(&q)
|
||||
client.transport = phase2_fal_transport
|
||||
client.max_retries = 3
|
||||
|
||||
panel := core.Panel{panel_id = "panel_1", panel_number = 1, description = "Hero jumps over a gap"}
|
||||
img, err := adapters.generate_panel_image(client, cfg, panel, nil, "manga", "proj_1")
|
||||
defer delete(img.prompt)
|
||||
|
||||
testing.expect(t, shared.is_ok(err), "fal panel generation should eventually succeed")
|
||||
testing.expect(t, fal_calls == 2, "expected one retry for network error")
|
||||
testing.expect(t, len(img.url) > 0, "image url should be set")
|
||||
}
|
||||
|
||||
@test
|
||||
fal_typed_response_parsing :: proc(t: ^testing.T) {
|
||||
body := "{\"images\":[{\"url\":\"https://example.com/a.png\",\"width\":1024,\"height\":1024}]}"
|
||||
resp, err := adapters.fal_parse_response_body(body)
|
||||
defer adapters.dispose_fal_response(&resp)
|
||||
|
||||
testing.expect(t, shared.is_ok(err), "typed parse should succeed")
|
||||
testing.expect(t, len(resp.images) == 1, "expected one image")
|
||||
testing.expect(t, resp.images[0].url == "https://example.com/a.png", "expected parsed URL")
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
package tests
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
import app "../src/app"
|
||||
import "../src/core"
|
||||
import "../src/shared"
|
||||
import "../src/ui"
|
||||
|
||||
@test
|
||||
cli_parse_commands :: proc(t: ^testing.T) {
|
||||
c1 := app.parse_cli_command(nil)
|
||||
testing.expect(t, c1.kind == .Demo, "no args should map to demo")
|
||||
|
||||
a2 := [1]string{"status"}
|
||||
c2 := app.parse_cli_command(a2[:])
|
||||
testing.expect(t, c2.kind == .Status, "status should parse")
|
||||
|
||||
a3 := [2]string{"save", "x.json"}
|
||||
c3 := app.parse_cli_command(a3[:])
|
||||
testing.expect(t, c3.kind == .Save, "save should parse")
|
||||
testing.expect(t, c3.path == "x.json", "save path should parse")
|
||||
|
||||
a4 := [1]string{"tui"}
|
||||
c4 := app.parse_cli_command(a4[:])
|
||||
testing.expect(t, c4.kind == .Tui, "tui should parse")
|
||||
|
||||
a5 := [1]string{"gui"}
|
||||
c5 := app.parse_cli_command(a5[:])
|
||||
testing.expect(t, c5.kind == .Gui, "gui should parse")
|
||||
|
||||
testing.expect(t, app.normalize_tui_command("q") == "quit", "q alias should expand")
|
||||
testing.expect(t, app.normalize_tui_command("1") == "goto story", "1 alias should map to story")
|
||||
testing.expect(t, app.normalize_tui_command("?") == "doctor", "? alias should map to doctor")
|
||||
testing.expect(t, app.normalize_tui_command("r") == "ready", "r alias should map to ready")
|
||||
testing.expect(t, app.normalize_tui_command("n") == "next", "n alias should map to next")
|
||||
testing.expect(t, app.normalize_tui_command("p") == "plan", "p alias should map to plan")
|
||||
testing.expect(t, app.normalize_tui_command("x") == "auto", "x alias should map to auto")
|
||||
|
||||
pages, matched, perr := app.parse_generate_script_pages("generate script 6")
|
||||
testing.expect(t, shared.is_ok(perr), "generate script pages parse should succeed")
|
||||
testing.expect(t, matched, "generate script pages should match")
|
||||
testing.expect(t, pages == 6, "generate script pages should parse value")
|
||||
|
||||
local_pages, lmatched, lerr := app.parse_generate_script_local_pages("generate script local 3")
|
||||
testing.expect(t, shared.is_ok(lerr), "generate script local parse should succeed")
|
||||
testing.expect(t, lmatched, "generate script local should match")
|
||||
testing.expect(t, local_pages == 3, "generate script local should parse value")
|
||||
|
||||
page, pmatch, pperr := app.parse_generate_panels_page("generate panels page 2")
|
||||
testing.expect(t, shared.is_ok(pperr), "generate panels page parse should succeed")
|
||||
testing.expect(t, pmatch, "generate panels page should match")
|
||||
testing.expect(t, page == 2, "generate panels page should parse value")
|
||||
|
||||
lpage, lpmatch, lperr := app.parse_generate_panels_local_page("generate panels local page 2")
|
||||
testing.expect(t, shared.is_ok(lperr), "generate panels local page parse should succeed")
|
||||
testing.expect(t, lpmatch, "generate panels local page should match")
|
||||
testing.expect(t, lpage == 2, "generate panels local page should parse value")
|
||||
|
||||
fmt_kind, export_path, ematch, eerr := app.parse_export_command("export cbz ./out.cbz")
|
||||
testing.expect(t, shared.is_ok(eerr), "export parse should succeed")
|
||||
testing.expect(t, ematch, "export parse should match")
|
||||
testing.expect(t, fmt_kind == .CBZ, "export format should parse")
|
||||
testing.expect(t, export_path == "./out.cbz", "export path should parse")
|
||||
|
||||
qfmt, qpath, qpages, qmatch, qerr := app.parse_quick_local_command("quick local pdf ./quick.pdf 3")
|
||||
testing.expect(t, shared.is_ok(qerr), "quick local parse should succeed")
|
||||
testing.expect(t, qmatch, "quick local parse should match")
|
||||
testing.expect(t, qfmt == .PDF, "quick local format should parse")
|
||||
testing.expect(t, qpath == "./quick.pdf", "quick local path should parse")
|
||||
testing.expect(t, qpages == 3, "quick local pages should parse")
|
||||
|
||||
proj, qafmt, qaout, qapages, qamatch, qaerr := app.parse_quick_local_all_command("quick local all ./p.comic.json cbz ./q.cbz 4")
|
||||
testing.expect(t, shared.is_ok(qaerr), "quick local all parse should succeed")
|
||||
testing.expect(t, qamatch, "quick local all parse should match")
|
||||
testing.expect(t, proj == "./p.comic.json", "quick local all project path should parse")
|
||||
testing.expect(t, qafmt == .CBZ, "quick local all format should parse")
|
||||
testing.expect(t, qaout == "./q.cbz", "quick local all export path should parse")
|
||||
testing.expect(t, qapages == 4, "quick local all pages should parse")
|
||||
|
||||
aafmt, aapath, aamatch, aaerr := app.parse_auto_all_command("auto all pdf ./auto.pdf")
|
||||
testing.expect(t, shared.is_ok(aaerr), "auto all parse should succeed")
|
||||
testing.expect(t, aamatch, "auto all parse should match")
|
||||
testing.expect(t, aafmt == .PDF, "auto all format should parse")
|
||||
testing.expect(t, aapath == "./auto.pdf", "auto all path should parse")
|
||||
|
||||
aalfmt, aalpath, aalpages, aalmatch, aalerr := app.parse_auto_all_local_command("auto all local cbz ./auto.cbz 3")
|
||||
testing.expect(t, shared.is_ok(aalerr), "auto all local parse should succeed")
|
||||
testing.expect(t, aalmatch, "auto all local parse should match")
|
||||
testing.expect(t, aalfmt == .CBZ, "auto all local format should parse")
|
||||
testing.expect(t, aalpath == "./auto.cbz", "auto all local path should parse")
|
||||
testing.expect(t, aalpages == 3, "auto all local pages should parse")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_save_and_load_roundtrip :: proc(t: ^testing.T) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
|
||||
path := fmt.aprintf("%s/project.comic.json", tmp_dir)
|
||||
defer delete(path)
|
||||
|
||||
state := core.new_initial_state()
|
||||
state.story_idea = "cli story"
|
||||
|
||||
save_out, save_err := app.run_cli_command(app.Parsed_CLI_Command{kind = .Save, path = path}, &state)
|
||||
testing.expect(t, shared.is_ok(save_err), "save command should succeed")
|
||||
testing.expect(t, strings.contains(save_out, "Saved project"), "save output should mention save")
|
||||
delete(save_out)
|
||||
|
||||
state.story_idea = "changed"
|
||||
load_out, load_err := app.run_cli_command(app.Parsed_CLI_Command{kind = .Load, path = path}, &state)
|
||||
testing.expect(t, shared.is_ok(load_err), "load command should succeed")
|
||||
testing.expect(t, strings.contains(load_out, "Loaded project"), "load output should mention load")
|
||||
testing.expect(t, state.story_idea == "cli story", "load should restore story")
|
||||
delete(load_out)
|
||||
|
||||
core.dispose_state_owned(&state)
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_generate_script_requires_key :: proc(t: ^testing.T) {
|
||||
prev := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
|
||||
defer {
|
||||
if len(prev) > 0 {
|
||||
_ = os.set_env("DEEPSEEK_API_KEY", prev)
|
||||
} else {
|
||||
_ = os.unset_env("DEEPSEEK_API_KEY")
|
||||
}
|
||||
}
|
||||
_ = os.unset_env("DEEPSEEK_API_KEY")
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, "generate script 6", &last_job)
|
||||
testing.expect(t, !shared.is_ok(err), "generate script should fail without configured key")
|
||||
testing.expect(t, len(out) == 0, "error path should not return output")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_generate_panels_page_requires_script :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, "generate panels page 2", &last_job)
|
||||
testing.expect(t, !shared.is_ok(err), "generate panels page should fail with empty script")
|
||||
testing.expect(t, len(out) == 0, "error path should not return output")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_generate_script_local_succeeds_without_key :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, "generate script local 2", &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "generate script local should succeed without key")
|
||||
testing.expect(t, strings.contains(out, "local script generated"), "local generation should return success message")
|
||||
delete(out)
|
||||
testing.expect(t, len(controller.state.script.pages) == 2, "local script should create requested pages")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_layout_and_export_require_data :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
last_job := 0
|
||||
_, out1, err1 := app.run_tui_command(&controller, "layout auto", &last_job)
|
||||
testing.expect(t, !shared.is_ok(err1), "layout auto should fail without script")
|
||||
testing.expect(t, len(out1) == 0, "layout error path should not return output")
|
||||
|
||||
_, out2, err2 := app.run_tui_command(&controller, "export pdf ./tmp.pdf", &last_job)
|
||||
testing.expect(t, !shared.is_ok(err2), "export should fail without layouts")
|
||||
testing.expect(t, len(out2) == 0, "export error path should not return output")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_local_panels_and_export_pdf :: proc(t: ^testing.T) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-local-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
|
||||
out_pdf := fmt.aprintf("%s/local.pdf", tmp_dir)
|
||||
defer delete(out_pdf)
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer {
|
||||
for _, img in controller.state.panel_images {
|
||||
delete(img.url)
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = nil
|
||||
ui.dispose_controller(&controller)
|
||||
}
|
||||
|
||||
last_job := 0
|
||||
_, out1, err1 := app.run_tui_command(&controller, "generate script local 2", &last_job)
|
||||
testing.expect(t, shared.is_ok(err1), "local script should succeed")
|
||||
delete(out1)
|
||||
|
||||
_, out2, err2 := app.run_tui_command(&controller, "generate panels local", &last_job)
|
||||
testing.expect(t, shared.is_ok(err2), "local panels should succeed")
|
||||
delete(out2)
|
||||
|
||||
_, out3, err3 := app.run_tui_command(&controller, "layout auto", &last_job)
|
||||
testing.expect(t, shared.is_ok(err3), "layout auto should succeed")
|
||||
delete(out3)
|
||||
|
||||
export_cmd := fmt.aprintf("export pdf %s", out_pdf)
|
||||
defer delete(export_cmd)
|
||||
_, out4, err4 := app.run_tui_command(&controller, export_cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err4), "export should succeed")
|
||||
delete(out4)
|
||||
testing.expect(t, os.exists(out_pdf), "exported pdf should exist")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_quick_local_export_pdf :: proc(t: ^testing.T) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-quick-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
|
||||
out_pdf := fmt.aprintf("%s/quick.pdf", tmp_dir)
|
||||
defer delete(out_pdf)
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer {
|
||||
for _, img in controller.state.panel_images {
|
||||
delete(img.url)
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = nil
|
||||
ui.dispose_controller(&controller)
|
||||
}
|
||||
|
||||
cmd := fmt.aprintf("quick local pdf %s 3", out_pdf)
|
||||
defer delete(cmd)
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "quick local should succeed")
|
||||
testing.expect(t, strings.contains(out, "quick local exported"), "quick local should return success output")
|
||||
delete(out)
|
||||
testing.expect(t, len(controller.state.script.pages) == 3, "quick local should build requested page count")
|
||||
testing.expect(t, os.exists(out_pdf), "quick-local exported pdf should exist")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_quick_local_all_saves_and_exports :: proc(t: ^testing.T) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-quick-all-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
|
||||
project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
|
||||
export_path := fmt.aprintf("%s/quick.cbz", tmp_dir)
|
||||
defer delete(project_path)
|
||||
defer delete(export_path)
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer {
|
||||
for _, img in controller.state.panel_images {
|
||||
delete(img.url)
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = nil
|
||||
ui.dispose_controller(&controller)
|
||||
}
|
||||
|
||||
cmd := fmt.aprintf("quick local all %s cbz %s 2", project_path, export_path)
|
||||
defer delete(cmd)
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "quick local all should succeed")
|
||||
testing.expect(t, strings.contains(out, "quick local all saved"), "quick local all should return success output")
|
||||
delete(out)
|
||||
testing.expect(t, os.exists(project_path), "quick local all should save project")
|
||||
testing.expect(t, os.exists(export_path), "quick local all should export artifact")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_doctor_reports_status :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, "doctor", &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "doctor should succeed")
|
||||
testing.expect(t, strings.contains(out, "Doctor"), "doctor output should include header")
|
||||
testing.expect(t, strings.contains(out, "deepseek key:"), "doctor output should include deepseek key status")
|
||||
testing.expect(t, strings.contains(out, "curl:"), "doctor output should include curl status")
|
||||
delete(out)
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_ready_reports_status :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
last_job := 0
|
||||
_, out, err := app.run_tui_command(&controller, "ready", &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "ready should succeed")
|
||||
testing.expect(t, strings.contains(out, "Ready"), "ready output should include header")
|
||||
testing.expect(t, strings.contains(out, "script generated:"), "ready output should include script status")
|
||||
testing.expect(t, strings.contains(out, "export ready:"), "ready output should include export status")
|
||||
delete(out)
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_next_recommends_action :: proc(t: ^testing.T) {
|
||||
prev_deep := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
|
||||
prev_fal := os.get_env("FAL_API_KEY", context.temp_allocator)
|
||||
defer {
|
||||
if len(prev_deep) > 0 { _ = os.set_env("DEEPSEEK_API_KEY", prev_deep) } else { _ = os.unset_env("DEEPSEEK_API_KEY") }
|
||||
if len(prev_fal) > 0 { _ = os.set_env("FAL_API_KEY", prev_fal) } else { _ = os.unset_env("FAL_API_KEY") }
|
||||
}
|
||||
_ = os.unset_env("DEEPSEEK_API_KEY")
|
||||
_ = os.unset_env("FAL_API_KEY")
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
last_job := 0
|
||||
|
||||
_, out1, err1 := app.run_tui_command(&controller, "next", &last_job)
|
||||
testing.expect(t, shared.is_ok(err1), "next should succeed")
|
||||
testing.expect(t, strings.contains(out1, "generate script local"), "next should recommend local script generation")
|
||||
delete(out1)
|
||||
|
||||
_, out2, err2 := app.run_tui_command(&controller, "generate script local 1", &last_job)
|
||||
testing.expect(t, shared.is_ok(err2), "generate script local should succeed")
|
||||
delete(out2)
|
||||
|
||||
_, out3, err3 := app.run_tui_command(&controller, "next", &last_job)
|
||||
testing.expect(t, shared.is_ok(err3), "next should succeed after script")
|
||||
testing.expect(t, strings.contains(out3, "generate panels local"), "next should recommend local panel generation")
|
||||
delete(out3)
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_plan_reports_progress :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
last_job := 0
|
||||
|
||||
_, out1, err1 := app.run_tui_command(&controller, "plan", &last_job)
|
||||
testing.expect(t, shared.is_ok(err1), "plan should succeed")
|
||||
testing.expect(t, strings.contains(out1, "Plan"), "plan output should include header")
|
||||
testing.expect(t, strings.contains(out1, "1) Script"), "plan output should include script step")
|
||||
delete(out1)
|
||||
|
||||
_, out2, err2 := app.run_tui_command(&controller, "generate script local 1", &last_job)
|
||||
testing.expect(t, shared.is_ok(err2), "local script should succeed")
|
||||
delete(out2)
|
||||
|
||||
_, out3, err3 := app.run_tui_command(&controller, "plan", &last_job)
|
||||
testing.expect(t, shared.is_ok(err3), "plan should succeed after script")
|
||||
testing.expect(t, strings.contains(out3, "next:"), "plan output should include next hint")
|
||||
delete(out3)
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_auto_runs_next_step :: proc(t: ^testing.T) {
|
||||
prev_deep := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
|
||||
prev_fal := os.get_env("FAL_API_KEY", context.temp_allocator)
|
||||
defer {
|
||||
if len(prev_deep) > 0 { _ = os.set_env("DEEPSEEK_API_KEY", prev_deep) } else { _ = os.unset_env("DEEPSEEK_API_KEY") }
|
||||
if len(prev_fal) > 0 { _ = os.set_env("FAL_API_KEY", prev_fal) } else { _ = os.unset_env("FAL_API_KEY") }
|
||||
}
|
||||
_ = os.unset_env("DEEPSEEK_API_KEY")
|
||||
_ = os.unset_env("FAL_API_KEY")
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
last_job := 0
|
||||
|
||||
_, out, err := app.run_tui_command(&controller, "auto", &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "auto should succeed")
|
||||
testing.expect(t, strings.contains(out, "auto ran:"), "auto output should include executed command")
|
||||
delete(out)
|
||||
testing.expect(t, len(controller.state.script.pages) > 0, "auto should progress by generating script")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_auto_all_runs_to_export :: proc(t: ^testing.T) {
|
||||
prev_deep := os.get_env("DEEPSEEK_API_KEY", context.temp_allocator)
|
||||
prev_fal := os.get_env("FAL_API_KEY", context.temp_allocator)
|
||||
defer {
|
||||
if len(prev_deep) > 0 { _ = os.set_env("DEEPSEEK_API_KEY", prev_deep) } else { _ = os.unset_env("DEEPSEEK_API_KEY") }
|
||||
if len(prev_fal) > 0 { _ = os.set_env("FAL_API_KEY", prev_fal) } else { _ = os.unset_env("FAL_API_KEY") }
|
||||
}
|
||||
_ = os.unset_env("DEEPSEEK_API_KEY")
|
||||
_ = os.unset_env("FAL_API_KEY")
|
||||
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-auto-all-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
out_pdf := fmt.aprintf("%s/auto_all.pdf", tmp_dir)
|
||||
defer delete(out_pdf)
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer {
|
||||
for _, img in controller.state.panel_images {
|
||||
delete(img.url)
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = nil
|
||||
ui.dispose_controller(&controller)
|
||||
}
|
||||
last_job := 0
|
||||
|
||||
cmd := fmt.aprintf("auto all pdf %s", out_pdf)
|
||||
defer delete(cmd)
|
||||
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "auto all should succeed")
|
||||
testing.expect(t, strings.contains(out, "auto all exported"), "auto all output should include success")
|
||||
delete(out)
|
||||
testing.expect(t, os.exists(out_pdf), "auto all should produce export file")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_auto_all_local_runs_to_export :: proc(t: ^testing.T) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-auto-all-local-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
out_cbz := fmt.aprintf("%s/auto_all_local.cbz", tmp_dir)
|
||||
defer delete(out_cbz)
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer {
|
||||
for _, img in controller.state.panel_images {
|
||||
delete(img.url)
|
||||
delete(img.prompt)
|
||||
}
|
||||
delete(controller.state.panel_images)
|
||||
controller.state.panel_images = nil
|
||||
ui.dispose_controller(&controller)
|
||||
}
|
||||
last_job := 0
|
||||
|
||||
cmd := fmt.aprintf("auto all local cbz %s 2", out_cbz)
|
||||
defer delete(cmd)
|
||||
_, out, err := app.run_tui_command(&controller, cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err), "auto all local should succeed")
|
||||
testing.expect(t, strings.contains(out, "auto all local exported"), "auto all local output should include success")
|
||||
delete(out)
|
||||
testing.expect(t, os.exists(out_cbz), "auto all local should produce export file")
|
||||
}
|
||||
|
||||
@test
|
||||
cli_tui_open_and_saveas_aliases :: proc(t: ^testing.T) {
|
||||
tmp_dir, terr := os.make_directory_temp("", "comic-cli-alias-*", context.temp_allocator)
|
||||
if terr != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return
|
||||
}
|
||||
defer os.remove_all(tmp_dir)
|
||||
|
||||
path := fmt.aprintf("%s/alias.comic.json", tmp_dir)
|
||||
defer delete(path)
|
||||
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
|
||||
controller.state.story_idea = "alias story"
|
||||
last_job := 0
|
||||
save_cmd := fmt.aprintf("saveas %s", path)
|
||||
defer delete(save_cmd)
|
||||
_, out1, err1 := app.run_tui_command(&controller, save_cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err1), "saveas should succeed")
|
||||
delete(out1)
|
||||
|
||||
controller.state.story_idea = "changed"
|
||||
open_cmd := fmt.aprintf("open %s", path)
|
||||
defer delete(open_cmd)
|
||||
_, out2, err2 := app.run_tui_command(&controller, open_cmd, &last_job)
|
||||
testing.expect(t, shared.is_ok(err2), "open should succeed")
|
||||
delete(out2)
|
||||
testing.expect(t, controller.state.story_idea == "alias story", "open should restore saved state")
|
||||
|
||||
if shared.is_ok(err2) {
|
||||
ui.dispose_controller_owned(&controller)
|
||||
} else {
|
||||
ui.dispose_controller(&controller)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package tests
|
||||
|
||||
import "core:testing"
|
||||
import "../src/core"
|
||||
|
||||
@test
|
||||
seed_is_deterministic :: proc(t: ^testing.T) {
|
||||
a := core.generate_panel_seed("proj_a", 1, 2, "panel_001")
|
||||
b := core.generate_panel_seed("proj_a", 1, 2, "panel_001")
|
||||
c := core.generate_panel_seed("proj_a", 1, 3, "panel_001")
|
||||
|
||||
testing.expect(t, a == b, "seed should be stable for same input")
|
||||
testing.expect(t, a != c, "seed should change for different panel number")
|
||||
}
|
||||
|
||||
@test
|
||||
layout_packs_panels_into_pages :: proc(t: ^testing.T) {
|
||||
panels_arr := [5]core.Panel{
|
||||
{panel_id = "p1", panel_number = 1},
|
||||
{panel_id = "p2", panel_number = 2},
|
||||
{panel_id = "p3", panel_number = 3},
|
||||
{panel_id = "p4", panel_number = 4},
|
||||
{panel_id = "p5", panel_number = 5},
|
||||
}
|
||||
layouts := core.auto_layout_pages(panels_arr[:], .A4, "action", "grid-2x2")
|
||||
defer {
|
||||
for l in layouts {
|
||||
delete(l.panels)
|
||||
}
|
||||
delete(layouts)
|
||||
}
|
||||
|
||||
testing.expect(t, len(layouts) == 1, "5 panels should fit action-dynamic in a single page")
|
||||
testing.expect(t, layouts[0].pattern_id == "action-dynamic", "expected smallest suitable action pattern")
|
||||
testing.expect(t, len(layouts[0].panels) == 5, "single page should contain all panels")
|
||||
}
|
||||
|
||||
@test
|
||||
bubble_autoplacement_creates_entries :: proc(t: ^testing.T) {
|
||||
dialogue_arr := [1]core.Dialogue{
|
||||
{speaker_id = "char_1", text = "Hello there", bubble_type = .Normal, emotion = "neutral"},
|
||||
}
|
||||
chars_arr := [1]string{"char_1"}
|
||||
panel := core.Panel{
|
||||
panel_id = "panel_1",
|
||||
dialogue = dialogue_arr[:],
|
||||
characters_present = chars_arr[:],
|
||||
caption = "Narration line",
|
||||
}
|
||||
|
||||
bubbles := core.auto_place_panel_bubbles(panel, 800, 600)
|
||||
defer {
|
||||
for b in bubbles {
|
||||
delete(b.id)
|
||||
}
|
||||
delete(bubbles)
|
||||
}
|
||||
testing.expect(t, len(bubbles) == 2, "dialogue + caption should produce 2 bubbles")
|
||||
}
|
||||
|
||||
@test
|
||||
script_normalization_fills_ids :: proc(t: ^testing.T) {
|
||||
panels_arr := [1]core.Panel{{description = "A scene"}}
|
||||
pages_arr := [1]core.Page{{panels = panels_arr[:]}}
|
||||
chars_arr := [1]core.Character{{name = "Hero"}}
|
||||
|
||||
raw := core.Comic_Script{
|
||||
title = "",
|
||||
synopsis = "",
|
||||
characters = chars_arr[:],
|
||||
pages = pages_arr[:],
|
||||
}
|
||||
|
||||
norm := core.normalize_script(raw)
|
||||
defer {
|
||||
delete(norm.title)
|
||||
delete(norm.synopsis)
|
||||
delete(norm.characters[0].id)
|
||||
delete(norm.pages[0].panels[0].panel_id)
|
||||
}
|
||||
testing.expect(t, len(norm.title) > 0, "title should be filled")
|
||||
testing.expect(t, len(norm.characters[0].id) > 0, "character id should be filled")
|
||||
testing.expect(t, len(norm.pages[0].panels[0].panel_id) > 0, "panel id should be filled")
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tests
|
||||
|
||||
import "core:testing"
|
||||
import "../src/core"
|
||||
|
||||
@test
|
||||
state_initial_step :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
testing.expect(t, state.workflow.current_step == .Story_Input, "expected Story_Input initial step")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package tests
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
import "../src/adapters"
|
||||
import "../src/core"
|
||||
import "../src/shared"
|
||||
|
||||
join2 :: proc(a, b: string) -> string {
|
||||
return fmt.aprintf("%s/%s", a, b)
|
||||
}
|
||||
|
||||
dispose_export_fixture :: proc(layouts: ^[]core.Page_Layout, panel_images: ^map[string]core.Panel_Image) {
|
||||
for _, img in panel_images^ {
|
||||
delete(img.url)
|
||||
}
|
||||
delete(panel_images^)
|
||||
for l in layouts^ {
|
||||
delete(l.panels)
|
||||
}
|
||||
delete(layouts^)
|
||||
}
|
||||
|
||||
setup_export_fixture :: proc(t: ^testing.T) -> (tmp_dir: string, layouts: []core.Page_Layout, panel_images: map[string]core.Panel_Image) {
|
||||
err: os.Error
|
||||
tmp_dir, err = os.make_directory_temp("", "comic-export-test-*", context.temp_allocator)
|
||||
if err != nil {
|
||||
testing.expect(t, false, "failed to create temp dir")
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
src_img := join2(tmp_dir, "src_panel.png")
|
||||
img_bytes := []byte{137, 80, 78, 71, 13, 10, 26, 10}
|
||||
werr := os.write_entire_file(src_img, img_bytes)
|
||||
testing.expect(t, werr == nil, "failed to write source image")
|
||||
|
||||
panel_id := "panel_1"
|
||||
layout_panel := core.Page_Layout_Panel{panel_id = panel_id, panel_number = 1, layout_cell = core.Layout_Cell{x = 0, y = 0, w = 1, h = 1}}
|
||||
layout_panels_dyn: [dynamic]core.Page_Layout_Panel
|
||||
append(&layout_panels_dyn, layout_panel)
|
||||
|
||||
layout := core.Page_Layout{page_number = 1, pattern_id = "grid-2x2", panels = layout_panels_dyn[:], width = 1000, height = 1400}
|
||||
layouts_dyn: [dynamic]core.Page_Layout
|
||||
append(&layouts_dyn, layout)
|
||||
|
||||
panel_images = make(map[string]core.Panel_Image)
|
||||
panel_images[panel_id] = core.Panel_Image{url = fmt.aprintf("file://%s", src_img), width = 100, height = 100, seed = 1, prompt = ""}
|
||||
delete(src_img)
|
||||
|
||||
return tmp_dir, layouts_dyn[:], panel_images
|
||||
}
|
||||
|
||||
@test
|
||||
export_png_and_cbz_create_files :: proc(t: ^testing.T) {
|
||||
tmp_dir, layouts, panel_images := setup_export_fixture(t)
|
||||
defer os.remove_all(tmp_dir)
|
||||
defer dispose_export_fixture(&layouts, &panel_images)
|
||||
|
||||
png_out := join2(tmp_dir, "out_png.zip")
|
||||
defer delete(png_out)
|
||||
cbz_out := join2(tmp_dir, "out_cbz.cbz")
|
||||
defer delete(cbz_out)
|
||||
|
||||
png_err := adapters.export_comic(png_out, layouts, panel_images, adapters.Export_Options{format = .PNG, page_size = .A4, dpi = 300, quality = 90})
|
||||
testing.expect(t, shared.is_ok(png_err), "png export failed")
|
||||
testing.expect(t, os.exists(png_out), "png archive output should exist")
|
||||
png_data, prerr := os.read_entire_file(png_out, context.temp_allocator)
|
||||
testing.expect(t, prerr == nil, "failed reading png zip")
|
||||
testing.expect(t, len(png_data) >= 2 && string(png_data[:2]) == "PK", "png export should be a zip archive")
|
||||
|
||||
cbz_err := adapters.export_comic(cbz_out, layouts, panel_images, adapters.Export_Options{format = .CBZ, page_size = .A4, dpi = 300, quality = 90})
|
||||
testing.expect(t, shared.is_ok(cbz_err), "cbz export failed")
|
||||
testing.expect(t, os.exists(cbz_out), "cbz output should exist")
|
||||
cbz_data, crerr := os.read_entire_file(cbz_out, context.temp_allocator)
|
||||
testing.expect(t, crerr == nil, "failed reading cbz")
|
||||
testing.expect(t, len(cbz_data) >= 2 && string(cbz_data[:2]) == "PK", "cbz should be a zip archive")
|
||||
}
|
||||
|
||||
@test
|
||||
export_pdf_creates_pdf_file :: proc(t: ^testing.T) {
|
||||
tmp_dir, layouts, panel_images := setup_export_fixture(t)
|
||||
defer os.remove_all(tmp_dir)
|
||||
defer dispose_export_fixture(&layouts, &panel_images)
|
||||
|
||||
pdf_out := join2(tmp_dir, "out.pdf")
|
||||
defer delete(pdf_out)
|
||||
pdf_err := adapters.export_comic(pdf_out, layouts, panel_images, adapters.Export_Options{format = .PDF, page_size = .A4, dpi = 300, quality = 90})
|
||||
testing.expect(t, shared.is_ok(pdf_err), "pdf export failed")
|
||||
testing.expect(t, os.exists(pdf_out), "pdf output should exist")
|
||||
|
||||
data, rerr := os.read_entire_file(pdf_out, context.temp_allocator)
|
||||
testing.expect(t, rerr == nil, "failed to read pdf output")
|
||||
testing.expect(t, strings.has_prefix(string(data), "%PDF-"), "pdf output should start with %PDF-")
|
||||
}
|
||||
@@ -0,0 +1,695 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package tests
|
||||
|
||||
import "core:testing"
|
||||
import "../src/core"
|
||||
import "../src/ui"
|
||||
|
||||
@test
|
||||
core_dispose_state_clears_collections :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
|
||||
sheet_urls_dyn: [dynamic]string
|
||||
append(&sheet_urls_dyn, "x")
|
||||
|
||||
chars_dyn: [dynamic]core.Character
|
||||
append(&chars_dyn, core.Character{name = "A", character_sheet_urls = sheet_urls_dyn[:]})
|
||||
state.characters = chars_dyn[:]
|
||||
|
||||
state.panel_images = make(map[string]core.Panel_Image)
|
||||
state.panel_images["p1"] = core.Panel_Image{url = "u"}
|
||||
|
||||
layout_panels_dyn: [dynamic]core.Page_Layout_Panel
|
||||
append(&layout_panels_dyn, core.Page_Layout_Panel{panel_id = "p1"})
|
||||
layouts_dyn: [dynamic]core.Page_Layout
|
||||
append(&layouts_dyn, core.Page_Layout{panels = layout_panels_dyn[:]})
|
||||
state.page_layouts = layouts_dyn[:]
|
||||
|
||||
state.speech_bubbles = make(map[string][]core.Speech_Bubble)
|
||||
bubbles_dyn: [dynamic]core.Speech_Bubble
|
||||
append(&bubbles_dyn, core.Speech_Bubble{id = "b1"})
|
||||
state.speech_bubbles["p1"] = bubbles_dyn[:]
|
||||
|
||||
steps_dyn: [dynamic]core.Workflow_Step
|
||||
append(&steps_dyn, core.Workflow_Step.Story_Input)
|
||||
state.workflow.completed_steps = steps_dyn[:]
|
||||
|
||||
core.dispose_state(&state)
|
||||
|
||||
testing.expect(t, len(state.characters) == 0, "characters should be cleared")
|
||||
testing.expect(t, len(state.panel_images) == 0, "panel_images should be cleared")
|
||||
testing.expect(t, len(state.page_layouts) == 0, "page_layouts should be cleared")
|
||||
testing.expect(t, len(state.speech_bubbles) == 0, "speech_bubbles should be cleared")
|
||||
testing.expect(t, len(state.workflow.completed_steps) == 0, "completed_steps should be cleared")
|
||||
}
|
||||
|
||||
@test
|
||||
ui_dispose_controller_clears_jobs_and_state :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
state.panel_images = make(map[string]core.Panel_Image)
|
||||
state.panel_images["p1"] = core.Panel_Image{url = "u"}
|
||||
|
||||
controller := ui.new_controller(state)
|
||||
_ = ui.submit_job(&controller.jobs, .Generate_Script, "job")
|
||||
|
||||
ui.dispose_controller(&controller)
|
||||
|
||||
testing.expect(t, len(controller.jobs.jobs) == 0, "jobs should be cleared")
|
||||
testing.expect(t, len(controller.state.panel_images) == 0, "state maps should be cleared")
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tests
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:testing"
|
||||
import "../src/adapters"
|
||||
import "../src/core"
|
||||
import "../src/shared"
|
||||
|
||||
make_temp_project_path :: proc(t: ^testing.T) -> (string, string) {
|
||||
tmp_dir, err := os.make_directory_temp("", "comic-odin-*", context.temp_allocator)
|
||||
if err != nil {
|
||||
testing.expect(t, false, "failed to create temp directory")
|
||||
return "", ""
|
||||
}
|
||||
project_path := fmt.aprintf("%s/project.comic.json", tmp_dir)
|
||||
return tmp_dir, project_path
|
||||
}
|
||||
|
||||
@test
|
||||
storage_save_load_roundtrip :: proc(t: ^testing.T) {
|
||||
tmp_dir, project_path := make_temp_project_path(t)
|
||||
defer os.remove_all(tmp_dir)
|
||||
defer delete(project_path)
|
||||
|
||||
state := core.new_initial_state()
|
||||
state.story_idea = "An inventor discovers a portal"
|
||||
state.story_genre = "scifi"
|
||||
state.user_mode = .Professional
|
||||
|
||||
err := adapters.save_project(project_path, state)
|
||||
testing.expect(t, shared.is_ok(err), "save_project should succeed")
|
||||
|
||||
loaded, lerr := adapters.load_project(project_path)
|
||||
defer core.dispose_state_owned(&loaded)
|
||||
testing.expect(t, shared.is_ok(lerr), "load_project should succeed")
|
||||
testing.expect(t, loaded.story_idea == state.story_idea, "story_idea should round-trip")
|
||||
testing.expect(t, loaded.story_genre == state.story_genre, "story_genre should round-trip")
|
||||
testing.expect(t, loaded.user_mode == state.user_mode, "user_mode should round-trip")
|
||||
}
|
||||
|
||||
@test
|
||||
storage_creates_asset_cache_dir :: proc(t: ^testing.T) {
|
||||
tmp_dir, project_path := make_temp_project_path(t)
|
||||
defer os.remove_all(tmp_dir)
|
||||
defer delete(project_path)
|
||||
|
||||
state := core.new_initial_state()
|
||||
err := adapters.save_project(project_path, state)
|
||||
testing.expect(t, shared.is_ok(err), "save_project should succeed")
|
||||
|
||||
asset_dir, derr := adapters.derive_asset_cache_dir(project_path)
|
||||
testing.expect(t, shared.is_ok(derr), "derive_asset_cache_dir should succeed")
|
||||
testing.expect(t, os.exists(asset_dir), "asset cache directory should exist")
|
||||
testing.expect(t, os.is_dir(asset_dir), "asset cache directory should be a directory")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tests
|
||||
|
||||
import "core:testing"
|
||||
import "../src/core"
|
||||
import "../src/shared"
|
||||
import "../src/ui"
|
||||
|
||||
@test
|
||||
ui_navigation_guard_blocks_locked_screen :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
err := ui.navigate_to_screen(&controller, .Layout)
|
||||
testing.expect(t, !shared.is_ok(err), "layout should be blocked before panels exist")
|
||||
}
|
||||
|
||||
@test
|
||||
ui_navigation_guard_allows_after_prereqs :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
state.panel_images = make(map[string]core.Panel_Image)
|
||||
state.panel_images["p1"] = core.Panel_Image{url = "https://example.com/p1.png", width = 1, height = 1}
|
||||
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
err := ui.navigate_to_screen(&controller, .Layout)
|
||||
testing.expect(t, shared.is_ok(err), "layout should be allowed once panel images exist")
|
||||
}
|
||||
|
||||
@test
|
||||
ui_background_job_lifecycle_and_cancel :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
job1 := ui.start_background_job(&controller, .Generate_Script, "job1")
|
||||
testing.expect(t, controller.state.workflow.is_generating, "generation should be on after starting a job")
|
||||
|
||||
job2 := ui.start_background_job(&controller, .Generate_Panel, "job2")
|
||||
_ = ui.mark_job_running(&controller.jobs, job1)
|
||||
_ = ui.mark_job_running(&controller.jobs, job2)
|
||||
|
||||
cerr := ui.cancel_background_job(&controller, job2)
|
||||
testing.expect(t, shared.is_ok(cerr), "cancel should succeed")
|
||||
|
||||
_ = ui.finish_background_job(&controller, job1, "")
|
||||
testing.expect(t, !controller.state.workflow.is_generating, "generation should be off after active jobs are done")
|
||||
}
|
||||
|
||||
@test
|
||||
ui_workflow_transition_guard :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
|
||||
err := ui.set_workflow_step(&controller, .Layout)
|
||||
testing.expect(t, !shared.is_ok(err), "should block invalid direct transition")
|
||||
|
||||
err2 := ui.set_workflow_step(&controller, .Generating_Script)
|
||||
testing.expect(t, shared.is_ok(err2), "should allow valid transition to generating script")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tests
|
||||
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
import "../src/core"
|
||||
import "../src/shared"
|
||||
import "../src/ui"
|
||||
|
||||
@test
|
||||
ui_render_contains_header_and_status :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
out := ui.render_app_to_string(controller)
|
||||
defer delete(out)
|
||||
|
||||
testing.expect(t, strings.contains(out, "[comic-odin]"), "render should contain app header")
|
||||
testing.expect(t, strings.contains(out, "jobs=0"), "render should show job count")
|
||||
testing.expect(t, strings.contains(out, "Story"), "default screen should be story")
|
||||
}
|
||||
|
||||
@test
|
||||
ui_runtime_apply_commands_flow :: proc(t: ^testing.T) {
|
||||
state := core.new_initial_state()
|
||||
pages_dyn: [dynamic]core.Page
|
||||
append(&pages_dyn, core.Page{page_number = 1})
|
||||
chars_dyn: [dynamic]core.Character
|
||||
append(&chars_dyn, core.Character{name = "A"})
|
||||
state.script = core.Comic_Script{title = "Script", pages = pages_dyn[:], characters = chars_dyn[:]}
|
||||
state.characters = chars_dyn[:]
|
||||
state.panel_images = make(map[string]core.Panel_Image)
|
||||
state.panel_images["p1"] = core.Panel_Image{url = "u", width = 1, height = 1}
|
||||
|
||||
controller := ui.new_controller(state)
|
||||
defer ui.dispose_controller(&controller)
|
||||
cmds := [2]ui.UI_Command{
|
||||
{kind = .Navigate, screen = .Layout},
|
||||
{kind = .Start_Generate, job_type = .Generate_Panel, message = "panel"},
|
||||
}
|
||||
|
||||
err := ui.apply_commands(&controller, cmds[:])
|
||||
testing.expect(t, shared.is_ok(err), "command sequence should succeed")
|
||||
testing.expect(t, controller.active_screen == .Layout, "expected layout screen")
|
||||
testing.expect(t, controller.state.workflow.is_generating, "generation should be active")
|
||||
}
|
||||
Reference in new issue
Block a user