170 lines
6.8 KiB
C#
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();
|
|
}
|
|
}
|