commit 0154f54fde8b0dd101f8258d56ff7db306334975 Author: Calic Date: Sun Jun 14 12:34:42 2026 +0200 feat: lib-core.crafting-display-test v0.1.0 — 8 TAP asserts Test suite for lib-core.crafting-display v0.1.0. Covers: 1. create returns widget_def with render, handle_input, title set from opts. 2. register_action + unregister_action round-trip: action-count goes 0 -> 1 -> 0. 3. Duplicate action label -> Loud-Error matching 'crafting-display.register_action.*duplicate'. 4. Non-function callback -> Loud-Error matching 'crafting-display.register_action.*callback'. 5. Default action-set is empty directly after create. 6. Rows filtered via is_known: only known recipes appear in the per-frame row-list (one recipe is_known=false is hidden). 7. Per-row availability flag matches can_craft.ok: container with 1x rock makes a recipe needing rock available=true and a recipe needing missing stick available=false. 8. Resolver overrides (set_icon_resolver / set_label_resolver / set_summary_resolver) actually take effect — backdoors return the overridden values. Uses engine.test.assert with string.find for pattern matching loud-error messages (engine.test has no assert_match helper). Per-test isolation via crafting._test_clear_all; composition templates are defined once at module-scope under namespace craft_display_test.* (composition has no clear-all hook). Depends on lib-core.crafting-display 0.1.0, lib-core.crafting 0.1.0, lib-core.panel 0.1.1, lib-core.inventory-list 0.1.0, lib-core.composition 0.3.0 — all five declared directly per engine's per-module non-transitive resolver. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..81c9d65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2026 Calic. All rights reserved. + +This software is part of the Sporel platform — **Tier 1 (Official / +Proprietary)** content per the Three-Tier Licensing Model documented in +`meta/docs/archive/design/vision.md §Licensing Model` (current source; +migration to `meta/docs/architecture/licensing-model.md` pending). + +⚠ **WIP — Legal review required before public launch.** The terms below +reflect design intent only; the formalized license framework will be +finalized through legal counsel before the first public release. Until +then, this notice serves as a placeholder defending the platform owner's +rights against unintentional re-licensing. + +No license is granted to copy, modify, distribute, sublicense, or otherwise +use this software in any form without prior written permission from the +copyright holder. + +References: +- Tier 1 (this file): all rights reserved, proprietary, sold/distributed + via official channels (Steam, etc.) +- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue- + share — see vision.md §Licensing Model +- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies + to community-uploaded libs/modules/assets, not this repo diff --git a/README.md b/README.md new file mode 100644 index 0000000..7550948 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# lib-core.crafting-display-test + +Test module for `lib-core.crafting-display`. Runs 8 assertions headless +via `engine.test.*` (TAP output). + +**Version:** 0.1.0 +**Lib-ID:** lib-core.crafting-display-test +**Role:** test +**Requires:** lib-core.crafting-display v0.1.0, lib-core.crafting v0.1.0, lib-core.panel v0.1.1, lib-core.inventory-list v0.1.0, lib-core.composition v0.3.0 + +All transitive deps are declared directly per engine's per-module +non-transitive resolver. + +## Running + +```bash +export PATH="/c/msys64/mingw64/bin:$PATH" +cd ~/Projects/Sporel/sporel-engine +SPOREL_LOG_STDERR=1 \ + ./build/Sporel.exe \ + --test-libs=lib-core.crafting-display-test \ + --libs-dir=/c/Games/Sporel/libs 2>&1 | tail -15 +``` + +Expected: `pass=8 fail=0`. + +## Assertions + +1. `create(container, opts)` returns widget_def with `render`, `handle_input`, and correct `title` +2. `register_action` + `unregister_action` round-trip: action-count goes 0 -> 1 -> 0 +3. Duplicate action label -> Loud-Error matching `crafting-display.register_action.*duplicate` +4. Non-function callback -> Loud-Error matching `crafting-display.register_action.*callback` +5. Default action-set is empty directly after `create` +6. Rows filtered via `is_known`: only known recipes appear in `_test_get_rows` +7. Per-row availability flag matches `can_craft.ok` (true for satisfied inputs, false otherwise) +8. Resolver overrides (`set_icon_resolver` / `set_label_resolver` / `set_summary_resolver`) actually take effect diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..9ed6500 --- /dev/null +++ b/init.lua @@ -0,0 +1,226 @@ +-- lib-core.crafting-display-test — v0.1.0 +-- 8 assertions covering widget creation, action lifecycle, default-empty, +-- row visibility (is_known filter), per-row availability, and resolver overrides. +-- 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 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 + +-- --------------------------------------------------------------------- +-- 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") +end + +return M diff --git a/manifest.core b/manifest.core new file mode 100644 index 0000000..06c7f96 --- /dev/null +++ b/manifest.core @@ -0,0 +1,6 @@ +{ + "id": "lib-core.crafting-display-test.core", + "kind": "lib", + "version": "0.1.0", + "license": "Proprietary" +} diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..4683814 --- /dev/null +++ b/manifest.lib @@ -0,0 +1,13 @@ +{ + "id": "lib-core.crafting-display-test", + "role": "test", + "version": "0.1.0", + "api_min": "0.1", + "deps": [ + {"id": "lib-core.crafting-display", "version": "0.1.0"}, + {"id": "lib-core.crafting", "version": "0.1.0"}, + {"id": "lib-core.panel", "version": "0.1.1"}, + {"id": "lib-core.inventory-list", "version": "0.1.0"}, + {"id": "lib-core.composition", "version": "0.3.0"} + ] +}