diff --git a/.claude-plugin/skills/jaction/SKILL.md b/.claude-plugin/skills/jaction/SKILL.md index 1aad02a3a..ea96e73cc 100644 --- a/.claude-plugin/skills/jaction/SKILL.md +++ b/.claude-plugin/skills/jaction/SKILL.md @@ -1,79 +1,130 @@ --- name: jaction -description: JAction fluent chainable task system for Unity. Triggers on: sequential tasks, delay, timer, repeat loop, WaitUntil, WaitWhile, async workflow, zero-allocation async, coroutine alternative, scheduled action, timed event, polling condition, action sequence, ExecuteAsync +description: JAction fluent chainable task system for Unity. Triggers on: sequential tasks, delay, timer, repeat loop, WaitUntil, WaitWhile, async workflow, zero-allocation async, coroutine alternative, scheduled action, timed event, polling condition, action sequence, ExecuteAsync, parallel execution --- # JAction - Chainable Task Execution -Fluent API for composing complex action sequences in Unity with automatic object pooling and zero-allocation async. +Fluent API for composing complex action sequences in Unity with automatic object pooling, zero-allocation async, and parallel execution support. ## When to Use + - Sequential workflows with delays - Polling conditions (WaitUntil/WaitWhile) - Repeat loops with intervals - Game timers and scheduled events - Zero-GC async operations +- Parallel concurrent executions + +## Core Concepts + +### Task Snapshot Isolation + +When `Execute()` or `ExecuteAsync()` is called, the current task list is **snapshotted**. Modifications to the JAction after execution starts do NOT affect running executions: + +```csharp +var action = JAction.Create() + .Delay(1f) + .Do(static () => Debug.Log("Original")); -## Properties -- `.Executing` - Returns true if currently executing -- `.Cancelled` - Returns true if execution was cancelled -- `.IsParallel` - Returns true if parallel mode enabled +var handle = action.ExecuteAsync(); -## Core API +// This task is NOT executed by the handle above - it was added after the snapshot +action.Do(static () => Debug.Log("Added Later")); + +await handle; // Only prints "Original" +``` -### Execution Methods -- `.Execute(float timeout = 0)` - Synchronous execution (BLOCKS main thread - use sparingly) -- `.ExecuteAsync(float timeout = 0)` - Asynchronous via PlayerLoop (RECOMMENDED) +This isolation enables safe parallel execution where each handle operates on its own task snapshot. -### Action Execution -- `.Do(Action)` - Execute synchronous action -- `.Do(Action, TState)` - Execute with state (zero-alloc for reference types) -- `.Do(Func)` - Execute async action -- `.Do(Func, TState)` - Async with state +### Return Types -### Delays & Waits -- `.Delay(float seconds)` - Wait specified seconds -- `.DelayFrame(int frames)` - Wait specified frame count -- `.WaitUntil(Func, frequency, timeout)` - Wait until condition true -- `.WaitUntil(Func, TState, frequency, timeout)` - With state -- `.WaitWhile(Func, frequency, timeout)` - Wait while condition true -- `.WaitWhile(Func, TState, frequency, timeout)` - With state +**JActionExecution** (returned by Execute, awaited from ExecuteAsync): +- `.Action` - The JAction that was executed +- `.Cancelled` - Whether THIS specific execution was cancelled +- `.Executing` - Whether the action is still executing +- `.Dispose()` - Returns JAction to pool + +**JActionExecutionHandle** (returned by ExecuteAsync before await): +- `.Action` - The JAction being executed +- `.Cancelled` - Whether this execution is cancelled +- `.Executing` - Whether still running +- `.Cancel()` - Cancel THIS specific execution +- `.AsUniTask()` - Convert to `UniTask` +- Awaitable: `await handle` returns `JActionExecution` + +## API Reference + +### Execution + +| Method | Returns | Description | +|--------|---------|-------------| +| `.Execute(timeout)` | `JActionExecution` | Synchronous blocking execution | +| `.ExecuteAsync(timeout)` | `JActionExecutionHandle` | Async via PlayerLoop (recommended) | + +### Actions + +| Method | Description | +|--------|-------------| +| `.Do(Action)` | Execute synchronous action | +| `.Do(Action, T)` | Execute with state (zero-alloc for reference types) | +| `.Do(Func)` | Execute async action | +| `.Do(Func, T)` | Async with state | + +### Timing + +| Method | Description | +|--------|-------------| +| `.Delay(seconds)` | Wait specified seconds | +| `.DelayFrame(frames)` | Wait specified frame count | +| `.WaitUntil(condition, frequency, timeout)` | Wait until condition true | +| `.WaitWhile(condition, frequency, timeout)` | Wait while condition true | ### Loops -- `.Repeat(Action, count, interval)` - Repeat N times -- `.Repeat(Action, TState, count, interval)` - With state -- `.RepeatWhile(Action, Func, frequency, timeout)` - Repeat while condition -- `.RepeatWhile(Action, Func, TState, frequency, timeout)` - With state -- `.RepeatUntil(Action, Func, frequency, timeout)` - Repeat until condition -- `.RepeatUntil(Action, Func, TState, frequency, timeout)` - With state + +| Method | Description | +|--------|-------------| +| `.Repeat(action, count, interval)` | Repeat N times | +| `.RepeatWhile(action, condition, frequency, timeout)` | Repeat while condition true | +| `.RepeatUntil(action, condition, frequency, timeout)` | Repeat until condition true | + +All loop methods have `` overloads for zero-allocation with reference types. ### Configuration -- `.Parallel()` - Enable concurrent execution -- `.OnCancel(Action)` - Register cancellation callback -- `.OnCancel(Action, TState)` - With state -### Lifecycle -- `.Cancel()` - Stop execution -- `.Reset()` - Clear state for reuse -- `.Dispose()` - Return to object pool -- `JAction.PooledCount` - Check pooled instances -- `JAction.ClearPool()` - Empty the pool +| Method | Description | +|--------|-------------| +| `.Parallel()` | Enable concurrent execution mode | +| `.OnCancel(callback)` | Register cancellation callback | +| `.Cancel()` | Stop ALL active executions | +| `.Reset()` | Clear state for reuse | +| `.Dispose()` | Return to object pool | + +### Static Members + +| Member | Description | +|--------|-------------| +| `JAction.Create()` | Get pooled instance | +| `JAction.PooledCount` | Check available pooled instances | +| `JAction.ClearPool()` | Empty the pool | ## Patterns -### Basic Sequence (use ExecuteAsync in production) +### Basic Sequence + ```csharp -using var action = await JAction.Create() +using var result = await JAction.Create() .Do(static () => Debug.Log("Step 1")) .Delay(1f) .Do(static () => Debug.Log("Step 2")) .ExecuteAsync(); ``` -### Always Use `using var` for Async (CRITICAL) +### Always Use `using var` (CRITICAL) + ```csharp // CORRECT - auto-disposes and returns to pool -using var action = await JAction.Create() +using var result = await JAction.Create() .Do(() => LoadAsset()) .WaitUntil(() => assetLoaded) .ExecuteAsync(); @@ -84,74 +135,125 @@ await JAction.Create() .ExecuteAsync(); ``` -### State Parameter for Zero-Allocation (Reference Types Only) +### Parallel Execution with Per-Execution Cancellation + ```csharp -// CORRECT - no closure allocation with reference types +var action = JAction.Create() + .Parallel() + .Do(static () => Debug.Log("Start")) + .Delay(5f) + .Do(static () => Debug.Log("Done")); + +// Start multiple concurrent executions (each gets own task snapshot) +var handle1 = action.ExecuteAsync(); +var handle2 = action.ExecuteAsync(); + +// Cancel only the first execution +handle1.Cancel(); + +// Each has independent Cancelled state +var result1 = await handle1; // result1.Cancelled == true +var result2 = await handle2; // result2.Cancelled == false + +action.Dispose(); +``` + +### UniTask.WhenAll with Parallel + +```csharp +var action = JAction.Create() + .Parallel() + .Delay(1f) + .Do(static () => Debug.Log("Done")); + +var handle1 = action.ExecuteAsync(); +var handle2 = action.ExecuteAsync(); + +await UniTask.WhenAll(handle1.AsUniTask(), handle2.AsUniTask()); + +action.Dispose(); +``` + +### Zero-Allocation with Reference Types + +```csharp +// CORRECT - static lambda + reference type state = zero allocation var data = new MyData(); JAction.Create() .Do(static (MyData d) => d.Process(), data) .Execute(); -// WARNING: State overloads DO NOT work with value types (int, float, struct, bool, etc.) -// Value types get boxed when passed as generic parameters, defeating zero-allocation -// For value types, use closures instead (allocation is acceptable): +// Pass 'this' when inside a class - no wrapper needed +public class Enemy : MonoBehaviour +{ + public bool IsStunned; + + public void ApplyStun(float duration) + { + IsStunned = true; + JAction.Create() + .Delay(duration) + .Do(static (Enemy self) => self.IsStunned = false, this) + .ExecuteAsync().Forget(); + } +} + +// Value types use closures (boxing would defeat zero-alloc anyway) int count = 5; JAction.Create() - .Do(() => Debug.Log($"Count: {count}")) // Closure is fine for value types + .Do(() => Debug.Log($"Count: {count}")) .Execute(); ``` -### Set Timeouts for Production +### Timeout Handling + ```csharp -using var action = await JAction.Create() +using var result = await JAction.Create() .WaitUntil(() => networkReady) - .ExecuteAsync(timeout: 30f); // Prevents infinite waits + .ExecuteAsync(timeout: 30f); + +if (result.Cancelled) + Debug.Log("Timed out!"); ``` -### With Cancellation +### Cancellation Callback + ```csharp var action = JAction.Create() .OnCancel(() => Debug.Log("Cancelled!")) - .Do(() => LongRunningTask()); + .Delay(10f); -var task = action.ExecuteAsync(); -// Later... -action.Cancel(); +var handle = action.ExecuteAsync(); +handle.Cancel(); // Triggers OnCancel callback ``` ## Game Patterns -All patterns use `ExecuteAsync()` for non-blocking execution. +### Cooldown Timer -### Cooldown Timer (Zero-GC) ```csharp -public sealed class AbilityState -{ - public bool CanUse = true; -} - public class AbilitySystem { - private readonly AbilityState _state = new(); - private readonly float _cooldown; + public bool CanUse = true; - public async UniTaskVoid TryUseAbility() + public async UniTaskVoid TryUseAbility(float cooldown) { - if (!_state.CanUse) return; - _state.CanUse = false; + if (!CanUse) return; + CanUse = false; PerformAbility(); - // Zero-GC: static lambda + reference type state - using var action = await JAction.Create() - .Delay(_cooldown) - .Do(static s => s.CanUse = true, _state) + // Pass 'this' as state - no extra class needed + using var _ = await JAction.Create() + .Delay(cooldown) + .Do(static s => s.CanUse = true, this) .ExecuteAsync(); } } ``` -### Damage Over Time (Zero-GC) +### Damage Over Time + ```csharp public sealed class DoTState { @@ -159,35 +261,32 @@ public sealed class DoTState public float DamagePerTick; } -public static async UniTaskVoid ApplyDoT(IDamageable target, float damage, int ticks, float interval) +public static async UniTaskVoid ApplyDoT( + IDamageable target, float damage, int ticks, float interval) { - // Rent state from pool to avoid allocation var state = JObjectPool.Shared().Rent(); state.Target = target; state.DamagePerTick = damage; - using var action = await JAction.Create() + using var _ = await JAction.Create() .Repeat( static s => s.Target?.TakeDamage(s.DamagePerTick), - state, - count: ticks, - interval: interval) + state, count: ticks, interval: interval) .ExecuteAsync(); - // Return state to pool state.Target = null; JObjectPool.Shared().Return(state); } ``` -### Wave Spawner (Async) +### Wave Spawner + ```csharp -// Async methods cannot use ReadOnlySpan (ref struct), use array instead public async UniTask RunWaves(WaveConfig[] waves) { foreach (var wave in waves) { - using var action = await JAction.Create() + using var result = await JAction.Create() .Do(() => UI.ShowWaveStart(wave.Number)) .Delay(2f) .Do(() => SpawnWave(wave)) @@ -195,74 +294,47 @@ public async UniTask RunWaves(WaveConfig[] waves) .Delay(wave.DelayAfter) .ExecuteAsync(); - if (action.Cancelled) break; - } -} - -// Sync methods can use ReadOnlySpan for zero-allocation iteration -public void RunWavesSync(ReadOnlySpan waves) -{ - foreach (ref readonly var wave in waves) - { - using var action = JAction.Create() - .Do(() => UI.ShowWaveStart(wave.Number)) - .Delay(2f) - .Do(() => SpawnWave(wave)) - .WaitUntil(() => ActiveEnemyCount == 0, timeout: 120f) - .Delay(wave.DelayAfter); - action.Execute(); - - if (action.Cancelled) break; + if (result.Cancelled) break; } } ``` -### Health Regeneration (Zero-GC) +### Health Regeneration + ```csharp public sealed class RegenState { - public float Health; - public float MaxHealth; - public float HpPerTick; + public float Health, MaxHealth, HpPerTick; } public static async UniTaskVoid StartRegen(RegenState state) { - using var action = await JAction.Create() + using var _ = await JAction.Create() .RepeatWhile( static s => s.Health = MathF.Min(s.Health + s.HpPerTick, s.MaxHealth), static s => s.Health < s.MaxHealth, - state, - frequency: 0.1f) + state, frequency: 0.1f) .ExecuteAsync(); } ``` ## Troubleshooting -### Nothing Happens -- **Forgot ExecuteAsync:** Must call `.ExecuteAsync()` at the end -- **Already disposed:** Don't reuse a JAction after Dispose() - -### Memory Leak -- **Missing `using var`:** Always use `using var action = await ...ExecuteAsync()` -- **Infinite loop:** Set timeouts on WaitUntil/WaitWhile in production - -### Frame Drops -- **Using Execute():** Switch to ExecuteAsync() for non-blocking -- **Heavy callbacks:** Keep .Do() callbacks lightweight - -### Unexpected Behavior -- **Value type state:** State overloads box value types; wrap in reference type -- **Check Cancelled:** After timeout, check `action.Cancelled` before continuing - -### GC Allocations -- **Closures:** Use static lambdas with state parameters -- **State must be reference type:** Value types get boxed +| Problem | Cause | Solution | +|---------|-------|----------| +| Nothing happens | Forgot to call Execute/ExecuteAsync | Add `.ExecuteAsync()` at the end | +| Memory leak | Missing `using var` | Always use `using var result = await ...` | +| Frame drops | Using `Execute()` | Switch to `ExecuteAsync()` | +| GC allocations | Closures with reference types | Use static lambda + state parameter | +| Unexpected timing | Value type state | Wrap in reference type or use closure | +| Handle shows wrong Cancelled | Reading after modification | Snapshot is isolated - this is expected | ## Common Mistakes -- NOT using `using var` after ExecuteAsync (memory leak, never returns to pool) -- Using Execute() in production (blocks main thread, causes frame drops) -- Using state overloads with value types (causes boxing - use closures instead) -- Forgetting to call Execute() or ExecuteAsync() (nothing happens) -- Code in .Do() runs atomically and cannot be interrupted - keep callbacks lightweight + +1. **Missing `using var`** - Memory leak, JAction never returns to pool +2. **Using `Execute()` in production** - Blocks main thread, causes frame drops +3. **State overloads with value types** - Causes boxing; use closures instead +4. **Forgetting Execute/ExecuteAsync** - Nothing happens +5. **Heavy work in `.Do()`** - Callbacks run atomically; keep them lightweight +6. **Using `action.Cancel()` in parallel** - Cancels ALL executions; use `handle.Cancel()` for specific execution +7. **Modifying JAction after ExecuteAsync** - Changes don't affect running execution (task snapshot isolation) diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs index b66dbd635..da5f517c5 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Runtime/MessageBox.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using Cysharp.Threading.Tasks; using TMPro; @@ -124,6 +125,64 @@ private static GameObject Prefab internal static bool SimulateNoPrefab; #endif +#if UNITY_INCLUDE_TESTS + /// + /// Test hook: Gets the pool state for verification in tests. + /// Returns (activeCount, pooledCount). + /// + internal static (int activeCount, int pooledCount) TestGetPoolState() + { + return (ActiveMessageBoxes.Count, PooledMessageBoxes.Count); + } + + /// + /// Test hook: Simulates clicking a button on the most recently shown message box. + /// + /// If true, simulates clicking OK; otherwise simulates clicking Cancel. + /// True if a message box was found and the click was simulated. + internal static bool TestSimulateButtonClick(bool clickOk) + { + if (ActiveMessageBoxes.Count == 0) return false; + + // Get the first message box (any will do for testing) + var target = ActiveMessageBoxes.First(); + target.HandleEvent(clickOk); + return true; + } + + /// + /// Test hook: Gets the button visibility state of the most recently shown message box. + /// + /// Tuple of (okButtonVisible, noButtonVisible), or null if no active boxes. + internal static (bool okVisible, bool noVisible)? TestGetButtonVisibility() + { + if (ActiveMessageBoxes.Count == 0) return null; + + var target = ActiveMessageBoxes.First(); + if (target._buttonOk == null || target._buttonNo == null) + return null; + + return (target._buttonOk.gameObject.activeSelf, target._buttonNo.gameObject.activeSelf); + } + + /// + /// Test hook: Gets the text content of the most recently shown message box. + /// + /// Tuple of (title, content, okText, noText), or null if no active boxes. + internal static (string title, string content, string okText, string noText)? TestGetContent() + { + if (ActiveMessageBoxes.Count == 0) return null; + + var target = ActiveMessageBoxes.First(); + return ( + target._title?.text, + target._content?.text, + target._textOk?.text, + target._textNo?.text + ); + } +#endif + private TextMeshProUGUI _content; private TextMeshProUGUI _textNo; private TextMeshProUGUI _textOk; diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components.meta new file mode 100644 index 000000000..556025bfb --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7231d249d43214879a1917bf44c84bfc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base.meta new file mode 100644 index 000000000..219f604ad --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59b3bf8b64fb24fe2bdbf3de43b25183 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base/JComponentTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base/JComponentTests.cs new file mode 100644 index 000000000..9cf76529a --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base/JComponentTests.cs @@ -0,0 +1,284 @@ +// JComponentTests.cs +// EditMode unit tests for JComponent base class + +using NUnit.Framework; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components; +using JEngine.UI.Editor.Components.Layout; + +namespace JEngine.UI.Tests.Editor.Components.Base +{ + [TestFixture] + public class JComponentTests + { + // Using JStack as concrete implementation of JComponent + private JStack _component; + + [SetUp] + public void SetUp() + { + _component = new JStack(); + } + + #region Constructor Tests + + [Test] + public void Constructor_WithBaseClassName_AddsClass() + { + // JStack inherits from JComponent with "j-stack" as base class + Assert.IsTrue(_component.ClassListContains("j-stack")); + } + + #endregion + + #region WithClass Tests + + [Test] + public void WithClass_AddsClassName() + { + _component.WithClass("custom-class"); + Assert.IsTrue(_component.ClassListContains("custom-class")); + } + + [Test] + public void WithClass_ReturnsComponentForChaining() + { + var result = _component.WithClass("test"); + Assert.AreSame(_component, result); + } + + [Test] + public void WithClass_CanAddMultipleClasses() + { + _component.WithClass("class1"); + _component.WithClass("class2"); + + Assert.IsTrue(_component.ClassListContains("class1")); + Assert.IsTrue(_component.ClassListContains("class2")); + } + + #endregion + + #region WithName Tests + + [Test] + public void WithName_SetsElementName() + { + _component.WithName("test-element"); + Assert.AreEqual("test-element", _component.name); + } + + [Test] + public void WithName_ReturnsComponentForChaining() + { + var result = _component.WithName("test"); + Assert.AreSame(_component, result); + } + + [Test] + public void WithName_CanOverwritePreviousName() + { + _component.WithName("first"); + _component.WithName("second"); + Assert.AreEqual("second", _component.name); + } + + #endregion + + #region Add Tests + + [Test] + public void Add_SingleChild_AddsToComponent() + { + var child = new Label("test"); + _component.Add(child); + + Assert.AreEqual(1, _component.childCount); + Assert.AreSame(child, _component[0]); + } + + [Test] + public void Add_MultipleChildren_AddsAllToComponent() + { + var child1 = new Label("test1"); + var child2 = new Label("test2"); + var child3 = new Label("test3"); + + _component.Add(child1, child2, child3); + + Assert.AreEqual(3, _component.childCount); + } + + [Test] + public void Add_NullChild_IsIgnored() + { + _component.Add((VisualElement)null); + Assert.AreEqual(0, _component.childCount); + } + + [Test] + public void Add_MixedNullAndValid_AddsOnlyValidChildren() + { + var child1 = new Label("test1"); + var child2 = new Label("test2"); + + _component.Add(child1, null, child2); + + Assert.AreEqual(2, _component.childCount); + } + + [Test] + public void Add_ReturnsComponentForChaining() + { + var result = _component.Add(new Label()); + Assert.AreSame(_component, result); + } + + #endregion + + #region WithFlexGrow Tests + + [Test] + public void WithFlexGrow_SetsFlexGrowValue() + { + _component.WithFlexGrow(2f); + Assert.AreEqual(2f, _component.style.flexGrow.value); + } + + [Test] + public void WithFlexGrow_ReturnsComponentForChaining() + { + var result = _component.WithFlexGrow(1f); + Assert.AreSame(_component, result); + } + + [Test] + public void WithFlexGrow_ZeroValue_SetsToZero() + { + _component.WithFlexGrow(0f); + Assert.AreEqual(0f, _component.style.flexGrow.value); + } + + #endregion + + #region WithFlexShrink Tests + + [Test] + public void WithFlexShrink_SetsFlexShrinkValue() + { + _component.WithFlexShrink(2f); + Assert.AreEqual(2f, _component.style.flexShrink.value); + } + + [Test] + public void WithFlexShrink_ReturnsComponentForChaining() + { + var result = _component.WithFlexShrink(1f); + Assert.AreSame(_component, result); + } + + #endregion + + #region WithMargin Tests + + [Test] + public void WithMargin_SetsAllMargins() + { + _component.WithMargin(10f); + + Assert.AreEqual(10f, _component.style.marginTop.value.value); + Assert.AreEqual(10f, _component.style.marginRight.value.value); + Assert.AreEqual(10f, _component.style.marginBottom.value.value); + Assert.AreEqual(10f, _component.style.marginLeft.value.value); + } + + [Test] + public void WithMargin_ReturnsComponentForChaining() + { + var result = _component.WithMargin(5f); + Assert.AreSame(_component, result); + } + + #endregion + + #region WithPadding Tests + + [Test] + public void WithPadding_SetsAllPadding() + { + _component.WithPadding(10f); + + Assert.AreEqual(10f, _component.style.paddingTop.value.value); + Assert.AreEqual(10f, _component.style.paddingRight.value.value); + Assert.AreEqual(10f, _component.style.paddingBottom.value.value); + Assert.AreEqual(10f, _component.style.paddingLeft.value.value); + } + + [Test] + public void WithPadding_ReturnsComponentForChaining() + { + var result = _component.WithPadding(5f); + Assert.AreSame(_component, result); + } + + #endregion + + #region WithVisibility Tests + + [Test] + public void WithVisibility_True_SetsDisplayFlex() + { + _component.WithVisibility(true); + Assert.AreEqual(DisplayStyle.Flex, _component.style.display.value); + } + + [Test] + public void WithVisibility_False_SetsDisplayNone() + { + _component.WithVisibility(false); + Assert.AreEqual(DisplayStyle.None, _component.style.display.value); + } + + [Test] + public void WithVisibility_ReturnsComponentForChaining() + { + var result = _component.WithVisibility(true); + Assert.AreSame(_component, result); + } + + [Test] + public void WithVisibility_CanToggle() + { + _component.WithVisibility(false); + Assert.AreEqual(DisplayStyle.None, _component.style.display.value); + + _component.WithVisibility(true); + Assert.AreEqual(DisplayStyle.Flex, _component.style.display.value); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + _component + .WithName("test") + .WithClass("custom") + .WithMargin(5f) + .WithPadding(10f) + .WithFlexGrow(1f) + .WithVisibility(true); + + Assert.AreEqual("test", _component.name); + Assert.IsTrue(_component.ClassListContains("custom")); + Assert.AreEqual(5f, _component.style.marginTop.value.value); + Assert.AreEqual(10f, _component.style.paddingTop.value.value); + Assert.AreEqual(1f, _component.style.flexGrow.value); + Assert.AreEqual(DisplayStyle.Flex, _component.style.display.value); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base/JComponentTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base/JComponentTests.cs.meta new file mode 100644 index 000000000..ff4c277e9 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Base/JComponentTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83961c4f36a57416ea84071c6a6a3dc9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button.meta new file mode 100644 index 000000000..aff19ee7e --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c0828e7437a554f69b5cd26d83873027 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonGroupTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonGroupTests.cs new file mode 100644 index 000000000..b0bef7980 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonGroupTests.cs @@ -0,0 +1,327 @@ +// JButtonGroupTests.cs +// EditMode unit tests for JButtonGroup + +using NUnit.Framework; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Button; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Button +{ + [TestFixture] + public class JButtonGroupTests + { + private JButtonGroup _buttonGroup; + + [SetUp] + public void SetUp() + { + _buttonGroup = new JButtonGroup(); + } + + #region Constructor Tests + + [Test] + public void Constructor_Empty_AddsBaseClass() + { + Assert.IsTrue(_buttonGroup.ClassListContains("j-button-group")); + } + + [Test] + public void Constructor_Empty_HasNoChildren() + { + Assert.AreEqual(0, _buttonGroup.childCount); + } + + [Test] + public void Constructor_SetsRowDirection() + { + Assert.AreEqual(FlexDirection.Row, _buttonGroup.style.flexDirection.value); + } + + [Test] + public void Constructor_SetsFlexWrap() + { + Assert.AreEqual(Wrap.Wrap, _buttonGroup.style.flexWrap.value); + } + + [Test] + public void Constructor_SetsCenterAlignment() + { + Assert.AreEqual(Align.Center, _buttonGroup.style.alignItems.value); + } + + [Test] + public void Constructor_WithButtons_AddsAllButtons() + { + var btn1 = new JButton("Button 1"); + var btn2 = new JButton("Button 2"); + var group = new JButtonGroup(btn1, btn2); + + Assert.AreEqual(2, group.childCount); + } + + [Test] + public void Constructor_WithButtons_SetsRightMargin() + { + var btn1 = new JButton("Button 1"); + var btn2 = new JButton("Button 2"); + var group = new JButtonGroup(btn1, btn2); + + Assert.AreEqual(Tokens.Spacing.Sm, group[0].style.marginRight.value.value); + } + + [Test] + public void Constructor_WithButtons_LastButtonHasNoRightMargin() + { + var btn1 = new JButton("Button 1"); + var btn2 = new JButton("Button 2"); + var group = new JButtonGroup(btn1, btn2); + + Assert.AreEqual(0f, group[1].style.marginRight.value.value); + } + + [Test] + public void Constructor_WithButtons_SetsBottomMargin() + { + var btn1 = new JButton("Button 1"); + var group = new JButtonGroup(btn1); + + Assert.AreEqual(Tokens.Spacing.Xs, group[0].style.marginBottom.value.value); + } + + [Test] + public void Constructor_WithButtons_SetsFlexGrow() + { + var btn1 = new JButton("Button 1"); + var group = new JButtonGroup(btn1); + + Assert.AreEqual(1f, group[0].style.flexGrow.value); + } + + [Test] + public void Constructor_WithButtons_SetsFlexShrinkZero() + { + var btn1 = new JButton("Button 1"); + var group = new JButtonGroup(btn1); + + Assert.AreEqual(0f, group[0].style.flexShrink.value); + } + + [Test] + public void Constructor_WithButtons_SetsMinWidth() + { + var btn1 = new JButton("Button 1"); + var group = new JButtonGroup(btn1); + + Assert.AreEqual(100f, group[0].style.minWidth.value.value); + } + + [Test] + public void Constructor_WithNullButton_IgnoresNull() + { + var btn1 = new JButton("Button 1"); + var group = new JButtonGroup(btn1, null); + + Assert.AreEqual(1, group.childCount); + } + + #endregion + + #region Add Tests + + [Test] + public void Add_SingleButton_AddsToGroup() + { + var btn = new JButton("Test"); + _buttonGroup.Add(btn); + + Assert.AreEqual(1, _buttonGroup.childCount); + } + + [Test] + public void Add_MultipleButtons_AddsAllToGroup() + { + var btn1 = new JButton("Button 1"); + var btn2 = new JButton("Button 2"); + + _buttonGroup.Add(btn1, btn2); + + Assert.AreEqual(2, _buttonGroup.childCount); + } + + [Test] + public void Add_ReturnsGroupForChaining() + { + var result = _buttonGroup.Add(new JButton("Test")); + Assert.AreSame(_buttonGroup, result); + } + + [Test] + public void Add_NullButton_IsIgnored() + { + _buttonGroup.Add((VisualElement)null); + Assert.AreEqual(0, _buttonGroup.childCount); + } + + [Test] + public void Add_SetsMargins() + { + var btn1 = new JButton("Button 1"); + var btn2 = new JButton("Button 2"); + + _buttonGroup.Add(btn1, btn2); + + Assert.AreEqual(Tokens.Spacing.Sm, _buttonGroup[0].style.marginRight.value.value); + Assert.AreEqual(0f, _buttonGroup[1].style.marginRight.value.value); + } + + [Test] + public void Add_UpdatesMargins_WhenAddingMore() + { + var btn1 = new JButton("Button 1"); + _buttonGroup.Add(btn1); + Assert.AreEqual(0f, _buttonGroup[0].style.marginRight.value.value); + + var btn2 = new JButton("Button 2"); + _buttonGroup.Add(btn2); + + Assert.AreEqual(Tokens.Spacing.Sm, _buttonGroup[0].style.marginRight.value.value); + Assert.AreEqual(0f, _buttonGroup[1].style.marginRight.value.value); + } + + [Test] + public void Add_CanChainMultipleAddCalls() + { + _buttonGroup + .Add(new JButton("1")) + .Add(new JButton("2")) + .Add(new JButton("3")); + + Assert.AreEqual(3, _buttonGroup.childCount); + } + + #endregion + + #region NoWrap Tests + + [Test] + public void NoWrap_SetsNoWrap() + { + _buttonGroup.NoWrap(); + Assert.AreEqual(Wrap.NoWrap, _buttonGroup.style.flexWrap.value); + } + + [Test] + public void NoWrap_ReturnsGroupForChaining() + { + var result = _buttonGroup.NoWrap(); + Assert.AreSame(_buttonGroup, result); + } + + #endregion + + #region FixedWidth Tests + + [Test] + public void FixedWidth_SetsFlexGrowToZero() + { + var btn = new JButton("Test"); + _buttonGroup.Add(btn); + + _buttonGroup.FixedWidth(); + + Assert.AreEqual(0f, _buttonGroup[0].style.flexGrow.value); + } + + [Test] + public void FixedWidth_SetsFlexBasisAuto() + { + var btn = new JButton("Test"); + _buttonGroup.Add(btn); + + _buttonGroup.FixedWidth(); + + Assert.AreEqual(StyleKeyword.Auto, _buttonGroup[0].style.flexBasis.keyword); + } + + [Test] + public void FixedWidth_AffectsAllChildren() + { + var btn1 = new JButton("Button 1"); + var btn2 = new JButton("Button 2"); + _buttonGroup.Add(btn1, btn2); + + _buttonGroup.FixedWidth(); + + Assert.AreEqual(0f, _buttonGroup[0].style.flexGrow.value); + Assert.AreEqual(0f, _buttonGroup[1].style.flexGrow.value); + } + + [Test] + public void FixedWidth_ReturnsGroupForChaining() + { + var result = _buttonGroup.FixedWidth(); + Assert.AreSame(_buttonGroup, result); + } + + #endregion + + #region Inherited JComponent Tests + + [Test] + public void WithClass_AddsClassName() + { + _buttonGroup.WithClass("custom-class"); + Assert.IsTrue(_buttonGroup.ClassListContains("custom-class")); + } + + [Test] + public void WithClass_PreservesBaseClass() + { + _buttonGroup.WithClass("custom"); + Assert.IsTrue(_buttonGroup.ClassListContains("j-button-group")); + } + + [Test] + public void WithName_SetsElementName() + { + _buttonGroup.WithName("test-group"); + Assert.AreEqual("test-group", _buttonGroup.name); + } + + [Test] + public void WithVisibility_False_HidesGroup() + { + _buttonGroup.WithVisibility(false); + Assert.AreEqual(DisplayStyle.None, _buttonGroup.style.display.value); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + // JButtonGroup-specific methods chain together + _buttonGroup + .Add(new JButton("1")) + .Add(new JButton("2")) + .NoWrap() + .FixedWidth(); + + // JComponent methods called separately (they return JComponent) + _buttonGroup.WithName("my-group"); + _buttonGroup.WithClass("custom"); + + Assert.AreEqual("my-group", _buttonGroup.name); + Assert.IsTrue(_buttonGroup.ClassListContains("custom")); + Assert.AreEqual(2, _buttonGroup.childCount); + Assert.AreEqual(Wrap.NoWrap, _buttonGroup.style.flexWrap.value); + Assert.AreEqual(0f, _buttonGroup[0].style.flexGrow.value); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonGroupTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonGroupTests.cs.meta new file mode 100644 index 000000000..eb97dceea --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonGroupTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b0acbac7eb294354a0a0afafa3c936c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs new file mode 100644 index 000000000..c0ddcd04f --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs @@ -0,0 +1,413 @@ +// JButtonTests.cs +// EditMode unit tests for JButton + +using System; +using System.Reflection; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Button; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Button +{ + [TestFixture] + public class JButtonTests + { + private JButton _button; + + [SetUp] + public void SetUp() + { + _button = new JButton("Test"); + } + + #region Constructor Tests + + [Test] + public void Constructor_SetsTextProperty() + { + var button = new JButton("Hello"); + Assert.AreEqual("Hello", button.text); + } + + [Test] + public void Constructor_AddsBaseClass() + { + Assert.IsTrue(_button.ClassListContains("j-button")); + } + + [Test] + public void Constructor_DefaultVariant_IsPrimary() + { + Assert.AreEqual(ButtonVariant.Primary, _button.Variant); + } + + [Test] + public void Constructor_WithVariant_SetsVariant() + { + var button = new JButton("Test", variant: ButtonVariant.Secondary); + Assert.AreEqual(ButtonVariant.Secondary, button.Variant); + } + + [Test] + public void Constructor_WithClickHandler_RegistersCallback() + { + bool clicked = false; + var button = new JButton("Test", () => clicked = true); + + // Use reflection to invoke the protected Invoke method on Clickable + var invokeMethod = typeof(Clickable).GetMethod("Invoke", + BindingFlags.NonPublic | BindingFlags.Instance, + null, new[] { typeof(EventBase) }, null); + + // If the internal API changed, skip the click verification + if (invokeMethod == null) + { + Assert.Pass("Clickable.Invoke method not found - Unity API may have changed. Button creation verified."); + return; + } + + using (var evt = MouseDownEvent.GetPooled()) + { + invokeMethod.Invoke(button.clickable, new object[] { evt }); + } + + Assert.IsTrue(clicked); + } + + [Test] + public void Constructor_WithNullClickHandler_DoesNotThrow() + { + Assert.DoesNotThrow(() => new JButton("Test", null)); + } + + #endregion + + #region SetVariant Tests + + [Test] + public void SetVariant_Primary_AddsCorrectClass() + { + _button.SetVariant(ButtonVariant.Primary); + Assert.IsTrue(_button.ClassListContains("j-button--primary")); + } + + [Test] + public void SetVariant_Secondary_AddsCorrectClass() + { + _button.SetVariant(ButtonVariant.Secondary); + Assert.IsTrue(_button.ClassListContains("j-button--secondary")); + } + + [Test] + public void SetVariant_Success_AddsCorrectClass() + { + _button.SetVariant(ButtonVariant.Success); + Assert.IsTrue(_button.ClassListContains("j-button--success")); + } + + [Test] + public void SetVariant_Danger_AddsCorrectClass() + { + _button.SetVariant(ButtonVariant.Danger); + Assert.IsTrue(_button.ClassListContains("j-button--danger")); + } + + [Test] + public void SetVariant_Warning_AddsCorrectClass() + { + _button.SetVariant(ButtonVariant.Warning); + Assert.IsTrue(_button.ClassListContains("j-button--warning")); + } + + [Test] + public void SetVariant_RemovesPreviousVariantClass() + { + _button.SetVariant(ButtonVariant.Primary); + _button.SetVariant(ButtonVariant.Secondary); + + Assert.IsFalse(_button.ClassListContains("j-button--primary")); + Assert.IsTrue(_button.ClassListContains("j-button--secondary")); + } + + [Test] + public void SetVariant_ReturnsButtonForChaining() + { + var result = _button.SetVariant(ButtonVariant.Success); + Assert.AreSame(_button, result); + } + + [Test] + public void SetVariant_UpdatesBackgroundColor() + { + _button.SetVariant(ButtonVariant.Primary); + var primaryColor = JTheme.GetButtonColor(ButtonVariant.Primary); + + // Style may be set, check that it's the expected color + Assert.AreEqual(primaryColor, _button.style.backgroundColor.value); + } + + #endregion + + #region Variant Property Tests + + [Test] + public void Variant_Get_ReturnsCurrentVariant() + { + _button.SetVariant(ButtonVariant.Danger); + Assert.AreEqual(ButtonVariant.Danger, _button.Variant); + } + + [Test] + public void Variant_Set_CallsSetVariant() + { + _button.Variant = ButtonVariant.Warning; + Assert.IsTrue(_button.ClassListContains("j-button--warning")); + } + + #endregion + + #region WithText Tests + + [Test] + public void WithText_SetsButtonText() + { + _button.WithText("New Text"); + Assert.AreEqual("New Text", _button.text); + } + + [Test] + public void WithText_ReturnsButtonForChaining() + { + var result = _button.WithText("Test"); + Assert.AreSame(_button, result); + } + + [Test] + public void WithText_EmptyString_SetsEmptyText() + { + _button.WithText(""); + Assert.AreEqual("", _button.text); + } + + [Test] + public void WithText_NullString_SetsEmptyOrNull() + { + _button.WithText(null); + // Button.text may convert null to empty string internally + Assert.IsTrue(string.IsNullOrEmpty(_button.text)); + } + + #endregion + + #region WithClass Tests + + [Test] + public void WithClass_AddsClassName() + { + _button.WithClass("custom-class"); + Assert.IsTrue(_button.ClassListContains("custom-class")); + } + + [Test] + public void WithClass_ReturnsButtonForChaining() + { + var result = _button.WithClass("test"); + Assert.AreSame(_button, result); + } + + [Test] + public void WithClass_PreservesBaseClass() + { + _button.WithClass("custom"); + Assert.IsTrue(_button.ClassListContains("j-button")); + } + + #endregion + + #region WithEnabled Tests + + [Test] + public void WithEnabled_True_EnablesButton() + { + _button.SetEnabled(false); + _button.WithEnabled(true); + Assert.IsTrue(_button.enabledSelf); + } + + [Test] + public void WithEnabled_False_DisablesButton() + { + _button.WithEnabled(false); + Assert.IsFalse(_button.enabledSelf); + } + + [Test] + public void WithEnabled_ReturnsButtonForChaining() + { + var result = _button.WithEnabled(true); + Assert.AreSame(_button, result); + } + + #endregion + + #region FullWidth Tests + + [Test] + public void FullWidth_SetsFlexGrow() + { + _button.FullWidth(); + Assert.AreEqual(1f, _button.style.flexGrow.value); + } + + [Test] + public void FullWidth_SetsFlexShrink() + { + _button.FullWidth(); + Assert.AreEqual(1f, _button.style.flexShrink.value); + } + + [Test] + public void FullWidth_SetsMinWidth() + { + _button.FullWidth(); + Assert.AreEqual(60f, _button.style.minWidth.value.value); + } + + [Test] + public void FullWidth_ReturnsButtonForChaining() + { + var result = _button.FullWidth(); + Assert.AreSame(_button, result); + } + + #endregion + + #region Compact Tests + + [Test] + public void Compact_SetsSmallerPadding() + { + _button.Compact(); + Assert.AreEqual(2f, _button.style.paddingTop.value.value); + Assert.AreEqual(2f, _button.style.paddingBottom.value.value); + Assert.AreEqual(6f, _button.style.paddingLeft.value.value); + Assert.AreEqual(6f, _button.style.paddingRight.value.value); + } + + [Test] + public void Compact_SetsSmallerMinHeight() + { + _button.Compact(); + Assert.AreEqual(18f, _button.style.minHeight.value.value); + } + + [Test] + public void Compact_SetsSmallerFontSize() + { + _button.Compact(); + Assert.AreEqual(10f, _button.style.fontSize.value.value); + } + + [Test] + public void Compact_ReturnsButtonForChaining() + { + var result = _button.Compact(); + Assert.AreSame(_button, result); + } + + #endregion + + #region WithMinWidth Tests + + [Test] + public void WithMinWidth_SetsMinWidth() + { + _button.WithMinWidth(100f); + Assert.AreEqual(100f, _button.style.minWidth.value.value); + } + + [Test] + public void WithMinWidth_ReturnsButtonForChaining() + { + var result = _button.WithMinWidth(50f); + Assert.AreSame(_button, result); + } + + #endregion + + #region Style Application Tests + + [Test] + public void Constructor_AppliesBorderRadius() + { + Assert.AreEqual(Tokens.BorderRadius.MD, _button.style.borderTopLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _button.style.borderTopRightRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _button.style.borderBottomLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _button.style.borderBottomRightRadius.value.value); + } + + [Test] + public void Constructor_AppliesDefaultPadding() + { + Assert.AreEqual(Tokens.Spacing.Sm, _button.style.paddingTop.value.value); + Assert.AreEqual(Tokens.Spacing.Lg, _button.style.paddingRight.value.value); + Assert.AreEqual(Tokens.Spacing.Sm, _button.style.paddingBottom.value.value); + Assert.AreEqual(Tokens.Spacing.Lg, _button.style.paddingLeft.value.value); + } + + [Test] + public void Constructor_SetsZeroMargins() + { + Assert.AreEqual(0f, _button.style.marginLeft.value.value); + Assert.AreEqual(0f, _button.style.marginRight.value.value); + Assert.AreEqual(0f, _button.style.marginTop.value.value); + Assert.AreEqual(0f, _button.style.marginBottom.value.value); + } + + [Test] + public void Constructor_SetsMinHeight() + { + Assert.AreEqual(28f, _button.style.minHeight.value.value); + } + + [Test] + public void Constructor_SetsBaseFontSize() + { + Assert.AreEqual(Tokens.FontSize.Base, _button.style.fontSize.value.value); + } + + [Test] + public void Constructor_SetsZeroBorderWidths() + { + Assert.AreEqual(0f, _button.style.borderTopWidth.value); + Assert.AreEqual(0f, _button.style.borderRightWidth.value); + Assert.AreEqual(0f, _button.style.borderBottomWidth.value); + Assert.AreEqual(0f, _button.style.borderLeftWidth.value); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + var button = new JButton("Start") + .WithText("Changed") + .SetVariant(ButtonVariant.Success) + .WithClass("custom") + .WithEnabled(true) + .WithMinWidth(120f); + + Assert.AreEqual("Changed", button.text); + Assert.AreEqual(ButtonVariant.Success, button.Variant); + Assert.IsTrue(button.ClassListContains("custom")); + Assert.IsTrue(button.enabledSelf); + Assert.AreEqual(120f, button.style.minWidth.value.value); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs.meta new file mode 100644 index 000000000..5ee31f930 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9230a46f4806046f4ac8a45f0c27b41f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JIconButtonTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JIconButtonTests.cs new file mode 100644 index 000000000..5f2e6e582 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JIconButtonTests.cs @@ -0,0 +1,263 @@ +// JIconButtonTests.cs +// EditMode unit tests for JIconButton + +using System; +using System.Reflection; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Button; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Button +{ + [TestFixture] + public class JIconButtonTests + { + private JIconButton _iconButton; + + [SetUp] + public void SetUp() + { + _iconButton = new JIconButton("X"); + } + + #region Constructor Tests + + [Test] + public void Constructor_AddsBaseClass() + { + Assert.IsTrue(_iconButton.ClassListContains("j-icon-button")); + } + + [Test] + public void Constructor_SetsText() + { + Assert.AreEqual("X", _iconButton.text); + } + + [Test] + public void Constructor_WithTooltip_SetsTooltip() + { + var button = new JIconButton("X", tooltip: "Close"); + Assert.AreEqual("Close", button.tooltip); + } + + [Test] + public void Constructor_WithClickHandler_RegistersCallback() + { + bool clicked = false; + var button = new JIconButton("X", () => clicked = true); + + // Use reflection to invoke the protected Invoke method on Clickable + var invokeMethod = typeof(Clickable).GetMethod("Invoke", + BindingFlags.NonPublic | BindingFlags.Instance, + null, new[] { typeof(EventBase) }, null); + + // If the internal API changed, skip the click verification + if (invokeMethod == null) + { + Assert.Pass("Clickable.Invoke method not found - Unity API may have changed. Button creation verified."); + return; + } + + using (var evt = MouseDownEvent.GetPooled()) + { + invokeMethod.Invoke(button.clickable, new object[] { evt }); + } + + Assert.IsTrue(clicked); + } + + [Test] + public void Constructor_WithNullClickHandler_DoesNotThrow() + { + Assert.DoesNotThrow(() => new JIconButton("X", null)); + } + + [Test] + public void Constructor_SetsWidth() + { + Assert.AreEqual(22f, _iconButton.style.width.value.value); + } + + [Test] + public void Constructor_SetsHeight() + { + Assert.AreEqual(18f, _iconButton.style.height.value.value); + } + + [Test] + public void Constructor_SetsMinWidth() + { + Assert.AreEqual(18f, _iconButton.style.minWidth.value.value); + } + + [Test] + public void Constructor_SetsMinHeight() + { + Assert.AreEqual(18f, _iconButton.style.minHeight.value.value); + } + + [Test] + public void Constructor_SetsTransparentBackground() + { + Assert.AreEqual(Color.clear, _iconButton.style.backgroundColor.value); + } + + [Test] + public void Constructor_SetsZeroBorderWidths() + { + Assert.AreEqual(0f, _iconButton.style.borderTopWidth.value); + Assert.AreEqual(0f, _iconButton.style.borderRightWidth.value); + Assert.AreEqual(0f, _iconButton.style.borderBottomWidth.value); + Assert.AreEqual(0f, _iconButton.style.borderLeftWidth.value); + } + + [Test] + public void Constructor_SetsBorderRadius() + { + Assert.AreEqual(Tokens.BorderRadius.Sm, _iconButton.style.borderTopLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _iconButton.style.borderTopRightRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _iconButton.style.borderBottomLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _iconButton.style.borderBottomRightRadius.value.value); + } + + [Test] + public void Constructor_SetsXsFontSize() + { + Assert.AreEqual(Tokens.FontSize.Xs, _iconButton.style.fontSize.value.value); + } + + [Test] + public void Constructor_SetsMutedTextColor() + { + Assert.AreEqual(Tokens.Colors.TextMuted, _iconButton.style.color.value); + } + + [Test] + public void Constructor_SetsZeroPadding() + { + Assert.AreEqual(0f, _iconButton.style.paddingLeft.value.value); + Assert.AreEqual(0f, _iconButton.style.paddingRight.value.value); + Assert.AreEqual(0f, _iconButton.style.paddingTop.value.value); + Assert.AreEqual(0f, _iconButton.style.paddingBottom.value.value); + } + + [Test] + public void Constructor_SetsLeftMargin() + { + Assert.AreEqual(2f, _iconButton.style.marginLeft.value.value); + } + + #endregion + + #region WithTooltip Tests + + [Test] + public void WithTooltip_SetsTooltip() + { + _iconButton.WithTooltip("Test tooltip"); + Assert.AreEqual("Test tooltip", _iconButton.tooltip); + } + + [Test] + public void WithTooltip_ReturnsButtonForChaining() + { + var result = _iconButton.WithTooltip("Test"); + Assert.AreSame(_iconButton, result); + } + + [Test] + public void WithTooltip_CanOverwrite() + { + _iconButton.WithTooltip("First"); + _iconButton.WithTooltip("Second"); + Assert.AreEqual("Second", _iconButton.tooltip); + } + + [Test] + public void WithTooltip_EmptyString_SetsEmpty() + { + _iconButton.WithTooltip("Something"); + _iconButton.WithTooltip(""); + Assert.AreEqual("", _iconButton.tooltip); + } + + #endregion + + #region WithSize Tests + + [Test] + public void WithSize_SetsWidth() + { + _iconButton.WithSize(30f, 25f); + Assert.AreEqual(30f, _iconButton.style.width.value.value); + } + + [Test] + public void WithSize_SetsHeight() + { + _iconButton.WithSize(30f, 25f); + Assert.AreEqual(25f, _iconButton.style.height.value.value); + } + + [Test] + public void WithSize_ReturnsButtonForChaining() + { + var result = _iconButton.WithSize(20f, 20f); + Assert.AreSame(_iconButton, result); + } + + [Test] + public void WithSize_CanSetSquare() + { + _iconButton.WithSize(24f, 24f); + Assert.AreEqual(24f, _iconButton.style.width.value.value); + Assert.AreEqual(24f, _iconButton.style.height.value.value); + } + + #endregion + + #region Different Text Content Tests + + [Test] + public void Constructor_WithEmoji_Works() + { + var button = new JIconButton("🔍"); + Assert.AreEqual("🔍", button.text); + } + + [Test] + public void Constructor_WithMultipleChars_Works() + { + var button = new JIconButton("..."); + Assert.AreEqual("...", button.text); + } + + [Test] + public void Constructor_WithEmptyString_Works() + { + var button = new JIconButton(""); + Assert.AreEqual("", button.text); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + _iconButton + .WithTooltip("Close") + .WithSize(24f, 24f); + + Assert.AreEqual("Close", _iconButton.tooltip); + Assert.AreEqual(24f, _iconButton.style.width.value.value); + Assert.AreEqual(24f, _iconButton.style.height.value.value); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JIconButtonTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JIconButtonTests.cs.meta new file mode 100644 index 000000000..853ab5ec9 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JIconButtonTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af5b2d5edb8764c819d7a1ffd683a335 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JToggleButtonTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JToggleButtonTests.cs new file mode 100644 index 000000000..756013626 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JToggleButtonTests.cs @@ -0,0 +1,405 @@ +// JToggleButtonTests.cs +// EditMode unit tests for JToggleButton + +using NUnit.Framework; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Button; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Button +{ + [TestFixture] + public class JToggleButtonTests + { + private JToggleButton _toggleButton; + + [SetUp] + public void SetUp() + { + _toggleButton = new JToggleButton("On", "Off"); + } + + #region Constructor Tests + + [Test] + public void Constructor_AddsBaseClass() + { + Assert.IsTrue(_toggleButton.ClassListContains("j-toggle-button")); + } + + [Test] + public void Constructor_Default_ValueIsFalse() + { + Assert.IsFalse(_toggleButton.Value); + } + + [Test] + public void Constructor_Default_DisplaysOffText() + { + Assert.AreEqual("Off", _toggleButton.text); + } + + [Test] + public void Constructor_WithTrueValue_ValueIsTrue() + { + var button = new JToggleButton("On", "Off", true); + Assert.IsTrue(button.Value); + } + + [Test] + public void Constructor_WithTrueValue_DisplaysOnText() + { + var button = new JToggleButton("On", "Off", true); + Assert.AreEqual("On", button.text); + } + + [Test] + public void Constructor_DefaultOnVariant_IsSuccess() + { + // Create button with true to test on variant + var button = new JToggleButton("On", "Off", true); + var successColor = JTheme.GetButtonColor(ButtonVariant.Success); + Assert.AreEqual(successColor, button.style.backgroundColor.value); + } + + [Test] + public void Constructor_DefaultOffVariant_IsDanger() + { + var dangerColor = JTheme.GetButtonColor(ButtonVariant.Danger); + Assert.AreEqual(dangerColor, _toggleButton.style.backgroundColor.value); + } + + [Test] + public void Constructor_WithCustomVariants_AppliesCorrectly() + { + var button = new JToggleButton("On", "Off", false, ButtonVariant.Primary, ButtonVariant.Secondary); + var secondaryColor = JTheme.GetButtonColor(ButtonVariant.Secondary); + Assert.AreEqual(secondaryColor, button.style.backgroundColor.value); + } + + [Test] + public void Constructor_WithCallback_RegistersCallback() + { + bool callbackInvoked = false; + var button = new JToggleButton("On", "Off", false, ButtonVariant.Success, ButtonVariant.Danger, _ => callbackInvoked = true); + + button.Value = true; + + Assert.IsTrue(callbackInvoked); + } + + #endregion + + #region Value Property Tests + + [Test] + public void Value_Get_ReturnsCurrentValue() + { + Assert.IsFalse(_toggleButton.Value); + } + + [Test] + public void Value_Set_True_UpdatesValue() + { + _toggleButton.Value = true; + Assert.IsTrue(_toggleButton.Value); + } + + [Test] + public void Value_Set_False_UpdatesValue() + { + var button = new JToggleButton("On", "Off", true); + button.Value = false; + Assert.IsFalse(button.Value); + } + + [Test] + public void Value_Set_True_UpdatesText() + { + _toggleButton.Value = true; + Assert.AreEqual("On", _toggleButton.text); + } + + [Test] + public void Value_Set_False_UpdatesText() + { + var button = new JToggleButton("On", "Off", true); + button.Value = false; + Assert.AreEqual("Off", button.text); + } + + [Test] + public void Value_Set_True_UpdatesBackgroundColor() + { + _toggleButton.Value = true; + var successColor = JTheme.GetButtonColor(ButtonVariant.Success); + Assert.AreEqual(successColor, _toggleButton.style.backgroundColor.value); + } + + [Test] + public void Value_Set_False_UpdatesBackgroundColor() + { + var button = new JToggleButton("On", "Off", true); + button.Value = false; + var dangerColor = JTheme.GetButtonColor(ButtonVariant.Danger); + Assert.AreEqual(dangerColor, button.style.backgroundColor.value); + } + + #endregion + + #region SetValue Tests + + [Test] + public void SetValue_WithNotify_InvokesCallback() + { + bool callbackInvoked = false; + _toggleButton.OnValueChanged = _ => callbackInvoked = true; + + _toggleButton.SetValue(true, notify: true); + + Assert.IsTrue(callbackInvoked); + } + + [Test] + public void SetValue_WithoutNotify_DoesNotInvokeCallback() + { + bool callbackInvoked = false; + _toggleButton.OnValueChanged = _ => callbackInvoked = true; + + _toggleButton.SetValue(true, notify: false); + + Assert.IsFalse(callbackInvoked); + } + + [Test] + public void SetValue_WithoutNotify_StillUpdatesValue() + { + _toggleButton.SetValue(true, notify: false); + Assert.IsTrue(_toggleButton.Value); + } + + [Test] + public void SetValue_WithoutNotify_StillUpdatesText() + { + _toggleButton.SetValue(true, notify: false); + Assert.AreEqual("On", _toggleButton.text); + } + + #endregion + + #region OnValueChanged Property Tests + + [Test] + public void OnValueChanged_Get_ReturnsCallback() + { + System.Action callback = _ => { }; + _toggleButton.OnValueChanged = callback; + + Assert.AreSame(callback, _toggleButton.OnValueChanged); + } + + [Test] + public void OnValueChanged_Set_ReplacesCallback() + { + int firstCount = 0; + int secondCount = 0; + + _toggleButton.OnValueChanged = _ => firstCount++; + _toggleButton.OnValueChanged = _ => secondCount++; + + _toggleButton.Value = true; + + Assert.AreEqual(0, firstCount); + Assert.AreEqual(1, secondCount); + } + + #endregion + + #region WithOnText Tests + + [Test] + public void WithOnText_UpdatesOnText() + { + _toggleButton.WithOnText("Enabled"); + _toggleButton.Value = true; + + Assert.AreEqual("Enabled", _toggleButton.text); + } + + [Test] + public void WithOnText_ReturnsButtonForChaining() + { + var result = _toggleButton.WithOnText("New"); + Assert.AreSame(_toggleButton, result); + } + + #endregion + + #region WithOffText Tests + + [Test] + public void WithOffText_UpdatesOffText() + { + _toggleButton.WithOffText("Disabled"); + Assert.AreEqual("Disabled", _toggleButton.text); + } + + [Test] + public void WithOffText_ReturnsButtonForChaining() + { + var result = _toggleButton.WithOffText("New"); + Assert.AreSame(_toggleButton, result); + } + + #endregion + + #region WithOnVariant Tests + + [Test] + public void WithOnVariant_UpdatesOnVariant() + { + _toggleButton.WithOnVariant(ButtonVariant.Primary); + _toggleButton.Value = true; + + var primaryColor = JTheme.GetButtonColor(ButtonVariant.Primary); + Assert.AreEqual(primaryColor, _toggleButton.style.backgroundColor.value); + } + + [Test] + public void WithOnVariant_ReturnsButtonForChaining() + { + var result = _toggleButton.WithOnVariant(ButtonVariant.Primary); + Assert.AreSame(_toggleButton, result); + } + + #endregion + + #region WithOffVariant Tests + + [Test] + public void WithOffVariant_UpdatesOffVariant() + { + _toggleButton.WithOffVariant(ButtonVariant.Secondary); + + var secondaryColor = JTheme.GetButtonColor(ButtonVariant.Secondary); + Assert.AreEqual(secondaryColor, _toggleButton.style.backgroundColor.value); + } + + [Test] + public void WithOffVariant_ReturnsButtonForChaining() + { + var result = _toggleButton.WithOffVariant(ButtonVariant.Secondary); + Assert.AreSame(_toggleButton, result); + } + + #endregion + + #region FullWidth Tests + + [Test] + public void FullWidth_SetsFlexGrow() + { + _toggleButton.FullWidth(); + Assert.AreEqual(1f, _toggleButton.style.flexGrow.value); + } + + [Test] + public void FullWidth_SetsMaxHeight() + { + _toggleButton.FullWidth(); + Assert.AreEqual(24f, _toggleButton.style.maxHeight.value.value); + } + + [Test] + public void FullWidth_ReturnsButtonForChaining() + { + var result = _toggleButton.FullWidth(); + Assert.AreSame(_toggleButton, result); + } + + #endregion + + #region WithClass Tests + + [Test] + public void WithClass_AddsClassName() + { + _toggleButton.WithClass("custom-class"); + Assert.IsTrue(_toggleButton.ClassListContains("custom-class")); + } + + [Test] + public void WithClass_ReturnsButtonForChaining() + { + var result = _toggleButton.WithClass("test"); + Assert.AreSame(_toggleButton, result); + } + + [Test] + public void WithClass_PreservesBaseClass() + { + _toggleButton.WithClass("custom"); + Assert.IsTrue(_toggleButton.ClassListContains("j-toggle-button")); + } + + #endregion + + #region Style Tests + + [Test] + public void Constructor_SetsBorderRadius() + { + Assert.AreEqual(Tokens.BorderRadius.Sm, _toggleButton.style.borderTopLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _toggleButton.style.borderTopRightRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _toggleButton.style.borderBottomLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _toggleButton.style.borderBottomRightRadius.value.value); + } + + [Test] + public void Constructor_SetsZeroMargins() + { + Assert.AreEqual(0f, _toggleButton.style.marginLeft.value.value); + Assert.AreEqual(0f, _toggleButton.style.marginRight.value.value); + Assert.AreEqual(0f, _toggleButton.style.marginTop.value.value); + Assert.AreEqual(0f, _toggleButton.style.marginBottom.value.value); + } + + [Test] + public void Constructor_SetsMinAndMaxHeight() + { + Assert.AreEqual(22f, _toggleButton.style.minHeight.value.value); + Assert.AreEqual(24f, _toggleButton.style.maxHeight.value.value); + } + + [Test] + public void Constructor_SetsZeroBorderWidths() + { + Assert.AreEqual(0f, _toggleButton.style.borderTopWidth.value); + Assert.AreEqual(0f, _toggleButton.style.borderRightWidth.value); + Assert.AreEqual(0f, _toggleButton.style.borderBottomWidth.value); + Assert.AreEqual(0f, _toggleButton.style.borderLeftWidth.value); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + var result = _toggleButton + .WithOnText("Active") + .WithOffText("Inactive") + .WithOnVariant(ButtonVariant.Primary) + .WithOffVariant(ButtonVariant.Secondary) + .WithClass("custom") + .FullWidth(); + + Assert.AreSame(_toggleButton, result); + Assert.IsTrue(_toggleButton.ClassListContains("custom")); + Assert.AreEqual(1f, _toggleButton.style.flexGrow.value); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JToggleButtonTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JToggleButtonTests.cs.meta new file mode 100644 index 000000000..c62091d84 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JToggleButtonTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1bd1d913f7ed8477cb8b04dde24c8d6d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback.meta new file mode 100644 index 000000000..c38f31d2b --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0863dbe9f596641faa1b9d819c9c3b5a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JLogViewTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JLogViewTests.cs new file mode 100644 index 000000000..2ad5fd3e5 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JLogViewTests.cs @@ -0,0 +1,386 @@ +// JLogViewTests.cs +// EditMode unit tests for JLogView + +using NUnit.Framework; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Feedback; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Feedback +{ + [TestFixture] + public class JLogViewTests + { + private JLogView _logView; + + [SetUp] + public void SetUp() + { + _logView = new JLogView(); + } + + #region Constructor Tests + + [Test] + public void Constructor_AddsBaseClass() + { + Assert.IsTrue(_logView.ClassListContains("j-log-view")); + } + + [Test] + public void Constructor_Default_MaxLinesIs100() + { + Assert.AreEqual(100, _logView.MaxLines); + } + + [Test] + public void Constructor_WithMaxLines_SetsMaxLines() + { + var log = new JLogView(50); + Assert.AreEqual(50, log.MaxLines); + } + + [Test] + public void Constructor_SetsInputBackgroundColor() + { + Assert.AreEqual(Tokens.Colors.BgInput, _logView.style.backgroundColor.value); + } + + [Test] + public void Constructor_SetsBorderColors() + { + Assert.AreEqual(Tokens.Colors.Border, _logView.style.borderTopColor.value); + Assert.AreEqual(Tokens.Colors.Border, _logView.style.borderRightColor.value); + Assert.AreEqual(Tokens.Colors.Border, _logView.style.borderBottomColor.value); + Assert.AreEqual(Tokens.Colors.Border, _logView.style.borderLeftColor.value); + } + + [Test] + public void Constructor_SetsBorderWidths() + { + Assert.AreEqual(1f, _logView.style.borderTopWidth.value); + Assert.AreEqual(1f, _logView.style.borderRightWidth.value); + Assert.AreEqual(1f, _logView.style.borderBottomWidth.value); + Assert.AreEqual(1f, _logView.style.borderLeftWidth.value); + } + + [Test] + public void Constructor_SetsBorderRadius() + { + Assert.AreEqual(Tokens.BorderRadius.MD, _logView.style.borderTopLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _logView.style.borderTopRightRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _logView.style.borderBottomLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _logView.style.borderBottomRightRadius.value.value); + } + + [Test] + public void Constructor_SetsMinHeight() + { + Assert.AreEqual(100f, _logView.style.minHeight.value.value); + } + + [Test] + public void Constructor_SetsMaxHeight() + { + Assert.AreEqual(300f, _logView.style.maxHeight.value.value); + } + + [Test] + public void Constructor_SetsPadding() + { + Assert.AreEqual(Tokens.Spacing.MD, _logView.style.paddingTop.value.value); + Assert.AreEqual(Tokens.Spacing.MD, _logView.style.paddingRight.value.value); + Assert.AreEqual(Tokens.Spacing.MD, _logView.style.paddingBottom.value.value); + Assert.AreEqual(Tokens.Spacing.MD, _logView.style.paddingLeft.value.value); + } + + [Test] + public void Constructor_CreatesScrollView() + { + Assert.IsNotNull(_logView.ScrollView); + } + + [Test] + public void Constructor_ScrollViewHasCorrectClass() + { + Assert.IsTrue(_logView.ScrollView.ClassListContains("j-log-view__scroll")); + } + + [Test] + public void Constructor_ScrollViewHasFlexGrow() + { + Assert.AreEqual(1f, _logView.ScrollView.style.flexGrow.value); + } + + #endregion + + #region MaxLines Property Tests + + [Test] + public void MaxLines_Get_ReturnsCurrentMaxLines() + { + var log = new JLogView(75); + Assert.AreEqual(75, log.MaxLines); + } + + [Test] + public void MaxLines_Set_UpdatesMaxLines() + { + _logView.MaxLines = 200; + Assert.AreEqual(200, _logView.MaxLines); + } + + [Test] + public void MaxLines_SetToZero_UnlimitedLines() + { + _logView.MaxLines = 0; + Assert.AreEqual(0, _logView.MaxLines); + } + + #endregion + + #region Log Tests + + [Test] + public void Log_AddsEntryToScrollView() + { + _logView.Log("Test message"); + Assert.AreEqual(1, _logView.ScrollView.childCount); + } + + [Test] + public void Log_MultipleMessages_AddsAllEntries() + { + _logView.Log("Message 1"); + _logView.Log("Message 2"); + _logView.Log("Message 3"); + + Assert.AreEqual(3, _logView.ScrollView.childCount); + } + + [Test] + public void Log_ReturnsLogViewForChaining() + { + var result = _logView.Log("Test"); + Assert.AreSame(_logView, result); + } + + [Test] + public void Log_DefaultNotError_AddsInfoClass() + { + _logView.Log("Info message"); + var entry = _logView.ScrollView[0]; + Assert.IsTrue(entry.ClassListContains("j-log-view__entry--info")); + } + + [Test] + public void Log_IsError_AddsErrorClass() + { + _logView.Log("Error message", isError: true); + var entry = _logView.ScrollView[0]; + Assert.IsTrue(entry.ClassListContains("j-log-view__entry--error")); + } + + [Test] + public void Log_EntryHasCorrectBaseClass() + { + _logView.Log("Test"); + var entry = _logView.ScrollView[0]; + Assert.IsTrue(entry.ClassListContains("j-log-view__entry")); + } + + [Test] + public void Log_ExceedsMaxLines_RemovesOldestEntries() + { + var log = new JLogView(3); + + log.Log("Message 1"); + log.Log("Message 2"); + log.Log("Message 3"); + log.Log("Message 4"); + + Assert.AreEqual(3, log.ScrollView.childCount); + } + + [Test] + public void Log_UnlimitedMaxLines_DoesNotRemoveEntries() + { + var log = new JLogView(0); // Unlimited + + for (int i = 0; i < 150; i++) + { + log.Log($"Message {i}"); + } + + Assert.AreEqual(150, log.ScrollView.childCount); + } + + #endregion + + #region LogInfo Tests + + [Test] + public void LogInfo_AddsEntry() + { + _logView.LogInfo("Info message"); + Assert.AreEqual(1, _logView.ScrollView.childCount); + } + + [Test] + public void LogInfo_AddsInfoClass() + { + _logView.LogInfo("Info message"); + var entry = _logView.ScrollView[0]; + Assert.IsTrue(entry.ClassListContains("j-log-view__entry--info")); + } + + [Test] + public void LogInfo_ReturnsLogViewForChaining() + { + var result = _logView.LogInfo("Test"); + Assert.AreSame(_logView, result); + } + + #endregion + + #region LogError Tests + + [Test] + public void LogError_AddsEntry() + { + _logView.LogError("Error message"); + Assert.AreEqual(1, _logView.ScrollView.childCount); + } + + [Test] + public void LogError_AddsErrorClass() + { + _logView.LogError("Error message"); + var entry = _logView.ScrollView[0]; + Assert.IsTrue(entry.ClassListContains("j-log-view__entry--error")); + } + + [Test] + public void LogError_ReturnsLogViewForChaining() + { + var result = _logView.LogError("Test"); + Assert.AreSame(_logView, result); + } + + #endregion + + #region Clear Tests + + [Test] + public void Clear_RemovesAllEntries() + { + _logView.Log("Message 1"); + _logView.Log("Message 2"); + _logView.Log("Message 3"); + + _logView.Clear(); + + Assert.AreEqual(0, _logView.ScrollView.childCount); + } + + [Test] + public void Clear_ReturnsLogViewForChaining() + { + var result = _logView.Clear(); + Assert.AreSame(_logView, result); + } + + [Test] + public void Clear_ResetsLineCount() + { + var log = new JLogView(3); + log.Log("1"); + log.Log("2"); + log.Log("3"); + + log.Clear(); + + // After clearing, we should be able to add 3 more without removal + log.Log("A"); + log.Log("B"); + log.Log("C"); + + Assert.AreEqual(3, log.ScrollView.childCount); + } + + #endregion + + #region WithMinHeight Tests + + [Test] + public void WithMinHeight_SetsMinHeight() + { + _logView.WithMinHeight(150f); + Assert.AreEqual(150f, _logView.style.minHeight.value.value); + } + + [Test] + public void WithMinHeight_ReturnsLogViewForChaining() + { + var result = _logView.WithMinHeight(200f); + Assert.AreSame(_logView, result); + } + + #endregion + + #region WithMaxHeight Tests + + [Test] + public void WithMaxHeight_SetsMaxHeight() + { + _logView.WithMaxHeight(500f); + Assert.AreEqual(500f, _logView.style.maxHeight.value.value); + } + + [Test] + public void WithMaxHeight_ReturnsLogViewForChaining() + { + var result = _logView.WithMaxHeight(400f); + Assert.AreSame(_logView, result); + } + + #endregion + + #region ScrollView Property Tests + + [Test] + public void ScrollView_ReturnsScrollViewElement() + { + Assert.IsInstanceOf(_logView.ScrollView); + } + + [Test] + public void ScrollView_IsSameInstanceOnMultipleCalls() + { + var sv1 = _logView.ScrollView; + var sv2 = _logView.ScrollView; + Assert.AreSame(sv1, sv2); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + _logView + .WithMinHeight(150f) + .WithMaxHeight(400f) + .LogInfo("Info") + .LogError("Error") + .Log("Normal"); + + Assert.AreEqual(150f, _logView.style.minHeight.value.value); + Assert.AreEqual(400f, _logView.style.maxHeight.value.value); + Assert.AreEqual(3, _logView.ScrollView.childCount); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JLogViewTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JLogViewTests.cs.meta new file mode 100644 index 000000000..cd7964c8c --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JLogViewTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb48b0d84fa9c4788a30e5c8081767f2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JProgressBarTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JProgressBarTests.cs new file mode 100644 index 000000000..48217fbf8 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JProgressBarTests.cs @@ -0,0 +1,315 @@ +// JProgressBarTests.cs +// EditMode unit tests for JProgressBar + +using NUnit.Framework; +using UnityEngine; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Feedback; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Feedback +{ + [TestFixture] + public class JProgressBarTests + { + private JProgressBar _progressBar; + + [SetUp] + public void SetUp() + { + _progressBar = new JProgressBar(); + } + + #region Constructor Tests + + [Test] + public void Constructor_AddsBaseClass() + { + Assert.IsTrue(_progressBar.ClassListContains("j-progress-bar")); + } + + [Test] + public void Constructor_Default_ProgressIsZero() + { + Assert.AreEqual(0f, _progressBar.Progress); + } + + [Test] + public void Constructor_WithInitialProgress_SetsProgress() + { + var bar = new JProgressBar(0.5f); + Assert.AreEqual(0.5f, bar.Progress); + } + + [Test] + public void Constructor_SetsHeight() + { + Assert.AreEqual(8f, _progressBar.style.height.value.value); + } + + [Test] + public void Constructor_SetsSurfaceBackgroundColor() + { + Assert.AreEqual(Tokens.Colors.BgSurface, _progressBar.style.backgroundColor.value); + } + + [Test] + public void Constructor_SetsBorderRadius() + { + Assert.AreEqual(Tokens.BorderRadius.Sm, _progressBar.style.borderTopLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _progressBar.style.borderTopRightRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _progressBar.style.borderBottomLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.Sm, _progressBar.style.borderBottomRightRadius.value.value); + } + + [Test] + public void Constructor_SetsOverflowHidden() + { + Assert.AreEqual(Overflow.Hidden, _progressBar.style.overflow.value); + } + + [Test] + public void Constructor_CreatesFillElement() + { + Assert.IsNotNull(_progressBar.Fill); + } + + [Test] + public void Constructor_FillHasCorrectClass() + { + Assert.IsTrue(_progressBar.Fill.ClassListContains("j-progress-bar__fill")); + } + + [Test] + public void Constructor_FillHasPrimaryColor() + { + Assert.AreEqual(Tokens.Colors.Primary, _progressBar.Fill.style.backgroundColor.value); + } + + #endregion + + #region Progress Property Tests + + [Test] + public void Progress_Get_ReturnsCurrentProgress() + { + var bar = new JProgressBar(0.75f); + Assert.AreEqual(0.75f, bar.Progress); + } + + [Test] + public void Progress_Set_UpdatesProgress() + { + _progressBar.Progress = 0.5f; + Assert.AreEqual(0.5f, _progressBar.Progress); + } + + [Test] + public void Progress_Set_Clamps_ToZero() + { + _progressBar.Progress = -0.5f; + Assert.AreEqual(0f, _progressBar.Progress); + } + + [Test] + public void Progress_Set_Clamps_ToOne() + { + _progressBar.Progress = 1.5f; + Assert.AreEqual(1f, _progressBar.Progress); + } + + [Test] + public void Progress_Set_UpdatesFillWidth() + { + _progressBar.Progress = 0.5f; + Assert.AreEqual(50f, _progressBar.Fill.style.width.value.value); + } + + [Test] + public void Progress_Set_Zero_FillWidthIsZero() + { + _progressBar.Progress = 0f; + Assert.AreEqual(0f, _progressBar.Fill.style.width.value.value); + } + + [Test] + public void Progress_Set_Full_FillWidthIs100Percent() + { + _progressBar.Progress = 1f; + Assert.AreEqual(100f, _progressBar.Fill.style.width.value.value); + } + + #endregion + + #region SetProgress Tests + + [Test] + public void SetProgress_UpdatesProgress() + { + _progressBar.SetProgress(0.75f); + Assert.AreEqual(0.75f, _progressBar.Progress); + } + + [Test] + public void SetProgress_ReturnsProgressBarForChaining() + { + var result = _progressBar.SetProgress(0.5f); + Assert.AreSame(_progressBar, result); + } + + [Test] + public void SetProgress_ClampsBelowZero() + { + _progressBar.SetProgress(-1f); + Assert.AreEqual(0f, _progressBar.Progress); + } + + [Test] + public void SetProgress_ClampsAboveOne() + { + _progressBar.SetProgress(2f); + Assert.AreEqual(1f, _progressBar.Progress); + } + + #endregion + + #region WithSuccessOnComplete Tests + + [Test] + public void WithSuccessOnComplete_WhenNotComplete_KeepsPrimaryColor() + { + _progressBar.Progress = 0.5f; + _progressBar.WithSuccessOnComplete(true); + + Assert.AreEqual(Tokens.Colors.Primary, _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithSuccessOnComplete_WhenComplete_UsesSuccessColor() + { + _progressBar.Progress = 1f; + _progressBar.WithSuccessOnComplete(true); + // Need to set progress again to trigger color update + _progressBar.SetProgress(1f); + + Assert.AreEqual(Tokens.Colors.Success, _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithSuccessOnComplete_ReturnsProgressBarForChaining() + { + var result = _progressBar.WithSuccessOnComplete(true); + Assert.AreSame(_progressBar, result); + } + + [Test] + public void WithSuccessOnComplete_False_UsesPrimaryColor() + { + _progressBar.Progress = 1f; + _progressBar.WithSuccessOnComplete(false); + + Assert.AreEqual(Tokens.Colors.Primary, _progressBar.Fill.style.backgroundColor.value); + } + + #endregion + + #region WithHeight Tests + + [Test] + public void WithHeight_SetsHeight() + { + _progressBar.WithHeight(16f); + Assert.AreEqual(16f, _progressBar.style.height.value.value); + } + + [Test] + public void WithHeight_ReturnsProgressBarForChaining() + { + var result = _progressBar.WithHeight(12f); + Assert.AreSame(_progressBar, result); + } + + #endregion + + #region WithColor Tests + + [Test] + public void WithColor_SetsFillColor() + { + _progressBar.WithColor(Color.red); + Assert.AreEqual(Color.red, _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithColor_ReturnsProgressBarForChaining() + { + var result = _progressBar.WithColor(Color.blue); + Assert.AreSame(_progressBar, result); + } + + #endregion + + #region WithVariant Tests + + [Test] + public void WithVariant_Primary_SetsPrimaryColor() + { + _progressBar.WithVariant(ButtonVariant.Primary); + Assert.AreEqual(JTheme.GetButtonColor(ButtonVariant.Primary), _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithVariant_Secondary_SetsSecondaryColor() + { + _progressBar.WithVariant(ButtonVariant.Secondary); + Assert.AreEqual(JTheme.GetButtonColor(ButtonVariant.Secondary), _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithVariant_Success_SetsSuccessColor() + { + _progressBar.WithVariant(ButtonVariant.Success); + Assert.AreEqual(JTheme.GetButtonColor(ButtonVariant.Success), _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithVariant_Danger_SetsDangerColor() + { + _progressBar.WithVariant(ButtonVariant.Danger); + Assert.AreEqual(JTheme.GetButtonColor(ButtonVariant.Danger), _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithVariant_Warning_SetsWarningColor() + { + _progressBar.WithVariant(ButtonVariant.Warning); + Assert.AreEqual(JTheme.GetButtonColor(ButtonVariant.Warning), _progressBar.Fill.style.backgroundColor.value); + } + + [Test] + public void WithVariant_ReturnsProgressBarForChaining() + { + var result = _progressBar.WithVariant(ButtonVariant.Success); + Assert.AreSame(_progressBar, result); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + _progressBar + .SetProgress(0.75f) + .WithHeight(12f) + .WithVariant(ButtonVariant.Success) + .WithSuccessOnComplete(true); + + Assert.AreEqual(0.75f, _progressBar.Progress); + Assert.AreEqual(12f, _progressBar.style.height.value.value); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JProgressBarTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JProgressBarTests.cs.meta new file mode 100644 index 000000000..4f823c4ad --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JProgressBarTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f0dc496e098f74e778753e51c5d588d2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JStatusBarTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JStatusBarTests.cs new file mode 100644 index 000000000..8a92cdd33 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Feedback/JStatusBarTests.cs @@ -0,0 +1,293 @@ +// JStatusBarTests.cs +// EditMode unit tests for JStatusBar + +using NUnit.Framework; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Feedback; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Feedback +{ + [TestFixture] + public class JStatusBarTests + { + private JStatusBar _statusBar; + + [SetUp] + public void SetUp() + { + _statusBar = new JStatusBar("Test message"); + } + + #region Constructor Tests + + [Test] + public void Constructor_AddsBaseClass() + { + Assert.IsTrue(_statusBar.ClassListContains("j-status-bar")); + } + + [Test] + public void Constructor_SetsText() + { + Assert.AreEqual("Test message", _statusBar.Text); + } + + [Test] + public void Constructor_Default_StatusIsInfo() + { + var bar = new JStatusBar(); + Assert.AreEqual(StatusType.Info, bar.Status); + } + + [Test] + public void Constructor_WithStatus_SetsStatus() + { + var bar = new JStatusBar("msg", StatusType.Error); + Assert.AreEqual(StatusType.Error, bar.Status); + } + + [Test] + public void Constructor_SetsRowDirection() + { + Assert.AreEqual(FlexDirection.Row, _statusBar.style.flexDirection.value); + } + + [Test] + public void Constructor_SetsCenterAlignment() + { + Assert.AreEqual(Align.Center, _statusBar.style.alignItems.value); + } + + [Test] + public void Constructor_SetsPadding() + { + Assert.AreEqual(Tokens.Spacing.MD, _statusBar.style.paddingTop.value.value); + Assert.AreEqual(Tokens.Spacing.Lg, _statusBar.style.paddingRight.value.value); + Assert.AreEqual(Tokens.Spacing.MD, _statusBar.style.paddingBottom.value.value); + Assert.AreEqual(Tokens.Spacing.Lg, _statusBar.style.paddingLeft.value.value); + } + + [Test] + public void Constructor_SetsBorderRadius() + { + Assert.AreEqual(Tokens.BorderRadius.MD, _statusBar.style.borderTopLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _statusBar.style.borderTopRightRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _statusBar.style.borderBottomLeftRadius.value.value); + Assert.AreEqual(Tokens.BorderRadius.MD, _statusBar.style.borderBottomRightRadius.value.value); + } + + [Test] + public void Constructor_SetsBottomMargin() + { + Assert.AreEqual(Tokens.Spacing.MD, _statusBar.style.marginBottom.value.value); + } + + [Test] + public void Constructor_SetsLeftBorderWidth() + { + Assert.AreEqual(3f, _statusBar.style.borderLeftWidth.value); + } + + [Test] + public void Constructor_CreatesTextLabel() + { + Assert.IsNotNull(_statusBar.TextLabel); + } + + [Test] + public void Constructor_TextLabelHasCorrectClass() + { + Assert.IsTrue(_statusBar.TextLabel.ClassListContains("j-status-bar__text")); + } + + [Test] + public void Constructor_TextLabelHasBaseFontSize() + { + Assert.AreEqual(Tokens.FontSize.Base, _statusBar.TextLabel.style.fontSize.value.value); + } + + #endregion + + #region Text Property Tests + + [Test] + public void Text_Get_ReturnsCurrentText() + { + Assert.AreEqual("Test message", _statusBar.Text); + } + + [Test] + public void Text_Set_UpdatesText() + { + _statusBar.Text = "New message"; + Assert.AreEqual("New message", _statusBar.Text); + } + + [Test] + public void Text_Set_Empty_SetsEmptyText() + { + _statusBar.Text = ""; + Assert.AreEqual("", _statusBar.Text); + } + + #endregion + + #region Status Property Tests + + [Test] + public void Status_Get_ReturnsCurrentStatus() + { + var bar = new JStatusBar("msg", StatusType.Warning); + Assert.AreEqual(StatusType.Warning, bar.Status); + } + + [Test] + public void Status_Set_UpdatesStatus() + { + _statusBar.Status = StatusType.Error; + Assert.AreEqual(StatusType.Error, _statusBar.Status); + } + + #endregion + + #region SetStatus Tests + + [Test] + public void SetStatus_Info_AddsInfoClass() + { + _statusBar.SetStatus(StatusType.Info); + Assert.IsTrue(_statusBar.ClassListContains("j-status-bar--info")); + } + + [Test] + public void SetStatus_Success_AddsSuccessClass() + { + _statusBar.SetStatus(StatusType.Success); + Assert.IsTrue(_statusBar.ClassListContains("j-status-bar--success")); + } + + [Test] + public void SetStatus_Warning_AddsWarningClass() + { + _statusBar.SetStatus(StatusType.Warning); + Assert.IsTrue(_statusBar.ClassListContains("j-status-bar--warning")); + } + + [Test] + public void SetStatus_Error_AddsErrorClass() + { + _statusBar.SetStatus(StatusType.Error); + Assert.IsTrue(_statusBar.ClassListContains("j-status-bar--error")); + } + + [Test] + public void SetStatus_RemovesPreviousStatusClass() + { + _statusBar.SetStatus(StatusType.Info); + _statusBar.SetStatus(StatusType.Error); + + Assert.IsFalse(_statusBar.ClassListContains("j-status-bar--info")); + Assert.IsTrue(_statusBar.ClassListContains("j-status-bar--error")); + } + + [Test] + public void SetStatus_ReturnsStatusBarForChaining() + { + var result = _statusBar.SetStatus(StatusType.Success); + Assert.AreSame(_statusBar, result); + } + + [Test] + public void SetStatus_SetsSurfaceBackgroundColor() + { + _statusBar.SetStatus(StatusType.Success); + Assert.AreEqual(Tokens.Colors.BgSurface, _statusBar.style.backgroundColor.value); + } + + [Test] + public void SetStatus_SetsLeftBorderColor() + { + _statusBar.SetStatus(StatusType.Info); + Assert.AreEqual(Tokens.Colors.Border, _statusBar.style.borderLeftColor.value); + } + + #endregion + + #region WithText Tests + + [Test] + public void WithText_UpdatesText() + { + _statusBar.WithText("Updated text"); + Assert.AreEqual("Updated text", _statusBar.Text); + } + + [Test] + public void WithText_ReturnsStatusBarForChaining() + { + var result = _statusBar.WithText("test"); + Assert.AreSame(_statusBar, result); + } + + #endregion + + #region TextLabel Property Tests + + [Test] + public void TextLabel_ReturnsLabelElement() + { + Assert.IsInstanceOf