Three new TAP cases cover the locale_factory rollout:
- §10 Form-1 bw-compat: display.create(container) wraps the bare
handle as a constant locale_factory; _test_get_locale returns the
same handle, _test_get_rows still resolves single-source
availability via the Form-1 shim.
- §11 Form-2 locale-factory: display.create(function() -> {sources,
sink}) returns the locale table per frame; a recipe needing two
templates split across two source-containers reports aggregate
availability=true.
- §12 ctx_inner propagation: monkey-patching panel.show_context_menu
captures the entries-table, then invokes the action callback with
a dummy menu_ctx. Asserts ctx_inner.locale matches the resolved
locale and ctx_inner.container points at locale.sink (Form-2) or
the bare handle (Form-1).
Test count grows 9 -> 12. Added a fresh_container helper so the
multi-container fixtures in §11/§12 do not lose recipes via repeated
_test_clear_all calls.
Depends on lib-core.crafting-display v0.2.0 and lib-core.crafting
v0.2.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
423 lines
19 KiB
Lua
423 lines
19 KiB
Lua
-- 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
|