// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Net;
using Aspire.Templates.Tests;
using Aspire.TestUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Playwright;
using SamplesIntegrationTests.Infrastructure;
using Xunit;
namespace Aspire.Playground.Tests;
[RequiresFeature(TestFeature.ContainerRuntime)]
public class BlazorWasmHostingTests(ITestOutputHelper testOutput)
{
[Fact]
public async Task HostedBlazorWasm_ServesAppAndWeatherApi()
{
await using var app = await CreateAppAsync(typeof(Projects.BlazorHosted_AppHost));
using var blazorClient = AppHostTests.CreateHttpClientWithResilience(app, "blazorapp");
var homeResponse = await blazorClient.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, homeResponse.StatusCode);
var homeContent = await homeResponse.Content.ReadAsStringAsync();
Assert.Contains("blazor.web.js", homeContent);
var configResponse = await blazorClient.GetAsync("/_blazor/_configuration");
Assert.Equal(HttpStatusCode.OK, configResponse.StatusCode);
var proxyApiResponse = await blazorClient.GetAsync("/_api/weatherapi/weatherforecast");
Assert.Equal(HttpStatusCode.OK, proxyApiResponse.StatusCode);
var proxyApiContent = await proxyApiResponse.Content.ReadAsStringAsync();
Assert.Contains("temperatureC", proxyApiContent, StringComparison.OrdinalIgnoreCase);
using var weatherApiClient = AppHostTests.CreateHttpClientWithResilience(app, "weatherapi");
var directApiResponse = await weatherApiClient.GetAsync("/weatherforecast");
Assert.Equal(HttpStatusCode.OK, directApiResponse.StatusCode);
app.EnsureNoErrorsLogged();
await app.StopAsync();
}
[Fact]
[RequiresFeature(TestFeature.Playwright)]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task HostedBlazorWasm_BrowserRendersWeatherAndSendsTelemetry()
{
await using var app = await CreateAppAsync(typeof(Projects.BlazorHosted_AppHost), enableDashboard: true);
var baseUrl = app.GetEndpoint("blazorapp").ToString().TrimEnd('/');
PlaywrightProvider.DetectAndSetInstalledPlaywrightDependenciesPath();
await using var browser = await PlaywrightProvider.CreateBrowserAsync();
await using var context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
// Track OTLP requests and their response status codes
var otlpRequests = new ConcurrentBag<(string Url, int StatusCode)>();
await context.RouteAsync("**/_otlp/v1/**", async route =>
{
var response = await route.FetchAsync();
otlpRequests.Add((route.Request.Url, response.Status));
testOutput.WriteLine($"[otlp-intercept] {route.Request.Method} {route.Request.Url} -> {response.Status}");
await route.FulfillAsync(new RouteFulfillOptions { Response = response });
});
var page = await context.NewPageAsync();
page.Console += (_, e) => testOutput.WriteLine($"[browser-console] {e.Text}");
page.PageError += (_, e) => testOutput.WriteLine($"[browser-error] {e}");
await page.GotoAsync(baseUrl, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
// Navigate to the weather page and verify data renders
await page.GotoAsync($"{baseUrl}/weather", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
var tableLocator = page.Locator("table");
await Assertions.Expect(tableLocator).ToBeVisibleAsync(new LocatorAssertionsToBeVisibleOptions { Timeout = 30_000 });
var rows = tableLocator.Locator("tbody tr");
await Assertions.Expect(rows).Not.ToHaveCountAsync(0);
// Wait up to 60s for all three OTLP signal types to arrive (metrics may take longest)
await WaitForOtlpSignalsAsync(page, otlpRequests);
Assert.Contains(otlpRequests, r => r.Url.Contains("/_otlp/v1/traces"));
Assert.Contains(otlpRequests, r => r.Url.Contains("/_otlp/v1/logs"));
Assert.Contains(otlpRequests, r => r.Url.Contains("/_otlp/v1/metrics"));
// Verify the OTLP proxy returned successful responses (not 503/401)
Assert.All(otlpRequests, r => Assert.True(r.StatusCode >= 200 && r.StatusCode < 300,
$"OTLP request to {r.Url} returned {r.StatusCode}"));
await page.CloseAsync();
await app.StopAsync();
}
[Fact]
public async Task StandaloneBlazorWasm_GatewayServesAppAndWeatherApi()
{
await using var app = await CreateAppAsync(typeof(Projects.BlazorStandalone_AppHost));
using var gatewayClient = AppHostTests.CreateHttpClientWithResilience(app, "gateway");
var homeResponse = await gatewayClient.GetAsync("/app/");
Assert.Equal(HttpStatusCode.OK, homeResponse.StatusCode);
var homeContent = await homeResponse.Content.ReadAsStringAsync();
Assert.Contains("blazor.webassembly.js", homeContent);
var configResponse = await gatewayClient.GetAsync("/app/_blazor/_configuration");
Assert.Equal(HttpStatusCode.OK, configResponse.StatusCode);
var apiResponse = await gatewayClient.GetAsync("/app/_api/weatherapi/weatherforecast");
Assert.Equal(HttpStatusCode.OK, apiResponse.StatusCode);
var apiContent = await apiResponse.Content.ReadAsStringAsync();
Assert.Contains("temperatureC", apiContent, StringComparison.OrdinalIgnoreCase);
using var weatherApiClient = AppHostTests.CreateHttpClientWithResilience(app, "weatherapi");
var directApiResponse = await weatherApiClient.GetAsync("/weatherforecast");
Assert.Equal(HttpStatusCode.OK, directApiResponse.StatusCode);
app.EnsureNoErrorsLogged();
await app.StopAsync();
}
[Fact]
[RequiresFeature(TestFeature.Playwright)]
[OuterloopTest("Resource-intensive Playwright browser test")]
public async Task StandaloneBlazorWasm_BrowserRendersWeatherAndSendsTelemetry()
{
await using var app = await CreateAppAsync(typeof(Projects.BlazorStandalone_AppHost), enableDashboard: true);
var baseUrl = app.GetEndpoint("gateway").ToString().TrimEnd('/');
PlaywrightProvider.DetectAndSetInstalledPlaywrightDependenciesPath();
await using var browser = await PlaywrightProvider.CreateBrowserAsync();
await using var context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
var otlpRequests = new ConcurrentBag<(string Url, int StatusCode)>();
await context.RouteAsync("**/_otlp/v1/**", async route =>
{
var response = await route.FetchAsync();
otlpRequests.Add((route.Request.Url, response.Status));
testOutput.WriteLine($"[otlp-intercept] {route.Request.Method} {route.Request.Url} -> {response.Status}");
await route.FulfillAsync(new RouteFulfillOptions { Response = response });
});
var page = await context.NewPageAsync();
page.Console += (_, e) => testOutput.WriteLine($"[browser-console] {e.Text}");
page.PageError += (_, e) => testOutput.WriteLine($"[browser-error] {e}");
await page.GotoAsync($"{baseUrl}/app/", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
await page.GotoAsync($"{baseUrl}/app/weather", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
var tableLocator = page.Locator("table");
await Assertions.Expect(tableLocator).ToBeVisibleAsync(new LocatorAssertionsToBeVisibleOptions { Timeout = 30_000 });
var rows = tableLocator.Locator("tbody tr");
await Assertions.Expect(rows).Not.ToHaveCountAsync(0);
// Wait up to 60s for all three OTLP signal types to arrive (metrics may take longest)
await WaitForOtlpSignalsAsync(page, otlpRequests);
Assert.Contains(otlpRequests, r => r.Url.Contains("/_otlp/v1/traces"));
Assert.Contains(otlpRequests, r => r.Url.Contains("/_otlp/v1/logs"));
Assert.Contains(otlpRequests, r => r.Url.Contains("/_otlp/v1/metrics"));
// Verify the OTLP proxy returned successful responses (not 503/401)
Assert.All(otlpRequests, r => Assert.True(r.StatusCode >= 200 && r.StatusCode < 300,
$"OTLP request to {r.Url} returned {r.StatusCode}"));
await page.CloseAsync();
await app.StopAsync();
}
private static async Task WaitForOtlpSignalsAsync(IPage page, ConcurrentBag<(string Url, int StatusCode)> otlpRequests)
{
var deadline = DateTime.UtcNow.AddSeconds(60);
while (DateTime.UtcNow < deadline)
{
var hasTraces = otlpRequests.Any(r => r.Url.Contains("/_otlp/v1/traces"));
var hasLogs = otlpRequests.Any(r => r.Url.Contains("/_otlp/v1/logs"));
var hasMetrics = otlpRequests.Any(r => r.Url.Contains("/_otlp/v1/metrics"));
if (hasTraces && hasLogs && hasMetrics)
{
return;
}
await page.WaitForTimeoutAsync(1_000);
}
}
private async Task<Aspire.Hosting.DistributedApplication> CreateAppAsync(Type appHostType, bool enableDashboard = false)
{
var builder = await DistributedApplicationTestingBuilder.CreateAsync(appHostType, [], (options, _) =>
{
if (enableDashboard)
{
options.DisableDashboard = false;
options.AllowUnsecuredTransport = true;
}
});
builder.WithRandomParameterValues();
builder.Services.AddLogging(logging =>
{
logging.AddFakeLogging();
logging.AddSimpleConsole(configure => configure.SingleLine = true);
logging.AddXunit(testOutput);
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddFilter("Aspire", LogLevel.Trace);
});
var app = await builder.BuildAsync();
await app.StartAsync();
await app.WaitForResources().WaitAsync(TimeSpan.FromMinutes(5));
return app;
}
}