From becb170f03a6b5f35ba86743bab2aedbabd1e054 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Fri, 6 Mar 2026 23:07:30 -0800 Subject: [PATCH 01/27] Add sample test infrastructure for PR validation - Create SampleTestHelpers.psm1 shared PowerShell module with assertion, logging, CLI invocation, and MSIX packaging helpers - Add self-contained test.ps1 for each sample: cpp-app, dotnet-app, electron, flutter-app, rust-app, tauri-app, wpf-app - Add test-samples.yml GitHub Actions workflow with matrix strategy running 7 samples in parallel, triggered by Build and Package workflow - Add scripts/test-samples.ps1 local orchestrator for running tests locally with pass/fail summary - Update AGENTS.md with sample testing conventions and instructions Each test validates: prerequisites -> build -> package MSIX -> verify output. Tests run without elevation. electron-winml skipped (requires ML models). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-samples.yml | 136 +++++++++++ AGENTS.md | 29 +++ samples/SampleTestHelpers.psm1 | 378 +++++++++++++++++++++++++++++ samples/cpp-app/test.ps1 | 95 ++++++++ samples/dotnet-app/test.ps1 | 84 +++++++ samples/electron/test.ps1 | 126 ++++++++++ samples/flutter-app/test.ps1 | 99 ++++++++ samples/rust-app/test.ps1 | 91 +++++++ samples/tauri-app/test.ps1 | 96 ++++++++ samples/wpf-app/test.ps1 | 84 +++++++ scripts/test-samples.ps1 | 114 +++++++++ 11 files changed, 1332 insertions(+) create mode 100644 .github/workflows/test-samples.yml create mode 100644 samples/SampleTestHelpers.psm1 create mode 100644 samples/cpp-app/test.ps1 create mode 100644 samples/dotnet-app/test.ps1 create mode 100644 samples/electron/test.ps1 create mode 100644 samples/flutter-app/test.ps1 create mode 100644 samples/rust-app/test.ps1 create mode 100644 samples/tauri-app/test.ps1 create mode 100644 samples/wpf-app/test.ps1 create mode 100644 scripts/test-samples.ps1 diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml new file mode 100644 index 00000000..45404047 --- /dev/null +++ b/.github/workflows/test-samples.yml @@ -0,0 +1,136 @@ +name: Test Samples + +on: + workflow_run: + workflows: ["Build and Package"] + types: [completed] + workflow_dispatch: + inputs: + sample: + description: 'Specific sample to test (or "all")' + required: false + default: 'all' + type: choice + options: + - all + - cpp-app + - dotnet-app + - electron + - flutter-app + - rust-app + - tauri-app + - wpf-app + +permissions: + contents: read + actions: read # Required for downloading cross-workflow artifacts + +jobs: + test-sample: + # Run on successful builds or manual dispatch + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + strategy: + fail-fast: false + matrix: + sample: [cpp-app, dotnet-app, electron, flutter-app, rust-app, tauri-app, wpf-app] + runs-on: windows-latest + name: ${{ matrix.sample }} + + steps: + - name: Check if sample should run + id: check + shell: pwsh + run: | + $requested = '${{ github.event.inputs.sample || 'all' }}' + if ($requested -ne 'all' -and $requested -ne '${{ matrix.sample }}') { + echo "skip=true" >> $env:GITHUB_OUTPUT + } else { + echo "skip=false" >> $env:GITHUB_OUTPUT + } + + - name: Checkout + if: steps.check.outputs.skip != 'true' + uses: actions/checkout@v5 + + # Download the npm package artifact from the triggering build + - name: Download npm package (workflow_run) + if: >- + steps.check.outputs.skip != 'true' && + github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: npm-package + path: artifacts/npm + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download npm package (workflow_dispatch) + if: >- + steps.check.outputs.skip != 'true' && + github.event_name == 'workflow_dispatch' + uses: actions/download-artifact@v4 + with: + name: npm-package + path: artifacts/npm + continue-on-error: true + + # --- Toolchain setup (conditional per sample) --- + + - name: Setup .NET + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["dotnet-app", "wpf-app", "electron"]'), matrix.sample) + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Setup Node.js + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["electron", "tauri-app", "cpp-app", "dotnet-app", "wpf-app", "rust-app", "flutter-app"]'), matrix.sample) + uses: actions/setup-node@v5 + with: + node-version: '24' + + - name: Setup Flutter + if: >- + steps.check.outputs.skip != 'true' && + matrix.sample == 'flutter-app' + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Setup Rust + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["rust-app", "tauri-app"]'), matrix.sample) + uses: dtolnay/rust-toolchain@stable + + # --- Run the sample's self-contained test --- + + - name: Run ${{ matrix.sample }} test + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + $winappPath = "artifacts/npm" + if (-not (Test-Path $winappPath)) { + # Fallback: use the local source if no artifact was downloaded + $winappPath = "src/winapp-npm" + } + .\samples\${{ matrix.sample }}\test.ps1 -WinappPath $winappPath -Verbose + + # Summary job to provide a single check status for branch protection + test-samples-result: + if: always() + needs: test-sample + runs-on: ubuntu-latest + steps: + - name: Check results + run: | + if [ "${{ needs.test-sample.result }}" = "failure" ]; then + echo "::error::One or more sample tests failed" + exit 1 + fi + echo "All sample tests passed (or were skipped)" diff --git a/AGENTS.md b/AGENTS.md index 269b536a..a612222c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,35 @@ When adding or changing public facing features, ensure all documentation is also If a feature is big enough and requires its own docs page, add it under docs\ +## Sample testing + +Each sample under `samples/` has a self-contained `test.ps1` that validates the sample builds and packages correctly. Tests share infrastructure via `samples/SampleTestHelpers.psm1`. + +### Running sample tests locally + +```powershell +# Run all sample tests +.\scripts\test-samples.ps1 + +# Run a specific sample +.\scripts\test-samples.ps1 -Samples dotnet-app + +# Run with a specific winapp package (e.g., from CI artifacts) +.\scripts\test-samples.ps1 -WinappPath .\artifacts\npm -Verbose +``` + +### Writing a new sample test + +1. Create `test.ps1` in the sample directory +2. Import the shared helpers: `Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force` +3. Use `New-SampleTestContext` to initialize, `Complete-SampleTest` to finalize +4. Follow the pattern: prerequisites → install winapp → build → package MSIX → validate +5. Clean up generated artifacts in a `finally` block (unless `-SkipCleanup`) +6. Add the sample name to the matrix in `.github/workflows/test-samples.yml` + +### CI integration + +Sample tests run via `.github/workflows/test-samples.yml` using a GitHub Actions matrix strategy. Each sample runs in its own parallel job after the main build completes. The workflow downloads the npm package artifact from the `Build and Package` workflow. ## Where to look first diff --git a/samples/SampleTestHelpers.psm1 b/samples/SampleTestHelpers.psm1 new file mode 100644 index 00000000..21ce25fa --- /dev/null +++ b/samples/SampleTestHelpers.psm1 @@ -0,0 +1,378 @@ +<# +.SYNOPSIS +Shared PowerShell helpers for sample tests. + +.DESCRIPTION +This module provides common test helper functions used by each sample's test.ps1 script. +Import with: Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force +#> + +# ============================================================================ +# Logging Helpers +# ============================================================================ + +function Write-TestHeader { + param([string]$Message) + Write-Host "`n$('='*80)" -ForegroundColor Cyan + Write-Host "TEST: $Message" -ForegroundColor Cyan + Write-Host "$('='*80)`n" -ForegroundColor Cyan +} + +function Write-TestStep { + param([string]$Message, [int]$Step) + Write-Host "[$Step] $Message" -ForegroundColor Yellow +} + +function Write-TestSuccess { + param([string]$Message) + Write-Host " ✓ $Message" -ForegroundColor Green +} + +function Write-TestError { + param([string]$Message) + Write-Host " ✗ $Message" -ForegroundColor Red +} + +# ============================================================================ +# Assertion Helpers +# ============================================================================ + +function Assert-ExitCode { + <# + .SYNOPSIS + Asserts the last exit code was 0, throwing with the given message if not. + #> + param( + [string]$FailMessage, + [int]$Expected = 0 + ) + if ($LASTEXITCODE -ne $Expected) { + Write-TestError "$FailMessage (exit code: $LASTEXITCODE, expected: $Expected)" + throw $FailMessage + } +} + +function Assert-Command { + <# + .SYNOPSIS + Runs a command string via Invoke-Expression, asserts exit code 0, and returns output. + #> + param( + [string]$Command, + [string]$FailMessage + ) + Write-Verbose "Running: $Command" + $output = Invoke-Expression $Command + if ($LASTEXITCODE -ne 0) { + Write-TestError $FailMessage + throw $FailMessage + } + Write-TestSuccess $Command + return $output +} + +function Assert-FileExists { + param( + [string]$Path, + [string]$Description + ) + if (-not (Test-Path $Path)) { + Write-TestError "$Description not found at $Path" + throw "$Description not found at $Path" + } + Write-TestSuccess "$Description exists: $Path" +} + +function Assert-DirectoryExists { + param( + [string]$Path, + [string]$Description + ) + if (-not (Test-Path $Path -PathType Container)) { + Write-TestError "$Description not found at $Path" + throw "$Description not found at $Path" + } + Write-TestSuccess "$Description exists: $Path" +} + +function Assert-OutputContains { + <# + .SYNOPSIS + Asserts that the given output string contains the expected substring. + #> + param( + [string]$Output, + [string]$Expected, + [string]$Description + ) + if ($Output -notmatch [regex]::Escape($Expected)) { + Write-TestError "$Description — expected output to contain '$Expected'" + throw "$Description — expected output to contain '$Expected'" + } + Write-TestSuccess "$Description — output contains '$Expected'" +} + +# ============================================================================ +# Winapp CLI Helpers +# ============================================================================ + +function Resolve-WinappCliPath { + <# + .SYNOPSIS + Resolves the winapp CLI path from artifacts or local build. + + .DESCRIPTION + Given -WinappPath, finds the npm tarball or package directory suitable for + `npm install`. Returns the resolved absolute path. If nothing is provided, + falls back to the default local build location. + #> + param( + [string]$WinappPath + ) + + $repoRoot = (Resolve-Path "$PSScriptRoot\..").Path + + if (-not $WinappPath) { + $WinappPath = Join-Path $repoRoot "artifacts\npm" + if (-not (Test-Path $WinappPath)) { + $WinappPath = Join-Path $repoRoot "src\winapp-npm" + } + } + + if (-not (Test-Path $WinappPath)) { + throw "Winapp path not found: $WinappPath" + } + + $resolved = (Resolve-Path $WinappPath).Path + + # If directory contains a .tgz, return the tgz path + if (Test-Path $resolved -PathType Container) { + $tgz = Get-ChildItem -Path $resolved -Filter "*.tgz" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($tgz) { + return $tgz.FullName + } + if (Test-Path (Join-Path $resolved "package.json")) { + return $resolved + } + throw "No .tgz or package.json found in $resolved" + } + + # Direct file path (e.g., a .tgz) + return $resolved +} + +function Invoke-Winapp { + <# + .SYNOPSIS + Invokes the winapp CLI with the given arguments. + + .DESCRIPTION + Uses npx winapp if an npm package was installed in the current project, + otherwise falls back to dotnet run with the WinApp.Cli project. + #> + param( + [Parameter(Mandatory)] + [string]$Arguments, + [string]$FailMessage = "winapp $Arguments failed" + ) + + # Prefer npx if available in the project + $npxWinapp = Join-Path (Get-Location) "node_modules\.bin\winapp.cmd" + if (Test-Path $npxWinapp) { + $cmd = "npx winapp $Arguments" + } else { + # Fallback to dotnet run + $cliProject = Join-Path $PSScriptRoot "..\src\winapp-CLI\WinApp.Cli\WinApp.Cli.csproj" + if (Test-Path $cliProject) { + $cmd = "dotnet run --project `"$cliProject`" -- $Arguments" + } else { + # Last resort: assume winapp is on PATH + $cmd = "winapp $Arguments" + } + } + + return Assert-Command -Command $cmd -FailMessage $FailMessage +} + +function Install-WinappNpmPackage { + <# + .SYNOPSIS + Installs the winapp npm package into the current project from a path or artifacts folder. + #> + param( + [Parameter(Mandatory)] + [string]$PackagePath + ) + + Write-Verbose "Installing winapp from: $PackagePath" + Assert-Command "npm install `"$PackagePath`" --save-dev" "Failed to install winapp npm package" + Assert-FileExists (Join-Path (Get-Location) "node_modules\.bin\winapp.cmd") "winapp CLI binary" +} + +function Install-WinappGlobal { + <# + .SYNOPSIS + Installs the winapp npm package globally so 'winapp' is available on PATH. + Use for non-Node samples (C++, .NET, Rust, Flutter) that call winapp from + build systems or the command line directly. + #> + param( + [Parameter(Mandatory)] + [string]$PackagePath + ) + + Write-Verbose "Installing winapp globally from: $PackagePath" + Assert-Command "npm install -g `"$PackagePath`"" "Failed to install winapp globally" + + # Verify winapp is now on PATH + try { + $winappVersion = & winapp --version 2>&1 | Select-Object -First 1 + Write-TestSuccess "winapp installed globally: $winappVersion" + } catch { + Write-TestError "winapp not found on PATH after global install" + throw "winapp global install did not put CLI on PATH" + } +} + +# ============================================================================ +# Prerequisite Checks +# ============================================================================ + +function Assert-Prerequisite { + <# + .SYNOPSIS + Asserts that a command-line tool is available on PATH. + #> + param( + [string]$Command, + [string]$DisplayName = $Command, + [string]$VersionFlag = "--version" + ) + try { + $version = & $Command $VersionFlag 2>&1 | Select-Object -First 1 + Write-TestSuccess "$DisplayName found: $version" + } catch { + Write-TestError "$DisplayName is not installed or not in PATH" + throw "$DisplayName is required but not found" + } +} + +# ============================================================================ +# Test Environment Management +# ============================================================================ + +function New-SampleTestContext { + <# + .SYNOPSIS + Initializes a test context for a sample test. Returns a context hashtable. + + .DESCRIPTION + Sets strict mode, resolves the sample directory, and prepares the context + object used by all sample tests. Does NOT create temporary directories — + sample tests run in-place against the sample's own source directory. + #> + param( + [Parameter(Mandatory)] + [string]$SampleName, + [string]$WinappPath, + [switch]$Verbose + ) + + Set-StrictMode -Version Latest + $ErrorActionPreference = 'Stop' + if ($Verbose) { $VerbosePreference = 'Continue' } + + $sampleDir = $PSScriptRoot # test.ps1 lives alongside sample files + $repoRoot = (Resolve-Path "$sampleDir\..\..").Path + + $ctx = @{ + SampleName = $SampleName + SampleDir = $sampleDir + RepoRoot = $repoRoot + WinappPath = $WinappPath + StartTime = Get-Date + } + + Write-TestHeader "$SampleName Sample Test" + Write-Verbose "Sample directory: $($ctx.SampleDir)" + Write-Verbose "Repo root: $($ctx.RepoRoot)" + + return $ctx +} + +function Complete-SampleTest { + <# + .SYNOPSIS + Reports success and elapsed time for a sample test. + #> + param( + [Parameter(Mandatory)] + [hashtable]$Context + ) + $elapsed = (Get-Date) - $Context.StartTime + Write-Host "`n$('='*80)" -ForegroundColor Green + Write-Host "$($Context.SampleName) SAMPLE TEST COMPLETED SUCCESSFULLY ($([math]::Round($elapsed.TotalSeconds, 1))s)" -ForegroundColor Green + Write-Host "$('='*80)`n" -ForegroundColor Green +} + +# ============================================================================ +# MSIX Packaging Helpers +# ============================================================================ + +function Assert-MsixCreated { + <# + .SYNOPSIS + Asserts that at least one .msix file exists in the given directory. + #> + param( + [string]$Directory, + [string]$Description = "MSIX package" + ) + $msixFiles = Get-ChildItem -Path $Directory -Filter "*.msix" -ErrorAction SilentlyContinue + if (-not $msixFiles) { + Write-TestError "No .msix file found in $Directory" + throw "$Description not found in $Directory" + } + Write-TestSuccess "$Description created: $($msixFiles[0].Name)" + return $msixFiles[0].FullName +} + +function New-DevCertificate { + <# + .SYNOPSIS + Generates a development certificate using winapp cert generate. + Returns the path to the generated .pfx file. + #> + param( + [string]$OutputDir = (Get-Location) + ) + Invoke-Winapp "cert generate" -FailMessage "Failed to generate development certificate" + $certPath = Join-Path $OutputDir "devcert.pfx" + Assert-FileExists $certPath "Development certificate" + return $certPath +} + +# ============================================================================ +# Exports +# ============================================================================ + +Export-ModuleMember -Function @( + 'Write-TestHeader' + 'Write-TestStep' + 'Write-TestSuccess' + 'Write-TestError' + 'Assert-ExitCode' + 'Assert-Command' + 'Assert-FileExists' + 'Assert-DirectoryExists' + 'Assert-OutputContains' + 'Resolve-WinappCliPath' + 'Invoke-Winapp' + 'Install-WinappNpmPackage' + 'Install-WinappGlobal' + 'Assert-Prerequisite' + 'New-SampleTestContext' + 'Complete-SampleTest' + 'Assert-MsixCreated' + 'New-DevCertificate' +) diff --git a/samples/cpp-app/test.ps1 b/samples/cpp-app/test.ps1 new file mode 100644 index 00000000..8431ebfa --- /dev/null +++ b/samples/cpp-app/test.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS +Test script for the cpp-app sample. + +.DESCRIPTION +Restores Windows App SDK headers via winapp, builds the C++ app with CMake, +then packages it as an MSIX. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "cpp-app" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "cmake" -DisplayName "CMake" + Assert-Prerequisite "npm" -DisplayName "npm" + + # ------------------------------------------------------------------ + # Install winapp globally (CMakeLists.txt calls winapp commands) + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Restore Windows App SDK headers + # ------------------------------------------------------------------ + Write-TestStep "Restoring Windows App SDK packages..." (++$step) + Assert-Command "winapp restore" "winapp restore failed" + Assert-DirectoryExists ".winapp" ".winapp directory" + + # ------------------------------------------------------------------ + # Configure CMake (Release to avoid debug-identity requirement) + # ------------------------------------------------------------------ + Write-TestStep "Configuring CMake project..." (++$step) + Assert-Command "cmake -B build -DCMAKE_BUILD_TYPE=Release" "CMake configure failed" + + # ------------------------------------------------------------------ + # Build + # ------------------------------------------------------------------ + Write-TestStep "Building C++ app..." (++$step) + Assert-Command "cmake --build build --config Release" "CMake build failed" + + $buildOutput = Join-Path $ctx.SampleDir "build\Release" + Assert-FileExists (Join-Path $buildOutput "cpp-app.exe") "cpp-app.exe" + + # ------------------------------------------------------------------ + # Generate certificate and package MSIX + # ------------------------------------------------------------------ + Write-TestStep "Generating development certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + + Write-TestStep "Packaging as MSIX..." (++$step) + Assert-Command "winapp pack `"$buildOutput`" --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "cpp-app MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $ctx.SampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/samples/dotnet-app/test.ps1 b/samples/dotnet-app/test.ps1 new file mode 100644 index 00000000..ad809db8 --- /dev/null +++ b/samples/dotnet-app/test.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS +Test script for the dotnet-app sample. + +.DESCRIPTION +Builds the .NET console app in Release mode, which triggers the automatic +MSIX packaging MSBuild target, and validates the output. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "dotnet-app" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" + Assert-Prerequisite "npm" -DisplayName "npm" + + # ------------------------------------------------------------------ + # Install winapp globally (MSBuild targets call winapp directly) + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Restore NuGet packages + # ------------------------------------------------------------------ + Write-TestStep "Restoring NuGet packages..." (++$step) + Assert-Command "dotnet restore" "dotnet restore failed" + + # ------------------------------------------------------------------ + # Generate dev certificate (required by Release MSBuild target) + # ------------------------------------------------------------------ + Write-TestStep "Generating development certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + Assert-FileExists "devcert.pfx" "Development certificate" + + # ------------------------------------------------------------------ + # Build Release (triggers automatic MSIX packaging) + # ------------------------------------------------------------------ + Write-TestStep "Building in Release mode (auto-packages MSIX)..." (++$step) + Assert-Command "dotnet build -c Release" "dotnet build -c Release failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "dotnet-app MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + # Clean up generated artifacts (keep source files) + Remove-Item -Path (Join-Path $ctx.SampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/samples/electron/test.ps1 b/samples/electron/test.ps1 new file mode 100644 index 00000000..7ca1be24 --- /dev/null +++ b/samples/electron/test.ps1 @@ -0,0 +1,126 @@ +<# +.SYNOPSIS +Test script for the electron sample. + +.DESCRIPTION +Installs dependencies, builds C++ and C# native addons, packages the +Electron app with Forge, generates a certificate, and creates an MSIX. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "electron" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "node" -DisplayName "Node.js" + Assert-Prerequisite "npm" -DisplayName "npm" + Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" + + # ------------------------------------------------------------------ + # Set up npm cache to avoid ECOMPROMISED errors in CI + # ------------------------------------------------------------------ + $npmCacheDir = Join-Path $ctx.SampleDir ".npm-cache" + $null = New-Item -ItemType Directory -Path $npmCacheDir -Force + $env:npm_config_cache = $npmCacheDir + + # ------------------------------------------------------------------ + # Install npm dependencies (skip postinstall to avoid debug-identity) + # ------------------------------------------------------------------ + Write-TestStep "Installing npm dependencies..." (++$step) + Assert-Command "npm install --ignore-scripts" "npm install failed" + + # ------------------------------------------------------------------ + # Install winapp as local dev dependency + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp npm package..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappNpmPackage -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Initialize winapp workspace (restore SDKs, generate config) + # ------------------------------------------------------------------ + Write-TestStep "Initializing winapp workspace..." (++$step) + Invoke-Winapp "init . --use-defaults --setup-sdks=stable" -FailMessage "winapp init failed" + Assert-DirectoryExists ".winapp" ".winapp directory" + + # ------------------------------------------------------------------ + # Build C++ addon + # ------------------------------------------------------------------ + Write-TestStep "Building C++ addon..." (++$step) + Assert-Command "npm run build-addon" "C++ addon build failed" + + # ------------------------------------------------------------------ + # Build C# addon + # ------------------------------------------------------------------ + Write-TestStep "Building C# addon..." (++$step) + Assert-Command "npm run build-csAddon" "C# addon build failed" + + # ------------------------------------------------------------------ + # Package Electron app with Forge + # ------------------------------------------------------------------ + Write-TestStep "Packaging Electron app..." (++$step) + Assert-Command "npm run package" "Electron packaging failed" + + $outDir = Join-Path $ctx.SampleDir "out" + Assert-DirectoryExists $outDir "Electron output directory" + + # Find the packaged app directory + $appPackageDirs = Get-ChildItem -Path $outDir -Directory -ErrorAction SilentlyContinue + if (-not $appPackageDirs) { + Write-TestError "No app package directories found in $outDir" + throw "Electron app packaging did not create output directory" + } + $appPackageDir = $appPackageDirs[0].FullName + Write-TestSuccess "Electron app packaged to: $appPackageDir" + + # ------------------------------------------------------------------ + # Generate certificate and package MSIX + # ------------------------------------------------------------------ + Write-TestStep "Generating development certificate..." (++$step) + $certPath = New-DevCertificate + + Write-TestStep "Packaging as MSIX..." (++$step) + Invoke-Winapp "pack `"$appPackageDir`" --cert `"$certPath`"" -FailMessage "winapp pack failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "electron MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $ctx.SampleDir "out") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir ".npm-cache") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/samples/flutter-app/test.ps1 b/samples/flutter-app/test.ps1 new file mode 100644 index 00000000..07f7a570 --- /dev/null +++ b/samples/flutter-app/test.ps1 @@ -0,0 +1,99 @@ +<# +.SYNOPSIS +Test script for the flutter-app sample. + +.DESCRIPTION +Restores packages, builds the Flutter Windows desktop app, and packages +the output as an MSIX. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "flutter-app" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "flutter" -DisplayName "Flutter SDK" + Assert-Prerequisite "npm" -DisplayName "npm" + + # ------------------------------------------------------------------ + # Install winapp globally + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Restore packages + # ------------------------------------------------------------------ + Write-TestStep "Getting Flutter dependencies..." (++$step) + Assert-Command "flutter pub get" "flutter pub get failed" + + Write-TestStep "Restoring Windows App SDK packages..." (++$step) + Assert-Command "winapp restore" "winapp restore failed" + Assert-DirectoryExists ".winapp" ".winapp directory" + + # ------------------------------------------------------------------ + # Build Flutter Windows app + # ------------------------------------------------------------------ + Write-TestStep "Building Flutter Windows app..." (++$step) + Assert-Command "flutter build windows" "flutter build windows failed" + + $buildOutput = Join-Path $ctx.SampleDir "build\windows\x64\runner\Release" + Assert-DirectoryExists $buildOutput "Flutter build output" + Assert-FileExists (Join-Path $buildOutput "flutter_app.exe") "flutter_app.exe" + + # ------------------------------------------------------------------ + # Prepare distribution folder and package MSIX + # ------------------------------------------------------------------ + Write-TestStep "Preparing distribution folder..." (++$step) + $distDir = Join-Path $ctx.SampleDir "dist" + if (Test-Path $distDir) { Remove-Item $distDir -Recurse -Force } + Copy-Item $buildOutput -Destination $distDir -Recurse + + Write-TestStep "Generating development certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + + Write-TestStep "Packaging as MSIX..." (++$step) + Assert-Command "winapp pack dist --cert devcert.pfx" "winapp pack failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "flutter-app MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $ctx.SampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "dist") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/samples/rust-app/test.ps1 b/samples/rust-app/test.ps1 new file mode 100644 index 00000000..72459e6a --- /dev/null +++ b/samples/rust-app/test.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS +Test script for the rust-app sample. + +.DESCRIPTION +Builds the Rust app with Cargo, then packages the binary as an MSIX using +winapp pack. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "rust-app" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "cargo" -DisplayName "Rust/Cargo" + Assert-Prerequisite "npm" -DisplayName "npm" + + # ------------------------------------------------------------------ + # Install winapp globally + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Build Rust app + # ------------------------------------------------------------------ + Write-TestStep "Building Rust app (release)..." (++$step) + Assert-Command "cargo build --release" "cargo build --release failed" + + $rustExe = Join-Path $ctx.SampleDir "target\release\rust-app.exe" + Assert-FileExists $rustExe "rust-app.exe" + + # ------------------------------------------------------------------ + # Prepare MSIX layout directory + # ------------------------------------------------------------------ + Write-TestStep "Preparing MSIX layout..." (++$step) + $msixDir = Join-Path $ctx.SampleDir "msix" + if (Test-Path $msixDir) { Remove-Item $msixDir -Recurse -Force } + $null = New-Item -ItemType Directory -Path $msixDir -Force + Copy-Item $rustExe -Destination $msixDir + + # ------------------------------------------------------------------ + # Generate certificate and package MSIX + # ------------------------------------------------------------------ + Write-TestStep "Generating development certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip --manifest appxmanifest.xml" "Failed to generate dev certificate" + + Write-TestStep "Packaging as MSIX..." (++$step) + Assert-Command "winapp pack msix --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "rust-app MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $ctx.SampleDir "target") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "msix") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/samples/tauri-app/test.ps1 b/samples/tauri-app/test.ps1 new file mode 100644 index 00000000..53d19ed9 --- /dev/null +++ b/samples/tauri-app/test.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS +Test script for the tauri-app sample. + +.DESCRIPTION +Installs npm and Rust dependencies, builds the Tauri app, and packages the +output as an MSIX. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "tauri-app" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "node" -DisplayName "Node.js" + Assert-Prerequisite "npm" -DisplayName "npm" + Assert-Prerequisite "cargo" -DisplayName "Rust/Cargo" + + # ------------------------------------------------------------------ + # Install winapp globally + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Install npm dependencies + # ------------------------------------------------------------------ + Write-TestStep "Installing npm dependencies..." (++$step) + Assert-Command "npm install" "npm install failed" + + # ------------------------------------------------------------------ + # Build Tauri app (cargo build for the Rust backend) + # ------------------------------------------------------------------ + Write-TestStep "Building Tauri app..." (++$step) + Assert-Command "cargo build --release --manifest-path src-tauri\Cargo.toml" "Tauri cargo build failed" + + $tauriExe = Join-Path $ctx.SampleDir "src-tauri\target\release\tauri-app.exe" + Assert-FileExists $tauriExe "tauri-app.exe" + + # ------------------------------------------------------------------ + # Generate certificate and package MSIX + # ------------------------------------------------------------------ + Write-TestStep "Generating development certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip --manifest appxmanifest.xml" "Failed to generate dev certificate" + + Write-TestStep "Preparing MSIX layout..." (++$step) + $msixDir = Join-Path $ctx.SampleDir "msix-layout" + if (Test-Path $msixDir) { Remove-Item $msixDir -Recurse -Force } + $null = New-Item -ItemType Directory -Path $msixDir -Force + Copy-Item $tauriExe -Destination $msixDir + + Write-TestStep "Packaging as MSIX..." (++$step) + Assert-Command "winapp pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "tauri-app MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $ctx.SampleDir "src-tauri\target") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "msix-layout") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/samples/wpf-app/test.ps1 b/samples/wpf-app/test.ps1 new file mode 100644 index 00000000..69d184f4 --- /dev/null +++ b/samples/wpf-app/test.ps1 @@ -0,0 +1,84 @@ +<# +.SYNOPSIS +Test script for the wpf-app sample. + +.DESCRIPTION +Builds the WPF app in Release mode, which triggers the automatic MSIX +packaging MSBuild target, and validates the output. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "wpf-app" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 + +try { + Push-Location $ctx.SampleDir + + # ------------------------------------------------------------------ + # Prerequisites + # ------------------------------------------------------------------ + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" + Assert-Prerequisite "npm" -DisplayName "npm" + + # ------------------------------------------------------------------ + # Install winapp globally (MSBuild targets call winapp directly) + # ------------------------------------------------------------------ + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ------------------------------------------------------------------ + # Restore NuGet packages + # ------------------------------------------------------------------ + Write-TestStep "Restoring NuGet packages..." (++$step) + Assert-Command "dotnet restore" "dotnet restore failed" + + # ------------------------------------------------------------------ + # Generate dev certificate (required by Release MSBuild target) + # ------------------------------------------------------------------ + Write-TestStep "Generating development certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + Assert-FileExists "devcert.pfx" "Development certificate" + + # ------------------------------------------------------------------ + # Build Release (triggers automatic MSIX packaging) + # WPF requires a platform-specific RID — not AnyCPU + # ------------------------------------------------------------------ + Write-TestStep "Building in Release mode (auto-packages MSIX)..." (++$step) + Assert-Command "dotnet build -c Release -r win-x64" "dotnet build -c Release -r win-x64 failed" + + # ------------------------------------------------------------------ + # Validate MSIX was created + # ------------------------------------------------------------------ + Write-TestStep "Validating MSIX output..." (++$step) + Assert-MsixCreated -Directory $ctx.SampleDir -Description "wpf-app MSIX package" + + Complete-SampleTest -Context $ctx + +} finally { + Pop-Location + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $ctx.SampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue + Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + } +} diff --git a/scripts/test-samples.ps1 b/scripts/test-samples.ps1 new file mode 100644 index 00000000..6adfd5eb --- /dev/null +++ b/scripts/test-samples.ps1 @@ -0,0 +1,114 @@ +<# +.SYNOPSIS +Local orchestrator to run sample tests. + +.DESCRIPTION +Discovers and runs test.ps1 for each sample (or a specified subset). +Reports a pass/fail summary at the end. + +.PARAMETER Samples +One or more sample names to test. Defaults to all samples that have a test.ps1. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) passed to each test. + +.PARAMETER SkipCleanup +Passed through to each test — keep build artifacts for debugging. + +.PARAMETER Verbose +Enable verbose output for all tests. + +.EXAMPLE +.\scripts\test-samples.ps1 +Run all sample tests. + +.EXAMPLE +.\scripts\test-samples.ps1 -Samples dotnet-app,rust-app -Verbose +Run only the dotnet-app and rust-app tests with verbose output. +#> + +param( + [string[]]$Samples, + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$samplesRoot = Join-Path $PSScriptRoot "..\samples" + +# Discover samples with test.ps1 +$allTests = Get-ChildItem -Path $samplesRoot -Directory | + Where-Object { Test-Path (Join-Path $_.FullName "test.ps1") } | + Select-Object -ExpandProperty Name + +if ($Samples) { + # Validate requested samples exist + foreach ($s in $Samples) { + if ($s -notin $allTests) { + Write-Warning "Sample '$s' does not have a test.ps1 — skipping" + } + } + $testList = $Samples | Where-Object { $_ -in $allTests } +} else { + $testList = $allTests +} + +if (-not $testList) { + Write-Host "No sample tests to run." -ForegroundColor Yellow + exit 0 +} + +Write-Host "`n$('='*80)" -ForegroundColor Cyan +Write-Host "SAMPLE TEST RUNNER — $($testList.Count) sample(s)" -ForegroundColor Cyan +Write-Host "$('='*80)`n" -ForegroundColor Cyan + +$results = @() + +foreach ($sample in $testList) { + $testScript = Join-Path $samplesRoot $sample "test.ps1" + Write-Host "`nRunning: $sample" -ForegroundColor Yellow + Write-Host ("-" * 40) -ForegroundColor DarkGray + + $sw = [System.Diagnostics.Stopwatch]::StartNew() + try { + $params = @{} + if ($WinappPath) { $params['WinappPath'] = $WinappPath } + if ($SkipCleanup) { $params['SkipCleanup'] = $true } + if ($Verbose) { $params['Verbose'] = $true } + + & $testScript @params + $sw.Stop() + $results += [PSCustomObject]@{ Sample = $sample; Status = 'PASS'; Duration = $sw.Elapsed; Error = $null } + } catch { + $sw.Stop() + Write-Host " ✗ $sample FAILED: $_" -ForegroundColor Red + $results += [PSCustomObject]@{ Sample = $sample; Status = 'FAIL'; Duration = $sw.Elapsed; Error = $_.ToString() } + } +} + +# Summary +Write-Host "`n$('='*80)" -ForegroundColor Cyan +Write-Host "RESULTS SUMMARY" -ForegroundColor Cyan +Write-Host "$('='*80)" -ForegroundColor Cyan + +$passed = ($results | Where-Object Status -eq 'PASS').Count +$failed = ($results | Where-Object Status -eq 'FAIL').Count + +foreach ($r in $results) { + $color = if ($r.Status -eq 'PASS') { 'Green' } else { 'Red' } + $dur = "{0:mm\:ss}" -f $r.Duration + $line = " [{0}] {1} ({2})" -f $r.Status, $r.Sample, $dur + Write-Host $line -ForegroundColor $color + if ($r.Error) { + Write-Host " $($r.Error)" -ForegroundColor DarkRed + } +} + +Write-Host "`n $passed passed, $failed failed out of $($results.Count) samples`n" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'Green' }) + +if ($failed -gt 0) { + exit 1 +} From 4edbf3b773c3a624632cb27c9db77d5e99ef1ab4 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Sat, 7 Mar 2026 07:28:29 -0800 Subject: [PATCH 02/27] Extend sample tests with guide-first workflows and packaging-cli test Rewrite all 7 sample test.ps1 scripts to follow a guide-first approach: - Phase 1: From-scratch guide workflow in a temp directory (scaffold project, winapp init, build, cert generate, cert info, pack MSIX) - Phase 2: Quick build of existing sample code to verify freshness Add new packaging-cli guide test (samples/packaging-cli/test.ps1): - Tests winapp manifest generate, cert generate, cert info, pack, sign - Validates the docs/guides/packaging-cli.md workflow end-to-end Update shared module with new helpers: - New-TempTestDirectory / Remove-TempTestDirectory for temp dir lifecycle - Assert-WinappInitOutput for verifying winapp init creates expected files - Assert-CertInfo for verifying winapp cert info output Add packaging-cli to CI workflow matrix (8 parallel jobs total). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-samples.yml | 3 +- samples/SampleTestHelpers.psm1 | 81 ++++++++++++++++ samples/cpp-app/test.ps1 | 100 ++++++++++++-------- samples/dotnet-app/test.ps1 | 77 +++++++++------- samples/electron/test.ps1 | 142 +++++++++++++++++------------ samples/flutter-app/test.ps1 | 92 +++++++++++-------- samples/packaging-cli/test.ps1 | 99 ++++++++++++++++++++ samples/rust-app/test.ps1 | 88 ++++++++++-------- samples/tauri-app/test.ps1 | 92 +++++++++++-------- samples/wpf-app/test.ps1 | 80 +++++++++------- 10 files changed, 575 insertions(+), 279 deletions(-) create mode 100644 samples/packaging-cli/test.ps1 diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index 45404047..cc50ea5e 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -17,6 +17,7 @@ on: - dotnet-app - electron - flutter-app + - packaging-cli - rust-app - tauri-app - wpf-app @@ -34,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - sample: [cpp-app, dotnet-app, electron, flutter-app, rust-app, tauri-app, wpf-app] + sample: [cpp-app, dotnet-app, electron, flutter-app, packaging-cli, rust-app, tauri-app, wpf-app] runs-on: windows-latest name: ${{ matrix.sample }} diff --git a/samples/SampleTestHelpers.psm1 b/samples/SampleTestHelpers.psm1 index 21ce25fa..7a93dbad 100644 --- a/samples/SampleTestHelpers.psm1 +++ b/samples/SampleTestHelpers.psm1 @@ -352,6 +352,83 @@ function New-DevCertificate { return $certPath } +# ============================================================================ +# Temp Directory Helpers (for from-scratch guide tests) +# ============================================================================ + +function New-TempTestDirectory { + <# + .SYNOPSIS + Creates a temporary directory for from-scratch guide workflow tests. + Returns the absolute path to the new directory. + #> + param( + [Parameter(Mandatory)] + [string]$Prefix + ) + $tempBase = Join-Path ([System.IO.Path]::GetTempPath()) "winapp-test" + $null = New-Item -ItemType Directory -Path $tempBase -Force + $tempDir = Join-Path $tempBase "$Prefix-$([System.IO.Path]::GetRandomFileName())" + $null = New-Item -ItemType Directory -Path $tempDir -Force + Write-TestSuccess "Created temp directory: $tempDir" + return $tempDir +} + +function Remove-TempTestDirectory { + <# + .SYNOPSIS + Removes a temporary test directory created by New-TempTestDirectory. + #> + param( + [Parameter(Mandatory)] + [string]$Path + ) + if (Test-Path $Path) { + Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue + Write-TestSuccess "Cleaned up temp directory: $Path" + } +} + +function Assert-WinappInitOutput { + <# + .SYNOPSIS + Validates that winapp init created the expected files in the current directory. + Checks for winapp.yaml, appxmanifest.xml, and .winapp/ directory. + #> + param( + [string]$Directory = (Get-Location), + [switch]$ExpectWinappYaml = $true, + [switch]$ExpectManifest = $true, + [switch]$ExpectDotWinapp + ) + if ($ExpectWinappYaml) { + Assert-FileExists (Join-Path $Directory "winapp.yaml") "winapp.yaml config" + } + if ($ExpectManifest) { + Assert-FileExists (Join-Path $Directory "appxmanifest.xml") "AppxManifest" + } + if ($ExpectDotWinapp) { + Assert-DirectoryExists (Join-Path $Directory ".winapp") ".winapp SDK directory" + } +} + +function Assert-CertInfo { + <# + .SYNOPSIS + Runs winapp cert info on a certificate and validates the output is non-empty. + #> + param( + [Parameter(Mandatory)] + [string]$CertPath + ) + $output = Invoke-Winapp "cert info `"$CertPath`"" -FailMessage "winapp cert info failed" + if (-not $output) { + Write-TestError "winapp cert info produced no output" + throw "winapp cert info produced no output" + } + Write-TestSuccess "winapp cert info returned certificate details" +} + # ============================================================================ # Exports # ============================================================================ @@ -375,4 +452,8 @@ Export-ModuleMember -Function @( 'Complete-SampleTest' 'Assert-MsixCreated' 'New-DevCertificate' + 'New-TempTestDirectory' + 'Remove-TempTestDirectory' + 'Assert-WinappInitOutput' + 'Assert-CertInfo' ) diff --git a/samples/cpp-app/test.ps1 b/samples/cpp-app/test.ps1 index 8431ebfa..3680be20 100644 --- a/samples/cpp-app/test.ps1 +++ b/samples/cpp-app/test.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS -Test script for the cpp-app sample. +Test script for the cpp-app sample and C++/CMake guide workflow. .DESCRIPTION -Restores Windows App SDK headers via winapp, builds the C++ app with CMake, -then packages it as an MSIX. +Phase 1: Follows the docs/guides/cpp.md guide from scratch — creates a minimal + C++ project, runs winapp init, builds with CMake, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,70 +27,93 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "cpp-app" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "cmake" -DisplayName "CMake" Assert-Prerequisite "npm" -DisplayName "npm" - # ------------------------------------------------------------------ - # Install winapp globally (CMakeLists.txt calls winapp commands) - # ------------------------------------------------------------------ Write-TestStep "Installing winapp CLI..." (++$step) $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Restore Windows App SDK headers - # ------------------------------------------------------------------ - Write-TestStep "Restoring Windows App SDK packages..." (++$step) - Assert-Command "winapp restore" "winapp restore failed" - Assert-DirectoryExists ".winapp" ".winapp directory" + # ================================================================== + # Phase 1 — Guide Workflow (from scratch) + # ================================================================== + Write-TestHeader "Phase 1: C++/CMake Guide Workflow (from scratch)" + + $tempDir = New-TempTestDirectory -Prefix "cpp-guide" + Push-Location $tempDir + + Write-TestStep "Creating minimal C++ project..." (++$step) + # Minimal main.cpp that uses Windows APIs (matches guide) + @' +#include +#include +int main() { + std::cout << "Hello from C++ app" << std::endl; + return 0; +} +'@ | Set-Content "main.cpp" + + @' +cmake_minimum_required(VERSION 3.20) +project(test-cpp-app LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +add_executable(test-cpp-app main.cpp) +'@ | Set-Content "CMakeLists.txt" + + Write-TestStep "Running winapp init..." (++$step) + Assert-Command "winapp init --use-defaults --setup-sdks=stable" "winapp init failed" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest -ExpectDotWinapp - # ------------------------------------------------------------------ - # Configure CMake (Release to avoid debug-identity requirement) - # ------------------------------------------------------------------ Write-TestStep "Configuring CMake project..." (++$step) Assert-Command "cmake -B build -DCMAKE_BUILD_TYPE=Release" "CMake configure failed" - # ------------------------------------------------------------------ - # Build - # ------------------------------------------------------------------ Write-TestStep "Building C++ app..." (++$step) Assert-Command "cmake --build build --config Release" "CMake build failed" - $buildOutput = Join-Path $ctx.SampleDir "build\Release" - Assert-FileExists (Join-Path $buildOutput "cpp-app.exe") "cpp-app.exe" + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - # ------------------------------------------------------------------ - # Generate certificate and package MSIX - # ------------------------------------------------------------------ - Write-TestStep "Generating development certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath "devcert.pfx" Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack `"$buildOutput`" --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + Assert-Command "winapp pack build\Release --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "cpp-app MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide cpp-app MSIX" + + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Restoring sample SDK packages..." (++$step) + Assert-Command "winapp restore" "winapp restore failed" + + Write-TestStep "Building existing sample..." (++$step) + Assert-Command "cmake -B build -DCMAKE_BUILD_TYPE=Release" "Sample CMake configure failed" + Assert-Command "cmake --build build --config Release" "Sample CMake build failed" + Assert-FileExists "build\Release\cpp-app.exe" "cpp-app.exe" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue } } diff --git a/samples/dotnet-app/test.ps1 b/samples/dotnet-app/test.ps1 index ad809db8..704a8d07 100644 --- a/samples/dotnet-app/test.ps1 +++ b/samples/dotnet-app/test.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS -Test script for the dotnet-app sample. +Test script for the dotnet-app sample and .NET guide workflow. .DESCRIPTION -Builds the .NET console app in Release mode, which triggers the automatic -MSIX packaging MSBuild target, and validates the output. +Phase 1: Follows the docs/guides/dotnet.md guide from scratch — creates a new + .NET console project, runs winapp init, builds in Release (auto-packages MSIX). +Phase 2: Quick build of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,59 +27,73 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "dotnet-app" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" Assert-Prerequisite "npm" -DisplayName "npm" - # ------------------------------------------------------------------ # Install winapp globally (MSBuild targets call winapp directly) - # ------------------------------------------------------------------ Write-TestStep "Installing winapp CLI..." (++$step) $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Restore NuGet packages - # ------------------------------------------------------------------ - Write-TestStep "Restoring NuGet packages..." (++$step) - Assert-Command "dotnet restore" "dotnet restore failed" + # ================================================================== + # Phase 1 — Guide Workflow (from scratch) + # ================================================================== + Write-TestHeader "Phase 1: .NET Guide Workflow (from scratch)" + + $tempDir = New-TempTestDirectory -Prefix "dotnet-guide" + Push-Location $tempDir - # ------------------------------------------------------------------ - # Generate dev certificate (required by Release MSBuild target) - # ------------------------------------------------------------------ - Write-TestStep "Generating development certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + Write-TestStep "Creating new .NET console project..." (++$step) + Assert-Command "dotnet new console -n test-dotnet-app" "dotnet new console failed" + Push-Location "test-dotnet-app" + + Write-TestStep "Running winapp init..." (++$step) + Assert-Command "winapp init --use-defaults" "winapp init failed" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" Assert-FileExists "devcert.pfx" "Development certificate" - # ------------------------------------------------------------------ - # Build Release (triggers automatic MSIX packaging) - # ------------------------------------------------------------------ + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath "devcert.pfx" + Write-TestStep "Building in Release mode (auto-packages MSIX)..." (++$step) Assert-Command "dotnet build -c Release" "dotnet build -c Release failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "dotnet-app MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide dotnet-app MSIX" + + Pop-Location # back to tempDir + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Building existing sample (Debug, skip identity)..." (++$step) + Assert-Command "dotnet restore" "dotnet restore failed" + Assert-Command "dotnet build -c Debug /p:ApplyDebugIdentity=false" "Sample build failed" + Write-TestSuccess "dotnet-app sample builds successfully" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { - # Clean up generated artifacts (keep source files) + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path (Join-Path $ctx.SampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue } } diff --git a/samples/electron/test.ps1 b/samples/electron/test.ps1 index 7ca1be24..66b8377b 100644 --- a/samples/electron/test.ps1 +++ b/samples/electron/test.ps1 @@ -1,10 +1,12 @@ <# .SYNOPSIS -Test script for the electron sample. +Test script for the electron sample and Electron guide workflows. .DESCRIPTION -Installs dependencies, builds C++ and C# native addons, packages the -Electron app with Forge, generates a certificate, and creates an MSIX. +Phase 1: Follows the Electron setup + packaging guides from scratch — creates a + new Electron app, installs winapp, runs init, creates C++ and C# addons from + scratch, builds addons, packages with Forge, and creates an MSIX. +Phase 2: Quick npm install of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,101 +28,121 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "electron" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "node" -DisplayName "Node.js" Assert-Prerequisite "npm" -DisplayName "npm" Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" - # ------------------------------------------------------------------ + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + + # ================================================================== + # Phase 1 — Guide Workflow (from scratch) + # ================================================================== + Write-TestHeader "Phase 1: Electron Guide Workflow (from scratch)" + + $tempDir = New-TempTestDirectory -Prefix "electron-guide" + Push-Location $tempDir + # Set up npm cache to avoid ECOMPROMISED errors in CI - # ------------------------------------------------------------------ - $npmCacheDir = Join-Path $ctx.SampleDir ".npm-cache" + $npmCacheDir = Join-Path $tempDir ".npm-cache" $null = New-Item -ItemType Directory -Path $npmCacheDir -Force $env:npm_config_cache = $npmCacheDir - # ------------------------------------------------------------------ - # Install npm dependencies (skip postinstall to avoid debug-identity) - # ------------------------------------------------------------------ - Write-TestStep "Installing npm dependencies..." (++$step) - Assert-Command "npm install --ignore-scripts" "npm install failed" + Write-TestStep "Creating new Electron app..." (++$step) + $maxRetries = 3 + $created = $false + for ($i = 1; $i -le $maxRetries; $i++) { + Write-Verbose "Attempt $i of $maxRetries..." + if ($i -gt 1) { + Remove-Item -Path (Join-Path $tempDir "electron-app") -Recurse -Force -ErrorAction SilentlyContinue + npm cache clean --force 2>$null + Start-Sleep -Seconds 2 + } + Invoke-Expression "npx -y create-electron-app@7.11.1 electron-app --template=webpack" + if ($LASTEXITCODE -eq 0) { $created = $true; break } + } + if (-not $created) { throw "Failed to create Electron app after $maxRetries attempts" } + Write-TestSuccess "Electron app created" + + Push-Location "electron-app" + + # Configure package.json for MSIX + $pkgJson = Get-Content "package.json" | ConvertFrom-Json + $pkgJson | Add-Member -MemberType NoteProperty -Name "displayName" -Value "WinApp Test App" -Force + $pkgJson | Add-Member -MemberType NoteProperty -Name "description" -Value "Guide test" -Force + $pkgJson | ConvertTo-Json -Depth 10 | Set-Content "package.json" - # ------------------------------------------------------------------ - # Install winapp as local dev dependency - # ------------------------------------------------------------------ Write-TestStep "Installing winapp npm package..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappNpmPackage -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Initialize winapp workspace (restore SDKs, generate config) - # ------------------------------------------------------------------ - Write-TestStep "Initializing winapp workspace..." (++$step) + Write-TestStep "Running winapp init..." (++$step) Invoke-Winapp "init . --use-defaults --setup-sdks=stable" -FailMessage "winapp init failed" - Assert-DirectoryExists ".winapp" ".winapp directory" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest -ExpectDotWinapp + + Write-TestStep "Creating C++ addon from scratch..." (++$step) + Invoke-Winapp "node create-addon --template cpp --name testCppAddon" -FailMessage "create C++ addon failed" + Assert-DirectoryExists "testCppAddon" "C++ addon directory" + Assert-FileExists "testCppAddon\binding.gyp" "binding.gyp" - # ------------------------------------------------------------------ - # Build C++ addon - # ------------------------------------------------------------------ Write-TestStep "Building C++ addon..." (++$step) - Assert-Command "npm run build-addon" "C++ addon build failed" + Assert-Command "npm run build-testCppAddon" "C++ addon build failed" + + Write-TestStep "Creating C# addon from scratch..." (++$step) + Invoke-Winapp "node create-addon --template cs --name testCsAddon" -FailMessage "create C# addon failed" + Assert-DirectoryExists "testCsAddon" "C# addon directory" + Assert-FileExists "testCsAddon\testCsAddon.csproj" "C# addon csproj" - # ------------------------------------------------------------------ - # Build C# addon - # ------------------------------------------------------------------ Write-TestStep "Building C# addon..." (++$step) - Assert-Command "npm run build-csAddon" "C# addon build failed" + Assert-Command "npm run build-testCsAddon" "C# addon build failed" - # ------------------------------------------------------------------ - # Package Electron app with Forge - # ------------------------------------------------------------------ Write-TestStep "Packaging Electron app..." (++$step) Assert-Command "npm run package" "Electron packaging failed" - $outDir = Join-Path $ctx.SampleDir "out" + $outDir = Join-Path (Get-Location) "out" Assert-DirectoryExists $outDir "Electron output directory" + $appPackageDir = (Get-ChildItem -Path $outDir -Directory | Select-Object -First 1).FullName + Write-TestSuccess "Packaged to: $appPackageDir" - # Find the packaged app directory - $appPackageDirs = Get-ChildItem -Path $outDir -Directory -ErrorAction SilentlyContinue - if (-not $appPackageDirs) { - Write-TestError "No app package directories found in $outDir" - throw "Electron app packaging did not create output directory" - } - $appPackageDir = $appPackageDirs[0].FullName - Write-TestSuccess "Electron app packaged to: $appPackageDir" - - # ------------------------------------------------------------------ - # Generate certificate and package MSIX - # ------------------------------------------------------------------ - Write-TestStep "Generating development certificate..." (++$step) + Write-TestStep "Generating dev certificate..." (++$step) $certPath = New-DevCertificate + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath $certPath + Write-TestStep "Packaging as MSIX..." (++$step) Invoke-Winapp "pack `"$appPackageDir`" --cert `"$certPath`"" -FailMessage "winapp pack failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "electron MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide electron MSIX" + + Pop-Location # back to tempDir + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Installing sample dependencies..." (++$step) + Assert-Command "npm install --ignore-scripts" "npm install failed" + Assert-DirectoryExists "node_modules" "node_modules" + Write-TestSuccess "electron sample dependencies install successfully" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { - Remove-Item -Path (Join-Path $ctx.SampleDir "out") -Recurse -Force -ErrorAction SilentlyContinue + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir ".npm-cache") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue } } diff --git a/samples/flutter-app/test.ps1 b/samples/flutter-app/test.ps1 index 07f7a570..ee33a26d 100644 --- a/samples/flutter-app/test.ps1 +++ b/samples/flutter-app/test.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS -Test script for the flutter-app sample. +Test script for the flutter-app sample and Flutter guide workflow. .DESCRIPTION -Restores packages, builds the Flutter Windows desktop app, and packages -the output as an MSIX. +Phase 1: Follows the docs/guides/flutter.md guide from scratch — creates a new + Flutter project, runs winapp init, builds, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,74 +27,85 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "flutter-app" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "flutter" -DisplayName "Flutter SDK" Assert-Prerequisite "npm" -DisplayName "npm" - # ------------------------------------------------------------------ - # Install winapp globally - # ------------------------------------------------------------------ Write-TestStep "Installing winapp CLI..." (++$step) $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Restore packages - # ------------------------------------------------------------------ - Write-TestStep "Getting Flutter dependencies..." (++$step) - Assert-Command "flutter pub get" "flutter pub get failed" + # ================================================================== + # Phase 1 — Guide Workflow (from scratch) + # ================================================================== + Write-TestHeader "Phase 1: Flutter Guide Workflow (from scratch)" - Write-TestStep "Restoring Windows App SDK packages..." (++$step) - Assert-Command "winapp restore" "winapp restore failed" - Assert-DirectoryExists ".winapp" ".winapp directory" + $tempDir = New-TempTestDirectory -Prefix "flutter-guide" + Push-Location $tempDir + + Write-TestStep "Creating new Flutter project..." (++$step) + Assert-Command "flutter create test_flutter_app --platforms=windows" "flutter create failed" + Push-Location "test_flutter_app" + + Write-TestStep "Running winapp init..." (++$step) + Assert-Command "winapp init --use-defaults --setup-sdks=stable" "winapp init failed" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest -ExpectDotWinapp - # ------------------------------------------------------------------ - # Build Flutter Windows app - # ------------------------------------------------------------------ Write-TestStep "Building Flutter Windows app..." (++$step) Assert-Command "flutter build windows" "flutter build windows failed" - $buildOutput = Join-Path $ctx.SampleDir "build\windows\x64\runner\Release" + $buildOutput = "build\windows\x64\runner\Release" Assert-DirectoryExists $buildOutput "Flutter build output" - Assert-FileExists (Join-Path $buildOutput "flutter_app.exe") "flutter_app.exe" - # ------------------------------------------------------------------ - # Prepare distribution folder and package MSIX - # ------------------------------------------------------------------ Write-TestStep "Preparing distribution folder..." (++$step) - $distDir = Join-Path $ctx.SampleDir "dist" - if (Test-Path $distDir) { Remove-Item $distDir -Recurse -Force } - Copy-Item $buildOutput -Destination $distDir -Recurse + Copy-Item $buildOutput -Destination "dist" -Recurse + + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - Write-TestStep "Generating development certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath "devcert.pfx" Write-TestStep "Packaging as MSIX..." (++$step) Assert-Command "winapp pack dist --cert devcert.pfx" "winapp pack failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "flutter-app MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide flutter-app MSIX" + + Pop-Location # back to tempDir + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Getting sample Flutter dependencies..." (++$step) + Assert-Command "flutter pub get" "flutter pub get failed" + + Write-TestStep "Restoring sample SDK packages..." (++$step) + Assert-Command "winapp restore" "winapp restore failed" + + Write-TestStep "Building existing sample..." (++$step) + Assert-Command "flutter build windows" "Sample flutter build failed" + Assert-FileExists "build\windows\x64\runner\Release\flutter_app.exe" "flutter_app.exe" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "dist") -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue } } diff --git a/samples/packaging-cli/test.ps1 b/samples/packaging-cli/test.ps1 new file mode 100644 index 00000000..776fc6f6 --- /dev/null +++ b/samples/packaging-cli/test.ps1 @@ -0,0 +1,99 @@ +<# +.SYNOPSIS +Test script for the packaging-cli guide workflow. + +.DESCRIPTION +Follows docs/guides/packaging-cli.md from scratch — takes a pre-built CLI +executable, generates a manifest, creates a certificate, packages as MSIX, +and signs the package. Tests winapp manifest generate, cert generate, +cert info, pack, and sign commands. + +This test has no corresponding sample — it exists purely to validate the +generic "package any CLI" guide. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. + +.PARAMETER Verbose +Enable verbose output. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup, + [switch]$Verbose +) + +Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + +$ctx = New-SampleTestContext -SampleName "packaging-cli" -WinappPath $WinappPath -Verbose:$Verbose +$step = 0 +$tempDir = $null + +try { + # ================================================================== + # Prerequisites + # ================================================================== + Write-TestStep "Checking prerequisites..." (++$step) + Assert-Prerequisite "npm" -DisplayName "npm" + + Write-TestStep "Installing winapp CLI..." (++$step) + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + # ================================================================== + # Guide Workflow — Package a CLI Executable as MSIX + # ================================================================== + Write-TestHeader "Packaging CLI Guide Workflow" + + $tempDir = New-TempTestDirectory -Prefix "packaging-cli-guide" + Push-Location $tempDir + + # Create a minimal dummy executable (copy cmd.exe as stand-in) + Write-TestStep "Preparing dummy CLI executable..." (++$step) + $null = New-Item -ItemType Directory -Path "MyCliPackage" -Force + Copy-Item "$env:SystemRoot\System32\cmd.exe" -Destination "MyCliPackage\mycli.exe" + Assert-FileExists "MyCliPackage\mycli.exe" "Dummy CLI executable" + + Push-Location "MyCliPackage" + + # Generate manifest from executable (core guide step) + Write-TestStep "Generating manifest from executable..." (++$step) + Assert-Command "winapp manifest generate --executable mycli.exe" "winapp manifest generate failed" + Assert-FileExists "appxmanifest.xml" "Generated AppxManifest" + + # Generate certificate + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" + Assert-FileExists "devcert.pfx" "Development certificate" + + # Verify certificate info (guides show this for verification) + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath "devcert.pfx" + + # Package as MSIX + Write-TestStep "Packaging as MSIX..." (++$step) + Assert-Command "winapp pack . --cert devcert.pfx" "winapp pack failed" + + Write-TestStep "Validating MSIX output..." (++$step) + $msixPath = Assert-MsixCreated -Directory (Get-Location) -Description "Packaging-CLI MSIX" + + # Sign the MSIX (standalone sign command from usage.md) + Write-TestStep "Signing MSIX (standalone sign command)..." (++$step) + Assert-Command "winapp sign `"$msixPath`" --cert devcert.pfx" "winapp sign failed" + Write-TestSuccess "MSIX signed successfully" + + Pop-Location # back to tempDir + Pop-Location # back to original + + Complete-SampleTest -Context $ctx + +} finally { + Set-Location $ctx.SampleDir + if (-not $SkipCleanup) { + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } + } +} diff --git a/samples/rust-app/test.ps1 b/samples/rust-app/test.ps1 index 72459e6a..7bcbd563 100644 --- a/samples/rust-app/test.ps1 +++ b/samples/rust-app/test.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS -Test script for the rust-app sample. +Test script for the rust-app sample and Rust guide workflow. .DESCRIPTION -Builds the Rust app with Cargo, then packages the binary as an MSIX using -winapp pack. +Phase 1: Follows the docs/guides/rust.md guide from scratch — creates a new + Rust project, runs winapp init, builds, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,66 +27,77 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "rust-app" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "cargo" -DisplayName "Rust/Cargo" Assert-Prerequisite "npm" -DisplayName "npm" - # ------------------------------------------------------------------ - # Install winapp globally - # ------------------------------------------------------------------ Write-TestStep "Installing winapp CLI..." (++$step) $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Build Rust app - # ------------------------------------------------------------------ + # ================================================================== + # Phase 1 — Guide Workflow (from scratch) + # ================================================================== + Write-TestHeader "Phase 1: Rust Guide Workflow (from scratch)" + + $tempDir = New-TempTestDirectory -Prefix "rust-guide" + Push-Location $tempDir + + Write-TestStep "Creating new Rust project..." (++$step) + Assert-Command "cargo new test-rust-app" "cargo new failed" + Push-Location "test-rust-app" + + Write-TestStep "Running winapp init..." (++$step) + Assert-Command "winapp init --use-defaults --setup-sdks=none" "winapp init failed" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + Write-TestStep "Building Rust app (release)..." (++$step) Assert-Command "cargo build --release" "cargo build --release failed" + Assert-FileExists "target\release\test-rust-app.exe" "test-rust-app.exe" - $rustExe = Join-Path $ctx.SampleDir "target\release\rust-app.exe" - Assert-FileExists $rustExe "rust-app.exe" - - # ------------------------------------------------------------------ - # Prepare MSIX layout directory - # ------------------------------------------------------------------ Write-TestStep "Preparing MSIX layout..." (++$step) - $msixDir = Join-Path $ctx.SampleDir "msix" - if (Test-Path $msixDir) { Remove-Item $msixDir -Recurse -Force } - $null = New-Item -ItemType Directory -Path $msixDir -Force - Copy-Item $rustExe -Destination $msixDir + $null = New-Item -ItemType Directory -Path "dist" -Force + Copy-Item "target\release\test-rust-app.exe" -Destination "dist\" + + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - # ------------------------------------------------------------------ - # Generate certificate and package MSIX - # ------------------------------------------------------------------ - Write-TestStep "Generating development certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip --manifest appxmanifest.xml" "Failed to generate dev certificate" + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath "devcert.pfx" Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack msix --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + Assert-Command "winapp pack dist --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "rust-app MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide rust-app MSIX" + + Pop-Location # back to tempDir + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Building existing sample..." (++$step) + Assert-Command "cargo build" "Sample cargo build failed" + Assert-FileExists "target\debug\rust-app.exe" "rust-app.exe" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "target") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "msix") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue } } diff --git a/samples/tauri-app/test.ps1 b/samples/tauri-app/test.ps1 index 53d19ed9..09b9288a 100644 --- a/samples/tauri-app/test.ps1 +++ b/samples/tauri-app/test.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS -Test script for the tauri-app sample. +Test script for the tauri-app sample and Tauri guide workflow. .DESCRIPTION -Installs npm and Rust dependencies, builds the Tauri app, and packages the -output as an MSIX. +Phase 1: Follows the docs/guides/tauri.md guide — copies sample to temp dir, + installs deps, runs winapp init, builds, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,71 +27,86 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "tauri-app" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "node" -DisplayName "Node.js" Assert-Prerequisite "npm" -DisplayName "npm" Assert-Prerequisite "cargo" -DisplayName "Rust/Cargo" - # ------------------------------------------------------------------ - # Install winapp globally - # ------------------------------------------------------------------ Write-TestStep "Installing winapp CLI..." (++$step) $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Install npm dependencies - # ------------------------------------------------------------------ + # ================================================================== + # Phase 1 — Guide Workflow (copy sample to temp, run full flow) + # Tauri scaffolding via npm create tauri-app is interactive and slow, + # so we copy the existing sample as a starting point (matching the + # guide's "start from a Tauri template" step). + # ================================================================== + Write-TestHeader "Phase 1: Tauri Guide Workflow" + + $tempDir = New-TempTestDirectory -Prefix "tauri-guide" + $tempApp = Join-Path $tempDir "tauri-app" + + Write-TestStep "Copying sample to temp directory..." (++$step) + Copy-Item -Path $ctx.SampleDir -Destination $tempApp -Recurse -Exclude @('.gitignore', 'node_modules', 'src-tauri\target') + Push-Location $tempApp + Write-TestStep "Installing npm dependencies..." (++$step) Assert-Command "npm install" "npm install failed" - # ------------------------------------------------------------------ - # Build Tauri app (cargo build for the Rust backend) - # ------------------------------------------------------------------ - Write-TestStep "Building Tauri app..." (++$step) + Write-TestStep "Running winapp init..." (++$step) + Assert-Command "winapp init --use-defaults --setup-sdks=none" "winapp init failed" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + + Write-TestStep "Building Tauri app (release)..." (++$step) Assert-Command "cargo build --release --manifest-path src-tauri\Cargo.toml" "Tauri cargo build failed" - $tauriExe = Join-Path $ctx.SampleDir "src-tauri\target\release\tauri-app.exe" + $tauriExe = Join-Path $tempApp "src-tauri\target\release\tauri-app.exe" Assert-FileExists $tauriExe "tauri-app.exe" - # ------------------------------------------------------------------ - # Generate certificate and package MSIX - # ------------------------------------------------------------------ - Write-TestStep "Generating development certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip --manifest appxmanifest.xml" "Failed to generate dev certificate" - Write-TestStep "Preparing MSIX layout..." (++$step) - $msixDir = Join-Path $ctx.SampleDir "msix-layout" - if (Test-Path $msixDir) { Remove-Item $msixDir -Recurse -Force } - $null = New-Item -ItemType Directory -Path $msixDir -Force - Copy-Item $tauriExe -Destination $msixDir + $null = New-Item -ItemType Directory -Path "msix-layout" -Force + Copy-Item $tauriExe -Destination "msix-layout\" + + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip --manifest appxmanifest.xml" "cert generate failed" Write-TestStep "Packaging as MSIX..." (++$step) Assert-Command "winapp pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "tauri-app MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide tauri-app MSIX" + + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Installing sample npm dependencies..." (++$step) + Assert-Command "npm install" "npm install failed" + + Write-TestStep "Building sample Rust backend..." (++$step) + Assert-Command "cargo build --manifest-path src-tauri\Cargo.toml" "Sample cargo build failed" + Assert-FileExists "src-tauri\target\debug\tauri-app.exe" "tauri-app.exe" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { - Remove-Item -Path (Join-Path $ctx.SampleDir "src-tauri\target") -Recurse -Force -ErrorAction SilentlyContinue + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "msix-layout") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $ctx.SampleDir "src-tauri\target") -Recurse -Force -ErrorAction SilentlyContinue } } diff --git a/samples/wpf-app/test.ps1 b/samples/wpf-app/test.ps1 index 69d184f4..3f5122aa 100644 --- a/samples/wpf-app/test.ps1 +++ b/samples/wpf-app/test.ps1 @@ -1,10 +1,11 @@ <# .SYNOPSIS -Test script for the wpf-app sample. +Test script for the wpf-app sample and WPF guide workflow. .DESCRIPTION -Builds the WPF app in Release mode, which triggers the automatic MSIX -packaging MSBuild target, and validates the output. +Phase 1: Creates a new WPF project from scratch, runs winapp init, builds in + Release with RID (auto-packages MSIX). +Phase 2: Quick build of the existing sample to verify it is not stale. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. @@ -26,59 +27,72 @@ Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $ctx = New-SampleTestContext -SampleName "wpf-app" -WinappPath $WinappPath -Verbose:$Verbose $step = 0 +$tempDir = $null try { - Push-Location $ctx.SampleDir - - # ------------------------------------------------------------------ + # ================================================================== # Prerequisites - # ------------------------------------------------------------------ + # ================================================================== Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" Assert-Prerequisite "npm" -DisplayName "npm" - # ------------------------------------------------------------------ - # Install winapp globally (MSBuild targets call winapp directly) - # ------------------------------------------------------------------ Write-TestStep "Installing winapp CLI..." (++$step) $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - # ------------------------------------------------------------------ - # Restore NuGet packages - # ------------------------------------------------------------------ - Write-TestStep "Restoring NuGet packages..." (++$step) - Assert-Command "dotnet restore" "dotnet restore failed" + # ================================================================== + # Phase 1 — Guide Workflow (from scratch) + # ================================================================== + Write-TestHeader "Phase 1: WPF Guide Workflow (from scratch)" + + $tempDir = New-TempTestDirectory -Prefix "wpf-guide" + Push-Location $tempDir - # ------------------------------------------------------------------ - # Generate dev certificate (required by Release MSBuild target) - # ------------------------------------------------------------------ - Write-TestStep "Generating development certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "Failed to generate dev certificate" + Write-TestStep "Creating new WPF project..." (++$step) + Assert-Command "dotnet new wpf -n test-wpf-app" "dotnet new wpf failed" + Push-Location "test-wpf-app" + + Write-TestStep "Running winapp init..." (++$step) + Assert-Command "winapp init --use-defaults" "winapp init failed" + Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + + Write-TestStep "Generating dev certificate..." (++$step) + Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" Assert-FileExists "devcert.pfx" "Development certificate" - # ------------------------------------------------------------------ - # Build Release (triggers automatic MSIX packaging) - # WPF requires a platform-specific RID — not AnyCPU - # ------------------------------------------------------------------ - Write-TestStep "Building in Release mode (auto-packages MSIX)..." (++$step) + Write-TestStep "Verifying certificate info..." (++$step) + Assert-CertInfo -CertPath "devcert.pfx" + + Write-TestStep "Building in Release mode with RID (auto-packages MSIX)..." (++$step) Assert-Command "dotnet build -c Release -r win-x64" "dotnet build -c Release -r win-x64 failed" - # ------------------------------------------------------------------ - # Validate MSIX was created - # ------------------------------------------------------------------ Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory $ctx.SampleDir -Description "wpf-app MSIX package" + Assert-MsixCreated -Directory (Get-Location) -Description "Guide wpf-app MSIX" + + Pop-Location # back to tempDir + Pop-Location # back to original + + # ================================================================== + # Phase 2 — Sample Build Check + # ================================================================== + Write-TestHeader "Phase 2: Sample Build Check" + Push-Location $ctx.SampleDir + + Write-TestStep "Building existing sample (Debug, skip identity)..." (++$step) + Assert-Command "dotnet restore" "dotnet restore failed" + Assert-Command "dotnet build -c Debug /p:ApplyDebugIdentity=false" "Sample build failed" + Write-TestSuccess "wpf-app sample builds successfully" + + Pop-Location Complete-SampleTest -Context $ctx } finally { - Pop-Location + Set-Location $ctx.SampleDir if (-not $SkipCleanup) { + if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -Path (Join-Path $ctx.SampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "devcert.pfx") -Force -ErrorAction SilentlyContinue - Get-ChildItem -Path $ctx.SampleDir -Filter "*.msix" -ErrorAction SilentlyContinue | - Remove-Item -Force -ErrorAction SilentlyContinue } } From ffeb3eab83dc9a2e435d638dac33b08678993b47 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Sat, 7 Mar 2026 08:39:03 -0800 Subject: [PATCH 03/27] Fix test bugs, simplify electron test, update naming to Sample & Guide - Fix [switch]$Verbose conflict with PowerShell common parameter in all test scripts and orchestrator (use [CmdletBinding()] instead) - Fix $PSScriptRoot in module functions pointing to wrong directory (pass -SampleDir $PSScriptRoot from callers) - Fix Assert-WinappInitOutput defaults (switches default to $false) - Remove -ExpectWinappYaml for .NET and --setup-sdks=none projects (.NET uses .csproj, Rust/Tauri with no SDKs skip winapp.yaml) - Fix dotnet/wpf tests to explicitly call winapp pack after build (auto-packaging MSBuild targets are optional, not added by init) - Find .exe output directory recursively (handles RID subdirectories) - Fix winapp sign syntax: positional args, not --cert flag - Simplify electron test to sample freshness check only (from-scratch Electron guide workflow is covered by E2E test in test-e2e-electron.ps1) - Rename workflow and headings to 'Sample & Guide' - Update CI matrix: packaging-cli needs Node.js, electron no longer needs .NET Validated locally: packaging-cli, dotnet-app, rust-app, electron, wpf-app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-samples.yml | 6 +- AGENTS.md | 16 ++-- samples/SampleTestHelpers.psm1 | 28 ++++--- samples/cpp-app/test.ps1 | 9 +-- samples/dotnet-app/test.ps1 | 21 +++-- samples/electron/test.ps1 | 118 ++++------------------------- samples/flutter-app/test.ps1 | 9 +-- samples/packaging-cli/test.ps1 | 11 +-- samples/rust-app/test.ps1 | 11 +-- samples/tauri-app/test.ps1 | 11 +-- samples/wpf-app/test.ps1 | 21 +++-- scripts/test-samples.ps1 | 24 +++--- 12 files changed, 100 insertions(+), 185 deletions(-) diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index cc50ea5e..d5b213e5 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -1,4 +1,4 @@ -name: Test Samples +name: Test Samples & Guides on: workflow_run: @@ -82,7 +82,7 @@ jobs: - name: Setup .NET if: >- steps.check.outputs.skip != 'true' && - contains(fromJson('["dotnet-app", "wpf-app", "electron"]'), matrix.sample) + contains(fromJson('["dotnet-app", "wpf-app", "packaging-cli"]'), matrix.sample) uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0.x' @@ -90,7 +90,7 @@ jobs: - name: Setup Node.js if: >- steps.check.outputs.skip != 'true' && - contains(fromJson('["electron", "tauri-app", "cpp-app", "dotnet-app", "wpf-app", "rust-app", "flutter-app"]'), matrix.sample) + contains(fromJson('["electron", "tauri-app", "cpp-app", "dotnet-app", "wpf-app", "rust-app", "flutter-app", "packaging-cli"]'), matrix.sample) uses: actions/setup-node@v5 with: node-version: '24' diff --git a/AGENTS.md b/AGENTS.md index a612222c..57875585 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,11 +47,11 @@ When adding or changing public facing features, ensure all documentation is also If a feature is big enough and requires its own docs page, add it under docs\ -## Sample testing +## Sample & guide testing -Each sample under `samples/` has a self-contained `test.ps1` that validates the sample builds and packages correctly. Tests share infrastructure via `samples/SampleTestHelpers.psm1`. +Each sample under `samples/` has a self-contained `test.ps1` that validates the corresponding guide workflow from scratch (Phase 1) and verifies the existing sample code still builds (Phase 2). Tests share infrastructure via `samples/SampleTestHelpers.psm1`. -### Running sample tests locally +### Running sample & guide tests locally ```powershell # Run all sample tests @@ -64,18 +64,18 @@ Each sample under `samples/` has a self-contained `test.ps1` that validates the .\scripts\test-samples.ps1 -WinappPath .\artifacts\npm -Verbose ``` -### Writing a new sample test +### Writing a new sample & guide test 1. Create `test.ps1` in the sample directory 2. Import the shared helpers: `Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force` -3. Use `New-SampleTestContext` to initialize, `Complete-SampleTest` to finalize -4. Follow the pattern: prerequisites → install winapp → build → package MSIX → validate -5. Clean up generated artifacts in a `finally` block (unless `-SkipCleanup`) +3. Use `New-SampleTestContext -SampleDir $PSScriptRoot` to initialize, `Complete-SampleTest` to finalize +4. Phase 1: from-scratch guide workflow in a temp directory (scaffold, winapp init, build, cert, pack) +5. Phase 2: quick build of existing sample code to verify freshness 6. Add the sample name to the matrix in `.github/workflows/test-samples.yml` ### CI integration -Sample tests run via `.github/workflows/test-samples.yml` using a GitHub Actions matrix strategy. Each sample runs in its own parallel job after the main build completes. The workflow downloads the npm package artifact from the `Build and Package` workflow. +Sample & guide tests run via `.github/workflows/test-samples.yml` using a GitHub Actions matrix strategy. Each sample runs in its own parallel job after the main build completes. The workflow downloads the npm package artifact from the `Build and Package` workflow. ## Where to look first diff --git a/samples/SampleTestHelpers.psm1 b/samples/SampleTestHelpers.psm1 index 7a93dbad..f2844304 100644 --- a/samples/SampleTestHelpers.psm1 +++ b/samples/SampleTestHelpers.psm1 @@ -268,32 +268,36 @@ function New-SampleTestContext { .DESCRIPTION Sets strict mode, resolves the sample directory, and prepares the context - object used by all sample tests. Does NOT create temporary directories — - sample tests run in-place against the sample's own source directory. + object used by all sample tests. + + .PARAMETER SampleDir + The directory of the calling test.ps1 script. Pass $PSScriptRoot from the + test script (the module's $PSScriptRoot points to the module directory, not + the caller). #> + [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SampleName, - [string]$WinappPath, - [switch]$Verbose + [Parameter(Mandatory)] + [string]$SampleDir, + [string]$WinappPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - if ($Verbose) { $VerbosePreference = 'Continue' } - $sampleDir = $PSScriptRoot # test.ps1 lives alongside sample files - $repoRoot = (Resolve-Path "$sampleDir\..\..").Path + $repoRoot = (Resolve-Path "$SampleDir\..\..").Path $ctx = @{ SampleName = $SampleName - SampleDir = $sampleDir + SampleDir = $SampleDir RepoRoot = $repoRoot WinappPath = $WinappPath StartTime = Get-Date } - Write-TestHeader "$SampleName Sample Test" + Write-TestHeader "$SampleName — Sample & Guide Test" Write-Verbose "Sample directory: $($ctx.SampleDir)" Write-Verbose "Repo root: $($ctx.RepoRoot)" @@ -311,7 +315,7 @@ function Complete-SampleTest { ) $elapsed = (Get-Date) - $Context.StartTime Write-Host "`n$('='*80)" -ForegroundColor Green - Write-Host "$($Context.SampleName) SAMPLE TEST COMPLETED SUCCESSFULLY ($([math]::Round($elapsed.TotalSeconds, 1))s)" -ForegroundColor Green + Write-Host "$($Context.SampleName) TEST COMPLETED SUCCESSFULLY ($([math]::Round($elapsed.TotalSeconds, 1))s)" -ForegroundColor Green Write-Host "$('='*80)`n" -ForegroundColor Green } @@ -397,8 +401,8 @@ function Assert-WinappInitOutput { #> param( [string]$Directory = (Get-Location), - [switch]$ExpectWinappYaml = $true, - [switch]$ExpectManifest = $true, + [switch]$ExpectWinappYaml, + [switch]$ExpectManifest, [switch]$ExpectDotWinapp ) if ($ExpectWinappYaml) { diff --git a/samples/cpp-app/test.ps1 b/samples/cpp-app/test.ps1 index 3680be20..93e79b8a 100644 --- a/samples/cpp-app/test.ps1 +++ b/samples/cpp-app/test.ps1 @@ -12,20 +12,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "cpp-app" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "cpp-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null diff --git a/samples/dotnet-app/test.ps1 b/samples/dotnet-app/test.ps1 index 704a8d07..9f31ea46 100644 --- a/samples/dotnet-app/test.ps1 +++ b/samples/dotnet-app/test.ps1 @@ -12,20 +12,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "dotnet-app" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "dotnet-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null @@ -56,7 +53,7 @@ try { Write-TestStep "Running winapp init..." (++$step) Assert-Command "winapp init --use-defaults" "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + Assert-WinappInitOutput -ExpectManifest Write-TestStep "Generating dev certificate..." (++$step) Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" @@ -65,9 +62,17 @@ try { Write-TestStep "Verifying certificate info..." (++$step) Assert-CertInfo -CertPath "devcert.pfx" - Write-TestStep "Building in Release mode (auto-packages MSIX)..." (++$step) + Write-TestStep "Building in Release mode..." (++$step) Assert-Command "dotnet build -c Release" "dotnet build -c Release failed" + # Find the actual output directory containing the exe (handles TFM + RID subdirs) + $exeFile = Get-ChildItem -Path "bin\Release" -Filter "*.exe" -Recurse | Select-Object -First 1 + if (-not $exeFile) { throw "No .exe found in Release output" } + $outputDir = $exeFile.DirectoryName + + Write-TestStep "Packaging MSIX with winapp pack..." (++$step) + Assert-Command "winapp pack `"$outputDir`" --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + Write-TestStep "Validating MSIX output..." (++$step) Assert-MsixCreated -Directory (Get-Location) -Description "Guide dotnet-app MSIX" diff --git a/samples/electron/test.ps1 b/samples/electron/test.ps1 index 66b8377b..d2416dda 100644 --- a/samples/electron/test.ps1 +++ b/samples/electron/test.ps1 @@ -1,34 +1,30 @@ <# .SYNOPSIS -Test script for the electron sample and Electron guide workflows. +Test script for the electron sample freshness check. .DESCRIPTION -Phase 1: Follows the Electron setup + packaging guides from scratch — creates a - new Electron app, installs winapp, runs init, creates C++ and C# addons from - scratch, builds addons, packages with Forge, and creates an MSIX. -Phase 2: Quick npm install of the existing sample to verify it is not stale. +Verifies the existing samples/electron code is not stale by installing +dependencies and validating structure. The from-scratch Electron guide +workflow (init, addon creation, Forge packaging, MSIX) is covered by +the dedicated E2E test in scripts/test-e2e-electron.ps1. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "electron" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "electron" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 -$tempDir = $null try { # ================================================================== @@ -37,103 +33,22 @@ try { Write-TestStep "Checking prerequisites..." (++$step) Assert-Prerequisite "node" -DisplayName "Node.js" Assert-Prerequisite "npm" -DisplayName "npm" - Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" - - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath # ================================================================== - # Phase 1 — Guide Workflow (from scratch) + # Sample Build Check # ================================================================== - Write-TestHeader "Phase 1: Electron Guide Workflow (from scratch)" - - $tempDir = New-TempTestDirectory -Prefix "electron-guide" - Push-Location $tempDir - - # Set up npm cache to avoid ECOMPROMISED errors in CI - $npmCacheDir = Join-Path $tempDir ".npm-cache" - $null = New-Item -ItemType Directory -Path $npmCacheDir -Force - $env:npm_config_cache = $npmCacheDir - - Write-TestStep "Creating new Electron app..." (++$step) - $maxRetries = 3 - $created = $false - for ($i = 1; $i -le $maxRetries; $i++) { - Write-Verbose "Attempt $i of $maxRetries..." - if ($i -gt 1) { - Remove-Item -Path (Join-Path $tempDir "electron-app") -Recurse -Force -ErrorAction SilentlyContinue - npm cache clean --force 2>$null - Start-Sleep -Seconds 2 - } - Invoke-Expression "npx -y create-electron-app@7.11.1 electron-app --template=webpack" - if ($LASTEXITCODE -eq 0) { $created = $true; break } - } - if (-not $created) { throw "Failed to create Electron app after $maxRetries attempts" } - Write-TestSuccess "Electron app created" - - Push-Location "electron-app" - - # Configure package.json for MSIX - $pkgJson = Get-Content "package.json" | ConvertFrom-Json - $pkgJson | Add-Member -MemberType NoteProperty -Name "displayName" -Value "WinApp Test App" -Force - $pkgJson | Add-Member -MemberType NoteProperty -Name "description" -Value "Guide test" -Force - $pkgJson | ConvertTo-Json -Depth 10 | Set-Content "package.json" - - Write-TestStep "Installing winapp npm package..." (++$step) - Install-WinappNpmPackage -PackagePath $resolvedPkg - - Write-TestStep "Running winapp init..." (++$step) - Invoke-Winapp "init . --use-defaults --setup-sdks=stable" -FailMessage "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest -ExpectDotWinapp - - Write-TestStep "Creating C++ addon from scratch..." (++$step) - Invoke-Winapp "node create-addon --template cpp --name testCppAddon" -FailMessage "create C++ addon failed" - Assert-DirectoryExists "testCppAddon" "C++ addon directory" - Assert-FileExists "testCppAddon\binding.gyp" "binding.gyp" - - Write-TestStep "Building C++ addon..." (++$step) - Assert-Command "npm run build-testCppAddon" "C++ addon build failed" - - Write-TestStep "Creating C# addon from scratch..." (++$step) - Invoke-Winapp "node create-addon --template cs --name testCsAddon" -FailMessage "create C# addon failed" - Assert-DirectoryExists "testCsAddon" "C# addon directory" - Assert-FileExists "testCsAddon\testCsAddon.csproj" "C# addon csproj" - - Write-TestStep "Building C# addon..." (++$step) - Assert-Command "npm run build-testCsAddon" "C# addon build failed" - - Write-TestStep "Packaging Electron app..." (++$step) - Assert-Command "npm run package" "Electron packaging failed" - - $outDir = Join-Path (Get-Location) "out" - Assert-DirectoryExists $outDir "Electron output directory" - $appPackageDir = (Get-ChildItem -Path $outDir -Directory | Select-Object -First 1).FullName - Write-TestSuccess "Packaged to: $appPackageDir" - - Write-TestStep "Generating dev certificate..." (++$step) - $certPath = New-DevCertificate - - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath $certPath - - Write-TestStep "Packaging as MSIX..." (++$step) - Invoke-Winapp "pack `"$appPackageDir`" --cert `"$certPath`"" -FailMessage "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide electron MSIX" - - Pop-Location # back to tempDir - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" + Write-TestHeader "Electron Sample Freshness Check" Push-Location $ctx.SampleDir Write-TestStep "Installing sample dependencies..." (++$step) Assert-Command "npm install --ignore-scripts" "npm install failed" Assert-DirectoryExists "node_modules" "node_modules" - Write-TestSuccess "electron sample dependencies install successfully" + + Write-TestStep "Verifying sample structure..." (++$step) + Assert-FileExists "package.json" "package.json" + Assert-FileExists "forge.config.js" "forge.config.js" + Assert-FileExists "appxmanifest.xml" "appxmanifest.xml" + Write-TestSuccess "electron sample is valid and installable" Pop-Location @@ -142,7 +57,6 @@ try { } finally { Set-Location $ctx.SampleDir if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue } } diff --git a/samples/flutter-app/test.ps1 b/samples/flutter-app/test.ps1 index ee33a26d..9d8b0b97 100644 --- a/samples/flutter-app/test.ps1 +++ b/samples/flutter-app/test.ps1 @@ -12,20 +12,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "flutter-app" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "flutter-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null diff --git a/samples/packaging-cli/test.ps1 b/samples/packaging-cli/test.ps1 index 776fc6f6..94007f35 100644 --- a/samples/packaging-cli/test.ps1 +++ b/samples/packaging-cli/test.ps1 @@ -16,20 +16,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "packaging-cli" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "packaging-cli" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null @@ -83,7 +80,7 @@ try { # Sign the MSIX (standalone sign command from usage.md) Write-TestStep "Signing MSIX (standalone sign command)..." (++$step) - Assert-Command "winapp sign `"$msixPath`" --cert devcert.pfx" "winapp sign failed" + Assert-Command "winapp sign `"$msixPath`" devcert.pfx" "winapp sign failed" Write-TestSuccess "MSIX signed successfully" Pop-Location # back to tempDir diff --git a/samples/rust-app/test.ps1 b/samples/rust-app/test.ps1 index 7bcbd563..d717560a 100644 --- a/samples/rust-app/test.ps1 +++ b/samples/rust-app/test.ps1 @@ -12,20 +12,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "rust-app" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "rust-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null @@ -55,7 +52,7 @@ try { Write-TestStep "Running winapp init..." (++$step) Assert-Command "winapp init --use-defaults --setup-sdks=none" "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + Assert-WinappInitOutput -ExpectManifest Write-TestStep "Building Rust app (release)..." (++$step) Assert-Command "cargo build --release" "cargo build --release failed" diff --git a/samples/tauri-app/test.ps1 b/samples/tauri-app/test.ps1 index 09b9288a..6e9111cd 100644 --- a/samples/tauri-app/test.ps1 +++ b/samples/tauri-app/test.ps1 @@ -12,20 +12,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "tauri-app" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "tauri-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null @@ -62,7 +59,7 @@ try { Write-TestStep "Running winapp init..." (++$step) Assert-Command "winapp init --use-defaults --setup-sdks=none" "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + Assert-WinappInitOutput -ExpectManifest Write-TestStep "Building Tauri app (release)..." (++$step) Assert-Command "cargo build --release --manifest-path src-tauri\Cargo.toml" "Tauri cargo build failed" diff --git a/samples/wpf-app/test.ps1 b/samples/wpf-app/test.ps1 index 3f5122aa..264fe278 100644 --- a/samples/wpf-app/test.ps1 +++ b/samples/wpf-app/test.ps1 @@ -12,20 +12,17 @@ Path to the winapp npm package (.tgz or directory) to install. .PARAMETER SkipCleanup Keep generated artifacts after test completes. - -.PARAMETER Verbose -Enable verbose output. #> +[CmdletBinding()] param( [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force -$ctx = New-SampleTestContext -SampleName "wpf-app" -WinappPath $WinappPath -Verbose:$Verbose +$ctx = New-SampleTestContext -SampleName "wpf-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference $step = 0 $tempDir = $null @@ -55,7 +52,7 @@ try { Write-TestStep "Running winapp init..." (++$step) Assert-Command "winapp init --use-defaults" "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest + Assert-WinappInitOutput -ExpectManifest Write-TestStep "Generating dev certificate..." (++$step) Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" @@ -64,9 +61,17 @@ try { Write-TestStep "Verifying certificate info..." (++$step) Assert-CertInfo -CertPath "devcert.pfx" - Write-TestStep "Building in Release mode with RID (auto-packages MSIX)..." (++$step) + Write-TestStep "Building in Release mode with RID..." (++$step) Assert-Command "dotnet build -c Release -r win-x64" "dotnet build -c Release -r win-x64 failed" + # Find the actual output directory containing the exe + $exeFile = Get-ChildItem -Path "bin\Release" -Filter "*.exe" -Recurse | Select-Object -First 1 + if (-not $exeFile) { throw "No .exe found in Release output" } + $outputDir = $exeFile.DirectoryName + + Write-TestStep "Packaging MSIX with winapp pack..." (++$step) + Assert-Command "winapp pack `"$outputDir`" --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" + Write-TestStep "Validating MSIX output..." (++$step) Assert-MsixCreated -Directory (Get-Location) -Description "Guide wpf-app MSIX" diff --git a/scripts/test-samples.ps1 b/scripts/test-samples.ps1 index 6adfd5eb..780b9e6d 100644 --- a/scripts/test-samples.ps1 +++ b/scripts/test-samples.ps1 @@ -1,9 +1,11 @@ <# .SYNOPSIS -Local orchestrator to run sample tests. +Local orchestrator to run sample & guide tests. .DESCRIPTION Discovers and runs test.ps1 for each sample (or a specified subset). +Each test validates the corresponding guide workflow from scratch and +verifies the existing sample code still builds. Reports a pass/fail summary at the end. .PARAMETER Samples @@ -27,11 +29,11 @@ Run all sample tests. Run only the dotnet-app and rust-app tests with verbose output. #> +[CmdletBinding()] param( [string[]]$Samples, [string]$WinappPath, - [switch]$SkipCleanup, - [switch]$Verbose + [switch]$SkipCleanup ) Set-StrictMode -Version Latest @@ -40,9 +42,9 @@ $ErrorActionPreference = 'Stop' $samplesRoot = Join-Path $PSScriptRoot "..\samples" # Discover samples with test.ps1 -$allTests = Get-ChildItem -Path $samplesRoot -Directory | +$allTests = @(Get-ChildItem -Path $samplesRoot -Directory | Where-Object { Test-Path (Join-Path $_.FullName "test.ps1") } | - Select-Object -ExpandProperty Name + Select-Object -ExpandProperty Name) if ($Samples) { # Validate requested samples exist @@ -51,7 +53,7 @@ if ($Samples) { Write-Warning "Sample '$s' does not have a test.ps1 — skipping" } } - $testList = $Samples | Where-Object { $_ -in $allTests } + $testList = @($Samples | Where-Object { $_ -in $allTests }) } else { $testList = $allTests } @@ -62,7 +64,7 @@ if (-not $testList) { } Write-Host "`n$('='*80)" -ForegroundColor Cyan -Write-Host "SAMPLE TEST RUNNER — $($testList.Count) sample(s)" -ForegroundColor Cyan +Write-Host "SAMPLE & GUIDE TEST RUNNER — $($testList.Count) test(s)" -ForegroundColor Cyan Write-Host "$('='*80)`n" -ForegroundColor Cyan $results = @() @@ -77,7 +79,7 @@ foreach ($sample in $testList) { $params = @{} if ($WinappPath) { $params['WinappPath'] = $WinappPath } if ($SkipCleanup) { $params['SkipCleanup'] = $true } - if ($Verbose) { $params['Verbose'] = $true } + if ($VerbosePreference -eq 'Continue') { $params['Verbose'] = $true } & $testScript @params $sw.Stop() @@ -94,8 +96,8 @@ Write-Host "`n$('='*80)" -ForegroundColor Cyan Write-Host "RESULTS SUMMARY" -ForegroundColor Cyan Write-Host "$('='*80)" -ForegroundColor Cyan -$passed = ($results | Where-Object Status -eq 'PASS').Count -$failed = ($results | Where-Object Status -eq 'FAIL').Count +$passed = @($results | Where-Object Status -eq 'PASS').Count +$failed = @($results | Where-Object Status -eq 'FAIL').Count foreach ($r in $results) { $color = if ($r.Status -eq 'PASS') { 'Green' } else { 'Red' } @@ -107,7 +109,7 @@ foreach ($r in $results) { } } -Write-Host "`n $passed passed, $failed failed out of $($results.Count) samples`n" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'Green' }) +Write-Host "`n $passed passed, $failed failed out of $($results.Count) test(s)`n" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'Green' }) if ($failed -gt 0) { exit 1 From ab586309a749a8df646c5aab6f74ead23667bef0 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:25:06 -0800 Subject: [PATCH 04/27] Migrate sample & guide tests to Pester 5.x framework - Replace 8 raw PowerShell test.ps1 files with Pester test.Tests.ps1 files - Simplify SampleTestHelpers.psm1 from ~460 to ~180 lines (Pester handles assertions/reporting) - Use BeforeDiscovery + BeforeAll dual-phase pattern for prerequisite skip logic - Update orchestrator (scripts/test-samples.ps1) as thin Pester wrapper with comma-split support - Update CI workflow for Invoke-Pester with JUnit XML test result reporting - Update AGENTS.md with Pester conventions for sample tests - Fix winapp cert info positional arg syntax in wpf-app test - Resolve WinappPath to absolute in orchestrator before passing to containers All 8 sample tests validated locally: packaging-cli: 10/10, electron: 7/7, dotnet-app: 15/15, rust-app: 15/15, wpf-app: 10/10, tauri-app: 17/17, cpp-app: 8 skipped (no cmake), flutter-app: 7 skipped (no flutter) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-samples.yml | 29 ++- AGENTS.md | 25 +- samples/SampleTestHelpers.psm1 | 352 +++------------------------ samples/cpp-app/test.Tests.ps1 | 134 ++++++++++ samples/cpp-app/test.ps1 | 116 --------- samples/dotnet-app/test.Tests.ps1 | 148 +++++++++++ samples/dotnet-app/test.ps1 | 104 -------- samples/electron/test.Tests.ps1 | 71 ++++++ samples/electron/test.ps1 | 62 ----- samples/flutter-app/test.Tests.ps1 | 115 +++++++++ samples/flutter-app/test.ps1 | 108 -------- samples/packaging-cli/test.Tests.ps1 | 103 ++++++++ samples/packaging-cli/test.ps1 | 96 -------- samples/rust-app/test.Tests.ps1 | 133 ++++++++++ samples/rust-app/test.ps1 | 100 -------- samples/tauri-app/test.Tests.ps1 | 147 +++++++++++ samples/tauri-app/test.ps1 | 109 --------- samples/wpf-app/test.Tests.ps1 | 123 ++++++++++ samples/wpf-app/test.ps1 | 103 -------- scripts/test-samples.ps1 | 103 ++++---- 20 files changed, 1092 insertions(+), 1189 deletions(-) create mode 100644 samples/cpp-app/test.Tests.ps1 delete mode 100644 samples/cpp-app/test.ps1 create mode 100644 samples/dotnet-app/test.Tests.ps1 delete mode 100644 samples/dotnet-app/test.ps1 create mode 100644 samples/electron/test.Tests.ps1 delete mode 100644 samples/electron/test.ps1 create mode 100644 samples/flutter-app/test.Tests.ps1 delete mode 100644 samples/flutter-app/test.ps1 create mode 100644 samples/packaging-cli/test.Tests.ps1 delete mode 100644 samples/packaging-cli/test.ps1 create mode 100644 samples/rust-app/test.Tests.ps1 delete mode 100644 samples/rust-app/test.ps1 create mode 100644 samples/tauri-app/test.Tests.ps1 delete mode 100644 samples/tauri-app/test.ps1 create mode 100644 samples/wpf-app/test.Tests.ps1 delete mode 100644 samples/wpf-app/test.ps1 diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index d5b213e5..3aee8be2 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -109,7 +109,13 @@ jobs: contains(fromJson('["rust-app", "tauri-app"]'), matrix.sample) uses: dtolnay/rust-toolchain@stable - # --- Run the sample's self-contained test --- + # --- Run the sample's self-contained Pester test --- + + - name: Install Pester + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser -MinimumVersion 5.0 - name: Run ${{ matrix.sample }} test if: steps.check.outputs.skip != 'true' @@ -117,10 +123,27 @@ jobs: run: | $winappPath = "artifacts/npm" if (-not (Test-Path $winappPath)) { - # Fallback: use the local source if no artifact was downloaded $winappPath = "src/winapp-npm" } - .\samples\${{ matrix.sample }}\test.ps1 -WinappPath $winappPath -Verbose + $container = New-PesterContainer -Path "samples/${{ matrix.sample }}/test.Tests.ps1" -Data @{ + WinappPath = $winappPath + } + $config = New-PesterConfiguration + $config.Run.Container = $container + $config.Run.Exit = $true + $config.Output.Verbosity = 'Detailed' + $config.TestResult.Enabled = $true + $config.TestResult.OutputPath = "test-results-${{ matrix.sample }}.xml" + $config.TestResult.OutputFormat = 'JUnitXml' + Invoke-Pester -Configuration $config + + - name: Upload test results + if: always() && steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.sample }} + path: test-results-${{ matrix.sample }}.xml + if-no-files-found: ignore # Summary job to provide a single check status for branch protection test-samples-result: diff --git a/AGENTS.md b/AGENTS.md index 57875585..70af3329 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ If a feature is big enough and requires its own docs page, add it under docs\ ## Sample & guide testing -Each sample under `samples/` has a self-contained `test.ps1` that validates the corresponding guide workflow from scratch (Phase 1) and verifies the existing sample code still builds (Phase 2). Tests share infrastructure via `samples/SampleTestHelpers.psm1`. +Each sample under `samples/` has a self-contained **Pester 5.x** test file (`test.Tests.ps1`) that validates the corresponding guide workflow from scratch (Phase 1) and verifies the existing sample code still builds (Phase 2). Tests share infrastructure via `samples/SampleTestHelpers.psm1`. ### Running sample & guide tests locally @@ -66,16 +66,25 @@ Each sample under `samples/` has a self-contained `test.ps1` that validates the ### Writing a new sample & guide test -1. Create `test.ps1` in the sample directory -2. Import the shared helpers: `Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force` -3. Use `New-SampleTestContext -SampleDir $PSScriptRoot` to initialize, `Complete-SampleTest` to finalize -4. Phase 1: from-scratch guide workflow in a temp directory (scaffold, winapp init, build, cert, pack) -5. Phase 2: quick build of existing sample code to verify freshness -6. Add the sample name to the matrix in `.github/workflows/test-samples.yml` +1. Create `test.Tests.ps1` in the sample directory (Pester naming convention) +2. Use `BeforeDiscovery` for skip logic (prerequisite checks run at discovery time) +3. Import shared helpers in `BeforeAll`: `Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force` +4. Accept `$WinappPath` and `$SkipCleanup` parameters via `param()` block +5. Phase 1 (`Context`): from-scratch guide workflow in a temp directory (scaffold, winapp init, build, cert, pack) +6. Phase 2 (`Context`): quick build of existing sample code to verify freshness +7. Add the sample name to the matrix in `.github/workflows/test-samples.yml` + +#### Pester conventions for sample tests + +- **`BeforeDiscovery`**: Set `$script:skip` using inline `Get-Command` checks (no module import). Pester evaluates `-Skip:$variable` during discovery, before `BeforeAll` runs. +- **`BeforeAll`**: Import `SampleTestHelpers.psm1`, install winapp, create temp directories. Guard with `if ($script:skip) { return }`. +- **`AfterAll`**: Clean up temp directories using `Remove-TempTestDirectory`. +- **`It` blocks**: Use `-Skip:$script:skip` for prerequisite gating. Use Pester `Should` assertions (`Should -Be 0`, `Should -Exist`, `Should -Not -BeNullOrEmpty`). +- **Shared helpers**: `Invoke-WinappCommand` (throws on failure), `Test-Prerequisite` (returns bool), `New-TempTestDirectory`, `Remove-TempTestDirectory`, `Install-WinappGlobal`. ### CI integration -Sample & guide tests run via `.github/workflows/test-samples.yml` using a GitHub Actions matrix strategy. Each sample runs in its own parallel job after the main build completes. The workflow downloads the npm package artifact from the `Build and Package` workflow. +Sample & guide tests run via `.github/workflows/test-samples.yml` using a GitHub Actions matrix strategy. Each sample runs in its own parallel job after the main build completes. The workflow downloads the npm package artifact from the `Build and Package` workflow. Test results are uploaded as JUnit XML via `Invoke-Pester` with `TestResult` configuration. ## Where to look first diff --git a/samples/SampleTestHelpers.psm1 b/samples/SampleTestHelpers.psm1 index f2844304..c529e370 100644 --- a/samples/SampleTestHelpers.psm1 +++ b/samples/SampleTestHelpers.psm1 @@ -1,117 +1,19 @@ <# .SYNOPSIS -Shared PowerShell helpers for sample tests. +Shared PowerShell helpers for sample & guide Pester tests. .DESCRIPTION -This module provides common test helper functions used by each sample's test.ps1 script. +This module provides setup and CLI helper functions used by each sample's +test.Tests.ps1 Pester test file. Assertion and reporting functions are handled +by Pester's built-in Should assertions — this module only provides: + - CLI path resolution and installation + - Prerequisite checks + - Temp directory management + - winapp invocation helpers + Import with: Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force #> -# ============================================================================ -# Logging Helpers -# ============================================================================ - -function Write-TestHeader { - param([string]$Message) - Write-Host "`n$('='*80)" -ForegroundColor Cyan - Write-Host "TEST: $Message" -ForegroundColor Cyan - Write-Host "$('='*80)`n" -ForegroundColor Cyan -} - -function Write-TestStep { - param([string]$Message, [int]$Step) - Write-Host "[$Step] $Message" -ForegroundColor Yellow -} - -function Write-TestSuccess { - param([string]$Message) - Write-Host " ✓ $Message" -ForegroundColor Green -} - -function Write-TestError { - param([string]$Message) - Write-Host " ✗ $Message" -ForegroundColor Red -} - -# ============================================================================ -# Assertion Helpers -# ============================================================================ - -function Assert-ExitCode { - <# - .SYNOPSIS - Asserts the last exit code was 0, throwing with the given message if not. - #> - param( - [string]$FailMessage, - [int]$Expected = 0 - ) - if ($LASTEXITCODE -ne $Expected) { - Write-TestError "$FailMessage (exit code: $LASTEXITCODE, expected: $Expected)" - throw $FailMessage - } -} - -function Assert-Command { - <# - .SYNOPSIS - Runs a command string via Invoke-Expression, asserts exit code 0, and returns output. - #> - param( - [string]$Command, - [string]$FailMessage - ) - Write-Verbose "Running: $Command" - $output = Invoke-Expression $Command - if ($LASTEXITCODE -ne 0) { - Write-TestError $FailMessage - throw $FailMessage - } - Write-TestSuccess $Command - return $output -} - -function Assert-FileExists { - param( - [string]$Path, - [string]$Description - ) - if (-not (Test-Path $Path)) { - Write-TestError "$Description not found at $Path" - throw "$Description not found at $Path" - } - Write-TestSuccess "$Description exists: $Path" -} - -function Assert-DirectoryExists { - param( - [string]$Path, - [string]$Description - ) - if (-not (Test-Path $Path -PathType Container)) { - Write-TestError "$Description not found at $Path" - throw "$Description not found at $Path" - } - Write-TestSuccess "$Description exists: $Path" -} - -function Assert-OutputContains { - <# - .SYNOPSIS - Asserts that the given output string contains the expected substring. - #> - param( - [string]$Output, - [string]$Expected, - [string]$Description - ) - if ($Output -notmatch [regex]::Escape($Expected)) { - Write-TestError "$Description — expected output to contain '$Expected'" - throw "$Description — expected output to contain '$Expected'" - } - Write-TestSuccess "$Description — output contains '$Expected'" -} - # ============================================================================ # Winapp CLI Helpers # ============================================================================ @@ -120,11 +22,7 @@ function Resolve-WinappCliPath { <# .SYNOPSIS Resolves the winapp CLI path from artifacts or local build. - - .DESCRIPTION - Given -WinappPath, finds the npm tarball or package directory suitable for - `npm install`. Returns the resolved absolute path. If nothing is provided, - falls back to the default local build location. + Returns the resolved absolute path to a .tgz or package directory. #> param( [string]$WinappPath @@ -145,30 +43,22 @@ function Resolve-WinappCliPath { $resolved = (Resolve-Path $WinappPath).Path - # If directory contains a .tgz, return the tgz path if (Test-Path $resolved -PathType Container) { $tgz = Get-ChildItem -Path $resolved -Filter "*.tgz" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($tgz) { - return $tgz.FullName - } - if (Test-Path (Join-Path $resolved "package.json")) { - return $resolved - } + if ($tgz) { return $tgz.FullName } + if (Test-Path (Join-Path $resolved "package.json")) { return $resolved } throw "No .tgz or package.json found in $resolved" } - # Direct file path (e.g., a .tgz) return $resolved } -function Invoke-Winapp { +function Invoke-WinappCommand { <# .SYNOPSIS - Invokes the winapp CLI with the given arguments. - - .DESCRIPTION - Uses npx winapp if an npm package was installed in the current project, - otherwise falls back to dotnet run with the WinApp.Cli project. + Invokes the winapp CLI with the given arguments and returns stdout lines. + Uses npx if in a Node project, falls back to dotnet run, then PATH. + Throws on non-zero exit code. #> param( [Parameter(Mandatory)] @@ -176,195 +66,78 @@ function Invoke-Winapp { [string]$FailMessage = "winapp $Arguments failed" ) - # Prefer npx if available in the project $npxWinapp = Join-Path (Get-Location) "node_modules\.bin\winapp.cmd" if (Test-Path $npxWinapp) { $cmd = "npx winapp $Arguments" } else { - # Fallback to dotnet run $cliProject = Join-Path $PSScriptRoot "..\src\winapp-CLI\WinApp.Cli\WinApp.Cli.csproj" if (Test-Path $cliProject) { $cmd = "dotnet run --project `"$cliProject`" -- $Arguments" } else { - # Last resort: assume winapp is on PATH $cmd = "winapp $Arguments" } } - return Assert-Command -Command $cmd -FailMessage $FailMessage + Write-Verbose "Running: $cmd" + $output = Invoke-Expression $cmd + if ($LASTEXITCODE -ne 0) { throw $FailMessage } + return $output } function Install-WinappNpmPackage { <# .SYNOPSIS - Installs the winapp npm package into the current project from a path or artifacts folder. + Installs the winapp npm package into the current project as a devDependency. #> param( [Parameter(Mandatory)] [string]$PackagePath ) - Write-Verbose "Installing winapp from: $PackagePath" - Assert-Command "npm install `"$PackagePath`" --save-dev" "Failed to install winapp npm package" - Assert-FileExists (Join-Path (Get-Location) "node_modules\.bin\winapp.cmd") "winapp CLI binary" + Invoke-Expression "npm install `"$PackagePath`" --save-dev" + if ($LASTEXITCODE -ne 0) { throw "Failed to install winapp npm package" } } function Install-WinappGlobal { <# .SYNOPSIS Installs the winapp npm package globally so 'winapp' is available on PATH. - Use for non-Node samples (C++, .NET, Rust, Flutter) that call winapp from - build systems or the command line directly. #> param( [Parameter(Mandatory)] [string]$PackagePath ) - Write-Verbose "Installing winapp globally from: $PackagePath" - Assert-Command "npm install -g `"$PackagePath`"" "Failed to install winapp globally" - - # Verify winapp is now on PATH - try { - $winappVersion = & winapp --version 2>&1 | Select-Object -First 1 - Write-TestSuccess "winapp installed globally: $winappVersion" - } catch { - Write-TestError "winapp not found on PATH after global install" - throw "winapp global install did not put CLI on PATH" - } + Invoke-Expression "npm install -g `"$PackagePath`"" + if ($LASTEXITCODE -ne 0) { throw "Failed to install winapp globally" } } # ============================================================================ # Prerequisite Checks # ============================================================================ -function Assert-Prerequisite { +function Test-Prerequisite { <# .SYNOPSIS - Asserts that a command-line tool is available on PATH. + Tests whether a command-line tool is available on PATH. Returns $true/$false. #> param( - [string]$Command, - [string]$DisplayName = $Command, - [string]$VersionFlag = "--version" - ) - try { - $version = & $Command $VersionFlag 2>&1 | Select-Object -First 1 - Write-TestSuccess "$DisplayName found: $version" - } catch { - Write-TestError "$DisplayName is not installed or not in PATH" - throw "$DisplayName is required but not found" - } -} - -# ============================================================================ -# Test Environment Management -# ============================================================================ - -function New-SampleTestContext { - <# - .SYNOPSIS - Initializes a test context for a sample test. Returns a context hashtable. - - .DESCRIPTION - Sets strict mode, resolves the sample directory, and prepares the context - object used by all sample tests. - - .PARAMETER SampleDir - The directory of the calling test.ps1 script. Pass $PSScriptRoot from the - test script (the module's $PSScriptRoot points to the module directory, not - the caller). - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$SampleName, [Parameter(Mandatory)] - [string]$SampleDir, - [string]$WinappPath - ) - - Set-StrictMode -Version Latest - $ErrorActionPreference = 'Stop' - - $repoRoot = (Resolve-Path "$SampleDir\..\..").Path - - $ctx = @{ - SampleName = $SampleName - SampleDir = $SampleDir - RepoRoot = $repoRoot - WinappPath = $WinappPath - StartTime = Get-Date - } - - Write-TestHeader "$SampleName — Sample & Guide Test" - Write-Verbose "Sample directory: $($ctx.SampleDir)" - Write-Verbose "Repo root: $($ctx.RepoRoot)" - - return $ctx -} - -function Complete-SampleTest { - <# - .SYNOPSIS - Reports success and elapsed time for a sample test. - #> - param( - [Parameter(Mandatory)] - [hashtable]$Context - ) - $elapsed = (Get-Date) - $Context.StartTime - Write-Host "`n$('='*80)" -ForegroundColor Green - Write-Host "$($Context.SampleName) TEST COMPLETED SUCCESSFULLY ($([math]::Round($elapsed.TotalSeconds, 1))s)" -ForegroundColor Green - Write-Host "$('='*80)`n" -ForegroundColor Green -} - -# ============================================================================ -# MSIX Packaging Helpers -# ============================================================================ - -function Assert-MsixCreated { - <# - .SYNOPSIS - Asserts that at least one .msix file exists in the given directory. - #> - param( - [string]$Directory, - [string]$Description = "MSIX package" - ) - $msixFiles = Get-ChildItem -Path $Directory -Filter "*.msix" -ErrorAction SilentlyContinue - if (-not $msixFiles) { - Write-TestError "No .msix file found in $Directory" - throw "$Description not found in $Directory" - } - Write-TestSuccess "$Description created: $($msixFiles[0].Name)" - return $msixFiles[0].FullName -} - -function New-DevCertificate { - <# - .SYNOPSIS - Generates a development certificate using winapp cert generate. - Returns the path to the generated .pfx file. - #> - param( - [string]$OutputDir = (Get-Location) + [string]$Command ) - Invoke-Winapp "cert generate" -FailMessage "Failed to generate development certificate" - $certPath = Join-Path $OutputDir "devcert.pfx" - Assert-FileExists $certPath "Development certificate" - return $certPath + $null = Get-Command $Command -ErrorAction SilentlyContinue + return $? } # ============================================================================ -# Temp Directory Helpers (for from-scratch guide tests) +# Temp Directory Helpers # ============================================================================ function New-TempTestDirectory { <# .SYNOPSIS Creates a temporary directory for from-scratch guide workflow tests. - Returns the absolute path to the new directory. + Returns the absolute path. #> param( [Parameter(Mandatory)] @@ -374,7 +147,6 @@ function New-TempTestDirectory { $null = New-Item -ItemType Directory -Path $tempBase -Force $tempDir = Join-Path $tempBase "$Prefix-$([System.IO.Path]::GetRandomFileName())" $null = New-Item -ItemType Directory -Path $tempDir -Force - Write-TestSuccess "Created temp directory: $tempDir" return $tempDir } @@ -389,48 +161,7 @@ function Remove-TempTestDirectory { ) if (Test-Path $Path) { Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue - Write-TestSuccess "Cleaned up temp directory: $Path" - } -} - -function Assert-WinappInitOutput { - <# - .SYNOPSIS - Validates that winapp init created the expected files in the current directory. - Checks for winapp.yaml, appxmanifest.xml, and .winapp/ directory. - #> - param( - [string]$Directory = (Get-Location), - [switch]$ExpectWinappYaml, - [switch]$ExpectManifest, - [switch]$ExpectDotWinapp - ) - if ($ExpectWinappYaml) { - Assert-FileExists (Join-Path $Directory "winapp.yaml") "winapp.yaml config" - } - if ($ExpectManifest) { - Assert-FileExists (Join-Path $Directory "appxmanifest.xml") "AppxManifest" - } - if ($ExpectDotWinapp) { - Assert-DirectoryExists (Join-Path $Directory ".winapp") ".winapp SDK directory" - } -} - -function Assert-CertInfo { - <# - .SYNOPSIS - Runs winapp cert info on a certificate and validates the output is non-empty. - #> - param( - [Parameter(Mandatory)] - [string]$CertPath - ) - $output = Invoke-Winapp "cert info `"$CertPath`"" -FailMessage "winapp cert info failed" - if (-not $output) { - Write-TestError "winapp cert info produced no output" - throw "winapp cert info produced no output" } - Write-TestSuccess "winapp cert info returned certificate details" } # ============================================================================ @@ -438,26 +169,11 @@ function Assert-CertInfo { # ============================================================================ Export-ModuleMember -Function @( - 'Write-TestHeader' - 'Write-TestStep' - 'Write-TestSuccess' - 'Write-TestError' - 'Assert-ExitCode' - 'Assert-Command' - 'Assert-FileExists' - 'Assert-DirectoryExists' - 'Assert-OutputContains' 'Resolve-WinappCliPath' - 'Invoke-Winapp' + 'Invoke-WinappCommand' 'Install-WinappNpmPackage' 'Install-WinappGlobal' - 'Assert-Prerequisite' - 'New-SampleTestContext' - 'Complete-SampleTest' - 'Assert-MsixCreated' - 'New-DevCertificate' + 'Test-Prerequisite' 'New-TempTestDirectory' 'Remove-TempTestDirectory' - 'Assert-WinappInitOutput' - 'Assert-CertInfo' ) diff --git a/samples/cpp-app/test.Tests.ps1 b/samples/cpp-app/test.Tests.ps1 new file mode 100644 index 00000000..3411a504 --- /dev/null +++ b/samples/cpp-app/test.Tests.ps1 @@ -0,0 +1,134 @@ +<# +.SYNOPSIS +Pester 5.x tests for the cpp-app sample and C++/CMake guide workflow. + +.DESCRIPTION +Phase 1: Follows the docs/guides/cpp.md guide from scratch — creates a minimal + C++ project, runs winapp init, builds with CMake, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command cmake -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "cpp-app sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command cmake -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + + if (-not $script:skip) { + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + } + } + + AfterAll { + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Phase 1: C++/CMake Guide Workflow (from scratch)" { + BeforeAll { + if (-not $script:skip) { + $script:tempDir = New-TempTestDirectory -Prefix "cpp-guide" + Push-Location $script:tempDir + + @' +#include +#include +int main() { + std::cout << "Hello from C++ app" << std::endl; + return 0; +} +'@ | Set-Content "main.cpp" + + @' +cmake_minimum_required(VERSION 3.20) +project(test-cpp-app LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +add_executable(test-cpp-app main.cpp) +'@ | Set-Content "CMakeLists.txt" + } + } + + AfterAll { + if (-not $script:skip) { + Pop-Location + } + } + + It "winapp init creates config files" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=stable" + "winapp.yaml" | Should -Exist + "appxmanifest.xml" | Should -Exist + ".winapp" | Should -Exist + } + + It "CMake configures successfully" -Skip:$script:skip { + $output = cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "CMake configure failed: $output" + } + + It "CMake builds successfully" -Skip:$script:skip { + $output = cmake --build build --config Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "CMake build failed: $output" + } + + It "generates a dev certificate" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + "devcert.pfx" | Should -Exist + } + + It "packages as MSIX" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "pack build\Release --manifest appxmanifest.xml --cert devcert.pfx" + Get-ChildItem -Filter "*.msix" | Should -Not -BeNullOrEmpty -Because "MSIX package should be created" + } + } + + Context "Phase 2: Sample Build Check" { + BeforeAll { + if (-not $script:skip) { + Push-Location $script:sampleDir + } + } + + AfterAll { + if (-not $script:skip) { + Pop-Location + } + } + + It "winapp restore succeeds" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "restore" + } + + It "sample CMake configures successfully" -Skip:$script:skip { + $output = cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "Sample CMake configure failed: $output" + } + + It "sample builds and produces cpp-app.exe" -Skip:$script:skip { + $output = cmake --build build --config Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "Sample CMake build failed: $output" + "build\Release\cpp-app.exe" | Should -Exist + } + } +} diff --git a/samples/cpp-app/test.ps1 b/samples/cpp-app/test.ps1 deleted file mode 100644 index 93e79b8a..00000000 --- a/samples/cpp-app/test.ps1 +++ /dev/null @@ -1,116 +0,0 @@ -<# -.SYNOPSIS -Test script for the cpp-app sample and C++/CMake guide workflow. - -.DESCRIPTION -Phase 1: Follows the docs/guides/cpp.md guide from scratch — creates a minimal - C++ project, runs winapp init, builds with CMake, and packages as MSIX. -Phase 2: Quick build of the existing sample to verify it is not stale. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "cpp-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "cmake" -DisplayName "CMake" - Assert-Prerequisite "npm" -DisplayName "npm" - - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Phase 1 — Guide Workflow (from scratch) - # ================================================================== - Write-TestHeader "Phase 1: C++/CMake Guide Workflow (from scratch)" - - $tempDir = New-TempTestDirectory -Prefix "cpp-guide" - Push-Location $tempDir - - Write-TestStep "Creating minimal C++ project..." (++$step) - # Minimal main.cpp that uses Windows APIs (matches guide) - @' -#include -#include -int main() { - std::cout << "Hello from C++ app" << std::endl; - return 0; -} -'@ | Set-Content "main.cpp" - - @' -cmake_minimum_required(VERSION 3.20) -project(test-cpp-app LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 20) -add_executable(test-cpp-app main.cpp) -'@ | Set-Content "CMakeLists.txt" - - Write-TestStep "Running winapp init..." (++$step) - Assert-Command "winapp init --use-defaults --setup-sdks=stable" "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest -ExpectDotWinapp - - Write-TestStep "Configuring CMake project..." (++$step) - Assert-Command "cmake -B build -DCMAKE_BUILD_TYPE=Release" "CMake configure failed" - - Write-TestStep "Building C++ app..." (++$step) - Assert-Command "cmake --build build --config Release" "CMake build failed" - - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath "devcert.pfx" - - Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack build\Release --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide cpp-app MSIX" - - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Restoring sample SDK packages..." (++$step) - Assert-Command "winapp restore" "winapp restore failed" - - Write-TestStep "Building existing sample..." (++$step) - Assert-Command "cmake -B build -DCMAKE_BUILD_TYPE=Release" "Sample CMake configure failed" - Assert-Command "cmake --build build --config Release" "Sample CMake build failed" - Assert-FileExists "build\Release\cpp-app.exe" "cpp-app.exe" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - Remove-Item -Path (Join-Path $ctx.SampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 new file mode 100644 index 00000000..580da82c --- /dev/null +++ b/samples/dotnet-app/test.Tests.ps1 @@ -0,0 +1,148 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe ".NET App Guide Workflow" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + $script:tempDir = $null + $script:sampleDir = $PSScriptRoot + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "dotnet-guide" + $script:projectDir = Join-Path $script:tempDir "test-dotnet-app" + $script:sampleDir = $PSScriptRoot + } + + AfterAll { + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + if ($script:sampleDir) { + Remove-Item -Path (Join-Path $script:sampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + + Context "Prerequisites" { + It "Should have dotnet available" -Skip:$script:skip { + Test-Prerequisite 'dotnet' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + } + + Context "Phase 1: .NET Guide Workflow (from scratch)" { + + Context "Project Creation" { + It "Should create a new .NET console project" -Skip:$script:skip { + Push-Location $script:tempDir + try { + Invoke-Expression "dotnet new console -n test-dotnet-app" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created the project directory" -Skip:$script:skip { + $script:projectDir | Should -Exist + } + } + + Context "Winapp Init" { + It "Should run winapp init successfully" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "winapp init --use-defaults" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created appxmanifest.xml" -Skip:$script:skip { + Join-Path $script:projectDir "appxmanifest.xml" | Should -Exist + } + } + + Context "Certificate Generation" { + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "winapp cert generate --if-exists skip" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:projectDir "devcert.pfx" | Should -Exist + } + + It "Should report certificate info" -Skip:$script:skip { + Push-Location $script:projectDir + try { + $output = Invoke-WinappCommand -Arguments "cert info devcert.pfx" + $output | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + } + + Context "Release Build and MSIX Packaging" { + It "Should build in Release mode" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "dotnet build -c Release" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should produce a Release executable" -Skip:$script:skip { + $exeFile = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Release") -Filter "*.exe" -Recurse | + Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty + $script:outputDir = $exeFile.DirectoryName + } + + It "Should package MSIX with winapp pack" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "winapp pack `"$($script:outputDir)`" --manifest appxmanifest.xml --cert devcert.pfx" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:projectDir -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + } + } + } + + Context "Phase 2: Sample Build Check" { + It "Should restore sample dependencies" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "dotnet restore" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should build sample in Debug mode" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "dotnet build -c Debug /p:ApplyDebugIdentity=false" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + } +} diff --git a/samples/dotnet-app/test.ps1 b/samples/dotnet-app/test.ps1 deleted file mode 100644 index 9f31ea46..00000000 --- a/samples/dotnet-app/test.ps1 +++ /dev/null @@ -1,104 +0,0 @@ -<# -.SYNOPSIS -Test script for the dotnet-app sample and .NET guide workflow. - -.DESCRIPTION -Phase 1: Follows the docs/guides/dotnet.md guide from scratch — creates a new - .NET console project, runs winapp init, builds in Release (auto-packages MSIX). -Phase 2: Quick build of the existing sample to verify it is not stale. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "dotnet-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" - Assert-Prerequisite "npm" -DisplayName "npm" - - # Install winapp globally (MSBuild targets call winapp directly) - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Phase 1 — Guide Workflow (from scratch) - # ================================================================== - Write-TestHeader "Phase 1: .NET Guide Workflow (from scratch)" - - $tempDir = New-TempTestDirectory -Prefix "dotnet-guide" - Push-Location $tempDir - - Write-TestStep "Creating new .NET console project..." (++$step) - Assert-Command "dotnet new console -n test-dotnet-app" "dotnet new console failed" - Push-Location "test-dotnet-app" - - Write-TestStep "Running winapp init..." (++$step) - Assert-Command "winapp init --use-defaults" "winapp init failed" - Assert-WinappInitOutput -ExpectManifest - - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - Assert-FileExists "devcert.pfx" "Development certificate" - - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath "devcert.pfx" - - Write-TestStep "Building in Release mode..." (++$step) - Assert-Command "dotnet build -c Release" "dotnet build -c Release failed" - - # Find the actual output directory containing the exe (handles TFM + RID subdirs) - $exeFile = Get-ChildItem -Path "bin\Release" -Filter "*.exe" -Recurse | Select-Object -First 1 - if (-not $exeFile) { throw "No .exe found in Release output" } - $outputDir = $exeFile.DirectoryName - - Write-TestStep "Packaging MSIX with winapp pack..." (++$step) - Assert-Command "winapp pack `"$outputDir`" --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide dotnet-app MSIX" - - Pop-Location # back to tempDir - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Building existing sample (Debug, skip identity)..." (++$step) - Assert-Command "dotnet restore" "dotnet restore failed" - Assert-Command "dotnet build -c Debug /p:ApplyDebugIdentity=false" "Sample build failed" - Write-TestSuccess "dotnet-app sample builds successfully" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - Remove-Item -Path (Join-Path $ctx.SampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 new file mode 100644 index 00000000..0a972970 --- /dev/null +++ b/samples/electron/test.Tests.ps1 @@ -0,0 +1,71 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "Electron Sample Freshness Check" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:originalLocation = Get-Location + } + + AfterAll { + Set-Location $script:sampleDir + + if (-not $SkipCleanup) { + Remove-Item -Path (Join-Path $script:sampleDir 'node_modules') -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Prerequisites" { + It "Should have Node.js available" -Skip:$script:skip { + Test-Prerequisite 'node' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + } + + Context "Sample Build Check" { + BeforeAll { + if (-not $script:skip) { + Push-Location $script:sampleDir + } + } + + AfterAll { + if (-not $script:skip) { + Set-Location $script:originalLocation + } + } + + It "Should install sample dependencies" -Skip:$script:skip { + Invoke-Expression "npm install --ignore-scripts" + $LASTEXITCODE | Should -Be 0 + } + + It "Should have created node_modules" -Skip:$script:skip { + Join-Path $script:sampleDir 'node_modules' | Should -Exist + } + + It "Should have package.json" -Skip:$script:skip { + Join-Path $script:sampleDir 'package.json' | Should -Exist + } + + It "Should have forge.config.js" -Skip:$script:skip { + Join-Path $script:sampleDir 'forge.config.js' | Should -Exist + } + + It "Should have appxmanifest.xml" -Skip:$script:skip { + Join-Path $script:sampleDir 'appxmanifest.xml' | Should -Exist + } + } +} diff --git a/samples/electron/test.ps1 b/samples/electron/test.ps1 deleted file mode 100644 index d2416dda..00000000 --- a/samples/electron/test.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -<# -.SYNOPSIS -Test script for the electron sample freshness check. - -.DESCRIPTION -Verifies the existing samples/electron code is not stale by installing -dependencies and validating structure. The from-scratch Electron guide -workflow (init, addon creation, Forge packaging, MSIX) is covered by -the dedicated E2E test in scripts/test-e2e-electron.ps1. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "electron" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "node" -DisplayName "Node.js" - Assert-Prerequisite "npm" -DisplayName "npm" - - # ================================================================== - # Sample Build Check - # ================================================================== - Write-TestHeader "Electron Sample Freshness Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Installing sample dependencies..." (++$step) - Assert-Command "npm install --ignore-scripts" "npm install failed" - Assert-DirectoryExists "node_modules" "node_modules" - - Write-TestStep "Verifying sample structure..." (++$step) - Assert-FileExists "package.json" "package.json" - Assert-FileExists "forge.config.js" "forge.config.js" - Assert-FileExists "appxmanifest.xml" "appxmanifest.xml" - Write-TestSuccess "electron sample is valid and installable" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/samples/flutter-app/test.Tests.ps1 b/samples/flutter-app/test.Tests.ps1 new file mode 100644 index 00000000..a9da13d1 --- /dev/null +++ b/samples/flutter-app/test.Tests.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS +Pester 5.x tests for the flutter-app sample and Flutter guide workflow. + +.DESCRIPTION +Phase 1: Follows the docs/guides/flutter.md guide from scratch — creates a new + Flutter project, runs winapp init, builds, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command flutter -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "flutter-app sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command flutter -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + $script:projectDir = $null + + if (-not $script:skip) { + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + } + } + + AfterAll { + Set-Location $script:sampleDir + if (-not $SkipCleanup -and -not $script:skip) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Phase 1: Flutter Guide Workflow (from scratch)" -Skip:$script:skip { + BeforeAll { + $script:tempDir = New-TempTestDirectory -Prefix "flutter-guide" + Set-Location $script:tempDir + + flutter create test_flutter_app --platforms=windows + if ($LASTEXITCODE -ne 0) { throw "flutter create failed" } + + $script:projectDir = Join-Path $script:tempDir "test_flutter_app" + Set-Location $script:projectDir + + Invoke-WinappCommand "init --use-defaults --setup-sdks=stable" + + flutter build windows + if ($LASTEXITCODE -ne 0) { throw "flutter build windows failed" } + + $script:buildOutput = Join-Path $script:projectDir "build\windows\x64\runner\Release" + Copy-Item $script:buildOutput -Destination (Join-Path $script:projectDir "dist") -Recurse + + Invoke-WinappCommand "cert generate --if-exists skip" + Invoke-WinappCommand "pack dist --cert devcert.pfx" + } + + It "Should create winapp.yaml after init" { + Join-Path $script:projectDir "winapp.yaml" | Should -Exist + } + + It "Should create appxmanifest.xml after init" { + Join-Path $script:projectDir "appxmanifest.xml" | Should -Exist + } + + It "Should create .winapp directory after init" { + Join-Path $script:projectDir ".winapp" | Should -Exist + } + + It "Should produce Flutter build output" { + $script:buildOutput | Should -Exist + } + + It "Should generate a dev certificate" { + Join-Path $script:projectDir "devcert.pfx" | Should -Exist + } + + It "Should produce an MSIX package" { + Get-ChildItem -Path $script:projectDir -Filter "*.msix" | Should -Not -BeNullOrEmpty + } + } + + Context "Phase 2: Sample Build Check" -Skip:$script:skip { + BeforeAll { + Set-Location $script:sampleDir + + flutter pub get + if ($LASTEXITCODE -ne 0) { throw "flutter pub get failed" } + + Invoke-WinappCommand "restore" + + flutter build windows + if ($LASTEXITCODE -ne 0) { throw "flutter build windows failed" } + } + + It "Should build flutter_app.exe" { + Join-Path $script:sampleDir "build\windows\x64\runner\Release\flutter_app.exe" | Should -Exist + } + } +} diff --git a/samples/flutter-app/test.ps1 b/samples/flutter-app/test.ps1 deleted file mode 100644 index 9d8b0b97..00000000 --- a/samples/flutter-app/test.ps1 +++ /dev/null @@ -1,108 +0,0 @@ -<# -.SYNOPSIS -Test script for the flutter-app sample and Flutter guide workflow. - -.DESCRIPTION -Phase 1: Follows the docs/guides/flutter.md guide from scratch — creates a new - Flutter project, runs winapp init, builds, and packages as MSIX. -Phase 2: Quick build of the existing sample to verify it is not stale. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "flutter-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "flutter" -DisplayName "Flutter SDK" - Assert-Prerequisite "npm" -DisplayName "npm" - - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Phase 1 — Guide Workflow (from scratch) - # ================================================================== - Write-TestHeader "Phase 1: Flutter Guide Workflow (from scratch)" - - $tempDir = New-TempTestDirectory -Prefix "flutter-guide" - Push-Location $tempDir - - Write-TestStep "Creating new Flutter project..." (++$step) - Assert-Command "flutter create test_flutter_app --platforms=windows" "flutter create failed" - Push-Location "test_flutter_app" - - Write-TestStep "Running winapp init..." (++$step) - Assert-Command "winapp init --use-defaults --setup-sdks=stable" "winapp init failed" - Assert-WinappInitOutput -ExpectWinappYaml -ExpectManifest -ExpectDotWinapp - - Write-TestStep "Building Flutter Windows app..." (++$step) - Assert-Command "flutter build windows" "flutter build windows failed" - - $buildOutput = "build\windows\x64\runner\Release" - Assert-DirectoryExists $buildOutput "Flutter build output" - - Write-TestStep "Preparing distribution folder..." (++$step) - Copy-Item $buildOutput -Destination "dist" -Recurse - - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath "devcert.pfx" - - Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack dist --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide flutter-app MSIX" - - Pop-Location # back to tempDir - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Getting sample Flutter dependencies..." (++$step) - Assert-Command "flutter pub get" "flutter pub get failed" - - Write-TestStep "Restoring sample SDK packages..." (++$step) - Assert-Command "winapp restore" "winapp restore failed" - - Write-TestStep "Building existing sample..." (++$step) - Assert-Command "flutter build windows" "Sample flutter build failed" - Assert-FileExists "build\windows\x64\runner\Release\flutter_app.exe" "flutter_app.exe" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - Remove-Item -Path (Join-Path $ctx.SampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/samples/packaging-cli/test.Tests.ps1 b/samples/packaging-cli/test.Tests.ps1 new file mode 100644 index 00000000..a9cc3816 --- /dev/null +++ b/samples/packaging-cli/test.Tests.ps1 @@ -0,0 +1,103 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "Packaging CLI Guide Workflow" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + $script:tempDir = $null + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "packaging-cli-guide" + $script:packageDir = Join-Path $script:tempDir "MyCliPackage" + $null = New-Item -ItemType Directory -Path $script:packageDir -Force + Copy-Item "$env:SystemRoot\System32\cmd.exe" -Destination (Join-Path $script:packageDir "mycli.exe") + } + + AfterAll { + if (-not $SkipCleanup -and $script:tempDir) { + Remove-TempTestDirectory -Path $script:tempDir + } + } + + Context "Prerequisites" { + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + + It "Should have dummy CLI executable" -Skip:$script:skip { + Join-Path $script:packageDir "mycli.exe" | Should -Exist + } + } + + Context "Manifest Generation" { + It "Should generate manifest from executable" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-Expression "winapp manifest generate --executable mycli.exe" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created appxmanifest.xml" -Skip:$script:skip { + Join-Path $script:packageDir "appxmanifest.xml" | Should -Exist + } + } + + Context "Certificate Generation" { + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-Expression "winapp cert generate --if-exists skip" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:packageDir "devcert.pfx" | Should -Exist + } + + It "Should report certificate info" -Skip:$script:skip { + Push-Location $script:packageDir + try { + $output = Invoke-WinappCommand -Arguments "cert info devcert.pfx" + $output | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + } + + Context "MSIX Packaging and Signing" { + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-Expression "winapp pack . --cert devcert.pfx" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:packageDir -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + $script:msixPath = $msix.FullName + } + + It "Should sign the MSIX" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-Expression "winapp sign `"$($script:msixPath)`" devcert.pfx" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + } +} diff --git a/samples/packaging-cli/test.ps1 b/samples/packaging-cli/test.ps1 deleted file mode 100644 index 94007f35..00000000 --- a/samples/packaging-cli/test.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -<# -.SYNOPSIS -Test script for the packaging-cli guide workflow. - -.DESCRIPTION -Follows docs/guides/packaging-cli.md from scratch — takes a pre-built CLI -executable, generates a manifest, creates a certificate, packages as MSIX, -and signs the package. Tests winapp manifest generate, cert generate, -cert info, pack, and sign commands. - -This test has no corresponding sample — it exists purely to validate the -generic "package any CLI" guide. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "packaging-cli" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "npm" -DisplayName "npm" - - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Guide Workflow — Package a CLI Executable as MSIX - # ================================================================== - Write-TestHeader "Packaging CLI Guide Workflow" - - $tempDir = New-TempTestDirectory -Prefix "packaging-cli-guide" - Push-Location $tempDir - - # Create a minimal dummy executable (copy cmd.exe as stand-in) - Write-TestStep "Preparing dummy CLI executable..." (++$step) - $null = New-Item -ItemType Directory -Path "MyCliPackage" -Force - Copy-Item "$env:SystemRoot\System32\cmd.exe" -Destination "MyCliPackage\mycli.exe" - Assert-FileExists "MyCliPackage\mycli.exe" "Dummy CLI executable" - - Push-Location "MyCliPackage" - - # Generate manifest from executable (core guide step) - Write-TestStep "Generating manifest from executable..." (++$step) - Assert-Command "winapp manifest generate --executable mycli.exe" "winapp manifest generate failed" - Assert-FileExists "appxmanifest.xml" "Generated AppxManifest" - - # Generate certificate - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - Assert-FileExists "devcert.pfx" "Development certificate" - - # Verify certificate info (guides show this for verification) - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath "devcert.pfx" - - # Package as MSIX - Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack . --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - $msixPath = Assert-MsixCreated -Directory (Get-Location) -Description "Packaging-CLI MSIX" - - # Sign the MSIX (standalone sign command from usage.md) - Write-TestStep "Signing MSIX (standalone sign command)..." (++$step) - Assert-Command "winapp sign `"$msixPath`" devcert.pfx" "winapp sign failed" - Write-TestSuccess "MSIX signed successfully" - - Pop-Location # back to tempDir - Pop-Location # back to original - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - } -} diff --git a/samples/rust-app/test.Tests.ps1 b/samples/rust-app/test.Tests.ps1 new file mode 100644 index 00000000..f66ba7d3 --- /dev/null +++ b/samples/rust-app/test.Tests.ps1 @@ -0,0 +1,133 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "Rust App Sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:sampleDir = $PSScriptRoot + $script:tempDir = New-TempTestDirectory -Prefix "rust-guide" + } + + AfterAll { + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "target") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Prerequisites" { + It "Should have cargo available" -Skip:$script:skip { + Test-Prerequisite 'cargo' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + } + + Context "Rust Guide Workflow (from scratch)" { + It "Should create a new Rust project" -Skip:$script:skip { + Push-Location $script:tempDir + try { + Invoke-Expression "cargo new test-rust-app" + $LASTEXITCODE | Should -Be 0 + $script:rustProjectDir = Join-Path $script:tempDir "test-rust-app" + $script:rustProjectDir | Should -Exist + } finally { Pop-Location } + } + + It "Should run winapp init" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "winapp init --use-defaults --setup-sdks=none" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created appxmanifest.xml" -Skip:$script:skip { + Join-Path $script:rustProjectDir "appxmanifest.xml" | Should -Exist + } + + It "Should build Rust app in release mode" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "cargo build --release" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced release executable" -Skip:$script:skip { + Join-Path $script:rustProjectDir "target\release\test-rust-app.exe" | Should -Exist + } + + It "Should prepare MSIX layout" -Skip:$script:skip { + $distDir = Join-Path $script:rustProjectDir "dist" + $null = New-Item -ItemType Directory -Path $distDir -Force + Copy-Item (Join-Path $script:rustProjectDir "target\release\test-rust-app.exe") -Destination $distDir + Join-Path $distDir "test-rust-app.exe" | Should -Exist + } + + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "winapp cert generate --if-exists skip" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:rustProjectDir "devcert.pfx" | Should -Exist + } + + It "Should report certificate info" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + $output = Invoke-WinappCommand -Arguments "cert info devcert.pfx" + $output | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "winapp pack dist --manifest appxmanifest.xml --cert devcert.pfx" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:rustProjectDir -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + } + } + + Context "Sample Build Check" { + It "Should build existing sample with cargo" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "cargo build" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced debug executable" -Skip:$script:skip { + Join-Path $script:sampleDir "target\debug\rust-app.exe" | Should -Exist + } + } +} diff --git a/samples/rust-app/test.ps1 b/samples/rust-app/test.ps1 deleted file mode 100644 index d717560a..00000000 --- a/samples/rust-app/test.ps1 +++ /dev/null @@ -1,100 +0,0 @@ -<# -.SYNOPSIS -Test script for the rust-app sample and Rust guide workflow. - -.DESCRIPTION -Phase 1: Follows the docs/guides/rust.md guide from scratch — creates a new - Rust project, runs winapp init, builds, and packages as MSIX. -Phase 2: Quick build of the existing sample to verify it is not stale. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "rust-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "cargo" -DisplayName "Rust/Cargo" - Assert-Prerequisite "npm" -DisplayName "npm" - - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Phase 1 — Guide Workflow (from scratch) - # ================================================================== - Write-TestHeader "Phase 1: Rust Guide Workflow (from scratch)" - - $tempDir = New-TempTestDirectory -Prefix "rust-guide" - Push-Location $tempDir - - Write-TestStep "Creating new Rust project..." (++$step) - Assert-Command "cargo new test-rust-app" "cargo new failed" - Push-Location "test-rust-app" - - Write-TestStep "Running winapp init..." (++$step) - Assert-Command "winapp init --use-defaults --setup-sdks=none" "winapp init failed" - Assert-WinappInitOutput -ExpectManifest - - Write-TestStep "Building Rust app (release)..." (++$step) - Assert-Command "cargo build --release" "cargo build --release failed" - Assert-FileExists "target\release\test-rust-app.exe" "test-rust-app.exe" - - Write-TestStep "Preparing MSIX layout..." (++$step) - $null = New-Item -ItemType Directory -Path "dist" -Force - Copy-Item "target\release\test-rust-app.exe" -Destination "dist\" - - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath "devcert.pfx" - - Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack dist --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide rust-app MSIX" - - Pop-Location # back to tempDir - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Building existing sample..." (++$step) - Assert-Command "cargo build" "Sample cargo build failed" - Assert-FileExists "target\debug\rust-app.exe" "rust-app.exe" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - Remove-Item -Path (Join-Path $ctx.SampleDir "target") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/samples/tauri-app/test.Tests.ps1 b/samples/tauri-app/test.Tests.ps1 new file mode 100644 index 00000000..3b87de2c --- /dev/null +++ b/samples/tauri-app/test.Tests.ps1 @@ -0,0 +1,147 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) -or $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) +} + +Describe "Tauri App Sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) -or $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "tauri-guide" + $script:tempApp = Join-Path $script:tempDir "tauri-app" + } + + AfterAll { + Set-Location $script:sampleDir + + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir "src-tauri\target") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Prerequisites" { + It "Should have Node.js available" -Skip:$script:skip { + Test-Prerequisite 'node' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + + It "Should have Rust/Cargo available" -Skip:$script:skip { + Test-Prerequisite 'cargo' | Should -Be $true + } + } + + Context "Tauri Guide Workflow (from scratch)" { + It "Should copy sample to temp directory" -Skip:$script:skip { + Copy-Item -Path $script:sampleDir -Destination $script:tempApp -Recurse -Exclude @('.gitignore', 'node_modules', 'src-tauri\target') + $script:tempApp | Should -Exist + } + + It "Should install npm dependencies" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "npm install" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should run winapp init" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "winapp init --use-defaults --setup-sdks=none" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created appxmanifest.xml" -Skip:$script:skip { + Join-Path $script:tempApp "appxmanifest.xml" | Should -Exist + } + + It "Should build Tauri app in release mode" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "cargo build --release --manifest-path src-tauri\Cargo.toml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced release executable" -Skip:$script:skip { + Join-Path $script:tempApp "src-tauri\target\release\tauri-app.exe" | Should -Exist + } + + It "Should prepare MSIX layout" -Skip:$script:skip { + Push-Location $script:tempApp + try { + $layoutDir = Join-Path $script:tempApp "msix-layout" + $null = New-Item -ItemType Directory -Path $layoutDir -Force + $tauriExe = Join-Path $script:tempApp "src-tauri\target\release\tauri-app.exe" + Copy-Item $tauriExe -Destination $layoutDir + Join-Path $layoutDir "tauri-app.exe" | Should -Exist + } finally { Pop-Location } + } + + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "winapp cert generate --if-exists skip --manifest appxmanifest.xml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:tempApp "devcert.pfx" | Should -Exist + } + + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "winapp pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:tempApp -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + } + } + + Context "Sample Build Check" { + It "Should install sample npm dependencies" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "npm install" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should build sample Rust backend" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "cargo build --manifest-path src-tauri\Cargo.toml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced debug executable" -Skip:$script:skip { + Join-Path $script:sampleDir "src-tauri\target\debug\tauri-app.exe" | Should -Exist + } + } +} diff --git a/samples/tauri-app/test.ps1 b/samples/tauri-app/test.ps1 deleted file mode 100644 index 6e9111cd..00000000 --- a/samples/tauri-app/test.ps1 +++ /dev/null @@ -1,109 +0,0 @@ -<# -.SYNOPSIS -Test script for the tauri-app sample and Tauri guide workflow. - -.DESCRIPTION -Phase 1: Follows the docs/guides/tauri.md guide — copies sample to temp dir, - installs deps, runs winapp init, builds, and packages as MSIX. -Phase 2: Quick build of the existing sample to verify it is not stale. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "tauri-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "node" -DisplayName "Node.js" - Assert-Prerequisite "npm" -DisplayName "npm" - Assert-Prerequisite "cargo" -DisplayName "Rust/Cargo" - - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Phase 1 — Guide Workflow (copy sample to temp, run full flow) - # Tauri scaffolding via npm create tauri-app is interactive and slow, - # so we copy the existing sample as a starting point (matching the - # guide's "start from a Tauri template" step). - # ================================================================== - Write-TestHeader "Phase 1: Tauri Guide Workflow" - - $tempDir = New-TempTestDirectory -Prefix "tauri-guide" - $tempApp = Join-Path $tempDir "tauri-app" - - Write-TestStep "Copying sample to temp directory..." (++$step) - Copy-Item -Path $ctx.SampleDir -Destination $tempApp -Recurse -Exclude @('.gitignore', 'node_modules', 'src-tauri\target') - Push-Location $tempApp - - Write-TestStep "Installing npm dependencies..." (++$step) - Assert-Command "npm install" "npm install failed" - - Write-TestStep "Running winapp init..." (++$step) - Assert-Command "winapp init --use-defaults --setup-sdks=none" "winapp init failed" - Assert-WinappInitOutput -ExpectManifest - - Write-TestStep "Building Tauri app (release)..." (++$step) - Assert-Command "cargo build --release --manifest-path src-tauri\Cargo.toml" "Tauri cargo build failed" - - $tauriExe = Join-Path $tempApp "src-tauri\target\release\tauri-app.exe" - Assert-FileExists $tauriExe "tauri-app.exe" - - Write-TestStep "Preparing MSIX layout..." (++$step) - $null = New-Item -ItemType Directory -Path "msix-layout" -Force - Copy-Item $tauriExe -Destination "msix-layout\" - - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip --manifest appxmanifest.xml" "cert generate failed" - - Write-TestStep "Packaging as MSIX..." (++$step) - Assert-Command "winapp pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide tauri-app MSIX" - - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Installing sample npm dependencies..." (++$step) - Assert-Command "npm install" "npm install failed" - - Write-TestStep "Building sample Rust backend..." (++$step) - Assert-Command "cargo build --manifest-path src-tauri\Cargo.toml" "Sample cargo build failed" - Assert-FileExists "src-tauri\target\debug\tauri-app.exe" "tauri-app.exe" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - Remove-Item -Path (Join-Path $ctx.SampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "src-tauri\target") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/samples/wpf-app/test.Tests.ps1 b/samples/wpf-app/test.Tests.ps1 new file mode 100644 index 00000000..bd6ce1ef --- /dev/null +++ b/samples/wpf-app/test.Tests.ps1 @@ -0,0 +1,123 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe 'wpf-app sample' { + + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + $script:originalLocation = Get-Location + + if (-not $script:skip) { + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + } + } + + AfterAll { + Set-Location $script:sampleDir + + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir 'bin') -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir 'obj') -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context 'Phase 1: WPF Guide Workflow (from scratch)' { + + BeforeAll { + if (-not $script:skip) { + $script:tempDir = New-TempTestDirectory -Prefix 'wpf-guide' + Push-Location $script:tempDir + + Invoke-Expression 'dotnet new wpf -n test-wpf-app' + $script:dotnetNewExit = $LASTEXITCODE + + if ($script:dotnetNewExit -eq 0) { + Push-Location 'test-wpf-app' + } + } + } + + AfterAll { + if (-not $script:skip) { + # Unwind any Push-Location calls made during this context + Set-Location $script:originalLocation + } + } + + It 'Creates a new WPF project' -Skip:$script:skip { + $script:dotnetNewExit | Should -Be 0 + } + + It 'Runs winapp init successfully' -Skip:$script:skip { + Invoke-WinappCommand -Arguments 'init --use-defaults' + } + + It 'Generates appxmanifest.xml from winapp init' -Skip:$script:skip { + 'appxmanifest.xml' | Should -Exist + } + + It 'Generates a dev certificate' -Skip:$script:skip { + Invoke-WinappCommand -Arguments 'cert generate --if-exists skip' + 'devcert.pfx' | Should -Exist + } + + It 'Shows certificate info without error' -Skip:$script:skip { + Invoke-WinappCommand -Arguments 'cert info devcert.pfx' + } + + It 'Builds in Release mode with RID' -Skip:$script:skip { + Invoke-Expression 'dotnet build -c Release -r win-x64' + $LASTEXITCODE | Should -Be 0 + } + + It 'Packages MSIX with winapp pack' -Skip:$script:skip { + $exeFile = Get-ChildItem -Path 'bin\Release' -Filter '*.exe' -Recurse | Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty -Because 'Release build should produce an .exe' + $script:outputDir = $exeFile.DirectoryName + + Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest appxmanifest.xml --cert devcert.pfx" + } + + It 'Produces an MSIX file' -Skip:$script:skip { + $msix = Get-ChildItem -Path '.' -Filter '*.msix' -ErrorAction SilentlyContinue | Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty -Because 'winapp pack should create an .msix file' + } + } + + Context 'Phase 2: Sample Build Check' { + + BeforeAll { + if (-not $script:skip) { + Push-Location $script:sampleDir + } + } + + AfterAll { + if (-not $script:skip) { + Set-Location $script:originalLocation + } + } + + It 'Restores NuGet packages' -Skip:$script:skip { + Invoke-Expression 'dotnet restore' + $LASTEXITCODE | Should -Be 0 + } + + It 'Builds existing sample in Debug mode' -Skip:$script:skip { + Invoke-Expression 'dotnet build -c Debug /p:ApplyDebugIdentity=false' + $LASTEXITCODE | Should -Be 0 + } + } +} diff --git a/samples/wpf-app/test.ps1 b/samples/wpf-app/test.ps1 deleted file mode 100644 index 264fe278..00000000 --- a/samples/wpf-app/test.ps1 +++ /dev/null @@ -1,103 +0,0 @@ -<# -.SYNOPSIS -Test script for the wpf-app sample and WPF guide workflow. - -.DESCRIPTION -Phase 1: Creates a new WPF project from scratch, runs winapp init, builds in - Release with RID (auto-packages MSIX). -Phase 2: Quick build of the existing sample to verify it is not stale. - -.PARAMETER WinappPath -Path to the winapp npm package (.tgz or directory) to install. - -.PARAMETER SkipCleanup -Keep generated artifacts after test completes. -#> - -[CmdletBinding()] -param( - [string]$WinappPath, - [switch]$SkipCleanup -) - -Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force - -$ctx = New-SampleTestContext -SampleName "wpf-app" -SampleDir $PSScriptRoot -WinappPath $WinappPath -Verbose:$VerbosePreference -$step = 0 -$tempDir = $null - -try { - # ================================================================== - # Prerequisites - # ================================================================== - Write-TestStep "Checking prerequisites..." (++$step) - Assert-Prerequisite "dotnet" -DisplayName ".NET SDK" - Assert-Prerequisite "npm" -DisplayName "npm" - - Write-TestStep "Installing winapp CLI..." (++$step) - $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath - Install-WinappGlobal -PackagePath $resolvedPkg - - # ================================================================== - # Phase 1 — Guide Workflow (from scratch) - # ================================================================== - Write-TestHeader "Phase 1: WPF Guide Workflow (from scratch)" - - $tempDir = New-TempTestDirectory -Prefix "wpf-guide" - Push-Location $tempDir - - Write-TestStep "Creating new WPF project..." (++$step) - Assert-Command "dotnet new wpf -n test-wpf-app" "dotnet new wpf failed" - Push-Location "test-wpf-app" - - Write-TestStep "Running winapp init..." (++$step) - Assert-Command "winapp init --use-defaults" "winapp init failed" - Assert-WinappInitOutput -ExpectManifest - - Write-TestStep "Generating dev certificate..." (++$step) - Assert-Command "winapp cert generate --if-exists skip" "cert generate failed" - Assert-FileExists "devcert.pfx" "Development certificate" - - Write-TestStep "Verifying certificate info..." (++$step) - Assert-CertInfo -CertPath "devcert.pfx" - - Write-TestStep "Building in Release mode with RID..." (++$step) - Assert-Command "dotnet build -c Release -r win-x64" "dotnet build -c Release -r win-x64 failed" - - # Find the actual output directory containing the exe - $exeFile = Get-ChildItem -Path "bin\Release" -Filter "*.exe" -Recurse | Select-Object -First 1 - if (-not $exeFile) { throw "No .exe found in Release output" } - $outputDir = $exeFile.DirectoryName - - Write-TestStep "Packaging MSIX with winapp pack..." (++$step) - Assert-Command "winapp pack `"$outputDir`" --manifest appxmanifest.xml --cert devcert.pfx" "winapp pack failed" - - Write-TestStep "Validating MSIX output..." (++$step) - Assert-MsixCreated -Directory (Get-Location) -Description "Guide wpf-app MSIX" - - Pop-Location # back to tempDir - Pop-Location # back to original - - # ================================================================== - # Phase 2 — Sample Build Check - # ================================================================== - Write-TestHeader "Phase 2: Sample Build Check" - Push-Location $ctx.SampleDir - - Write-TestStep "Building existing sample (Debug, skip identity)..." (++$step) - Assert-Command "dotnet restore" "dotnet restore failed" - Assert-Command "dotnet build -c Debug /p:ApplyDebugIdentity=false" "Sample build failed" - Write-TestSuccess "wpf-app sample builds successfully" - - Pop-Location - - Complete-SampleTest -Context $ctx - -} finally { - Set-Location $ctx.SampleDir - if (-not $SkipCleanup) { - if ($tempDir) { Remove-TempTestDirectory -Path $tempDir } - Remove-Item -Path (Join-Path $ctx.SampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path (Join-Path $ctx.SampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue - } -} diff --git a/scripts/test-samples.ps1 b/scripts/test-samples.ps1 index 780b9e6d..ff8657d1 100644 --- a/scripts/test-samples.ps1 +++ b/scripts/test-samples.ps1 @@ -1,15 +1,16 @@ <# .SYNOPSIS -Local orchestrator to run sample & guide tests. +Local orchestrator to run sample & guide Pester tests. .DESCRIPTION -Discovers and runs test.ps1 for each sample (or a specified subset). -Each test validates the corresponding guide workflow from scratch and -verifies the existing sample code still builds. -Reports a pass/fail summary at the end. +Discovers and runs test.Tests.ps1 for each sample (or a specified subset) +using Invoke-Pester. Each test validates the corresponding guide workflow +from scratch and verifies the existing sample code still builds. + +Requires Pester 5.x: Install-Module -Name Pester -Force -MinimumVersion 5.0 .PARAMETER Samples -One or more sample names to test. Defaults to all samples that have a test.ps1. +One or more sample names to test. Defaults to all samples that have a test.Tests.ps1. .PARAMETER WinappPath Path to the winapp npm package (.tgz or directory) passed to each test. @@ -17,16 +18,13 @@ Path to the winapp npm package (.tgz or directory) passed to each test. .PARAMETER SkipCleanup Passed through to each test — keep build artifacts for debugging. -.PARAMETER Verbose -Enable verbose output for all tests. - .EXAMPLE .\scripts\test-samples.ps1 -Run all sample tests. +Run all sample & guide tests. .EXAMPLE -.\scripts\test-samples.ps1 -Samples dotnet-app,rust-app -Verbose -Run only the dotnet-app and rust-app tests with verbose output. +.\scripts\test-samples.ps1 -Samples dotnet-app,rust-app +Run only the dotnet-app and rust-app tests. #> [CmdletBinding()] @@ -39,22 +37,30 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Ensure Pester 5.x is available +$pester = Get-Module -Name Pester -ListAvailable | Where-Object { $_.Version.Major -ge 5 } | Select-Object -First 1 +if (-not $pester) { + Write-Error "Pester 5.x is required. Install with: Install-Module -Name Pester -Force -MinimumVersion 5.0" + exit 1 +} + $samplesRoot = Join-Path $PSScriptRoot "..\samples" -# Discover samples with test.ps1 +# Discover samples with test.Tests.ps1 $allTests = @(Get-ChildItem -Path $samplesRoot -Directory | - Where-Object { Test-Path (Join-Path $_.FullName "test.ps1") } | + Where-Object { Test-Path (Join-Path $_.FullName "test.Tests.ps1") } | Select-Object -ExpandProperty Name) if ($Samples) { - # Validate requested samples exist + # Support comma-separated values (e.g., -Samples "dotnet-app,rust-app") + $Samples = @($Samples | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) foreach ($s in $Samples) { if ($s -notin $allTests) { - Write-Warning "Sample '$s' does not have a test.ps1 — skipping" + Write-Warning "Sample '$s' does not have a test.Tests.ps1 — skipping" } } $testList = @($Samples | Where-Object { $_ -in $allTests }) -} else { +}else { $testList = $allTests } @@ -63,54 +69,27 @@ if (-not $testList) { exit 0 } -Write-Host "`n$('='*80)" -ForegroundColor Cyan -Write-Host "SAMPLE & GUIDE TEST RUNNER — $($testList.Count) test(s)" -ForegroundColor Cyan -Write-Host "$('='*80)`n" -ForegroundColor Cyan - -$results = @() - -foreach ($sample in $testList) { - $testScript = Join-Path $samplesRoot $sample "test.ps1" - Write-Host "`nRunning: $sample" -ForegroundColor Yellow - Write-Host ("-" * 40) -ForegroundColor DarkGray - - $sw = [System.Diagnostics.Stopwatch]::StartNew() - try { - $params = @{} - if ($WinappPath) { $params['WinappPath'] = $WinappPath } - if ($SkipCleanup) { $params['SkipCleanup'] = $true } - if ($VerbosePreference -eq 'Continue') { $params['Verbose'] = $true } - - & $testScript @params - $sw.Stop() - $results += [PSCustomObject]@{ Sample = $sample; Status = 'PASS'; Duration = $sw.Elapsed; Error = $null } - } catch { - $sw.Stop() - Write-Host " ✗ $sample FAILED: $_" -ForegroundColor Red - $results += [PSCustomObject]@{ Sample = $sample; Status = 'FAIL'; Duration = $sw.Elapsed; Error = $_.ToString() } +# Resolve WinappPath to absolute before passing to containers +if ($WinappPath) { + if (-not [System.IO.Path]::IsPathRooted($WinappPath)) { + $WinappPath = (Resolve-Path $WinappPath -ErrorAction Stop).Path } } -# Summary -Write-Host "`n$('='*80)" -ForegroundColor Cyan -Write-Host "RESULTS SUMMARY" -ForegroundColor Cyan -Write-Host "$('='*80)" -ForegroundColor Cyan - -$passed = @($results | Where-Object Status -eq 'PASS').Count -$failed = @($results | Where-Object Status -eq 'FAIL').Count - -foreach ($r in $results) { - $color = if ($r.Status -eq 'PASS') { 'Green' } else { 'Red' } - $dur = "{0:mm\:ss}" -f $r.Duration - $line = " [{0}] {1} ({2})" -f $r.Status, $r.Sample, $dur - Write-Host $line -ForegroundColor $color - if ($r.Error) { - Write-Host " $($r.Error)" -ForegroundColor DarkRed - } +# Build Pester containers for each sample +$containers = @() +foreach ($sample in $testList) { + $testFile = Join-Path $samplesRoot $sample "test.Tests.ps1" + $data = @{} + if ($WinappPath) { $data['WinappPath'] = $WinappPath } + if ($SkipCleanup) { $data['SkipCleanup'] = $true } + $containers += New-PesterContainer -Path $testFile -Data $data } -Write-Host "`n $passed passed, $failed failed out of $($results.Count) test(s)`n" -ForegroundColor $(if ($failed -gt 0) { 'Red' } else { 'Green' }) +# Configure and run Pester +$config = New-PesterConfiguration +$config.Run.Container = $containers +$config.Run.Exit = $true +$config.Output.Verbosity = if ($VerbosePreference -eq 'Continue') { 'Detailed' } else { 'Normal' } -if ($failed -gt 0) { - exit 1 -} +Invoke-Pester -Configuration $config From f757d507bdf69ce54e81a6f035abec13751d6bdb Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:19:05 -0800 Subject: [PATCH 05/27] Fix review findings: standardize Invoke-WinappCommand usage across all sample tests - Replace all Invoke-Expression 'winapp ...' calls with Invoke-WinappCommand -Arguments in dotnet-app, packaging-cli, rust-app, and tauri-app tests. This ensures tests work in environments where winapp isn't on PATH by using the helper's fallback chain (npx -> dotnet run -> PATH). - Fix flutter-app to use -Arguments named parameter consistently. - Remove duplicate \ assignments in dotnet-app and rust-app. - Update AGENTS.md to document Context-level -Skip as acceptable when BeforeAll has prerequisite-dependent setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 2 +- samples/dotnet-app/test.Tests.ps1 | 10 +++------- samples/flutter-app/test.Tests.ps1 | 8 ++++---- samples/packaging-cli/test.Tests.ps1 | 12 ++++-------- samples/rust-app/test.Tests.ps1 | 10 +++------- samples/tauri-app/test.Tests.ps1 | 9 +++------ 6 files changed, 18 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 70af3329..3ed343ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,7 @@ Each sample under `samples/` has a self-contained **Pester 5.x** test file (`tes - **`BeforeDiscovery`**: Set `$script:skip` using inline `Get-Command` checks (no module import). Pester evaluates `-Skip:$variable` during discovery, before `BeforeAll` runs. - **`BeforeAll`**: Import `SampleTestHelpers.psm1`, install winapp, create temp directories. Guard with `if ($script:skip) { return }`. - **`AfterAll`**: Clean up temp directories using `Remove-TempTestDirectory`. -- **`It` blocks**: Use `-Skip:$script:skip` for prerequisite gating. Use Pester `Should` assertions (`Should -Be 0`, `Should -Exist`, `Should -Not -BeNullOrEmpty`). +- **`It` blocks**: Use `-Skip:$script:skip` for prerequisite gating. Use Pester `Should` assertions (`Should -Be 0`, `Should -Exist`, `Should -Not -BeNullOrEmpty`). When all setup happens in `BeforeAll` and depends on the prerequisite, you may apply `-Skip:$script:skip` to the enclosing `Context` instead. - **Shared helpers**: `Invoke-WinappCommand` (throws on failure), `Test-Prerequisite` (returns bool), `New-TempTestDirectory`, `Remove-TempTestDirectory`, `Install-WinappGlobal`. ### CI integration diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 index 580da82c..ef3bab05 100644 --- a/samples/dotnet-app/test.Tests.ps1 +++ b/samples/dotnet-app/test.Tests.ps1 @@ -21,7 +21,6 @@ Describe ".NET App Guide Workflow" { $script:tempDir = New-TempTestDirectory -Prefix "dotnet-guide" $script:projectDir = Join-Path $script:tempDir "test-dotnet-app" - $script:sampleDir = $PSScriptRoot } AfterAll { @@ -64,8 +63,7 @@ Describe ".NET App Guide Workflow" { It "Should run winapp init successfully" -Skip:$script:skip { Push-Location $script:projectDir try { - Invoke-Expression "winapp init --use-defaults" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "init --use-defaults" } finally { Pop-Location } } @@ -78,8 +76,7 @@ Describe ".NET App Guide Workflow" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:projectDir try { - Invoke-Expression "winapp cert generate --if-exists skip" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" } finally { Pop-Location } } @@ -115,8 +112,7 @@ Describe ".NET App Guide Workflow" { It "Should package MSIX with winapp pack" -Skip:$script:skip { Push-Location $script:projectDir try { - Invoke-Expression "winapp pack `"$($script:outputDir)`" --manifest appxmanifest.xml --cert devcert.pfx" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest appxmanifest.xml --cert devcert.pfx" } finally { Pop-Location } } diff --git a/samples/flutter-app/test.Tests.ps1 b/samples/flutter-app/test.Tests.ps1 index a9da13d1..8fa315a7 100644 --- a/samples/flutter-app/test.Tests.ps1 +++ b/samples/flutter-app/test.Tests.ps1 @@ -58,7 +58,7 @@ Describe "flutter-app sample" { $script:projectDir = Join-Path $script:tempDir "test_flutter_app" Set-Location $script:projectDir - Invoke-WinappCommand "init --use-defaults --setup-sdks=stable" + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=stable" flutter build windows if ($LASTEXITCODE -ne 0) { throw "flutter build windows failed" } @@ -66,8 +66,8 @@ Describe "flutter-app sample" { $script:buildOutput = Join-Path $script:projectDir "build\windows\x64\runner\Release" Copy-Item $script:buildOutput -Destination (Join-Path $script:projectDir "dist") -Recurse - Invoke-WinappCommand "cert generate --if-exists skip" - Invoke-WinappCommand "pack dist --cert devcert.pfx" + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + Invoke-WinappCommand -Arguments "pack dist --cert devcert.pfx" } It "Should create winapp.yaml after init" { @@ -102,7 +102,7 @@ Describe "flutter-app sample" { flutter pub get if ($LASTEXITCODE -ne 0) { throw "flutter pub get failed" } - Invoke-WinappCommand "restore" + Invoke-WinappCommand -Arguments "restore" flutter build windows if ($LASTEXITCODE -ne 0) { throw "flutter build windows failed" } diff --git a/samples/packaging-cli/test.Tests.ps1 b/samples/packaging-cli/test.Tests.ps1 index a9cc3816..f02a5025 100644 --- a/samples/packaging-cli/test.Tests.ps1 +++ b/samples/packaging-cli/test.Tests.ps1 @@ -44,8 +44,7 @@ Describe "Packaging CLI Guide Workflow" { It "Should generate manifest from executable" -Skip:$script:skip { Push-Location $script:packageDir try { - Invoke-Expression "winapp manifest generate --executable mycli.exe" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "manifest generate --executable mycli.exe" } finally { Pop-Location } } @@ -58,8 +57,7 @@ Describe "Packaging CLI Guide Workflow" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:packageDir try { - Invoke-Expression "winapp cert generate --if-exists skip" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" } finally { Pop-Location } } @@ -80,8 +78,7 @@ Describe "Packaging CLI Guide Workflow" { It "Should package as MSIX" -Skip:$script:skip { Push-Location $script:packageDir try { - Invoke-Expression "winapp pack . --cert devcert.pfx" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "pack . --cert devcert.pfx" } finally { Pop-Location } } @@ -95,8 +92,7 @@ Describe "Packaging CLI Guide Workflow" { It "Should sign the MSIX" -Skip:$script:skip { Push-Location $script:packageDir try { - Invoke-Expression "winapp sign `"$($script:msixPath)`" devcert.pfx" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "sign `"$($script:msixPath)`" devcert.pfx" } finally { Pop-Location } } } diff --git a/samples/rust-app/test.Tests.ps1 b/samples/rust-app/test.Tests.ps1 index f66ba7d3..098e5a3b 100644 --- a/samples/rust-app/test.Tests.ps1 +++ b/samples/rust-app/test.Tests.ps1 @@ -19,7 +19,6 @@ Describe "Rust App Sample" { $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath Install-WinappGlobal -PackagePath $resolvedPkg - $script:sampleDir = $PSScriptRoot $script:tempDir = New-TempTestDirectory -Prefix "rust-guide" } @@ -54,8 +53,7 @@ Describe "Rust App Sample" { It "Should run winapp init" -Skip:$script:skip { Push-Location $script:rustProjectDir try { - Invoke-Expression "winapp init --use-defaults --setup-sdks=none" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=none" } finally { Pop-Location } } @@ -85,8 +83,7 @@ Describe "Rust App Sample" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:rustProjectDir try { - Invoke-Expression "winapp cert generate --if-exists skip" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" } finally { Pop-Location } } @@ -105,8 +102,7 @@ Describe "Rust App Sample" { It "Should package as MSIX" -Skip:$script:skip { Push-Location $script:rustProjectDir try { - Invoke-Expression "winapp pack dist --manifest appxmanifest.xml --cert devcert.pfx" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "pack dist --manifest appxmanifest.xml --cert devcert.pfx" } finally { Pop-Location } } diff --git a/samples/tauri-app/test.Tests.ps1 b/samples/tauri-app/test.Tests.ps1 index 3b87de2c..53f14726 100644 --- a/samples/tauri-app/test.Tests.ps1 +++ b/samples/tauri-app/test.Tests.ps1 @@ -64,8 +64,7 @@ Describe "Tauri App Sample" { It "Should run winapp init" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-Expression "winapp init --use-defaults --setup-sdks=none" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=none" } finally { Pop-Location } } @@ -99,8 +98,7 @@ Describe "Tauri App Sample" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-Expression "winapp cert generate --if-exists skip --manifest appxmanifest.xml" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "cert generate --if-exists skip --manifest appxmanifest.xml" } finally { Pop-Location } } @@ -111,8 +109,7 @@ Describe "Tauri App Sample" { It "Should package as MSIX" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-Expression "winapp pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" - $LASTEXITCODE | Should -Be 0 + Invoke-WinappCommand -Arguments "pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" } finally { Pop-Location } } From 16b1a76c405f307430aac0f75621bd604d0dc3f5 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:46:08 -0700 Subject: [PATCH 06/27] Consolidate Electron e2e test into Pester sample test framework Migrate the full Electron guide workflow from scripts/test-e2e-electron.ps1 into samples/electron/test.Tests.ps1 as Phase 1, making electron consistent with all other sample tests (Phase 1: from-scratch guide + Phase 2: sample freshness check). Changes: - Rewrite electron test.Tests.ps1 with Phase 1 covering: Electron app creation, winapp init, C++/C# addon creation and build, debug identity, Electron packaging, certificate generation, and MSIX packaging. - Remove e2e-test job from build-package.yml (now covered by test-samples.yml). - Delete scripts/test-e2e-electron.ps1 (replaced by Pester test). - Add .NET SDK setup for electron in test-samples.yml (needed for C# addon). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build-package.yml | 23 -- .github/workflows/test-samples.yml | 2 +- samples/electron/test.Tests.ps1 | 179 +++++++++-- scripts/test-e2e-electron.ps1 | 457 ---------------------------- 4 files changed, 160 insertions(+), 501 deletions(-) delete mode 100644 scripts/test-e2e-electron.ps1 diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index 62b17ff7..49e2fbdb 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -163,26 +163,3 @@ jobs: body: updatedBody, }); core.info(`Marked comment ${existing.id} as stale.`); - - # E2E test for Electron workflow - runs after build completes - e2e-test: - runs-on: windows-latest - needs: build-and-package - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '24' - - - name: Download npm package artifact - uses: actions/download-artifact@v4 - with: - name: npm-package - path: artifacts/npm - - - name: Run E2E Electron test - run: | - .\scripts\test-e2e-electron.ps1 -ArtifactsPath "artifacts/npm" -Verbose \ No newline at end of file diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index 3aee8be2..439c53c2 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -82,7 +82,7 @@ jobs: - name: Setup .NET if: >- steps.check.outputs.skip != 'true' && - contains(fromJson('["dotnet-app", "wpf-app", "packaging-cli"]'), matrix.sample) + contains(fromJson('["dotnet-app", "wpf-app", "packaging-cli", "electron"]'), matrix.sample) uses: actions/setup-dotnet@v5 with: dotnet-version: '10.0.x' diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 index 0a972970..a2f4806d 100644 --- a/samples/electron/test.Tests.ps1 +++ b/samples/electron/test.Tests.ps1 @@ -1,3 +1,20 @@ +<# +.SYNOPSIS +Pester 5.x tests for the Electron sample and guide workflow. + +.DESCRIPTION +Phase 1: Follows the Electron guide from scratch — scaffolds an Electron app, + installs winapp, initializes workspace, creates and builds C++/C# addons, + packages the app, and creates a signed MSIX package. +Phase 2: Quick install of the existing sample to verify it is not stale. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. +#> + param( [string]$WinappPath, [switch]$SkipCleanup @@ -7,52 +24,174 @@ BeforeDiscovery { $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) } -Describe "Electron Sample Freshness Check" { +Describe "Electron Sample" { BeforeAll { Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) $script:sampleDir = $PSScriptRoot - $script:originalLocation = Get-Location + $script:tempDir = $null + $script:appDir = $null + $script:resolvedPkg = $null + + if (-not $script:skip) { + $script:resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + } } AfterAll { Set-Location $script:sampleDir if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } Remove-Item -Path (Join-Path $script:sampleDir 'node_modules') -Recurse -Force -ErrorAction SilentlyContinue } } - Context "Prerequisites" { - It "Should have Node.js available" -Skip:$script:skip { - Test-Prerequisite 'node' | Should -Be $true + Context "Phase 1: Electron Guide Workflow (from scratch)" { + BeforeAll { + if (-not $script:skip) { + $script:tempDir = New-TempTestDirectory -Prefix "electron-guide" + + # Use a dedicated npm cache to avoid ECOMPROMISED errors in CI + $npmCacheDir = Join-Path $script:tempDir ".npm-cache" + $null = New-Item -ItemType Directory -Path $npmCacheDir -Force + $env:npm_config_cache = $npmCacheDir + } } - It "Should have npm available" -Skip:$script:skip { - Test-Prerequisite 'npm' | Should -Be $true + It "Should create a new Electron app" -Skip:$script:skip { + Push-Location $script:tempDir + try { + $maxRetries = 3 + $created = $false + for ($i = 1; $i -le $maxRetries; $i++) { + if ($i -gt 1) { + Remove-Item -Path (Join-Path $script:tempDir "electron-app") -Recurse -Force -ErrorAction SilentlyContinue + Invoke-Expression "npm cache clean --force" 2>$null + Start-Sleep -Seconds 2 + } + Invoke-Expression "npx -y create-electron-app@7.11.1 electron-app --template=webpack" + if ($LASTEXITCODE -eq 0) { $created = $true; break } + } + $created | Should -Be $true -Because "Electron app creation should succeed within $maxRetries attempts" + $script:appDir = Join-Path $script:tempDir "electron-app" + $script:appDir | Should -Exist + } finally { Pop-Location } } - } - Context "Sample Build Check" { - BeforeAll { - if (-not $script:skip) { - Push-Location $script:sampleDir - } + It "Should configure package.json for MSIX" -Skip:$script:skip { + $pkgPath = Join-Path $script:appDir "package.json" + $pkg = Get-Content $pkgPath | ConvertFrom-Json + $pkg | Add-Member -MemberType NoteProperty -Name "displayName" -Value "WinApp Electron Test" -Force + $pkg | Add-Member -MemberType NoteProperty -Name "description" -Value "Test app for winapp CLI" -Force + if ([string]::IsNullOrEmpty($pkg.version)) { $pkg.version = "1.0.0" } + $pkg | ConvertTo-Json -Depth 10 | Set-Content $pkgPath + $pkgPath | Should -Exist } - AfterAll { - if (-not $script:skip) { - Set-Location $script:originalLocation - } + It "Should install winapp as a local devDependency" -Skip:$script:skip { + Push-Location $script:appDir + try { + Install-WinappNpmPackage -PackagePath $script:resolvedPkg + Join-Path $script:appDir "node_modules\.bin\winapp.cmd" | Should -Exist + } finally { Pop-Location } + } + + It "Should initialize winapp workspace" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable" + } finally { Pop-Location } } + It "Should create workspace files" -Skip:$script:skip { + Join-Path $script:appDir ".winapp" | Should -Exist + Join-Path $script:appDir "winapp.yaml" | Should -Exist + Join-Path $script:appDir "appxmanifest.xml" | Should -Exist + } + + It "Should create a C++ native addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "node create-addon --template cpp --name testCppAddon" + Join-Path $script:appDir "testCppAddon" | Should -Exist + Join-Path $script:appDir "testCppAddon\binding.gyp" | Should -Exist + } finally { Pop-Location } + } + + It "Should create a C# native addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "node create-addon --template cs --name testCsAddon" + Join-Path $script:appDir "testCsAddon" | Should -Exist + Join-Path $script:appDir "testCsAddon\testCsAddon.csproj" | Should -Exist + } finally { Pop-Location } + } + + It "Should build the C++ addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-Expression "npm run build-testCppAddon" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should build the C# addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-Expression "npm run build-testCsAddon" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should add Electron debug identity" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "node add-electron-debug-identity --no-install" + } finally { Pop-Location } + } + + It "Should package the Electron app" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-Expression "npm run package" + $LASTEXITCODE | Should -Be 0 + $script:outDir = Join-Path $script:appDir "out" + $script:outDir | Should -Exist + $script:appPackageDir = (Get-ChildItem -Path $script:outDir -Directory | Select-Object -First 1).FullName + $script:appPackageDir | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + + It "Should generate a development certificate" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "cert generate" + Join-Path $script:appDir "devcert.pfx" | Should -Exist + } finally { Pop-Location } + } + + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:appDir + try { + $certPath = Join-Path $script:appDir "devcert.pfx" + Invoke-WinappCommand -Arguments "pack `"$($script:appPackageDir)`" --cert `"$certPath`"" + Get-ChildItem -Path $script:appDir -Filter "*.msix" | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + } + + Context "Phase 2: Sample Build Check" { It "Should install sample dependencies" -Skip:$script:skip { - Invoke-Expression "npm install --ignore-scripts" - $LASTEXITCODE | Should -Be 0 + Push-Location $script:sampleDir + try { + Invoke-Expression "npm install --ignore-scripts" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } } - It "Should have created node_modules" -Skip:$script:skip { + It "Should have node_modules" -Skip:$script:skip { Join-Path $script:sampleDir 'node_modules' | Should -Exist } diff --git a/scripts/test-e2e-electron.ps1 b/scripts/test-e2e-electron.ps1 deleted file mode 100644 index e26e5eb2..00000000 --- a/scripts/test-e2e-electron.ps1 +++ /dev/null @@ -1,457 +0,0 @@ -<# -.SYNOPSIS -End-to-end test for WinApp CLI with Electron framework. - -.DESCRIPTION -This script tests the complete WinApp CLI workflow with Electron: -1. Creates a new Electron application -2. Installs the locally-built winapp npm package -3. Runs 'winapp init' with non-interactive mode -4. Creates C++ and C# native addons -5. Builds the addons to validate they compile -6. Adds Electron debug identity -7. Packages the app to MSIX -8. Signs the MSIX package - -The test creates a 'test-wd' directory in the repo root for the test project and cleans it up after completion. - -.PARAMETER ArtifactsPath -Path to the artifacts folder containing the built winapp npm package. -Default: "$PSScriptRoot\..\artifacts\npm" - -.PARAMETER NpmPackagePath -Path to the winapp npm package. If not specified, uses the one from ArtifactsPath. -Default: "$PSScriptRoot\..\src\winapp-npm" - -.PARAMETER SkipCleanup -If specified, does not delete the test project after completion (useful for debugging). - -.PARAMETER Verbose -Enable verbose output for debugging. - -.EXAMPLE -.\test-e2e-electron.ps1 -Run the test with default settings. - -.EXAMPLE -.\test-e2e-electron.ps1 -SkipCleanup -Verbose -Run the test, keep the project folder, and show detailed output. -#> - -param( - [string]$ArtifactsPath = "$PSScriptRoot\..\artifacts\npm", - [string]$NpmPackagePath = "$PSScriptRoot\..\src\winapp-npm", - [switch]$SkipCleanup, - [switch]$Verbose -) - -# Enable strict mode -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$VerbosePreference = if ($Verbose) { 'Continue' } else { 'SilentlyContinue' } - -# ============================================================================ -# Helper Functions -# ============================================================================ - -function Write-TestHeader { - param([string]$Message) - Write-Host "`n$('='*80)" -ForegroundColor Cyan - Write-Host "TEST: $Message" -ForegroundColor Cyan - Write-Host "$('='*80)`n" -ForegroundColor Cyan -} - -function Write-TestStep { - param([string]$Message, [int]$Step) - Write-Host "[$Step] $Message" -ForegroundColor Yellow -} - -function Write-TestSuccess { - param([string]$Message) - Write-Host "✓ $Message" -ForegroundColor Green -} - -function Write-TestError { - param([string]$Message) - Write-Host "✗ $Message" -ForegroundColor Red -} - -function Assert-Command { - param( - [string]$Command, - [string]$FailMessage - ) - Write-Verbose "Running: $Command" - $result = Invoke-Expression $Command - if ($LASTEXITCODE -ne 0) { - Write-TestError $FailMessage - throw $FailMessage - } - Write-TestSuccess "$Command" - return $result -} - -function Assert-FileExists { - param( - [string]$Path, - [string]$Description - ) - if (-not (Test-Path $Path)) { - Write-TestError "$Description not found at $Path" - throw "$Description not found at $Path" - } - Write-TestSuccess "$Description exists: $Path" -} - -function Assert-DirectoryExists { - param( - [string]$Path, - [string]$Description - ) - if (-not (Test-Path $Path -PathType Container)) { - Write-TestError "$Description not found at $Path" - throw "$Description not found at $Path" - } - Write-TestSuccess "$Description exists: $Path" -} - -# ============================================================================ -# Validation -# ============================================================================ - -Write-TestHeader "E2E Electron Test - Validation Phase" - -Write-TestStep "Validating prerequisites..." 1 - -# Check Node.js -try { - $nodeVersion = node --version - Write-TestSuccess "Node.js found: $nodeVersion" -} catch { - Write-TestError "Node.js is not installed or not in PATH" - throw "Node.js is required but not found" -} - -# Check npm -try { - $npmVersion = npm --version - Write-TestSuccess "npm found: $npmVersion" -} catch { - Write-TestError "npm is not installed or not in PATH" - throw "npm is required but not found" -} - - - -# Verify artifacts path or npm package path -if ($ArtifactsPath -and (Test-Path $ArtifactsPath)) { - # Convert to absolute path to ensure it works after directory changes - $resolvedArtifactsPath = (Resolve-Path $ArtifactsPath).Path - - # Check if this is a directory containing .tgz files (from CI artifact download) - # or a directory with package.json (local npm package) - $tgzFiles = Get-ChildItem -Path $resolvedArtifactsPath -Filter "*.tgz" -ErrorAction SilentlyContinue - if ($tgzFiles) { - # Use the first .tgz file found - $localNpmPackagePath = $tgzFiles[0].FullName - Write-TestSuccess "Found npm tarball: $localNpmPackagePath" - } elseif (Test-Path (Join-Path $resolvedArtifactsPath "package.json")) { - # It's a directory with package.json (local development) - $localNpmPackagePath = $resolvedArtifactsPath - Write-TestSuccess "Found npm package directory: $localNpmPackagePath" - } else { - Write-TestError "Artifacts path exists but contains no .tgz files or package.json: $resolvedArtifactsPath" - throw "Invalid artifacts path - no installable npm package found" - } -} elseif (Test-Path $NpmPackagePath) { - Write-TestSuccess "npm package found: $NpmPackagePath" - # Convert to absolute path to ensure it works after directory changes - $localNpmPackagePath = (Resolve-Path $NpmPackagePath).Path -} else { - Write-TestError "Neither artifacts path nor npm package path exists" - throw "Cannot find winapp npm package at $ArtifactsPath or $NpmPackagePath" -} - -Write-Verbose "Using npm package path: $localNpmPackagePath" - -# ============================================================================ -# Setup Test Environment -# ============================================================================ - -Write-TestHeader "E2E Electron Test - Setup Phase" - -Write-TestStep "Creating test directory..." 2 - -$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path -$testDir = Join-Path $repoRoot "test-wd" - -# Clean up any existing test directory -if (Test-Path $testDir) { - Remove-Item -Path $testDir -Recurse -Force -ErrorAction SilentlyContinue -} - -$null = New-Item -ItemType Directory -Path $testDir -Force -Write-TestSuccess "Test directory created: $testDir" - -# Save original location to restore on exit -$originalLocation = Get-Location - -try { - Push-Location $testDir - Write-Verbose "Working directory: $(Get-Location)" - - # ======================================================================== - # Configure npm for CI environment - # ======================================================================== - - # Set a unique npm cache directory to avoid ECOMPROMISED errors in CI - # This prevents conflicts with concurrent builds and stale cache issues - $npmCacheDir = Join-Path $testDir ".npm-cache" - $null = New-Item -ItemType Directory -Path $npmCacheDir -Force - $env:npm_config_cache = $npmCacheDir - Write-Verbose "npm cache directory set to: $npmCacheDir" - - # ======================================================================== - # Create Electron Application - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Create Electron App Phase" - - Write-TestStep "Creating new Electron app..." 3 - - # Use Electron Forge to scaffold basic app (no webpack) - # Retry logic for CI environments where npm can have transient failures - $maxRetries = 3 - $retryCount = 0 - $electronAppCreated = $false - - while (-not $electronAppCreated -and $retryCount -lt $maxRetries) { - $retryCount++ - Write-Verbose "Attempt $retryCount of $maxRetries to create Electron app..." - - try { - # Use --prefer-offline to reduce network issues and clear package-lock before retry - if ($retryCount -gt 1) { - Write-Verbose "Cleaning up failed attempt..." - Remove-Item -Path (Join-Path $testDir "electron-app") -Recurse -Force -ErrorAction SilentlyContinue - npm cache clean --force 2>$null - Start-Sleep -Seconds 2 - } - - $electronCommand = "npx -y create-electron-app@7.11.1 electron-app --template=webpack" - Write-Verbose "Running: $electronCommand" - Invoke-Expression $electronCommand - - if ($LASTEXITCODE -eq 0) { - $electronAppCreated = $true - Write-TestSuccess "Electron app created successfully" - } else { - Write-Verbose "npx command failed with exit code $LASTEXITCODE" - } - } catch { - Write-Verbose "Exception during Electron app creation: $_" - } - } - - if (-not $electronAppCreated) { - Write-TestError "Failed to create Electron app after $maxRetries attempts" - throw "Failed to create Electron app" - } - - $electronAppDir = Join-Path $testDir "electron-app" - Assert-DirectoryExists $electronAppDir "Electron app directory" - - Push-Location $electronAppDir - - # Update package.json to add required fields for MSIX - Write-TestStep "Configuring package.json for Windows packaging..." 4 - - $packageJsonPath = Join-Path $electronAppDir "package.json" - $packageJson = Get-Content $packageJsonPath | ConvertFrom-Json - - # Add required fields for MSIX packaging - $packageJson | Add-Member -MemberType NoteProperty -Name "displayName" -Value "WinApp Electron Test" -Force - $packageJson | Add-Member -MemberType NoteProperty -Name "description" -Value "E2E test application for WinApp CLI" -Force - - # Ensure version is set - if ([string]::IsNullOrEmpty($packageJson.version)) { - $packageJson.version = "1.0.0" - } - - $packageJson | ConvertTo-Json -Depth 10 | Set-Content $packageJsonPath - Write-TestSuccess "package.json configured" - - # ======================================================================== - # Install WinApp npm package - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Install WinApp Phase" - - Write-TestStep "Installing winapp npm package from local artifacts..." 5 - - # Install the local winapp package - $installCommand = "npm install $localNpmPackagePath --save-dev" - Assert-Command $installCommand "Failed to install winapp npm package" - - # Verify winapp is installed - $nodeModulesPath = Join-Path $electronAppDir "node_modules" ".bin" "winapp" - $winappCli = Join-Path $electronAppDir "node_modules" ".bin" "winapp.cmd" - Assert-FileExists $winappCli "winapp CLI" - - # ======================================================================== - # Initialize WinApp Workspace - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Initialize Workspace Phase" - - Write-TestStep "Running 'winapp init' with non-interactive mode..." 6 - - # Use --use-defaults for non-interactive initialization - # Setup stable SDKs for packaging - $initCommand = "npx winapp init . --use-defaults --setup-sdks=stable" - Assert-Command $initCommand "Failed to initialize winapp workspace" - - # Verify workspace was created - Assert-DirectoryExists ".winapp" ".winapp directory" - Assert-FileExists "winapp.yaml" "winapp.yaml configuration file" - Assert-FileExists "appxmanifest.xml" "appxmanifest.xml manifest file" - - # ======================================================================== - # Create Native Addons - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Create Native Addons Phase" - - Write-TestStep "Creating C++ addon..." 7 - - $addCppCommand = "npx winapp node create-addon --template cpp --name testCppAddon" - Assert-Command $addCppCommand "Failed to create C++ addon" - - Assert-DirectoryExists "testCppAddon" "C++ addon directory" - Assert-FileExists "testCppAddon\binding.gyp" "C++ addon binding.gyp file" - - Write-TestSuccess "C++ addon created" - - Write-TestStep "Creating C# addon..." 8 - - $addCsharpCommand = "npx winapp node create-addon --template cs --name testCsAddon" - Assert-Command $addCsharpCommand "Failed to create C# addon" - - Assert-DirectoryExists "testCsAddon" "C# addon directory" - Assert-FileExists "testCsAddon\testCsAddon.csproj" "C# addon project file" - - Write-TestSuccess "C# addon created" - - # ======================================================================== - # Build Native Addons - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Build Addons Phase" - - Write-TestStep "Building C++ addon..." 9 - - $buildCppCommand = "npm run build-testCppAddon" - Assert-Command $buildCppCommand "Failed to build C++ addon" - - Write-TestSuccess "C++ addon built successfully" - - Write-TestStep "Building C# addon..." 10 - - $buildCsCommand = "npm run build-testCsAddon" - Assert-Command $buildCsCommand "Failed to build C# addon" - - Write-TestSuccess "C# addon built successfully" - - # ======================================================================== - # Add Electron Debug Identity - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Debug Identity Phase" - - Write-TestStep "Adding Electron debug identity..." 11 - - $addIdentityCommand = "npx winapp node add-electron-debug-identity --no-install" - Assert-Command $addIdentityCommand "Failed to add Electron debug identity" - - # ======================================================================== - # Package Application - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Package Phase" - - Write-TestStep "Building Electron application package..." 12 - - # First, run npm package to create the packaged app - $packageCommand = "npm run package" - Assert-Command $packageCommand "Failed to package Electron app" - - # Find the output directory created by electron-forge - $outDir = Join-Path $electronAppDir "out" - if (-not (Test-Path $outDir)) { - Write-TestError "Electron package output directory not found at $outDir" - throw "Electron app packaging did not create output directory" - } - - # Find the app package directory (typically 'out/' or similar) - $appPackageDirs = Get-ChildItem -Path $outDir -Directory -ErrorAction SilentlyContinue - if (-not $appPackageDirs) { - Write-TestError "No app package directories found in $outDir" - throw "Electron app package not created" - } - - $appPackageDir = $appPackageDirs[0].FullName - Write-TestSuccess "Electron app packaged to: $appPackageDir" - - Write-TestStep "Generating development certificate..." 13 - - $certGenCommand = "npx winapp cert generate" - Assert-Command $certGenCommand "Failed to generate development certificate" - - $certPath = Join-Path $electronAppDir "devcert.pfx" - Assert-FileExists $certPath "Development certificate" - - Write-TestStep "Packaging app to MSIX..." 14 - - $packCommand = "npx winapp pack `"$appPackageDir`" --cert `"$certPath`"" - Assert-Command $packCommand "Failed to package app to MSIX" - - # Verify MSIX was created (winapp pack outputs to the project root) - $msixFiles = Get-ChildItem -Path $electronAppDir -Filter "*.msix" -ErrorAction SilentlyContinue - if ($msixFiles) { - Write-TestSuccess "MSIX package created and signed: $($msixFiles[0].Name)" - } else { - Write-TestError "No MSIX file found after packaging" - throw "MSIX packaging failed - no output file generated" - } - - # ======================================================================== - # Final Verification - # ======================================================================== - - Write-Host "`n$('='*80)" -ForegroundColor Green - Write-Host "E2E ELECTRON TEST COMPLETED SUCCESSFULLY" -ForegroundColor Green - Write-Host "$('='*80)`n" -ForegroundColor Green - -} finally { - # Restore to original location (handles any number of Push-Location calls) - Set-Location $originalLocation - - # ======================================================================== - # Cleanup - # ======================================================================== - - if (-not $SkipCleanup) { - Write-TestHeader "Cleanup" - Write-TestStep "Cleaning up temporary test directory..." 15 - - try { - Remove-Item -Path $testDir -Recurse -Force -ErrorAction SilentlyContinue - Write-TestSuccess "Test directory cleaned up: $testDir" - } catch { - Write-Verbose "Warning: Could not fully clean up test directory: $_" - } - } else { - Write-Host "Test directory preserved at: $testDir" -ForegroundColor Yellow - } -} From 355fa72d15317c71fd9221d7c66b8a8823e2af30 Mon Sep 17 00:00:00 2001 From: Nikola Metulev <711864+nmetulev@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:31:54 -0700 Subject: [PATCH 07/27] Add pull_request trigger to sample tests workflow The workflow_run trigger only works for workflow files on the default branch, so test-samples.yml won't trigger until merged. Adding pull_request trigger lets sample tests run as proper PR checks. For PR events, a build job produces the npm-package artifact first. The test-sample jobs then download it, same as workflow_run/dispatch. The build job is skipped for non-PR triggers since the artifact comes from the Build and Package workflow instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-samples.yml | 47 ++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index 439c53c2..b03c9e8e 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -1,6 +1,8 @@ name: Test Samples & Guides on: + pull_request: + branches: [ "main" ] workflow_run: workflows: ["Build and Package"] types: [completed] @@ -27,11 +29,37 @@ permissions: actions: read # Required for downloading cross-workflow artifacts jobs: + # Build npm package for pull_request events (workflow_run already has it) + build: + if: github.event_name == 'pull_request' + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + - uses: actions/setup-node@v5 + with: + node-version: '24' + - name: Build npm package + shell: pwsh + run: .\scripts\build-cli.ps1 -SkipTests -SkipMsix + - uses: actions/upload-artifact@v4 + with: + name: npm-package + path: artifacts/*.tgz + test-sample: - # Run on successful builds or manual dispatch + needs: [build] + # Run when: PR build succeeded, workflow_run build succeeded, or manual dispatch if: >- - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' + always() && + (needs.build.result == 'success' || needs.build.result == 'skipped') && + ( + github.event_name == 'pull_request' || + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + ) strategy: fail-fast: false matrix: @@ -55,7 +83,16 @@ jobs: if: steps.check.outputs.skip != 'true' uses: actions/checkout@v5 - # Download the npm package artifact from the triggering build + # Download the npm package artifact — source varies by trigger + - name: Download npm package (pull_request) + if: >- + steps.check.outputs.skip != 'true' && + github.event_name == 'pull_request' + uses: actions/download-artifact@v4 + with: + name: npm-package + path: artifacts/npm + - name: Download npm package (workflow_run) if: >- steps.check.outputs.skip != 'true' && @@ -148,7 +185,7 @@ jobs: # Summary job to provide a single check status for branch protection test-samples-result: if: always() - needs: test-sample + needs: [build, test-sample] runs-on: ubuntu-latest steps: - name: Check results From 9c17a1af8111cd5fea5ee8a7edeebd1d02a82605 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:37:25 -0400 Subject: [PATCH 08/27] Changes to the packaging cli guide --- docs/guides/packaging-cli.md | 63 +++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/docs/guides/packaging-cli.md b/docs/guides/packaging-cli.md index 69bd84bc..40fb87c5 100644 --- a/docs/guides/packaging-cli.md +++ b/docs/guides/packaging-cli.md @@ -22,9 +22,10 @@ cd MyCliPackage ### 2. Install winapp CLI -The quickest way to get started is to install winapp CLI via Windows Package Manager: +Install the winapp CLI via Windows Package Manager, or update to the latest version if you already have it: ```powershell +# Install (or update if already installed) winget install microsoft.winappcli --source winget ``` @@ -40,14 +41,11 @@ This command creates an `appxmanifest.xml` file in the current directory with de ### 4. Configure the Manifest -You'll need to edit the generated `appxmanifest.xml` to: -- Add an execution alias so users can run your CLI from any directory -- Hide the app from the Start menu app list -- Update application details to match your CLI +Edit the generated `appxmanifest.xml` to customize your package. Each sub-step below explains what to change and why. #### 4.1 Add Required Namespace -Add the `uap5` namespace to the `Package` element if it's not already present: +Add the `uap5` namespace to the `Package` element if it's not already present. This is needed for the execution alias in step 4.3: ```xml ` element, add `AppListEntry="none"` to prevent the app from appearing in the Start menu: +In the `` element, add `AppListEntry="none"` to hide the app from the Start menu. CLI tools are invoked from the terminal, so they don't need a Start menu entry: ```xml ` element, add `AppListEntry="none"` to prevent the #### 4.3 Add Execution Alias Extension -Add the execution alias extension within the `` element (after ``): +Add an execution alias so users can run your CLI by name from any terminal window. Add this within the `` element (after ``): ```xml @@ -92,7 +90,9 @@ Replace `yourcli.exe` with the desired command name for your CLI. Once a user in #### 4.4 Update Application Metadata -Update the following fields to match your CLI application: +Update the following fields to match your CLI application. + +> **Important**: The `Publisher` value in your manifest must match the publisher in your signing certificate. If you generate a certificate later (step 5), it will use the publisher from your manifest. If you change the publisher after generating a certificate, you'll need to regenerate the certificate to match. - **Identity**: Update `Name`, `Publisher`, and `Version` ```xml @@ -131,7 +131,7 @@ Update the following fields to match your CLI application: For local testing and distribution outside the Microsoft Store, you'll need to sign your MSIX package with a certificate. -Generate a development certificate: +Generate a development certificate. Keep it outside your CLI folder to avoid accidentally including it in the package: ```powershell # Navigate to a location outside your CLI folder (e.g., your home directory) @@ -139,28 +139,55 @@ cd ~ winapp cert generate ``` -This creates a `devcert.pfx` file. To trust this certificate on your development machine, install it (requires administrator privileges): +This creates a `devcert.pfx` file in your home directory (e.g., `C:\Users\yourname\devcert.pfx`). + +To trust this certificate on your development machine, install it (requires administrator privileges): ```powershell # Run PowerShell as Administrator -winapp cert install +winapp cert install ~\devcert.pfx ``` -**Important**: Keep your development certificate outside the folder containing your CLI executable to avoid accidentally including it in the package. - ### 6. Package Your CLI Now you're ready to create the MSIX package: ```powershell -# Run from outside CLI folder +# Navigate back outside of your project folder # Package with dev certificate (for local testing/distribution) -winapp pack .\MyCliPackage --cert path\to\devcert.pfx +winapp pack .\path\to\MyCliPackage --cert .\path\to\devcert.pfx +``` + +This creates an `.msix` file in the current directory. + +### 7. Install and Verify + +Install the MSIX package to verify everything works: + +```powershell +Add-AppxPackage .\MyCliPackage.msix ``` -This creates an `.msix` file in the current directory +If you added an execution alias in step 4.3, you can now run your CLI from any terminal: + +```powershell +yourcli --help +``` + +To uninstall later: + +```powershell +Get-AppxPackage *YourCLI* | Remove-AppxPackage +``` + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline + +## Tips -### Tips: 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. 3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) \ No newline at end of file From c1be1395fcb0f3e82038de165ea0f66f44a907f7 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:14:09 -0400 Subject: [PATCH 09/27] dotnet tests improved and added winapp run tests for dotnet apps --- docs/guides/dotnet.md | 45 +++++++++++++++++++++------- docs/guides/packaging-cli.md | 14 ++++----- samples/dotnet-app/dotnet-app.csproj | 10 +++---- samples/dotnet-app/test.Tests.ps1 | 36 ++++++++++++++++++++++ 4 files changed, 82 insertions(+), 23 deletions(-) diff --git a/docs/guides/dotnet.md b/docs/guides/dotnet.md index ccdecfae..28b0ee8e 100644 --- a/docs/guides/dotnet.md +++ b/docs/guides/dotnet.md @@ -10,12 +10,12 @@ A standard executable (like one created with `dotnet build`) does not have packa ## Prerequisites -1. **.NET SDK**: Install the .NET SDK: +1. **.NET SDK**: Install the .NET SDK (requires a restart after installation): ```powershell winget install Microsoft.DotNet.SDK.10 --source winget ``` -2. **winapp CLI**: Install the `winapp` tool via winget: +2. **winapp CLI**: Install the `winapp` tool via winget (or update if already installed): ```powershell winget install Microsoft.winappcli --source winget ``` @@ -104,6 +104,14 @@ This command will: You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. +To verify the packages were added to your project: + +```powershell +dotnet list package +``` + +You should see `Microsoft.WindowsAppSDK` and `Microsoft.Windows.SDK.BuildTools` in the output. + ## 5. Debug with Identity To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp create-debug-identity`. This applies a temporary identity to your executable using the manifest we just generated. @@ -113,8 +121,10 @@ To test features that require identity (like Notifications) without fully packag dotnet build -c Debug ``` + > **Note**: You may see NuGet vulnerability warnings (NU1900) about package sources. These are safe to ignore — they don't affect your build. + 2. **Apply Debug Identity**: - Run the following command on your built executable: + Run the following command on your built executable (replace `dotnet-app` with your project name if different): ```powershell winapp create-debug-identity .\bin\Debug\net10.0-windows10.0.26100.0\dotnet-app.exe ``` @@ -146,9 +156,13 @@ To streamline your development workflow, you can configure MSBuild to automatica With this configuration, simply running `dotnet build` or `dotnet run` will automatically apply the debug identity, and you can immediately run the executable with identity without the manual step. -## 6. Using Windows App SDK +> **When to skip this**: If you prefer explicit control over when identity is applied, or if you're working on code that doesn't need identity for most of your development cycle, the manual approach above may be simpler. -If you ran `winapp init` (Step 4), `Microsoft.WindowsAppSDK` was already added as a NuGet package reference to your `.csproj`. If you skipped SDK setup during init, or need to add it manually, run: +## 6. Using Windows App SDK (Optional) + +The Windows App SDK gives you access to modern Windows APIs beyond what the base Windows SDK provides — things like the notification system, windowing APIs, app lifecycle management, and on-device AI. If your app needs any of these capabilities, this step is for you. If you just need package identity for distribution, you can skip to step 7. + +If you ran `winapp init` (Step 4), `Microsoft.WindowsAppSDK` was already added as a NuGet package reference to your `.csproj`. You can verify with `dotnet list package`. If you skipped SDK setup during init, or need to add it manually, run: ```powershell dotnet add package Microsoft.WindowsAppSDK @@ -156,7 +170,7 @@ dotnet add package Microsoft.WindowsAppSDK ### Update Program.cs -Let's update the app to use the Windows App Runtime API to get the runtime version: +Replace the entire contents of `Program.cs` with the following code, which adds a Windows App Runtime version check: ```csharp using Windows.ApplicationModel; @@ -186,7 +200,7 @@ class Program ### Build and Run -Rebuild and run the application with Windows App SDK. Since we've added the WinAppSDK, we need to re-generate the debug identity, so `winapp` adds the runtime dependency to the WinAppSDK. If you updated the csproj to auto set debug identity, simply run `dotnet run`. Otherwise: +Rebuild and run the application with Windows App SDK. Since we've added the WinAppSDK, we need to re-generate the debug identity, so `winapp` adds the runtime dependency to the WinAppSDK. If you updated the csproj to auto set debug identity, simply run `dotnet run`. Otherwise (replace `dotnet-app` with your project name): ```powershell dotnet build -c Debug @@ -219,8 +233,10 @@ First, build your application in release mode for optimal performance: dotnet build -c Release ``` +> **Note**: You may see NuGet vulnerability warnings (NU1900). These are safe to ignore and don't affect your build output. + ### Add Execution Alias (for console apps) -To allow users to run your app from the command line after installation (like `dotnet-app`), add an execution alias to the `appxmanifest.xml`. If you are building a WPF or WinForms app, this step is not necessary. +To allow users to run your app from the command line after installation (like `dotnet-app`), add an execution alias to the `appxmanifest.xml`. If you are building a WPF or WinForms app, this step is not necessary — those apps launch from the Start menu instead. Open `appxmanifest.xml` and add the `uap5` namespace to the `` tag if it's missing, and then add the extension inside `...`: @@ -262,7 +278,7 @@ winapp cert generate --if-exists skip ### Sign and Pack -Now you can package and sign. Point the pack command to your build output folder: +Now you can package and sign. Point the pack command to your build output folder (replace `dotnet-app` and the TFM path with your project's values): ```powershell # package and sign the app with the generated certificate @@ -290,7 +306,9 @@ dotnet-app You should see the "Package Family Name" output, confirming it's installed and running with identity. -### Tips: +> **Tip**: If you need to repackage your app (e.g., after code changes), increment the `Version` in your `appxmanifest.xml` before running `winapp pack` again. Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. 3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64). Use the `-r` flag with `dotnet build` to target specific architectures: `dotnet build -c Release -r win-x64` or `dotnet build -c Release -r win-arm64`. @@ -315,3 +333,10 @@ With this configuration: - The final `.msix` file will be in the root of the project You can also create a custom configuration (e.g., `PackagedRelease`) by modifying the condition to `'$(Configuration)' == 'PackagedRelease'`. + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/docs/guides/packaging-cli.md b/docs/guides/packaging-cli.md index 40fb87c5..23050bc8 100644 --- a/docs/guides/packaging-cli.md +++ b/docs/guides/packaging-cli.md @@ -180,14 +180,14 @@ To uninstall later: Get-AppxPackage *YourCLI* | Remove-AppxPackage ``` -## Next Steps - -- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) -- **Publish to the Microsoft Store**: Use `winapp store` to submit your package -- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline - ## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. -3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) \ No newline at end of file +3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline \ No newline at end of file diff --git a/samples/dotnet-app/dotnet-app.csproj b/samples/dotnet-app/dotnet-app.csproj index 3e78e53c..57c97dd6 100644 --- a/samples/dotnet-app/dotnet-app.csproj +++ b/samples/dotnet-app/dotnet-app.csproj @@ -9,21 +9,19 @@ - - + + - + - + diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 index ef3bab05..db5bfd3b 100644 --- a/samples/dotnet-app/test.Tests.ps1 +++ b/samples/dotnet-app/test.Tests.ps1 @@ -72,6 +72,42 @@ Describe ".NET App Guide Workflow" { } } + Context "Debug with Identity" { + It "Should build in Debug mode" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "dotnet build -c Debug" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should apply debug identity with create-debug-identity" -Skip:$script:skip { + Push-Location $script:projectDir + try { + $exeFile = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Debug") -Filter "*.exe" -Recurse | + Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty + Invoke-WinappCommand -Arguments "create-debug-identity `"$($exeFile.FullName)`"" + } finally { Pop-Location } + } + + It "Should add execution alias to manifest" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "manifest add-alias" + } finally { Pop-Location } + } + + It "Should run app with identity via winapp run" -Skip:$script:skip { + Push-Location $script:projectDir + try { + $debugDir = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Debug") -Filter "*.exe" -Recurse | + Select-Object -First 1 + Invoke-WinappCommand -Arguments "run `"$($debugDir.DirectoryName)`" --with-alias --unregister-on-exit" + } finally { Pop-Location } + } + } + Context "Certificate Generation" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:projectDir From 8ab4c9daedca3dc88eece4425aa327f8f07b481c Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:15:41 -0400 Subject: [PATCH 10/27] Wpf tests --- samples/wpf-app/test.Tests.ps1 | 19 ++++++++++++++++++- samples/wpf-app/wpf-app.csproj | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/samples/wpf-app/test.Tests.ps1 b/samples/wpf-app/test.Tests.ps1 index bd6ce1ef..c613b228 100644 --- a/samples/wpf-app/test.Tests.ps1 +++ b/samples/wpf-app/test.Tests.ps1 @@ -68,6 +68,22 @@ Describe 'wpf-app sample' { 'appxmanifest.xml' | Should -Exist } + It 'Builds in Debug mode' -Skip:$script:skip { + Invoke-Expression 'dotnet build -c Debug /p:ApplyDebugIdentity=false' + $LASTEXITCODE | Should -Be 0 + } + + It 'Applies debug identity with create-debug-identity' -Skip:$script:skip { + $exeFile = Get-ChildItem -Path 'bin\Debug' -Filter '*.exe' -Recurse | Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty + Invoke-WinappCommand -Arguments "create-debug-identity `"$($exeFile.FullName)`"" + } + + It 'Registers app with winapp run --no-launch' -Skip:$script:skip { + $exeFile = Get-ChildItem -Path 'bin\Debug' -Filter '*.exe' -Recurse | Select-Object -First 1 + Invoke-WinappCommand -Arguments "run `"$($exeFile.DirectoryName)`" --no-launch" + } + It 'Generates a dev certificate' -Skip:$script:skip { Invoke-WinappCommand -Arguments 'cert generate --if-exists skip' 'devcert.pfx' | Should -Exist @@ -78,7 +94,8 @@ Describe 'wpf-app sample' { } It 'Builds in Release mode with RID' -Skip:$script:skip { - Invoke-Expression 'dotnet build -c Release -r win-x64' + $rid = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'win-arm64' } else { 'win-x64' } + Invoke-Expression "dotnet build -c Release -r $rid" $LASTEXITCODE | Should -Be 0 } diff --git a/samples/wpf-app/wpf-app.csproj b/samples/wpf-app/wpf-app.csproj index 1858849f..cd06d873 100644 --- a/samples/wpf-app/wpf-app.csproj +++ b/samples/wpf-app/wpf-app.csproj @@ -16,7 +16,7 @@ - + From 5fa970bdef795c9e7ea7102965ff5a7a2e65fb12 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:20:50 -0400 Subject: [PATCH 11/27] improvements to the rust guide, winapp run tests for rust and wpf + ensure nuget package is referenced correctly in the pipeline --- .github/workflows/test-samples.yml | 56 ++++++++++++++++++++++++++++ docs/guides/rust.md | 59 +++++++++++++++++++++--------- samples/rust-app/test.Tests.ps1 | 22 +++++++++++ 3 files changed, 119 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index b03c9e8e..ab4ee9c4 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -48,6 +48,11 @@ jobs: with: name: npm-package path: artifacts/*.tgz + - uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: artifacts/nuget/*.nupkg + if-no-files-found: ignore test-sample: needs: [build] @@ -114,6 +119,57 @@ jobs: path: artifacts/npm continue-on-error: true + # Download NuGet package for .NET samples + - name: Download NuGet package (pull_request) + if: >- + steps.check.outputs.skip != 'true' && + github.event_name == 'pull_request' && + contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) + uses: actions/download-artifact@v4 + with: + name: nuget-package + path: artifacts/nuget + continue-on-error: true + + - name: Download NuGet package (workflow_run) + if: >- + steps.check.outputs.skip != 'true' && + github.event_name == 'workflow_run' && + contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) + uses: actions/download-artifact@v4 + with: + name: nuget-package + path: artifacts/nuget + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Download NuGet package (workflow_dispatch) + if: >- + steps.check.outputs.skip != 'true' && + github.event_name == 'workflow_dispatch' && + contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) + uses: actions/download-artifact@v4 + with: + name: nuget-package + path: artifacts/nuget + continue-on-error: true + + - name: Add local NuGet source + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) + shell: pwsh + run: | + $nugetPath = "artifacts/nuget" + if (Test-Path $nugetPath) { + $resolvedPath = (Resolve-Path $nugetPath).Path + dotnet nuget add source $resolvedPath --name WinAppLocal + Write-Host "Added local NuGet source: $resolvedPath" + } else { + Write-Warning "No NuGet artifacts found — .NET samples may fail to restore" + } + # --- Toolchain setup (conditional per sample) --- - name: Setup .NET diff --git a/docs/guides/rust.md b/docs/guides/rust.md index 4f4b9cfc..ae7fbbd0 100644 --- a/docs/guides/rust.md +++ b/docs/guides/rust.md @@ -10,12 +10,12 @@ A standard executable (like one created with `cargo build`) does not have packag ## Prerequisites -1. **Rust Toolchain**: Install Rust using [rustup](https://rustup.rs/) or winget: +1. **Rust Toolchain**: Install Rust using [rustup](https://rustup.rs/) or winget (or update if already installed): ```powershell winget install Rustlang.Rustup --source winget ``` -2. **winapp CLI**: Install the `winapp` tool via winget: +2. **winapp CLI**: Install the `winapp` tool via winget (or update if already installed): ```powershell winget install microsoft.winappcli --source winget ``` @@ -38,7 +38,7 @@ cargo run ## 2. Update Code to Check Identity -We'll update the app to check if it's running with package identity. We'll use the `windows` crate to access Windows APIs. +We'll update the app to check if it's running with package identity. This will help us verify that identity is working correctly in later steps. We'll use the `windows` crate to access Windows APIs. First, add the `windows` dependency to your `Cargo.toml` by running: @@ -46,7 +46,9 @@ First, add the `windows` dependency to your `Cargo.toml` by running: cargo add windows --features ApplicationModel ``` -Next, replace the contents of `src/main.rs` with the following code. This code attempts to retrieve the current package identity. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". +This adds the Windows API bindings with the `ApplicationModel` feature, which gives us access to the `Package` API for checking identity. + +Next, replace the entire contents of `src/main.rs` with the following code. This code attempts to retrieve the current package identity. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". > **Note**: The [full sample](../../samples/rust-app) also includes code to show a Windows Notification if identity is present, but for this guide, we'll focus on the identity check. @@ -81,7 +83,7 @@ You should see the output "Not packaged". This confirms that the standard execut ## 4. Initialize Project with winapp CLI -The `winapp init` command sets up everything you need in one go: app manifest and assets. +The `winapp init` command sets up everything you need in one go: app manifest and assets. The manifest defines your app's identity (name, publisher, version) which Windows uses to grant API access. Run the following command and follow the prompts: @@ -94,15 +96,16 @@ When prompted: - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 - **Description**: Press Enter to accept the default or enter a description -- **Setup SDKs**: Select "Do not setup SDKs" +- **Setup SDKs**: Select "Do not setup SDKs" (Rust uses its own `windows` crate, not the C++ SDK headers) This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. ### Add Execution Alias (for console apps) -To allow users to run your app from the command line after installation (like `rust-app`), and to use `winapp run --with-alias` during development (which keeps console output in the current terminal), add an execution alias to the `appxmanifest.xml`. +An execution alias lets users run your app by name from any terminal (like `rust-app`). It also enables `winapp run --with-alias` during development, which keeps console output in the current terminal instead of opening a new window. You can add one automatically: @@ -137,7 +140,7 @@ Or manually: open `appxmanifest.xml` and add the `uap5` namespace to the ` **Note**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 6. Use `winapp unregister` to clean up development packages when done. You should now see output similar to: ``` @@ -161,7 +166,7 @@ This confirms your app is running with a valid package identity! ## 6. Package with MSIX -Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. +Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. MSIX provides clean install/uninstall, auto-updates, and a trusted installation experience. ### Prepare the Package Directory First, build your application in release mode for optimal performance: @@ -170,7 +175,7 @@ First, build your application in release mode for optimal performance: cargo build --release ``` -Then, create a directory to hold your package files and copy your release executable. +Then, create a directory with just the files needed for distribution. The `target\release` folder contains build artifacts that aren't part of your app — we only need the executable: ```powershell mkdir dist @@ -179,15 +184,17 @@ copy .\target\release\rust-app.exe .\dist\ ### Generate a Development Certificate -Before packaging, you need a development certificate for signing. Generate one if you haven't already: +MSIX packages must be signed. For local testing, generate a self-signed development certificate: ```powershell winapp cert generate --if-exists skip ``` +> **Important**: The certificate's publisher must match the `Publisher` in your `appxmanifest.xml`. The `cert generate` command reads this automatically from your manifest. + ### Sign and Pack -Now you can package and sign: +Now you can package and sign in one step: ```powershell winapp pack .\dist --cert .\devcert.pfx @@ -197,14 +204,21 @@ winapp pack .\dist --cert .\devcert.pfx ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator: +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx ``` ### Install and Run -Install the package by double-clicking the generated *.msix file + +> **Note**: If you used `winapp run` in step 5, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +Install the package by double-clicking the generated `.msix` file, or via PowerShell: + +```powershell +Add-AppxPackage .\rust-app.msix +``` Now you can run your app from anywhere in the terminal by typing: @@ -214,7 +228,16 @@ rust-app You should see the "Package Family Name" output, confirming it's installed and running with identity. -### Tips: +> **Tip**: If you need to repackage your app (e.g., after code changes), increment the `Version` in your `appxmanifest.xml` before running `winapp pack` again. Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. -3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) \ No newline at end of file +3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) \ No newline at end of file diff --git a/samples/rust-app/test.Tests.ps1 b/samples/rust-app/test.Tests.ps1 index 098e5a3b..460b5122 100644 --- a/samples/rust-app/test.Tests.ps1 +++ b/samples/rust-app/test.Tests.ps1 @@ -61,6 +61,28 @@ Describe "Rust App Sample" { Join-Path $script:rustProjectDir "appxmanifest.xml" | Should -Exist } + It "Should add execution alias to manifest" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "manifest add-alias" + } finally { Pop-Location } + } + + It "Should build Rust app in debug mode" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "cargo build" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should run app with identity via winapp run" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "run .\target\debug --with-alias --unregister-on-exit" + } finally { Pop-Location } + } + It "Should build Rust app in release mode" -Skip:$script:skip { Push-Location $script:rustProjectDir try { From 839b4808e5c3fba2a29529ece0c3a688296fc927 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:31:56 -0400 Subject: [PATCH 12/27] moving the step to add alias before we create the debug idenity --- samples/dotnet-app/test.Tests.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 index db5bfd3b..a8aec5e5 100644 --- a/samples/dotnet-app/test.Tests.ps1 +++ b/samples/dotnet-app/test.Tests.ps1 @@ -73,6 +73,13 @@ Describe ".NET App Guide Workflow" { } Context "Debug with Identity" { + It "Should add execution alias to manifest" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "manifest add-alias" + } finally { Pop-Location } + } + It "Should build in Debug mode" -Skip:$script:skip { Push-Location $script:projectDir try { @@ -91,13 +98,6 @@ Describe ".NET App Guide Workflow" { } finally { Pop-Location } } - It "Should add execution alias to manifest" -Skip:$script:skip { - Push-Location $script:projectDir - try { - Invoke-WinappCommand -Arguments "manifest add-alias" - } finally { Pop-Location } - } - It "Should run app with identity via winapp run" -Skip:$script:skip { Push-Location $script:projectDir try { From 59814afc460371a2e6ca467a67fcdc1a7864190c Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:53:35 -0400 Subject: [PATCH 13/27] fixing dotnet test ordering --- .github/workflows/build-package.yml | 28 ---------------------------- samples/dotnet-app/test.Tests.ps1 | 14 +++++++------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index 6662ee8a..336a4e16 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -173,34 +173,6 @@ jobs: }); core.info(`Marked comment ${existing.id} as stale.`); - # E2E test for Electron workflow - runs after build completes - e2e-test: - runs-on: windows-latest - needs: build-and-package - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Enable Windows Developer Mode - run: | - # Registry key to enable Developer Mode - reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /v "AllowDevelopmentWithoutDevLicense" /d 1 /f - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '24' - - - name: Download npm package artifact - uses: actions/download-artifact@v4 - with: - name: npm-package - path: artifacts/npm - - - name: Run E2E Electron test - run: | - .\scripts\test-e2e-electron.ps1 -ArtifactsPath "artifacts/npm" -Verbose - # E2E test for winapp ui commands against WinUI 3 sample app e2e-test-ui: runs-on: windows-latest diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 index a8aec5e5..db5bfd3b 100644 --- a/samples/dotnet-app/test.Tests.ps1 +++ b/samples/dotnet-app/test.Tests.ps1 @@ -73,13 +73,6 @@ Describe ".NET App Guide Workflow" { } Context "Debug with Identity" { - It "Should add execution alias to manifest" -Skip:$script:skip { - Push-Location $script:projectDir - try { - Invoke-WinappCommand -Arguments "manifest add-alias" - } finally { Pop-Location } - } - It "Should build in Debug mode" -Skip:$script:skip { Push-Location $script:projectDir try { @@ -98,6 +91,13 @@ Describe ".NET App Guide Workflow" { } finally { Pop-Location } } + It "Should add execution alias to manifest" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "manifest add-alias" + } finally { Pop-Location } + } + It "Should run app with identity via winapp run" -Skip:$script:skip { Push-Location $script:projectDir try { From 636c4d8a8555b031518c4f6d9fba59e61079ae2a Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:08:02 -0400 Subject: [PATCH 14/27] working on dotnet test --- samples/dotnet-app/test.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 index db5bfd3b..7f357ac1 100644 --- a/samples/dotnet-app/test.Tests.ps1 +++ b/samples/dotnet-app/test.Tests.ps1 @@ -103,7 +103,7 @@ Describe ".NET App Guide Workflow" { try { $debugDir = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Debug") -Filter "*.exe" -Recurse | Select-Object -First 1 - Invoke-WinappCommand -Arguments "run `"$($debugDir.DirectoryName)`" --with-alias --unregister-on-exit" + Invoke-WinappCommand -Arguments "run `"$($debugDir.DirectoryName)`" --unregister-on-exit" } finally { Pop-Location } } } From f6f77a1d1f3f5b0c5dd55fb9196370075b4484d0 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:06:53 -0400 Subject: [PATCH 15/27] Improvements to the c++ guide and test --- docs/guides/cpp.md | 150 +++++++++++++++++---------------- samples/cpp-app/test.Tests.ps1 | 19 ++++- 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/docs/guides/cpp.md b/docs/guides/cpp.md index e71ed31f..cf6ce3f4 100644 --- a/docs/guides/cpp.md +++ b/docs/guides/cpp.md @@ -8,18 +8,18 @@ A standard executable (like one created with `cmake --build`) does not have pack ## Prerequisites -1. **Build Tools**: Use a compiler toolchain supported by CMake. This example uses Visual Studio. You can install the community edition with: +1. **Build Tools**: Use a compiler toolchain supported by CMake. This example uses Visual Studio. You can install the community edition with (or update if already installed): ```powershell winget install --id Microsoft.VisualStudio.Community --source winget --override "--add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --passive --wait" ``` Reboot after installation. -2. **CMake**: Install CMake: +2. **CMake**: Install CMake (or update if already installed): ```powershell winget install Kitware.CMake --source winget ``` -3. **winapp CLI**: Install the `winapp` cli via winget: +3. **winapp CLI**: Install the `winapp` cli via winget (or update if already installed): ```powershell winget install Microsoft.winappcli --source winget ``` @@ -67,24 +67,16 @@ cmake --build build --config Debug ## 2. Update Code to Check Identity -We'll update the app to check if it's running with package identity. We'll use the Windows Runtime C++ API to access the Package APIs. +We'll update the app to check if it's running with package identity. This will help us verify that identity is working correctly in later steps. We'll use the Windows Runtime C++ API to access the Package APIs. -First, update your `CMakeLists.txt` to link against the Windows App Model library: +First, add the following line to the end of your `CMakeLists.txt` to link against the Windows App Model library: ```cmake -cmake_minimum_required(VERSION 3.20) -project(cpp-app) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_executable(cpp-app main.cpp) - # Link Windows Runtime libraries target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib) ``` -Next, replace the contents of `main.cpp` with the following code. This code attempts to retrieve the current package identity using the Windows Runtime API. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". +Next, replace the entire contents of `main.cpp` with the following code. This code attempts to retrieve the current package identity using the Windows Runtime API. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". ```cpp #include @@ -134,7 +126,7 @@ The `winapp init` command sets up everything you need in one go: app manifest, a Run the following command and follow the prompts: ```powershell -winapp init +winapp init . ``` When prompted: @@ -142,18 +134,54 @@ When prompted: - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 - **Entry point**: Press Enter to accept the default (cpp-app.exe) -- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate headers +- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate C++ headers This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission - Create a `.winapp` folder with Windows App SDK headers and libraries -- Create a `winapp.yaml` configuration file for pinning sdk versions +- Create a `winapp.yaml` configuration file for pinning SDK versions You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. +### Add Execution Alias (for console apps) + +An execution alias lets users run your app by name from any terminal (like `cpp-app`). It also enables `winapp run --with-alias` during development, which keeps console output in the current terminal instead of opening a new window. + +You can add one automatically: + +```powershell +winapp manifest add-alias +``` + +Or manually: open `appxmanifest.xml` and add the `uap5` namespace to the `` tag if it's missing, and then add the extension inside `...`: + +```diff + + + ... + + + ... ++ ++ ++ ++ ++ ++ ++ + + + +``` + ## 5. Debug with Identity -To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. +To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. No certificate or signing is needed for debugging. 1. **Build the executable**: ```powershell @@ -165,7 +193,9 @@ To test features that require identity (like Notifications) without fully packag winapp run .\build\Debug --with-alias ``` -The `--with-alias` flag launches the app via its execution alias so console output stays in the current terminal. This requires a `uap5:ExecutionAlias` in the manifest — you can add one with `winapp manifest add-alias`. +The `--with-alias` flag launches the app via its execution alias so console output stays in the current terminal. This requires the `uap5:ExecutionAlias` we added in step 4. + +> **Tip**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 8. Use `winapp unregister` to clean up development packages when done. You should now see output similar to: ``` @@ -186,34 +216,22 @@ winapp create-debug-identity .\build\Debug\cpp-app.exe ## 6. Using Windows App SDK (Optional) -If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK headers in the `.winapp/include` folder. This gives you access to modern Windows APIs like notifications, windowing, and more. +If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK headers in the `.winapp/include` folder. This gives you access to modern Windows APIs like notifications, windowing, on-device AI, and more. If you just need package identity for distribution, you can skip to step 7. Let's add a simple example that prints the Windows App Runtime version. ### Update CMakeLists.txt -Add the Windows App SDK include directory and link the necessary libraries: +Add the following line to the end of your `CMakeLists.txt` to include the Windows App SDK headers: ```cmake -cmake_minimum_required(VERSION 3.20) -project(cpp-app) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_executable(cpp-app main.cpp) - -# Link Windows Runtime libraries -target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib) - # Add Windows App SDK include directory target_include_directories(cpp-app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include) - ``` ### Update main.cpp -Replace the contents of `main.cpp` to use the Windows App Runtime API: +Replace the entire contents of `main.cpp` to use the Windows App Runtime API: ```cpp #include @@ -382,7 +400,7 @@ With this setup: ## 8. Package with MSIX -Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. +Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. MSIX provides clean install/uninstall, auto-updates, and a trusted installation experience. ### Prepare the Package Directory First, build your application in release mode for optimal performance: @@ -391,51 +409,23 @@ First, build your application in release mode for optimal performance: cmake --build build --config Release ``` -Then, create a directory to hold your package files and copy your release executable: +Then, create a directory with just the files needed for distribution and copy your release executable: ```powershell mkdir dist copy .\build\Release\cpp-app.exe .\dist\ ``` -### Add Execution Alias -To allow users to run your app from the command line after installation (like `cpp-app`), add an execution alias to the `appxmanifest.xml`. - -Open `appxmanifest.xml` and add the `uap5` namespace to the `` tag if it's missing, and then add the extension inside `...`: - -```xml - - - ... - - - ... - - - - - - - - - ... - - - -``` - ### Generate a Development Certificate -Before packaging, you need a development certificate for signing. Generate one if you haven't already: +MSIX packages must be signed. For local testing, generate a self-signed development certificate: ```powershell winapp cert generate --if-exists skip ``` +> **Tip**: The certificate's publisher must match the `Publisher` in your `appxmanifest.xml`. The `cert generate` command reads this automatically from your manifest. + ### Sign and Pack Now you can package and sign: @@ -445,23 +435,28 @@ Now you can package and sign: winapp pack .\dist --cert .\devcert.pfx ``` -> Note: The appxmanifest.xml and assets need to be in the target folder for packaging. To simplify, the `pack` command by default uses the appxmanifest.xml in your current directory and copies it to the target folder before packaging. +> **Tip**: The `pack` command automatically uses the appxmanifest.xml from your current directory and copies it to the target folder before packaging. The generated `.msix` file will be in the current directory. ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator (you only need to do this once): +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx ``` ### Install and Run -The `winapp pack` command generates the MSIX file in your project root directory. You can install the package using PowerShell: + +> **Tip**: If you used `winapp run` in step 5, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +The `winapp pack` command generates the MSIX file in your project root directory. Install the package by double-clicking the generated `.msix` file, or using PowerShell: ```powershell -Add-AppxPackage .\cpp-app.msix +Add-AppxPackage .\cpp-app_1.0.0.0_x64.msix ``` +> **Tip**: The MSIX filename includes the version and architecture (e.g., `cpp-app_1.0.0.0_arm64.msix`). Check your directory for the exact filename. + Now you can run your app from anywhere in the terminal by typing: ```powershell @@ -470,8 +465,17 @@ cpp-app You should see the "Package Family Name" output, confirming it's installed and running with identity. -### Tips: +> **Tip**: If you need to repackage your app (e.g., after code changes), increment the `Version` in your `appxmanifest.xml` before running `winapp pack` again. Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. 2. The [Azure Trusted Signing](https://azure.microsoft.com/products/trusted-signing) service is a great way to manage your certificates securely and integrate signing into your CI/CD pipeline. 3. The Microsoft Store will sign the MSIX for you, no need to sign before submission. 4. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64). Configure CMake with the appropriate generator and architecture flags. + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/samples/cpp-app/test.Tests.ps1 b/samples/cpp-app/test.Tests.ps1 index 3411a504..a20f7f0b 100644 --- a/samples/cpp-app/test.Tests.ps1 +++ b/samples/cpp-app/test.Tests.ps1 @@ -76,18 +76,31 @@ add_executable(test-cpp-app main.cpp) } It "winapp init creates config files" -Skip:$script:skip { - Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=stable" + Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable" "winapp.yaml" | Should -Exist "appxmanifest.xml" | Should -Exist ".winapp" | Should -Exist } + It "adds execution alias to manifest" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "manifest add-alias" + } + It "CMake configures successfully" -Skip:$script:skip { - $output = cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 + $output = cmake -B build -DCMAKE_BUILD_TYPE=Debug 2>&1 $LASTEXITCODE | Should -Be 0 -Because "CMake configure failed: $output" } - It "CMake builds successfully" -Skip:$script:skip { + It "CMake builds debug successfully" -Skip:$script:skip { + $output = cmake --build build --config Debug 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "CMake build failed: $output" + } + + It "runs app with identity via winapp run" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "run build\Debug --unregister-on-exit" + } + + It "CMake builds release successfully" -Skip:$script:skip { $output = cmake --build build --config Release 2>&1 $LASTEXITCODE | Should -Be 0 -Because "CMake build failed: $output" } From 8d250a835f5f33f3aa83677df28acdf5ffd7d13d Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:03:30 -0400 Subject: [PATCH 16/27] Improve Flutter guide, restructure test into individual steps, add winapp run --- docs/guides/flutter.md | 105 +++++++++++++++++++++++------ samples/flutter-app/test.Tests.ps1 | 42 ++++++++---- 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/docs/guides/flutter.md b/docs/guides/flutter.md index 37248239..a70fc97d 100644 --- a/docs/guides/flutter.md +++ b/docs/guides/flutter.md @@ -12,7 +12,7 @@ A standard Flutter Windows build does not have package identity. This guide show 1. **Flutter SDK**: Install Flutter following the [official guide](https://docs.flutter.dev/install/quick). -2. **winapp CLI**: Install the `winapp` CLI via winget: +2. **winapp CLI**: Install the `winapp` CLI via winget (or update if already installed): ```powershell winget install Microsoft.winappcli --source winget ``` @@ -176,14 +176,21 @@ Now, build and run the app as usual: ```powershell flutter build windows +``` + +Run the executable directly (replace `flutter_app` with your project name if different): + +```powershell .\build\windows\x64\runner\Release\flutter_app.exe ``` +> **Tip**: The build output is in the `x64` folder regardless of your machine's architecture — this is expected for Flutter's Windows build. + You should see the app with an orange "Not packaged" indicator. This confirms that the standard executable is running without any package identity. ## 4. Initialize Project with winapp CLI -The `winapp init` command sets up everything you need in one go: app manifest, assets, and optionally Windows App SDK headers for C++ development. +The `winapp init` command sets up everything you need in one go: app manifest, assets, and optionally Windows App SDK headers for C++ development. The manifest defines your app's identity (name, publisher, version) which Windows uses to grant API access. Run the following command and follow the prompts: @@ -192,22 +199,23 @@ winapp init ``` When prompted: -- **Package name**: Press Enter to accept the default (flutterapp) +- **Package name**: Press Enter to accept the default (derived from your project name) - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 - **Description**: Press Enter to accept the default (Windows Application) -- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate C++ headers +- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate C++ headers (needed for step 6) This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission - Create a `.winapp` folder with Windows App SDK headers and libraries -- Create a `winapp.yaml` configuration file for pinning sdk versions +- Create a `winapp.yaml` configuration file for pinning SDK versions You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. ## 5. Debug with Identity -To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. +To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. No certificate or signing is needed for debugging. 1. **Build the app**: ```powershell @@ -219,6 +227,8 @@ To test features that require identity (like Notifications) without fully packag winapp run .\build\windows\x64\runner\Release ``` +> **Tip**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 7. Use `winapp unregister` to clean up development packages when done. + You should now see the app with a green indicator showing: ``` Package Family Name: flutterapp.debug_xxxxxxxx @@ -229,7 +239,7 @@ This confirms your app is running with a valid package identity! ## 6. Using Windows App SDK (Optional) -If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK C++ headers in the `.winapp/include` folder. Since Flutter's Windows runner is C++, you can call Windows App SDK APIs from native code and expose them to Dart via a method channel. +If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK C++ headers in the `.winapp/include` folder. Since Flutter's Windows runner is C++, you can call Windows App SDK APIs from native code and expose them to Dart via a method channel. If you just need package identity for distribution, you can skip to step 7. Let's add a simple example that displays the Windows App Runtime version. @@ -294,23 +304,24 @@ void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine) { ### Update CMakeLists.txt -Edit `windows/runner/CMakeLists.txt` to add the new source file, include the Windows App SDK headers, and link the required libraries: +Edit `windows/runner/CMakeLists.txt` to make three changes. Find the `add_executable` block and add `"winapp_sdk_plugin.cpp"` to the source file list: ```cmake -# Add the new source file to the executable add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" - "winapp_sdk_plugin.cpp" + "winapp_sdk_plugin.cpp" # <-- add this line "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) +``` -# ... existing settings ... +Then add these two lines at the end of the file to link WinRT libraries and include the Windows App SDK headers: +```cmake # Link Windows Runtime libraries for WinRT target_link_libraries(${BINARY_NAME} PRIVATE "WindowsApp.lib") @@ -321,23 +332,30 @@ target_include_directories(${BINARY_NAME} PRIVATE ### Register the Plugin -In `windows/runner/flutter_window.cpp`, include the header and register the plugin: +In `windows/runner/flutter_window.cpp`, add the include at the top of the file with the other includes: ```cpp #include "winapp_sdk_plugin.h" +``` + +Then find the `RegisterPlugins` call in `FlutterWindow::OnCreate()` and add `RegisterWinAppSdkPlugin` on the line right after it: -// In FlutterWindow::OnCreate(), after RegisterPlugins: -RegisterPlugins(flutter_controller_->engine()); -RegisterWinAppSdkPlugin(flutter_controller_->engine()); +```cpp + RegisterPlugins(flutter_controller_->engine()); + RegisterWinAppSdkPlugin(flutter_controller_->engine()); // <-- add this line ``` ### Update main.dart -Add a method channel call in Dart to query the runtime version and display it: +Add the following import at the top of `lib/main.dart`, alongside the existing imports: ```dart import 'package:flutter/services.dart'; +``` + +Add this function below the existing `getPackageFamilyName()` function (outside any class): +```dart /// Queries the Windows App Runtime version via a native method channel. Future getWindowsAppRuntimeVersion() async { if (!Platform.isWindows) return null; @@ -351,7 +369,41 @@ Future getWindowsAppRuntimeVersion() async { } ``` -Call it in `initState()` and display it in the UI alongside the package identity indicator. +In the `_MyHomePageState` class, add a new field next to the existing `_packageFamilyName`: + +```dart + late final String? _packageFamilyName; + String? _runtimeVersion; // <-- add this line +``` + +Update `initState()` to call the new function: + +```dart + @override + void initState() { + super.initState(); + _packageFamilyName = getPackageFamilyName(); + // Fetch the runtime version asynchronously + getWindowsAppRuntimeVersion().then((version) { + setState(() { + _runtimeVersion = version; + }); + }); + } +``` + +Finally, display the runtime version in the `build` method. Add this widget inside the `Column` children list, right after the `Container` that shows the package identity: + +```dart + if (_runtimeVersion != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + 'Windows App Runtime: $_runtimeVersion', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), +``` ### Build and Run @@ -418,7 +470,7 @@ winapp pack .\dist --cert .\devcert.pfx ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator (you only need to do this once): +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx @@ -426,14 +478,25 @@ winapp cert install .\devcert.pfx ### Install and Run -Install the package by double-clicking the generated `flutterapp.msix` file, or using PowerShell: +> **Tip**: If you used `winapp run` in step 5, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +Install the package by double-clicking the generated `.msix` file, or using PowerShell: ```powershell Add-AppxPackage .\flutterapp.msix ``` -### Tips +> **Tip**: The MSIX filename includes the version and architecture (e.g., `flutterapplication1_1.0.0.0_x64.msix`). Check your directory for the exact filename. If you need to repackage after code changes, increment the `Version` in your `appxmanifest.xml` — Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. 2. The [Azure Trusted Signing](https://azure.microsoft.com/products/trusted-signing) service is a great way to manage your certificates securely and integrate signing into your CI/CD pipeline. 3. The Microsoft Store will sign the MSIX for you, no need to sign before submission. + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/samples/flutter-app/test.Tests.ps1 b/samples/flutter-app/test.Tests.ps1 index 8fa315a7..baac648d 100644 --- a/samples/flutter-app/test.Tests.ps1 +++ b/samples/flutter-app/test.Tests.ps1 @@ -51,23 +51,18 @@ Describe "flutter-app sample" { BeforeAll { $script:tempDir = New-TempTestDirectory -Prefix "flutter-guide" Set-Location $script:tempDir + } + It "Should create a new Flutter project" { flutter create test_flutter_app --platforms=windows - if ($LASTEXITCODE -ne 0) { throw "flutter create failed" } - + $LASTEXITCODE | Should -Be 0 $script:projectDir = Join-Path $script:tempDir "test_flutter_app" - Set-Location $script:projectDir + $script:projectDir | Should -Exist + } + It "Should run winapp init successfully" { + Set-Location $script:projectDir Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=stable" - - flutter build windows - if ($LASTEXITCODE -ne 0) { throw "flutter build windows failed" } - - $script:buildOutput = Join-Path $script:projectDir "build\windows\x64\runner\Release" - Copy-Item $script:buildOutput -Destination (Join-Path $script:projectDir "dist") -Recurse - - Invoke-WinappCommand -Arguments "cert generate --if-exists skip" - Invoke-WinappCommand -Arguments "pack dist --cert devcert.pfx" } It "Should create winapp.yaml after init" { @@ -82,15 +77,34 @@ Describe "flutter-app sample" { Join-Path $script:projectDir ".winapp" | Should -Exist } - It "Should produce Flutter build output" { + It "Should build Flutter app for Windows" { + Set-Location $script:projectDir + flutter build windows + $LASTEXITCODE | Should -Be 0 + $script:buildOutput = Join-Path $script:projectDir "build\windows\x64\runner\Release" $script:buildOutput | Should -Exist } + It "Should run app with identity via winapp run" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "run $($script:buildOutput) --no-launch" + } + It "Should generate a dev certificate" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" Join-Path $script:projectDir "devcert.pfx" | Should -Exist } - It "Should produce an MSIX package" { + It "Should prepare dist directory" { + Set-Location $script:projectDir + Copy-Item $script:buildOutput -Destination (Join-Path $script:projectDir "dist") -Recurse + Join-Path $script:projectDir "dist" | Should -Exist + } + + It "Should package as MSIX" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "pack dist --cert devcert.pfx" Get-ChildItem -Path $script:projectDir -Filter "*.msix" | Should -Not -BeNullOrEmpty } } From 158ec581acd619bda99edc186bdc2bf6a7ee98de Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:24:29 -0400 Subject: [PATCH 17/27] Improved Tauri guide and tests --- docs/guides/tauri.md | 62 +++++++++++---- samples/tauri-app/src-tauri/Cargo.lock | 103 +++++++++++-------------- samples/tauri-app/test.Tests.ps1 | 18 +++++ 3 files changed, 112 insertions(+), 71 deletions(-) diff --git a/docs/guides/tauri.md b/docs/guides/tauri.md index e337df86..aa3f4640 100644 --- a/docs/guides/tauri.md +++ b/docs/guides/tauri.md @@ -10,8 +10,11 @@ For a complete working example, check out the [Tauri sample](../../samples/tauri 1. **Windows 11** 1. **Node.js** - `winget install OpenJS.NodeJS --source winget` +1. **Rust Toolchain** - Install Rust using [rustup](https://rustup.rs/) or `winget install Rustlang.Rustup --source winget` 1. **winapp CLI** - `winget install microsoft.winappcli --source winget` +> **Tip**: If you already have these installed, run the `winget install` commands anyway to check for updates. + ## 1. Create a New Tauri App Start by creating a new Tauri application using the official scaffolding tool: @@ -19,7 +22,12 @@ Start by creating a new Tauri application using the official scaffolding tool: ```powershell npm create tauri-app@latest ``` -Follow the prompts (e.g., Project name: `tauri-app`, Frontend language: `JavaScript`, Package manager: `npm`). +Follow the prompts: +- **Project name**: `tauri-app` (or your preferred name) +- **Frontend language**: `JavaScript` +- **Package manager**: `npm` +- **UI template**: `Vanilla` +- **UI flavor**: `JavaScript` Navigate to your project directory and install dependencies: @@ -40,14 +48,14 @@ We'll update the app to check if it's running with package identity. We'll use t ### Backend Changes (Rust) -1. **Add Dependency**: Open `src-tauri/Cargo.toml` and add the `windows` dependency for the Windows target: +1. **Add Dependency**: Open `src-tauri/Cargo.toml` and add the following lines at the end of the file. This adds the Windows API bindings so we can check for package identity: ```toml [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = ["ApplicationModel"] } ``` -2. **Add Command**: Open `src-tauri/src/lib.rs` and add the `get_package_family_name` command. This function attempts to retrieve the current package identity. +2. **Add Command**: Open `src-tauri/src/lib.rs` and add the `get_package_family_name` function. Place it before the `pub fn run()` function: ```rust #[tauri::command] @@ -130,7 +138,7 @@ We'll update the app to check if it's running with package identity. We'll use t ## 3. Initialize Project with winapp CLI -The `winapp init` command sets up everything you need in one go: app manifest and assets. +The `winapp init` command sets up everything you need in one go: app manifest and assets. The manifest defines your app's identity (name, publisher, version) which Windows uses to grant API access. Run the following command and follow the prompts: @@ -143,16 +151,17 @@ When prompted: - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 - **Entry point**: Press Enter to accept the default (tauri-app.exe) -- **Setup SDKs**: Select "Do not setup SDKs" +- **Setup SDKs**: Select "Do not setup SDKs" (Tauri uses Rust's `windows` crate, not the C++ SDK headers) This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. ## 4. Debug with Identity -To debug with identity, we need to build the Rust backend and run it with `winapp run`. Since `npm run tauri dev` manages the process lifecycle, it's harder to inject the identity there. Instead, we'll create a custom script. +To debug with identity, we need to build the Rust backend and run it with `winapp run`. Since `npm run tauri dev` manages the process lifecycle, it's harder to inject the identity there. Instead, we'll create a custom script. No certificate or signing is needed for debugging. 1. **Add Script**: Open `package.json` and add a new script `tauri:dev:withidentity`: @@ -174,7 +183,11 @@ To debug with identity, we need to build the Rust backend and run it with `winap npm run tauri:dev:withidentity ``` -You should now see the app open and display a "Package family name", confirming it is running with identity! You can now start using and debugging APIs that require package identity, such as Notifications or the new AI APIs like Phi Silica. +> **Tip**: You may see a terminal/console window appear behind the app window — this is normal for Tauri debug builds (it's the Rust process's console). + +You should now see the app open and display a "Package family name", confirming it is running with identity! You can now start using and debugging APIs that require package identity, such as Notifications or the new AI APIs like Phi Silica. + +> **Tip**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 5. Use `winapp unregister` to clean up development packages when done. > **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). @@ -199,30 +212,53 @@ First, add a `pack:msix` script to your `package.json`: ### Generate a Development Certificate -Before packaging, you need a development certificate for signing. Generate one if you haven't already: +MSIX packages must be signed. For local testing, generate a self-signed development certificate: ```powershell winapp cert generate --if-exists skip ``` +> **Tip**: The certificate's publisher must match the `Publisher` in your `appxmanifest.xml`. The `cert generate` command reads this automatically from your manifest. + ### Build, Stage, and Pack ```powershell npm run pack:msix ``` -> Note: The `pack` command automatically uses the appxmanifest.xml from your current directory and copies it to the target folder before packaging. The generated .msix file will be in the current directory. +> **Tip**: The `pack` command automatically uses the appxmanifest.xml from your current directory and copies it to the target folder before packaging. The generated .msix file will be in the current directory. ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator: +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx ``` ### Install and Run -Install the package by double-clicking the generated `.msix` file. Once installed, you can launch your app from the start menu. +> **Tip**: If you used `winapp run` in step 4, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +Install the package by double-clicking the generated `.msix` file, or using PowerShell: + +```powershell +Add-AppxPackage .\tauri-app.msix +``` + +> **Tip**: The MSIX filename includes the version and architecture (e.g., `tauri-app_1.0.0.0_x64.msix`). Check your directory for the exact filename. If you need to repackage after code changes, increment the `Version` in your `appxmanifest.xml` — Windows requires a higher version number to update an installed package. + +Once installed, you can launch your app from the Start menu. You should see the app running with identity. + +## Tips + +1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. +2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. +3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64). + +## Next Steps -You should see the app running with identity. +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/samples/tauri-app/src-tauri/Cargo.lock b/samples/tauri-app/src-tauri/Cargo.lock index 21cf00af..e98aa13c 100644 --- a/samples/tauri-app/src-tauri/Cargo.lock +++ b/samples/tauri-app/src-tauri/Cargo.lock @@ -3538,7 +3538,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", - "windows 0.58.0", + "windows 0.62.2", ] [[package]] @@ -4407,8 +4407,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", ] [[package]] @@ -4481,25 +4481,27 @@ dependencies = [ [[package]] name = "windows" -version = "0.58.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", ] [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -4512,16 +4514,12 @@ dependencies = [ ] [[package]] -name = "windows-core" -version = "0.58.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -4530,8 +4528,8 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -4543,8 +4541,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -4558,18 +4556,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -4583,17 +4581,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -4628,12 +4615,13 @@ dependencies = [ ] [[package]] -name = "windows-result" -version = "0.2.0" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -4654,16 +4642,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -4775,6 +4753,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" diff --git a/samples/tauri-app/test.Tests.ps1 b/samples/tauri-app/test.Tests.ps1 index 53f14726..320f5712 100644 --- a/samples/tauri-app/test.Tests.ps1 +++ b/samples/tauri-app/test.Tests.ps1 @@ -72,6 +72,24 @@ Describe "Tauri App Sample" { Join-Path $script:tempApp "appxmanifest.xml" | Should -Exist } + It "Should build Tauri app in debug mode" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "cargo build --manifest-path src-tauri\Cargo.toml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should run app with identity via winapp run" -Skip:$script:skip { + Push-Location $script:tempApp + try { + $distDir = Join-Path $script:tempApp "dist" + $null = New-Item -ItemType Directory -Path $distDir -Force + Copy-Item (Join-Path $script:tempApp "src-tauri\target\debug\tauri-app.exe") -Destination $distDir + Invoke-WinappCommand -Arguments "run dist --no-launch" + } finally { Pop-Location } + } + It "Should build Tauri app in release mode" -Skip:$script:skip { Push-Location $script:tempApp try { From 7e4160b0df417de9c84784dd57eec4ed6bfe71a7 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:30:26 -0400 Subject: [PATCH 18/27] Changes to electron setup and winml addon guide --- docs/guides/electron/packaging.md | 14 ++-- docs/guides/electron/phi-silica-addon.md | 9 ++- docs/guides/electron/setup.md | 32 +++++++-- docs/guides/electron/winml-addon.md | 90 +++++++++++++++++++----- docs/guides/flutter.md | 3 +- 5 files changed, 113 insertions(+), 35 deletions(-) diff --git a/docs/guides/electron/packaging.md b/docs/guides/electron/packaging.md index f4869b1a..6bf9c7a1 100644 --- a/docs/guides/electron/packaging.md +++ b/docs/guides/electron/packaging.md @@ -11,15 +11,17 @@ Before packaging, make sure you've: ## Prepare for Packaging -> **📝 Note:** Before packaging, make sure to configure your build tool (Electron Forge, webpack, etc.) to exclude temporary files from the final build: +> [!NOTE] +> Before packaging, make sure to configure your build tool (Electron Forge, webpack, etc.) to exclude temporary files from the final build: > - `.winapp/` folder > - `winapp.yaml` > - Certificate files (`.pfx`) > - Debug symbols (`.pdb`) > - C# build artifacts (`obj/`, `bin/` folders) > - MSIX packages (*.msix) -> -> **⚠️ Important:** Verify that your `appxmanifest.xml` matches your packaged app structure: + +> [!IMPORTANT] +> Verify that your `appxmanifest.xml` matches your packaged app structure: > - The `Executable` attribute should point to the correct .exe file in your packaged output ## Packaging Options @@ -66,7 +68,8 @@ The `--out` option is also optional. If not provided, the current directory will The MSIX package will be created as `./out/.msix`. -> **💡 Tip:** You can add these commands to your `package.json` scripts for convenience: +> [!TIP] +> You can add these commands to your `package.json` scripts for convenience: > ```json > { > "scripts": { @@ -137,7 +140,8 @@ npm run make The MSIX package will be created in the `./out/make/msix/` folder. -> **💡 Tip:** This approach is more integrated with the Electron Forge workflow and automatically handles packaging and MSIX creation in one step. +> [!TIP] +> This approach is more integrated with the Electron Forge workflow and automatically handles packaging and MSIX creation in one step. ## Install and Test the MSIX diff --git a/docs/guides/electron/phi-silica-addon.md b/docs/guides/electron/phi-silica-addon.md index 1138e3c7..c50a423c 100644 --- a/docs/guides/electron/phi-silica-addon.md +++ b/docs/guides/electron/phi-silica-addon.md @@ -99,7 +99,8 @@ namespace csAddon } ``` -> **📝 Note:** Phi Silica requires Windows 11 with an NPU-equipped device (Copilot+ PC). If you don't have compatible hardware, the API will return a message indicating the model is not available. You can still complete this tutorial and package the app - it will gracefully handle devices without NPU support. +> [!NOTE] +> Phi Silica requires Windows 11 with an NPU-equipped device (Copilot+ PC). If you don't have compatible hardware, the API will return a message indicating the model is not available. You can still complete this tutorial and package the app - it will gracefully handle devices without NPU support. ## Step 3: Build the C# Addon @@ -162,7 +163,8 @@ Before you can use the Phi Silica API, you need to declare the required capabili ``` -> **💡 Tip:** Different Windows APIs require different capabilities. Always check the API documentation to see what capabilities are needed. Common ones include `microphone`, `webcam`, `location`, and `bluetooth`. +> [!TIP] +> Different Windows APIs require different capabilities. Always check the API documentation to see what capabilities are needed. Common ones include `microphone`, `webcam`, `location`, and `bluetooth`. ## Step 6: Update Debug Identity @@ -177,7 +179,8 @@ This command: 2. Registers `electron.exe` in your `node_modules` with a temporary identity 3. Enables you to test identity-required APIs without full MSIX packaging -> **📝 Note:** This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: +> [!NOTE] +> This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: > - Modify `appxmanifest.xml` (change capabilities, identity, or properties) > - Update app assets (icons, logos, etc.) > - Reinstall or update dependencies diff --git a/docs/guides/electron/setup.md b/docs/guides/electron/setup.md index 6990dd5c..42c6f593 100644 --- a/docs/guides/electron/setup.md +++ b/docs/guides/electron/setup.md @@ -20,6 +20,12 @@ npm create electron-app@latest my-windows-app cd my-windows-app ``` +When prompted by Electron Forge: +- **Bundler**: Select **None** (recommended — native addons work without extra configuration) +- **Language**: Select **JavaScript** (this guide uses JS; TypeScript works too) +- **Electron version**: Select **latest** +- **Initialize git**: Your preference + Verify the app runs: ```bash @@ -30,6 +36,8 @@ You should see the default Electron Forge window. Close it and let's add Windows ## Step 2: Install winapp CLI +The Electron workflow requires the **npm package** (`@microsoft/winappcli`) rather than the standalone CLI installed from winget. The npm package includes Node.js-specific helpers (like `add-electron-debug-identity` and `create-addon`) that are not available in the native CLI. If you already have winapp installed from winget, that's fine — the npm package adds Node.js-specific tools as a project dependency and won't conflict with your system installation. + ```bash npm install --save-dev @microsoft/winappcli ``` @@ -41,7 +49,7 @@ The `winapp init` command sets up everything you need in one go: app manifest, a Run the following command and follow the prompts: ```bash -npx winapp init +npx winapp init . ``` When prompted: @@ -66,16 +74,17 @@ This command sets up everything you need for Windows development: 4. **Creates `winapp.yaml`** - Tracks SDK versions and project configuration -6. **Installs Windows App SDK runtime** - Required runtime components for modern APIs +5. **Installs Windows App SDK runtime** - Required runtime components for modern APIs -7. **Enables Developer Mode in Windows** - Required for debugging our application +6. **Enables Developer Mode in Windows** - Required for debugging our application > [!NOTE] > The `.winapp/` folder is automatically added to `.gitignore` and should not be checked in to source. You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. -> **💡 About the Windows SDKs:** +> [!TIP] +> **About the Windows SDKs:** > > - **[Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/)** - A development platform that lets you build Win32/desktop apps. It's designed around Windows APIs that are coupled to particular versions of the OS. Use this to access core Win32 APIs like file system, networking, and system services. > @@ -100,7 +109,14 @@ This script automatically runs after `npm install` and does two things: 1. **`winapp restore`** - Downloads and restores all Windows SDK packages to the `.winapp/` folder 2. **`winapp node add-electron-debug-identity`** - Registers your Electron app with debug identity (more on this in the next steps) -Now whenever someone runs `npm install`, the Windows environment is automatically configured! +Now run `npm install` to trigger the postinstall script and configure the Windows environment: + +```bash +npm install +``` + +> [!NOTE] +> The `postinstall` script runs automatically after every `npm install`. This means the Windows environment will be configured automatically whenever someone clones your project and runs `npm install`.
💡 Cross-Platform Development (click to expand) @@ -138,7 +154,7 @@ This ensures Windows-specific setup only runs on Windows machines, allowing deve ## Step 5: Understanding Debug Identity -The `postinstall` script in Step 4 includes the `winapp node add-electron-debug-identity` command, which enables you to test Windows APIs that require app identity during development. +The `npm install` you ran in Step 4 triggered the `postinstall` script, which ran `winapp node add-electron-debug-identity`. This gives your app a temporary debug identity so you can test Windows APIs that require app identity during development. ### What Does Debug Identity Do? @@ -147,7 +163,7 @@ This command: 2. Registers `electron.exe` in your `node_modules` with a temporary identity 3. Enables you to test identity-required APIs without creating a full MSIX package -The debug identity is automatically applied when you run `npm install` thanks to the `postinstall` script. +The debug identity was applied automatically when you ran `npm install` in Step 4. Going forward, it will be reapplied whenever anyone runs `npm install`. ### When to Manually Update Debug Identity @@ -165,6 +181,8 @@ You can now test your Electron app with the debug identity applied: npm start ``` +You should see a **desktop application window** open (not a browser tab) — this is how Electron apps run. +
⚠️ Known Issue: App Crashes or Blank Window (click to expand) diff --git a/docs/guides/electron/winml-addon.md b/docs/guides/electron/winml-addon.md index 5860b02c..17fbb25b 100644 --- a/docs/guides/electron/winml-addon.md +++ b/docs/guides/electron/winml-addon.md @@ -11,6 +11,9 @@ Before starting this guide, make sure you've: > [!NOTE] > WinML runs on any Windows 10 (1809+) or Windows 11 device. For best performance, devices with GPUs or NPUs are recommended, but the API works on CPU as well. +> [!IMPORTANT] +> The WinML addon requires the **experimental** Windows App SDK. If you selected "Stable SDKs" during `winapp init` in the setup guide, you'll need to update your SDK version. Edit `winapp.yaml` and change the `Microsoft.WindowsAppSDK` version to `2.0.0-experimental3`, then run `npx winapp restore` to update. + ## Step 1: Create a C# Native Addon Let's create a native addon that will use WinML APIs. We'll use a C# template that leverages [node-api-dotnet](https://github.com/microsoft/node-api-dotnet) to bridge JavaScript and C#. @@ -24,11 +27,12 @@ This creates a `winMlAddon/` folder with: - `winMlAddon.csproj` - Project file with references to Windows SDK and Windows App SDK - `README.md` - Documentation on how to use the addon -The command also adds a `build-winMlAddon` script to your `package.json` for building the addon: +The command also adds a `build-winMlAddon` script to your `package.json` for building the addon, and a `clean-winMlAddon` script for cleaning build artifacts: ```json { "scripts": { - "build-winMlAddon": "dotnet publish ./winMlAddon/winMlAddon.csproj -c Release" + "build-winMlAddon": "dotnet publish ./winMlAddon/winMlAddon.csproj -c Release", + "clean-winMlAddon": "dotnet clean ./winMlAddon/winMlAddon.csproj" } } ``` @@ -42,7 +46,8 @@ Let's verify everything is set up correctly by building the addon: npm run build-winMlAddon ``` -> **Note:** You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. +> [!NOTE] +> You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. ## Step 2: Download the SqueezeNet Model and Get Sample Code @@ -64,7 +69,7 @@ We'll use the **Classify Image** sample from the [AI Dev Gallery](https://aka.ms ## Step 3: Add Required NuGet Packages -Before adding the WinML code, we need to add two additional NuGet packages that are required for image processing and ONNX Runtime extensions. +Before adding the WinML code, we need to add additional NuGet packages required for image processing, ONNX Runtime, and GenAI support. ### 3.1. Update Directory.packages.props @@ -79,9 +84,12 @@ Add the following package versions to the `Directory.packages.props` file in the - + + + ++ ++ ++ @@ -98,9 +106,12 @@ Open `winMlAddon/winMlAddon.csproj` and add the package references to the ` - + + + ++ ++ ++ @@ -110,6 +121,9 @@ Open `winMlAddon/winMlAddon.csproj` and add the package references to the ` [!IMPORTANT] +> You must copy the **entire folder**, not just `addon.cs`. The addon depends on helper files in the `Utils/` subfolder (`Prediction.cs`, `ImageNet.cs`, `BitmapFunctions.cs`, etc.). ### Key Implementation Details @@ -212,7 +223,8 @@ module.exports = { - `.msix` files - Packaged outputs - `winMlAddon/` source files - Keeps only the `dist/` folder with compiled binaries -> **📝 Note:** If you're using a different packaging tool (electron-builder, etc.), you'll need to configure similar settings for unpacking native dependencies and excluding development files. Check your packager's documentation for ASAR unpacking options. +> [!NOTE] +> If you're using a different packaging tool (electron-builder, etc.), you'll need to configure similar settings for unpacking native dependencies and excluding development files. Check your packager's documentation for ASAR unpacking options. #### 4. Image Classification @@ -232,7 +244,8 @@ The complete implementation handles: - Running the model inference - Post-processing results to get top predictions with labels and confidence scores -> **📝 Note:** The full source code includes image preprocessing, tensor creation, and result parsing. Check the [sample implementation](../../../samples/electron-winml/winMlAddon/addon.cs) for all the details. +> [!NOTE] +> The full source code includes image preprocessing, tensor creation, and result parsing. Check the [sample implementation](../../../samples/electron-winml/winMlAddon/addon.cs) for all the details. ### Understanding the Code @@ -261,7 +274,7 @@ The compiled addon will be in `winMlAddon/dist/winMlAddon.node`. ## Step 6: Test the Addon -Now let's test the addon works by calling it from the main process. Open `src/index.js` and follow these steps: +Now let's test the addon works by calling it from the main process. Open `src/main.js` and follow these steps: ### 6.1. Load the Addon @@ -320,12 +333,13 @@ testWinML(); To test image classification: 1. Create a `test-images/` folder in your project root -2. Add some test images (e.g., `sample.jpg`, `cat.jpg`, `dog.jpg`) -3. The SqueezeNet model recognizes 1000 different ImageNet classes +2. Add a test image named `sample.jpg` (the code expects this exact filename) +3. The SqueezeNet model recognizes 1000 different ImageNet classes (animals, objects, scenes, etc.) When you run the app, you'll see the classification results in the console! -> **💡 Tip:** For a complete implementation with IPC handlers, file selection dialogs, and a UI, see the [electron-winml sample](../../../samples/electron-winml/src/index.js). +> [!TIP] +> For a complete implementation with IPC handlers, file selection dialogs, and a UI, see the [electron-winml sample](../../../samples/electron-winml/src/index.js). ## Step 7: Update Debug Identity @@ -340,7 +354,8 @@ This command: 2. Registers `electron.exe` in your `node_modules` with a temporary identity 3. Enables you to test identity-required APIs without full MSIX packaging -> **📝 Note:** This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: +> [!NOTE] +> This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: > - Modify `appxmanifest.xml` (change capabilities, identity, or properties) > - Update app assets (icons, logos, etc.) @@ -390,6 +405,43 @@ To fully integrate your ONNX model, you'll need to: - **[Windows App SDK Samples](https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/WindowsML)** - Collection of Windows App SDK samples - **[node-api-dotnet](https://github.com/microsoft/node-api-dotnet)** - C# ↔ JavaScript interop library +### Troubleshooting + +
+Build fails with NU1010: PackageReference items do not define a corresponding PackageVersion + +Ensure all packages referenced in `winMlAddon.csproj` have matching entries in `Directory.packages.props`. See Step 3 for the complete list of required packages. + +
+ +
+"not a valid Win32 application" when loading the addon + +This means the addon was built for a different architecture than your Node.js/Electron runtime. Check your Node.js architecture: + +```bash +node -e "console.log(process.arch)" +``` + +Then rebuild the addon with the matching target: + +```bash +# For x64 Node.js: +dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-x64 + +# For ARM64 Node.js: +dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-arm64 +``` + +If you recently changed your Node.js installation, also reinstall `node_modules` to get the matching Electron binary: + +```bash +rm -rf node_modules package-lock.json +npm install +``` + +
+ ### Get Help - **Found a bug?** [File an issue](https://github.com/microsoft/WinAppCli/issues) diff --git a/docs/guides/flutter.md b/docs/guides/flutter.md index a70fc97d..020a72ab 100644 --- a/docs/guides/flutter.md +++ b/docs/guides/flutter.md @@ -184,7 +184,8 @@ Run the executable directly (replace `flutter_app` with your project name if dif .\build\windows\x64\runner\Release\flutter_app.exe ``` -> **Tip**: The build output is in the `x64` folder regardless of your machine's architecture — this is expected for Flutter's Windows build. +> [!TIP] +> The build output is in the `x64` folder regardless of your machine's architecture — this is expected for Flutter's Windows build. You should see the app with an orange "Not packaged" indicator. This confirms that the standard executable is running without any package identity. From b1f73038f615edb0fba5dd8d1bcb8e5dcda7e982 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:38:56 -0400 Subject: [PATCH 19/27] Fixes for PhiSilica guide --- docs/guides/electron/phi-silica-addon.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/guides/electron/phi-silica-addon.md b/docs/guides/electron/phi-silica-addon.md index c50a423c..846c6b98 100644 --- a/docs/guides/electron/phi-silica-addon.md +++ b/docs/guides/electron/phi-silica-addon.md @@ -24,11 +24,12 @@ This creates a `csAddon/` folder with: - `csAddon.csproj` - Project file with references to Windows SDK and Windows App SDK - `README.md` - Documentation on how to use the addon -The command also adds a `build-csAddon` script to your `package.json` for building the addon: +The command also adds a `build-csAddon` script to your `package.json` for building the addon, and a `clean-csAddon` script for cleaning build artifacts: ```json { "scripts": { - "build-csAddon": "dotnet publish ./csAddon/csAddon.csproj -c Release" + "build-csAddon": "dotnet publish ./csAddon/csAddon.csproj -c Release", + "clean-csAddon": "dotnet clean ./csAddon/csAddon.csproj" } } ``` @@ -42,7 +43,8 @@ Let's verify everything is set up correctly by building the addon: npm run build-csAddon ``` -> **Note:** You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. +> [!NOTE] +> You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. ## Step 2: Add AI Capabilities with Phi Silica @@ -107,7 +109,7 @@ namespace csAddon Now build the addon again: ```bash -npm run build-addon +npm run build-csAddon ``` This compiles your C# code using **Native AOT** (Ahead-of-Time compilation), which: @@ -116,11 +118,11 @@ This compiles your C# code using **Native AOT** (Ahead-of-Time compilation), whi - Requires **no .NET runtime** on target machines - Provides native performance -The compiled addon will be in `csAddon/bin/Release/net10.0/win-/publish/csAddon.node` . +The compiled addon will be in `csAddon/dist/csAddon.node`. ## Step 4: Test the Windows API -Now let's verify the addon works by calling it from the main process. Open `src/index.js` and follow these steps: +Now let's verify the addon works by calling it from the main process. Open `src/main.js` and follow these steps: ### 4.1. Load the C# Addon From 839385c2a79f5dc5ee83174ba4f07d2bb46121aa Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:11:50 -0400 Subject: [PATCH 20/27] electron improvements --- docs/guides/electron/packaging.md | 58 +++++++++++++---- samples/electron/addon/binding.gyp | 2 +- samples/electron/test.Tests.ps1 | 19 +++++- src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj | 72 --------------------- src/winapp-npm/addon-template/binding.gyp | 2 +- 5 files changed, 63 insertions(+), 90 deletions(-) delete mode 100644 src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj diff --git a/docs/guides/electron/packaging.md b/docs/guides/electron/packaging.md index 6bf9c7a1..1d340980 100644 --- a/docs/guides/electron/packaging.md +++ b/docs/guides/electron/packaging.md @@ -11,19 +11,40 @@ Before packaging, make sure you've: ## Prepare for Packaging -> [!NOTE] -> Before packaging, make sure to configure your build tool (Electron Forge, webpack, etc.) to exclude temporary files from the final build: -> - `.winapp/` folder -> - `winapp.yaml` -> - Certificate files (`.pfx`) -> - Debug symbols (`.pdb`) -> - C# build artifacts (`obj/`, `bin/` folders) -> - MSIX packages (*.msix) +Configure Electron Forge to exclude temporary files from the final build. Add an `ignore` array to your `packagerConfig` in `forge.config.js`: + +```javascript +module.exports = { + packagerConfig: { + asar: true, + ignore: [ + /^\/\.winapp($|\/)/, // SDK packages and headers + /^\/winapp\.yaml$/, // SDK config + /\.pfx$/, // Certificate files + /\.pdb$/, // Debug symbols + /\/obj($|\/)/, // C# build artifacts + /\/bin($|\/)/, // C# build artifacts + /\.msix$/ // MSIX packages + ] + }, + // ... rest of your config +}; +``` > [!IMPORTANT] > Verify that your `appxmanifest.xml` matches your packaged app structure: > - The `Executable` attribute should point to the correct .exe file in your packaged output +## Generate a Development Certificate + +Before creating a signed MSIX package, generate a development certificate: + +```bash +npx winapp cert generate +``` + +This creates a `devcert.pfx` file in your project root that will be used to sign the MSIX package. + ## Packaging Options You have two options for creating an MSIX package for your Electron app: @@ -116,7 +137,7 @@ module.exports = { #### Update appxmanifest.xml -The Electron Forge MSIX maker uses a different folder layout than the winapp CLI approach. Update the `Executable` path in your `appxmanifest.xml` to point to the `app` folder: +The Electron Forge MSIX maker uses a different folder layout than the winapp CLI approach. It places your app inside an `app\` folder in the MSIX. This folder is created automatically during packaging — you don't need to create it yourself. Update the `Executable` path in your `appxmanifest.xml` to point to the `app` folder: ```xml @@ -128,17 +149,20 @@ The Electron Forge MSIX maker uses a different folder layout than the winapp CLI ``` -Replace `my-app.exe` with your actual executable name. +Replace `my-app.exe` with your actual executable name. This is based on the `productName` (or `name`) field in your `package.json`. + +> [!NOTE] +> The Forge MSIX maker looks for Windows SDK tools based on the `MinVersion` in your `appxmanifest.xml`. If you get an error about WindowsKit not being found, ensure the SDK version specified in `MinVersion` is installed on your machine, or update `MinVersion` to match an installed SDK version. #### Create the MSIX Package -Now you can create the MSIX package with a single command: +Now you can create the MSIX package. Use the `--targets` flag to run only the MSIX maker (otherwise Forge will run all configured makers): ```bash -npm run make +npx electron-forge make --targets @electron-forge/maker-msix ``` -The MSIX package will be created in the `./out/make/msix/` folder. +The MSIX package will be created in the `./out/make/msix//` folder (e.g., `./out/make/msix/arm64/` or `./out/make/msix/x64/`). > [!TIP] > This approach is more integrated with the Electron Forge workflow and automatically handles packaging and MSIX creation in one step. @@ -155,9 +179,15 @@ npx winapp cert install .\devcert.pfx Now install the MSIX package. Double click the msix file or run the following command: ```bash -Add-AppxPackage .\my-windows-app.msix +# Option 1 output: +Add-AppxPackage .\out\.msix + +# Option 2 output: +Add-AppxPackage .\out\make\msix\\.msix ``` +Replace `` and `` with the actual values from your build output. + Your app will appear in the Start Menu! Launch it and test your Windows API features. ## Distribution Options diff --git a/samples/electron/addon/binding.gyp b/samples/electron/addon/binding.gyp index f2c4fcf6..6021073e 100644 --- a/samples/electron/addon/binding.gyp +++ b/samples/electron/addon/binding.gyp @@ -11,7 +11,7 @@ "msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1, - "DebugInformationFormat": "OldStyle", + "DebugInformationFormat": 1, "AdditionalOptions": [ "/FS" ] diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 index a2f4806d..1825fc15 100644 --- a/samples/electron/test.Tests.ps1 +++ b/samples/electron/test.Tests.ps1 @@ -4,7 +4,7 @@ Pester 5.x tests for the Electron sample and guide workflow. .DESCRIPTION Phase 1: Follows the Electron guide from scratch — scaffolds an Electron app, - installs winapp, initializes workspace, creates and builds C++/C# addons, + installs winapp, initializes workspace, creates and builds C#/C++ addons, packages the app, and creates a signed MSIX package. Phase 2: Quick install of the existing sample to verify it is not stale. @@ -71,7 +71,7 @@ Describe "Electron Sample" { Invoke-Expression "npm cache clean --force" 2>$null Start-Sleep -Seconds 2 } - Invoke-Expression "npx -y create-electron-app@7.11.1 electron-app --template=webpack" + Invoke-Expression "npx -y create-electron-app@latest electron-app" if ($LASTEXITCODE -eq 0) { $created = $true; break } } $created | Should -Be $true -Because "Electron app creation should succeed within $maxRetries attempts" @@ -164,6 +164,13 @@ Describe "Electron Sample" { } finally { Pop-Location } } + It "Should register app with winapp run --no-launch" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "run `"$($script:appPackageDir)`" --no-launch" + } finally { Pop-Location } + } + It "Should generate a development certificate" -Skip:$script:skip { Push-Location $script:appDir try { @@ -206,5 +213,13 @@ Describe "Electron Sample" { It "Should have appxmanifest.xml" -Skip:$script:skip { Join-Path $script:sampleDir 'appxmanifest.xml' | Should -Exist } + + It "Should build the C# addon" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "npm run build-csAddon" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } } } diff --git a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj deleted file mode 100644 index 2993a072..00000000 --- a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj +++ /dev/null @@ -1,72 +0,0 @@ - - - - Exe - net10.0-windows10.0.19041.0 - enable - enable - winapp - Assets\winapp.ico - app.manifest - A command-line tool for managing Windows SDKs, packaging, generating app identity, manifests, certificates, and using build tools with any app framework. - app.manifest - - - true - Recommended - - - CA1852 - - - true - true - true - true - TrimRoots.xml - Size - full - true - true - true - - - false - false - true - - - TELEMETRYEVENTSOURCE_PUBLIC - - - - true - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - <_Parameter1>WinApp.Cli.Tests - - - - diff --git a/src/winapp-npm/addon-template/binding.gyp b/src/winapp-npm/addon-template/binding.gyp index abd73c32..cdfe145f 100644 --- a/src/winapp-npm/addon-template/binding.gyp +++ b/src/winapp-npm/addon-template/binding.gyp @@ -11,7 +11,7 @@ "msvs_settings": { "VCCLCompilerTool": { "ExceptionHandling": 1, - "DebugInformationFormat": "OldStyle", + "DebugInformationFormat": 1, "AdditionalOptions": [ "/FS" ] From 12399e26a24168ee0f4dcbaf1d3cd4ba5ee5e8ad Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:08:03 -0400 Subject: [PATCH 21/27] More verbose logs --- samples/electron/test.Tests.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 index 1825fc15..23281bf1 100644 --- a/samples/electron/test.Tests.ps1 +++ b/samples/electron/test.Tests.ps1 @@ -132,7 +132,9 @@ Describe "Electron Sample" { It "Should build the C++ addon" -Skip:$script:skip { Push-Location $script:appDir try { - Invoke-Expression "npm run build-testCppAddon" + # Use --verbose to capture full MSBuild output for CI diagnostics + $output = Invoke-Expression "npx node-gyp clean configure build --directory=testCppAddon --verbose 2>&1" + $output | ForEach-Object { Write-Host $_ } $LASTEXITCODE | Should -Be 0 } finally { Pop-Location } } From 5f1f732a8409125b8e42e53809c4b1daa6fdd7d7 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:58:09 -0400 Subject: [PATCH 22/27] attempt to fix electron tests --- docs/npm-usage.md | 2 ++ samples/electron/test.Tests.ps1 | 1 - .../Services/PackageInstallationService.cs | 34 ++++++++++++++++--- src/winapp-npm/src/winapp-commands.ts | 3 ++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/npm-usage.md b/docs/npm-usage.md index d2e69046..3cf1b2c9 100644 --- a/docs/npm-usage.md +++ b/docs/npm-usage.md @@ -343,6 +343,7 @@ function run(options: RunOptions): Promise | `manifest` | `string \| undefined` | No | Path to the appxmanifest.xml (default: auto-detect from input folder or current directory) | | `noLaunch` | `boolean \| undefined` | No | Only create the debug identity and register the package without launching the application | | `outputAppxDirectory` | `string \| undefined` | No | Output directory for the loose layout package. If not specified, a directory named AppX inside the input-folder directory will be used. | +| `symbols` | `boolean \| undefined` | No | Download symbols from Microsoft Symbol Server for richer native crash analysis. Only used with --debug-output. First run downloads symbols and caches them locally; subsequent runs use the cache. | | `unregisterOnExit` | `boolean \| undefined` | No | Unregister the development package after the application exits. Only removes packages registered in development mode. | | `withAlias` | `boolean \| undefined` | No | Launch the app using its execution alias instead of AUMID activation. The app runs in the current terminal with inherited stdin/stdout/stderr. Requires a uap5:ExecutionAlias in the manifest. Use "winapp manifest add-alias" to add an execution alias to the manifest. | @@ -1249,6 +1250,7 @@ type ManifestTemplates = "packaged" | "sparse" | `manifest` | `string \| undefined` | No | Path to the appxmanifest.xml (default: auto-detect from input folder or current directory) | | `noLaunch` | `boolean \| undefined` | No | Only create the debug identity and register the package without launching the application | | `outputAppxDirectory` | `string \| undefined` | No | Output directory for the loose layout package. If not specified, a directory named AppX inside the input-folder directory will be used. | +| `symbols` | `boolean \| undefined` | No | Download symbols from Microsoft Symbol Server for richer native crash analysis. Only used with --debug-output. First run downloads symbols and caches them locally; subsequent runs use the cache. | | `unregisterOnExit` | `boolean \| undefined` | No | Unregister the development package after the application exits. Only removes packages registered in development mode. | | `withAlias` | `boolean \| undefined` | No | Launch the app using its execution alias instead of AUMID activation. The app runs in the current terminal with inherited stdin/stdout/stderr. Requires a uap5:ExecutionAlias in the manifest. Use "winapp manifest add-alias" to add an execution alias to the manifest. | | `quiet` | `boolean \| undefined` | No | Suppress progress messages. | diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 index 23281bf1..a0e0989d 100644 --- a/samples/electron/test.Tests.ps1 +++ b/samples/electron/test.Tests.ps1 @@ -132,7 +132,6 @@ Describe "Electron Sample" { It "Should build the C++ addon" -Skip:$script:skip { Push-Location $script:appDir try { - # Use --verbose to capture full MSBuild output for CI diagnostics $output = Invoke-Expression "npx node-gyp clean configure build --directory=testCppAddon --verbose 2>&1" $output | ForEach-Object { Write-Host $_ } $LASTEXITCODE | Should -Be 0 diff --git a/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs b/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs index 3ec6188c..05bdd982 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs @@ -119,7 +119,7 @@ public async Task> InstallPackagesAsync( // Add the main package to installed versions allInstalledVersions[packageName] = version; - // Try to get package information about what else is installed with this package + // Resolve transitive dependencies and ensure they are also present on disk try { var cachedPackages = await nugetService.GetPackageDependenciesAsync(packageName, version, cancellationToken); @@ -128,16 +128,40 @@ public async Task> InstallPackagesAsync( var depVersion = NugetService.ParseMinimumVersion(packageVersion); if (!string.IsNullOrEmpty(depVersion)) { - if (allInstalledVersions.TryGetValue(packageId, out var existingVersion)) + // Check if the dependency actually exists on disk — if not, install it + var depDir = nugetService.GetNuGetPackageDir(packageId, depVersion); + if (!depDir.Exists) { - if (NugetService.CompareVersions(depVersion, existingVersion) > 0) + logger.LogDebug("Transitive dependency {PackageId} {Version} missing from cache, installing", packageId, depVersion); + var depInstalledVersions = await nugetService.InstallPackageAsync(packageId, depVersion, taskContext, cancellationToken); + foreach (var (depPkg, depVer) in depInstalledVersions) { - allInstalledVersions[packageId] = depVersion; + if (allInstalledVersions.TryGetValue(depPkg, out var existingDepVersion)) + { + if (NugetService.CompareVersions(depVer, existingDepVersion) > 0) + { + allInstalledVersions[depPkg] = depVer; + } + } + else + { + allInstalledVersions[depPkg] = depVer; + } } } else { - allInstalledVersions[packageId] = depVersion; + if (allInstalledVersions.TryGetValue(packageId, out var existingVersion)) + { + if (NugetService.CompareVersions(depVersion, existingVersion) > 0) + { + allInstalledVersions[packageId] = depVersion; + } + } + else + { + allInstalledVersions[packageId] = depVersion; + } } } } diff --git a/src/winapp-npm/src/winapp-commands.ts b/src/winapp-npm/src/winapp-commands.ts index b5032437..31b6fe27 100644 --- a/src/winapp-npm/src/winapp-commands.ts +++ b/src/winapp-npm/src/winapp-commands.ts @@ -455,6 +455,8 @@ export interface RunOptions extends CommonOptions { noLaunch?: boolean; /** Output directory for the loose layout package. If not specified, a directory named AppX inside the input-folder directory will be used. */ outputAppxDirectory?: string; + /** Download symbols from Microsoft Symbol Server for richer native crash analysis. Only used with --debug-output. First run downloads symbols and caches them locally; subsequent runs use the cache. */ + symbols?: boolean; /** Unregister the development package after the application exits. Only removes packages registered in development mode. */ unregisterOnExit?: boolean; /** Launch the app using its execution alias instead of AUMID activation. The app runs in the current terminal with inherited stdin/stdout/stderr. Requires a uap5:ExecutionAlias in the manifest. Use "winapp manifest add-alias" to add an execution alias to the manifest. */ @@ -475,6 +477,7 @@ export async function run(options: RunOptions): Promise { if (options.manifest) args.push('--manifest', options.manifest); if (options.noLaunch) args.push('--no-launch'); if (options.outputAppxDirectory) args.push('--output-appx-directory', options.outputAppxDirectory); + if (options.symbols) args.push('--symbols'); if (options.unregisterOnExit) args.push('--unregister-on-exit'); if (options.withAlias) args.push('--with-alias'); return execCommand(args, options); From 3fe41811a9bafb2f82863c58fa4205bc3933b3a2 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:14:14 -0400 Subject: [PATCH 23/27] More verbose logs for debugging --- samples/electron/test.Tests.ps1 | 10 +++++++++- src/winapp-CLI/WinApp.Cli/Services/NugetService.cs | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 index a0e0989d..00da0844 100644 --- a/samples/electron/test.Tests.ps1 +++ b/samples/electron/test.Tests.ps1 @@ -101,7 +101,15 @@ Describe "Electron Sample" { It "Should initialize winapp workspace" -Skip:$script:skip { Push-Location $script:appDir try { - Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable" + Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable --verbose" + # Diagnostic: list what libs were copied + $libDir = Join-Path $script:appDir ".winapp\lib\x64" + if (Test-Path $libDir) { + Write-Host "=== .winapp/lib/x64 contents ===" + Get-ChildItem $libDir | ForEach-Object { Write-Host " $($_.Name)" } + } else { + Write-Host "=== .winapp/lib/x64 does NOT exist ===" + } } finally { Pop-Location } } diff --git a/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs b/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs index e42bd770..6c8370fc 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs @@ -166,9 +166,11 @@ private async Task ResolveDependenciesAsync(DirectoryInfo packageDir, string pac } } } - catch + catch (Exception ex) { - // Dependency resolution failures are non-fatal; the main package is installed + // Dependency resolution failures are non-fatal; the main package is installed. + // Log so transitive dependency issues are visible in verbose/debug output. + taskContext.AddDebugMessage($"{UiSymbols.Note} Dependency resolution for {package} {version}: {ex.Message}"); } } From 7b1b52b556019c55cca20c5611faa1fde7dbacc9 Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:25:28 -0400 Subject: [PATCH 24/27] Debugging path issue --- samples/electron/addon/binding.gyp | 4 ++-- samples/electron/test.Tests.ps1 | 10 +--------- src/winapp-npm/addon-template/binding.gyp | 4 ++-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/samples/electron/addon/binding.gyp b/samples/electron/addon/binding.gyp index 6021073e..5d1540c1 100644 --- a/samples/electron/addon/binding.gyp +++ b/samples/electron/addon/binding.gyp @@ -6,7 +6,7 @@ "include_dirs": [ " Date: Thu, 16 Apr 2026 14:47:15 -0400 Subject: [PATCH 25/27] Fix test failures: update manifest filename from appxmanifest.xml to Package.appxmanifest winapp init and manifest generate now output Package.appxmanifest instead of appxmanifest.xml. Update all Phase 1 test assertions and --manifest arguments to match the new filename. Phase 2 (existing sample) references are unchanged since those samples still have appxmanifest.xml committed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/cpp-app/test.Tests.ps1 | 4 ++-- samples/dotnet-app/test.Tests.ps1 | 6 +++--- samples/electron/test.Tests.ps1 | 2 +- samples/flutter-app/test.Tests.ps1 | 4 ++-- samples/packaging-cli/test.Tests.ps1 | 4 ++-- samples/rust-app/test.Tests.ps1 | 6 +++--- samples/tauri-app/test.Tests.ps1 | 8 ++++---- samples/wpf-app/test.Tests.ps1 | 6 +++--- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/samples/cpp-app/test.Tests.ps1 b/samples/cpp-app/test.Tests.ps1 index a20f7f0b..0da097e8 100644 --- a/samples/cpp-app/test.Tests.ps1 +++ b/samples/cpp-app/test.Tests.ps1 @@ -78,7 +78,7 @@ add_executable(test-cpp-app main.cpp) It "winapp init creates config files" -Skip:$script:skip { Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable" "winapp.yaml" | Should -Exist - "appxmanifest.xml" | Should -Exist + "Package.appxmanifest" | Should -Exist ".winapp" | Should -Exist } @@ -111,7 +111,7 @@ add_executable(test-cpp-app main.cpp) } It "packages as MSIX" -Skip:$script:skip { - Invoke-WinappCommand -Arguments "pack build\Release --manifest appxmanifest.xml --cert devcert.pfx" + Invoke-WinappCommand -Arguments "pack build\Release --manifest Package.appxmanifest --cert devcert.pfx" Get-ChildItem -Filter "*.msix" | Should -Not -BeNullOrEmpty -Because "MSIX package should be created" } } diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 index 7f357ac1..276a17ff 100644 --- a/samples/dotnet-app/test.Tests.ps1 +++ b/samples/dotnet-app/test.Tests.ps1 @@ -67,8 +67,8 @@ Describe ".NET App Guide Workflow" { } finally { Pop-Location } } - It "Should have created appxmanifest.xml" -Skip:$script:skip { - Join-Path $script:projectDir "appxmanifest.xml" | Should -Exist + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:projectDir "Package.appxmanifest" | Should -Exist } } @@ -148,7 +148,7 @@ Describe ".NET App Guide Workflow" { It "Should package MSIX with winapp pack" -Skip:$script:skip { Push-Location $script:projectDir try { - Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest appxmanifest.xml --cert devcert.pfx" + Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest Package.appxmanifest --cert devcert.pfx" } finally { Pop-Location } } diff --git a/samples/electron/test.Tests.ps1 b/samples/electron/test.Tests.ps1 index a0e0989d..813c2685 100644 --- a/samples/electron/test.Tests.ps1 +++ b/samples/electron/test.Tests.ps1 @@ -108,7 +108,7 @@ Describe "Electron Sample" { It "Should create workspace files" -Skip:$script:skip { Join-Path $script:appDir ".winapp" | Should -Exist Join-Path $script:appDir "winapp.yaml" | Should -Exist - Join-Path $script:appDir "appxmanifest.xml" | Should -Exist + Join-Path $script:appDir "Package.appxmanifest" | Should -Exist } It "Should create a C++ native addon" -Skip:$script:skip { diff --git a/samples/flutter-app/test.Tests.ps1 b/samples/flutter-app/test.Tests.ps1 index baac648d..c84ffe80 100644 --- a/samples/flutter-app/test.Tests.ps1 +++ b/samples/flutter-app/test.Tests.ps1 @@ -69,8 +69,8 @@ Describe "flutter-app sample" { Join-Path $script:projectDir "winapp.yaml" | Should -Exist } - It "Should create appxmanifest.xml after init" { - Join-Path $script:projectDir "appxmanifest.xml" | Should -Exist + It "Should create Package.appxmanifest after init" { + Join-Path $script:projectDir "Package.appxmanifest" | Should -Exist } It "Should create .winapp directory after init" { diff --git a/samples/packaging-cli/test.Tests.ps1 b/samples/packaging-cli/test.Tests.ps1 index f02a5025..17432e9a 100644 --- a/samples/packaging-cli/test.Tests.ps1 +++ b/samples/packaging-cli/test.Tests.ps1 @@ -48,8 +48,8 @@ Describe "Packaging CLI Guide Workflow" { } finally { Pop-Location } } - It "Should have created appxmanifest.xml" -Skip:$script:skip { - Join-Path $script:packageDir "appxmanifest.xml" | Should -Exist + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:packageDir "Package.appxmanifest" | Should -Exist } } diff --git a/samples/rust-app/test.Tests.ps1 b/samples/rust-app/test.Tests.ps1 index 460b5122..d1537761 100644 --- a/samples/rust-app/test.Tests.ps1 +++ b/samples/rust-app/test.Tests.ps1 @@ -57,8 +57,8 @@ Describe "Rust App Sample" { } finally { Pop-Location } } - It "Should have created appxmanifest.xml" -Skip:$script:skip { - Join-Path $script:rustProjectDir "appxmanifest.xml" | Should -Exist + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:rustProjectDir "Package.appxmanifest" | Should -Exist } It "Should add execution alias to manifest" -Skip:$script:skip { @@ -124,7 +124,7 @@ Describe "Rust App Sample" { It "Should package as MSIX" -Skip:$script:skip { Push-Location $script:rustProjectDir try { - Invoke-WinappCommand -Arguments "pack dist --manifest appxmanifest.xml --cert devcert.pfx" + Invoke-WinappCommand -Arguments "pack dist --manifest Package.appxmanifest --cert devcert.pfx" } finally { Pop-Location } } diff --git a/samples/tauri-app/test.Tests.ps1 b/samples/tauri-app/test.Tests.ps1 index 320f5712..e706b484 100644 --- a/samples/tauri-app/test.Tests.ps1 +++ b/samples/tauri-app/test.Tests.ps1 @@ -68,8 +68,8 @@ Describe "Tauri App Sample" { } finally { Pop-Location } } - It "Should have created appxmanifest.xml" -Skip:$script:skip { - Join-Path $script:tempApp "appxmanifest.xml" | Should -Exist + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:tempApp "Package.appxmanifest" | Should -Exist } It "Should build Tauri app in debug mode" -Skip:$script:skip { @@ -116,7 +116,7 @@ Describe "Tauri App Sample" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-WinappCommand -Arguments "cert generate --if-exists skip --manifest appxmanifest.xml" + Invoke-WinappCommand -Arguments "cert generate --if-exists skip --manifest Package.appxmanifest" } finally { Pop-Location } } @@ -127,7 +127,7 @@ Describe "Tauri App Sample" { It "Should package as MSIX" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-WinappCommand -Arguments "pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" + Invoke-WinappCommand -Arguments "pack msix-layout --manifest Package.appxmanifest --cert devcert.pfx" } finally { Pop-Location } } diff --git a/samples/wpf-app/test.Tests.ps1 b/samples/wpf-app/test.Tests.ps1 index c613b228..b883278e 100644 --- a/samples/wpf-app/test.Tests.ps1 +++ b/samples/wpf-app/test.Tests.ps1 @@ -64,8 +64,8 @@ Describe 'wpf-app sample' { Invoke-WinappCommand -Arguments 'init --use-defaults' } - It 'Generates appxmanifest.xml from winapp init' -Skip:$script:skip { - 'appxmanifest.xml' | Should -Exist + It 'Generates Package.appxmanifest from winapp init' -Skip:$script:skip { + 'Package.appxmanifest' | Should -Exist } It 'Builds in Debug mode' -Skip:$script:skip { @@ -104,7 +104,7 @@ Describe 'wpf-app sample' { $exeFile | Should -Not -BeNullOrEmpty -Because 'Release build should produce an .exe' $script:outputDir = $exeFile.DirectoryName - Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest appxmanifest.xml --cert devcert.pfx" + Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest Package.appxmanifest --cert devcert.pfx" } It 'Produces an MSIX file' -Skip:$script:skip { From 9a4c36f21adcb8cc892f6cf3a957ad197ad9474a Mon Sep 17 00:00:00 2001 From: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:14:41 -0400 Subject: [PATCH 26/27] Fix tauri test: use appxmanifest.xml since init preserves existing manifest The tauri test copies the existing sample (which has appxmanifest.xml) before running winapp init. Since init with --use-defaults preserves existing manifests, the file remains appxmanifest.xml rather than being regenerated as Package.appxmanifest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/tauri-app/test.Tests.ps1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/tauri-app/test.Tests.ps1 b/samples/tauri-app/test.Tests.ps1 index e706b484..cbee2b91 100644 --- a/samples/tauri-app/test.Tests.ps1 +++ b/samples/tauri-app/test.Tests.ps1 @@ -68,8 +68,9 @@ Describe "Tauri App Sample" { } finally { Pop-Location } } - It "Should have created Package.appxmanifest" -Skip:$script:skip { - Join-Path $script:tempApp "Package.appxmanifest" | Should -Exist + It "Should have appxmanifest" -Skip:$script:skip { + # winapp init preserves the existing appxmanifest.xml copied from the sample + Join-Path $script:tempApp "appxmanifest.xml" | Should -Exist } It "Should build Tauri app in debug mode" -Skip:$script:skip { @@ -116,7 +117,7 @@ Describe "Tauri App Sample" { It "Should generate dev certificate" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-WinappCommand -Arguments "cert generate --if-exists skip --manifest Package.appxmanifest" + Invoke-WinappCommand -Arguments "cert generate --if-exists skip --manifest appxmanifest.xml" } finally { Pop-Location } } @@ -127,7 +128,7 @@ Describe "Tauri App Sample" { It "Should package as MSIX" -Skip:$script:skip { Push-Location $script:tempApp try { - Invoke-WinappCommand -Arguments "pack msix-layout --manifest Package.appxmanifest --cert devcert.pfx" + Invoke-WinappCommand -Arguments "pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" } finally { Pop-Location } } From 657130662cb566c864eab9cabe401fb6912b0037 Mon Sep 17 00:00:00 2001 From: Nikola Metulev Date: Fri, 17 Apr 2026 13:02:12 -0700 Subject: [PATCH 27/27] Address PR #351 review comments - Resolve-WinappCliPath: include artifacts\ in default candidates and pick newest .tgz by LastWriteTime - Invoke-WinappCommand: prefer winapp on PATH over dotnet run against repo source so tests exercise the packaged CLI; opt back into dotnet via WINAPP_TEST_USE_DOTNET=1 - test-samples.yml: drop workflow_run trigger to avoid duplicate runs per PR; build job now also runs on workflow_dispatch so the workflow is self-contained; simplify artifact downloads - AGENTS.md: fix local example to use .\artifacts\ where package-npm.ps1 emits the tarball Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-samples.yml | 81 ++++-------------------------- AGENTS.md | 5 +- samples/SampleTestHelpers.psm1 | 38 ++++++++------ 3 files changed, 37 insertions(+), 87 deletions(-) diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml index ab4ee9c4..2f9a53e4 100644 --- a/.github/workflows/test-samples.yml +++ b/.github/workflows/test-samples.yml @@ -3,9 +3,6 @@ name: Test Samples & Guides on: pull_request: branches: [ "main" ] - workflow_run: - workflows: ["Build and Package"] - types: [completed] workflow_dispatch: inputs: sample: @@ -26,12 +23,12 @@ on: permissions: contents: read - actions: read # Required for downloading cross-workflow artifacts jobs: - # Build npm package for pull_request events (workflow_run already has it) + # Build npm (and NuGet) package for pull_request and workflow_dispatch events. + # This makes the workflow self-contained — no cross-workflow artifact chaining. build: - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' runs-on: windows-latest steps: - uses: actions/checkout@v5 @@ -56,15 +53,7 @@ jobs: test-sample: needs: [build] - # Run when: PR build succeeded, workflow_run build succeeded, or manual dispatch - if: >- - always() && - (needs.build.result == 'success' || needs.build.result == 'skipped') && - ( - github.event_name == 'pull_request' || - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' - ) + if: needs.build.result == 'success' strategy: fail-fast: false matrix: @@ -88,66 +77,18 @@ jobs: if: steps.check.outputs.skip != 'true' uses: actions/checkout@v5 - # Download the npm package artifact — source varies by trigger - - name: Download npm package (pull_request) - if: >- - steps.check.outputs.skip != 'true' && - github.event_name == 'pull_request' - uses: actions/download-artifact@v4 - with: - name: npm-package - path: artifacts/npm - - - name: Download npm package (workflow_run) - if: >- - steps.check.outputs.skip != 'true' && - github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: npm-package - path: artifacts/npm - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Download npm package (workflow_dispatch) - if: >- - steps.check.outputs.skip != 'true' && - github.event_name == 'workflow_dispatch' + # Download the npm package artifact built by the `build` job above + - name: Download npm package + if: steps.check.outputs.skip != 'true' uses: actions/download-artifact@v4 with: name: npm-package path: artifacts/npm - continue-on-error: true # Download NuGet package for .NET samples - - name: Download NuGet package (pull_request) + - name: Download NuGet package if: >- steps.check.outputs.skip != 'true' && - github.event_name == 'pull_request' && - contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) - uses: actions/download-artifact@v4 - with: - name: nuget-package - path: artifacts/nuget - continue-on-error: true - - - name: Download NuGet package (workflow_run) - if: >- - steps.check.outputs.skip != 'true' && - github.event_name == 'workflow_run' && - contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) - uses: actions/download-artifact@v4 - with: - name: nuget-package - path: artifacts/nuget - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - continue-on-error: true - - - name: Download NuGet package (workflow_dispatch) - if: >- - steps.check.outputs.skip != 'true' && - github.event_name == 'workflow_dispatch' && contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) uses: actions/download-artifact@v4 with: @@ -214,12 +155,8 @@ jobs: if: steps.check.outputs.skip != 'true' shell: pwsh run: | - $winappPath = "artifacts/npm" - if (-not (Test-Path $winappPath)) { - $winappPath = "src/winapp-npm" - } $container = New-PesterContainer -Path "samples/${{ matrix.sample }}/test.Tests.ps1" -Data @{ - WinappPath = $winappPath + WinappPath = "artifacts/npm" } $config = New-PesterConfiguration $config.Run.Container = $container diff --git a/AGENTS.md b/AGENTS.md index 85d824da..6e2f9d70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,10 @@ Each sample under `samples/` has a self-contained **Pester 5.x** test file (`tes # Run a specific sample .\scripts\test-samples.ps1 -Samples dotnet-app -# Run with a specific winapp package (e.g., from CI artifacts) +# Run with a locally built winapp npm tarball (package-npm.ps1 outputs to .\artifacts\) +.\scripts\test-samples.ps1 -WinappPath .\artifacts -Verbose + +# Or pass a specific .tgz / a directory containing one (e.g., a CI artifact download) .\scripts\test-samples.ps1 -WinappPath .\artifacts\npm -Verbose ``` diff --git a/samples/SampleTestHelpers.psm1 b/samples/SampleTestHelpers.psm1 index c529e370..d91a70f8 100644 --- a/samples/SampleTestHelpers.psm1 +++ b/samples/SampleTestHelpers.psm1 @@ -31,20 +31,25 @@ function Resolve-WinappCliPath { $repoRoot = (Resolve-Path "$PSScriptRoot\..").Path if (-not $WinappPath) { - $WinappPath = Join-Path $repoRoot "artifacts\npm" - if (-not (Test-Path $WinappPath)) { - $WinappPath = Join-Path $repoRoot "src\winapp-npm" - } + # Default search order: CI artifact dir, local package-npm.ps1 output dir, then source dir. + $defaultCandidates = @( + (Join-Path $repoRoot "artifacts\npm"), + (Join-Path $repoRoot "artifacts"), + (Join-Path $repoRoot "src\winapp-npm") + ) + $WinappPath = $defaultCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 } - if (-not (Test-Path $WinappPath)) { + if (-not $WinappPath -or -not (Test-Path $WinappPath)) { throw "Winapp path not found: $WinappPath" } $resolved = (Resolve-Path $WinappPath).Path if (Test-Path $resolved -PathType Container) { - $tgz = Get-ChildItem -Path $resolved -Filter "*.tgz" -ErrorAction SilentlyContinue | Select-Object -First 1 + $tgz = Get-ChildItem -Path $resolved -Filter "*.tgz" -ErrorAction SilentlyContinue | + Sort-Object -Property LastWriteTime -Descending | + Select-Object -First 1 if ($tgz) { return $tgz.FullName } if (Test-Path (Join-Path $resolved "package.json")) { return $resolved } throw "No .tgz or package.json found in $resolved" @@ -57,8 +62,9 @@ function Invoke-WinappCommand { <# .SYNOPSIS Invokes the winapp CLI with the given arguments and returns stdout lines. - Uses npx if in a Node project, falls back to dotnet run, then PATH. - Throws on non-zero exit code. + Resolution order: local node_modules/.bin/winapp -> winapp on PATH -> + dotnet run against the repo CLI project (only when WINAPP_TEST_USE_DOTNET=1 + or no other winapp is available). Throws on non-zero exit code. #> param( [Parameter(Mandatory)] @@ -67,15 +73,19 @@ function Invoke-WinappCommand { ) $npxWinapp = Join-Path (Get-Location) "node_modules\.bin\winapp.cmd" + $pathWinapp = Get-Command winapp -ErrorAction SilentlyContinue + $cliProject = Join-Path $PSScriptRoot "..\src\winapp-CLI\WinApp.Cli\WinApp.Cli.csproj" + $useDotnet = $env:WINAPP_TEST_USE_DOTNET -eq '1' + if (Test-Path $npxWinapp) { $cmd = "npx winapp $Arguments" + } elseif ($pathWinapp -and -not $useDotnet) { + $cmd = "winapp $Arguments" + } elseif (Test-Path $cliProject) { + # Fall back to dotnet run when no installed winapp is on PATH, or when explicitly requested. + $cmd = "dotnet run --project `"$cliProject`" -- $Arguments" } else { - $cliProject = Join-Path $PSScriptRoot "..\src\winapp-CLI\WinApp.Cli\WinApp.Cli.csproj" - if (Test-Path $cliProject) { - $cmd = "dotnet run --project `"$cliProject`" -- $Arguments" - } else { - $cmd = "winapp $Arguments" - } + $cmd = "winapp $Arguments" } Write-Verbose "Running: $cmd"