feat: restore legacy modal and drag behavior
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user