feat: add native runtime settings
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyOperatorSettingsNativeContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string AppRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp");
|
||||
private static readonly string Window = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
|
||||
private static readonly string Settings = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.Settings.cs"));
|
||||
private static readonly string Playout = File.ReadAllText(
|
||||
Path.Combine(AppRoot, "MainWindow.Playout.cs"));
|
||||
|
||||
[Fact]
|
||||
public void FolderPathsComeOnlyFromTheWindowOwnedNativePicker()
|
||||
{
|
||||
Assert.Contains("var picker = new FolderPicker", Settings, StringComparison.Ordinal);
|
||||
Assert.Contains("picker.FileTypeFilter.Add(\"*\")", Settings,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("InitializeWithWindow.Initialize(picker, owner);", Settings,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("await picker.PickSingleFolderAsync()", Settings,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("selected.Path", Settings, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyOperatorFolderValidator.TryValidate(", Settings,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("intent.Path", Settings, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("choose.Path", Settings, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectedFoldersAreLoadedAtStartupAndAppliedOnlyAtCompositionBoundaries()
|
||||
{
|
||||
Assert.Contains("_operatorSettingsStore.Load()", Window, StringComparison.Ordinal);
|
||||
Assert.Contains("_appliedOperatorSettings = operatorSettingsLoad.Settings", Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ResolveFromResourceDirectory(", Window, StringComparison.Ordinal);
|
||||
Assert.Contains("preferLegacyOverride: selectedLegacyDatabaseIni is not null", Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory",
|
||||
Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory",
|
||||
Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("InitializePlayoutRuntime();", Settings, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("CreateLegacyExecutableDefault(", Settings,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateAUsesOnlyThePinnedJsonDatabaseProfile()
|
||||
{
|
||||
var databaseComposition = Slice(
|
||||
Window,
|
||||
"string? initializationError = null;",
|
||||
"DataQueryExecutor.Configure(_databaseRuntime.Executor);");
|
||||
var gateA = RequiredIndex(
|
||||
databaseComposition,
|
||||
"_playoutLaunchAuthorization.IsGateA");
|
||||
var pinnedJson = RequiredIndex(
|
||||
databaseComposition,
|
||||
"DatabaseRuntime.CreateDefault()",
|
||||
gateA);
|
||||
var legacyIni = RequiredIndex(
|
||||
databaseComposition,
|
||||
"DatabaseRuntime.CreateLegacyExecutableDefault(",
|
||||
pinnedJson);
|
||||
|
||||
Assert.True(gateA < pinnedJson);
|
||||
Assert.True(pinnedJson < legacyIni);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsWritesAreGateABlockedAndDoNotWaitForDatabaseHealthActivity()
|
||||
{
|
||||
var process = Slice(
|
||||
Window,
|
||||
"private async Task ProcessIntentAsync(LegacyUiIntent intent)",
|
||||
"private static bool IsOrderedPlayoutIntent");
|
||||
var gateABlock = RequiredIndex(process, "IsPersistentWriteIntent(intent)");
|
||||
var settingsDispatch = RequiredIndex(
|
||||
process,
|
||||
"await HandleOperatorSettingsIntentAsync(",
|
||||
gateABlock);
|
||||
var databaseWait = RequiredIndex(
|
||||
process,
|
||||
"await AcquirePriorityDatabaseActivityAsync(_lifetimeCancellation.Token)");
|
||||
|
||||
Assert.True(gateABlock < settingsDispatch);
|
||||
Assert.True(settingsDispatch < databaseWait);
|
||||
Assert.Contains("HandleOperatorSettingsIntentAsync(", process,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyChooseOperatorFolderIntent", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyResetOperatorFolderIntent", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("LegacySetOperatorNavigationExpandedIntent", Playout,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ReconcileRejectedOperatorSettingsIntent(", process,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsStateIsProjectedWithoutPublishingFullNativePaths()
|
||||
{
|
||||
Assert.Contains("operatorSettings: CreateOperatorSettingsSnapshot()", Window,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("AbbreviatedPath", Settings, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("CanonicalPath,\n _operatorSettingsMessage", Settings,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("DatabasePollingSeconds: (int)DatabaseHealthPollingInterval.TotalSeconds",
|
||||
Settings,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedSettingsSaveRevisionsTheSnapshotToUndoOptimisticNavigation()
|
||||
{
|
||||
Assert.Contains(
|
||||
"if (!result.Succeeded || result.SavedSettings is null)",
|
||||
Settings,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Interlocked.Increment(ref _operatorSettingsRevision);",
|
||||
Settings,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string Slice(string value, string start, string end)
|
||||
{
|
||||
var startIndex = RequiredIndex(value, start);
|
||||
var endIndex = RequiredIndex(value, end, startIndex + start.Length);
|
||||
return value[startIndex..endIndex];
|
||||
}
|
||||
|
||||
private static int RequiredIndex(string value, string needle, int startIndex = 0)
|
||||
{
|
||||
var index = value.IndexOf(needle, startIndex, StringComparison.Ordinal);
|
||||
Assert.True(index >= 0, $"Required marker was not found: {needle}");
|
||||
return index;
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyOperatorSettingsWebContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string WebRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
||||
"Web");
|
||||
private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
|
||||
private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
|
||||
private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js"));
|
||||
|
||||
[Fact]
|
||||
public void SettingsScreenOffersThreeNativeFolderPickersAndRestartFeedback()
|
||||
{
|
||||
var expectedKinds = new[] { "design", "resource", "background" };
|
||||
var actualKinds = Regex.Matches(Markup, "data-operator-folder=\"([^\"]+)\"")
|
||||
.Select(match => match.Groups[1].Value)
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal(expectedKinds, actualKinds);
|
||||
foreach (var kind in expectedKinds)
|
||||
{
|
||||
Assert.Contains($"id=\"operator-{kind}-folder-name\"", Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains($"id=\"operator-{kind}-folder-status\"", Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains($"id=\"operator-{kind}-folder-select\"", Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains($"id=\"operator-{kind}-folder-reset\"", Markup,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Matches(
|
||||
"id=\"operator-settings-message\"[^>]*role=\"status\"[^>]*aria-live=\"polite\"",
|
||||
Markup);
|
||||
Assert.Matches(
|
||||
"id=\"operator-settings-restart\"[^>]*role=\"status\"[^>]*aria-live=\"polite\"",
|
||||
Markup);
|
||||
Assert.Contains("앱을 다시 시작하면 새 폴더가 적용됩니다.", Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("settings-status-badge.valid", Styles, StringComparison.Ordinal);
|
||||
Assert.Contains("settings-status-badge.invalid", Styles, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsDispatchesOnlyClosedFolderKindsAndNeverWebSuppliedPaths()
|
||||
{
|
||||
var dispatch = Slice(
|
||||
App,
|
||||
"Object.keys(operatorFolderControls).forEach(function (kind)",
|
||||
"searchButton.addEventListener");
|
||||
|
||||
Assert.Contains("send(\"choose-operator-folder\", { kind: kind })", dispatch,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("send(\"reset-operator-folder\", { kind: kind })", dispatch,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"send(\"set-operator-navigation-expanded\", { expanded: expanded })",
|
||||
dispatch,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("path", dispatch, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("displayName", dispatch, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RepeatedSettingsStateKeepsStaticControlsAndAppliesNavigationByRevision()
|
||||
{
|
||||
var folderRender = Slice(App, "function renderOperatorFolder", "function render(state)");
|
||||
var settingsRender = Slice(
|
||||
App,
|
||||
"function renderOperatorSettings",
|
||||
"function render(state)");
|
||||
|
||||
Assert.DoesNotContain("innerHTML", folderRender, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("replaceChildren", folderRender, StringComparison.Ordinal);
|
||||
Assert.Contains("operatorSettingsRevisionInitialized", settingsRender,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("revision !== operatorSettingsRenderRevision", settingsRender,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Matches(
|
||||
new Regex(
|
||||
@"if \(revisionChanged\) \{[\s\S]*?LegacyWorkspaceNavigation\.setExpanded\(expanded\);[\s\S]*?operatorSettingsRenderRevision = revision;",
|
||||
RegexOptions.CultureInvariant),
|
||||
settingsRender);
|
||||
Assert.Single(
|
||||
Regex.Matches(settingsRender, "LegacyWorkspaceNavigation.setExpanded").Cast<Match>());
|
||||
Assert.Contains("pendingOperatorSettingsFocusId", settingsRender,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("target.focus({ preventScroll: true });", settingsRender,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsShowsSafeDiagnosticsAndNamesProtectedValues()
|
||||
{
|
||||
foreach (var id in new[]
|
||||
{
|
||||
"operator-settings-app-version",
|
||||
"operator-settings-playout-mode",
|
||||
"operator-settings-playout-connection",
|
||||
"operator-settings-oracle-state",
|
||||
"operator-settings-maria-state",
|
||||
"operator-settings-db-monitor"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"id=\"{id}\"", Markup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("id=\"operator-navigation-expanded\"", Markup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("databasePollingSeconds", App, StringComparison.Ordinal);
|
||||
Assert.Contains("DB 자격 증명", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("Tornado2 송출 대상", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("Live/DryRun 권한", Markup, StringComparison.Ordinal);
|
||||
Assert.Contains("장면 허용 목록", Markup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string Slice(string source, string startMarker, string endMarker)
|
||||
{
|
||||
var start = source.IndexOf(startMarker, StringComparison.Ordinal);
|
||||
Assert.True(start >= 0, $"Missing start marker: {startMarker}");
|
||||
var end = source.IndexOf(endMarker, start, StringComparison.Ordinal);
|
||||
Assert.True(end > start, $"Missing end marker: {endMarker}");
|
||||
return source[start..end];
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root could not be located.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user