Files
sporel-lib-core.panel-test/init.lua
Calic 07f55c6412 test: v0.3.0 z_tier + persistent + input_block assertions
Add 25 new W.2 assertions covering panel v0.3.0 features:
- persistent flag auto-opens at register-time
- z_tier render-order hud->normal->top
- within-tier open-order preserved
- input_block='none' passes through (no handler call)
- input_block='self' hit-tests within bounds
- input_block='all' swallows outside-clicks (modal block)
- persistent + close() no-op when focused is persistent
- close(id) explicit DOES close persistent
- 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

Total: 79 assertions pass. Manifest bumped to 0.3.0 + panel-dep to 0.3.0.
2026-06-15 02:25:52 +02:00

556 lines
24 KiB
Lua

-- 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,
-- 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).
local panel = require("lib-core.panel")
local T = engine.test
local M = {}
-- -----------------------------------------------------------------------
-- Shared dummy widget factories
-- -----------------------------------------------------------------------
local function make_widget(overrides)
local w = {
title = "Test Widget",
render = function(_ctx) end,
handle_input = function(_ctx, _event) end,
}
if overrides then
for k, v in pairs(overrides) do w[k] = v end
end
return w
end
-- -----------------------------------------------------------------------
-- Test suite
-- -----------------------------------------------------------------------
function M.run_tests(_ctx)
-- ==================================================================
-- v0.1.1 baseline tests (bw-compat verification)
-- ==================================================================
-- ------------------------------------------------------------------
-- 1. register + open / close round-trip
-- ------------------------------------------------------------------
panel._test_reset_all()
panel.register("w1", make_widget())
panel.open("w1")
T.assert(panel.is_open(), "register + open makes is_open() true")
-- ------------------------------------------------------------------
-- 2. close after open works
-- ------------------------------------------------------------------
panel.close()
T.assert(not panel.is_open(), "close() after open makes is_open() false")
-- ------------------------------------------------------------------
-- 3. toggle twice → opens then closes
-- ------------------------------------------------------------------
panel.toggle("w1")
T.assert(panel.is_open(), "toggle (first) opens panel")
panel.toggle("w1")
T.assert(not panel.is_open(), "toggle (second) closes panel")
-- ------------------------------------------------------------------
-- 4. open("unknown_id") → loud-error
-- ------------------------------------------------------------------
local ok_unknown = pcall(panel.open, "does_not_exist")
T.assert(not ok_unknown,
"panel.open with unknown widget_id raises an error")
-- ------------------------------------------------------------------
-- 5. set_theme with valid override + get_theme reads merged
-- ------------------------------------------------------------------
panel.set_theme({ padding = 16 })
local t = panel.get_theme()
T.equals(t.padding, 16, "set_theme override is visible via get_theme")
-- Restore for subsequent tests
panel.set_theme({ padding = 8 })
-- ------------------------------------------------------------------
-- 6. set_theme with unknown key → loud-error
-- ------------------------------------------------------------------
local ok_bad_key = pcall(panel.set_theme, { nonexistent_key = 99 })
T.assert(not ok_bad_key,
"set_theme with unknown key raises an error")
-- ------------------------------------------------------------------
-- 7. register without render function → loud-error
-- ------------------------------------------------------------------
local ok_no_render = pcall(panel.register, "w_bad", {
title = "Bad",
handle_input = function() end,
-- render intentionally missing
})
T.assert(not ok_no_render,
"panel.register without render function raises an error")
-- ------------------------------------------------------------------
-- 8. pause_on_open=true widget makes is_pausing()=true; close → false
-- ------------------------------------------------------------------
panel.register("w_pause", make_widget({ pause_on_open = true }))
panel.open("w_pause")
T.assert(panel.is_pausing(),
"is_pausing() is true when active widget has pause_on_open=true")
panel.close()
T.assert(not panel.is_pausing(),
"is_pausing() is false after close()")
-- ------------------------------------------------------------------
-- 9. show_context_menu + simulated click on action row → callback invoked
-- ------------------------------------------------------------------
-- Need an active widget for context-menu dispatch path
panel.open("w1")
local callback_fired = false
-- The context-menu renders at a known position; compute the click coords:
-- show_context_menu is called at (100, 100). With padding=8, row_height=24:
-- row 1 spans y=100..(100+24). Use x=120, y=110 (inside row 1).
panel.show_context_menu(100, 100, {
{
label = "Do Thing",
callback = function(_info)
callback_fired = true
end,
},
})
-- Simulate a left-click inside the first action row
panel._dispatch_event_for_test({
kind = "click",
x = 120,
y = 110,
button = "left",
})
T.assert(callback_fired,
"context-menu action callback is invoked on click inside its row")
-- ------------------------------------------------------------------
-- 10. non-pause widget: is_pausing() = false
-- ------------------------------------------------------------------
panel.close() -- close w1 from test 9
panel.open("w1") -- w1 has no pause_on_open
T.assert(not panel.is_pausing(),
"is_pausing() is false for widget without pause_on_open")
-- Cleanup
panel.close()
panel.unregister("w1")
panel.unregister("w_pause")
-- ==================================================================
-- v0.2.0 — Multi-Active + Layout-Slots + Bw-Compat-Shim
-- ==================================================================
-- ------------------------------------------------------------------
-- W.0 #1: bw-compat register(id, widget) without opts uses center default
-- ------------------------------------------------------------------
panel._test_reset_all()
panel.register("w_bc1", make_widget({ title = "BC" }))
panel.open("w_bc1")
T.assert(panel.is_open(), "is_open() any-open bw-compat")
T.assert(panel.is_open("w_bc1"), "is_open(id) per-id")
local bounds_bc = panel._test_get_window_bounds("w_bc1")
T.assert(bounds_bc ~= nil, "bounds resolved for open window")
-- center slot: x = sw*0.25, w = sw*0.50, h = sh*0.60
T.assert(bounds_bc.x > 0, "default center slot x > 0")
T.assert(bounds_bc.w > 0, "default center slot w > 0")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #2: multi-active open(A) + open(B) both stay open
-- ------------------------------------------------------------------
panel.register("w_mA", make_widget({ title = "A" }))
panel.register("w_mB", make_widget({ title = "B" }))
panel.open("w_mA")
panel.open("w_mB")
T.assert(panel.is_open("w_mA"), "A still open after opening B")
T.assert(panel.is_open("w_mB"), "B is open")
local open_ids = panel._test_get_open_ids()
T.equals(#open_ids, 2, "two windows open")
T.equals(open_ids[1], "w_mA", "A first in open_order")
T.equals(open_ids[2], "w_mB", "B second in open_order")
T.equals(panel._test_get_focused_id(), "w_mB",
"B (last-opened) is focused")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #3: close(id) per-id closes only that one
-- ------------------------------------------------------------------
panel.register("w_cA", make_widget({ title = "A" }))
panel.register("w_cB", make_widget({ title = "B" }))
panel.open("w_cA")
panel.open("w_cB")
panel.close("w_cA")
T.assert(not panel.is_open("w_cA"), "A closed by per-id close")
T.assert(panel.is_open("w_cB"), "B still open after per-id close(A)")
T.equals(panel._test_get_focused_id(), "w_cB",
"B is focused after A closed")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #4: close() bw-compat closes the focused (last-opened)
-- ------------------------------------------------------------------
panel.register("w_fA", make_widget({ title = "A" }))
panel.register("w_fB", make_widget({ title = "B" }))
panel.open("w_fA")
panel.open("w_fB")
panel.close() -- no arg
T.assert(not panel.is_open("w_fB"), "B (focused) closed by close()")
T.assert(panel.is_open("w_fA"), "A still open after close() (bw-compat)")
T.equals(panel._test_get_focused_id(), "w_fA",
"A is now focused after focused-close")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #5: is_open() bw-compat = any-open
-- ------------------------------------------------------------------
panel.register("w_io", make_widget({ title = "X" }))
T.assert(not panel.is_open(), "is_open() false when nothing open")
panel.open("w_io")
T.assert(panel.is_open(), "is_open() true after open (bw-compat any)")
panel.close()
T.assert(not panel.is_open(), "is_open() false after close")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #6: layout left-half resolves correctly
-- ------------------------------------------------------------------
panel.register("w_lh", make_widget({ title = "L" }), { layout = "left-half" })
panel.open("w_lh")
local b_lh = panel._test_get_window_bounds("w_lh")
local sw_lh, sh_lh = panel._test_get_screen_size()
T.assert(b_lh.x == 0, "left-half x = 0")
T.assert(math.abs(b_lh.w - sw_lh * 0.5) < 0.01, "left-half w = sw/2")
T.assert(math.abs(b_lh.h - sh_lh) < 0.01, "left-half h = sh")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #7: layout right-half resolves correctly
-- ------------------------------------------------------------------
panel.register("w_rh", make_widget({ title = "R" }), { layout = "right-half" })
panel.open("w_rh")
local b_rh = panel._test_get_window_bounds("w_rh")
local sw_rh = panel._test_get_screen_size()
T.assert(math.abs(b_rh.x - sw_rh * 0.5) < 0.01, "right-half x = sw/2")
T.assert(math.abs(b_rh.w - sw_rh * 0.5) < 0.01, "right-half w = sw/2")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #8: layout center default
-- ------------------------------------------------------------------
panel.register("w_c", make_widget({ title = "C" })) -- no opts = center
panel.open("w_c")
local b_c = panel._test_get_window_bounds("w_c")
local sw_c, sh_c = panel._test_get_screen_size()
T.assert(math.abs(b_c.x - sw_c * 0.25) < 0.01, "center x = sw*0.25")
T.assert(math.abs(b_c.y - sh_c * 0.20) < 0.01, "center y = sh*0.20")
T.assert(math.abs(b_c.w - sw_c * 0.50) < 0.01, "center w = sw*0.50")
T.assert(math.abs(b_c.h - sh_c * 0.60) < 0.01, "center h = sh*0.60")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #9: layout custom function
-- ------------------------------------------------------------------
panel.register("w_cu", make_widget({ title = "Cu" }), {
layout = function(_sw, _sh)
return { x = 10, y = 20, w = 100, h = 50 }
end,
})
panel.open("w_cu")
local b_cu = panel._test_get_window_bounds("w_cu")
T.equals(b_cu.x, 10, "custom layout x = 10")
T.equals(b_cu.y, 20, "custom layout y = 20")
T.equals(b_cu.w, 100, "custom layout w = 100")
T.equals(b_cu.h, 50, "custom layout h = 50")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #10: Loud-Error on unknown layout-slot
-- ------------------------------------------------------------------
local ok_bog, err_bog = pcall(panel.register, "w_bog",
make_widget({ title = "X" }), { layout = "bogus" })
T.assert(not ok_bog, "register with unknown layout slot errors")
T.assert(err_bog ~= nil and string.find(err_bog, "panel%.register") ~= nil,
"error message mentions panel.register")
T.assert(err_bog ~= nil and string.find(err_bog, "bogus") ~= nil,
"error message mentions the bad slot name")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #11: hit-test reverse-open-order
-- ------------------------------------------------------------------
local captured_a, captured_b = nil, nil
local w_hA = make_widget({
title = "A",
handle_input = function(_ctx, event) captured_a = event end,
})
local w_hB = make_widget({
title = "B",
handle_input = function(_ctx, event) captured_b = event end,
})
panel.register("w_hA", w_hA, { layout = "left-half" })
panel.register("w_hB", w_hB, { layout = "right-half" })
panel.open("w_hA")
panel.open("w_hB")
local sw_h, sh_h = panel._test_get_screen_size()
-- Click in left half: hits A only (B's bounds are right-half so click misses)
panel._dispatch_event_for_test({
kind = "click", button = "left",
x = sw_h * 0.25, y = sh_h * 0.5,
})
T.assert(captured_a ~= nil, "A captured click in its left-half bounds")
T.assert(captured_b == nil, "B did not capture click outside its bounds")
-- Reset captures, click in right half
captured_a, captured_b = nil, nil
panel._dispatch_event_for_test({
kind = "click", button = "left",
x = sw_h * 0.75, y = sh_h * 0.5,
})
T.assert(captured_b ~= nil, "B captured click in its right-half bounds")
T.assert(captured_a == nil, "A did not capture click outside its bounds")
panel._test_reset_all()
-- ------------------------------------------------------------------
-- W.0 #12: is_pausing() checks ALL open windows, not just focused
-- ------------------------------------------------------------------
panel.register("w_np", make_widget({ title = "NP" })) -- non-pausing
panel.register("w_p", make_widget({ title = "P", pause_on_open = true }))
panel.open("w_p")
panel.open("w_np") -- focused = non-pausing
T.assert(panel.is_pausing(),
"is_pausing() true when a non-focused open window has pause_on_open")
panel.close("w_p")
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