Prompt file imported from autocloudarc-digital-services/ghcp-dotnet-calculator (
.github/prompts/9.00.playwright-e2e-testing.prompt.md). Copyright stays with the author.
Playwright End-to-End Testing Frameworkls
Version: 1.0 Last Updated: January 29, 2026 Framework: Playwright for .NET (pwsh) Target: Local development and CI/CD integration
Overview
This framework sets up Playwright for local E2E testing of the calculator application (currently console, later web UI). Playwright enables:
- Cross-browser testing – Chromium, Firefox, WebKit validation
- Headless & headed modes – Automated testing or visual debugging
- User interaction simulation – Full workflow automation
- Screenshot/video capture – Visual regression detection
- Performance profiling – Response time validation
- API mocking – Isolated component testing
Setup Instructions
Prerequisites
Required:
- .NET 8.0 SDK or later
- PowerShell 5.0+
- Playwright binaries (auto-installed)
- Chrome/Chromium browser (for headless tests)
Optional:
- VS Code Playwright Test for VSCode extension (
ms-playwright.playwright) - Playwright Inspector for UI debugging
Installation Steps
Step 1: Create E2E Test Project
# Navigate to solution directory
cd src/workspace/calculator-xunit-testing
# Create new xUnit test project for E2E tests
dotnet new xunit -n calculator.tests.e2e -f net8.0
# Add to solution
dotnet sln calculator-xunit-testing.slnx add calculator.tests.e2e/calculator.tests.e2e.csproj
# Add project reference to main calculator project
cd calculator.tests.e2e
dotnet add reference ../calculator/calculator.csproj
cd ..
Step 2: Install Playwright NuGet Package
cd calculator.tests.e2e
# Install Playwright for .NET
dotnet add package Microsoft.Playwright --version 1.40.0
# Install Playwright browser binaries
pwsh bin/Debug/net8.0/playwright.ps1 install
# Return to solution root
cd ..
Step 3: Verify Installation
# Check Playwright installation
dotnet build calculator.tests.e2e/calculator.tests.e2e.csproj
# Verify binaries installed
ls ~/.cache/ms-playwright* # Linux/macOS
dir $env:USERPROFILE\.cache\ms-playwright* # Windows
E2E Test Scenarios (Calculator Console)
Test Project Structure
calculator.tests.e2e/
├── calculator.tests.e2e.csproj # Project file with Playwright dependencies
├── CalculatorConsoleE2ETests.cs # Main console interaction tests
├── CalculatorWorkflowTests.cs # Multi-step workflow tests
├── CalculatorErrorHandlingTests.cs # Error scenario tests
├── Fixtures/
│ └── CalculatorFixture.cs # Test setup/teardown
└── Helpers/
└── ProcessHelper.cs # Process execution utilities
Phase 1: Console Application Process Testing
Console Process Fixture
using System.Diagnostics;
using Xunit;
public class CalculatorConsoleFixture : IAsyncLifetime
{
private Process? _process;
private StreamReader? _outputReader;
private StreamWriter? _inputWriter;
private const string CalculatorExePath =
"./calculator/bin/Release/net8.0/calculator.exe";
/// <summary>
/// Initialize and start calculator console process
/// </summary>
public async Task InitializeAsync()
{
if (!File.Exists(CalculatorExePath))
{
throw new FileNotFoundException($"Calculator executable not found: {CalculatorExePath}");
}
var psi = new ProcessStartInfo
{
FileName = CalculatorExePath,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
_process = new Process { StartInfo = psi };
_process.Start();
_inputWriter = _process.StandardInput;
_outputReader = _process.StandardOutput;
// Give process time to start
await Task.Delay(500);
}
/// <summary>
/// Send input to calculator and read output
/// </summary>
public async Task<string> SendInputAsync(string input, int timeoutMs = 2000)
{
if (_inputWriter == null || _outputReader == null)
throw new InvalidOperationException("Calculator process not initialized");
await _inputWriter.WriteLineAsync(input);
await _inputWriter.FlushAsync();
// Read response with timeout
var cts = new CancellationTokenSource(timeoutMs);
var output = new StringBuilder();
try
{
while (!cts.Token.IsCancellationRequested)
{
var line = await _outputReader.ReadLineAsync();
if (line == null) break;
output.AppendLine(line);
// Stop if we've received a prompt or result
if (line.Contains("Enter") || line.Contains("Result") || line.Contains("Error"))
break;
}
}
catch (OperationCanceledException)
{
// Timeout is acceptable
}
return output.ToString();
}
/// <summary>
/// Cleanup: terminate calculator process
/// </summary>
public async Task DisposeAsync()
{
if (_process != null && !_process.HasExited)
{
_inputWriter?.WriteLine("exit");
_inputWriter?.Flush();
if (!_process.WaitForExit(2000))
{
_process.Kill();
}
}
_inputWriter?.Dispose();
_outputReader?.Dispose();
_process?.Dispose();
await Task.CompletedTask;
}
}
Phase 2: Basic Interaction Tests
Test: Single Calculation
public class CalculatorConsoleE2ETests : IAsyncLifetime
{
private CalculatorConsoleFixture? _fixture;
public async Task InitializeAsync()
{
_fixture = new CalculatorConsoleFixture();
await _fixture.InitializeAsync();
}
public async Task DisposeAsync()
{
if (_fixture != null)
await _fixture.DisposeAsync();
}
[Fact]
public async Task E2E_Addition_UserEntersValidInputs_CalculatesCorrectly()
{
// Arrange
var expectedOutput = "8";
// Act - Simulate user input: "5", "+", "3"
var output1 = await _fixture!.SendInputAsync("5"); // First operand
var output2 = await _fixture.SendInputAsync("+"); // Operator
var output3 = await _fixture.SendInputAsync("3"); // Second operand
var result = output1 + output2 + output3;
// Assert
Assert.Contains(expectedOutput, result);
Assert.DoesNotContain("Error", result, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task E2E_DivisionByZero_UserEntersZeroDivisor_DisplaysError()
{
// Act
await _fixture!.SendInputAsync("5");
await _fixture.SendInputAsync("/");
var errorOutput = await _fixture.SendInputAsync("0");
// Assert
Assert.Contains("Error", errorOutput, StringComparison.OrdinalIgnoreCase);
Assert.Contains("zero", errorOutput, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task E2E_InvalidOperator_UserEntersInvalidOperator_DisplaysUsefulError()
{
// Act
await _fixture!.SendInputAsync("5");
await _fixture.SendInputAsync("&");
var errorOutput = await _fixture.SendInputAsync("3");
// Assert
Assert.Contains("invalid", errorOutput, StringComparison.OrdinalIgnoreCase);
}
}
Phase 3: Multi-Step Workflow Testing
Test: Sequential Calculations
public class CalculatorWorkflowTests : IAsyncLifetime
{
private CalculatorConsoleFixture? _fixture;
public async Task InitializeAsync()
{
_fixture = new CalculatorConsoleFixture();
await _fixture.InitializeAsync();
}
public async Task DisposeAsync()
{
if (_fixture != null)
await _fixture.DisposeAsync();
}
[Fact]
public async Task E2E_MultipleCalculations_UserPerforms3Calculations_AllSucceed()
{
// Workflow: (5+3), (10*2), (100/5)
var outputs = new List<string>();
// Calculation 1: 5 + 3 = 8
outputs.Add(await _fixture!.SendInputAsync("5"));
outputs.Add(await _fixture.SendInputAsync("+"));
outputs.Add(await _fixture.SendInputAsync("3"));
// Calculation 2: 10 * 2 = 20
outputs.Add(await _fixture.SendInputAsync("10"));
outputs.Add(await _fixture.SendInputAsync("*"));
outputs.Add(await _fixture.SendInputAsync("2"));
// Calculation 3: 100 / 5 = 20
outputs.Add(await _fixture.SendInputAsync("100"));
outputs.Add(await _fixture.SendInputAsync("/"));
outputs.Add(await _fixture.SendInputAsync("5"));
var combinedOutput = string.Join("\n", outputs);
// Assert - All calculations succeeded without errors
Assert.DoesNotContain("Error", combinedOutput, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task E2E_ErrorRecovery_UserRetries_AfterInvalidInput_Succeeds()
{
// Try invalid input first
var error = await _fixture!.SendInputAsync("invalid");
Assert.Contains("Error", error, StringComparison.OrdinalIgnoreCase);
// Then valid calculation should succeed
var result1 = await _fixture.SendInputAsync("5");
var result2 = await _fixture.SendInputAsync("+");
var result3 = await _fixture.SendInputAsync("3");
var output = result1 + result2 + result3;
Assert.DoesNotContain("Error", output, StringComparison.OrdinalIgnoreCase);
}
}
Phase 4: Error Handling Tests
public class CalculatorErrorHandlingTests : IAsyncLifetime
{
private CalculatorConsoleFixture? _fixture;
public async Task InitializeAsync()
{
_fixture = new CalculatorConsoleFixture();
await _fixture.InitializeAsync();
}
public async Task DisposeAsync()
{
if (_fixture != null)
await _fixture.DisposeAsync();
}
[Theory]
[InlineData("&", "5")] // Invalid operators
[InlineData("|", "3")]
[InlineData("@", "2")]
public async Task E2E_InvalidOperators_AllRejected_WithClearMessage(string op, string operand)
{
// Act
await _fixture!.SendInputAsync("5");
var error = await _fixture.SendInputAsync(op);
// Assert
Assert.Contains("invalid", error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task E2E_ModuloByZero_DisplaysError()
{
// Act
await _fixture!.SendInputAsync("10");
await _fixture.SendInputAsync("%");
var error = await _fixture.SendInputAsync("0");
// Assert
Assert.Contains("Error", error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task E2E_LargeNumbers_CalculatedCorrectly()
{
// Act
var large1 = "999999999";
var large2 = "999999999";
await _fixture!.SendInputAsync(large1);
await _fixture.SendInputAsync("+");
var result = await _fixture.SendInputAsync(large2);
// Assert
Assert.DoesNotContain("Error", result);
}
}
Execution Commands
Run All E2E Tests
# Run all E2E tests with verbose output
dotnet test calculator.tests.e2e/calculator.tests.e2e.csproj --configuration Release --verbosity normal
# Run with detailed test names
dotnet test calculator.tests.e2e/calculator.tests.e2e.csproj --configuration Release --logger "console;verbosity=detailed"
# Generate coverage report
dotnet test calculator.tests.e2e/calculator.tests.e2e.csproj --configuration Release --collect:"XPlat Code Coverage"
Run Specific E2E Test
# Run single test class
dotnet test calculator.tests.e2e/calculator.tests.e2e.csproj --filter "CalculatorConsoleE2ETests"
# Run single test method
dotnet test calculator.tests.e2e/calculator.tests.e2e.csproj --filter "E2E_Addition_UserEntersValidInputs_CalculatesCorrectly"
Run in Watch Mode (Development)
# Watch mode with auto-rerun on file changes
dotnet watch test calculator.tests.e2e/calculator.tests.e2e.csproj
Future: Browser-Based E2E Testing
When Angular UI is implemented, enhance Playwright tests for web interface:
// Future: Web UI E2E Tests (Planned for Story #81)
[PlaywrightTest]
public class CalculatorWebUIE2ETests : PageTest
{
[Test]
public async Task E2E_WebUI_Addition_DisplaysCorrectResult()
{
await Page.GotoAsync("https://localhost:4200/calculator");
// Click operand 1 input
await Page.FillAsync("input#operand1", "5");
// Select operator
await Page.SelectOptionAsync("select#operator", "+");
// Click operand 2 input
await Page.FillAsync("input#operand2", "3");
// Click calculate button
await Page.ClickAsync("button#calculate");
// Assert result displayed
var result = await Page.TextContentAsync("#result");
Assert.AreEqual("8", result);
}
[Test]
public async Task E2E_WebUI_MultiStep_Workflow()
{
// Test complex user journey through web UI
// - Open calculator
// - Perform 5+ calculations
// - Verify all results correct
// - Test error scenarios
// - Verify UI state management
}
}
CI/CD Integration
Azure Pipelines Configuration
Add to azure-pipelines/01-basic-triggers-pipeline.yml:
- job: E2ETests
displayName: "End-to-End Tests"
dependsOn: UnitTests
condition: succeeded()
steps:
- task: UseDotNet@2
inputs:
version: "8.0.x"
- task: PowerShell@2
displayName: "Install Playwright Browsers"
inputs:
targetType: "inline"
script: |
cd src/workspace/calculator-xunit-testing/calculator.tests.e2e
dotnet build
pwsh bin/Debug/net8.0/playwright.ps1 install
- task: DotNetCoreCLI@2
displayName: "Run E2E Tests"
inputs:
command: "test"
arguments: 'src/workspace/calculator-xunit-testing/calculator.tests.e2e/calculator.tests.e2e.csproj --configuration Release --logger "trx;LogFileName=e2e-results.trx"'
- task: PublishTestResults@2
displayName: "Publish E2E Test Results"
inputs:
testResultsFormat: "VSTest"
testResultsFiles: "**/*e2e-results.trx"
mergeTestResults: true
Success Criteria
✅ E2E Testing Requirements:
| Criterion | Status | Target |
|---|---|---|
| Console process launches successfully | ✅ | Instant startup |
| All basic operations work end-to-end | ✅ | 100% pass rate |
| Error scenarios handled gracefully | ✅ | No crashes |
| Multi-step workflows succeed | ✅ | Sequential operations work |
| Process cleanup proper | ✅ | No orphaned processes |
| CI/CD integration ready | 🔄 | Pipeline ready |
| Browser testing implemented | 📅 | Post-Story #81 |