test(playwright): M8 import Map step + per-line diff coverage (#230)
This commit is contained in:
+236
@@ -240,4 +240,240 @@ public sealed class TransportImportTests
|
||||
try { File.Delete(bundlePath); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M8 Map step (#230): drive an import whose bundle carries a <c>Site</c>, so the
|
||||
/// preview surfaces a <see cref="RequiredSiteMapping"/> and the wizard's Step-3
|
||||
/// <strong>Map</strong> sub-section (<c>[data-testid='map-section']</c>) renders.
|
||||
///
|
||||
/// <para>
|
||||
/// ARRANGE: create a throwaway site with a unique <c>SiteIdentifier</c>, export a
|
||||
/// site-scoped bundle, then DELETE the source site. With no same-identifier site
|
||||
/// in the destination the importer finds no auto-match, so the Map row defaults to
|
||||
/// "Create new" — the operator must engage the dropdown. The site itself diffs as
|
||||
/// <see cref="ConflictKind.New"/> (a static Add, no blocker), so Next is enabled
|
||||
/// throughout; the assertion value is that the Map UI renders and that picking an
|
||||
/// existing target (the live <c>site-a</c>) is accepted and the wizard advances to
|
||||
/// Confirm.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The test deliberately STOPS at the Confirm step and never clicks Apply: mapping
|
||||
/// the source site onto <c>site-a</c> would Overwrite the live site on apply, and
|
||||
/// the Map-step coverage doesn't require a mutation to be proven. Reaching Confirm
|
||||
/// with the mapping chosen is the evidence that resolving the reference unblocks the
|
||||
/// wizard.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task ImportSiteBundle_RendersMapStep_AndMappingEnablesNext()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
var env = CliRunner.UniqueName("env");
|
||||
// The SiteIdentifier doubles as the export selector and the source identifier
|
||||
// the Map row shows; keep it inside the zztest- namespace so a unique-prefix
|
||||
// teardown can find it even if the test fails before the in-line delete.
|
||||
var siteIdentifier = CliRunner.UniqueName("siteid");
|
||||
var siteName = CliRunner.UniqueName("site");
|
||||
var pass = "pw-" + env;
|
||||
var bundlePath = Path.Combine(Path.GetTempPath(), siteIdentifier + ".scadabundle");
|
||||
|
||||
// The live cluster's canonical site — the existing target the operator maps to.
|
||||
const string ExistingTargetIdentifier = "site-a";
|
||||
|
||||
try
|
||||
{
|
||||
// ── ARRANGE: create + export a site-scoped bundle, then drop the source ──
|
||||
await CliRunner.CreateSiteAsync(siteName, siteIdentifier);
|
||||
await CliRunner.BundleExportSiteAsync(bundlePath, siteIdentifier, pass, env);
|
||||
|
||||
// Delete the source site so the destination has no same-identifier match:
|
||||
// the Map row then has no auto-match and defaults to "Create new".
|
||||
foreach (var id in await CliRunner.ListSiteIdsByIdentifierPrefixAsync(siteIdentifier))
|
||||
{
|
||||
await CliRunner.DeleteSiteAsync(id);
|
||||
}
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
// ── STEP 1: Upload ────────────────────────────────────────────────────────
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/import");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await page.Locator("#bundle-input").SetInputFilesAsync(bundlePath);
|
||||
await Assertions.Expect(page.Locator("[data-testid='encrypted-bundle-notice']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await page.Locator("button.btn.btn-primary:has-text('Next')").ClickAsync();
|
||||
|
||||
// ── STEP 2: Passphrase ──────────────────────────────────────────────────────
|
||||
await Assertions.Expect(page.Locator("#import-passphrase"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await page.Locator("#import-passphrase").FillAsync(pass);
|
||||
await page.Locator("button.btn.btn-primary:has-text('Unlock')").ClickAsync();
|
||||
|
||||
// ── STEP 3: Diff + Map ───────────────────────────────────────────────────────
|
||||
await Assertions.Expect(page.Locator("#import-passphrase"))
|
||||
.ToBeHiddenAsync(new() { Timeout = 20_000 });
|
||||
|
||||
// The Map sub-section renders because the bundle references a source site.
|
||||
await Assertions.Expect(page.Locator("[data-testid='map-section']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await Assertions.Expect(page.Locator("[data-testid='map-sites-table']")).ToBeVisibleAsync();
|
||||
|
||||
// Exactly one source-site row, for the identifier the bundle carried.
|
||||
var siteRow = page.Locator("tr[data-testid='map-site-row']");
|
||||
await Assertions.Expect(siteRow).ToHaveCountAsync(1);
|
||||
await Assertions.Expect(siteRow).ToContainTextAsync(siteIdentifier);
|
||||
|
||||
// The per-site dropdown carries a "Create new" option plus every existing
|
||||
// destination site. Map the source onto the live site-a target.
|
||||
var siteSelect = page.Locator($"[data-testid='map-site-select-{siteIdentifier}']");
|
||||
await Assertions.Expect(siteSelect).ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await siteSelect.SelectOptionAsync(ExistingTargetIdentifier);
|
||||
await Assertions.Expect(siteSelect).ToHaveValueAsync(ExistingTargetIdentifier);
|
||||
|
||||
// The site item is a New (Add) row — no blocker — so Next stays enabled and
|
||||
// advances to Confirm. (We do NOT apply: see the method remarks.)
|
||||
var diffNext = page.Locator("button.btn.btn-primary:has-text('Next')");
|
||||
await Assertions.Expect(diffNext).ToBeEnabledAsync(new() { Timeout = 15_000 });
|
||||
await diffNext.ClickAsync();
|
||||
|
||||
// ── STEP 4: Confirm (reached, not submitted) ──────────────────────────────────
|
||||
await Assertions.Expect(page.Locator("#confirm-env"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Best-effort teardown: drop the source site (already deleted in the happy
|
||||
// path, but recover if the test failed before the in-line delete) and the
|
||||
// staged bundle. We never applied, so no destination entity was created.
|
||||
try
|
||||
{
|
||||
foreach (var id in await CliRunner.ListSiteIdsByIdentifierPrefixAsync(siteIdentifier))
|
||||
{
|
||||
await CliRunner.DeleteSiteAsync(id);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort — never mask the test's own failure.
|
||||
}
|
||||
|
||||
try { File.Delete(bundlePath); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M8 per-line diff (#230): drive an import where a bundled shared script's
|
||||
/// <c>Code</c> diverges from the destination's, so the preview classifies it
|
||||
/// <see cref="ConflictKind.Modified"/> and the row's "Field diff" carries a
|
||||
/// structured <c>lineDiff</c> — rendered by the <c>LineDiffView</c> component as a
|
||||
/// GitHub-style +/- list.
|
||||
///
|
||||
/// <para>
|
||||
/// ARRANGE: create a shared script with a known multi-line body, export it, then
|
||||
/// UPDATE the destination copy's code to a divergent body. On import the
|
||||
/// <c>ArtifactDiff.CompareSharedScript</c> Code comparison emits a
|
||||
/// <c>changes[].lineDiff</c> payload (Myers hunks with at least one <c>add</c> and
|
||||
/// one <c>remove</c> for the changed line), which the diff row surfaces inside a
|
||||
/// collapsed <c><details></c>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The test STOPS at the diff step (no Apply): the assertion target is the render
|
||||
/// of the +/- hunk rows, not the mutation. Expanding the <c><details></c> is
|
||||
/// required because the line-diff block is not laid out until the disclosure opens.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task ImportModifiedSharedScript_RendersPerLineDiff()
|
||||
{
|
||||
Skip.IfNot(await ClusterAvailability.IsAvailableAsync(), ClusterAvailability.SkipReason);
|
||||
|
||||
var env = CliRunner.UniqueName("env");
|
||||
var scriptName = CliRunner.UniqueName("shsc");
|
||||
var pass = "pw-" + env;
|
||||
var bundlePath = Path.Combine(Path.GetTempPath(), scriptName + ".scadabundle");
|
||||
|
||||
// A small multi-line body whose middle line changes between export and import.
|
||||
// The differing line yields a remove (old) + add (new) hunk; the surrounding
|
||||
// lines stay as context, so the render shows all three op classes.
|
||||
const string CodeV1 = "var a = 1;\nvar b = 2;\nreturn a + b;";
|
||||
const string CodeV2 = "var a = 1;\nvar b = 99;\nreturn a + b;";
|
||||
|
||||
int scriptId = 0;
|
||||
try
|
||||
{
|
||||
// ── ARRANGE: create + export the shared script, then diverge the destination ──
|
||||
scriptId = await CliRunner.CreateSharedScriptAsync(scriptName, CodeV1);
|
||||
await CliRunner.BundleExportSharedScriptAsync(bundlePath, scriptName, pass, env);
|
||||
|
||||
// Update the destination's copy so the incoming (bundle) body differs — the
|
||||
// preview now classifies the shared script as Modified with a code lineDiff.
|
||||
await CliRunner.UpdateSharedScriptCodeAsync(scriptId, scriptName, CodeV2);
|
||||
|
||||
var page = await _fixture.NewAuthenticatedPageAsync();
|
||||
|
||||
// ── STEP 1: Upload ────────────────────────────────────────────────────────
|
||||
await page.GotoAsync($"{PlaywrightFixture.BaseUrl}/design/transport/import");
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||
|
||||
await page.Locator("#bundle-input").SetInputFilesAsync(bundlePath);
|
||||
await Assertions.Expect(page.Locator("[data-testid='encrypted-bundle-notice']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await page.Locator("button.btn.btn-primary:has-text('Next')").ClickAsync();
|
||||
|
||||
// ── STEP 2: Passphrase ──────────────────────────────────────────────────────
|
||||
await Assertions.Expect(page.Locator("#import-passphrase"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 10_000 });
|
||||
await page.Locator("#import-passphrase").FillAsync(pass);
|
||||
await page.Locator("button.btn.btn-primary:has-text('Unlock')").ClickAsync();
|
||||
|
||||
// ── STEP 3: Diff — locate the Modified shared-script row's line diff ──────────
|
||||
await Assertions.Expect(page.Locator("#import-passphrase"))
|
||||
.ToBeHiddenAsync(new() { Timeout = 20_000 });
|
||||
await Assertions.Expect(page.Locator("[data-testid='diff-summary']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
// The Modified row attaches a code line-diff cell. The structured payload is
|
||||
// wrapped in [data-testid='code-line-diff'] inside a collapsed <details>; the
|
||||
// line-diff block is laid out only once the disclosure is opened, so open it.
|
||||
var codeLineDiff = page.Locator("[data-testid='code-line-diff']");
|
||||
await Assertions.Expect(codeLineDiff).ToHaveCountAsync(1, new() { Timeout = 15_000 });
|
||||
|
||||
// Open the enclosing <details> ("Field diff" summary) so the diff renders.
|
||||
await page.Locator("details:has([data-testid='code-line-diff']) > summary").ClickAsync();
|
||||
|
||||
// The LineDiffView root + at least one add and one remove hunk render.
|
||||
await Assertions.Expect(page.Locator("[data-testid='line-diff']"))
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await Assertions.Expect(page.Locator("[data-testid='line-diff-add']").First)
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
await Assertions.Expect(page.Locator("[data-testid='line-diff-remove']").First)
|
||||
.ToBeVisibleAsync(new() { Timeout = 15_000 });
|
||||
|
||||
// The new-line text (CodeV2's changed middle line) appears in an add hunk.
|
||||
await Assertions.Expect(page.Locator("[data-testid='line-diff-add']").First)
|
||||
.ToContainTextAsync("99");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Best-effort teardown: drop the shared script + the staged bundle. We never
|
||||
// applied, so the destination only carries the (now-diverged) source script.
|
||||
try
|
||||
{
|
||||
if (scriptId != 0)
|
||||
{
|
||||
await CliRunner.DeleteSharedScriptAsync(scriptId);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort — never mask the test's own failure.
|
||||
}
|
||||
|
||||
try { File.Delete(bundlePath); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user