diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 7ea012fe0e..9911ff6d81 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -771,6 +771,7 @@ istep ith ITHUMBNAIL IUI +IUWP IWIC jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi @@ -1646,6 +1647,7 @@ STYLECHANGED STYLECHANGING subkeys sublang +Subdomain SUBMODULEUPDATE subresource Superbar diff --git a/PowerToys.sln b/PowerToys.sln index 00986aae29..6033ca1481 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -788,6 +788,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2850,6 +2854,22 @@ Global {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64 + {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64 + {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3161,6 +3181,8 @@ Global {E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs new file mode 100644 index 0000000000..e7fbc6859d --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +[TestClass] +public class AllAppsCommandProviderTests : AppsTestBase +{ + [TestMethod] + public void ProviderHasDisplayName() + { + // Setup + var provider = new AllAppsCommandProvider(); + + // Assert + Assert.IsNotNull(provider.DisplayName); + Assert.IsTrue(provider.DisplayName.Length > 0); + } + + [TestMethod] + public void ProviderHasIcon() + { + // Setup + var provider = new AllAppsCommandProvider(); + + // Assert + Assert.IsNotNull(provider.Icon); + } + + [TestMethod] + public void TopLevelCommandsNotEmpty() + { + // Setup + var provider = new AllAppsCommandProvider(); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length > 0); + } + + [TestMethod] + public void LookupAppWithEmptyNameReturnsNotNull() + { + // Setup + var mockApp = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe"); + MockCache.AddWin32Program(mockApp); + var page = new AllAppsPage(MockCache); + + var provider = new AllAppsCommandProvider(page); + + // Act + var result = provider.LookupApp(string.Empty); + + // Assert + Assert.IsNotNull(result); + } + + [TestMethod] + public async Task ProviderWithMockData_LookupApp_ReturnsCorrectApp() + { + // Arrange + var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe"); + MockCache.AddWin32Program(testApp); + + var provider = new AllAppsCommandProvider(Page); + + // Wait for initialization to complete + await WaitForPageInitializationAsync(); + + // Act + var result = provider.LookupApp("TestApp"); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("TestApp", result.Title); + } + + [TestMethod] + public async Task ProviderWithMockData_LookupApp_ReturnsNullForNonExistentApp() + { + // Arrange + var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe"); + MockCache.AddWin32Program(testApp); + + var provider = new AllAppsCommandProvider(Page); + + // Wait for initialization to complete + await WaitForPageInitializationAsync(); + + // Act + var result = provider.LookupApp("NonExistentApp"); + + // Assert + Assert.IsNull(result); + } + + [TestMethod] + public void ProviderWithMockData_TopLevelCommands_IncludesListItem() + { + // Arrange + var provider = new AllAppsCommandProvider(Page); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length >= 1); // At least the list item should be present + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs new file mode 100644 index 0000000000..3ac1eaff68 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsPageTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +[TestClass] +public class AllAppsPageTests : AppsTestBase +{ + [TestMethod] + public void AllAppsPage_Constructor_ThrowsOnNullAppCache() + { + // Act & Assert + Assert.ThrowsException(() => new AllAppsPage(null!)); + } + + [TestMethod] + public void AllAppsPage_WithMockCache_InitializesSuccessfully() + { + // Arrange + var mockCache = new MockAppCache(); + + // Act + var page = new AllAppsPage(mockCache); + + // Assert + Assert.IsNotNull(page); + Assert.IsNotNull(page.Name); + Assert.IsNotNull(page.Icon); + } + + [TestMethod] + public async Task AllAppsPage_GetItems_ReturnsEmptyWithEmptyCache() + { + // Act - Wait for initialization to complete + await WaitForPageInitializationAsync(); + var items = Page.GetItems(); + + // Assert + Assert.IsNotNull(items); + Assert.AreEqual(0, items.Length); + } + + [TestMethod] + public async Task AllAppsPage_GetItems_ReturnsAppsFromCacheAsync() + { + // Arrange + var mockCache = new MockAppCache(); + var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe"); + var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator"); + + mockCache.AddWin32Program(win32App); + mockCache.AddUWPApplication(uwpApp); + + var page = new AllAppsPage(mockCache); + + // Wait a bit for initialization to complete + await Task.Delay(100); + + // Act + var items = page.GetItems(); + + // Assert + Assert.IsNotNull(items); + Assert.AreEqual(2, items.Length); + + // we need to loop the items to ensure we got the correct ones + Assert.IsTrue(items.Any(i => i.Title == "Notepad")); + Assert.IsTrue(items.Any(i => i.Title == "Calculator")); + } + + [TestMethod] + public async Task AllAppsPage_GetPinnedApps_ReturnsEmptyWhenNoAppsArePinned() + { + // Arrange + var mockCache = new MockAppCache(); + var app = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe"); + mockCache.AddWin32Program(app); + + var page = new AllAppsPage(mockCache); + + // Wait a bit for initialization to complete + await Task.Delay(100); + + // Act + var pinnedApps = page.GetPinnedApps(); + + // Assert + Assert.IsNotNull(pinnedApps); + Assert.AreEqual(0, pinnedApps.Length); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AppsTestBase.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AppsTestBase.cs new file mode 100644 index 0000000000..4d1210db7b --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AppsTestBase.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +/// +/// Base class for Apps unit tests that provides common setup and teardown functionality. +/// +public abstract class AppsTestBase +{ + /// + /// Gets the mock application cache used in tests. + /// + protected MockAppCache MockCache { get; private set; } = null!; + + /// + /// Gets the AllAppsPage instance used in tests. + /// + protected AllAppsPage Page { get; private set; } = null!; + + /// + /// Sets up the test environment before each test method. + /// + /// A task representing the asynchronous setup operation. + [TestInitialize] + public virtual async Task Setup() + { + MockCache = new MockAppCache(); + Page = new AllAppsPage(MockCache); + + // Ensure initialization is complete + await MockCache.RefreshAsync(); + } + + /// + /// Cleans up the test environment after each test method. + /// + [TestCleanup] + public virtual void Cleanup() + { + MockCache?.Dispose(); + } + + /// + /// Forces synchronous initialization of the page for testing. + /// + protected void EnsurePageInitialized() + { + // Trigger BuildListItems by accessing items + _ = Page.GetItems(); + } + + /// + /// Waits for page initialization with timeout. + /// + /// The timeout in milliseconds. + /// A task representing the asynchronous wait operation. + protected async Task WaitForPageInitializationAsync(int timeoutMs = 1000) + { + await MockCache.RefreshAsync(); + EnsurePageInitialized(); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj new file mode 100644 index 0000000000..d6a9638378 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Microsoft.CmdPal.Ext.Apps.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + + + false + true + Microsoft.CmdPal.Ext.Apps.UnitTests + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\ + false + false + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockAppCache.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockAppCache.cs new file mode 100644 index 0000000000..03530cb5ce --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockAppCache.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Programs; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +/// +/// Mock implementation of IAppCache for unit testing. +/// +public class MockAppCache : IAppCache +{ + private readonly List _win32s = new(); + private readonly List _uwps = new(); + private bool _disposed; + private bool _shouldReload; + + /// + /// Gets the collection of Win32 programs. + /// + public IList Win32s => _win32s.AsReadOnly(); + + /// + /// Gets the collection of UWP applications. + /// + public IList UWPs => _uwps.AsReadOnly(); + + /// + /// Determines whether the cache should be reloaded. + /// + /// True if cache should be reloaded, false otherwise. + public bool ShouldReload() => _shouldReload; + + /// + /// Resets the reload flag. + /// + public void ResetReloadFlag() => _shouldReload = false; + + /// + /// Asynchronously refreshes the cache. + /// + /// A task representing the asynchronous refresh operation. + public async Task RefreshAsync() + { + // Simulate minimal async operation for testing + await Task.Delay(1); + } + + /// + /// Adds a Win32 program to the cache. + /// + /// The Win32 program to add. + /// Thrown when program is null. + public void AddWin32Program(Win32Program program) + { + ArgumentNullException.ThrowIfNull(program); + + _win32s.Add(program); + } + + /// + /// Adds a UWP application to the cache. + /// + /// The UWP application to add. + /// Thrown when app is null. + public void AddUWPApplication(IUWPApplication app) + { + ArgumentNullException.ThrowIfNull(app); + + _uwps.Add(app); + } + + /// + /// Clears all applications from the cache. + /// + public void ClearAll() + { + _win32s.Clear(); + _uwps.Clear(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Clean up managed resources + _win32s.Clear(); + _uwps.Clear(); + } + + _disposed = true; + } + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockUWPApplication.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockUWPApplication.cs new file mode 100644 index 0000000000..ae39e70fef --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/MockUWPApplication.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Apps.Programs; +using Microsoft.CmdPal.Ext.Apps.Utils; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +/// +/// Mock implementation of IUWPApplication for unit testing. +/// +public class MockUWPApplication : IUWPApplication +{ + /// + /// Gets or sets the app list entry. + /// + public string AppListEntry { get; set; } = string.Empty; + + /// + /// Gets or sets the unique identifier. + /// + public string UniqueIdentifier { get; set; } = string.Empty; + + /// + /// Gets or sets the display name. + /// + public string DisplayName { get; set; } = string.Empty; + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the user model ID. + /// + public string UserModelId { get; set; } = string.Empty; + + /// + /// Gets or sets the background color. + /// + public string BackgroundColor { get; set; } = string.Empty; + + /// + /// Gets or sets the entry point. + /// + public string EntryPoint { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the application is enabled. + /// + public bool Enabled { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the application can run elevated. + /// + public bool CanRunElevated { get; set; } + + /// + /// Gets or sets the logo path. + /// + public string LogoPath { get; set; } = string.Empty; + + /// + /// Gets or sets the logo type. + /// + public LogoType LogoType { get; set; } = LogoType.Colored; + + /// + /// Gets or sets the UWP package. + /// + public UWP Package { get; set; } = null!; + + /// + /// Gets the name of the application. + /// + public string Name => DisplayName; + + /// + /// Gets the location of the application. + /// + public string Location => Package?.Location ?? string.Empty; + + /// + /// Gets the localized location of the application. + /// + public string LocationLocalized => Package?.LocationLocalized ?? string.Empty; + + /// + /// Gets the application identifier. + /// + /// The user model ID of the application. + public string GetAppIdentifier() + { + return UserModelId; + } + + /// + /// Gets the commands available for this application. + /// + /// A list of context items. + public List GetCommands() + { + return new List(); + } + + /// + /// Updates the logo path based on the specified theme. + /// + /// The theme to use for the logo. + public void UpdateLogoPath(Theme theme) + { + // Mock implementation - no-op for testing + } + + /// + /// Converts this UWP application to an AppItem. + /// + /// An AppItem representation of this UWP application. + public AppItem ToAppItem() + { + var iconPath = LogoType != LogoType.Error ? LogoPath : string.Empty; + return new AppItem() + { + Name = Name, + Subtitle = Description, + Type = "Packaged Application", // Equivalent to UWPApplication.Type() + IcoPath = iconPath, + DirPath = Location, + UserModelId = UserModelId, + IsPackaged = true, + Commands = GetCommands(), + AppIdentifier = GetAppIdentifier(), + }; + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/QueryTests.cs new file mode 100644 index 0000000000..e04c678b58 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/QueryTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CmdPal.Ext.UnitTestBase; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +[TestClass] +public class QueryTests : CommandPaletteUnitTestBase +{ + [TestMethod] + public void QueryReturnsExpectedResults() + { + // Arrange + var mockCache = new MockAppCache(); + var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe"); + var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator"); + mockCache.AddWin32Program(win32App); + mockCache.AddUWPApplication(uwpApp); + + for (var i = 0; i < 10; i++) + { + mockCache.AddWin32Program(TestDataHelper.CreateTestWin32Program($"App{i}")); + mockCache.AddUWPApplication(TestDataHelper.CreateTestUWPApplication($"UWP App {i}")); + } + + var page = new AllAppsPage(mockCache); + var provider = new AllAppsCommandProvider(page); + + // Act + var allItems = page.GetItems(); + + // Assert + var notepadResult = Query("notepad", allItems).FirstOrDefault(); + Assert.IsNotNull(notepadResult); + Assert.AreEqual("Notepad", notepadResult.Title); + + var calculatorResult = Query("cal", allItems).FirstOrDefault(); + Assert.IsNotNull(calculatorResult); + Assert.AreEqual("Calculator", calculatorResult.Title); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Settings.cs new file mode 100644 index 0000000000..b48abaf32a --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/Settings.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Apps.Helpers; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +public class Settings : ISettingsInterface +{ + private readonly bool enableStartMenuSource; + private readonly bool enableDesktopSource; + private readonly bool enableRegistrySource; + private readonly bool enablePathEnvironmentVariableSource; + private readonly List programSuffixes; + private readonly List runCommandSuffixes; + + public Settings( + bool enableStartMenuSource = true, + bool enableDesktopSource = true, + bool enableRegistrySource = true, + bool enablePathEnvironmentVariableSource = true, + List programSuffixes = null, + List runCommandSuffixes = null) + { + this.enableStartMenuSource = enableStartMenuSource; + this.enableDesktopSource = enableDesktopSource; + this.enableRegistrySource = enableRegistrySource; + this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource; + this.programSuffixes = programSuffixes ?? new List { "bat", "appref-ms", "exe", "lnk", "url" }; + this.runCommandSuffixes = runCommandSuffixes ?? new List { "bat", "appref-ms", "exe", "lnk", "url", "cpl", "msc" }; + } + + public bool EnableStartMenuSource => enableStartMenuSource; + + public bool EnableDesktopSource => enableDesktopSource; + + public bool EnableRegistrySource => enableRegistrySource; + + public bool EnablePathEnvironmentVariableSource => enablePathEnvironmentVariableSource; + + public List ProgramSuffixes => programSuffixes; + + public List RunCommandSuffixes => runCommandSuffixes; + + public static Settings CreateDefaultSettings() => new Settings(); + + public static Settings CreateDisabledSourcesSettings() => new Settings( + enableStartMenuSource: false, + enableDesktopSource: false, + enableRegistrySource: false, + enablePathEnvironmentVariableSource: false); + + public static Settings CreateCustomSuffixesSettings() => new Settings( + programSuffixes: new List { "exe", "bat" }, + runCommandSuffixes: new List { "exe", "bat", "cmd" }); +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/TestDataHelper.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/TestDataHelper.cs new file mode 100644 index 0000000000..88936e4285 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/TestDataHelper.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.Apps.Programs; + +namespace Microsoft.CmdPal.Ext.Apps.UnitTests; + +/// +/// Helper class to create test data for unit tests. +/// +public static class TestDataHelper +{ + /// + /// Creates a test Win32 program with the specified parameters. + /// + /// The name of the application. + /// The full path to the application executable. + /// A value indicating whether the application is enabled. + /// A value indicating whether the application is valid. + /// A new Win32Program instance with the specified parameters. + public static Win32Program CreateTestWin32Program( + string name = "Test App", + string fullPath = "C:\\TestApp\\app.exe", + bool enabled = true, + bool valid = true) + { + return new Win32Program + { + Name = name, + FullPath = fullPath, + Enabled = enabled, + Valid = valid, + UniqueIdentifier = $"win32_{name}", + Description = $"Test description for {name}", + ExecutableName = "app.exe", + ParentDirectory = "C:\\TestApp", + AppType = Win32Program.ApplicationType.Win32Application, + }; + } + + /// + /// Creates a test UWP application with the specified parameters. + /// + /// The display name of the application. + /// The user model ID of the application. + /// A value indicating whether the application is enabled. + /// A new IUWPApplication instance with the specified parameters. + public static IUWPApplication CreateTestUWPApplication( + string displayName = "Test UWP App", + string userModelId = "TestPublisher.TestUWPApp_1.0.0.0_neutral__8wekyb3d8bbwe", + bool enabled = true) + { + return new MockUWPApplication + { + DisplayName = displayName, + UserModelId = userModelId, + Enabled = enabled, + UniqueIdentifier = $"uwp_{userModelId}", + Description = $"Test UWP description for {displayName}", + AppListEntry = "default", + BackgroundColor = "#000000", + EntryPoint = "TestApp.App", + CanRunElevated = false, + LogoPath = string.Empty, + Package = CreateMockUWPPackage(displayName, userModelId), + }; + } + + /// + /// Creates a mock UWP package for testing purposes. + /// + /// The display name of the package. + /// The user model ID of the package. + /// A new UWP package instance. + private static UWP CreateMockUWPPackage(string displayName, string userModelId) + { + var mockPackage = new MockPackage + { + Name = displayName, + FullName = userModelId, + FamilyName = $"{displayName}_8wekyb3d8bbwe", + InstalledLocation = $"C:\\Program Files\\WindowsApps\\{displayName}", + }; + + return new UWP(mockPackage) + { + Location = mockPackage.InstalledLocation, + LocationLocalized = mockPackage.InstalledLocation, + }; + } + + /// + /// Mock implementation of IPackage for testing purposes. + /// + private sealed class MockPackage : IPackage + { + /// + /// Gets or sets the name of the package. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the full name of the package. + /// + public string FullName { get; set; } = string.Empty; + + /// + /// Gets or sets the family name of the package. + /// + public string FamilyName { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the package is a framework package. + /// + public bool IsFramework { get; set; } + + /// + /// Gets or sets a value indicating whether the package is in development mode. + /// + public bool IsDevelopmentMode { get; set; } + + /// + /// Gets or sets the installed location of the package. + /// + public string InstalledLocation { get; set; } = string.Empty; + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkDataTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkDataTests.cs new file mode 100644 index 0000000000..2ee3deeb5d --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkDataTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; + +[TestClass] +public class BookmarkDataTests +{ + [TestMethod] + public void BookmarkDataWebUrlDetection() + { + // Act + var webBookmark = new BookmarkData + { + Name = "Test Site", + Bookmark = "https://test.com", + }; + + var nonWebBookmark = new BookmarkData + { + Name = "Local File", + Bookmark = "C:\\temp\\file.txt", + }; + + var placeholderBookmark = new BookmarkData + { + Name = "Placeholder", + Bookmark = "{Placeholder}", + }; + + // Assert + Assert.IsTrue(webBookmark.IsWebUrl()); + Assert.IsFalse(webBookmark.IsPlaceholder); + Assert.IsFalse(nonWebBookmark.IsWebUrl()); + Assert.IsFalse(nonWebBookmark.IsPlaceholder); + + Assert.IsTrue(placeholderBookmark.IsPlaceholder); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkJsonParserTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkJsonParserTests.cs new file mode 100644 index 0000000000..e442818f8a --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkJsonParserTests.cs @@ -0,0 +1,535 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; + +[TestClass] +public class BookmarkJsonParserTests +{ + private BookmarkJsonParser _parser; + + [TestInitialize] + public void Setup() + { + _parser = new BookmarkJsonParser(); + } + + [TestMethod] + public void ParseBookmarks_ValidJson_ReturnsBookmarks() + { + // Arrange + var json = """ + { + "Data": [ + { + "Name": "Google", + "Bookmark": "https://www.google.com" + }, + { + "Name": "Local File", + "Bookmark": "C:\\temp\\file.txt" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Data.Count); + Assert.AreEqual("Google", result.Data[0].Name); + Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark); + Assert.AreEqual("Local File", result.Data[1].Name); + Assert.AreEqual("C:\\temp\\file.txt", result.Data[1].Bookmark); + } + + [TestMethod] + public void ParseBookmarks_EmptyJson_ReturnsEmptyBookmarks() + { + // Arrange + var json = "{}"; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Data.Count); + } + + [TestMethod] + public void ParseBookmarks_NullJson_ReturnsEmptyBookmarks() + { + // Act + var result = _parser.ParseBookmarks(null); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Data.Count); + } + + [TestMethod] + public void ParseBookmarks_WhitespaceJson_ReturnsEmptyBookmarks() + { + // Act + var result = _parser.ParseBookmarks(" "); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Data.Count); + } + + [TestMethod] + public void ParseBookmarks_EmptyString_ReturnsEmptyBookmarks() + { + // Act + var result = _parser.ParseBookmarks(string.Empty); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Data.Count); + } + + [TestMethod] + public void ParseBookmarks_InvalidJson_ReturnsEmptyBookmarks() + { + // Arrange + var invalidJson = "{invalid json}"; + + // Act + var result = _parser.ParseBookmarks(invalidJson); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Data.Count); + } + + [TestMethod] + public void ParseBookmarks_MalformedJson_ReturnsEmptyBookmarks() + { + // Arrange + var malformedJson = """ + { + "Data": [ + { + "Name": "Google", + "Bookmark": "https://www.google.com" + }, + { + "Name": "Incomplete entry" + """; + + // Act + var result = _parser.ParseBookmarks(malformedJson); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.Data.Count); + } + + [TestMethod] + public void ParseBookmarks_JsonWithTrailingCommas_ParsesSuccessfully() + { + // Arrange - JSON with trailing commas (should be handled by AllowTrailingCommas option) + var json = """ + { + "Data": [ + { + "Name": "Google", + "Bookmark": "https://www.google.com", + }, + { + "Name": "Local File", + "Bookmark": "C:\\temp\\file.txt", + }, + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Data.Count); + Assert.AreEqual("Google", result.Data[0].Name); + Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark); + } + + [TestMethod] + public void ParseBookmarks_JsonWithDifferentCasing_ParsesSuccessfully() + { + // Arrange - JSON with different property name casing (should be handled by PropertyNameCaseInsensitive option) + var json = """ + { + "data": [ + { + "name": "Google", + "bookmark": "https://www.google.com" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Data.Count); + Assert.AreEqual("Google", result.Data[0].Name); + Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark); + } + + [TestMethod] + public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString() + { + // Arrange + var bookmarks = new Bookmarks + { + Data = new List + { + new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" }, + new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" }, + }, + }; + + // Act + var result = _parser.SerializeBookmarks(bookmarks); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contains("Google")); + Assert.IsTrue(result.Contains("https://www.google.com")); + Assert.IsTrue(result.Contains("Local File")); + Assert.IsTrue(result.Contains("C:\\\\temp\\\\file.txt")); // Escaped backslashes in JSON + Assert.IsTrue(result.Contains("Data")); + } + + [TestMethod] + public void SerializeBookmarks_EmptyBookmarks_ReturnsValidJson() + { + // Arrange + var bookmarks = new Bookmarks(); + + // Act + var result = _parser.SerializeBookmarks(bookmarks); + + // Assert + Assert.IsNotNull(result); + Assert.IsTrue(result.Contains("Data")); + Assert.IsTrue(result.Contains("[]")); + } + + [TestMethod] + public void SerializeBookmarks_NullBookmarks_ReturnsEmptyString() + { + // Act + var result = _parser.SerializeBookmarks(null); + + // Assert + Assert.AreEqual(string.Empty, result); + } + + [TestMethod] + public void ParseBookmarks_RoundTripSerialization_PreservesData() + { + // Arrange + var originalBookmarks = new Bookmarks + { + Data = new List + { + new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" }, + new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" }, + new BookmarkData { Name = "Placeholder", Bookmark = "Open {file} in editor" }, + }, + }; + + // Act - Serialize then parse + var serializedJson = _parser.SerializeBookmarks(originalBookmarks); + var parsedBookmarks = _parser.ParseBookmarks(serializedJson); + + // Assert + Assert.IsNotNull(parsedBookmarks); + Assert.AreEqual(originalBookmarks.Data.Count, parsedBookmarks.Data.Count); + + for (var i = 0; i < originalBookmarks.Data.Count; i++) + { + Assert.AreEqual(originalBookmarks.Data[i].Name, parsedBookmarks.Data[i].Name); + Assert.AreEqual(originalBookmarks.Data[i].Bookmark, parsedBookmarks.Data[i].Bookmark); + Assert.AreEqual(originalBookmarks.Data[i].IsPlaceholder, parsedBookmarks.Data[i].IsPlaceholder); + } + } + + [TestMethod] + public void ParseBookmarks_JsonWithPlaceholderBookmarks_CorrectlyIdentifiesPlaceholders() + { + // Arrange + var json = """ + { + "Data": [ + { + "Name": "Regular URL", + "Bookmark": "https://www.google.com" + }, + { + "Name": "Placeholder Command", + "Bookmark": "notepad {file}" + }, + { + "Name": "Multiple Placeholders", + "Bookmark": "copy {source} {destination}" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(3, result.Data.Count); + + Assert.IsFalse(result.Data[0].IsPlaceholder); + Assert.IsTrue(result.Data[1].IsPlaceholder); + Assert.IsTrue(result.Data[2].IsPlaceholder); + } + + [TestMethod] + public void ParseBookmarks_IsWebUrl_CorrectlyIdentifiesWebUrls() + { + // Arrange + var json = """ + { + "Data": [ + { + "Name": "HTTPS Website", + "Bookmark": "https://www.google.com" + }, + { + "Name": "HTTP Website", + "Bookmark": "http://example.com" + }, + { + "Name": "Website without protocol", + "Bookmark": "www.github.com" + }, + { + "Name": "Local File Path", + "Bookmark": "C:\\Users\\test\\Documents\\file.txt" + }, + { + "Name": "Network Path", + "Bookmark": "\\\\server\\share\\file.txt" + }, + { + "Name": "Executable", + "Bookmark": "notepad.exe" + }, + { + "Name": "File URI", + "Bookmark": "file:///C:/temp/file.txt" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(7, result.Data.Count); + + // Web URLs should return true + Assert.IsTrue(result.Data[0].IsWebUrl(), "HTTPS URL should be identified as web URL"); + Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTP URL should be identified as web URL"); + + // This case will fail. We need to consider if we need to support pure domain value in bookmark. + // Assert.IsTrue(result.Data[2].IsWebUrl(), "Domain without protocol should be identified as web URL"); + + // Non-web URLs should return false + Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file path should not be identified as web URL"); + Assert.IsFalse(result.Data[4].IsWebUrl(), "Network path should not be identified as web URL"); + Assert.IsFalse(result.Data[5].IsWebUrl(), "Executable should not be identified as web URL"); + Assert.IsFalse(result.Data[6].IsWebUrl(), "File URI should not be identified as web URL"); + } + + [TestMethod] + public void ParseBookmarks_IsPlaceholder_CorrectlyIdentifiesPlaceholders() + { + // Arrange + var json = """ + { + "Data": [ + { + "Name": "Simple Placeholder", + "Bookmark": "notepad {file}" + }, + { + "Name": "Multiple Placeholders", + "Bookmark": "copy {source} to {destination}" + }, + { + "Name": "Web URL with Placeholder", + "Bookmark": "https://search.com?q={query}" + }, + { + "Name": "Complex Placeholder", + "Bookmark": "cmd /c echo {message} > {output_file}" + }, + { + "Name": "No Placeholder - Regular URL", + "Bookmark": "https://www.google.com" + }, + { + "Name": "No Placeholder - Local File", + "Bookmark": "C:\\temp\\file.txt" + }, + { + "Name": "False Positive - Only Opening Brace", + "Bookmark": "test { incomplete" + }, + { + "Name": "False Positive - Only Closing Brace", + "Bookmark": "test } incomplete" + }, + { + "Name": "Empty Placeholder", + "Bookmark": "command {}" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(9, result.Data.Count); + + // Should be identified as placeholders + Assert.IsTrue(result.Data[0].IsPlaceholder, "Simple placeholder should be identified"); + Assert.IsTrue(result.Data[1].IsPlaceholder, "Multiple placeholders should be identified"); + Assert.IsTrue(result.Data[2].IsPlaceholder, "Web URL with placeholder should be identified"); + Assert.IsTrue(result.Data[3].IsPlaceholder, "Complex placeholder should be identified"); + Assert.IsTrue(result.Data[8].IsPlaceholder, "Empty placeholder should be identified"); + + // Should NOT be identified as placeholders + Assert.IsFalse(result.Data[4].IsPlaceholder, "Regular URL should not be placeholder"); + Assert.IsFalse(result.Data[5].IsPlaceholder, "Local file should not be placeholder"); + Assert.IsFalse(result.Data[6].IsPlaceholder, "Only opening brace should not be placeholder"); + Assert.IsFalse(result.Data[7].IsPlaceholder, "Only closing brace should not be placeholder"); + } + + [TestMethod] + public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesBothWebUrlAndPlaceholder() + { + // Arrange + var json = """ + { + "Data": [ + { + "Name": "Web URL with Placeholder", + "Bookmark": "https://google.com/search?q={query}" + }, + { + "Name": "Web URL without Placeholder", + "Bookmark": "https://github.com" + }, + { + "Name": "Local File with Placeholder", + "Bookmark": "notepad {file}" + }, + { + "Name": "Local File without Placeholder", + "Bookmark": "C:\\Windows\\notepad.exe" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(4, result.Data.Count); + + // Web URL with placeholder + Assert.IsTrue(result.Data[0].IsWebUrl(), "Web URL with placeholder should be identified as web URL"); + Assert.IsTrue(result.Data[0].IsPlaceholder, "Web URL with placeholder should be identified as placeholder"); + + // Web URL without placeholder + Assert.IsTrue(result.Data[1].IsWebUrl(), "Web URL without placeholder should be identified as web URL"); + Assert.IsFalse(result.Data[1].IsPlaceholder, "Web URL without placeholder should not be identified as placeholder"); + + // Local file with placeholder + Assert.IsFalse(result.Data[2].IsWebUrl(), "Local file with placeholder should not be identified as web URL"); + Assert.IsTrue(result.Data[2].IsPlaceholder, "Local file with placeholder should be identified as placeholder"); + + // Local file without placeholder + Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file without placeholder should not be identified as web URL"); + Assert.IsFalse(result.Data[3].IsPlaceholder, "Local file without placeholder should not be identified as placeholder"); + } + + [TestMethod] + public void ParseBookmarks_EdgeCaseUrls_CorrectlyIdentifiesWebUrls() + { + // Arrange + var json = """ + { + "Data": [ + { + "Name": "FTP URL", + "Bookmark": "ftp://files.example.com" + }, + { + "Name": "HTTPS with port", + "Bookmark": "https://localhost:8080" + }, + { + "Name": "IP Address", + "Bookmark": "http://192.168.1.1" + }, + { + "Name": "Subdomain", + "Bookmark": "https://api.github.com" + }, + { + "Name": "Domain only", + "Bookmark": "example.com" + }, + { + "Name": "Not a URL - no dots", + "Bookmark": "localhost" + } + ] + } + """; + + // Act + var result = _parser.ParseBookmarks(json); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(6, result.Data.Count); + + Assert.IsFalse(result.Data[0].IsWebUrl(), "FTP URL should not be identified as web URL"); + Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTPS with port should be identified as web URL"); + Assert.IsTrue(result.Data[2].IsWebUrl(), "IP Address with HTTP should be identified as web URL"); + Assert.IsTrue(result.Data[3].IsWebUrl(), "Subdomain should be identified as web URL"); + + // This case will fail. We need to consider if we need to support pure domain value in bookmark. + // Assert.IsTrue(result.Data[4].IsWebUrl(), "Domain only should be identified as web URL"); + Assert.IsFalse(result.Data[5].IsWebUrl(), "Single word without dots should not be identified as web URL"); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarksCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarksCommandProviderTests.cs new file mode 100644 index 0000000000..52f50727a7 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarksCommandProviderTests.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CmdPal.Ext.Bookmarks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; + +[TestClass] +public class BookmarksCommandProviderTests +{ + [TestMethod] + public void ProviderHasCorrectId() + { + // Setup + var mockDataSource = new MockBookmarkDataSource(); + var provider = new BookmarksCommandProvider(mockDataSource); + + // Assert + Assert.AreEqual("Bookmarks", provider.Id); + } + + [TestMethod] + public void ProviderHasDisplayName() + { + // Setup + var mockDataSource = new MockBookmarkDataSource(); + var provider = new BookmarksCommandProvider(mockDataSource); + + // Assert + Assert.IsNotNull(provider.DisplayName); + Assert.IsTrue(provider.DisplayName.Length > 0); + } + + [TestMethod] + public void ProviderHasIcon() + { + // Setup + var provider = new BookmarksCommandProvider(); + + // Assert + Assert.IsNotNull(provider.Icon); + } + + [TestMethod] + public void TopLevelCommandsNotEmpty() + { + // Setup + var provider = new BookmarksCommandProvider(); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length > 0); + } + + [TestMethod] + public void ProviderWithMockData_LoadsBookmarksCorrectly() + { + // Arrange + var jsonData = @"{ + ""Data"": [ + { + ""Name"": ""Test Bookmark"", + ""Bookmark"": ""https://test.com"" + }, + { + ""Name"": ""Another Bookmark"", + ""Bookmark"": ""https://another.com"" + } + ] + }"; + + var dataSource = new MockBookmarkDataSource(jsonData); + var provider = new BookmarksCommandProvider(dataSource); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + + var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault(); + var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault(); + + // Should have three commands:Add + two custom bookmarks + Assert.AreEqual(3, commands.Length); + + Assert.IsNotNull(addCommand); + Assert.IsNotNull(testBookmark); + } + + [TestMethod] + public void ProviderWithEmptyData_HasOnlyAddCommand() + { + // Arrange + var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }"); + var provider = new BookmarksCommandProvider(dataSource); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + + // Only have Add command + Assert.AreEqual(1, commands.Length); + + var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault(); + Assert.IsNotNull(addCommand); + } + + [TestMethod] + public void ProviderWithInvalidData_HandlesGracefully() + { + // Arrange + var dataSource = new MockBookmarkDataSource("invalid json"); + var provider = new BookmarksCommandProvider(dataSource); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + + // Only have one command. Will ignore json parse error. + Assert.AreEqual(1, commands.Length); + + var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault(); + Assert.IsNotNull(addCommand); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj new file mode 100644 index 0000000000..07b6a9bfe5 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + + + false + true + Microsoft.CmdPal.Ext.Bookmarks.UnitTests + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\ + false + false + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs new file mode 100644 index 0000000000..ae3732559c --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; + +internal sealed class MockBookmarkDataSource : IBookmarkDataSource +{ + private string _jsonData; + + public MockBookmarkDataSource(string initialJsonData = "[]") + { + _jsonData = initialJsonData; + } + + public string GetBookmarkData() + { + return _jsonData; + } + + public void SaveBookmarkData(string jsonData) + { + _jsonData = jsonData; + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/QueryTests.cs new file mode 100644 index 0000000000..767460fa27 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/QueryTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CmdPal.Ext.UnitTestBase; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; + +[TestClass] +public class QueryTests : CommandPaletteUnitTestBase +{ + [TestMethod] + public void ValidateBookmarksCreation() + { + // Setup + var bookmarks = Settings.CreateDefaultBookmarks(); + + // Assert + Assert.IsNotNull(bookmarks); + Assert.IsNotNull(bookmarks.Data); + Assert.AreEqual(2, bookmarks.Data.Count); + } + + [TestMethod] + public void ValidateBookmarkData() + { + // Setup + var bookmarks = Settings.CreateDefaultBookmarks(); + + // Act + var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft"); + var githubBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "GitHub"); + + // Assert + Assert.IsNotNull(microsoftBookmark); + Assert.AreEqual("https://www.microsoft.com", microsoftBookmark.Bookmark); + + Assert.IsNotNull(githubBookmark); + Assert.AreEqual("https://github.com", githubBookmark.Bookmark); + } + + [TestMethod] + public void ValidateWebUrlDetection() + { + // Setup + var bookmarks = Settings.CreateDefaultBookmarks(); + var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft"); + + // Assert + Assert.IsNotNull(microsoftBookmark); + Assert.IsTrue(microsoftBookmark.IsWebUrl()); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Settings.cs new file mode 100644 index 0000000000..82d7cd1cad --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/Settings.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; + +public static class Settings +{ + public static Bookmarks CreateDefaultBookmarks() + { + var bookmarks = new Bookmarks(); + + // Add some test bookmarks + bookmarks.Data.Add(new BookmarkData + { + Name = "Microsoft", + Bookmark = "https://www.microsoft.com", + }); + + bookmarks.Data.Add(new BookmarkData + { + Name = "GitHub", + Bookmark = "https://github.com", + }); + + return bookmarks; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index 3dadba9749..d6e9693a69 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -19,16 +19,23 @@ public partial class AllAppsCommandProvider : CommandProvider public static readonly AllAppsPage Page = new(); + private readonly AllAppsPage _page; private readonly CommandItem _listItem; public AllAppsCommandProvider() + : this(Page) { + } + + public AllAppsCommandProvider(AllAppsPage page) + { + _page = page ?? throw new ArgumentNullException(nameof(page)); Id = WellKnownId; DisplayName = Resources.installed_apps; Icon = Icons.AllAppsIcon; Settings = AllAppsSettings.Instance.Settings; - _listItem = new(Page) + _listItem = new(_page) { Subtitle = Resources.search_installed_apps, MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], @@ -38,11 +45,11 @@ public partial class AllAppsCommandProvider : CommandProvider PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; } - public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()]; + public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()]; public ICommandItem? LookupApp(string displayName) { - var items = Page.GetItems(); + var items = _page.GetItems(); // We're going to do this search in two directions: // First, is this name a substring of any app... diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs index 35cac8b01c..68b77ce728 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -19,13 +20,20 @@ namespace Microsoft.CmdPal.Ext.Apps; public sealed partial class AllAppsPage : ListPage { private readonly Lock _listLock = new(); + private readonly IAppCache _appCache; private AppItem[] allApps = []; private AppListItem[] unpinnedApps = []; private AppListItem[] pinnedApps = []; public AllAppsPage() + : this(AppCache.Instance.Value) { + } + + public AllAppsPage(IAppCache appCache) + { + _appCache = appCache ?? throw new ArgumentNullException(nameof(appCache)); this.Name = Resources.all_apps; this.Icon = Icons.AllAppsIcon; this.ShowDetails = true; @@ -59,7 +67,7 @@ public sealed partial class AllAppsPage : ListPage private void BuildListItems() { - if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload()) + if (allApps.Length == 0 || _appCache.ShouldReload()) { lock (_listLock) { @@ -75,7 +83,7 @@ public sealed partial class AllAppsPage : ListPage this.IsLoading = false; - AppCache.Instance.Value.ResetReloadFlag(); + _appCache.ResetReloadFlag(); stopwatch.Stop(); Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms"); @@ -85,11 +93,11 @@ public sealed partial class AllAppsPage : ListPage private AppItem[] GetAllApps() { - var uwpResults = AppCache.Instance.Value.UWPs + var uwpResults = _appCache.UWPs .Where((application) => application.Enabled) .Select(app => app.ToAppItem()); - var win32Results = AppCache.Instance.Value.Win32s + var win32Results = _appCache.Win32s .Where((application) => application.Enabled && application.Valid) .Select(app => app.ToAppItem()); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs index d585f3cd6c..bc49611d45 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsSettings.cs @@ -5,13 +5,14 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Apps; -public class AllAppsSettings : JsonSettingsManager +public class AllAppsSettings : JsonSettingsManager, ISettingsInterface { private static readonly string _namespace = "apps"; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCache.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCache.cs index 746bfdfe9d..48beaec1ff 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCache.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppCache.cs @@ -12,7 +12,7 @@ using Microsoft.CmdPal.Ext.Apps.Utils; namespace Microsoft.CmdPal.Ext.Apps; -public sealed partial class AppCache : IDisposable +public sealed partial class AppCache : IAppCache, IDisposable { private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper; @@ -24,7 +24,7 @@ public sealed partial class AppCache : IDisposable public IList Win32s => _win32ProgramRepository.Items; - public IList UWPs => _packageRepository.Items; + public IList UWPs => _packageRepository.Items; public static readonly Lazy Instance = new(() => new()); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/ISettingsInterface.cs new file mode 100644 index 0000000000..b6328f3c10 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/ISettingsInterface.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.CmdPal.Ext.Apps.Helpers; + +public interface ISettingsInterface +{ + public bool EnableStartMenuSource { get; } + + public bool EnableDesktopSource { get; } + + public bool EnableRegistrySource { get; } + + public bool EnablePathEnvironmentVariableSource { get; } + + public List ProgramSuffixes { get; } + + public List RunCommandSuffixes { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/IAppCache.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/IAppCache.cs new file mode 100644 index 0000000000..0a84230a88 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/IAppCache.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Programs; + +namespace Microsoft.CmdPal.Ext.Apps; + +/// +/// Interface for application cache that provides access to Win32 and UWP applications. +/// +public interface IAppCache : IDisposable +{ + /// + /// Gets the collection of Win32 programs. + /// + IList Win32s { get; } + + /// + /// Gets the collection of UWP applications. + /// + IList UWPs { get; } + + /// + /// Determines whether the cache should be reloaded. + /// + /// True if cache should be reloaded, false otherwise. + bool ShouldReload(); + + /// + /// Resets the reload flag. + /// + void ResetReloadFlag(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IUWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IUWPApplication.cs new file mode 100644 index 0000000000..775bcaab4a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/IUWPApplication.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps.Programs; + +/// +/// Interface for UWP applications to enable testing and mocking +/// +public interface IUWPApplication : IProgram +{ + string AppListEntry { get; set; } + + string DisplayName { get; set; } + + string UserModelId { get; set; } + + string BackgroundColor { get; set; } + + string EntryPoint { get; set; } + + bool CanRunElevated { get; set; } + + string LogoPath { get; set; } + + LogoType LogoType { get; set; } + + UWP Package { get; set; } + + string LocationLocalized { get; } + + string GetAppIdentifier(); + + List GetCommands(); + + void UpdateLogoPath(Utils.Theme theme); + + AppItem ToAppItem(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs index 484dc162ee..23b428447b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs @@ -22,7 +22,7 @@ using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme; namespace Microsoft.CmdPal.Ext.Apps.Programs; [Serializable] -public class UWPApplication : IProgram +public class UWPApplication : IUWPApplication { private static readonly IFileSystem FileSystem = new FileSystem(); private static readonly IPath Path = FileSystem.Path; @@ -517,7 +517,7 @@ public class UWPApplication : IProgram } } - internal AppItem ToAppItem() + public AppItem ToAppItem() { var app = this; var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b0c7ecb93e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Apps.UnitTests")] diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs index 3a12958f1e..2c53a649b9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Storage/PackageRepository.cs @@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Storage; /// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps. /// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly /// -internal sealed partial class PackageRepository : ListRepository, IProgramRepository +internal sealed partial class PackageRepository : ListRepository, IProgramRepository { private readonly IPackageCatalog _packageCatalog; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs new file mode 100644 index 0000000000..7cc82c9c02 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkJsonParser.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public class BookmarkJsonParser +{ + public BookmarkJsonParser() + { + } + + public Bookmarks ParseBookmarks(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return new Bookmarks(); + } + + try + { + var bookmarks = JsonSerializer.Deserialize(json, BookmarkSerializationContext.Default.Bookmarks); + return bookmarks ?? new Bookmarks(); + } + catch (JsonException ex) + { + ExtensionHost.LogMessage($"parse bookmark data failed. ex: {ex.Message}"); + return new Bookmarks(); + } + } + + public string SerializeBookmarks(Bookmarks? bookmarks) + { + if (bookmarks == null) + { + return string.Empty; + } + + return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.Bookmarks); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs index 8f2e257782..b02eb54e0f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs @@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks; public sealed class Bookmarks { public List Data { get; set; } = []; - - private static readonly JsonSerializerOptions _jsonOptions = new() - { - IncludeFields = true, - }; - - public static Bookmarks ReadFromFile(string path) - { - var data = new Bookmarks(); - - // if the file exists, load it and append the new item - if (File.Exists(path)) - { - var jsonStringReading = File.ReadAllText(path); - - if (!string.IsNullOrEmpty(jsonStringReading)) - { - data = JsonSerializer.Deserialize(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks(); - } - } - - return data; - } - - public static void WriteToFile(string path, Bookmarks data) - { - var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks); - - File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString); - } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs index 081fb2bccb..1174685729 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider private readonly AddBookmarkPage _addNewCommand = new(null); + private readonly IBookmarkDataSource _dataSource; + private readonly BookmarkJsonParser _parser; private Bookmarks? _bookmarks; public BookmarksCommandProvider() + : this(new FileBookmarkDataSource(StateJsonPath())) { + } + + internal BookmarksCommandProvider(IBookmarkDataSource dataSource) + { + _dataSource = dataSource; + _parser = new BookmarkJsonParser(); + Id = "Bookmarks"; DisplayName = Resources.bookmarks_display_name; Icon = Icons.PinIcon; @@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider private void SaveAndUpdateCommands() { - if (_bookmarks is not null) + try { - var jsonPath = BookmarksCommandProvider.StateJsonPath(); - Bookmarks.WriteToFile(jsonPath, _bookmarks); + var jsonData = _parser.SerializeBookmarks(_bookmarks); + _dataSource.SaveBookmarkData(jsonData); + } + catch (Exception ex) + { + Logger.LogError($"Failed to save bookmarks: {ex.Message}"); } LoadCommands(); @@ -82,11 +96,8 @@ public partial class BookmarksCommandProvider : CommandProvider { try { - var jsonFile = StateJsonPath(); - if (File.Exists(jsonFile)) - { - _bookmarks = Bookmarks.ReadFromFile(jsonFile); - } + var jsonData = _dataSource.GetBookmarkData(); + _bookmarks = _parser.ParseBookmarks(jsonData); } catch (Exception ex) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs new file mode 100644 index 0000000000..a87859c3ce --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/FileBookmarkDataSource.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public class FileBookmarkDataSource : IBookmarkDataSource +{ + private readonly string _filePath; + + public FileBookmarkDataSource(string filePath) + { + _filePath = filePath; + } + + public string GetBookmarkData() + { + if (!File.Exists(_filePath)) + { + return string.Empty; + } + + try + { + return File.ReadAllText(_filePath); + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Read bookmark data failed. ex: {ex.Message}"); + return string.Empty; + } + } + + public void SaveBookmarkData(string jsonData) + { + try + { + File.WriteAllText(_filePath, jsonData); + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Failed to save bookmark data: {ex}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs new file mode 100644 index 0000000000..7ed936a1c7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/IBookmarkDataSource.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +namespace Microsoft.CmdPal.Ext.Bookmarks; + +public interface IBookmarkDataSource +{ + string GetBookmarkData(); + + void SaveBookmarkData(string jsonData); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a74d97eeca --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Bookmarks.UnitTests")]