# Playwright Integration Testing

Demonstrates browser-based integration testing using Playwright with Aspire's `DistributedApplicationTestingBuilder` to spin up a complete distributed application for end-to-end testing.

## 📋 Overview

This example shows how to:

- Set up Playwright for browser automation in .NET integration tests
- Use `DistributedApplicationTestingBuilder` to orchestrate a full-stack app during tests
- Test a React + Vite frontend communicating with a .NET API
- Generate TypeScript API clients using Kiota from TypeSpec definitions

## 🏗️ Architecture

```mermaid
flowchart LR
    subgraph TestBuilder["DistributedApplicationTestingBuilder"]
        Playwright["Playwright<br/>(Browser)"] --> Frontend["React + Vite<br/>Frontend<br/>(Kiota TS)"]
        Frontend --> API["Weather API<br/>(.NET 10)<br/>(OpenAPI)"]
    end
```

## 🧩 Components

| Project                             | Description                                        |
| ----------------------------------- | -------------------------------------------------- |
| `aspire-playwright.AppHost`         | Aspire orchestration for API and frontend          |
| `aspire-playwright.Api`             | .NET 10 Weather API with built-in OpenAPI          |
| `aspire-playwright.Frontend`        | React + Vite frontend with Kiota TypeScript client |
| `aspire-playwright.ServiceDefaults` | OpenTelemetry and health check configuration       |
| `aspire-playwright.Tests`           | Playwright integration tests                       |
| `aspire-playwright.TypeSpec`        | TypeSpec API definition generating OpenAPI         |

## 🔑 Key Features

- **Full-Stack Integration Tests**: Spin up complete distributed apps for browser testing
- **Playwright Browser Automation**: Real browser testing with Chromium
- **Aspire 13 `AddJavaScriptApp`**: Modern JavaScript/TypeScript app orchestration
- **.NET 10 OpenAPI**: Built-in `AddOpenApi()` and `MapOpenApi()` (no Swashbuckle)
- **Kiota TypeScript Client**: Type-safe API calls from React frontend
- **TypeSpec API Definition**: Contract-first API design

## 📂 Project Structure

```
foundry/dotnet/aspire-playwright/
├── aspire-playwright.sln
├── Directory.Build.props             # Shared build properties
├── Directory.Packages.props          # Central package management
├── aspire-playwright.AppHost/
│   ├── Program.cs                    # Aspire orchestration
│   └── Properties/launchSettings.json
├── aspire-playwright.Api/
│   └── Program.cs                    # Weather API with OpenAPI
├── aspire-playwright.Frontend/
│   ├── src/
│   │   ├── App.tsx                   # Main React component
│   │   └── client/                   # Kiota-generated TS client
│   ├── package.json
│   └── vite.config.ts
├── aspire-playwright.ServiceDefaults/
│   └── Extensions.cs                 # OpenTelemetry configuration
├── aspire-playwright.Tests/
│   └── WeatherAppTests.cs            # Playwright integration tests
└── aspire-playwright.TypeSpec/
    └── main.tsp                      # TypeSpec API definition
```

## 🚀 Getting Started

### Prerequisites

- .NET 10.0 SDK
- Node.js 22.x or 24.x
- Playwright browsers (auto-installed on first test run)

### Running the Application

```bash
cd foundry/dotnet/aspire-playwright
dotnet run --project aspire-playwright.AppHost
```

### Running the Tests

```bash
cd foundry/dotnet/aspire-playwright
dotnet test
```

On first run, Playwright will download the required browser (Chromium).

## 🧪 Test Examples

The test suite includes **10 integration tests**:

| Test                                           | Description                                     |
| ---------------------------------------------- | ----------------------------------------------- |
| `WeatherTable_Should_Display_5_Records`        | Verifies API returns exactly 5 weather records  |
| `RefreshButton_Should_Update_Weather_Data`     | Verifies refresh button fetches new random data |
| `WeatherTable_Should_Have_Correct_Columns`     | Validates table headers (Date, °C, °F, Summary) |
| `MultipleRefreshes_Should_Track_Refresh_Count` | Tests refresh counter increments correctly      |
| `Temperature_Conversion_Should_Be_Correct`     | Validates °F = 32 + (°C / 0.5556) formula       |
| `WeatherData_Should_Have_Valid_Summaries`      | Validates summaries from known set              |
| `WeatherData_Should_Have_Valid_Date_Format`    | Validates dates are parseable and in range      |
| `Page_Should_Have_Correct_Title_And_Heading`   | Verifies page title and h1 heading              |
| `LoadingState_Should_Display_While_Fetching`   | Tests loading indicator visibility              |
| `Temperature_Range_Should_Be_Valid`            | Validates temperatures within API range         |

### Test Pattern

```csharp
public class WeatherAppTests : IAsyncLifetime
{
    private DistributedApplication? _app;
    private IPlaywright? _playwright;
    private IBrowser? _browser;

    public async Task InitializeAsync()
    {
        // Start the distributed application
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.aspire_playwright_AppHost>();
        _app = await appHost.BuildAsync();
        await _app.StartAsync();

        // Initialize Playwright
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync();
    }

    [Fact]
    public async Task WeatherTable_Should_Display_5_Records()
    {
        var page = await _browser!.NewPageAsync();
        await page.GotoAsync(_frontendUrl!);

        var rows = await page.Locator("[data-testid='weather-table'] tbody tr")
            .CountAsync();

        Assert.Equal(5, rows);
    }
}
```

## 🔧 Key Implementation Details

### AppHost Configuration (Aspire 13)

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var weatherApi = builder.AddProject<Projects.aspire_playwright_Api>("weather-api");

builder.AddJavaScriptApp("frontend", "../aspire-playwright.Frontend", "dev")
    .WithEnvironment("VITE_API_URL", weatherApi.GetEndpoint("http"))
    .WithHttpEndpoint(env: "PORT")
    .WaitFor(weatherApi);

builder.Build().Run();
```

### .NET 10 OpenAPI (No Swashbuckle)

```csharp
builder.Services.AddOpenApi();

app.MapOpenApi();  // Serves at /openapi/v1.json
```

### Kiota TypeScript Client

```typescript
import { WeatherApiClient } from './generated/weatherApiClient';
import { FetchRequestAdapter } from '@microsoft/kiota-http-fetchlibrary';

const adapter = new FetchRequestAdapter(new AnonymousAuthenticationProvider());
adapter.baseUrl = import.meta.env.VITE_API_URL;

const client = new WeatherApiClient(adapter);
const forecasts = await client.weatherforecast.get();
```

## 📡 API Endpoints

| Endpoint           | Method | Description                    |
| ------------------ | ------ | ------------------------------ |
| `/weatherforecast` | GET    | Returns 5 random weather items |
| `/openapi/v1.json` | GET    | OpenAPI specification          |

## 🔗 Related Documentation

- [Aspire Documentation](../reference/tools/aspire.md)
- [Kiota Tool Documentation](../reference/tools/kiota.md)
- [TypeSpec Tool Documentation](../reference/tools/typespec.md)
- [Microsoft Playwright Docs](https://playwright.dev/dotnet/)

## 📍 Source Code

**Location:** [`foundry/dotnet/aspire-playwright/`](https://dev.azure.com/SAIFCorporation/Platform/_git/forge?path=/foundry/dotnet/aspire-playwright)
