test(playwright): M8 import Map step + per-line diff coverage (#230)

This commit is contained in:
Joseph Doherty
2026-06-19 04:23:12 -04:00
parent 7bd081ba50
commit 529921f2de
2 changed files with 362 additions and 0 deletions
@@ -156,6 +156,55 @@ public static partial class CliRunner
return RequireId(doc, "site area create");
}
/// <summary>
/// Creates a site via <c>site create</c> and returns its new <c>id</c>.
/// </summary>
/// <param name="name">Site display name (typically from <see cref="UniqueName"/>).</param>
/// <param name="identifier">
/// Stable <c>SiteIdentifier</c> the bundle subsystem matches on. Must be unique
/// across the cluster; pass a <see cref="UniqueName"/>-derived value so the import
/// preview finds no auto-match in the destination once the source site is deleted.
/// </param>
/// <returns>The id of the newly created site.</returns>
/// <exception cref="InvalidOperationException">
/// The CLI failed, or the response did not carry an integer <c>id</c>.
/// </exception>
public static async Task<int> CreateSiteAsync(string name, string identifier)
{
using var doc = await RunJsonAsync(
"site", "create", "--name", name, "--identifier", identifier);
return RequireId(doc, "site create");
}
/// <summary>
/// Returns the ids of all sites whose <c>siteIdentifier</c> starts with
/// <paramref name="identifierPrefix"/> (ordinal), via <c>site list</c>. Used to
/// drop any throwaway site a Transport test created (the create helper hands back
/// the id, but teardown re-resolves defensively in case the test failed mid-flow).
/// </summary>
/// <param name="identifierPrefix">The <c>siteIdentifier</c> prefix to match.</param>
public static async Task<IReadOnlyList<int>> ListSiteIdsByIdentifierPrefixAsync(string identifierPrefix)
{
using var doc = await RunJsonAsync("site", "list");
var ids = new List<int>();
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
foreach (var site in doc.RootElement.EnumerateArray())
{
if (site.TryGetProperty("siteIdentifier", out var idtf)
&& idtf.ValueKind == JsonValueKind.String
&& (idtf.GetString()?.StartsWith(identifierPrefix, StringComparison.Ordinal) ?? false)
&& site.TryGetProperty("id", out var id)
&& id.TryGetInt32(out var siteId))
{
ids.Add(siteId);
}
}
}
return ids;
}
/// <summary>
/// Resolves a site's numeric id from its <c>siteIdentifier</c> (e.g.
/// <c>"site-a"</c>) via <c>site list</c>.
@@ -519,6 +568,27 @@ public static partial class CliRunner
return RequireId(doc, "shared-script create");
}
/// <summary>
/// Updates a shared script's <c>Code</c> via <c>shared-script update</c>. The
/// CLI requires both <c>--name</c> and <c>--code</c>, so the (unchanged) name is
/// passed through alongside the new body. Used by the Transport import line-diff
/// test to diverge the destination's code from the bundle's after export, so the
/// preview classifies the shared script as <c>Modified</c> and emits a per-line
/// <c>lineDiff</c>. Surfaces CLI errors (the test asserts the update succeeded).
/// </summary>
/// <param name="id">Shared script id.</param>
/// <param name="name">Shared script name (unchanged; the CLI requires it).</param>
/// <param name="code">The new script body.</param>
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
public static async Task UpdateSharedScriptCodeAsync(int id, string name, string code)
{
await RunAsync(
"shared-script", "update",
"--id", id.ToString(System.Globalization.CultureInfo.InvariantCulture),
"--name", name,
"--code", code);
}
/// <summary>
/// Creates an LDAP→role mapping via <c>security role-mapping create</c> and returns its new <c>id</c>.
/// </summary>
@@ -659,6 +729,62 @@ public static partial class CliRunner
"--source-environment", sourceEnvironment);
}
/// <summary>
/// Exports a Transport bundle scoped to a single site via <c>bundle export
/// --sites &lt;siteIdentifier&gt;</c>. A bundle that carries a site produces a
/// <c>RequiredSiteMapping</c> in the import preview, which is what makes the
/// import wizard's Map step render. With the source site deleted before import,
/// the mapping has no auto-match, so the Map row defaults to "Create new" and the
/// operator must pick a target — exactly the path the Map-step test exercises.
/// </summary>
/// <param name="outputPath">Destination <c>.scadabundle</c> path.</param>
/// <param name="siteIdentifier">The <c>SiteIdentifier</c> of the single site to export.</param>
/// <param name="passphrase">Encryption passphrase for the bundle.</param>
/// <param name="sourceEnvironment"><c>SourceEnvironment</c> value stamped into the bundle manifest.</param>
/// <exception cref="InvalidOperationException">The CLI failed.</exception>
public static async Task BundleExportSiteAsync(
string outputPath, string siteIdentifier, string passphrase, string sourceEnvironment)
{
await RunAsync(
"bundle", "export",
"--output", outputPath,
"--passphrase", passphrase,
"--sites", siteIdentifier,
"--source-environment", sourceEnvironment);
}
/// <summary>
/// Exports a Transport bundle scoped to a single shared script via
/// <c>bundle export --shared-scripts &lt;name&gt;</c>. A shared script carries a
/// <c>Code</c> field, so when the destination's copy is later diverged the import
/// preview classifies it as <c>Modified</c> and emits a structured per-line
/// <c>lineDiff</c> — the payload the <c>LineDiffView</c> render asserts on.
/// </summary>
/// <param name="outputPath">Destination <c>.scadabundle</c> path.</param>
/// <param name="sharedScriptName">Name of the single shared script to export.</param>
/// <param name="passphrase">Encryption passphrase for the bundle.</param>
/// <param name="sourceEnvironment"><c>SourceEnvironment</c> value stamped into the bundle manifest.</param>
/// <exception cref="InvalidOperationException">
/// The shared-script name contains a comma (it would split the comma-separated
/// <c>--shared-scripts</c> selector), or the CLI failed.
/// </exception>
public static async Task BundleExportSharedScriptAsync(
string outputPath, string sharedScriptName, string passphrase, string sourceEnvironment)
{
if (sharedScriptName.Contains(','))
{
throw new InvalidOperationException(
$"Shared-script name '{sharedScriptName}' contains a comma and cannot be used with '--shared-scripts'.");
}
await RunAsync(
"bundle", "export",
"--output", outputPath,
"--passphrase", passphrase,
"--shared-scripts", sharedScriptName,
"--source-environment", sourceEnvironment);
}
/// <summary>
/// Resolves a template's name from its id via <c>template list</c>.
/// </summary>