|
// 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.Cli.Tests.Utils;
using Hex1b.Automation;
using Hex1b.Input;
using Aspire.TestUtilities;
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 installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot);
var workspace = TemporaryWorkspace.Create(output);
using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace);
var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
var counter = new SequenceCounter();
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
await auto.PrepareDockerEnvironmentAsync(counter, workspace);
await auto.InstallAspireCliInDockerAsync(installMode, 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.
// First prompt: workspace path
await auto.TypeAsync("aspire agent init");
await auto.EnterAsync();
await auto.WaitUntilTextAsync("workspace:", timeout: TimeSpan.FromSeconds(30));
await auto.WaitAsync(500);
await auto.EnterAsync(); // Accept default workspace path
// Second prompt: agent environments (select Claude Code)
await auto.WaitUntilTextAsync("agent environments", timeout: TimeSpan.FromSeconds(60));
await auto.TypeAsync(" "); // Toggle first option (Claude Code)
await auto.EnterAsync();
// Third prompt: additional options (select Playwright CLI installation)
// Aspire skill file (priority 0) appears first, Playwright CLI (priority 1) second.
await auto.WaitUntilTextAsync("additional options", timeout: TimeSpan.FromSeconds(30));
await auto.WaitUntilTextAsync("Install Playwright CLI", timeout: TimeSpan.FromSeconds(10));
await auto.TypeAsync(" "); // Toggle first option (Aspire skill file)
await auto.DownAsync(); // Move to Playwright CLI option
await auto.TypeAsync(" "); // Toggle Playwright CLI option
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);
await auto.TypeAsync("exit");
await auto.EnterAsync();
await pendingRun;
}
/// <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_WhenCwdDiffersFromWorkspaceRoot_PlacesSkillFilesInWorkspaceRoot()
{
var repoRoot = CliE2ETestHelpers.GetRepoRoot();
var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot);
var workspace = TemporaryWorkspace.Create(output);
using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace);
var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
var counter = new SequenceCounter();
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
await auto.PrepareDockerEnvironmentAsync(counter, workspace);
await auto.InstallAspireCliInDockerAsync(installMode, 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.WaitForSuccessPromptFailFastAsync(counter);
// Step 3: Run aspire agent init from the PARENT directory.
// When prompted for workspace root, provide the project subdirectory.
await auto.TypeAsync("aspire agent init");
await auto.EnterAsync();
await auto.WaitUntilTextAsync("workspace:", timeout: TimeSpan.FromSeconds(30));
await auto.WaitAsync(500);
// Clear the default path and type the project subdirectory instead.
// The default will be the git root or CWD — we need to override it.
await auto.Ctrl().KeyAsync(Hex1bKey.U); // Clear the line
await auto.TypeAsync("TestProject");
await auto.EnterAsync();
// Select Claude Code environment.
await auto.WaitUntilTextAsync("agent environments", timeout: TimeSpan.FromSeconds(60));
await auto.TypeAsync(" ");
await auto.EnterAsync();
// Select Playwright CLI installation.
await auto.WaitUntilTextAsync("additional options", timeout: TimeSpan.FromSeconds(30));
await auto.WaitUntilTextAsync("Install Playwright CLI", timeout: TimeSpan.FromSeconds(10));
await auto.TypeAsync(" "); // Toggle first option (Aspire skill file)
await auto.DownAsync();
await auto.TypeAsync(" "); // Toggle Playwright CLI
await auto.EnterAsync();
await auto.WaitUntilTextAsync("configuration complete", timeout: TimeSpan.FromMinutes(3));
await auto.WaitForSuccessPromptFailFastAsync(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.WaitForSuccessPromptFailFastAsync(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.WaitForSuccessPromptFailFastAsync(counter);
await auto.TypeAsync("exit");
await auto.EnterAsync();
await pendingRun;
}
}
|