refactor(config): complete rewrite of configuration system

BREAKING CHANGE: New section-based configuration format

New Features:
- Type-safe BlockKey sealed interface (Vanilla, Oraxen, ItemsAdder)
- Section-based config: RegionConfig, BlockConfig, ResetConfig, RewardConfig, UIConfig, TeleportConfig
- Human-readable durations: 5m, 30s, 2h, 1d
- YAML inheritance from defaults.yml
- MiniMessage support for action bar and item names
- Immutable config records with validation

Removed:
- Legacy MineConfig (17-field god object)
- ConfigParser, ConfigValidator, ConfigSerializer split classes
- Legacy FillMode enum (replaced with better version)
- Old ConfigLoader with complex parsing logic

New Files:
- data/config/block/BlockKey.java - sealed interface for block types
- data/config/block/BlockConfig.java - type-safe block weights
- data/config/block/FillMode.java - fill strategies
- data/config/region/RegionConfig.java - region management
- data/config/reset/ResetConfig.java - reset behavior with Duration
- data/config/reward/RewardConfig.java - rewards with ItemReward
- data/config/teleport/TeleportConfig.java - teleport on reset
- data/config/ui/UIConfig.java - action bar and messages
- data/config/MineConfig.java - composed from sections
- data/config/ConfigLoader.java - clean loader with defaults inheritance
- defaults.yml - default configuration template

Tests:
- BlockKeyTest.java - serialization/deserialization
- BlockConfigTest.java - weight normalization, validation
- ResetConfigTest.java - duration parsing, defaults
- UIConfigTest.java - timer formatting, action bar
- MineConfigTest.java - builder, defaults, validation
This commit is contained in:
loki5512344 2026-05-31 21:57:10 +02:00
parent f4e9458544
commit f1060f34d4
22 changed files with 2012 additions and 896 deletions

View file

@ -0,0 +1,191 @@
package dev.loki.lomines.data.config;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.block.BlockKey;
import dev.loki.lomines.data.config.block.FillMode;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.util.location.Cuboid;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class MineConfigTest {
@Test
void testBuilder() {
try (MockedStatic<org.bukkit.Bukkit> bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world);
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 10, 74, 10);
RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2));
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
MineConfig config = MineConfig.builder("testmine")
.region(region)
.blocks(blocks)
.reset(ResetConfig.defaults())
.rewards(RewardConfig.empty())
.teleport(TeleportConfig.disabled())
.ui(UIConfig.defaults())
.build();
assertEquals("testmine", config.name());
assertEquals(1, config.region().regionCount());
assertEquals(1, config.blocks().blockCount());
}
}
@Test
void testDefaultsFactory() {
try (MockedStatic<org.bukkit.Bukkit> bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world);
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 10, 74, 10);
RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2));
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
MineConfig config = MineConfig.defaults("mymine", region, blocks);
assertEquals("mymine", config.name());
assertNotNull(config.reset());
assertNotNull(config.rewards());
assertNotNull(config.teleport());
assertNotNull(config.ui());
}
}
@Test
void testNameNormalized() {
try (MockedStatic<org.bukkit.Bukkit> bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world);
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 10, 74, 10);
RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2));
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
MineConfig config = MineConfig.builder("TestMine_123 ")
.region(region)
.blocks(blocks)
.build();
assertEquals("testmine_123", config.name());
}
}
@Test
void testNullNameThrows() {
assertThrows(NullPointerException.class, () ->
MineConfig.builder(null));
}
@Test
void testBlankNameThrows() {
try (MockedStatic<org.bukkit.Bukkit> bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world);
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 10, 74, 10);
RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2));
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
assertThrows(IllegalArgumentException.class, () ->
MineConfig.builder(" ")
.region(region)
.blocks(blocks)
.build());
}
}
@Test
void testNullRegionThrows() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
assertThrows(NullPointerException.class, () ->
MineConfig.builder("test")
.region(null)
.blocks(blocks)
.build());
}
@Test
void testNullBlocksThrows() {
try (MockedStatic<org.bukkit.Bukkit> bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) {
World world = mock(World.class);
when(world.getName()).thenReturn("world");
bukkit.when(() -> org.bukkit.Bukkit.getWorld("world")).thenReturn(world);
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 10, 74, 10);
RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2));
assertThrows(NullPointerException.class, () ->
MineConfig.builder("test")
.region(region)
.blocks(null)
.build());
}
}
@Test
void testWorldNameDelegation() {
try (MockedStatic<org.bukkit.Bukkit> bukkit = Mockito.mockStatic(org.bukkit.Bukkit.class)) {
World world = mock(World.class);
when(world.getName()).thenReturn("nether");
bukkit.when(() -> org.bukkit.Bukkit.getWorld("nether")).thenReturn(world);
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 10, 74, 10);
RegionConfig region = RegionConfig.fromSelections(List.of(loc1, loc2));
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig blocks = new BlockConfig(weights, FillMode.CUBOID, null);
MineConfig config = MineConfig.builder("test")
.region(region)
.blocks(blocks)
.build();
assertEquals("nether", config.worldName());
}
}
}

View file

@ -0,0 +1,144 @@
package dev.loki.lomines.data.config.block;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class BlockConfigTest {
@Test
void testValidConfig() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 50.0);
weights.put(new BlockKey.Vanilla(Material.DIRT), 30.0);
weights.put(new BlockKey.Vanilla(Material.COAL_ORE), 20.0);
BlockConfig config = new BlockConfig(weights, FillMode.CUBOID, null);
assertEquals(3, config.blockCount());
assertFalse(config.hasCustomBlocks());
assertEquals(FillMode.CUBOID, config.fillMode());
}
@Test
void testWeightsAreNormalized() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
weights.put(new BlockKey.Vanilla(Material.DIRT), 100.0);
BlockConfig config = new BlockConfig(weights, FillMode.CUBOID, null);
// Should be normalized to sum to 1.0
double stoneWeight = config.weightFor(new BlockKey.Vanilla(Material.STONE));
double dirtWeight = config.weightFor(new BlockKey.Vanilla(Material.DIRT));
assertEquals(0.5, stoneWeight, 0.001);
assertEquals(0.5, dirtWeight, 0.001);
}
@Test
void testEmptyConfigThrows() {
assertThrows(IllegalArgumentException.class, () ->
new BlockConfig(Map.of(), FillMode.CUBOID, null));
}
@Test
void testNegativeWeightThrows() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), -10.0);
assertThrows(IllegalArgumentException.class, () ->
new BlockConfig(weights, FillMode.CUBOID, null));
}
@Test
void testZeroWeightThrows() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 0.0);
assertThrows(IllegalArgumentException.class, () ->
new BlockConfig(weights, FillMode.CUBOID, null));
}
@Test
void testNullWeightThrows() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), null);
assertThrows(IllegalArgumentException.class, () ->
new BlockConfig(weights, FillMode.CUBOID, null));
}
@Test
void testNullBlockKeyThrows() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(null, 50.0);
assertThrows(NullPointerException.class, () ->
new BlockConfig(weights, FillMode.CUBOID, null));
}
@Test
void testCustomBlocksDetected() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 50.0);
weights.put(new BlockKey.Oraxen("ruby_ore"), 50.0);
BlockConfig config = new BlockConfig(weights, FillMode.CUBOID, null);
assertTrue(config.hasCustomBlocks());
}
@Test
void testVanillaOnlyFactory() {
Map<Material, Double> materials = new HashMap<>();
materials.put(Material.STONE, 60.0);
materials.put(Material.DIRT, 40.0);
BlockConfig config = BlockConfig.vanilla(materials);
assertEquals(2, config.blockCount());
assertFalse(config.hasCustomBlocks());
}
@Test
void testImmutableWeights() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockConfig config = new BlockConfig(weights, FillMode.CUBOID, null);
// Try to modify the returned map
Map<BlockKey, Double> returnedWeights = config.weights();
assertThrows(UnsupportedOperationException.class, () ->
returnedWeights.put(new BlockKey.Vanilla(Material.DIRT), 50.0));
}
@Test
void testMaskConfig() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
BlockKey marker = new BlockKey.Vanilla(Material.PINK_CONCRETE);
Map<String, Boolean> positions = new HashMap<>();
positions.put("world;10;64;10;0;0", true);
BlockConfig.MaskConfig mask = new BlockConfig.MaskConfig(marker, positions);
BlockConfig config = new BlockConfig(weights, FillMode.MASK, mask);
assertEquals(FillMode.MASK, config.fillMode());
assertNotNull(config.mask());
}
@Test
void testFillModeNullThrows() {
Map<BlockKey, Double> weights = new HashMap<>();
weights.put(new BlockKey.Vanilla(Material.STONE), 100.0);
assertThrows(NullPointerException.class, () ->
new BlockConfig(weights, null, null));
}
}

View file

@ -0,0 +1,112 @@
package dev.loki.lomines.data.config.block;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BlockKeyTest {
@Test
void testVanillaSerialization() {
BlockKey.Vanilla stone = new BlockKey.Vanilla(Material.STONE);
assertEquals("stone", stone.serialize());
BlockKey.Vanilla diamondOre = new BlockKey.Vanilla(Material.DIAMOND_ORE);
assertEquals("diamond_ore", diamondOre.serialize());
}
@Test
void testOraxenSerialization() {
BlockKey.Oraxen custom = new BlockKey.Oraxen("ruby_ore");
assertEquals("oraxen:ruby_ore", custom.serialize());
}
@Test
void testItemsAdderSerialization() {
BlockKey.ItemsAdder custom = new BlockKey.ItemsAdder("mythic_block");
assertEquals("itemsadder:mythic_block", custom.serialize());
}
@Test
void testDeserializeVanilla() {
BlockKey key = BlockKey.deserialize("stone");
assertInstanceOf(BlockKey.Vanilla.class, key);
assertEquals(Material.STONE, ((BlockKey.Vanilla) key).material());
}
@Test
void testDeserializeOraxen() {
BlockKey key = BlockKey.deserialize("oraxen:custom_block");
assertInstanceOf(BlockKey.Oraxen.class, key);
assertEquals("custom_block", ((BlockKey.Oraxen) key).id());
}
@Test
void testDeserializeItemsAdder() {
BlockKey key = BlockKey.deserialize("itemsadder:mythic_ore");
assertInstanceOf(BlockKey.ItemsAdder.class, key);
assertEquals("mythic_ore", ((BlockKey.ItemsAdder) key).id());
}
@Test
void testDeserializeNullThrows() {
assertThrows(IllegalArgumentException.class, () -> BlockKey.deserialize(null));
}
@Test
void testDeserializeEmptyThrows() {
assertThrows(IllegalArgumentException.class, () -> BlockKey.deserialize(""));
}
@Test
void testDeserializeBlankThrows() {
assertThrows(IllegalArgumentException.class, () -> BlockKey.deserialize(" "));
}
@Test
void testDeserializeUnknownVanillaThrows() {
assertThrows(IllegalArgumentException.class, () -> BlockKey.deserialize("not_a_real_block"));
}
@Test
void testVanillaNotBlockThrows() {
assertThrows(IllegalArgumentException.class, () -> new BlockKey.Vanilla(Material.DIAMOND));
}
@Test
void testOraxenNullIdThrows() {
assertThrows(NullPointerException.class, () -> new BlockKey.Oraxen(null));
}
@Test
void testOraxenBlankIdThrows() {
assertThrows(IllegalArgumentException.class, () -> new BlockKey.Oraxen(" "));
}
@Test
void testItemsAdderEmptyIdThrows() {
assertThrows(IllegalArgumentException.class, () -> new BlockKey.ItemsAdder(""));
}
@Test
void testCaseInsensitiveDeserialization() {
BlockKey key1 = BlockKey.deserialize("STONE");
BlockKey key2 = BlockKey.deserialize("stone");
BlockKey key3 = BlockKey.deserialize("StOnE");
assertEquals(key1.serialize(), key2.serialize());
assertEquals(key2.serialize(), key3.serialize());
}
@Test
void testEquality() {
BlockKey a = new BlockKey.Vanilla(Material.STONE);
BlockKey b = new BlockKey.Vanilla(Material.STONE);
BlockKey c = new BlockKey.Vanilla(Material.DIRT);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
assertNotEquals(a, c);
}
}

View file

@ -0,0 +1,169 @@
package dev.loki.lomines.data.config.reset;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ResetConfigTest {
@Test
void testDefaultConfig() {
ResetConfig config = ResetConfig.defaults();
assertEquals(Duration.ofMinutes(5), config.interval());
assertEquals(10.0, config.percentTrigger());
assertFalse(config.percentEnabled());
assertTrue(config.commands().isEmpty());
assertEquals("", config.broadcastMessage());
}
@Test
void testBuilder() {
ResetConfig config = ResetConfig.builder()
.interval("10m")
.percentTrigger(25.0)
.percentEnabled(true)
.commands(List.of("broadcast Reset!", "eco give %player% 10"))
.broadcastMessage("Mine reset!")
.build();
assertEquals(Duration.ofMinutes(10), config.interval());
assertEquals(25.0, config.percentTrigger());
assertTrue(config.percentEnabled());
assertEquals(2, config.commands().size());
assertEquals("Mine reset!", config.broadcastMessage());
}
@Test
void testBuilderWithTicks() {
ResetConfig config = ResetConfig.builder()
.intervalTicks(1200) // 60 seconds
.build();
assertEquals(Duration.ofSeconds(60), config.interval());
assertEquals(1200, config.intervalTicks());
}
@Test
void testIntervalParsing() {
assertEquals(Duration.ofSeconds(30), ResetConfig.builder().interval("30s").build().interval());
assertEquals(Duration.ofMinutes(5), ResetConfig.builder().interval("5m").build().interval());
assertEquals(Duration.ofHours(2), ResetConfig.builder().interval("2h").build().interval());
assertEquals(Duration.ofDays(1), ResetConfig.builder().interval("1d").build().interval());
}
@Test
void testInvalidIntervalDefaultsToFiveMinutes() {
ResetConfig config = ResetConfig.builder()
.interval("invalid")
.build();
assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval());
}
@Test
void testNegativeIntervalDefaults() {
ResetConfig config = new ResetConfig(
Duration.ofSeconds(-10),
10.0,
false,
List.of(),
""
);
assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval());
}
@Test
void testZeroIntervalDefaults() {
ResetConfig config = new ResetConfig(
Duration.ZERO,
10.0,
false,
List.of(),
""
);
assertEquals(ResetConfig.DEFAULT_INTERVAL, config.interval());
}
@Test
void testTooShortIntervalClamped() {
ResetConfig config = ResetConfig.builder()
.interval("0.5s")
.build();
assertEquals(Duration.ofSeconds(1), config.interval());
}
@Test
void testTooLongIntervalClamped() {
ResetConfig config = ResetConfig.builder()
.interval("48h")
.build();
assertEquals(Duration.ofHours(24), config.interval());
}
@Test
void testInvalidPercentTriggerClamped() {
ResetConfig negative = new ResetConfig(
Duration.ofMinutes(5),
-10.0,
false,
List.of(),
""
);
assertEquals(ResetConfig.DEFAULT_PERCENT_TRIGGER, negative.percentTrigger());
ResetConfig over100 = new ResetConfig(
Duration.ofMinutes(5),
150.0,
false,
List.of(),
""
);
assertEquals(ResetConfig.DEFAULT_PERCENT_TRIGGER, over100.percentTrigger());
}
@Test
void testIntervalDisplay() {
assertEquals("30s", new ResetConfig(Duration.ofSeconds(30), 10.0, false, List.of(), "").intervalDisplay());
assertEquals("5m", new ResetConfig(Duration.ofMinutes(5), 10.0, false, List.of(), "").intervalDisplay());
assertEquals("2h", new ResetConfig(Duration.ofHours(2), 10.0, false, List.of(), "").intervalDisplay());
}
@Test
void testPercentTriggerEnabled() {
assertFalse(ResetConfig.defaults().isPercentTriggerEnabled());
assertFalse(new ResetConfig(Duration.ofMinutes(5), 10.0, false, List.of(), "").isPercentTriggerEnabled());
assertTrue(new ResetConfig(Duration.ofMinutes(5), 10.0, true, List.of(), "").isPercentTriggerEnabled());
assertFalse(new ResetConfig(Duration.ofMinutes(5), 0.0, true, List.of(), "").isPercentTriggerEnabled());
}
@Test
void testImmutableCommands() {
ResetConfig config = ResetConfig.builder()
.commands(List.of("cmd1"))
.build();
assertThrows(UnsupportedOperationException.class, () ->
config.commands().add("cmd2"));
}
@Test
void testNullCommandsDefaultsToEmpty() {
ResetConfig config = new ResetConfig(
Duration.ofMinutes(5),
10.0,
false,
null,
"test"
);
assertTrue(config.commands().isEmpty());
}
}

View file

@ -0,0 +1,112 @@
package dev.loki.lomines.data.config.ui;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UIConfigTest {
@Test
void testDefaults() {
UIConfig config = UIConfig.defaults();
assertTrue(config.actionBarEnabled());
assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat());
assertEquals(50.0, config.actionBarRange());
assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat());
}
@Test
void testDisabled() {
UIConfig config = UIConfig.disabled();
assertFalse(config.actionBarEnabled());
assertEquals("", config.actionBarFormat());
assertEquals(0, config.actionBarRange());
}
@Test
void testCustomConfig() {
UIConfig config = new UIConfig(
true,
"<red>{mine}</red>",
100.0,
"HH:mm:ss"
);
assertTrue(config.actionBarEnabled());
assertEquals("<red>{mine}</red>", config.actionBarFormat());
assertEquals(100.0, config.actionBarRange());
assertEquals("HH:mm:ss", config.timerFormat());
}
@Test
void testNullFormatDefaults() {
UIConfig config = new UIConfig(true, null, 50.0, null);
assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat());
assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat());
}
@Test
void testBlankFormatDefaults() {
UIConfig config = new UIConfig(true, " ", 50.0, " ");
assertEquals(UIConfig.DEFAULT_ACTIONBAR_FORMAT, config.actionBarFormat());
assertEquals(UIConfig.DEFAULT_TIMER_FORMAT, config.timerFormat());
}
@Test
void testNegativeRangeClamped() {
UIConfig config = new UIConfig(true, "test", -10.0, "mm:ss");
assertEquals(1.0, config.actionBarRange());
}
@Test
void testRangeSquared() {
UIConfig config = new UIConfig(true, "test", 50.0, "mm:ss");
assertEquals(2500.0, config.actionBarRangeSquared(), 0.001);
}
@Test
void testFormatActionBar() {
UIConfig config = UIConfig.defaults();
Component result = config.formatActionBar("TestMine", 50.5, "02:30", 500, 1000);
String serialized = MiniMessage.miniMessage().serialize(result);
assertTrue(serialized.contains("TestMine"));
assertTrue(serialized.contains("50.5"));
assertTrue(serialized.contains("02:30"));
}
@Test
void testFormatTimerMmSs() {
UIConfig config = new UIConfig(true, "", 0, "mm:ss");
assertEquals("00:30", config.formatTimer(30));
assertEquals("05:00", config.formatTimer(300));
assertEquals("59:59", config.formatTimer(3599));
}
@Test
void testFormatTimerHhMmSs() {
UIConfig config = new UIConfig(true, "", 0, "HH:mm:ss");
assertEquals("0:00:30", config.formatTimer(30));
assertEquals("0:05:00", config.formatTimer(300));
assertEquals("1:00:00", config.formatTimer(3600));
assertEquals("2:30:45", config.formatTimer(9045));
}
@Test
void testFormatTimerDefault() {
UIConfig config = UIConfig.defaults();
assertEquals("02:30", config.formatTimer(150));
assertEquals("10:00", config.formatTimer(600));
}
}