diff --git a/.claude/rules/coding-patterns.md b/.claude/rules/coding-patterns.md index 4e1cccfe..025c0796 100644 --- a/.claude/rules/coding-patterns.md +++ b/.claude/rules/coding-patterns.md @@ -48,3 +48,51 @@ public abstract class ConfigBase : ScriptableObject where T : ConfigBase - Handle Unity domain reloads properly (state resets on recompile) - Use `SessionState` or `EditorPrefs` for persistent editor state - Clean up resources in `EditorApplication.quitting` + +## Unit Testing + +**IMPORTANT**: When adding or modifying features in non-core packages (JEngine.UI, JEngine.Util), you MUST add unit tests. + +### Test Location + +Tests go in `Packages/com.jasonxudeveloper.jengine./Tests/Editor/`: +- Components: `Tests/Editor/Components//Tests.cs` +- Theming: `Tests/Editor/Theming/Tests.cs` +- Utilities: `Tests/Editor/Utilities/Tests.cs` + +### Test Pattern + +Follow the existing test structure: +```csharp +using NUnit.Framework; +using JEngine.UI.Editor.Components.Layout; + +namespace JEngine.UI.Tests.Editor.Components.Layout +{ + [TestFixture] + public class JContainerTests + { + private JContainer _container; + + [SetUp] + public void SetUp() + { + _container = new JContainer(); + } + + [Test] + public void Constructor_Default_AddsBaseClass() + { + Assert.IsTrue(_container.ClassListContains("j-container")); + } + } +} +``` + +### Test Coverage Requirements + +- Constructor behavior (default and parameterized) +- All public methods and properties +- Fluent API chaining +- Edge cases and error conditions +- Inherited behavior from base classes diff --git a/CLAUDE.md b/CLAUDE.md index a1e108c1..4b8238af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,6 +81,7 @@ Document all public APIs with XML comments. - Run tests via Unity Test Runner - Use `[UnityTest]` with `UniTask.ToCoroutine()` for async tests - Editor code should check `TestRunnerCallbacks.IsRunningTests` +- **IMPORTANT**: Non-core packages (JEngine.UI, JEngine.Util) require unit tests for new/modified features ## Code Review Checklist @@ -89,3 +90,4 @@ Document all public APIs with XML comments. - [ ] Uses UniTask for async (not Task) - [ ] Thread-safe where needed - [ ] Proper resource cleanup +- [ ] Unit tests added for non-core package changes diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs new file mode 100644 index 00000000..48c292d2 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs @@ -0,0 +1,182 @@ +// JContainer.cs +// +// Author: +// JasonXuDeveloper +// +// Copyright (c) 2025 JEngine +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using JEngine.UI.Editor.Theming; +using UnityEngine.UIElements; + +namespace JEngine.UI.Editor.Components.Layout +{ + /// + /// Responsive container component with max-width constraints. + /// Centers content horizontally with configurable maximum width. + /// Based on Tailwind CSS container patterns. + /// + public class JContainer : JComponent + { + private ContainerSize _size; + + /// + /// Creates a new container with the default large (1024px) size. + /// + public JContainer() : this(ContainerSize.Lg) + { + } + + /// + /// Creates a new container with the specified size. + /// + /// The container size (max-width constraint). + public JContainer(ContainerSize size) : base("j-container") + { + // Set width to 100% and center with auto margins + style.width = Length.Percent(100); + style.marginLeft = StyleKeyword.Auto; + style.marginRight = StyleKeyword.Auto; + + SetSize(size); + } + + /// + /// Gets or sets the container size. + /// + public ContainerSize Size + { + get => _size; + set => SetSize(value); + } + + /// + /// Sets the container size. + /// + /// The container size. + /// This container for chaining. + public JContainer WithSize(ContainerSize size) + { + SetSize(size); + return this; + } + + /// + /// Sets horizontal padding on the container. + /// + /// The padding value in pixels. + /// This container for chaining. + public JContainer WithHorizontalPadding(float padding) + { + style.paddingLeft = padding; + style.paddingRight = padding; + return this; + } + + /// + /// Applies responsive horizontal padding based on container size. + /// Larger containers get more padding. + /// + /// This container for chaining. + public JContainer WithResponsivePadding() + { + var padding = _size switch + { + ContainerSize.Xs => Tokens.Spacing.MD, + ContainerSize.Sm => Tokens.Spacing.Lg, + ContainerSize.Md => Tokens.Spacing.Xl, + ContainerSize.Lg => Tokens.Spacing.Xxl, + ContainerSize.Xl => Tokens.Spacing.Xxl, + ContainerSize.Full => Tokens.Spacing.Lg, + _ => Tokens.Spacing.Lg + }; + + style.paddingLeft = padding; + style.paddingRight = padding; + return this; + } + + /// + /// Sets the container to fluid mode (no max-width constraint). + /// + /// This container for chaining. + public JContainer Fluid() + { + SetSize(ContainerSize.Full); + return this; + } + + /// + /// Adds child elements to this container. + /// + /// The elements to add. + /// This container for chaining. + public new JContainer Add(params VisualElement[] children) + { + base.Add(children); + return this; + } + + private void SetSize(ContainerSize size) + { + _size = size; + + // Remove existing size classes + RemoveFromClassList("j-container--xs"); + RemoveFromClassList("j-container--sm"); + RemoveFromClassList("j-container--md"); + RemoveFromClassList("j-container--lg"); + RemoveFromClassList("j-container--xl"); + RemoveFromClassList("j-container--full"); + + // Add new size class + var sizeClass = size switch + { + ContainerSize.Xs => "j-container--xs", + ContainerSize.Sm => "j-container--sm", + ContainerSize.Md => "j-container--md", + ContainerSize.Lg => "j-container--lg", + ContainerSize.Xl => "j-container--xl", + ContainerSize.Full => "j-container--full", + _ => "j-container--lg" + }; + AddToClassList(sizeClass); + + // Also set max-width style directly for immediate effect + if (size == ContainerSize.Full) + { + style.maxWidth = StyleKeyword.None; + } + else + { + var maxWidth = size switch + { + ContainerSize.Xs => Tokens.Container.Xs, + ContainerSize.Sm => Tokens.Container.Sm, + ContainerSize.Md => Tokens.Container.Md, + ContainerSize.Lg => Tokens.Container.Lg, + ContainerSize.Xl => Tokens.Container.Xl, + _ => Tokens.Container.Lg + }; + style.maxWidth = maxWidth; + } + } + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs.meta new file mode 100644 index 00000000..602fe7b3 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Layout/JContainer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f145055519c18441e8829f359dce243b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs index 521730b8..6ea9b9e1 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/BootstrapEditorUI.cs @@ -76,6 +76,9 @@ public static VisualElement CreateInspector(SerializedObject serializedObject, B root.style.paddingRight = Tokens.Spacing.MD; root.style.paddingBottom = Tokens.Spacing.MD; + // Centered container for compact inspector layout + var container = new JContainer(ContainerSize.Xs); + var content = new JStack(GapSize.Sm); // Header @@ -98,7 +101,8 @@ public static VisualElement CreateInspector(SerializedObject serializedObject, B // UI Settings content.Add(CreateUISettingsSection()); - root.Add(content); + container.Add(content); + root.Add(container); // Register undo/redo callback Undo.undoRedoPerformed += OnUndoRedo; @@ -131,6 +135,9 @@ private static void OnUndoRedo() // Rebuild the entire UI _currentRoot.Clear(); + // Centered container for compact inspector layout + var container = new JContainer(ContainerSize.Xs); + // Recreate content var content = new JStack(GapSize.Sm); @@ -144,7 +151,8 @@ private static void OnUndoRedo() content.Add(CreateSecuritySettingsSection()); content.Add(CreateUISettingsSection()); - _currentRoot.Add(content); + container.Add(content); + _currentRoot.Add(container); } private static VisualElement CreateHeader() diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/PanelUI.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/PanelUI.cs index 64d85597..95d39385 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/PanelUI.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Internal/PanelUI.cs @@ -80,6 +80,9 @@ public static VisualElement CreateContent(Panel panel, BuildManager buildManager var scrollView = new ScrollView(ScrollViewMode.Vertical); scrollView.style.flexGrow = 1; + // Centered container for compact panel layout + var container = new JContainer(ContainerSize.Xs); + var content = new JStack(GapSize.Sm); // Header @@ -103,7 +106,8 @@ public static VisualElement CreateContent(Panel panel, BuildManager buildManager // Status Section content.Add(CreateStatusSection()); - scrollView.Add(content); + container.Add(content); + scrollView.Add(container); root.Add(scrollView); return root; diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/base.uss b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/base.uss index 45dd7988..1741ea4d 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/base.uss +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/base.uss @@ -2,8 +2,8 @@ * Base reset and common styles for JEngine Editor UI */ -/* Base container */ -.j-container { +/* Base panel styling */ +.j-panel-base { background-color: var(--j-bg-base); color: var(--j-text-primary); padding: var(--j-spacing-lg); diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/components.uss b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/components.uss index 6559f5d2..242217c2 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/components.uss +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Styles/components.uss @@ -2,6 +2,41 @@ * Component-specific styles for JEngine Editor UI */ +/* =========================== + CONTAINER COMPONENT + =========================== */ + +/* JContainer - responsive max-width container with horizontal centering */ +.j-container { + width: 100%; + margin-left: auto; + margin-right: auto; +} + +.j-container--xs { + max-width: 480px; +} + +.j-container--sm { + max-width: 640px; +} + +.j-container--md { + max-width: 768px; +} + +.j-container--lg { + max-width: 1024px; +} + +.j-container--xl { + max-width: 1280px; +} + +.j-container--full { + max-width: none; +} + /* =========================== LAYOUT COMPONENTS =========================== */ diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/JTheme.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/JTheme.cs index 852c584f..02e6a737 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/JTheme.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/JTheme.cs @@ -411,4 +411,24 @@ public enum StatusType Warning, Error } + + /// + /// Container size options for max-width constraints. + /// Based on Tailwind CSS breakpoints. + /// + public enum ContainerSize + { + /// 480px - Mobile-like, narrow inspectors. + Xs, + /// 640px - Compact panels, side docks. + Sm, + /// 768px - Standard inspector width. + Md, + /// 1024px - Wide panels (default). + Lg, + /// 1280px - Full-width editor windows. + Xl, + /// No max-width constraint (fluid). + Full + } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/Tokens.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/Tokens.cs index b9cec817..9c409bc2 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/Tokens.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Theming/Tokens.cs @@ -296,5 +296,23 @@ public static class Layout /// Minimum control width (80px). public const float MinControlWidth = 80f; } + + /// + /// Container max-width values based on Tailwind CSS breakpoints. + /// Used for responsive layout containers. + /// + public static class Container + { + /// Extra small container (480px) - Mobile-like, narrow inspectors. + public const float Xs = 480f; + /// Small container (640px) - Compact panels, side docks. + public const float Sm = 640f; + /// Medium container (768px) - Standard inspector width. + public const float Md = 768f; + /// Large container (1024px) - Wide panels (default). + public const float Lg = 1024f; + /// Extra large container (1280px) - Full-width editor windows. + public const float Xl = 1280f; + } } } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Layout/JContainerTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Layout/JContainerTests.cs new file mode 100644 index 00000000..76d89039 --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Layout/JContainerTests.cs @@ -0,0 +1,465 @@ +// JContainerTests.cs +// EditMode unit tests for JContainer layout component + +using NUnit.Framework; +using UnityEngine.UIElements; +using JEngine.UI.Editor.Components.Layout; +using JEngine.UI.Editor.Theming; + +namespace JEngine.UI.Tests.Editor.Components.Layout +{ + [TestFixture] + public class JContainerTests + { + private JContainer _container; + + [SetUp] + public void SetUp() + { + _container = new JContainer(); + } + + #region Constructor Tests + + [Test] + public void Constructor_Default_AddsBaseClass() + { + Assert.IsTrue(_container.ClassListContains("j-container")); + } + + [Test] + public void Constructor_Default_SetsWidth100Percent() + { + Assert.AreEqual(100f, _container.style.width.value.value); + Assert.AreEqual(LengthUnit.Percent, _container.style.width.value.unit); + } + + [Test] + public void Constructor_Default_SetsAutoMargins() + { + Assert.AreEqual(StyleKeyword.Auto, _container.style.marginLeft.keyword); + Assert.AreEqual(StyleKeyword.Auto, _container.style.marginRight.keyword); + } + + [Test] + public void Constructor_Default_UsesDefaultLgSize() + { + Assert.IsTrue(_container.ClassListContains("j-container--lg")); + Assert.AreEqual(ContainerSize.Lg, _container.Size); + } + + [Test] + public void Constructor_Default_SetsLgMaxWidth() + { + Assert.AreEqual(Tokens.Container.Lg, _container.style.maxWidth.value.value); + } + + [Test] + public void Constructor_WithXsSize_SetsCorrectClass() + { + var container = new JContainer(ContainerSize.Xs); + Assert.IsTrue(container.ClassListContains("j-container--xs")); + } + + [Test] + public void Constructor_WithXsSize_SetsCorrectMaxWidth() + { + var container = new JContainer(ContainerSize.Xs); + Assert.AreEqual(Tokens.Container.Xs, container.style.maxWidth.value.value); + } + + [Test] + public void Constructor_WithSmSize_SetsCorrectClass() + { + var container = new JContainer(ContainerSize.Sm); + Assert.IsTrue(container.ClassListContains("j-container--sm")); + } + + [Test] + public void Constructor_WithSmSize_SetsCorrectMaxWidth() + { + var container = new JContainer(ContainerSize.Sm); + Assert.AreEqual(Tokens.Container.Sm, container.style.maxWidth.value.value); + } + + [Test] + public void Constructor_WithMdSize_SetsCorrectClass() + { + var container = new JContainer(ContainerSize.Md); + Assert.IsTrue(container.ClassListContains("j-container--md")); + } + + [Test] + public void Constructor_WithMdSize_SetsCorrectMaxWidth() + { + var container = new JContainer(ContainerSize.Md); + Assert.AreEqual(Tokens.Container.Md, container.style.maxWidth.value.value); + } + + [Test] + public void Constructor_WithLgSize_SetsCorrectClass() + { + var container = new JContainer(ContainerSize.Lg); + Assert.IsTrue(container.ClassListContains("j-container--lg")); + } + + [Test] + public void Constructor_WithLgSize_SetsCorrectMaxWidth() + { + var container = new JContainer(ContainerSize.Lg); + Assert.AreEqual(Tokens.Container.Lg, container.style.maxWidth.value.value); + } + + [Test] + public void Constructor_WithXlSize_SetsCorrectClass() + { + var container = new JContainer(ContainerSize.Xl); + Assert.IsTrue(container.ClassListContains("j-container--xl")); + } + + [Test] + public void Constructor_WithXlSize_SetsCorrectMaxWidth() + { + var container = new JContainer(ContainerSize.Xl); + Assert.AreEqual(Tokens.Container.Xl, container.style.maxWidth.value.value); + } + + [Test] + public void Constructor_WithFullSize_SetsCorrectClass() + { + var container = new JContainer(ContainerSize.Full); + Assert.IsTrue(container.ClassListContains("j-container--full")); + } + + [Test] + public void Constructor_WithFullSize_SetsNoMaxWidth() + { + var container = new JContainer(ContainerSize.Full); + Assert.AreEqual(StyleKeyword.None, container.style.maxWidth.keyword); + } + + #endregion + + #region Size Property Tests + + [Test] + public void Size_Get_ReturnsCurrentSize() + { + var container = new JContainer(ContainerSize.Sm); + Assert.AreEqual(ContainerSize.Sm, container.Size); + } + + [Test] + public void Size_Set_UpdatesSizeAndClass() + { + _container.Size = ContainerSize.Xs; + Assert.AreEqual(ContainerSize.Xs, _container.Size); + Assert.IsTrue(_container.ClassListContains("j-container--xs")); + } + + [Test] + public void Size_Set_RemovesPreviousClass() + { + _container.Size = ContainerSize.Xs; + Assert.IsFalse(_container.ClassListContains("j-container--lg")); + } + + #endregion + + #region WithSize Tests + + [Test] + public void WithSize_Xs_SetsCorrectClass() + { + _container.WithSize(ContainerSize.Xs); + Assert.IsTrue(_container.ClassListContains("j-container--xs")); + } + + [Test] + public void WithSize_Sm_SetsCorrectClass() + { + _container.WithSize(ContainerSize.Sm); + Assert.IsTrue(_container.ClassListContains("j-container--sm")); + } + + [Test] + public void WithSize_Md_SetsCorrectClass() + { + _container.WithSize(ContainerSize.Md); + Assert.IsTrue(_container.ClassListContains("j-container--md")); + } + + [Test] + public void WithSize_Xl_SetsCorrectClass() + { + _container.WithSize(ContainerSize.Xl); + Assert.IsTrue(_container.ClassListContains("j-container--xl")); + } + + [Test] + public void WithSize_Full_SetsCorrectClass() + { + _container.WithSize(ContainerSize.Full); + Assert.IsTrue(_container.ClassListContains("j-container--full")); + } + + [Test] + public void WithSize_RemovesPreviousSizeClass() + { + _container.WithSize(ContainerSize.Xs); + _container.WithSize(ContainerSize.Xl); + + Assert.IsFalse(_container.ClassListContains("j-container--xs")); + Assert.IsTrue(_container.ClassListContains("j-container--xl")); + } + + [Test] + public void WithSize_ReturnsContainerForChaining() + { + var result = _container.WithSize(ContainerSize.Sm); + Assert.AreSame(_container, result); + } + + #endregion + + #region WithHorizontalPadding Tests + + [Test] + public void WithHorizontalPadding_SetsLeftPadding() + { + _container.WithHorizontalPadding(20f); + Assert.AreEqual(20f, _container.style.paddingLeft.value.value); + } + + [Test] + public void WithHorizontalPadding_SetsRightPadding() + { + _container.WithHorizontalPadding(20f); + Assert.AreEqual(20f, _container.style.paddingRight.value.value); + } + + [Test] + public void WithHorizontalPadding_ReturnsContainerForChaining() + { + var result = _container.WithHorizontalPadding(10f); + Assert.AreSame(_container, result); + } + + #endregion + + #region WithResponsivePadding Tests + + [Test] + public void WithResponsivePadding_Xs_SetsMdPadding() + { + var container = new JContainer(ContainerSize.Xs); + container.WithResponsivePadding(); + Assert.AreEqual(Tokens.Spacing.MD, container.style.paddingLeft.value.value); + } + + [Test] + public void WithResponsivePadding_Sm_SetsLgPadding() + { + var container = new JContainer(ContainerSize.Sm); + container.WithResponsivePadding(); + Assert.AreEqual(Tokens.Spacing.Lg, container.style.paddingLeft.value.value); + } + + [Test] + public void WithResponsivePadding_Md_SetsXlPadding() + { + var container = new JContainer(ContainerSize.Md); + container.WithResponsivePadding(); + Assert.AreEqual(Tokens.Spacing.Xl, container.style.paddingLeft.value.value); + } + + [Test] + public void WithResponsivePadding_Lg_SetsXxlPadding() + { + var container = new JContainer(ContainerSize.Lg); + container.WithResponsivePadding(); + Assert.AreEqual(Tokens.Spacing.Xxl, container.style.paddingLeft.value.value); + } + + [Test] + public void WithResponsivePadding_Xl_SetsXxlPadding() + { + var container = new JContainer(ContainerSize.Xl); + container.WithResponsivePadding(); + Assert.AreEqual(Tokens.Spacing.Xxl, container.style.paddingLeft.value.value); + } + + [Test] + public void WithResponsivePadding_Full_SetsLgPadding() + { + var container = new JContainer(ContainerSize.Full); + container.WithResponsivePadding(); + Assert.AreEqual(Tokens.Spacing.Lg, container.style.paddingLeft.value.value); + } + + [Test] + public void WithResponsivePadding_ReturnsContainerForChaining() + { + var result = _container.WithResponsivePadding(); + Assert.AreSame(_container, result); + } + + #endregion + + #region Fluid Tests + + [Test] + public void Fluid_SetsFullSize() + { + _container.Fluid(); + Assert.AreEqual(ContainerSize.Full, _container.Size); + } + + [Test] + public void Fluid_AddsFullClass() + { + _container.Fluid(); + Assert.IsTrue(_container.ClassListContains("j-container--full")); + } + + [Test] + public void Fluid_RemovesMaxWidth() + { + _container.Fluid(); + Assert.AreEqual(StyleKeyword.None, _container.style.maxWidth.keyword); + } + + [Test] + public void Fluid_ReturnsContainerForChaining() + { + var result = _container.Fluid(); + Assert.AreSame(_container, result); + } + + #endregion + + #region Add Tests + + [Test] + public void Add_SingleChild_AddsToContainer() + { + var child = new Label("test"); + _container.Add(child); + + Assert.AreEqual(1, _container.childCount); + Assert.AreSame(child, _container[0]); + } + + [Test] + public void Add_MultipleChildren_AddsAllToContainer() + { + var child1 = new Label("test1"); + var child2 = new Label("test2"); + var child3 = new Label("test3"); + + _container.Add(child1, child2, child3); + + Assert.AreEqual(3, _container.childCount); + } + + [Test] + public void Add_ReturnsContainerForChaining() + { + var result = _container.Add(new Label()); + Assert.AreSame(_container, result); + } + + [Test] + public void Add_CanChainMultipleAddCalls() + { + _container + .Add(new Label("1")) + .Add(new Label("2")) + .Add(new Label("3")); + + Assert.AreEqual(3, _container.childCount); + } + + #endregion + + #region Inherited JComponent Tests + + [Test] + public void WithClass_AddsClassName() + { + _container.WithClass("custom-class"); + Assert.IsTrue(_container.ClassListContains("custom-class")); + } + + [Test] + public void WithClass_PreservesBaseClass() + { + _container.WithClass("custom"); + Assert.IsTrue(_container.ClassListContains("j-container")); + } + + [Test] + public void WithName_SetsElementName() + { + _container.WithName("test-container"); + Assert.AreEqual("test-container", _container.name); + } + + [Test] + public void WithMargin_SetsAllMargins() + { + _container.WithMargin(10f); + Assert.AreEqual(10f, _container.style.marginTop.value.value); + Assert.AreEqual(10f, _container.style.marginBottom.value.value); + } + + [Test] + public void WithPadding_SetsAllPadding() + { + _container.WithPadding(10f); + Assert.AreEqual(10f, _container.style.paddingTop.value.value); + Assert.AreEqual(10f, _container.style.paddingBottom.value.value); + } + + [Test] + public void WithFlexGrow_SetsFlexGrow() + { + _container.WithFlexGrow(1f); + Assert.AreEqual(1f, _container.style.flexGrow.value); + } + + [Test] + public void WithVisibility_False_HidesContainer() + { + _container.WithVisibility(false); + Assert.AreEqual(DisplayStyle.None, _container.style.display.value); + } + + #endregion + + #region Chaining Tests + + [Test] + public void FluentApi_CanChainMultipleMethods() + { + // JContainer-specific methods chain together + _container + .WithSize(ContainerSize.Md) + .WithHorizontalPadding(16f) + .Add(new Label("item1")) + .Add(new Label("item2")); + + // JComponent methods called separately (they return JComponent) + _container.WithName("my-container"); + _container.WithClass("custom"); + + Assert.AreEqual("my-container", _container.name); + Assert.IsTrue(_container.ClassListContains("custom")); + Assert.IsTrue(_container.ClassListContains("j-container--md")); + Assert.AreEqual(16f, _container.style.paddingLeft.value.value); + Assert.AreEqual(2, _container.childCount); + } + + #endregion + } +} diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Layout/JContainerTests.cs.meta b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Layout/JContainerTests.cs.meta new file mode 100644 index 00000000..d74a09dc --- /dev/null +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Layout/JContainerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6bd5105bd48be4ed1a8ef08717c3d7b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/PanelUITests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/PanelUITests.cs index c56888db..1d07affb 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/PanelUITests.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Internal/PanelUITests.cs @@ -205,21 +205,19 @@ public void BuildAllButton_HasLargerMinHeight() [Category("Integration")] public IEnumerator BuildAllButton_Click_CompletesSuccessfully() { - // Ignore Unity internal error logs (e.g., "Bake Ambient Probe") - // These are filtered by BuildManager but Unity Test Framework still catches them + // MUST set this to prevent test failure from Unity internal errors like "Bake Ambient Probe" LogAssert.ignoreFailingMessages = true; - // Disable auto-baking to prevent "Bake Ambient Probe" errors when creating scenes -#pragma warning disable 618 - var oldGiWorkflowMode = Lightmapping.giWorkflowMode; - Lightmapping.giWorkflowMode = Lightmapping.GIWorkflowMode.OnDemand; -#pragma warning restore 618 + // Use the init scene which already has proper lighting setup + const string initScenePath = "Assets/Init.unity"; + EditorSceneManager.OpenScene(initScenePath); - // Create a new empty scene to avoid dirty scene prompts from other tests - EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + // Wait for Unity editor to stabilize after scene change + yield return null; // Clear build cache to avoid "output directory exists" error from previous test runs _settings.clearBuildCache = true; + _settings.startUpScenePath = initScenePath; // Track build completion state bool buildCompleted = false; @@ -259,11 +257,6 @@ public IEnumerator BuildAllButton_Click_CompletesSuccessfully() elapsed += Time.deltaTime; } - // Restore old lighting mode -#pragma warning disable 618 - Lightmapping.giWorkflowMode = oldGiWorkflowMode; -#pragma warning restore 618 - // Assert build completed successfully Assert.IsTrue(buildCompleted, $"Build did not complete within {timeout} seconds"); Assert.IsTrue(buildSucceeded, $"Build failed with error: {buildError?.Message}"); @@ -292,21 +285,20 @@ public IEnumerator BuildAssetsOnly_Addon1_ChaCha20_CompletesSuccessfully() private IEnumerator BuildAssetsOnlyTest(string packageName, EncryptionOption encryption) { - // Ignore Unity internal error logs + // MUST set this to prevent test failure from Unity internal errors like "Bake Ambient Probe" LogAssert.ignoreFailingMessages = true; - // Disable auto-baking to prevent "Bake Ambient Probe" errors when creating scenes -#pragma warning disable 618 - var oldGiWorkflowMode = Lightmapping.giWorkflowMode; - Lightmapping.giWorkflowMode = Lightmapping.GIWorkflowMode.OnDemand; -#pragma warning restore 618 + // Use the init scene which already has proper lighting setup + const string initScenePath = "Assets/Init.unity"; + EditorSceneManager.OpenScene(initScenePath); - // Create a new empty scene to avoid dirty scene prompts - EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); + // Wait for Unity editor to stabilize after scene change + yield return null; // Configure settings for this test _settings.packageName = packageName; _settings.encryptionOption = encryption; + _settings.startUpScenePath = initScenePath; // Clear build cache to avoid "output directory exists" error from previous test runs _settings.clearBuildCache = true; @@ -348,11 +340,6 @@ private IEnumerator BuildAssetsOnlyTest(string packageName, EncryptionOption enc elapsed += Time.deltaTime; } - // Restore old lighting mode -#pragma warning disable 618 - Lightmapping.giWorkflowMode = oldGiWorkflowMode; -#pragma warning restore 618 - // Assert build completed successfully Assert.IsTrue(buildCompleted, $"Build did not complete within {timeout} seconds"); Assert.IsTrue(buildSucceeded, $"Build failed for {packageName} with {encryption}: {buildError?.Message}"); diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngineTestBase.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngineTestBase.cs index c4dab22a..e5021629 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngineTestBase.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/JEngineTestBase.cs @@ -9,7 +9,7 @@ namespace JEngine.UI.Tests /// /// Base class for all JEngine UI tests. /// Provides common setup that suppresses Unity internal errors - /// (like "Bake Ambient Probe") that occur during build operations. + /// that may occur during test operations. /// /// /// Test classes should inherit from this base class and override @@ -23,15 +23,14 @@ public abstract class JEngineTestBase /// /// Setup method called before each test. - /// Suppresses Unity internal error logs that occur during build operations. + /// Suppresses Unity internal error logs that may occur during test operations. /// Override this method and call base.BaseSetUp() first. /// [SetUp] public virtual void BaseSetUp() { - // Suppress Unity internal errors that occur during build operations. - // These errors come from Unity's Enlighten lighting system and BuildPipeline, - // and cannot be prevented at the source. They don't indicate test failures. + // Suppress Unity internal errors that occur during test operations. + // This prevents tests from failing due to unrelated Unity internal issues. _previousIgnoreFailingMessages = LogAssert.ignoreFailingMessages; LogAssert.ignoreFailingMessages = true; } diff --git a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Theming/TokensTests.cs b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Theming/TokensTests.cs index 29e36dff..ea7779d8 100644 --- a/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Theming/TokensTests.cs +++ b/UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Theming/TokensTests.cs @@ -465,6 +465,49 @@ public void Layout_FormLabelMinWidth_IsLessThanFormLabelWidth() #endregion + #region Container Token Tests + + [Test] + public void Container_Xs_IsCorrectValue() + { + Assert.AreEqual(480f, Tokens.Container.Xs); + } + + [Test] + public void Container_Sm_IsCorrectValue() + { + Assert.AreEqual(640f, Tokens.Container.Sm); + } + + [Test] + public void Container_Md_IsCorrectValue() + { + Assert.AreEqual(768f, Tokens.Container.Md); + } + + [Test] + public void Container_Lg_IsCorrectValue() + { + Assert.AreEqual(1024f, Tokens.Container.Lg); + } + + [Test] + public void Container_Xl_IsCorrectValue() + { + Assert.AreEqual(1280f, Tokens.Container.Xl); + } + + [Test] + public void Container_Hierarchy_IsIncreasing() + { + Assert.Less(Tokens.Container.Xs, Tokens.Container.Sm); + Assert.Less(Tokens.Container.Sm, Tokens.Container.Md); + Assert.Less(Tokens.Container.Md, Tokens.Container.Lg); + Assert.Less(Tokens.Container.Lg, Tokens.Container.Xl); + } + + #endregion + #region Color Alias Tests [Test]