test(playwright): Sites duplicate-identifier + cancel-from-edit edge cases (Wave 4)
This commit is contained in:
@@ -195,6 +195,127 @@ public class SiteCrudTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creating a site whose Identifier collides with the always-present seeded
|
||||
/// <c>site-a</c> must fail at save and persist nothing. SiteForm.SaveSite()
|
||||
/// catches the duplicate DbUpdateException → <c>_formError = "Save failed: …"</c>
|
||||
/// (a raw EF message) and stays on /create. The Name is deliberately distinct so
|
||||
/// only the SiteIdentifier unique index can trip (not the Name unique index).
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Create_DuplicateIdentifier_ShowsSaveFailedError()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
// Confirm the collision target exists (site-a is a core cluster seed; throws if absent).
|
||||
await CliRunner.ResolveSiteIdAsync("site-a");
|
||||
|
||||
// Distinct name so the failure can only be the SiteIdentifier unique index.
|
||||
var distinctName = $"zztest-dup-{Guid.NewGuid():N}"[..16];
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/sites/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// Identifier collides with the seeded site-a; Name is distinct. Node addresses blank.
|
||||
await page.Locator("label:has-text('Identifier') + input.form-control.form-control-sm").FillAsync("site-a");
|
||||
await page.Locator("label:has-text('Name') + input.form-control.form-control-sm").FillAsync(distinctName);
|
||||
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
|
||||
// The inline error surface must report the failed save and we must stay on /create.
|
||||
// The full message is a raw DbUpdateException — assert only the "Save failed" prefix.
|
||||
await Expect(page.Locator("div.text-danger.small.mt-2")).ToContainTextAsync("Save failed");
|
||||
await Assertions.Expect(page)
|
||||
.ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/admin/sites/create"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Defensive: the failed create persists nothing, but if a stray site by the
|
||||
// distinct name somehow exists, best-effort delete it (swallow not-found).
|
||||
try
|
||||
{
|
||||
await CliRunner.DeleteSiteAsync(await CliRunner.ResolveSiteIdAsync(distinctName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Nothing persisted (expected) or cluster unreachable — ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Editing a site and clicking Cancel must discard the change. SiteForm's Cancel
|
||||
/// (like Back) calls GoBack() → /admin/sites with NO dirty-check and NO persistence,
|
||||
/// so re-opening the edit must show the ORIGINAL (empty) Description.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task EditCancel_DiscardsChanges()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
var hex = Guid.NewGuid().ToString("N")[..8];
|
||||
var ident = $"zztest-{hex}";
|
||||
var name = $"zztest-site-{hex}";
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// ── CREATE (mirror the round-trip test's create steps) ──────────────────────
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/admin/sites/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await page.Locator("label:has-text('Identifier') + input.form-control.form-control-sm").FillAsync(ident);
|
||||
await page.Locator("label:has-text('Name') + input.form-control.form-control-sm").FillAsync(name);
|
||||
|
||||
await page.ClickAsync("button.btn.btn-success.btn-sm:has-text('Save')");
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// ── EDIT → change Description → CANCEL ──────────────────────────────────────
|
||||
var card = page.Locator("div.card", new() { HasText = name });
|
||||
await Assertions.Expect(card).ToBeVisibleAsync();
|
||||
await card.Locator("button.btn.btn-outline-primary.btn-sm:has-text('Edit')").ClickAsync();
|
||||
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/edit", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await page.Locator("label:has-text('Description') + input.form-control.form-control-sm")
|
||||
.FillAsync("zztest-CANCELLED-EDIT");
|
||||
|
||||
await page.ClickAsync("button:has-text('Cancel')");
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/admin/sites", excludePath: "/edit");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
// ── RE-OPEN EDIT → Description must be the ORIGINAL (empty) ──────────────────
|
||||
var cardAfterCancel = page.Locator("div.card", new() { HasText = name });
|
||||
await Assertions.Expect(cardAfterCancel).ToBeVisibleAsync();
|
||||
await cardAfterCancel.Locator("button.btn.btn-outline-primary.btn-sm:has-text('Edit')").ClickAsync();
|
||||
|
||||
await PlaywrightFixture.WaitForPathAsync(page, "/edit", excludePath: "/create");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
var descInput = page.Locator("label:has-text('Description') + input.form-control.form-control-sm");
|
||||
await Expect(descInput).ToHaveValueAsync("");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Best-effort teardown: delete the site via CLI.
|
||||
try
|
||||
{
|
||||
await CliRunner.DeleteSiteAsync(await CliRunner.ResolveSiteIdAsync(ident));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already gone or cluster unreachable — ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ILocatorAssertions Expect(ILocator locator) =>
|
||||
Assertions.Expect(locator);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user