Skip to content

Commit eaa3919

Browse files
feat(ui): add JTabView component and improve editor UI test coverage
- Add JTabView tabbed container component with configurable maxTabsPerRow - Refactor BootstrapEditorUI Text Settings into 6 categorized tabs (3x2 grid) - Add 46 new tests across 8 components using reflection for private methods (JButton, JCard, JObjectField, JTextField, JDropdown, JStack, JTabView, BootstrapEditorUI) - Update GitHub instructions to require 93%+ coverage for non-core packages with support for both EditMode and non-interactive PlayMode tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net>
1 parent 3d4e791 commit eaa3919

File tree

14 files changed

+1612
-67
lines changed

14 files changed

+1612
-67
lines changed

.github/instructions/code-review.instructions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,19 @@ Avoid LINQ in hot paths and UI code for performance:
4343
- Use array/list indexing instead of `.First()` / `.Last()`
4444
- LINQ allocates iterators and delegates - avoid in frequently called code
4545

46+
### 7. Unit Test Coverage
47+
New features and new logic in non-core packages (JEngine.UI, JEngine.Util, and any future packages) MUST include unit tests:
48+
- Target **93%+ code coverage** for all new/modified code
49+
- **Applies to**: All `Packages/com.jasonxudeveloper.jengine.*` packages **except** `jengine.core`
50+
- Prefer **EditMode tests** (`Tests/Editor/`) for most logic
51+
- Use **PlayMode tests** (`Tests/Runtime/`) when runtime behavior requires it (MonoBehaviour lifecycle, scene loading, etc.) — these must run **non-interactively** (no user input, no manual scene setup)
52+
- Cover: constructors, public API, fluent chaining, edge cases, event handlers
53+
- Use reflection to test private methods (e.g. `OnAttachToPanel`, hover handlers) when they contain meaningful logic
54+
- Verify tests exercise both happy paths and error/boundary conditions
55+
4656
## Common Issues to Flag
4757

58+
- Missing or insufficient unit tests for new features
4859
- Missing XML documentation on public APIs
4960
- Direct `Debug.Log` (should use proper logging)
5061
- `Task` instead of `UniTask`

.github/instructions/jengine.instructions.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,84 @@ internal class MyEditorClass
102102
}
103103
```
104104

105+
## Unit Testing
106+
107+
### Scope
108+
Unit tests are required for all non-core JEngine packages — i.e. any package under `Packages/com.jasonxudeveloper.jengine.*` **except** `jengine.core`. This includes JEngine.UI, JEngine.Util, and any future packages.
109+
110+
### Coverage Requirement
111+
New features and new logic MUST include unit tests targeting **93%+ code coverage**:
112+
- All public methods, properties, and constructors
113+
- Fluent API chaining
114+
- Edge cases and error conditions
115+
- Event handlers and callbacks (use reflection for private handlers)
116+
117+
### Test Modes
118+
- **EditMode tests** (`Tests/Editor/`): Preferred for most logic — fast, no scene required.
119+
- **PlayMode tests** (`Tests/Runtime/`): Use when the test needs a running game loop, MonoBehaviour lifecycle, or scene loading. PlayMode tests **must run non-interactively** (no user input, no manual scene setup). Use `[UnityTest]` with `UniTask.ToCoroutine()` for async PlayMode tests.
120+
121+
### Test Location
122+
Tests mirror the source structure under each package's test folders:
123+
```
124+
Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Button/JButton.cs
125+
→ Packages/com.jasonxudeveloper.jengine.ui/Tests/Editor/Components/Button/JButtonTests.cs
126+
127+
Packages/com.jasonxudeveloper.jengine.util/Runtime/JAction.cs
128+
→ Packages/com.jasonxudeveloper.jengine.util/Tests/Editor/JActionTests.cs
129+
130+
# PlayMode tests when runtime behavior requires it:
131+
Packages/com.jasonxudeveloper.jengine.util/Runtime/SomeFeature.cs
132+
→ Packages/com.jasonxudeveloper.jengine.util/Tests/Runtime/SomeFeatureTests.cs
133+
```
134+
135+
### EditMode Test Pattern
136+
```csharp
137+
[TestFixture]
138+
public class MyComponentTests
139+
{
140+
private MyComponent _component;
141+
142+
[SetUp]
143+
public void SetUp()
144+
{
145+
_component = new MyComponent();
146+
}
147+
148+
[Test]
149+
public void Constructor_Default_AddsBaseClass()
150+
{
151+
Assert.IsTrue(_component.ClassListContains("my-component"));
152+
}
153+
}
154+
```
155+
156+
### PlayMode Test Pattern
157+
PlayMode tests must be fully automated — no interactive input or manual scene setup:
158+
```csharp
159+
[TestFixture]
160+
public class MyRuntimeTests
161+
{
162+
[UnityTest]
163+
public IEnumerator MyAsyncTest() => UniTask.ToCoroutine(async () =>
164+
{
165+
var go = new GameObject();
166+
var component = go.AddComponent<MyBehaviour>();
167+
await UniTask.DelayFrame(1);
168+
Assert.IsTrue(component.IsInitialized);
169+
Object.Destroy(go);
170+
});
171+
}
172+
```
173+
174+
### Testing Private Methods via Reflection
175+
For private event handlers and internal styling methods:
176+
```csharp
177+
var method = typeof(MyComponent).GetMethod("OnMouseEnter",
178+
BindingFlags.NonPublic | BindingFlags.Instance);
179+
method.Invoke(_component, new object[] { null });
180+
Assert.AreEqual(expectedColor, _component.style.backgroundColor.value);
181+
```
182+
105183
## Review Focus Areas
106184

107185
When reviewing JEngine code, check:
@@ -110,3 +188,4 @@ When reviewing JEngine code, check:
110188
3. Resource cleanup (ScriptableObjects, events)
111189
4. Thread safety for callback-accessed state
112190
5. Proper namespace usage
191+
6. Unit tests with 93%+ coverage for new features/logic
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// JTabView.cs
2+
//
3+
// Author:
4+
// JasonXuDeveloper <jason@xgamedev.net>
5+
//
6+
// Copyright (c) 2025 JEngine
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
// THE SOFTWARE.
25+
26+
using System.Collections.Generic;
27+
using JEngine.UI.Editor.Theming;
28+
using UnityEngine;
29+
using UnityEngine.UIElements;
30+
31+
namespace JEngine.UI.Editor.Components.Navigation
32+
{
33+
/// <summary>
34+
/// A tabbed container that shows one content panel at a time.
35+
/// Each tab has a button in the tab bar and an associated content element.
36+
/// </summary>
37+
public class JTabView : VisualElement
38+
{
39+
private readonly VisualElement _tabBar;
40+
private readonly VisualElement _contentArea;
41+
private readonly List<Label> _tabButtons = new();
42+
private readonly List<VisualElement> _contentPanels = new();
43+
private int _selectedIndex = -1;
44+
private int _maxTabsPerRow;
45+
46+
/// <summary>
47+
/// Creates a new tab view with a tab bar and content area.
48+
/// </summary>
49+
/// <param name="maxTabsPerRow">Maximum number of tabs per row. 0 means no limit (auto-wrap).</param>
50+
public JTabView(int maxTabsPerRow = 0)
51+
{
52+
_maxTabsPerRow = maxTabsPerRow;
53+
54+
AddToClassList("j-tab-view");
55+
56+
// Tab bar - horizontal row of tab buttons
57+
_tabBar = new VisualElement();
58+
_tabBar.AddToClassList("j-tab-view__bar");
59+
_tabBar.style.flexDirection = FlexDirection.Row;
60+
_tabBar.style.flexWrap = Wrap.Wrap;
61+
_tabBar.style.backgroundColor = Tokens.Colors.BgSurface;
62+
_tabBar.style.borderBottomWidth = 1;
63+
_tabBar.style.borderBottomColor = Tokens.Colors.BorderSubtle;
64+
_tabBar.style.borderTopLeftRadius = Tokens.BorderRadius.MD;
65+
_tabBar.style.borderTopRightRadius = Tokens.BorderRadius.MD;
66+
hierarchy.Add(_tabBar);
67+
68+
// Content area
69+
_contentArea = new VisualElement();
70+
_contentArea.AddToClassList("j-tab-view__content");
71+
_contentArea.style.paddingTop = Tokens.Spacing.Lg;
72+
_contentArea.style.paddingBottom = Tokens.Spacing.Lg;
73+
_contentArea.style.paddingLeft = Tokens.Spacing.Lg;
74+
_contentArea.style.paddingRight = Tokens.Spacing.Lg;
75+
hierarchy.Add(_contentArea);
76+
}
77+
78+
/// <summary>
79+
/// Gets the currently selected tab index, or -1 if no tabs exist.
80+
/// </summary>
81+
public int SelectedIndex => _selectedIndex;
82+
83+
/// <summary>
84+
/// Gets the number of tabs.
85+
/// </summary>
86+
public int TabCount => _tabButtons.Count;
87+
88+
/// <summary>
89+
/// Gets the maximum number of tabs per row. 0 means no limit (auto-wrap).
90+
/// </summary>
91+
public int MaxTabsPerRow => _maxTabsPerRow;
92+
93+
/// <summary>
94+
/// Adds a tab with the given label and content.
95+
/// The first tab added is automatically selected.
96+
/// </summary>
97+
/// <param name="label">The tab button text.</param>
98+
/// <param name="content">The content element shown when this tab is active.</param>
99+
/// <returns>This tab view for fluent chaining.</returns>
100+
public JTabView AddTab(string label, VisualElement content)
101+
{
102+
var index = _tabButtons.Count;
103+
104+
// Create tab button
105+
var tabButton = new Label(label);
106+
tabButton.AddToClassList("j-tab-view__tab");
107+
tabButton.style.fontSize = Tokens.FontSize.Sm;
108+
tabButton.style.paddingTop = Tokens.Spacing.Sm;
109+
tabButton.style.paddingBottom = Tokens.Spacing.Sm;
110+
tabButton.style.paddingLeft = Tokens.Spacing.Lg;
111+
tabButton.style.paddingRight = Tokens.Spacing.Lg;
112+
tabButton.style.unityTextAlign = TextAnchor.MiddleCenter;
113+
114+
// Rounded corners like buttons
115+
tabButton.style.borderTopLeftRadius = Tokens.BorderRadius.MD;
116+
tabButton.style.borderTopRightRadius = Tokens.BorderRadius.MD;
117+
tabButton.style.borderBottomLeftRadius = Tokens.BorderRadius.MD;
118+
tabButton.style.borderBottomRightRadius = Tokens.BorderRadius.MD;
119+
tabButton.style.marginTop = Tokens.Spacing.Xs;
120+
tabButton.style.marginBottom = Tokens.Spacing.Xs;
121+
tabButton.style.marginLeft = Tokens.Spacing.Xs;
122+
tabButton.style.marginRight = Tokens.Spacing.Xs;
123+
124+
// Constrain tabs per row via percentage width
125+
if (_maxTabsPerRow > 0)
126+
{
127+
// Reduce basis to account for per-tab margins; flexGrow fills remaining space
128+
var percent = (100f - _maxTabsPerRow * 2f) / _maxTabsPerRow;
129+
tabButton.style.flexBasis = new StyleLength(new Length(percent, LengthUnit.Percent));
130+
tabButton.style.flexGrow = 1;
131+
tabButton.style.flexShrink = 1;
132+
}
133+
134+
JTheme.ApplyTransition(tabButton);
135+
JTheme.ApplyPointerCursor(tabButton);
136+
137+
// Click handler using closure-free pattern via userData
138+
tabButton.userData = index;
139+
tabButton.RegisterCallback<MouseDownEvent>(OnTabClicked);
140+
141+
// Hover handlers
142+
tabButton.RegisterCallback<MouseEnterEvent>(OnTabMouseEnter);
143+
tabButton.RegisterCallback<MouseLeaveEvent>(OnTabMouseLeave);
144+
145+
_tabButtons.Add(tabButton);
146+
_tabBar.Add(tabButton);
147+
148+
// Add content panel (hidden by default)
149+
content.style.display = DisplayStyle.None;
150+
_contentPanels.Add(content);
151+
_contentArea.Add(content);
152+
153+
// Auto-select first tab
154+
if (_tabButtons.Count == 1)
155+
{
156+
SelectTab(0);
157+
}
158+
else
159+
{
160+
ApplyInactiveStyle(tabButton);
161+
}
162+
163+
return this;
164+
}
165+
166+
/// <summary>
167+
/// Selects the tab at the given index.
168+
/// </summary>
169+
/// <param name="index">The zero-based tab index.</param>
170+
public void SelectTab(int index)
171+
{
172+
if (index < 0 || index >= _tabButtons.Count)
173+
return;
174+
175+
// Deselect previous tab
176+
if (_selectedIndex >= 0 && _selectedIndex < _tabButtons.Count)
177+
{
178+
ApplyInactiveStyle(_tabButtons[_selectedIndex]);
179+
_contentPanels[_selectedIndex].style.display = DisplayStyle.None;
180+
}
181+
182+
_selectedIndex = index;
183+
184+
// Select new tab
185+
ApplyActiveStyle(_tabButtons[index]);
186+
_contentPanels[index].style.display = DisplayStyle.Flex;
187+
}
188+
189+
private static void ApplyActiveStyle(Label tab)
190+
{
191+
tab.style.backgroundColor = Tokens.Colors.Primary;
192+
tab.style.color = Tokens.Colors.PrimaryText;
193+
tab.style.unityFontStyleAndWeight = FontStyle.Bold;
194+
}
195+
196+
private static void ApplyInactiveStyle(Label tab)
197+
{
198+
tab.style.backgroundColor = StyleKeyword.None;
199+
tab.style.color = Tokens.Colors.TextSecondary;
200+
tab.style.unityFontStyleAndWeight = FontStyle.Normal;
201+
}
202+
203+
private static void OnTabClicked(MouseDownEvent evt)
204+
{
205+
if (evt.target is Label tab && tab.userData is int index)
206+
{
207+
// Walk up to find the JTabView parent
208+
var parent = tab.parent?.parent;
209+
if (parent is JTabView tabView)
210+
{
211+
tabView.SelectTab(index);
212+
}
213+
}
214+
}
215+
216+
private void OnTabMouseEnter(MouseEnterEvent evt)
217+
{
218+
if (evt.target is Label tab && tab.userData is int index && index != _selectedIndex)
219+
{
220+
tab.style.backgroundColor = Tokens.Colors.BgHover;
221+
}
222+
}
223+
224+
private void OnTabMouseLeave(MouseLeaveEvent evt)
225+
{
226+
if (evt.target is Label tab && tab.userData is int index && index != _selectedIndex)
227+
{
228+
tab.style.backgroundColor = StyleKeyword.None;
229+
}
230+
}
231+
}
232+
}

UnityProject/Packages/com.jasonxudeveloper.jengine.ui/Editor/Components/Navigation/JTabView.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)