diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportExportTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportExportTests.cs new file mode 100644 index 00000000..081ce804 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Transport/TransportExportTests.cs @@ -0,0 +1,112 @@ +using Microsoft.Playwright; +using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Transport; + +/// +/// Happy-path end-to-end for the Transport Export wizard (Component #24, Task T21). +/// +/// +/// Drives the full four-step wizard at /design/transport/export against the +/// running dev cluster, exporting a single throwaway template the test creates via +/// the CLI: +/// +/// +/// Step 1 — Select : filter to the zztest template and tick its checkbox. +/// Step 2 — Review : confirm the resolved closure (Next). +/// Step 3 — Encrypt: supply a matching passphrase (≥ 8 chars) and Export. +/// Step 4 — Download: assert the success summary. +/// +/// +/// +/// Step 1's template list is a TemplateFolderTree in checkbox mode. The inner +/// TreeView renders an <input type="checkbox" class="tv-checkbox"> +/// per node and the name in a sibling span.tv-label; there is no +/// <label for> association, and in checkbox mode clicking the label text +/// does NOT toggle the box (content-click only selects in Single mode). So the test +/// clicks the checkbox input itself, scoped to the row carrying the template's +/// label text. +/// +/// +/// +/// Step 4's download is a JS-interop blob stream (NOT a DOM <a download>), +/// so the test asserts the success DOM ([data-testid='download-summary']) rather +/// than waiting for a Playwright download event — which would hang, since no +/// browser-level download fires. +/// +/// +[Collection("Playwright")] +public sealed class TransportExportTests +{ + private readonly PlaywrightFixture _fixture; + + public TransportExportTests(PlaywrightFixture fixture) + { + _fixture = fixture; + } + + [SkippableFact] + public async Task ExportTemplate_ReachesDownloadSummary() + { + Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason); + + var tmplName = CliRunner.UniqueName("exptmpl"); + var tmplId = await CliRunner.CreateTemplateAsync(tmplName); + await CliRunner.AddAttributeAsync(tmplId, "Value", "Double"); + try + { + var page = await _fixture.NewAuthenticatedPageAsync("multi-role", "password"); + await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/export"); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + // ── STEP 1: Select ──────────────────────────────────────────────────────── + // Narrow the tree to just the zztest template, then tick its checkbox. + await page.Locator("#export-filter").FillAsync(tmplName); + + // The template node is a TreeView leaf row: a checkbox input plus a + // span.tv-label carrying the name. Clicking the label text is a no-op in + // checkbox mode, so we click the input directly, scoped to the row whose + // label matches the unique zztest name. + var templateCheckbox = page + .Locator("[data-testid='group-templates'] li") + .Filter(new() { Has = page.Locator($"span.tv-label:text-is('{tmplName}')") }) + .Locator("input.tv-checkbox") + .First; + await Assertions.Expect(templateCheckbox).ToBeVisibleAsync(new() { Timeout = 10_000 }); + await templateCheckbox.CheckAsync(); + + // Next enables only once a selection exists; clicking it resolves the closure. + var step1Next = page.Locator("button.btn.btn-primary:has-text('Next')"); + await Assertions.Expect(step1Next).ToBeEnabledAsync(new() { Timeout = 10_000 }); + await step1Next.ClickAsync(); + + // ── STEP 2: Review ──────────────────────────────────────────────────────── + // The seed group lists the picked template; Next is always enabled here. + await Assertions.Expect(page.Locator("[data-testid='seed-group']")) + .ToBeVisibleAsync(new() { Timeout = 15_000 }); + await page.Locator("button.btn.btn-primary:has-text('Next')").ClickAsync(); + + // ── STEP 3: Encrypt ─────────────────────────────────────────────────────── + const string passphrase = "zztest-passphrase-123"; + await Assertions.Expect(page.Locator("#passphrase")) + .ToBeVisibleAsync(new() { Timeout = 10_000 }); + await page.Locator("#passphrase").FillAsync(passphrase); + await page.Locator("#passphrase-confirm").FillAsync(passphrase); + + // Export enables only when the two passphrases match AND length ≥ 8. + var exportBtn = page.Locator("button.btn.btn-primary:has-text('Export')"); + await Assertions.Expect(exportBtn).ToBeEnabledAsync(new() { Timeout = 10_000 }); + await exportBtn.ClickAsync(); + + // ── STEP 4: Download ────────────────────────────────────────────────────── + // JS-interop blob download — assert the success DOM, never WaitForDownload. + var summary = page.Locator("[data-testid='download-summary']"); + await Assertions.Expect(summary).ToBeVisibleAsync(new() { Timeout = 20_000 }); + await Assertions.Expect(summary).ToContainTextAsync("Bundle ready"); + } + finally + { + await CliRunner.DeleteTemplateAsync(tmplId); + } + } +}