| File: PlaywrightCliInstallTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.TestUtilities; using Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; /// <summary> /// End-to-end test verifying that the Playwright CLI installation flow works correctly /// through <c>aspire agent init</c>, including npm provenance verification and skill file generation. /// </summary> [OuterloopTest("Requires npm and network access to install @playwright/cli from the npm registry")] public sealed class PlaywrightCliInstallTests(ITestOutputHelper output) { /// <summary> /// Verifies the full Playwright CLI installation lifecycle: /// 1. Playwright CLI is not initially installed /// 2. An Aspire project is created /// 3. <c>aspire agent init</c> is run with Claude Code environment selected /// 4. Playwright CLI is installed and available on PATH /// 5. The <c>.claude/skills/playwright-cli/SKILL.md</c> skill file is generated /// </summary> [Fact] public async Task AgentInit_InstallsPlaywrightCli_AndGeneratesSkillFiles() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); // Step 1: Verify playwright-cli is not installed. await auto.TypeAsync("playwright-cli --version 2>&1 || true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Step 2: Create an Aspire project (accept all defaults). await auto.AspireNewAsync("TestProject", counter); // Step 3: Navigate into the project and create .claude folder to trigger Claude Code detection. await auto.TypeAsync("cd TestProject && mkdir -p .claude"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Step 4: Run aspire agent init for Playwright only. This test is about // @playwright/cli acquisition, not the Aspire skills bundle. await auto.TypeAsync("aspire agent init --workspace-root . --skill-locations claudecode --skills playwright-cli --mcp=false"); await auto.EnterAsync(); // Wait for installation to complete (this downloads from npm, can take a while) await auto.WaitUntilTextAsync("configuration complete", timeout: TimeSpan.FromMinutes(3)); await auto.WaitForSuccessPromptAsync(counter); // Step 5: Verify playwright-cli is now installed. await auto.TypeAsync("playwright-cli --version"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Step 6: Verify the skill file was generated. await auto.TypeAsync("ls .claude/skills/playwright-cli/SKILL.md"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("SKILL.md", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); } /// <summary> /// Verifies that when <c>aspire agent init</c> is run from a different directory than the /// workspace root, <c>playwright-cli install --skills</c> generates skill files in the /// workspace root, not the current working directory. /// /// This is a regression test for https://github.com/microsoft/aspire/issues/15140 where /// the missing <c>WorkingDirectory</c> on <c>ProcessStartInfo</c> caused skill files /// to be dropped in the CLI process's current working directory. /// </summary> [Fact] public async Task AgentInit_CwdDiffersFromRoot_PlacesSkillsInWorkspaceRoot() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); // Step 1: Create an Aspire project. await auto.AspireNewAsync("TestProject", counter); // Step 2: Create .claude folder inside the project to trigger Claude Code detection. // Crucially, do NOT cd into the project — stay in the parent directory. await auto.TypeAsync("mkdir -p TestProject/.claude"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Step 3: Run aspire agent init from the PARENT directory for Playwright // only. When provided as options, the workspace root and skill selection // are deterministic and do not depend on Aspire default skills. await auto.TypeAsync("aspire agent init --workspace-root TestProject --skill-locations claudecode --skills playwright-cli --mcp=false"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("configuration complete", timeout: TimeSpan.FromMinutes(3)); await auto.WaitForSuccessPromptAsync(counter); // Step 4: Verify skill file exists in the workspace root (project subdirectory). await auto.TypeAsync("ls TestProject/.claude/skills/playwright-cli/SKILL.md"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("SKILL.md", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); // Step 5: Verify no stray skill files were created in the CWD (parent directory). await auto.TypeAsync("test -d .claude/skills/playwright-cli && echo 'STRAY_FILES_FOUND' || echo 'NO_STRAY_FILES'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("NO_STRAY_FILES", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); } }