diff --git a/README.md b/README.md index 835b963..50baad4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ # lib-core.panel-test -Test-module for `lib-core.panel`. 10 assertions covering registry lifecycle, -theme validation, error handling, pause-gate, and context-menu dispatch. +Test-module for `lib-core.panel`. v0.3.0 expands to 79 assertions: +- v0.1.x: registry lifecycle, theme validation, error handling, + pause-gate, context-menu dispatch. +- v0.2.0 (W.0): multi-active model, 11 layout-slots, custom-fn layout, + hit-test reverse-open-order, bw-compat shim. +- v0.3.0 (W.2): z-tiers (hud/normal/top), persistent windows, + input_block routing (none/self/all), default-input_block per tier, + panel.point_in_any_panel public API. -**Version:** 0.1.0 +**Version:** 0.3.0 **Lib-ID:** lib-core.panel-test -**Requires:** lib-core.panel v0.1.0 -**Tags:** test, panel, ui +**Requires:** lib-core.panel v0.3.0 +**Tags:** test, panel, ui, window-manager ## Running @@ -17,10 +23,11 @@ SPOREL_LIBS_DIR=~/Projects/Sporel/sporel-libs SPOREL_CI=1 SPOREL_LOG_STDERR=1 \ ./build/Sporel.exe --test-libs=lib-core.panel-test 2>&1 | tail -15 ``` -Expected: `pass=10 fail=0`. +Expected: `pass=79 fail=0`. -## Assertion Summary +## Assertion Summary (v0.3.0) +**v0.1.x baseline (10 assertions):** 1. register + open / close round-trip 2. close after open works 3. toggle twice opens then closes @@ -31,3 +38,23 @@ Expected: `pass=10 fail=0`. 8. pause_on_open=true widget makes is_pausing() true; close makes it false 9. context-menu action callback invoked on click inside its row 10. non-pause widget: is_pausing() is false + +**v0.2.0 W.0 multi-active + layout-slots (~44 assertions):** bw-compat +shim, multi-active open(A)+open(B), per-id close, focused-close, +layout-slot resolution (left-half, right-half, center, custom fn), +loud-error on unknown slot, hit-test reverse-open-order, is_pausing +across all open windows. + +**v0.3.0 W.2 z-tiers + persistent + input-block (25 assertions):** +- persistent flag auto-opens at register-time + is in open_order +- z_tier render-order is hud → normal → top +- within z_tier, open-order preserved +- input_block="none" passes through (no handler call) +- input_block="self" hit-tests within bounds (inside vs outside) +- input_block="all" swallows outside-clicks (modal block) +- persistent + close() no-op when focused is persistent +- close(id) explicit DOES close persistent (modder-controlled) +- Loud-Error on unknown z_tier +- Loud-Error on unknown input_block +- default input_block per tier (hud=none, normal=self, top=all) +- point_in_any_panel public API (inside / outside / none open) diff --git a/init.lua b/init.lua index d66013b..c5a1661 100644 --- a/init.lua +++ b/init.lua @@ -1,6 +1,8 @@ --- lib-core.panel-test v0.2.0 — Panel test suite +-- lib-core.panel-test v0.3.0 — Panel test suite -- Exercises: registry lifecycle, theme, errors, pause-gate, context-menu --- dispatch, multi-active model, layout-slots, hit-test reverse-order. +-- dispatch, multi-active model, layout-slots, hit-test reverse-order, +-- z_tiers, persistent windows, input_block routing, +-- panel.point_in_any_panel. -- Render paths are NOT tested here: engine.render.* is blocked in test-mode -- (assert_in_render_phase guard in engine_lua_render.c). Input dispatch is -- exercised via panel._dispatch_event_for_test(event). @@ -335,6 +337,219 @@ function M.run_tests(_ctx) T.assert(not panel.is_pausing(), "is_pausing() false after closing the pausing window (np still open)") panel._test_reset_all() + + -- ================================================================== + -- W.2 v0.3.0 — Z-Tiers + Persistent + Input-Block + -- ================================================================== + + -- ------------------------------------------------------------------ + -- W.2 #1: persistent flag auto-opens at register-time + -- ------------------------------------------------------------------ + panel.register("w_persist", make_widget({ title = "P" }), + { persistent = true, layout = "top" }) + T.assert(panel.is_open("w_persist"), + "W.2 persistent=true makes widget open immediately after register") + local persist_ids = panel._test_get_open_ids() + T.equals(persist_ids[1], "w_persist", + "W.2 persistent widget is in open_order") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #2: z_tier render-order is hud → normal → top + -- ------------------------------------------------------------------ + -- Custom render that records call-order via test backdoor. We can't + -- exercise render() directly (engine.render.* blocked in test-mode); + -- instead we verify the per-tier iteration logic via the test backdoor + -- _test_get_open_ids and confirm windows persist across z_tiers. + -- For the actual render-order claim, panel._test_get_render_order() + -- is added in W.2 (returns the would-be iteration sequence). + panel.register("w_hud", make_widget({ title = "Hud" }), + { z_tier = "hud", layout = "bottom" }) + panel.register("w_n", make_widget({ title = "Normal" }), + { z_tier = "normal", layout = "left" }) + panel.register("w_top", make_widget({ title = "Top" }), + { z_tier = "top", layout = "center" }) + panel.open("w_hud") + panel.open("w_n") + panel.open("w_top") + local render_order = panel._test_get_render_order() + T.equals(render_order[1], "w_hud", + "W.2 hud-tier widget renders first") + T.equals(render_order[2], "w_n", + "W.2 normal-tier widget renders second") + T.equals(render_order[3], "w_top", + "W.2 top-tier widget renders third") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #3: within z_tier, open-order is preserved in render + -- ------------------------------------------------------------------ + panel.register("w_n1", make_widget({ title = "N1" }), + { z_tier = "normal", layout = "left-half" }) + panel.register("w_n2", make_widget({ title = "N2" }), + { z_tier = "normal", layout = "right-half" }) + panel.open("w_n1") + panel.open("w_n2") + local ro2 = panel._test_get_render_order() + T.equals(ro2[1], "w_n1", + "W.2 within same tier: earlier-opened renders first") + T.equals(ro2[2], "w_n2", + "W.2 within same tier: later-opened renders second (= on top)") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #4: input_block="none" passes through (no handler call) + -- ------------------------------------------------------------------ + local n_calls = 0 + panel.register("w_pass", make_widget({ + title = "Pass", + handle_input = function(_ctx, _ev) n_calls = n_calls + 1 end, + }), { + z_tier = "hud", + input_block = "none", + layout = function(_sw, _sh) return { x = 0, y = 0, w = 1280, h = 720 } end, + }) + -- HUD covers the entire screen; despite click landing inside, the + -- "none" input_block skips hit-test entirely. + panel._dispatch_event_for_test({ + kind = "click", button = "left", x = 100, y = 100, + }) + T.equals(n_calls, 0, + "W.2 input_block='none' skips hit-test (handler not invoked)") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #5: input_block="self" hit-tests within bounds + -- ------------------------------------------------------------------ + local hit_self = nil + panel.register("w_self", make_widget({ + title = "Self", + handle_input = function(_ctx, _ev) hit_self = "yes" end, + }), { + z_tier = "normal", + input_block = "self", + layout = "left-half", + }) + panel.open("w_self") + local sw_s, sh_s = panel._test_get_screen_size() + -- Click within left-half → hit + panel._dispatch_event_for_test({ + kind = "click", button = "left", + x = sw_s * 0.25, y = sh_s * 0.5, + }) + T.equals(hit_self, "yes", + "W.2 input_block='self' routes click inside bounds to widget") + -- Click in right-half → miss; widget should NOT see the event + hit_self = nil + panel._dispatch_event_for_test({ + kind = "click", button = "left", + x = sw_s * 0.75, y = sh_s * 0.5, + }) + T.assert(hit_self == nil, + "W.2 input_block='self' does NOT route click outside bounds") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #6: input_block="all" swallows outside-clicks (modal) + -- ------------------------------------------------------------------ + local hit_modal = false + panel.register("w_modal", make_widget({ + title = "Modal", + handle_input = function(_ctx, _ev) hit_modal = true end, + }), { + z_tier = "top", + input_block = "all", + layout = "center", + }) + panel.open("w_modal") + -- Click outside the center (at 0,0) — modal swallows; handler not invoked. + panel._dispatch_event_for_test({ + kind = "click", button = "left", x = 0, y = 0, + }) + T.assert(not hit_modal, + "W.2 input_block='all' outside-click does NOT reach handler") + -- Sanity: click inside DOES reach the handler + hit_modal = false + local sw_m, sh_m = panel._test_get_screen_size() + panel._dispatch_event_for_test({ + kind = "click", button = "left", + x = sw_m * 0.5, y = sh_m * 0.5, + }) + T.assert(hit_modal, + "W.2 input_block='all' inside-click DOES reach handler") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #7: persistent + close() no-op when focused is persistent + -- ------------------------------------------------------------------ + panel.register("w_pe", make_widget({ title = "Pe" }), + { persistent = true, layout = "left" }) + T.assert(panel.is_open("w_pe"), + "W.2 persistent open at register-time") + panel.close() -- bw-compat no-arg close: should be NO-OP for persistent + T.assert(panel.is_open("w_pe"), + "W.2 persistent: close() (no arg) is no-op when focused is persistent") + panel.close("w_pe") -- explicit per-id close: DOES close persistent + T.assert(not panel.is_open("w_pe"), + "W.2 persistent: close(id) explicit DOES close persistent") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #8: Loud-Error on unknown z_tier + -- ------------------------------------------------------------------ + local ok_zt, err_zt = pcall(panel.register, "w_zt", + make_widget({ title = "X" }), { z_tier = "bogus" }) + T.assert(not ok_zt, + "W.2 register with unknown z_tier raises an error") + T.assert(err_zt ~= nil and string.find(err_zt, "z_tier") ~= nil, + "W.2 unknown-z_tier error message mentions 'z_tier'") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #9: Loud-Error on unknown input_block + -- ------------------------------------------------------------------ + local ok_ib, err_ib = pcall(panel.register, "w_ib", + make_widget({ title = "X" }), { input_block = "modal" }) + T.assert(not ok_ib, + "W.2 register with unknown input_block raises an error") + T.assert(err_ib ~= nil and string.find(err_ib, "input_block") ~= nil, + "W.2 unknown-input_block error message mentions 'input_block'") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #10: default input_block per tier (hud=none, normal=self, top=all) + -- ------------------------------------------------------------------ + panel.register("w_h_def", make_widget({ title = "H" }), + { z_tier = "hud", layout = "top" }) + panel.register("w_n_def", make_widget({ title = "N" }), + { z_tier = "normal", layout = "bottom" }) + panel.register("w_t_def", make_widget({ title = "T" }), + { z_tier = "top", layout = "center" }) + panel.open("w_h_def") + panel.open("w_n_def") + panel.open("w_t_def") + T.equals(panel._test_get_input_block("w_h_def"), "none", + "W.2 hud default input_block = 'none'") + T.equals(panel._test_get_input_block("w_n_def"), "self", + "W.2 normal default input_block = 'self'") + T.equals(panel._test_get_input_block("w_t_def"), "all", + "W.2 top default input_block = 'all'") + panel._test_reset_all() + + -- ------------------------------------------------------------------ + -- W.2 #11: point_in_any_panel public API + -- ------------------------------------------------------------------ + T.assert(not panel.point_in_any_panel(100, 100), + "W.2 point_in_any_panel false when nothing open") + panel.register("w_pia", make_widget({ title = "PIA" }), + { layout = "left-half" }) + panel.open("w_pia") + local sw_p, sh_p = panel._test_get_screen_size() + T.assert(panel.point_in_any_panel(sw_p * 0.25, sh_p * 0.5), + "W.2 point_in_any_panel true when point inside open panel") + T.assert(not panel.point_in_any_panel(sw_p * 0.75, sh_p * 0.5), + "W.2 point_in_any_panel false when point outside any panel") + panel._test_reset_all() end return M diff --git a/manifest.lib b/manifest.lib index 1d70dc1..67ec5be 100644 --- a/manifest.lib +++ b/manifest.lib @@ -1,9 +1,9 @@ { "id": "lib-core.panel-test", "role": "test", - "version": "0.2.0", + "version": "0.3.0", "api_min": "0.1", "deps": [ - {"id": "lib-core.panel", "version": "0.2.0"} + {"id": "lib-core.panel", "version": "0.3.0"} ] }