Files
prefab-page-detail/Prefab.Tests/Playwright/ProductListingPlaywrightShould.cs
2025-10-27 17:39:18 -04:00

170 lines
6.8 KiB
C#

using Microsoft.Playwright;
using Microsoft.EntityFrameworkCore;
using Prefab.Catalog.Data;
using Prefab.Tests.Infrastructure;
using Shouldly;
namespace Prefab.Tests.EndToEnd;
[Collection("Prefab.Ephemeral")]
public sealed class ProductListingPlaywrightShould(PrefabCompositeFixture_Ephemeral fixture)
{
[Fact]
public async Task NavigateViaMegaMenuToLeafCategoryListing()
{
using var scope = fixture.CreateWebScope();
var seeder = scope.ServiceProvider.GetRequiredService<Seeder>();
await seeder.Execute(scope.ServiceProvider, TestContext.Current.CancellationToken);
var catalogDb = scope.ServiceProvider.GetRequiredService<IModuleDbReadOnly>();
var expectedSlugs = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"ceiling-supports",
"boxes-and-covers",
"prefab-assemblies",
"rod-strut-hardware"
};
var catalogReady = false;
var maxAttempts = 120; // ~60 seconds total with 500ms delay
for (var attempt = 1; attempt <= maxAttempts && !catalogReady; attempt++)
{
var slugs = await catalogDb.Categories
.Where(category => category.Slug != null && expectedSlugs.Contains(category.Slug))
.Select(category => category.Slug!)
.ToListAsync(TestContext.Current.CancellationToken);
if (slugs.Count == expectedSlugs.Count)
{
catalogReady = true;
break;
}
if (attempt % 20 == 0)
{
Console.WriteLine($"[Playwright] catalog readiness attempt {attempt}: found [{string.Join(", ", slugs)}]");
}
await Task.Delay(500, TestContext.Current.CancellationToken);
}
catalogReady.ShouldBeTrue("Catalog seed should populate root categories before UI navigation.");
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true });
var context = await browser.NewContextAsync(new BrowserNewContextOptions
{
BaseURL = fixture.PrefabBaseAddress.ToString().TrimEnd('/')
});
var page = await context.NewPageAsync();
await page.GotoAsync("/");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await page.WaitForFunctionAsync("window.Blazor && window.Blazor._internal && window.Blazor._internal.navigationManager");
var menuItem = page.Locator("li.main-nav__item--with--menu").First;
await menuItem.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 60_000
});
var interceptDebug = await menuItem.EvaluateAsync<string?>(
@"element => {
if (!element) { return null; }
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const stack = document.elementsFromPoint(x, y);
return stack.map(node => `${node.tagName.toLowerCase()}#${node.id}.${node.className}`).join(' > ');
}");
Console.WriteLine($"[Playwright] elementsFromPoint at nav center: {interceptDebug}");
await menuItem.HoverAsync();
var menuPanel = page.Locator("#productsMenuPanel");
await menuPanel.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Attached,
Timeout = 60_000
});
var boundingBox = await menuItem.BoundingBoxAsync();
if (boundingBox is null)
{
throw new InvalidOperationException("Navigation menu item bounding box could not be determined.");
}
var menuReady = false;
for (var attempt = 1; attempt <= 10 && !menuReady; attempt++)
{
var responseTask = page.WaitForResponseAsync(
resp => resp.Url.Contains("/bff/nav-menu/"),
new PageWaitForResponseOptions { Timeout = 5000 });
await page.Mouse.MoveAsync(
boundingBox.X + boundingBox.Width / 2,
boundingBox.Y + boundingBox.Height / 2);
await menuItem.DispatchEventAsync("mouseenter");
await page.WaitForTimeoutAsync(350);
IResponse? navResponse = null;
try
{
navResponse = await responseTask;
}
catch (TimeoutException)
{
// No navigation menu response observed within the window; proceed to inspect DOM.
}
menuReady = await page.EvaluateAsync<bool>(
"selector => document.querySelectorAll(selector).length > 0",
"#productsMenuPanel .megamenu__links--root li");
if (!menuReady)
{
var panelText = await menuPanel.InnerTextAsync();
Console.WriteLine($"[Playwright] nav menu attempt {attempt} panel text: {panelText}");
if (navResponse is not null)
{
var payload = await navResponse.TextAsync();
Console.WriteLine($"[Playwright] nav menu attempt {attempt} response: {payload}");
}
else
{
Console.WriteLine($"[Playwright] nav menu attempt {attempt} response: timed out waiting for /bff/nav-menu.");
}
await menuItem.DispatchEventAsync("mouseleave");
await page.WaitForTimeoutAsync(900);
}
}
menuReady.ShouldBeTrue("Mega menu should load category links within retry window.");
var ceilingLink = menuPanel
.GetByRole(AriaRole.Link, new() { Name = "Ceiling Supports" })
.First;
await Microsoft.Playwright.Assertions.Expect(ceilingLink)
.ToBeVisibleAsync(new LocatorAssertionsToBeVisibleOptions { Timeout = 60000 });
await Microsoft.Playwright.Assertions.Expect(menuPanel)
.ToHaveAttributeAsync("aria-hidden", "false");
await ceilingLink.ClickAsync();
await page.WaitForURLAsync("**/catalog/products?category-slug=ceiling-supports");
await page.WaitForSelectorAsync(".products-view .product-card");
var productNamesLocator = page.Locator(".product-card__name");
await Microsoft.Playwright.Assertions.Expect(productNamesLocator).ToHaveCountAsync(2);
var productNames = (await productNamesLocator.AllInnerTextsAsync())
.Select(name => name.Trim())
.ToList();
productNames.ShouldContain("Ceiling T-Bar Box Assembly");
productNames.ShouldContain("Fan Hanger Assembly");
await context.CloseAsync();
}
}