112 lines
5.6 KiB
C#
112 lines
5.6 KiB
C#
using Microsoft.Playwright;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Design;
|
|
|
|
[Collection("Playwright")]
|
|
public class ApiMethodFormTests
|
|
{
|
|
private readonly PlaywrightFixture _pw;
|
|
|
|
public ApiMethodFormTests(PlaywrightFixture pw)
|
|
{
|
|
_pw = pw;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Add-API-Method form's required-field gate fires when Name is present but the
|
|
/// script is empty. This deliberately exercises the validation WITHOUT touching the
|
|
/// Monaco editor (brittle to type into): fill only the Name, click Save, and expect the
|
|
/// inline "Name and script required." error. The form does not navigate, so nothing is
|
|
/// saved — this fixture name never reaches the database.
|
|
/// </summary>
|
|
[SkippableFact]
|
|
public async Task CreateForm_NameWithoutScript_ShowsInlineError()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var page = await _pw.NewAuthenticatedPageAsync();
|
|
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/api-methods/create");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
await Assertions.Expect(page.Locator("h4:has-text('Add API Method')")).ToBeVisibleAsync();
|
|
|
|
// The form has several text inputs (Name + SchemaBuilder/parameter rows that also
|
|
// carry .form-control), so anchor the Name input to its labelled mb-3 wrapper and
|
|
// require .form-control (the peer ExternalSystemCrudTests pattern). The count guard
|
|
// makes any future selector ambiguity fail loudly instead of filling the wrong field.
|
|
var nameInput = page.Locator("div.mb-3:has(label:has-text('Name')) input[type=text].form-control");
|
|
await Assertions.Expect(nameInput).ToHaveCountAsync(1);
|
|
await nameInput.FillAsync("zzapimethod");
|
|
|
|
// Leave the Monaco script empty and submit — the gate should reject it.
|
|
await page.Locator("button.btn-success:has-text('Save')").ClickAsync();
|
|
|
|
await Assertions.Expect(page.Locator("div.text-danger.small:has-text('Name and script required.')"))
|
|
.ToBeVisibleAsync();
|
|
|
|
// The form must not have navigated away.
|
|
await Assertions.Expect(page).ToHaveURLAsync(new System.Text.RegularExpressions.Regex("/design/api-methods/create$"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// CLI-create an inbound API method, confirm it renders on the External Systems page's
|
|
/// "Inbound API Methods" tab, then delete it via the card kebab and confirm dialog. The
|
|
/// create/edit form is Monaco-driven (brittle), so authoring happens via the CLI; the UI
|
|
/// delete round-trip is the behavior under test. A best-effort CLI delete in finally
|
|
/// guarantees no zztest-method-* row leaks if the UI path fails mid-way.
|
|
/// </summary>
|
|
[SkippableFact]
|
|
public async Task CliCreated_Method_VisibleAndDeletes()
|
|
{
|
|
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
|
|
|
var name = CliRunner.UniqueName("method");
|
|
int id = await CliRunner.CreateApiMethodAsync(name);
|
|
|
|
try
|
|
{
|
|
var page = await _pw.NewAuthenticatedPageAsync();
|
|
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/external-systems");
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
// Switch to the Inbound API Methods tab; only its panel renders, so the card
|
|
// locator below stays unambiguous against External System / DB Connection cards.
|
|
// The tab switch is a Blazor SignalR re-render (@if (_tab == "inbound")), so let
|
|
// the circuit settle before querying the freshly-rendered panel.
|
|
await page.Locator("button.nav-link:has-text('Inbound API Methods')").ClickAsync();
|
|
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
|
|
|
var card = page.Locator("div.card").Filter(new() { HasText = name });
|
|
await Assertions.Expect(card).ToHaveCountAsync(1, new() { Timeout = 10_000 });
|
|
await Assertions.Expect(card.Locator($"code:has-text('POST /api/{name}')")).ToBeVisibleAsync();
|
|
|
|
// Scope the kebab + delete item to the card's .dropdown for strict-mode safety.
|
|
var dropdown = card.Locator(".dropdown");
|
|
await dropdown.Locator("button[aria-label^='More actions']").ClickAsync();
|
|
await dropdown.Locator(".dropdown-menu button.dropdown-item.text-danger").ClickAsync();
|
|
|
|
// Confirm we are on the right dialog before clicking danger. The api-method delete
|
|
// uses the generic "Delete" title (ConfirmAsync("Delete", "Delete API method '{name}'?")).
|
|
await Assertions.Expect(page.Locator(".modal-title:has-text('Delete')")).ToBeVisibleAsync();
|
|
await page.Locator(".modal-footer .btn-danger").ClickAsync();
|
|
|
|
// Single web-first assertion on the success toast — toasts auto-dismiss, so we
|
|
// do NOT chase the toast body in a second sequential check.
|
|
await Assertions.Expect(page.Locator(".toast", new() { HasText = "Deleted." }))
|
|
.ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
|
|
|
// The card must be gone.
|
|
await Assertions.Expect(page.Locator("div.card").Filter(new() { HasText = name }))
|
|
.ToHaveCountAsync(0, new() { Timeout = 10_000 });
|
|
}
|
|
finally
|
|
{
|
|
// Best-effort teardown: the happy path deletes via the UI, so this finds
|
|
// nothing; it only fires if the UI path failed mid-way.
|
|
await CliRunner.DeleteApiMethodAsync(id);
|
|
}
|
|
}
|
|
}
|