This commit is contained in:
2025-10-27 21:39:50 -04:00
parent 31f723bea4
commit 2fecd5b315
22 changed files with 1198 additions and 14 deletions

View File

@@ -0,0 +1,128 @@
using System.Net;
using Microsoft.AspNetCore.Components;
using System.Globalization;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Prefab.Web.Client.Models.Catalog;
using Prefab.Web.Client.Models.Shared;
using Prefab.Web.Client.Pages.Browse;
using Prefab.Web.Client.Services;
using Shouldly;
namespace Prefab.Tests.Web.Client.Pages;
[Trait(TraitName.Category, TraitCategory.Unit)]
public sealed class ProductPageTests : BunitContext
{
[Fact]
public void ProductPage_Renders_Product_Details_From_Service()
{
var model = CreateSampleProduct();
Services.AddScoped<IProductDisplayService>(_ =>
new FakeProductDisplayService(Result<ProductDisplayModel>.Success(model)));
var navigation = Services.GetRequiredService<NavigationManager>();
navigation.NavigateTo(navigation.GetUriWithQueryParameter("slug", model.Slug));
var component = Render<Product>();
component.Markup.ShouldContain(model.Name);
model.Description.ShouldNotBeNull();
component.Markup.ShouldContain(model.Description!);
component.FindAll("input[type=radio]").Count.ShouldBe(2);
component.Markup.ShouldContain("Quantity");
component.FindAll(".product__add-to-cart").Count.ShouldBe(1);
component.Markup.ShouldContain("Documents");
component.Markup.ShouldContain("Spec Sheet");
component.Markup.ShouldContain("SW-001");
component.Markup.ShouldContain("Lighting");
var expectedPrice = model.BasePrice!.Value.ToString("C", CultureInfo.CurrentCulture);
component.Markup.ShouldContain(expectedPrice);
}
[Fact]
public void ProductPage_Shows_Problem_When_NotFound()
{
Services.AddScoped<IProductDisplayService>(_ =>
new FakeProductDisplayService(Result<ProductDisplayModel>.Failure(new ResultProblem
{
Title = "Product not found.",
Detail = "Product 'unknown' was not found.",
StatusCode = (int)HttpStatusCode.NotFound
})));
var navigation = Services.GetRequiredService<NavigationManager>();
navigation.NavigateTo(navigation.GetUriWithQueryParameter("slug", "unknown"));
var component = Render<Product>();
component.Markup.ShouldContain("Product not found.");
component.FindAll(".alert-warning").Count.ShouldBe(1);
}
private static ProductDisplayModel CreateSampleProduct() => new()
{
Id = Guid.NewGuid(),
Slug = "sample-widget",
Name = "Sample Widget",
Description = "<p>Sample description.</p>",
BasePrice = 42.50m,
Sku = "SW-001",
Categories =
[
new ProductDisplayModel.CategoryRef(Guid.NewGuid(), "Lighting", "lighting")
],
Options =
[
new ProductDisplayModel.OptionDefinition(
Id: Guid.NewGuid(),
Code: "color",
Name: "Color",
DataType: 0,
IsVariantAxis: true,
Unit: null,
Min: null,
Max: null,
Step: null,
PricePerUnit: null,
Tiers: Array.Empty<ProductDisplayModel.Tier>(),
Values:
[
new ProductDisplayModel.OptionValue(Guid.NewGuid(), "red", "Red", 0m, 0),
new ProductDisplayModel.OptionValue(Guid.NewGuid(), "blue", "Blue", 0m, 0)
]),
new ProductDisplayModel.OptionDefinition(
Id: Guid.NewGuid(),
Code: "length",
Name: "Length",
DataType: 1,
IsVariantAxis: false,
Unit: "ft",
Min: 1,
Max: 10,
Step: 1,
PricePerUnit: null,
Tiers:
[
new ProductDisplayModel.Tier(1, 5, 1, null)
],
Values: Array.Empty<ProductDisplayModel.OptionValue>())
],
Specs =
[
new ProductDisplayModel.Spec("Weight", "25 lb", null, null)
],
GenericAttributes = new Dictionary<string, string>
{
["catalog.product.docs"] = """[{ "title": "Spec Sheet", "url": "https://example.com/spec.pdf" }]"""
}
};
private sealed class FakeProductDisplayService(Result<ProductDisplayModel> result) : IProductDisplayService
{
private readonly Result<ProductDisplayModel> _result = result;
public Task<Result<ProductDisplayModel>> Get(string slug, CancellationToken cancellationToken = default) =>
Task.FromResult(_result);
}
}