using Microsoft.Playwright;
using Tests.E2e.Infra;
using Tests.E2e.PageObjectModels;
using Xunit.Abstractions;
namespace Tests.E2e;
///
/// Coverage for /tags/{TagName} and /labels/{LabelName} — the pages that
/// aggregate resources across kinds. The tag and label editors on the cards
/// were already covered; where those links lead was not.
/// Both pages read the whole config and tests share a container, so every
/// test uses a unique tag or label key to stay deterministic.
///
public class TagAndLabelPageTests(
PlaywrightFixture fixture,
ITestOutputHelper output) : E2ETestBase(fixture, output) {
private readonly PlaywrightFixture _fixture = fixture;
private readonly ITestOutputHelper _output = output;
private static string Unique(string prefix) => $"{prefix}{Guid.NewGuid():N}"[..14];
// =============================================================
// Tag page — aggregation across kinds
// =============================================================
[Fact]
public async Task Tag_Page_Lists_Resources_Of_Every_Kind_Carrying_The_Tag() {
(IBrowserContext context, IPage page) = await CreatePageAsync();
var tag = Unique("e2etag");
var serverName = Unique("e2e-tsv-");
var systemName = Unique("e2e-tsy-");
try {
await CreateServerWithTagAsync(page, serverName, tag);
await CreateSystemWithTagAsync(page, systemName, tag);
var tagPage = new TagPagePom(page);
await tagPage.GotoAsync(_fixture.BaseUrl, tag);
await tagPage.AssertTitleContainsAsync(tag);
// A hardware resource and a system, aggregated onto one page.
await tagPage.AssertListsAsync(serverName);
await tagPage.AssertListsAsync(systemName);
}
catch (Exception) {
await DumpAsync(page);
throw;
}
finally {
await context.CloseAsync();
}
}
// =============================================================
// Tag page — click-through from a card, and back out again
// =============================================================
[Fact]
public async Task User_Can_Click_A_Card_Tag_Through_To_The_Tag_Page_And_Back() {
(IBrowserContext context, IPage page) = await CreatePageAsync();
var tag = Unique("e2enav");
var serverName = Unique("e2e-tnv-");
try {
await CreateServerWithTagAsync(page, serverName, tag);
// Follow the tag chip on the card.
var card = new ServerCardPom(page);
await card.Tags.NavigateToTagAsync("server", tag);
var tagPage = new TagPagePom(page);
await tagPage.AssertLoadedAsync();
await tagPage.AssertListsAsync(serverName);
// And back out to the resource it points at.
await tagPage.OpenResourceAsync(serverName);
await card.AssertVisibleAsync(serverName);
}
catch (Exception) {
await DumpAsync(page);
throw;
}
finally {
await context.CloseAsync();
}
}
[Fact]
public async Task Tag_Page_Reports_An_Unused_Tag_As_Empty() {
(IBrowserContext context, IPage page) = await CreatePageAsync();
try {
var tagPage = new TagPagePom(page);
await tagPage.GotoAsync(_fixture.BaseUrl, Unique("e2enone"));
await tagPage.AssertEmptyAsync();
}
catch (Exception) {
await DumpAsync(page);
throw;
}
finally {
await context.CloseAsync();
}
}
// =============================================================
// Label page — values and detected types
// =============================================================
[Fact]
public async Task Label_Page_Shows_Each_Resource_Value_And_Its_Detected_Type() {
(IBrowserContext context, IPage page) = await CreatePageAsync();
var labelKey = Unique("e2elbl");
var textServer = Unique("e2e-ltx-");
var numberServer = Unique("e2e-lnm-");
try {
await CreateServerWithLabelAsync(page, textServer, labelKey, "production");
await CreateServerWithLabelAsync(page, numberServer, labelKey, "42");
var labelPage = new LabelPagePom(page);
await labelPage.GotoAsync(_fixture.BaseUrl, labelKey);
await labelPage.AssertTitleContainsAsync(labelKey);
// The page classifies each value, not just displays it.
await labelPage.AssertValueAsync(textServer, "production", "TEXT");
await labelPage.AssertValueAsync(numberServer, "42", "NUMBER");
}
catch (Exception) {
await DumpAsync(page);
throw;
}
finally {
await context.CloseAsync();
}
}
// =============================================================
// Label page — filtering
// =============================================================
[Fact]
public async Task Label_Page_Filter_Narrows_The_List_And_Can_Match_Nothing() {
(IBrowserContext context, IPage page) = await CreatePageAsync();
var labelKey = Unique("e2efil");
var keptServer = Unique("e2e-lkp-");
var filteredServer = Unique("e2e-lfl-");
try {
await CreateServerWithLabelAsync(page, keptServer, labelKey, "alpha");
await CreateServerWithLabelAsync(page, filteredServer, labelKey, "beta");
var labelPage = new LabelPagePom(page);
await labelPage.GotoAsync(_fixture.BaseUrl, labelKey);
await labelPage.AssertListsAsync(keptServer);
await labelPage.AssertListsAsync(filteredServer);
// The filter matches on the label value as well as the name.
await labelPage.FilterAsync("alpha");
await labelPage.AssertListsAsync(keptServer);
await labelPage.AssertDoesNotListAsync(filteredServer);
// Nothing matching falls through to its own state, not an empty list.
await labelPage.FilterAsync(Unique("nomatch"));
await labelPage.AssertNoResultsAsync();
}
catch (Exception) {
await DumpAsync(page);
throw;
}
finally {
await context.CloseAsync();
}
}
// =============================================================
// Label page — sort direction
// =============================================================
[Fact]
public async Task Label_Page_Sort_Direction_Reverses_The_List() {
(IBrowserContext context, IPage page) = await CreatePageAsync();
var labelKey = Unique("e2esrt");
var lowServer = Unique("e2e-slo-");
var highServer = Unique("e2e-shi-");
try {
// Numeric values so the default "Value" sort is unambiguous.
await CreateServerWithLabelAsync(page, lowServer, labelKey, "1");
await CreateServerWithLabelAsync(page, highServer, labelKey, "2");
var labelPage = new LabelPagePom(page);
await labelPage.GotoAsync(_fixture.BaseUrl, labelKey);
IReadOnlyList ascending = await labelPage.ListedOrderAsync();
Assert.Equal(new[] { lowServer, highServer }, ascending);
await labelPage.ToggleSortDirectionAsync();
IReadOnlyList descending = await labelPage.ListedOrderAsync();
Assert.Equal(new[] { highServer, lowServer }, descending);
}
catch (Exception) {
await DumpAsync(page);
throw;
}
finally {
await context.CloseAsync();
}
}
// =============================================================
// Helpers
// =============================================================
private async Task CreateServerAsync(IPage page, string name) {
await page.GotoAsync($"{_fixture.BaseUrl}/servers/list");
var list = new ServersListPom(page);
await list.AssertLoadedAsync();
await list.AddServerAsync(name);
if (!page.Url.Contains($"/resources/hardware/{name}", StringComparison.OrdinalIgnoreCase))
await list.OpenServerAsync(name);
var card = new ServerCardPom(page);
await card.AssertVisibleAsync(name);
}
private async Task CreateServerWithTagAsync(IPage page, string name, string tag) {
await CreateServerAsync(page, name);
var card = new ServerCardPom(page);
await card.Tags.AddTagsAsync("server", tag);
await card.Tags.AssertTagVisibleAsync("server", tag);
}
private async Task CreateServerWithLabelAsync(IPage page, string name, string key, string value) {
await CreateServerAsync(page, name);
var card = new ServerCardPom(page);
await card.Labels.AddLabelAsync("server", key, value);
await card.Labels.AssertLabelVisibleAsync("server", key);
}
private async Task CreateSystemWithTagAsync(IPage page, string name, string tag) {
await page.GotoAsync($"{_fixture.BaseUrl}/systems/list");
var list = new SystemsListPom(page);
await list.AssertLoadedAsync();
await list.AddSystemAsync(name);
if (!page.Url.Contains($"/resources/systems/{name}", StringComparison.OrdinalIgnoreCase))
await list.OpenSystemAsync(name);
var card = new SystemCardPom(page);
await card.AssertVisibleAsync(name);
await card.Tags.AddTagsAsync("system", tag);
await card.Tags.AssertTagVisibleAsync("system", tag);
}
private async Task DumpAsync(IPage page) {
_output.WriteLine("TEST FAILED — Capturing diagnostics");
_output.WriteLine($"Current URL: {page.Url}");
var html = await page.ContentAsync();
_output.WriteLine("==== DOM SNAPSHOT START ====");
_output.WriteLine(html);
_output.WriteLine("==== DOM SNAPSHOT END ====");
}
}