-- lib-core.crafting-display-test — v0.2.0 -- 12 assertions: 9 v0.1 existing (widget creation, action lifecycle, -- default-empty, row visibility, per-row availability, resolver -- overrides, panel widget-contract) + 3 v0.2 new (Form-1 bw-compat -- entity-handle, Form-2 locale-factory with multi-source aggregate -- availability, ctx_inner.locale + container alias propagation). -- Pattern follows P.2.4 Test-Module-Pattern; TAP output via engine.test.* local composition = require("lib-core.composition") local inventory = require("lib-core.inventory-list") local crafting = require("lib-core.crafting") local display = require("lib-core.crafting-display") local panel = require("lib-core.panel") local T = engine.test local M = {} -- --------------------------------------------------------------------- -- Template definitions (one-time, shared across tests). -- composition does not expose a clear-all, so we never re-define. -- All template-ids namespaced to this test lib. -- --------------------------------------------------------------------- composition.define_template{ id = "craft_display_test.container", properties = { sprite_path = "sprites/bag.png", position = {x = 0, y = 0}, }, tags = {"renderable"}, container = { kind = "list" }, } composition.define_template{ id = "craft_display_test.rock", properties = { stack_mode = "individual", sprite_path = "sprites/rock.png", position = {x = 0, y = 0}, }, tags = {"renderable", "item"}, } composition.define_template{ id = "craft_display_test.stick", properties = { stack_mode = "individual", sprite_path = "sprites/stick.png", position = {x = 0, y = 0}, }, tags = {"renderable", "item"}, } composition.define_template{ id = "craft_display_test.rock_pick", properties = { stack_mode = "individual", sprite_path = "sprites/pick.png", position = {x = 0, y = 0}, }, tags = {"renderable", "item"}, } -- --------------------------------------------------------------------- -- Fixture helpers -- --------------------------------------------------------------------- local function setup_container(items) crafting._test_clear_all() local container = composition.create{ template = "craft_display_test.container" } for _, spec in ipairs(items) do for _ = 1, spec.count do local e = composition.create{ template = spec.template } inventory.add(container, e) end end return container end -- fresh_container builds a populated container without touching the -- recipe-registry. Used in §11 to create a second container after -- setup_container has already been called for the first one (so the -- multi-source locale test has two distinct containers without losing -- the just-defined recipe). local function fresh_container(items) local container = composition.create{ template = "craft_display_test.container" } for _, spec in ipairs(items) do for _ = 1, spec.count do local e = composition.create{ template = spec.template } inventory.add(container, e) end end return container end -- --------------------------------------------------------------------- -- Tests -- --------------------------------------------------------------------- function M.run_tests(_ctx) -- ================================================================ -- §1: create(container, opts) returns widget_def with render, -- handle_input, title. -- ================================================================ crafting._test_clear_all() local container1 = composition.create{ template = "craft_display_test.container" } local widget1 = display.create(container1, { title = "Workbench" }) T.assert(widget1 ~= nil and type(widget1.render) == "function" and type(widget1.handle_input) == "function" and widget1.title == "Workbench", "1. create returns widget_def with render, handle_input, title set from opts") -- ================================================================ -- §2: register_action + unregister_action round-trip (count 0 -> 1 -> 0). -- ================================================================ crafting._test_clear_all() local container2 = composition.create{ template = "craft_display_test.container" } local widget2 = display.create(container2) local count_before = #widget2._actions display.register_action(widget2, "Craft", function(_rid, _ctx) end) local count_after_reg = #widget2._actions display.unregister_action(widget2, "Craft") local count_after_unreg = #widget2._actions T.assert(count_before == 0 and count_after_reg == 1 and count_after_unreg == 0, "2. register_action + unregister_action round-trip: count goes 0 -> 1 -> 0") -- ================================================================ -- §3: Duplicate label -> Loud-Error matching -- 'crafting%-display.register_action.*duplicate'. -- ================================================================ crafting._test_clear_all() local container3 = composition.create{ template = "craft_display_test.container" } local widget3 = display.create(container3) display.register_action(widget3, "Craft", function() end) local ok3, err3 = pcall(display.register_action, widget3, "Craft", function() end) T.assert(not ok3 and type(err3) == "string" and string.find(err3, "crafting%-display%.register_action") ~= nil and string.find(err3, "duplicate") ~= nil, "3. duplicate label -> Loud-Error matching 'crafting-display.register_action.*duplicate'") -- ================================================================ -- §4: Non-function callback -> Loud-Error matching -- 'crafting%-display.register_action.*callback'. -- ================================================================ crafting._test_clear_all() local container4 = composition.create{ template = "craft_display_test.container" } local widget4 = display.create(container4) local ok4, err4 = pcall(display.register_action, widget4, "Craft", "not_a_function") T.assert(not ok4 and type(err4) == "string" and string.find(err4, "crafting%-display%.register_action") ~= nil and string.find(err4, "callback") ~= nil, "4. non-function callback -> Loud-Error matching 'crafting-display.register_action.*callback'") -- ================================================================ -- §5: Default action-set is empty (count=0 directly after create). -- ================================================================ crafting._test_clear_all() local container5 = composition.create{ template = "craft_display_test.container" } local widget5 = display.create(container5) T.assert(#widget5._actions == 0, "5. default action-set is empty directly after create") -- ================================================================ -- §6: Rows filtered via is_known. Two recipes — one with -- is_known=function()->false → only the visible one appears. -- ================================================================ local container6 = setup_container{} crafting.define_recipe{ id = "visible_r6", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Visible Recipe", } crafting.define_recipe{ id = "hidden_r6", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Hidden Recipe", is_known = function(_) return false end, } local widget6 = display.create(container6) local rows6 = display._test_get_rows(widget6) local found_visible = false local found_hidden = false for _, row in ipairs(rows6) do if row.recipe.id == "visible_r6" then found_visible = true end if row.recipe.id == "hidden_r6" then found_hidden = true end end T.assert(#rows6 == 1 and found_visible and not found_hidden, "6. rows filtered via is_known: only known recipes appear in row-list") -- ================================================================ -- §7: Per-row availability flag matches can_craft.ok. -- Container with 1x rock → recipe needing rock available=true, -- recipe needing stick (missing) available=false. -- ================================================================ local container7 = setup_container{ { template = "craft_display_test.rock", count = 1 }, } crafting.define_recipe{ id = "have_rock_r7", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Rock Recipe", } crafting.define_recipe{ id = "need_stick_r7", inputs = { { template = "craft_display_test.stick", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Stick Recipe", } local widget7 = display.create(container7) local rows7 = display._test_get_rows(widget7) local available_map = {} for _, row in ipairs(rows7) do available_map[row.recipe.id] = row.available end T.assert(#rows7 == 2 and available_map["have_rock_r7"] == true and available_map["need_stick_r7"] == false, "7. per-row availability flag matches can_craft.ok") -- ================================================================ -- §8: Resolver overrides invoked after set_icon/label/summary_resolver. -- ================================================================ local container8 = setup_container{} crafting.define_recipe{ id = "resolver_r8", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Default Name", } local widget8 = display.create(container8) display.set_icon_resolver(widget8, function(_r) return "ICON_X" end) display.set_label_resolver(widget8, function(_r) return "LABEL_X" end) display.set_summary_resolver(widget8, function(_r) return "SUMMARY_X" end) local recipe8 = crafting.get_recipe("resolver_r8") local icon8 = display._test_resolve_icon(widget8, recipe8) local label8 = display._test_resolve_label(widget8, recipe8) local summary8 = display._test_resolve_summary(widget8, recipe8) T.assert(icon8 == "ICON_X" and label8 == "LABEL_X" and summary8 == "SUMMARY_X", "8. resolver overrides (icon/label/summary) invoked after set_*_resolver") -- ================================================================ -- §9: widget.render(ctx) with a panel-shaped ctx populates -- widget._render_rows. Drives the panel widget contract: -- render(ctx) where ctx = {bounds, theme, is_focused}. Catches -- contract-drift between this lib and lib-core.panel. -- ================================================================ local container9 = setup_container{} crafting.define_recipe{ id = "render_r9", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Render Recipe", } local widget9 = display.create(container9, { title = "C", widget_id = "cw" }) local ctx9 = { bounds = { x = 0, y = 0, w = 400, h = 300 }, theme = { row_height = 28, padding = 8, text_color = 0xFFFFFFFF, text_color_dim = 0x999999FF, font_size_body = 14, }, is_focused = false, } -- engine.render.draw_text is registered but errors in test_mode (no -- OpenGL context); stub it for the duration of this render call so -- the layout pass runs to completion. local saved_draw_text = engine.render.draw_text engine.render.draw_text = function() end local render_ok, render_err = pcall(widget9.render, ctx9) engine.render.draw_text = saved_draw_text T.assert(render_ok and widget9._render_rows ~= nil and #widget9._render_rows == 1 and widget9._render_rows[1].recipe_id == "render_r9", "9. widget.render(ctx) per panel widget contract populates _render_rows" .. (render_ok and "" or (" (render errored: " .. tostring(render_err) .. ")"))) -- ================================================================ -- §10 (v0.2): Form-1 bw-compat — create(container, opts) with bare -- handle. _test_get_locale returns the same handle; _test_get_rows -- still works (single-source availability via Form-1 shim). -- ================================================================ local container10 = setup_container{ { template = "craft_display_test.rock", count = 1 }, } crafting.define_recipe{ id = "form1_r10", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Form-1 Recipe", } local widget10 = display.create(container10) local locale10 = display._test_get_locale(widget10) local rows10 = display._test_get_rows(widget10) T.assert(locale10 == container10 and #rows10 == 1 and rows10[1].recipe.id == "form1_r10" and rows10[1].available == true, "10. Form-1 bw-compat: _test_get_locale returns bare handle; " .. "rows resolve via Form-1 shim with correct availability") -- ================================================================ -- §11 (v0.2): Form-2 locale-factory — create(function() -> locale). -- locale = {sources={c1,c2}, sink=c1}. Recipe needs rock + stick; -- rock lives in c1, stick in c2. Aggregate availability across -- sources => row.available == true. -- ================================================================ -- Re-initialise registry, define recipe first so both containers -- can be created after via fresh_container without recipe loss. crafting._test_clear_all() crafting.define_recipe{ id = "form2_r11", inputs = { { template = "craft_display_test.rock", count = 1 }, { template = "craft_display_test.stick", count = 1 }, }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Form-2 Recipe", } local c11_a = fresh_container{ { template = "craft_display_test.rock", count = 1 }, } local c11_b = fresh_container{ { template = "craft_display_test.stick", count = 1 }, } local locale_table11 = { sources = { c11_a, c11_b }, sink = c11_a } local widget11 = display.create(function() return locale_table11 end) local got_locale11 = display._test_get_locale(widget11) local rows11 = display._test_get_rows(widget11) T.assert(type(got_locale11) == "table" and got_locale11.sources ~= nil and #got_locale11.sources == 2 and got_locale11.sources[1] == c11_a and got_locale11.sources[2] == c11_b and got_locale11.sink == c11_a and #rows11 == 1 and rows11[1].recipe.id == "form2_r11" and rows11[1].available == true, "11. Form-2 locale-factory: _test_get_locale returns the locale " .. "table; aggregate availability across sources is true") -- ================================================================ -- §12 (v0.2): ctx_inner.locale + container alias propagation. -- -- The panel.show_context_menu callback path is hard to fire in -- headless test mode (no real click events + no panel-spy hook). -- Pragmatic substitute: monkey-patch panel.show_context_menu to -- capture the entries-table, then invoke the first entry's -- callback directly with a dummy menu_ctx. The captured ctx_inner -- must carry locale + the sink-alias. -- -- Covers both Form-1 (locale == container, alias == container) -- and Form-2 (locale == table, alias == locale.sink). -- ================================================================ crafting._test_clear_all() crafting.define_recipe{ id = "alias_r12", inputs = { { template = "craft_display_test.rock", count = 1 } }, output = { template = "craft_display_test.rock_pick", count = 1 }, name = "Alias Recipe", } local saved_show = panel.show_context_menu local captured_entries panel.show_context_menu = function(_x, _y, entries) captured_entries = entries end -- Form-1 path local container12a = fresh_container{ { template = "craft_display_test.rock", count = 1 }, } local widget12a = display.create(container12a) display.register_action(widget12a, "Craft", function(rid, ctx_inner) widget12a._captured = { rid = rid, ctx_inner = ctx_inner } end) display._invoke_context_menu(widget12a, "alias_r12", 0, 0) local f1_ok = false if captured_entries and #captured_entries == 1 then captured_entries[1].callback({ close_menu = function() end }) local cap = widget12a._captured f1_ok = cap ~= nil and cap.rid == "alias_r12" and cap.ctx_inner.locale == container12a and cap.ctx_inner.container == container12a end -- Form-2 path captured_entries = nil local c12b_src1 = fresh_container{ { template = "craft_display_test.rock", count = 1 }, } local c12b_src2 = fresh_container{} local locale_table12 = { sources = { c12b_src1, c12b_src2 }, sink = c12b_src2 } local widget12b = display.create(function() return locale_table12 end) display.register_action(widget12b, "Craft", function(rid, ctx_inner) widget12b._captured = { rid = rid, ctx_inner = ctx_inner } end) display._invoke_context_menu(widget12b, "alias_r12", 0, 0) local f2_ok = false if captured_entries and #captured_entries == 1 then captured_entries[1].callback({ close_menu = function() end }) local cap = widget12b._captured f2_ok = cap ~= nil and cap.rid == "alias_r12" and cap.ctx_inner.locale == locale_table12 and cap.ctx_inner.container == c12b_src2 -- sink alias end panel.show_context_menu = saved_show T.assert(f1_ok and f2_ok, "12. ctx_inner carries locale + container alias: Form-1 (handle == " .. "locale == container); Form-2 (locale==table, container==sink)") end return M