[CmdPal][UnitTests] Add/Migrate unit test for Apps and Bookmarks extension (#41238)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
1. Create Apps and Bookmarks ut project.
2. Refactor Apps and Bookmarks. And some interface in these extensions
to add a abstraction layer for testing purpose.
New interface list:
* ISettingsInterface
* IUWPApplication
* IAppCache
* IBookmarkDataSource
3. Add/Migrate some test case for Apps and Bookmarks extension

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41239 #41240
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Yu Leng 2025-08-20 16:25:46 +08:00 committed by GitHub
parent 75526b9580
commit 3bc746d0ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1927 additions and 51 deletions

View File

@ -771,6 +771,7 @@ istep
ith ith
ITHUMBNAIL ITHUMBNAIL
IUI IUI
IUWP
IWIC IWIC
jfif jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
@ -1646,6 +1647,7 @@ STYLECHANGED
STYLECHANGING STYLECHANGING
subkeys subkeys
sublang sublang
Subdomain
SUBMODULEUPDATE SUBMODULEUPDATE
subresource subresource
Superbar Superbar

View File

@ -788,6 +788,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject 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}" 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 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64 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|ARM64.Build.0 = Release|ARM64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -3161,6 +3181,8 @@ Global
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {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} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@ -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
}
}

View File

@ -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<ArgumentNullException>(() => 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);
}
}

View File

@ -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;
/// <summary>
/// Base class for Apps unit tests that provides common setup and teardown functionality.
/// </summary>
public abstract class AppsTestBase
{
/// <summary>
/// Gets the mock application cache used in tests.
/// </summary>
protected MockAppCache MockCache { get; private set; } = null!;
/// <summary>
/// Gets the AllAppsPage instance used in tests.
/// </summary>
protected AllAppsPage Page { get; private set; } = null!;
/// <summary>
/// Sets up the test environment before each test method.
/// </summary>
/// <returns>A task representing the asynchronous setup operation.</returns>
[TestInitialize]
public virtual async Task Setup()
{
MockCache = new MockAppCache();
Page = new AllAppsPage(MockCache);
// Ensure initialization is complete
await MockCache.RefreshAsync();
}
/// <summary>
/// Cleans up the test environment after each test method.
/// </summary>
[TestCleanup]
public virtual void Cleanup()
{
MockCache?.Dispose();
}
/// <summary>
/// Forces synchronous initialization of the page for testing.
/// </summary>
protected void EnsurePageInitialized()
{
// Trigger BuildListItems by accessing items
_ = Page.GetItems();
}
/// <summary>
/// Waits for page initialization with timeout.
/// </summary>
/// <param name="timeoutMs">The timeout in milliseconds.</param>
/// <returns>A task representing the asynchronous wait operation.</returns>
protected async Task WaitForPageInitializationAsync(int timeoutMs = 1000)
{
await MockCache.RefreshAsync();
EnsurePageInitialized();
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Apps.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@ -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;
/// <summary>
/// Mock implementation of IAppCache for unit testing.
/// </summary>
public class MockAppCache : IAppCache
{
private readonly List<Win32Program> _win32s = new();
private readonly List<IUWPApplication> _uwps = new();
private bool _disposed;
private bool _shouldReload;
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
public IList<Win32Program> Win32s => _win32s.AsReadOnly();
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
public IList<IUWPApplication> UWPs => _uwps.AsReadOnly();
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
public bool ShouldReload() => _shouldReload;
/// <summary>
/// Resets the reload flag.
/// </summary>
public void ResetReloadFlag() => _shouldReload = false;
/// <summary>
/// Asynchronously refreshes the cache.
/// </summary>
/// <returns>A task representing the asynchronous refresh operation.</returns>
public async Task RefreshAsync()
{
// Simulate minimal async operation for testing
await Task.Delay(1);
}
/// <summary>
/// Adds a Win32 program to the cache.
/// </summary>
/// <param name="program">The Win32 program to add.</param>
/// <exception cref="ArgumentNullException">Thrown when program is null.</exception>
public void AddWin32Program(Win32Program program)
{
ArgumentNullException.ThrowIfNull(program);
_win32s.Add(program);
}
/// <summary>
/// Adds a UWP application to the cache.
/// </summary>
/// <param name="app">The UWP application to add.</param>
/// <exception cref="ArgumentNullException">Thrown when app is null.</exception>
public void AddUWPApplication(IUWPApplication app)
{
ArgumentNullException.ThrowIfNull(app);
_uwps.Add(app);
}
/// <summary>
/// Clears all applications from the cache.
/// </summary>
public void ClearAll()
{
_win32s.Clear();
_uwps.Clear();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Clean up managed resources
_win32s.Clear();
_uwps.Clear();
}
_disposed = true;
}
}
}

View File

@ -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;
/// <summary>
/// Mock implementation of IUWPApplication for unit testing.
/// </summary>
public class MockUWPApplication : IUWPApplication
{
/// <summary>
/// Gets or sets the app list entry.
/// </summary>
public string AppListEntry { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the unique identifier.
/// </summary>
public string UniqueIdentifier { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the display name.
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the user model ID.
/// </summary>
public string UserModelId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the background color.
/// </summary>
public string BackgroundColor { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the entry point.
/// </summary>
public string EntryPoint { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the application is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the application can run elevated.
/// </summary>
public bool CanRunElevated { get; set; }
/// <summary>
/// Gets or sets the logo path.
/// </summary>
public string LogoPath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the logo type.
/// </summary>
public LogoType LogoType { get; set; } = LogoType.Colored;
/// <summary>
/// Gets or sets the UWP package.
/// </summary>
public UWP Package { get; set; } = null!;
/// <summary>
/// Gets the name of the application.
/// </summary>
public string Name => DisplayName;
/// <summary>
/// Gets the location of the application.
/// </summary>
public string Location => Package?.Location ?? string.Empty;
/// <summary>
/// Gets the localized location of the application.
/// </summary>
public string LocationLocalized => Package?.LocationLocalized ?? string.Empty;
/// <summary>
/// Gets the application identifier.
/// </summary>
/// <returns>The user model ID of the application.</returns>
public string GetAppIdentifier()
{
return UserModelId;
}
/// <summary>
/// Gets the commands available for this application.
/// </summary>
/// <returns>A list of context items.</returns>
public List<IContextItem> GetCommands()
{
return new List<IContextItem>();
}
/// <summary>
/// Updates the logo path based on the specified theme.
/// </summary>
/// <param name="theme">The theme to use for the logo.</param>
public void UpdateLogoPath(Theme theme)
{
// Mock implementation - no-op for testing
}
/// <summary>
/// Converts this UWP application to an AppItem.
/// </summary>
/// <returns>An AppItem representation of this UWP application.</returns>
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(),
};
}
}

View File

@ -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);
}
}

View File

@ -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<string> programSuffixes;
private readonly List<string> runCommandSuffixes;
public Settings(
bool enableStartMenuSource = true,
bool enableDesktopSource = true,
bool enableRegistrySource = true,
bool enablePathEnvironmentVariableSource = true,
List<string> programSuffixes = null,
List<string> runCommandSuffixes = null)
{
this.enableStartMenuSource = enableStartMenuSource;
this.enableDesktopSource = enableDesktopSource;
this.enableRegistrySource = enableRegistrySource;
this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
this.programSuffixes = programSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url" };
this.runCommandSuffixes = runCommandSuffixes ?? new List<string> { "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<string> ProgramSuffixes => programSuffixes;
public List<string> 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<string> { "exe", "bat" },
runCommandSuffixes: new List<string> { "exe", "bat", "cmd" });
}

View File

@ -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;
/// <summary>
/// Helper class to create test data for unit tests.
/// </summary>
public static class TestDataHelper
{
/// <summary>
/// Creates a test Win32 program with the specified parameters.
/// </summary>
/// <param name="name">The name of the application.</param>
/// <param name="fullPath">The full path to the application executable.</param>
/// <param name="enabled">A value indicating whether the application is enabled.</param>
/// <param name="valid">A value indicating whether the application is valid.</param>
/// <returns>A new Win32Program instance with the specified parameters.</returns>
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,
};
}
/// <summary>
/// Creates a test UWP application with the specified parameters.
/// </summary>
/// <param name="displayName">The display name of the application.</param>
/// <param name="userModelId">The user model ID of the application.</param>
/// <param name="enabled">A value indicating whether the application is enabled.</param>
/// <returns>A new IUWPApplication instance with the specified parameters.</returns>
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),
};
}
/// <summary>
/// Creates a mock UWP package for testing purposes.
/// </summary>
/// <param name="displayName">The display name of the package.</param>
/// <param name="userModelId">The user model ID of the package.</param>
/// <returns>A new UWP package instance.</returns>
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,
};
}
/// <summary>
/// Mock implementation of IPackage for testing purposes.
/// </summary>
private sealed class MockPackage : IPackage
{
/// <summary>
/// Gets or sets the name of the package.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the full name of the package.
/// </summary>
public string FullName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the family name of the package.
/// </summary>
public string FamilyName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the package is a framework package.
/// </summary>
public bool IsFramework { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the package is in development mode.
/// </summary>
public bool IsDevelopmentMode { get; set; }
/// <summary>
/// Gets or sets the installed location of the package.
/// </summary>
public string InstalledLocation { get; set; } = string.Empty;
}
}

View File

@ -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);
}
}

View File

@ -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<BookmarkData>
{
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<BookmarkData>
{
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");
}
}

View File

@ -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 commandsAdd + 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);
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -19,16 +19,23 @@ public partial class AllAppsCommandProvider : CommandProvider
public static readonly AllAppsPage Page = new(); public static readonly AllAppsPage Page = new();
private readonly AllAppsPage _page;
private readonly CommandItem _listItem; private readonly CommandItem _listItem;
public AllAppsCommandProvider() public AllAppsCommandProvider()
: this(Page)
{ {
}
public AllAppsCommandProvider(AllAppsPage page)
{
_page = page ?? throw new ArgumentNullException(nameof(page));
Id = WellKnownId; Id = WellKnownId;
DisplayName = Resources.installed_apps; DisplayName = Resources.installed_apps;
Icon = Icons.AllAppsIcon; Icon = Icons.AllAppsIcon;
Settings = AllAppsSettings.Instance.Settings; Settings = AllAppsSettings.Instance.Settings;
_listItem = new(Page) _listItem = new(_page)
{ {
Subtitle = Resources.search_installed_apps, Subtitle = Resources.search_installed_apps,
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
@ -38,11 +45,11 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
} }
public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()]; public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName) public ICommandItem? LookupApp(string displayName)
{ {
var items = Page.GetItems(); var items = _page.GetItems();
// We're going to do this search in two directions: // We're going to do this search in two directions:
// First, is this name a substring of any app... // First, is this name a substring of any app...

View File

@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -19,13 +20,20 @@ namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AllAppsPage : ListPage public sealed partial class AllAppsPage : ListPage
{ {
private readonly Lock _listLock = new(); private readonly Lock _listLock = new();
private readonly IAppCache _appCache;
private AppItem[] allApps = []; private AppItem[] allApps = [];
private AppListItem[] unpinnedApps = []; private AppListItem[] unpinnedApps = [];
private AppListItem[] pinnedApps = []; private AppListItem[] pinnedApps = [];
public AllAppsPage() public AllAppsPage()
: this(AppCache.Instance.Value)
{ {
}
public AllAppsPage(IAppCache appCache)
{
_appCache = appCache ?? throw new ArgumentNullException(nameof(appCache));
this.Name = Resources.all_apps; this.Name = Resources.all_apps;
this.Icon = Icons.AllAppsIcon; this.Icon = Icons.AllAppsIcon;
this.ShowDetails = true; this.ShowDetails = true;
@ -59,7 +67,7 @@ public sealed partial class AllAppsPage : ListPage
private void BuildListItems() private void BuildListItems()
{ {
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload()) if (allApps.Length == 0 || _appCache.ShouldReload())
{ {
lock (_listLock) lock (_listLock)
{ {
@ -75,7 +83,7 @@ public sealed partial class AllAppsPage : ListPage
this.IsLoading = false; this.IsLoading = false;
AppCache.Instance.Value.ResetReloadFlag(); _appCache.ResetReloadFlag();
stopwatch.Stop(); stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms"); Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
@ -85,11 +93,11 @@ public sealed partial class AllAppsPage : ListPage
private AppItem[] GetAllApps() private AppItem[] GetAllApps()
{ {
var uwpResults = AppCache.Instance.Value.UWPs var uwpResults = _appCache.UWPs
.Where((application) => application.Enabled) .Where((application) => application.Enabled)
.Select(app => app.ToAppItem()); .Select(app => app.ToAppItem());
var win32Results = AppCache.Instance.Value.Win32s var win32Results = _appCache.Win32s
.Where((application) => application.Enabled && application.Valid) .Where((application) => application.Enabled && application.Valid)
.Select(app => app.ToAppItem()); .Select(app => app.ToAppItem());

View File

@ -5,13 +5,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps; namespace Microsoft.CmdPal.Ext.Apps;
public class AllAppsSettings : JsonSettingsManager public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
{ {
private static readonly string _namespace = "apps"; private static readonly string _namespace = "apps";

View File

@ -12,7 +12,7 @@ using Microsoft.CmdPal.Ext.Apps.Utils;
namespace Microsoft.CmdPal.Ext.Apps; namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AppCache : IDisposable public sealed partial class AppCache : IAppCache, IDisposable
{ {
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper; private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;
@ -24,7 +24,7 @@ public sealed partial class AppCache : IDisposable
public IList<Win32Program> Win32s => _win32ProgramRepository.Items; public IList<Win32Program> Win32s => _win32ProgramRepository.Items;
public IList<UWPApplication> UWPs => _packageRepository.Items; public IList<IUWPApplication> UWPs => _packageRepository.Items;
public static readonly Lazy<AppCache> Instance = new(() => new()); public static readonly Lazy<AppCache> Instance = new(() => new());

View File

@ -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<string> ProgramSuffixes { get; }
public List<string> RunCommandSuffixes { get; }
}

View File

@ -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;
/// <summary>
/// Interface for application cache that provides access to Win32 and UWP applications.
/// </summary>
public interface IAppCache : IDisposable
{
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
IList<Win32Program> Win32s { get; }
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
IList<IUWPApplication> UWPs { get; }
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
bool ShouldReload();
/// <summary>
/// Resets the reload flag.
/// </summary>
void ResetReloadFlag();
}

View File

@ -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;
/// <summary>
/// Interface for UWP applications to enable testing and mocking
/// </summary>
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<IContextItem> GetCommands();
void UpdateLogoPath(Utils.Theme theme);
AppItem ToAppItem();
}

View File

@ -22,7 +22,7 @@ using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme;
namespace Microsoft.CmdPal.Ext.Apps.Programs; namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable] [Serializable]
public class UWPApplication : IProgram public class UWPApplication : IUWPApplication
{ {
private static readonly IFileSystem FileSystem = new FileSystem(); private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path; 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 app = this;
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty; var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;

View File

@ -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")]

View File

@ -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. /// 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 /// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly
/// </summary> /// </summary>
internal sealed partial class PackageRepository : ListRepository<UWPApplication>, IProgramRepository internal sealed partial class PackageRepository : ListRepository<IUWPApplication>, IProgramRepository
{ {
private readonly IPackageCatalog _packageCatalog; private readonly IPackageCatalog _packageCatalog;

View File

@ -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<Bookmarks>(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);
}
}

View File

@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks public sealed class Bookmarks
{ {
public List<BookmarkData> Data { get; set; } = []; public List<BookmarkData> 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<Bookmarks>(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);
}
} }

View File

@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider
private readonly AddBookmarkPage _addNewCommand = new(null); private readonly AddBookmarkPage _addNewCommand = new(null);
private readonly IBookmarkDataSource _dataSource;
private readonly BookmarkJsonParser _parser;
private Bookmarks? _bookmarks; private Bookmarks? _bookmarks;
public BookmarksCommandProvider() public BookmarksCommandProvider()
: this(new FileBookmarkDataSource(StateJsonPath()))
{ {
}
internal BookmarksCommandProvider(IBookmarkDataSource dataSource)
{
_dataSource = dataSource;
_parser = new BookmarkJsonParser();
Id = "Bookmarks"; Id = "Bookmarks";
DisplayName = Resources.bookmarks_display_name; DisplayName = Resources.bookmarks_display_name;
Icon = Icons.PinIcon; Icon = Icons.PinIcon;
@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider
private void SaveAndUpdateCommands() private void SaveAndUpdateCommands()
{ {
if (_bookmarks is not null) try
{ {
var jsonPath = BookmarksCommandProvider.StateJsonPath(); var jsonData = _parser.SerializeBookmarks(_bookmarks);
Bookmarks.WriteToFile(jsonPath, _bookmarks); _dataSource.SaveBookmarkData(jsonData);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
} }
LoadCommands(); LoadCommands();
@ -82,11 +96,8 @@ public partial class BookmarksCommandProvider : CommandProvider
{ {
try try
{ {
var jsonFile = StateJsonPath(); var jsonData = _dataSource.GetBookmarkData();
if (File.Exists(jsonFile)) _bookmarks = _parser.ParseBookmarks(jsonData);
{
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -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}");
}
}
}

View File

@ -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);
}

View File

@ -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")]