feat: restore legacy modal and drag behavior

This commit is contained in:
2026-07-16 01:33:48 +09:00
parent 83398044c6
commit d7ec571343
30 changed files with 3597 additions and 321 deletions

View File

@@ -17,7 +17,7 @@ public sealed class LegacyHiddenNonOriginalControlsTests
"styles.css"));
[Fact]
public void Non_original_shortcuts_are_hidden_but_original_dissolve_control_is_visible()
public void Non_original_shortcuts_and_original_hidden_dissolve_panel_are_not_operator_controls()
{
Assert.Contains("class=\"manual-list-actions\" aria-label=\"수동 송출 목록\" hidden",
Markup,
@@ -25,13 +25,14 @@ public sealed class LegacyHiddenNonOriginalControlsTests
Assert.Contains("id=\"playout-prepare\" class=\"prepare\" type=\"button\" disabled hidden",
Markup,
StringComparison.Ordinal);
Assert.Contains("class=\"fade-control\" for=\"playout-fade-duration\">DissolveTime</label>",
Assert.Contains("class=\"fade-control\" for=\"playout-fade-duration\" hidden",
Markup,
StringComparison.Ordinal);
Assert.Contains("id=\"playout-fade-duration\" disabled aria-label=\"DissolveTime\"",
Assert.Contains("aria-hidden=\"true\">DissolveTime</label>",
Markup,
StringComparison.Ordinal);
Assert.DoesNotContain("id=\"playout-fade-duration\" disabled aria-label=\"DissolveTime\" hidden",
Assert.Contains(
"id=\"playout-fade-duration\" disabled hidden aria-hidden=\"true\" tabindex=\"-1\"",
Markup,
StringComparison.Ordinal);
Assert.Contains("*[hidden] { display: none !important; }", Styles,

View File

@@ -0,0 +1,137 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyInteractionMetricsNativeContractTests
{
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"));
[Fact]
public void NativeWindowReadsBothWindowsDragThresholdsInCssDipCoordinates()
{
Assert.Contains("private const int SmCxDrag = 68;", Window,
StringComparison.Ordinal);
Assert.Contains("private const int SmCyDrag = 69;", Window,
StringComparison.Ordinal);
Assert.Contains("[DllImport(\"user32.dll\", ExactSpelling = true)]", Window,
StringComparison.Ordinal);
Assert.Contains(
"GetSystemMetricsForDpi(\n" +
" SmCxDrag,\n" +
" LegacyInteractionMetrics.CssPixelsPerInch)",
Window.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
Assert.Contains(
"GetSystemMetricsForDpi(\n" +
" SmCyDrag,\n" +
" LegacyInteractionMetrics.CssPixelsPerInch)",
Window.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
Assert.DoesNotContain("GetSystemMetrics(", Window, StringComparison.Ordinal);
Assert.Contains("LegacyInteractionMetrics.FromPixelsAtDpi(", Window,
StringComparison.Ordinal);
Assert.Contains("logical DIP coordinate space used by XAML", Window,
StringComparison.Ordinal);
Assert.Contains("Root.XamlRoot?.RasterizationScale", Window,
StringComparison.Ordinal);
}
[Fact]
public void NativeWindowRefreshesMetricsAfterRuntimeSettingsAndDpiChanges()
{
Assert.Contains("interactionMetrics: _interactionMetrics", Window,
StringComparison.Ordinal);
Assert.Contains("private const uint WmSettingChange = 0x001A;", Window,
StringComparison.Ordinal);
Assert.Contains("private const uint WmDisplayChange = 0x007E;", Window,
StringComparison.Ordinal);
Assert.Contains("private const uint WmDpiChanged = 0x02E0;", Window,
StringComparison.Ordinal);
Assert.Contains("AttachInteractionMetricsRefresh();", Window,
StringComparison.Ordinal);
Assert.Contains("DetachInteractionMetricsRefresh();", Window,
StringComparison.Ordinal);
Assert.Contains("SetWindowSubclass(", Window, StringComparison.Ordinal);
Assert.Contains("RemoveWindowSubclass(", Window, StringComparison.Ordinal);
Assert.Contains(
"message is WmSettingChange or WmDisplayChange or WmDpiChanged",
Window,
StringComparison.Ordinal);
Assert.Contains("QueueInteractionMetricsRefresh();", Window,
StringComparison.Ordinal);
Assert.Contains("DispatcherQueuePriority.High", Window,
StringComparison.Ordinal);
Assert.True(
CountOccurrences(Window, "RefreshInteractionMetrics();") >= 3,
"Metrics must be refreshed at setup, runtime change, and state posting.");
Assert.DoesNotContain(
"private readonly LegacyInteractionMetrics _interactionMetrics",
Window,
StringComparison.Ordinal);
}
[Fact]
public void NativeWindowDisablesFutureWebZoomAndFallsBackWithoutPrivateApis()
{
Assert.Contains(
"coreWebView.Settings.AreBrowserAcceleratorKeysEnabled = false;",
Window,
StringComparison.Ordinal);
Assert.Contains("coreWebView.Settings.IsZoomControlEnabled = false;", Window,
StringComparison.Ordinal);
Assert.Contains("coreWebView.Settings.IsPinchZoomEnabled = false;", Window,
StringComparison.Ordinal);
Assert.DoesNotContain("CoreWebView2Controller.ZoomFactor", Window,
StringComparison.Ordinal);
Assert.Contains("catch (DllNotFoundException)", Window,
StringComparison.Ordinal);
Assert.Contains("catch (EntryPointNotFoundException)", Window,
StringComparison.Ordinal);
Assert.Contains("catch (BadImageFormatException)", Window,
StringComparison.Ordinal);
Assert.Contains("catch (PlatformNotSupportedException)", Window,
StringComparison.Ordinal);
Assert.Equal(
4,
CountOccurrences(Window, "return CreateFallbackInteractionMetrics();"));
Assert.Contains(
"LegacyInteractionMetrics.Default with\n" +
" {\n" +
" HostRasterizationScale = ReadHostRasterizationScale()",
Window.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
}
private static int CountOccurrences(string value, string search)
{
var count = 0;
var index = 0;
while ((index = value.IndexOf(search, index, StringComparison.Ordinal)) >= 0)
{
count++;
index += search.Length;
}
return count;
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
{
return current.FullName;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Repository root was not found.");
}
}

View File

@@ -0,0 +1,100 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyNamedPlaylistDispatchFailureContractTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string MainWindow = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.xaml.cs"));
[Fact]
public void NativeRejectionsAndUnexpectedExceptionsTerminateCorrelatedCommands()
{
Assert.Contains(
"PostState(IsNamedPlaylistModalIntent(intent)",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"var failureState = ReportIntentFailure(",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"ObserveOperatorMutationQuarantine(failureState);",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"writeOutcomeMayBeUnknown: dispatchStarted",
MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"_controller.ReportNamedPlaylistWriteOutcomeUnknown(",
MainWindow,
StringComparison.Ordinal);
Assert.Contains("RefreshNamedPlaylistsForDispatchAsync(", MainWindow,
StringComparison.Ordinal);
Assert.Contains("SelectNamedPlaylistForDispatch(", MainWindow,
StringComparison.Ordinal);
Assert.Contains("CreateNamedPlaylistForDispatchAsync(", MainWindow,
StringComparison.Ordinal);
Assert.Contains("DeleteNamedPlaylistForDispatchAsync(", MainWindow,
StringComparison.Ordinal);
Assert.Contains("_controller.CompleteNamedPlaylistDispatch(", MainWindow,
StringComparison.Ordinal);
Assert.Contains(
"_controller.ReportNamedPlaylistCommandFailure(\n" +
" \"load-named-playlist-by-id\"",
MainWindow.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
Assert.Contains(
"_controller.ReportNamedPlaylistCommandFailure(\n" +
" \"save-current-named-playlist-to\"",
MainWindow.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
}
[Fact]
public void SelectionUsesAnExactReceiptWithoutDisablingTheSecondDoubleClick()
{
var normalized = MainWindow.Replace("\r\n", "\n", StringComparison.Ordinal);
Assert.Contains(
"LegacySelectNamedPlaylistIntent selection =>\n" +
" SelectNamedPlaylistForDispatch(",
normalized,
StringComparison.Ordinal);
Assert.Contains(
"intent is LegacyRefreshNamedPlaylistsIntent or\n" +
" LegacySelectNamedPlaylistIntent or",
normalized,
StringComparison.Ordinal);
Assert.DoesNotContain(
"LegacyRefreshNamedPlaylistsIntent or\n" +
" LegacySelectNamedPlaylistIntent or",
normalized,
StringComparison.Ordinal);
Assert.Contains(
"\"select-named-playlist\",\n" +
" requestId,\n" +
" succeeded",
normalized,
StringComparison.Ordinal);
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
{
return current.FullName;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Repository root was not found.");
}
}

View File

@@ -14,7 +14,7 @@ public sealed class LegacyPlayoutWebContractTests
Path.Combine(WebRoot, "playout-ui.js"));
[Fact]
public void ShellExposesOriginalPlayoutBackgroundAndFadeControls()
public void ShellExposesBackgroundControlsButKeepsOriginalHiddenFadeStateInert()
{
foreach (var id in new[]
{
@@ -40,10 +40,11 @@ public sealed class LegacyPlayoutWebContractTests
var fadeMarkup = ExtractElement(Markup, "playout-fade-duration", "</select>");
Assert.Equal(20, CountOccurrences(fadeMarkup, "<option value="));
Assert.Contains(
"class=\"fade-control\" for=\"playout-fade-duration\">DissolveTime</label>",
"class=\"fade-control\" for=\"playout-fade-duration\" hidden",
Markup,
StringComparison.Ordinal);
Assert.DoesNotContain(" hidden", fadeMarkup, StringComparison.Ordinal);
Assert.Contains("hidden aria-hidden=\"true\" tabindex=\"-1\"", fadeMarkup,
StringComparison.Ordinal);
for (var index = 0; index < 20; index++)
{
Assert.Contains(
@@ -65,8 +66,7 @@ public sealed class LegacyPlayoutWebContractTests
"next-playout",
"take-out",
"choose-background",
"toggle-background",
"set-fade-duration"
"toggle-background"
})
{
Assert.Contains($"\"{intent}\"", Script, StringComparison.Ordinal);
@@ -82,6 +82,14 @@ public sealed class LegacyPlayoutWebContractTests
Assert.Contains("post(\"take-in\", {})", Script, StringComparison.Ordinal);
Assert.Contains("post(\"next-playout\", {})", Script, StringComparison.Ordinal);
Assert.Contains("post(\"take-out\", {})", Script, StringComparison.Ordinal);
Assert.DoesNotContain("post(\"set-fade-duration\"", Script,
StringComparison.Ordinal);
Assert.DoesNotContain("fadeDuration.addEventListener", Script,
StringComparison.Ordinal);
Assert.Contains("fadeDuration.disabled = true;", Script,
StringComparison.Ordinal);
Assert.Contains("fadeDuration.value = String(playout.fadeDuration);", Script,
StringComparison.Ordinal);
}
[Fact]
@@ -92,7 +100,11 @@ public sealed class LegacyPlayoutWebContractTests
Assert.Contains("event.key === \"F3\"", Script, StringComparison.Ordinal);
Assert.Contains("event.key === \"F8\"", Script, StringComparison.Ordinal);
Assert.Contains("event.key === \"Escape\"", Script, StringComparison.Ordinal);
Assert.Contains("event.defaultPrevented || event.repeat || hasOpenDialog()", Script,
Assert.Contains("if (event.defaultPrevented) return;", Script,
StringComparison.Ordinal);
Assert.Contains("if (hasOpenDialog())", Script,
StringComparison.Ordinal);
Assert.Contains("if (legacyShortcut) event.preventDefault();", Script,
StringComparison.Ordinal);
}

View File

@@ -56,18 +56,36 @@ public sealed class LegacyRowDoubleClickParityTests
var doubleClick = Extract(
"namedPlaylistDefinitions.addEventListener(\"dblclick\"",
"namedPlaylistDefinitions.addEventListener(\"click\"");
var action = Extract(
"function runNamedPlaylistDoubleClick",
"function lockNamedPlaylistControls");
var confirmation = Extract(
"function resolveNamedPlaylistConfirmation",
"function renderNamedPlaylist");
var matcher = Extract(
"function isMatchingNamedPlaylistReceipt",
"function openNamedPlaylistConfirmation");
var render = Extract("function renderNamedPlaylist", "function renderOperatorCatalog");
Assert.Contains("\"load-named-playlist-by-id\"", doubleClick,
Assert.Contains("namedPlaylistSelection.defer", doubleClick,
StringComparison.Ordinal);
Assert.Contains("\"save-current-named-playlist-to\"", doubleClick,
Assert.Contains("runNamedPlaylistDoubleClick", doubleClick,
StringComparison.Ordinal);
Assert.Contains("\"load-named-playlist-by-id\"", action,
StringComparison.Ordinal);
Assert.Contains("openNamedPlaylistConfirmation", action,
StringComparison.Ordinal);
Assert.Contains("message: \"저장하겠습니까?\"", action,
StringComparison.Ordinal);
Assert.Contains("showSavedAlert: false", action, StringComparison.Ordinal);
Assert.Contains("closeOnNo: true", action, StringComparison.Ordinal);
Assert.DoesNotContain("window.confirm", action + doubleClick, StringComparison.Ordinal);
Assert.Contains("\"save-current-named-playlist-to\"", confirmation,
StringComparison.Ordinal);
Assert.Contains("window.confirm", doubleClick, StringComparison.Ordinal);
Assert.Contains("definitionId: definitionId", doubleClick, StringComparison.Ordinal);
Assert.DoesNotContain("select-named-playlist", doubleClick, StringComparison.Ordinal);
Assert.Contains("commandResult.succeeded === true", render, StringComparison.Ordinal);
Assert.Contains("commandResult.targetId === pendingNamedPlaylistCommand.definitionId",
render,
Assert.Contains("commandResult.targetId === pending.targetId",
matcher,
StringComparison.Ordinal);
Assert.Contains("closeNamedPlaylist()", render, StringComparison.Ordinal);
}

View File

@@ -161,8 +161,11 @@ public sealed class LegacyWebContractTests
Assert.Contains("event.dataTransfer.setData(playlistDragMime, rowId)", App,
StringComparison.Ordinal);
Assert.DoesNotContain("selectedRowIds", App, StringComparison.Ordinal);
Assert.Contains("element.draggable = !cutDragLocked", App, StringComparison.Ordinal);
Assert.Contains("if (playlistMutationLocked || !event.dataTransfer)", App,
Assert.Contains("element.draggable = false", App, StringComparison.Ordinal);
Assert.Contains("beginCutDragGesture(element, event, position)", App,
StringComparison.Ordinal);
Assert.Contains("window.LegacyCutDrag.shouldStart", App, StringComparison.Ordinal);
Assert.Contains("if (playlistMutationLocked || !rowId || !event.dataTransfer", App,
StringComparison.Ordinal);
Assert.Contains("if (!playlistMutationLocked) send(\"delete-selected-playlist-rows\"", App,
StringComparison.Ordinal);
@@ -181,7 +184,7 @@ public sealed class LegacyWebContractTests
"} else if ((event.key === \"Home\"",
StringComparison.Ordinal);
var homeEndBranchEnd = App.IndexOf(
"} else if (event.key === \"Escape\"",
"\n });",
homeEndBranchStart,
StringComparison.Ordinal);
Assert.True(homeEndBranchStart >= 0 && homeEndBranchEnd > homeEndBranchStart);
@@ -348,9 +351,9 @@ public sealed class LegacyWebContractTests
{
"refresh-named-playlists",
"select-named-playlist",
"load-selected-named-playlist",
"load-named-playlist-by-id",
"create-named-playlist",
"save-current-named-playlist",
"save-current-named-playlist-to",
"delete-selected-named-playlist"
})
{
@@ -359,9 +362,16 @@ public sealed class LegacyWebContractTests
Assert.Contains("state.namedPlaylist", App, StringComparison.Ordinal);
Assert.Contains("dataset.namedPlaylistDefinitionId", App, StringComparison.Ordinal);
Assert.Contains("definitionId: definition.dataset.namedPlaylistDefinitionId", App,
Assert.Contains(
"beginNamedPlaylistSelection(definition.dataset.namedPlaylistDefinitionId)", App,
StringComparison.Ordinal);
Assert.Contains(
"dbLoadButton.disabled = operatorMutationLocked || busy || !named",
App,
StringComparison.Ordinal);
Assert.Contains("requestId: requestId", App, StringComparison.Ordinal);
Assert.Contains("commandResult.targetId === pending.targetId", App,
StringComparison.Ordinal);
Assert.Contains("dbLoadButton.disabled = busy || !named", App, StringComparison.Ordinal);
Assert.Contains("named.isWriteQuarantined", App, StringComparison.Ordinal);
Assert.DoesNotContain("programCode", App, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("listText", App, StringComparison.OrdinalIgnoreCase);
@@ -375,8 +385,13 @@ public sealed class LegacyWebContractTests
[Fact]
public void SeparatorRowsPreserveOriginalDragGesture()
{
Assert.Contains("element.draggable = true", App, StringComparison.Ordinal);
Assert.DoesNotContain("draggable = !row.isSeparator", App, StringComparison.Ordinal);
Assert.Contains(
"element.dataset.cutDragEnabled = cutSelectionEnabled ? \"true\" : \"false\"",
App,
StringComparison.Ordinal);
Assert.Contains("element.draggable = false", App, StringComparison.Ordinal);
Assert.DoesNotContain("cutDragEnabled = !row.isSeparator", App,
StringComparison.Ordinal);
}
[Fact]