test(playwright): Templates edit-attribute + delete-blocked-by-instance edge cases (Wave 4)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Playwright;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Deployment;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
|
||||
|
||||
@@ -9,13 +11,15 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
|
||||
/// running dev cluster.
|
||||
/// </summary>
|
||||
[Collection("Playwright")]
|
||||
public class TemplateCrudTests
|
||||
public class TemplateCrudTests : IClassFixture<DeploymentFixture>
|
||||
{
|
||||
private readonly PlaywrightFixture _fixture;
|
||||
private readonly DeploymentFixture _cluster;
|
||||
|
||||
public TemplateCrudTests(PlaywrightFixture fixture)
|
||||
public TemplateCrudTests(PlaywrightFixture fixture, DeploymentFixture cluster)
|
||||
{
|
||||
_fixture = fixture;
|
||||
_cluster = cluster;
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -212,4 +216,122 @@ public class TemplateCrudTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task EditAttribute_PersistsChange()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
// CLI-seed a template with a single Double "Val" attribute, then edit its
|
||||
// Value through the page-local Edit-Attribute modal and confirm it persists.
|
||||
var name = CliRunner.UniqueName("tmpl");
|
||||
var id = await CliRunner.CreateTemplateAsync(name);
|
||||
await CliRunner.AddAttributeAsync(id, "Val", "Double");
|
||||
|
||||
try
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/{id}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Open the Val row's actions dropdown, then click the "Edit…" item
|
||||
// (note the ellipsis char "…" — it is in the markup verbatim).
|
||||
await page.ClickAsync("button[aria-label=\"More actions for Val\"]");
|
||||
await page.ClickAsync("button.dropdown-item:has-text('Edit…')");
|
||||
|
||||
// The page-local attribute modal is .modal.show.d-block WITHOUT `fade`
|
||||
// (the DialogHost confirm modal HAS `fade`). :not(.fade) pins the
|
||||
// page-local one.
|
||||
var modal = page.Locator(".modal.show.d-block:not(.fade)");
|
||||
await Assertions.Expect(modal).ToBeVisibleAsync();
|
||||
await Assertions.Expect(modal.Locator("h6.modal-title")).ToHaveTextAsync("Edit Attribute");
|
||||
|
||||
// When editing, the Name input is rendered readonly (readonly="@editing").
|
||||
await Assertions.Expect(
|
||||
modal.Locator("div.col-12:has(label:has-text('Name')) input.form-control"))
|
||||
.ToHaveAttributeAsync("readonly", "");
|
||||
|
||||
// The Value input is label-anchored: its containing div carries the
|
||||
// "Value" label. ("Data Source Ref" does not contain "Value", so the
|
||||
// substring match resolves uniquely to the Value field.)
|
||||
var valueInput = modal.Locator("div.col-12:has(label:has-text('Value')) input.form-control");
|
||||
await valueInput.FillAsync("42.5");
|
||||
|
||||
// Footer button text is "Save" when editing (vs "Add" when adding).
|
||||
await modal.Locator(".modal-footer button.btn-success.btn-sm:has-text('Save')").ClickAsync();
|
||||
|
||||
// SaveAttribute persists then reloads, dismissing the modal.
|
||||
await Assertions.Expect(modal).ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
||||
|
||||
// The attribute table renders the value in a <td class="small">@effectiveValue</td>
|
||||
// cell (for a non-derived template effectiveValue == attr.Value), so the
|
||||
// persisted "42.5" must appear in the Val row's value cell. Web-first wait
|
||||
// covers the post-save reload.
|
||||
await Assertions.Expect(page.Locator("table td.small:has-text('42.5')"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
}
|
||||
finally
|
||||
{
|
||||
await CliRunner.DeleteTemplateAsync(id);
|
||||
// Defensive sweep by name in case the delete above was a no-op.
|
||||
try
|
||||
{
|
||||
foreach (var leftover in await CliRunner.ListTemplateIdsByNamePrefixAsync(name))
|
||||
{
|
||||
await CliRunner.DeleteTemplateAsync(leftover);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort — swallow to avoid masking the original test failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task DeleteTemplate_WithInstance_IsBlocked()
|
||||
{
|
||||
Skip.IfNot(_cluster.Available, ClusterAvailability.SkipReason);
|
||||
|
||||
// Mint a non-deployed instance referencing the fixture's template — that
|
||||
// reference is sufficient for TemplateDeletionService to block the delete
|
||||
// with the "instance(s) reference it" error.
|
||||
var (instId, _) = await _cluster.CreateInstanceAsync();
|
||||
try
|
||||
{
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/templates/{_cluster.TemplateId}");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Header Delete button (btn-outline-danger btn-sm).
|
||||
await page.ClickAsync("button.btn.btn-outline-danger.btn-sm:has-text('Delete')");
|
||||
|
||||
// Confirm via the global DialogHost modal's danger button (labelled
|
||||
// "Delete"). This modal HAS the `fade` class — distinct from the
|
||||
// page-local attribute modal.
|
||||
var confirmBtn = page.Locator(".modal-footer .btn-danger:has-text('Delete')");
|
||||
await Assertions.Expect(confirmBtn).ToBeVisibleAsync(new() { Timeout = 5_000 });
|
||||
await confirmBtn.ClickAsync();
|
||||
|
||||
// The delete fails: DeleteTemplate surfaces result.Error on a toast and
|
||||
// does NOT navigate. The error reads
|
||||
// "Cannot delete template '{name}': {n} instance(s) reference it (...)".
|
||||
await Assertions.Expect(page.Locator(".toast"))
|
||||
.ToContainTextAsync("instance(s) reference it", new() { Timeout = 10_000 });
|
||||
|
||||
// The template still exists — the page stayed on the detail URL because
|
||||
// the failed delete did not navigate away.
|
||||
await Assertions.Expect(page)
|
||||
.ToHaveURLAsync(new System.Text.RegularExpressions.Regex(
|
||||
$"/design/templates/{_cluster.TemplateId}$"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Remove this test's instance so the fixture template is instance-free
|
||||
// again; the fixture's DisposeAsync sweeps the template + any leftovers.
|
||||
await CliRunner.DeleteInstanceAsync(instId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user