test(e2e): cover Template create/add-attribute/delete round-trip
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using Microsoft.Playwright;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end round-trip for the Template design pages:
|
||||
/// create → add attribute → delete, all via the Central UI against the
|
||||
/// running dev cluster.
|
||||
/// </summary>
|
||||
[Collection("Playwright")]
|
||||
public class TemplateCrudTests
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
|
||||
public TemplateCrudTests(PlaywrightFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task CreateAddAttributeDelete_Template_RoundTrips()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
var name = $"zztest-tmpl-{Guid.NewGuid().ToString("N")[..8]}";
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// ── CREATE ────────────────────────────────────────────────────────────────
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Name input is label-anchored: <label class="form-label">Name</label>
|
||||
// followed by <input class="form-control" @bind="_createName" />.
|
||||
// Use the label's sibling input scoped to the containing div to avoid any
|
||||
// strict-mode violation from the Description input (also form-control).
|
||||
await page.Locator("div.mb-3:has(label:has-text('Name')) input.form-control").FillAsync(name);
|
||||
|
||||
// Leave Parent Template at the default "(None - root template)".
|
||||
// Click the green Create button.
|
||||
await page.ClickAsync("button.btn.btn-success:has-text('Create')");
|
||||
|
||||
// After a successful create, Blazor navigates to /design/templates/{id}.
|
||||
// Poll window.location until the path matches /design/templates/ + digits
|
||||
// and does not still say /create.
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/design/templates/", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Sanity: we must be on a numeric template detail URL.
|
||||
Assert.Matches(@"/design/templates/\d+$", page.Url);
|
||||
|
||||
// ── ADD ATTRIBUTE ─────────────────────────────────────────────────────────
|
||||
// The Attributes tab is the default-active tab (_activeTab = "attributes"),
|
||||
// so we don't need to click it, but we do wait for the tab panel to render.
|
||||
await Assertions.Expect(
|
||||
page.Locator("button.nav-link:has-text('Attributes')"))
|
||||
.ToBeVisibleAsync();
|
||||
|
||||
// Click Add Attribute.
|
||||
await page.ClickAsync("button.btn.btn-primary.btn-sm:has-text('Add Attribute')");
|
||||
|
||||
// The modal is a page-local .modal.show.d-block — NOT the global DialogHost.
|
||||
var modal = page.Locator(".modal.show.d-block");
|
||||
await Assertions.Expect(modal).ToBeVisibleAsync();
|
||||
await Assertions.Expect(modal.Locator(".modal-title")).ToHaveTextAsync("Add Attribute");
|
||||
|
||||
// Fill Name field inside the modal.
|
||||
await modal.Locator("div.col-12:has(label:has-text('Name')) input.form-control").FillAsync("Val");
|
||||
|
||||
// Select Data Type = Double.
|
||||
// The select is label-anchored: <label>Data Type</label> + <select class="form-select">.
|
||||
await modal.Locator("div.col-12:has(label:has-text('Data Type')) select.form-select").SelectOptionAsync("Double");
|
||||
|
||||
// Click the footer Add button (btn-success btn-sm, text "Add").
|
||||
await modal.Locator(".modal-footer button.btn-success.btn-sm:has-text('Add')").ClickAsync();
|
||||
|
||||
// The modal should dismiss and the attribute table should show "Val".
|
||||
await Assertions.Expect(modal).ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
||||
await Assertions.Expect(page.Locator("table td:has-text('Val')"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
|
||||
// ── DELETE ────────────────────────────────────────────────────────────────
|
||||
// Click the header Delete button (btn-outline-danger btn-sm, text "Delete").
|
||||
await page.ClickAsync("button.btn.btn-outline-danger.btn-sm:has-text('Delete')");
|
||||
|
||||
// The global DialogHost renders a confirm dialog.
|
||||
// The danger confirm button carries class "btn-danger" and text "Delete".
|
||||
var confirmBtn = page.Locator(".modal-footer .btn-danger:has-text('Delete')");
|
||||
await Assertions.Expect(confirmBtn).ToBeVisibleAsync(new() { Timeout = 5_000 });
|
||||
await confirmBtn.ClickAsync();
|
||||
|
||||
// After delete, Blazor navigates back to /design/templates (the list page).
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/design/templates", excludePath: $"/design/templates/{page.Url.Split('/').Last()}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// The template name must no longer appear in the tree view.
|
||||
await Assertions.Expect(
|
||||
page.Locator("span.tv-label", new() { HasText = name }))
|
||||
.ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Best-effort safety net: clean up any leftover zztest-tmpl-* templates
|
||||
// that were not deleted by the UI (e.g. test failed mid-flow).
|
||||
try
|
||||
{
|
||||
foreach (var id in await CliRunner.ListTemplateIdsByNamePrefixAsync(name))
|
||||
{
|
||||
await CliRunner.DeleteTemplateAsync(id);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort — swallow to avoid masking the original test failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user