-- lib-core.panel-test v0.2.0 — Panel test suite -- Exercises: registry lifecycle, theme, errors, pause-gate, context-menu -- dispatch, multi-active model, layout-slots, hit-test reverse-order. -- 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() end return M