Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="$(MicrosoftWindowsCsWinRTPackageVersion)" />
<PackageVersion Include="Microsoft.SourceLink.Common" Version="$(MicrosoftSourceLinkCommonVersion)" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="$(MicrosoftSourceLinkGitHubVersion)" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.2.2" />
<PackageVersion Include="MSTest.TestFramework" Version="3.2.2" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Microsoft.VisualStudio.SDK" Version="17.3.32804.24" />
<PackageVersion Include="Microsoft.VisualStudio.Threading" Version="17.13.2" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
<PackageVersion Include="NuGet.VisualStudio" Version="17.6.1" />
<PackageVersion Include="Microsoft.VisualStudio.TemplateWizardInterface" Version="17.5.33428.366" />
Expand Down
147 changes: 147 additions & 0 deletions dev/VSIX/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Windows App SDK Visual Studio Extensions

This directory contains the Visual Studio extensions (VSIX) for Windows App SDK, including project templates, item templates, and project wizards for creating WinUI 3 applications.

## Structure

```
dev/VSIX/
├── Extension/ # VSIX extension packages
│ ├── Cs/ # C# extension
│ │ ├── Common/ # Localized VSPackage resource files
│ │ └── Dev17/ # VS 2022 extension project & manifests
│ └── Cpp/ # C++ extension
│ ├── Common/ # Localized VSPackage resource files
│ └── Dev17/ # VS 2022 extension project, manifests & NuGetPackageList.cs
├── ProjectTemplates/ # Visual Studio project templates
│ ├── Desktop/ # Desktop app templates
│ │ ├── CSharp/ # SingleProjectPackagedApp, PackagedApp, ClassLibrary, UnitTestApp
│ │ └── CppWinRT/ # SingleProjectPackagedApp, PackagedApp, UnitTestApp
│ └── Neutral/ # Platform-neutral templates
│ └── CppWinRT/ # RuntimeComponent
├── ItemTemplates/ # Visual Studio item templates
│ ├── Desktop/ # Desktop-specific items
│ │ ├── CSharp/ # BlankWindow
│ │ └── CppWinRT/ # BlankWindow
│ └── Neutral/ # Platform-neutral items
│ ├── CSharp/ # BlankPage, UserControl, TemplatedControl, ResourceDictionary, Resw
│ └── CppWinRT/ # BlankPage, UserControl, TemplatedControl, ResourceDictionary, Resw
├── Shared/ # Shared code used by wizards and extensions
│ ├── WizardImplementation.cs # Main wizard logic (NuGetPackageInstaller)
│ ├── WizardInfoBarEvents.cs # InfoBar event handlers (NuGetInfoBarUIEvents)
│ └── OutputWindowHelper.cs # Output window utilities
└── Tests/ # Unit tests for VSIX components
└── WindowsAppSDK.VSIX.UnitTests/
```

## Testing

### Unit Tests

The `Tests/WindowsAppSDK.VSIX.UnitTests` project contains unit tests for the VSIX wizard and UI components, targeting `net8.0-windows10.0.19041.0`.

#### Test Classes

1. **NuGetPackageInstallerTests** (20 tests) — Package parsing, `ProjectFinishedGenerating` behavior, NuGet installation failures, happy-path installation, `ShouldAddProjectItem`, and template-specific package count verification.

2. **WizardInfoBarEventsTests** (9 tests) — Null parameter handling, `OnClosed`, constructor storage, `SeeErrorDetails` / `ManageNuGetPackages` hyperlink routing, unknown action context, and non-hyperlink action items.

3. **ErrorMessageTests** (8 tests) — `CreateErrorMessage` for InfoBar and MessageBox formats (single/multiple packages, null project fallback, format differences) and `CreateDetailedErrorMessage` content/structure.

#### Running Tests

```powershell
# Run all VSIX unit tests
dotnet test dev\VSIX\Tests\WindowsAppSDK.VSIX.UnitTests\WindowsAppSDK.VSIX.UnitTests.csproj

# Run a specific test class
dotnet test dev\VSIX\Tests\WindowsAppSDK.VSIX.UnitTests\WindowsAppSDK.VSIX.UnitTests.csproj --filter "FullyQualifiedName~NuGetPackageInstallerTests"
```

### Test Infrastructure

The tests use:
- **MSTest** — Test framework
- **Moq** — Mocking framework for VS SDK interfaces
- **VsTestBase** — Abstract base class that pins culture to `en-US` and configures `ThreadHelper` via reflection so `ThrowIfNotOnUIThread()` passes in tests

Key helper classes:
- `VsTestBase` — Configures `JoinableTaskContext` with the test thread as the main thread
- `MockServiceSetup` — Creates mock `IComponentModel`, `EnvDTE.Project` instances (C# and C++), and provides reflection helpers (`SetPrivateField`, `GetPrivateField`, `InvokePrivateMethod`)
- `TemplateTestData` — Defines NuGet package lists and `TemplateInfo` metadata for all shipped project templates

### Shared Source Linking

The unit test project compiles the same shared wizard source files used by the extension projects (`WizardImplementation.cs`, `WizardInfoBarEvents.cs`, `OutputWindowHelper.cs`) with the `CSHARP_EXTENSION` constant defined. It also links the C# extension's `VSPackage.resx` and `VSPackage.Designer.cs` for resource string access.

## Building

The VSIX extensions require the Visual Studio SDK (VSSDK) build targets and must be
built with Visual Studio or `msbuild` from a VS Developer Command Prompt.
**`dotnet build` is not supported** — the template and extension projects import
`$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets`, which is only available when the
VS SDK workload is installed. Building with `dotnet` CLI will produce MSB4019 errors
because `$(VSToolsPath)` resolves to the .NET SDK directory, which does not contain
the VSSDK targets.

```powershell
# Build from repository root (uses msbuild)
.\BuildAll.ps1

# Or build the VSIX solution directly from a VS Developer Command Prompt
msbuild dev\VSIX\WindowsAppSDK.Extension.sln
```

> **Note:** Unit tests can still be run with `dotnet test` — the test project does
> not depend on VSSDK targets. See the [Tests README](Tests/README.md) for details.

## Key Components

### Wizard Implementation

`Shared/WizardImplementation.cs` implements the `NuGetPackageInstaller` VS project wizard (`IWizard`) that:
- Parses the `$NuGetPackages$` replacement parameter to determine required packages
- Detects project type (C++ via `SolutionVCProjectGuid` vs C#)
- For C++ projects, installs NuGet packages immediately in `ProjectFinishedGenerating`
- For C# projects, defers installation until `SolutionRestoreFinished`
- Constructs error messages (`CreateErrorMessage`, `CreateDetailedErrorMessage`) and displays them via InfoBar or MessageBox

### InfoBar Events

`Shared/WizardInfoBarEvents.cs` implements `NuGetInfoBarUIEvents` (`IVsInfoBarUIEvents`) to handle user interactions with InfoBar hyperlinks:
- **ManageNuGetPackages** — Opens NuGet Package Manager via DTE command `{25fd982b-8cae-4cbd-a440-e03ffccde106}`, ID `0x100`
- **SeeErrorDetails** — Writes detailed error information to the VS Output window

### Output Window Helper

`Shared/OutputWindowHelper.cs` provides `ShowMessageInOutputWindow` for writing messages to the VS Output window with proper pane creation, activation, and formatting.

## Design Principles

### Defensive Programming

The wizard and UI components follow defensive programming practices:
- Null checks on all VS service calls
- Graceful degradation when services are unavailable
- Error logging to Output window instead of throwing exceptions
- Try-catch blocks around external service interactions

### Robustness

- InfoBar event handlers never crash the extension
- Failed package installations show user-friendly error messages
- Missing VS services log warnings rather than failing silently

## Contributing

When adding new features:
1. Add corresponding unit tests in `Tests/WindowsAppSDK.VSIX.UnitTests`
2. Follow the defensive programming patterns used in existing code
3. Document expected VS service interactions
4. Ensure all builds and tests pass before submitting

## Related Documentation

- [Windows App SDK Documentation](https://docs.microsoft.com/windows/apps/windows-app-sdk/)
- [Coding Guidelines](../../docs/Coding-Guidelines.md)
- [Contributor Guide](../../docs/contributor-guide.md)
5 changes: 4 additions & 1 deletion dev/VSIX/Shared/WizardImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public partial class NuGetPackageInstaller : IWizard
private IVsThreadedWaitDialog2 _waitDialog;
private Dictionary<string, Exception> _failedPackageExceptions = new Dictionary<string, Exception>();

// Replaceable in unit tests to avoid blocking MessageBox popups.
internal static Func<string, string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessageBox = MessageBox.Show;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the MessageBox class/object coming from?


public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
{
ThreadHelper.ThrowIfNotOnUIThread();
Expand Down Expand Up @@ -192,7 +195,7 @@ public void RunFinished()
var errorMessage = CreateErrorMessage(ErrorMessageFormat.MessageBox);
LogError(errorMessage);

var result = MessageBox.Show(
var result = ShowMessageBox(
errorMessage,
Resources._1046,
MessageBoxButtons.OK,
Expand Down
29 changes: 29 additions & 0 deletions dev/VSIX/Tests/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project>
<!--
Override the VSIX Directory.Build.props for test projects.
Test projects don't need VSIX-specific build settings.
-->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<VSIXBuild>false</VSIXBuild>
<DeployExtension>false</DeployExtension>
<EnableDefaultNoneItems>true</EnableDefaultNoneItems>
<!--
Suppress VS threading analyzer warnings in test projects:
VSSDK005: Avoid instantiating JoinableTaskContext (test setup creates its own)
VSTHRD002: Synchronous waits on tasks (tests block synchronously by design)
VSTHRD010: Accessing members on the main thread (tests use mocks/COM interop,
not the VS UI thread, so ThrowIfNotOnUIThread() would fail at runtime)
-->
<NoWarn>$(NoWarn);VSSDK005;VSTHRD002;VSTHRD010</NoWarn>
</PropertyGroup>

<!-- Useful folder paths (match VSIX conventions) -->
<PropertyGroup>
<RepoRoot>$(MSBuildThisFileDirectory)..\..\..\</RepoRoot>
<VSIXRootDir>$(MSBuildThisFileDirectory)..\</VSIXRootDir>
<SharedDir>$(VSIXRootDir)Shared\</SharedDir>
<ExtensionDir>$(VSIXRootDir)Extension\</ExtensionDir>
<ProjectTemplatesDir>$(VSIXRootDir)ProjectTemplates\</ProjectTemplatesDir>
</PropertyGroup>
</Project>
56 changes: 56 additions & 0 deletions dev/VSIX/Tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# VSIX Unit Tests

Unit tests for the Windows App SDK Visual Studio project template wizard and UI components.

## Prerequisites

- .NET 8.0 SDK (target framework: `net8.0-windows10.0.19041.0`)
- No Visual Studio installation required

> **Note:** While the VSIX extension and template projects require Visual Studio /
> `msbuild` to build (they depend on VSSDK targets not available in the `dotnet` CLI),
> the unit test project uses the SDK-style format and can be built and run with
> `dotnet test` independently.

## Running Tests

```powershell
# Run all VSIX unit tests
dotnet test dev\VSIX\Tests\WindowsAppSDK.VSIX.UnitTests\WindowsAppSDK.VSIX.UnitTests.csproj --verbosity normal

# Run a specific test class
dotnet test dev\VSIX\Tests\WindowsAppSDK.VSIX.UnitTests\WindowsAppSDK.VSIX.UnitTests.csproj --filter "FullyQualifiedName~NuGetPackageInstallerTests"
```

## Test Coverage (37 tests)

| Area | Tests | Description |
|------|-------|-------------|
| NuGetPackageInstaller | 20 | Package parsing (standard, empty, C++, unit-test, null), `ProjectFinishedGenerating` (C++ immediate install, C# deferred, project ref storage), installation failures (exception catch, partial failure, null component model / installer / project), happy path (all succeed, `RunFinished` no failures / with failures), `ShouldAddProjectItem`, and template-specific package counts |
| WizardInfoBarEvents | 9 | Null parameter handling (null element, null action item, both null), `OnClosed`, constructor storage, `SeeErrorDetails` routing, `ManageNuGetPackages` routing, unknown action context, non-hyperlink action item |
| ErrorMessages | 8 | `CreateErrorMessage` for InfoBar (single/multiple packages, null project fallback) and MessageBox formats, InfoBar vs MessageBox difference, `CreateDetailedErrorMessage` (exception type/message, multiple packages, manual-install instruction) |

## Project Structure

```
Tests/
├── Directory.Build.props # Overrides VSIX build props for test projects
└── WindowsAppSDK.VSIX.UnitTests/
├── WindowsAppSDK.VSIX.UnitTests.csproj # SDK-style project (net8.0-windows)
├── NuGetPackageInstallerTests.cs # Tests for NuGetPackageInstaller wizard
├── WizardInfoBarEventsTests.cs # Tests for NuGetInfoBarUIEvents
├── ErrorMessageTests.cs # Tests for error message construction
└── TestHelpers/
├── VsTestBase.cs # Base class: ThreadHelper + culture setup
├── MockServiceSetup.cs # Mock factory: IComponentModel, EnvDTE.Project, reflection helpers
└── TemplateTestData.cs # NuGet package lists and TemplateInfo for all templates
```

## Architecture Notes

- **Shared source linking** — Unit tests compile the same shared wizard source files (`WizardImplementation.cs`, `WizardInfoBarEvents.cs`, `OutputWindowHelper.cs`) with the `CSHARP_EXTENSION` constant defined, matching the C# extension project.
- **VSPackage resources** — The C# extension's `VSPackage.resx` and `VSPackage.Designer.cs` are linked into the test project so that resource strings (e.g., `Resources._1044`) resolve at runtime.
- **ThreadHelper configuration** — `VsTestBase` creates a `JoinableTaskContext` with the test thread as the main thread and sets it on `ThreadHelper` via reflection so `ThrowIfNotOnUIThread()` passes.
- **StartInstallationAsync** — Called directly via reflection, bypassing the `JoinableTaskFactory.Run` wrapper in `ProjectFinishedGenerating` which requires a VS message pump.
- **Error paths** that call `SwitchToMainThreadAsync` are tested for invocation but not full error message display (requires a VS message pump).
- **MessageBox interception** — `NuGetPackageInstaller.ShowMessageBox` is a replaceable `Func<>` field, allowing tests to capture and verify MessageBox content without blocking UI.
Loading