diff --git a/src/LogExpert.Tests/LogExpert.Tests.csproj b/src/LogExpert.Tests/LogExpert.Tests.csproj index d8bfb785..d55c5437 100644 --- a/src/LogExpert.Tests/LogExpert.Tests.csproj +++ b/src/LogExpert.Tests/LogExpert.Tests.csproj @@ -29,6 +29,8 @@ + + diff --git a/src/LogExpert.Tests/Services/TabControllerTests.cs b/src/LogExpert.Tests/Services/TabControllerTests.cs index bcd23ac9..c55f5054 100644 --- a/src/LogExpert.Tests/Services/TabControllerTests.cs +++ b/src/LogExpert.Tests/Services/TabControllerTests.cs @@ -1,8 +1,14 @@ using System.Runtime.Versioning; +using LogExpert.Core.Config; +using LogExpert.Core.Entities; +using LogExpert.Core.Interfaces; using LogExpert.UI.Controls.LogWindow; +using LogExpert.UI.Interface; using LogExpert.UI.Services.TabControllerService; +using Moq; + using NUnit.Framework; using WeifenLuo.WinFormsUI.Docking; @@ -11,11 +17,8 @@ namespace LogExpert.Tests.Services; /// /// Unit tests for TabController. -/// Note: Many tests are limited because LogWindow is a complex WinForms control that cannot be easily mocked or -/// subclassed. Tests that require actual LogWindow instances would need to be run as integration tests with full UI -/// infrastructure. -/// These tests focus on the core TabController functionality that can be tested without instantiating LogWindow -/// objects. +/// Most tests focus on behavior that does not require LogWindow instances. +/// Tests that verify DockPanel tab order create real LogWindow instances through CreateLogWindow. /// [TestFixture] [SupportedOSPlatform("windows")] @@ -36,7 +39,10 @@ public void Setup () _dockPanel = new DockPanel { Dock = DockStyle.Fill, - DocumentStyle = DocumentStyle.DockingMdi + // Match LogExpert's document style; DockingMdi required test form to be an MDI container. + // Using DockingWindow both matches production behavior and allows the test to create real tabs without setting up an artificial MDI container. + DocumentStyle = DocumentStyle.DockingWindow, + Theme = new VS2015LightTheme() }; _testForm.Controls.Add(_dockPanel); _testForm.Show(); // Must show form for DockPanel to work @@ -191,9 +197,53 @@ public void GetAllWindowsFromDockPanel_ReturnsReadOnlyList () Assert.That(result, Is.InstanceOf>()); } + [Test] + public void GetAllWindowsFromDockPanel_ReturnsDisplayedWindowsInTabOrder () + { + // Arrange + using var firstWindow = CreateLogWindow("first.log"); + using var secondWindow = CreateLogWindow("second.log"); + using var thirdWindow = CreateLogWindow("third.log"); + + _tabController.AddWindow(firstWindow, "first.log"); + _tabController.AddWindow(secondWindow, "second.log"); + _tabController.AddWindow(thirdWindow, "third.log"); + + // Act + var result = _tabController.GetAllWindowsFromDockPanel(); + + // Assert + Assert.That(result, Is.EqualTo(new[] { firstWindow, secondWindow, thirdWindow })); + } + #endregion - #region GetAllWindows Tests + #region Helpers + + private static LogWindow CreateLogWindow (string fileName) + { + var coordinatorMock = new Mock(); + _ = coordinatorMock.Setup(coordinator => coordinator.ResolveHighlightGroup(It.IsAny(), It.IsAny())).Returns(new HighlightGroup()); + _ = coordinatorMock.SetupGet(coordinator => coordinator.SearchParams).Returns(new SearchParams()); + + var configManagerMock = new Mock(); + _ = configManagerMock.SetupGet(configManager => configManager.Settings).Returns(new Settings()); + + var pluginRegistryMock = new Mock(); + _ = pluginRegistryMock.SetupGet(pluginRegistry => pluginRegistry.RegisteredColumnizers).Returns([new DefaultLogfileColumnizer()]); + + return new LogWindow( + coordinatorMock.Object, + fileName, + isTempFile: false, + forcePersistenceLoading: false, + configManagerMock.Object, + pluginRegistryMock.Object); + } + + #endregion + + #region GetAllWindows Tests [Test] public void GetAllWindows_WhenEmpty_ReturnsEmptyList () @@ -315,10 +365,8 @@ public void AddWindow_WhenNotInitialized_ThrowsInvalidOperationException () // Arrange using var controller = new TabController(); - // Create a mock-like object that's not null to avoid ArgumentNullException - // We need to test that the "not initialized" check happens - // Unfortunately, LogWindow cannot be instantiated without its dependencies - // So we can only verify the ArgumentNullException is thrown first for null + // CreateLogWindow can now provide a non-null LogWindow when needed. + // This test verifies that null argument validation happens before the initialization check. var ex = Assert.Throws(() => controller.AddWindow(null, "Test")); Assert.That(ex.ParamName, Is.EqualTo("window")); } diff --git a/src/LogExpert.UI/Services/TabControllerService/TabController.cs b/src/LogExpert.UI/Services/TabControllerService/TabController.cs index b91a1a24..4bc9a023 100644 --- a/src/LogExpert.UI/Services/TabControllerService/TabController.cs +++ b/src/LogExpert.UI/Services/TabControllerService/TabController.cs @@ -461,12 +461,29 @@ protected virtual void Dispose (bool disposing) /// Read-only list of all LogWindows in the DockPanel public IReadOnlyList GetAllWindowsFromDockPanel () { - return !_initialized || _dockPanel == null - ? [] - : _dockPanel.Panes - .SelectMany(pane => pane.DisplayingContents.OfType()) - .ToList() - .AsReadOnly(); + if (!_initialized || _dockPanel == null) + { + return []; + } + + var windows = new List(); + + foreach (DockPane pane in _dockPanel.Panes) + { + var displayingContents = pane.DisplayingContents; + + // Use 'for' instead of 'foreach': DisplayingContents exposes displayed tabs through Count and its indexer. + // 'foreach' uses the inherited ReadOnlyCollection enumerator and does not return displayed tabs. + for (int index = 0; index < displayingContents.Count; index++) + { + if (displayingContents[index] is LogWindow logWindow) + { + windows.Add(logWindow); + } + } + } + + return windows.AsReadOnly(); } #endregion