Files
factorio-expcluster/exp_scenario/test/module/features.lua
T
bbassieandClaude Opus 5.5 4bc1773c82 Address review: page layout, five more features, framework changes
Framework:
- Intervals are gone. Update periods are hardcoded on_nth_tick values
  again, since nobody has needed to change them.
- apply is a single loop over the declared fields and logs a warning
  when an override is invalid. receive_update also warns about
  unknown keys.
- Nested tables are addressed as "section.key". Lists of strings can be
  overridden. Features.optional declares a value with no default.
- Features.on_apply rebuilds values derived from the config, such as
  lookup sets. It also runs during on_load.

exp_roles is assumed to be present:
- AFK Kick picks a role (active_role_id) instead of a custom check
  function.
- Chat Auto Reply, Protection, and Nuke Protection no longer take a
  permission as a setting.

New features, each chosen for an edge case:
- Discord Alerts: conditional handlers, and a command map that becomes
  a list.
- Entity Protection: lists turned into sets at load, and bookkeeping
  that must keep running while disabled.
- Nuke Protection: config keyed by defines, flattened to a banned items
  list.
- Popups: one config shared by two control files.
- Spawn Area: nested sections, and settings that only apply when the
  map is created.

Page: now laid out like the role page. It has a search across every
setting, one long list with a sticky sidebar to jump between sections,
and the enabled switch separated at the front of each header with
Reset to defaults on the right. Changed rows are highlighted, and a
bottom bar offers Revert and Apply. Role settings use a role picker and
lists use a tag select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:28:23 +00:00

138 lines
7.2 KiB
Lua

local Suite = ... --- @type Suite<ExpScenario.TestEnv>
Suite.test("config() enables features unless the defaults say otherwise", function(env)
Suite.eq(env.Features.config("on", { value = 1 }).enabled, true, "enabled by default")
Suite.eq(env.Features.config("off", { enabled = false }).enabled, false, "the defaults can disable it")
Suite.eq(env.Features.config("empty"), { enabled = true }, "config is optional")
Suite.throws(function() env.Features.config("on", {}) end, "Feature already declared: on", "names are unique")
end)
Suite.test("is_enabled() takes a name or a config", function(env)
local config = env.Features.config("test", { enabled = false })
Suite.eq(env.Features.is_enabled("test"), false, "by name")
Suite.eq(env.Features.is_enabled(config), false, "by config")
Suite.throws(function() env.Features.is_enabled("unknown") end, "Unknown feature: unknown", "unknown names error")
end)
Suite.test("receive_update() changes the config in place", function(env)
local config = env.Features.config("test", { flag = true, count = 5, text = "a" })
env.Features.receive_update{ { name = "test", enabled = false, values = { flag = false, count = 7 } } }
Suite.eq(config, { enabled = false, flag = false, count = 7, text = "a" }, "given values replace the defaults")
env.Features.receive_update{ { name = "test", enabled = true, values = { text = "b" } } }
Suite.eq(config, { enabled = true, flag = true, count = 5, text = "b" }, "values left out go back to their default")
Suite.eq(env.storage, { test = { enabled = true, text = "b" } }, "the overrides are kept in storage")
end)
Suite.test("receive_update() raises on_feature_changed for each known feature", function(env)
env.Features.config("test")
env.Features.receive_update{
{ name = "test", enabled = false, values = {} },
{ name = "unknown", enabled = false, values = {} },
}
Suite.eq(env.events, {
{ name = env.Features.events.on_feature_changed, tick = game.tick, feature_name = "test" },
}, "raised once")
end)
Suite.test("receive_update() ignores values which can not be overridden", function(env)
local function check() return true end
local config = env.Features.config("test", { flag = true, check = check, icon = nil })
env.Features.receive_update{
{ name = "unknown", enabled = false, values = {} },
{ name = "test", enabled = true, values = { flag = "yes", check = false, icon = "x", extra = 1 } },
}
Suite.eq(config, { enabled = true, flag = true, check = check }, "wrong types, functions, and unknown keys are skipped")
Suite.eq(env.storage.unknown, nil, "unknown features are not stored")
end)
Suite.test("on_load applies the overrides from storage", function(env)
local config = env.Features.config("test", { flag = true })
env.on_load{ test = { enabled = false, flag = false } }
Suite.eq(config, { enabled = false, flag = false }, "the saved overrides apply")
env.Features.receive_update{ { name = "test", enabled = true, values = {} } }
Suite.eq(env.storage, { test = { enabled = true } }, "later updates go to the loaded storage table")
end)
Suite.test("guard() skips handlers while the feature is disabled", function(env)
local config = env.Features.config("test")
local calls = {}
local lib = env.Features.guard(config, {
events = { on_event = function(event) calls[#calls + 1] = event end },
on_nth_tick = { [60] = function(event) calls[#calls + 1] = event end },
on_init = function() end,
})
lib.events.on_event("a")
lib.on_nth_tick[60]("b")
env.Features.receive_update{ { name = "test", enabled = false, values = {} } }
lib.events.on_event("c")
lib.on_nth_tick[60]("d")
Suite.eq(calls, { "a", "b" }, "only called while enabled")
end)
Suite.test("guard() declares features by name", function(env)
local calls = 0
local lib = env.Features.guard("test", { events = { on_event = function() calls = calls + 1 end } })
env.Features.receive_update{ { name = "test", enabled = false, values = {} } }
lib.events.on_event()
Suite.eq(calls, 0, "the name is declared without config")
Suite.eq(env.Features.is_enabled("test"), false, "and can be looked up")
end)
Suite.test("receive_update() logs invalid and unknown overrides", function(env)
local config = env.Features.config("test", { flag = true })
env.Features.receive_update{ { name = "test", enabled = true, values = { flag = 1, extra = 2 } } }
Suite.eq(config.flag, true, "the default is used")
Suite.eq(#env.logged, 2, "both are logged")
end)
Suite.test("config() addresses nested values by path", function(env)
local offset = { 1, 2 }
local config = env.Features.config("test", { section = { flag = true, count = 1 }, offset = offset })
env.Features.receive_update{ { name = "test", enabled = true, values = { ["section.flag"] = false, offset = { "x" } } } }
Suite.eq(config.section, { flag = false, count = 1 }, "the nested value changes in place")
Suite.eq(config.offset, offset, "lists of other types can not be overridden")
env.Features.receive_update{ { name = "test", enabled = true, values = {} } }
Suite.eq(config.section.flag, true, "and goes back to its default")
end)
Suite.test("config() allows lists of strings to be overridden", function(env)
local config = env.Features.config("test", { names = { "a", "b" }, empty = {} })
env.Features.receive_update{ { name = "test", enabled = true, values = { names = { "c" }, empty = { "d" } } } }
Suite.eq(config.names, { "c" }, "a list is replaced")
Suite.eq(config.empty, { "d" }, "an empty default is a list")
env.Features.receive_update{ { name = "test", enabled = true, values = { names = { 1 } } } }
Suite.eq(config.names, { "a", "b" }, "a list with other types is invalid")
end)
Suite.test("optional() declares a value without a default", function(env)
local config = env.Features.config("test", { role_id = env.Features.optional("number") })
Suite.eq(config.role_id, nil, "nil until overridden")
env.Features.receive_update{ { name = "test", enabled = true, values = { role_id = 3 } } }
Suite.eq(config.role_id, 3, "overridden with the given type")
env.Features.receive_update{ { name = "test", enabled = true, values = {} } }
Suite.eq(config.role_id, nil, "and back to nil")
end)
Suite.test("on_apply() is called now and after every change", function(env)
local config = env.Features.config("test", { names = { "a" } })
local seen = {}
env.Features.on_apply("test", function(applied) seen[#seen + 1] = applied.names[1] end)
env.Features.receive_update{ { name = "test", enabled = true, values = { names = { "b" } } } }
env.on_load{ test = { enabled = true, names = { "c" } } }
Suite.eq(seen, { "a", "b", "c" }, "on register, update, and load")
Suite.eq(config.names, { "c" }, "after the config is applied")
end)
Suite.test("config files declare their feature", function(env)
local config = env.load_config("popups")
env.Features.receive_update{ { name = "popups", enabled = true, values = { show_player_damage = false } } }
Suite.eq(config.show_player_damage, false, "popups can be changed")
Suite.eq(config.show_player_health, true, "other values keep their default")
end)
return Suite.run()