feat: add configurable operator appearance and layout

This commit is contained in:
2026-07-23 11:14:10 +09:00
parent cf117de144
commit f43483533a
34 changed files with 5942 additions and 740 deletions

View File

@@ -0,0 +1,89 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyOperatorFeedbackWebContractTests
{
private static readonly string App = File.ReadAllText(Path.Combine(
FindRepositoryRoot(),
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"Web",
"app.js"));
[Fact]
public void StockSearchFeedbackStaysConciseAndUsesNativeState()
{
var feedback = Slice(
App,
"function renderStockSearchMeta(state)",
"function pointerPosition(event)");
Assert.Contains("state.isBusy === true", feedback, StringComparison.Ordinal);
Assert.Contains("label = \"검색 중\"", feedback, StringComparison.Ordinal);
Assert.Contains("String(results.length) + \"건\"", feedback,
StringComparison.Ordinal);
Assert.Contains("row.index === state.selectedStockIndex", feedback,
StringComparison.Ordinal);
Assert.Contains("selected.displayName", feedback, StringComparison.Ordinal);
Assert.Contains("stockSearchMeta.hidden = label.length === 0", feedback,
StringComparison.Ordinal);
Assert.Contains("stockResults.dataset.emptyLabel = \"결과 없음\"", feedback,
StringComparison.Ordinal);
Assert.Contains("delete stockResults.dataset.emptyLabel", feedback,
StringComparison.Ordinal);
}
[Fact]
public void EmptyCutFeedbackDistinguishesStockSelectionAndClearsWhenRowsReturn()
{
var cuts = Slice(App, "function renderCuts(state)", "cutList.addEventListener");
Assert.Contains("state.cutRows.length === 0", cuts, StringComparison.Ordinal);
Assert.Contains("Number.isInteger(state.selectedStockIndex)", cuts,
StringComparison.Ordinal);
Assert.Contains("? \"컷 없음\"", cuts, StringComparison.Ordinal);
Assert.Contains(": \"종목을 선택하세요\"", cuts, StringComparison.Ordinal);
Assert.Contains("delete cutList.dataset.emptyLabel", cuts,
StringComparison.Ordinal);
}
[Fact]
public void GlobalBusyFeedbackDoesNotMislabelEveryOperationAsSearch()
{
var render = Slice(App, "function render(state)",
"Object.keys(operatorFolderControls)");
Assert.Contains("document.body.classList.toggle(\"busy\", isBusy)", render,
StringComparison.Ordinal);
Assert.Contains("document.body.setAttribute(\"aria-busy\"", render,
StringComparison.Ordinal);
Assert.Contains("renderStockResults(state);\n renderStockSearchMeta(state);", render,
StringComparison.Ordinal);
Assert.Contains("isBusy ? \"처리 중\"", render, StringComparison.Ordinal);
Assert.DoesNotContain("종목을 검색하고 있습니다.", render, 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.");
}
}

View File

@@ -102,10 +102,35 @@ public sealed class LegacyOperatorSettingsNativeContractTests
StringComparison.Ordinal);
Assert.Contains("LegacySetOperatorNavigationExpandedIntent", Playout,
StringComparison.Ordinal);
Assert.Contains("LegacySetOperatorAppearanceIntent", Playout,
StringComparison.Ordinal);
Assert.Contains("ReconcileRejectedOperatorSettingsIntent(", process,
StringComparison.Ordinal);
}
[Fact]
public void AppearanceIntentIsPersistedRoutedAndProjectedAsOneClosedUpdate()
{
Assert.Contains(
"case LegacySetOperatorAppearanceIntent appearance:",
Settings,
StringComparison.Ordinal);
Assert.Contains("ColorTheme = appearance.ColorTheme", Settings,
StringComparison.Ordinal);
Assert.Contains("ViewMode = appearance.ViewMode", Settings,
StringComparison.Ordinal);
Assert.Contains("StartWorkspace = appearance.StartWorkspace", Settings,
StringComparison.Ordinal);
Assert.Contains("_operatorSettings.ColorTheme", Settings,
StringComparison.Ordinal);
Assert.Contains("_operatorSettings.ViewMode", Settings,
StringComparison.Ordinal);
Assert.Contains("_operatorSettings.StartWorkspace", Settings,
StringComparison.Ordinal);
Assert.Contains("LegacySetOperatorAppearanceIntent", Window,
StringComparison.Ordinal);
}
[Fact]
public void SettingsStateIsProjectedWithoutPublishingFullNativePaths()
{
@@ -132,6 +157,28 @@ public sealed class LegacyOperatorSettingsNativeContractTests
StringComparison.Ordinal);
}
[Fact]
public void SettingsUsesConciseRoutineStatusCopy()
{
var cancelledPicker = Slice(
Settings,
"if (selected is null)",
"cancellationToken.ThrowIfCancellationRequested();");
Assert.DoesNotContain(
"SetOperatorSettingsMessage",
cancelledPicker,
StringComparison.Ordinal);
Assert.Contains("\"저장됨\"", Settings, StringComparison.Ordinal);
Assert.Contains("\"저장됨 · 재시작 필요\"", Settings, StringComparison.Ordinal);
Assert.Contains("\"초기화됨 · 재시작 필요\"", Settings, StringComparison.Ordinal);
Assert.Contains("\"앱 기본값\"", Settings, StringComparison.Ordinal);
Assert.Contains("\"자동\"", Settings, StringComparison.Ordinal);
Assert.Contains("\"사용 가능\"", Settings, StringComparison.Ordinal);
Assert.DoesNotContain("외부 영상", Settings, StringComparison.Ordinal);
Assert.DoesNotContain("핵심 자산 확인됨", Settings, StringComparison.Ordinal);
}
private static string Slice(string value, string start, string end)
{
var startIndex = RequiredIndex(value, start);

View File

@@ -13,9 +13,14 @@ public sealed class LegacyOperatorSettingsWebContractTests
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"));
private static readonly string SettingsHost = File.ReadAllText(Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MainWindow.Settings.cs"));
[Fact]
public void SettingsScreenOffersThreeNativeFolderPickersAndRestartFeedback()
public void SettingsScreenKeepsThreeNativeFolderPickersWithConciseFeedback()
{
var expectedKinds = new[] { "design", "resource", "background" };
var actualKinds = Regex.Matches(Markup, "data-operator-folder=\"([^\"]+)\"")
@@ -33,6 +38,9 @@ public sealed class LegacyOperatorSettingsWebContractTests
StringComparison.Ordinal);
Assert.Contains($"id=\"operator-{kind}-folder-reset\"", Markup,
StringComparison.Ordinal);
Assert.Matches(
$"id=\"operator-{kind}-folder-source\"[^>]*hidden",
Markup);
}
Assert.Matches(
@@ -41,10 +49,31 @@ public sealed class LegacyOperatorSettingsWebContractTests
Assert.Matches(
"id=\"operator-settings-restart\"[^>]*role=\"status\"[^>]*aria-live=\"polite\"",
Markup);
Assert.Contains("앱을 다시 시작하면 새 폴더가 적용됩니다.", Markup,
Assert.Contains("<h3>폴더</h3>", Markup, StringComparison.Ordinal);
Assert.Contains("<h3>메뉴</h3>", Markup, StringComparison.Ordinal);
Assert.Contains("<h3>상태</h3>", Markup, StringComparison.Ordinal);
Assert.Contains("재시작하면 적용됩니다.", Markup,
StringComparison.Ordinal);
Assert.Contains("id=\"operator-design-folder-name\">앱 기본값", Markup,
StringComparison.Ordinal);
Assert.Contains("id=\"operator-resource-folder-name\">앱 기본값", Markup,
StringComparison.Ordinal);
Assert.Contains("id=\"operator-background-folder-name\">자동", Markup,
StringComparison.Ordinal);
Assert.Contains("id=\"operator-navigation-expanded\"", Markup,
StringComparison.Ordinal);
Assert.Contains("메뉴 펼치기", Markup, StringComparison.Ordinal);
Assert.Contains("settings-status-badge.valid", Styles, StringComparison.Ordinal);
Assert.Contains("settings-status-badge.invalid", Styles, StringComparison.Ordinal);
Assert.DoesNotContain("settings-intro", Markup, StringComparison.Ordinal);
Assert.DoesNotContain("이 PC에서 사용할 디자인·설정·배경 폴더", Markup,
StringComparison.Ordinal);
Assert.DoesNotContain("경로를 직접 입력하지 않고", Markup,
StringComparison.Ordinal);
Assert.DoesNotContain("송출 장면(.t2s)과 장면별 디자인 자산", Markup,
StringComparison.Ordinal);
Assert.DoesNotContain("핵심 자산 확인됨", Markup, StringComparison.Ordinal);
}
[Fact]
@@ -67,6 +96,63 @@ public sealed class LegacyOperatorSettingsWebContractTests
Assert.DoesNotContain("displayName", dispatch, StringComparison.Ordinal);
}
[Fact]
public void AppearanceSettingsExposeThreeConciseClosedChoices()
{
var appearance = Slice(
Markup,
"<article class=\"settings-card settings-appearance-card\">",
"<article class=\"settings-card\">");
Assert.Contains("<h3>화면</h3>", appearance, StringComparison.Ordinal);
Assert.Contains("id=\"operator-color-theme\"", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"system\">자동", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"light\">라이트", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"dark\">다크", appearance, StringComparison.Ordinal);
Assert.Contains("id=\"operator-view-mode\"", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"automatic\">자동", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"compact\">컴팩트", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"cards\">카드", appearance, StringComparison.Ordinal);
Assert.Contains("id=\"operator-start-workspace\"", appearance,
StringComparison.Ordinal);
Assert.Contains("value=\"stockCut\">종목·컷", appearance, StringComparison.Ordinal);
Assert.Contains("value=\"lastWorkspace\">마지막 화면", appearance,
StringComparison.Ordinal);
Assert.DoesNotContain("<p", appearance, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("<small", appearance, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void AppearanceSettingsApplyClosedValuesAndDispatchOneCompleteSnapshot()
{
var render = Slice(App, "function normalizeOperatorAppearanceValue", "function render(state)");
var dispatch = Slice(
App,
"Object.keys(operatorFolderControls).forEach(function (kind)",
"document.addEventListener(\"input\"");
Assert.Contains("definition.values.includes(value)", render, StringComparison.Ordinal);
Assert.Contains("element.dataset.colorTheme = appearance.colorTheme", render,
StringComparison.Ordinal);
Assert.Contains("element.dataset.viewMode = appearance.viewMode", render,
StringComparison.Ordinal);
Assert.Contains("element.dataset.startWorkspace = appearance.startWorkspace", render,
StringComparison.Ordinal);
Assert.Contains("operatorAppearanceControls[key].element.disabled = isBusy", render,
StringComparison.Ordinal);
Assert.Contains("applyStartPreference(appearance.startWorkspace)", render,
StringComparison.Ordinal);
Assert.DoesNotContain("isInitialSettingsSnapshot", render, StringComparison.Ordinal);
Assert.Contains("send(\"set-operator-appearance\", {", dispatch,
StringComparison.Ordinal);
Assert.Contains("colorTheme: appearance.colorTheme", dispatch, StringComparison.Ordinal);
Assert.Contains("viewMode: appearance.viewMode", dispatch, StringComparison.Ordinal);
Assert.Contains("startWorkspace: appearance.startWorkspace", dispatch,
StringComparison.Ordinal);
Assert.Contains("syncOperatorAppearanceControls(operatorAppearanceSnapshot)", dispatch,
StringComparison.Ordinal);
}
[Fact]
public void RepeatedSettingsStateKeepsStaticControlsAndAppliesNavigationByRevision()
{
@@ -96,7 +182,7 @@ public sealed class LegacyOperatorSettingsWebContractTests
}
[Fact]
public void SettingsShowsSafeDiagnosticsAndNamesProtectedValues()
public void SettingsKeepsSafeDiagnosticsWithoutProtectedValueExplanations()
{
foreach (var id in new[]
{
@@ -114,10 +200,26 @@ public sealed class LegacyOperatorSettingsWebContractTests
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);
Assert.Contains("LegacyOperatorFolderValidator.TryValidate", SettingsHost,
StringComparison.Ordinal);
Assert.Contains("CanReadSelectedDatabaseProfile", SettingsHost,
StringComparison.Ordinal);
Assert.Contains("ResolveDatabaseHealthLabel", SettingsHost,
StringComparison.Ordinal);
Assert.Contains("\"저장됨\"", SettingsHost, StringComparison.Ordinal);
Assert.Contains("\"저장됨 · 재시작 필요\"", SettingsHost,
StringComparison.Ordinal);
Assert.Contains("\"초기화됨 · 재시작 필요\"", SettingsHost,
StringComparison.Ordinal);
Assert.Contains("\"사용 가능\"", SettingsHost, StringComparison.Ordinal);
Assert.DoesNotContain("외부 영상", SettingsHost, StringComparison.Ordinal);
Assert.DoesNotContain("보호되는 운영 설정", Markup, StringComparison.Ordinal);
Assert.DoesNotContain("DB 자격 증명", Markup, StringComparison.Ordinal);
Assert.DoesNotContain("Tornado2 송출 대상", Markup, StringComparison.Ordinal);
Assert.DoesNotContain("Live/DryRun 권한", Markup, StringComparison.Ordinal);
Assert.DoesNotContain("장면 허용 목록", Markup, StringComparison.Ordinal);
}
private static string Slice(string source, string startMarker, string endMarker)

View File

@@ -0,0 +1,191 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyPackageAppearanceSmokeContractTests
{
private static readonly string Root = FindRepositoryRoot();
private static readonly string Wrapper = Read(
"scripts", "Invoke-LegacyPackageAppearanceSmoke.ps1");
private static readonly string Harness = Read(
"scripts", "Test-LegacyPackageAppearanceSmoke.mjs");
[Fact]
public void Dedicated_runner_reuses_exact_debug_package_dryrun_and_close_guards()
{
Assert.Contains("-BuildFlavor DebugAppX", Wrapper, StringComparison.Ordinal);
Assert.Contains("-DefinitionsOnly", Wrapper, StringComparison.Ordinal);
Assert.Contains("Get-ExactDevelopmentPackage", Wrapper, StringComparison.Ordinal);
Assert.Contains("Assert-LocalConfigurationIsDryRun", Wrapper,
StringComparison.Ordinal);
Assert.Contains("Assert-NoTornadoConnection", Wrapper, StringComparison.Ordinal);
Assert.Contains("Start-ExactAppearanceApplication $registration 1", Wrapper,
StringComparison.Ordinal);
Assert.Contains("Start-ExactAppearanceApplication $registration 2", Wrapper,
StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(Wrapper, "Close-ExactAppearanceApplication $"));
Assert.DoesNotContain("Stop-Process", Wrapper,
StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("taskkill", Wrapper,
StringComparison.OrdinalIgnoreCase);
Assert.DoesNotMatch(@"(?i)C:\\Users\\[^\\]+", Wrapper);
}
[Fact]
public void Three_selects_use_trusted_windows_mouse_and_keyboard_once_per_phase()
{
Assert.Contains("SendApplicationClick(", Wrapper, StringComparison.Ordinal);
Assert.Contains("SendClosedSelectKeys(", Wrapper, StringComparison.Ordinal);
Assert.Contains("[FieldOffset(0)] public MouseInput Mouse", Wrapper,
StringComparison.Ordinal);
Assert.Contains("[FieldOffset(0)] public KeyboardInput Keyboard", Wrapper,
StringComparison.Ordinal);
Assert.Contains("[FieldOffset(0)] public HardwareInput Hardware", Wrapper,
StringComparison.Ordinal);
Assert.Contains("IntPtr.Size == 8 ? 40 : 28", Wrapper,
StringComparison.Ordinal);
Assert.Contains("inputSize != ExpectedInputStructureBytes()", Wrapper,
StringComparison.Ordinal);
Assert.Contains("event.isTrusted === true", Harness, StringComparison.Ordinal);
Assert.Contains("trustedChange: true", Harness, StringComparison.Ordinal);
Assert.Contains("physicalKeyCounts:", Harness, StringComparison.Ordinal);
Assert.Contains("home: ack.homeKeyCalls", Harness, StringComparison.Ordinal);
Assert.Contains("arrowDown: ack.arrowDownKeyCalls", Harness,
StringComparison.Ordinal);
Assert.Contains("enter: ack.enterKeyCalls", Harness, StringComparison.Ordinal);
Assert.Contains("A native HTML <select> popup consumes Home/ArrowDown/Enter", Harness,
StringComparison.Ordinal);
Assert.DoesNotContain("trustedKeyDowns.length >= expectedKeyCalls", Harness,
StringComparison.Ordinal);
Assert.DoesNotContain("trustedKeyDownCount -lt", Wrapper,
StringComparison.Ordinal);
Assert.Contains("selection.physicalKeyCounts.home -ne 1", Wrapper,
StringComparison.Ordinal);
Assert.Contains("selection.physicalKeyCounts.arrowDown -ne $expectedDownCount", Wrapper,
StringComparison.Ordinal);
Assert.Contains("selection.physicalKeyCounts.enter -ne 1", Wrapper,
StringComparison.Ordinal);
Assert.Contains("exchange.physicalInput.change.isTrusted -ne $true", Wrapper,
StringComparison.Ordinal);
Assert.Contains("exchange.physicalInput.change.value -cne", Wrapper,
StringComparison.Ordinal);
Assert.Contains("exchange.physicalKeyCounts.arrowDown -ne", Wrapper,
StringComparison.Ordinal);
Assert.Contains("nextExchangeSequence !== 5", Harness,
StringComparison.Ordinal);
Assert.Contains("evidence.exchanges.length !== 4", Harness,
StringComparison.Ordinal);
Assert.Contains(
"[\"colorTheme\", \"viewMode\", \"startWorkspace\"]",
Harness,
StringComparison.Ordinal);
Assert.Contains("inputRetryCount: 0", Harness, StringComparison.Ordinal);
Assert.Contains("inputRetryCount = 0", Wrapper, StringComparison.Ordinal);
}
[Fact]
public void Harness_has_no_synthetic_input_or_appearance_bridge_post()
{
foreach (var method in new[]
{
"Input.dispatchMouseEvent",
"Input.dispatchKeyEvent",
"Input.insertText"
})
{
Assert.Contains($"\"{method}\"", Harness, StringComparison.Ordinal);
Assert.DoesNotContain($"rpc(\"{method}\"", Harness,
StringComparison.Ordinal);
}
Assert.DoesNotMatch(
@"postMessage\s*\(\s*\{\s*type:\s*['""]set-operator-appearance",
Harness);
Assert.DoesNotMatch(
@"(?:\.value\s*=(?!=)|dispatchEvent\s*\(|\.click\s*\(|localStorage\.setItem)",
Harness);
Assert.Contains(
"window.chrome.webview.postMessage({ type: \"ready\", payload: {} })",
Harness,
StringComparison.Ordinal);
Assert.Contains("outbound.length !== 1", Harness, StringComparison.Ordinal);
}
[Fact]
public void Relaunch_proves_persisted_start_behavior_before_any_settings_click()
{
Assert.Contains("persisted-appearance-after-relaunch", Harness,
StringComparison.Ordinal);
Assert.Contains("persisted start workspace behavior", Harness,
StringComparison.Ordinal);
Assert.Contains(
"snapshot.dom.activeWorkspace === expectedStartupWorkspace",
Harness,
StringComparison.Ordinal);
Assert.Contains("startWorkspaceBehaviorVerifiedBeforeInput = true", Harness,
StringComparison.Ordinal);
Assert.Contains("startWorkspaceBehaviorVerifiedBeforeInput -ne $true", Wrapper,
StringComparison.Ordinal);
Assert.Contains("persistenceVerifiedBeforeInput", Wrapper,
StringComparison.Ordinal);
}
[Fact]
public void Original_settings_have_exact_raw_recovery_for_present_and_absent_baselines()
{
Assert.Contains("Initialize-ExactBaselineRecovery", Wrapper,
StringComparison.Ordinal);
Assert.Contains("operatorSettingsOriginallyAbsent", Wrapper,
StringComparison.Ordinal);
Assert.Contains("Restore-ExactBaselineSettings", Wrapper,
StringComparison.Ordinal);
Assert.Contains("BaselineRestoreAttempted", Wrapper,
StringComparison.Ordinal);
Assert.Contains("restoration is one-shot and was already attempted", Wrapper,
StringComparison.Ordinal);
Assert.Contains("[IO.File]::Replace(", Wrapper, StringComparison.Ordinal);
Assert.Contains("[IO.File]::Delete($OperatorSettingsPath)", Wrapper,
StringComparison.Ordinal);
Assert.Contains("exactOriginalBytesRestored = $true", Wrapper,
StringComparison.Ordinal);
Assert.Contains("recoveryCopyRemoved", Wrapper, StringComparison.Ordinal);
Assert.Contains("The application was intentionally left open", Wrapper,
StringComparison.Ordinal);
Assert.Contains("no input retry, close retry,", Wrapper,
StringComparison.Ordinal);
Assert.Contains("one-shot settings restoration had an unclear or failed result", Wrapper,
StringComparison.Ordinal);
Assert.Contains("retried. Preserve the recovery evidence", Wrapper,
StringComparison.Ordinal);
}
[Fact]
public void Evidence_is_bounded_create_new_and_hash_sealed()
{
Assert.Contains("flag: \"wx\"", Harness, StringComparison.Ordinal);
Assert.Contains("$item.Length -gt 512KB", Wrapper,
StringComparison.Ordinal);
Assert.Contains("[IO.FileMode]::CreateNew", Wrapper,
StringComparison.Ordinal);
Assert.Contains("$stream.Flush($true)", Wrapper, StringComparison.Ordinal);
Assert.Contains("phaseEvidenceSha256", Wrapper, StringComparison.Ordinal);
Assert.Contains("screenshotSha256", Wrapper, StringComparison.Ordinal);
}
private static string Read(params string[] segments) =>
File.ReadAllText(Path.Combine([Root, .. segments]));
private static int CountOccurrences(string value, string token) =>
value.Split(token, StringSplitOptions.None).Length - 1;
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

@@ -73,6 +73,107 @@ public sealed class LegacyPackageInputSmokeContractTests
}
}
[Fact]
public void PhysicalPointerCoordinatesSupportDpiAwareAndVirtualizedWindowsHosts()
{
var geometry = Slice(
Runner,
"private static ClientGeometry ReadAndAssertClientGeometry(",
"private static void AssertStableClientGeometry(");
foreach (var token in new[]
{
"bool deviceMatches =",
"bool logicalMatches =",
"if (!deviceMatches && !logicalMatches)",
"double cssScaleX = client.Right / viewportCssWidth;",
"double cssScaleY = client.Bottom / viewportCssHeight;",
"CssScaleX = cssScaleX",
"CssScaleY = cssScaleY"
})
{
Assert.Contains(token, geometry, StringComparison.Ordinal);
}
var conversion = Slice(
Runner,
"private static Point CssPointToScreen(",
"private static void SendMousePath(");
Assert.Contains("cssX * geometry.CssScaleX", conversion,
StringComparison.Ordinal);
Assert.Contains("cssY * geometry.CssScaleY", conversion,
StringComparison.Ordinal);
Assert.DoesNotContain("cssX * devicePixelRatio", conversion,
StringComparison.Ordinal);
Assert.DoesNotContain("cssY * devicePixelRatio", conversion,
StringComparison.Ordinal);
var drag = Slice(
Runner,
"public static void SendRowHeaderDragAfterThenHome(",
"public static void SendShiftClickRangeThenDeleteAndRestore(");
Assert.Contains("12.0 * baseline.CssScaleX", drag,
StringComparison.Ordinal);
Assert.DoesNotContain("12.0 * devicePixelRatio", drag,
StringComparison.Ordinal);
}
[Fact]
public void WorkspaceSplitterUsesThePointerPipelinePersistsAndRestoresItsWidth()
{
var splitter = Slice(
Harness,
"function assertSplitterResizeResult(",
"async function ensureWorkspace(");
AssertInOrder(
splitter,
"await pressMouse(start, 0);",
"await moveMouse(target, 0, 1);",
"await releaseMouse(target, 0);",
"async function exerciseWorkspaceSplitter(baseline)",
"dragWorkspaceSplitterToPercent(",
"pressKey(keyboardKey, \"resize workspace splitter from keyboard\")",
"const restoredLayout = await dragWorkspaceSplitterToPercent(",
"assertFixedSchedule(baseline, restoredLayout",
"storedPercent: Number.parseFloat(restoredLayout.storedScheduleWidth)");
Assert.Contains("splitterState?.valueNow", splitter,
StringComparison.Ordinal);
Assert.Matches(
@"Math\.abs\(layout\.splitterState\.valueNow -\s*" +
@"Number\(layout\.scheduleWidthPercent\.toFixed\(1\)\)\) > 0\.001",
splitter);
Assert.DoesNotMatch(
@"splitterState\??\.valueNow\s*!==\s*" +
@"Math\.round\(layout\.scheduleWidthPercent\)",
splitter);
Assert.Contains("outboundMessages.length", splitter,
StringComparison.Ordinal);
Assert.DoesNotContain("Input.dispatchMouseEvent", splitter,
StringComparison.Ordinal);
Assert.DoesNotContain(".click(", splitter, StringComparison.Ordinal);
Assert.DoesNotContain("localStorage.setItem", splitter,
StringComparison.Ordinal);
Assert.DoesNotContain("style.setProperty", splitter,
StringComparison.Ordinal);
Assert.Contains(
"ArrowLeft: { key: \"ArrowLeft\", code: \"ArrowLeft\", virtualKey: 37 }",
Harness,
StringComparison.Ordinal);
Assert.Contains(
"ArrowRight: { key: \"ArrowRight\", code: \"ArrowRight\", virtualKey: 39 }",
Harness,
StringComparison.Ordinal);
var sequence = Slice(
Harness,
"async function verifyWorkspaceNavigationAndLayout(preflight)",
"async function playlistDropGeometry(label)");
AssertInOrder(
sequence,
"const splitterResize = await exerciseWorkspaceSplitter(baseline);",
"collapse workspace menu",
"splitterResize,");
}
[Fact]
public void DryRunFinalSealKeepsWindowsDragOrderAndRequiresTheSafeTailAppend()
{
@@ -846,6 +947,11 @@ public sealed class LegacyPackageInputSmokeContractTests
Assert.Contains("[IO.Compression.ZipFile]::OpenRead($msix)", guard,
StringComparison.Ordinal);
Assert.Contains("'Web/app.js'", guard, StringComparison.Ordinal);
Assert.Contains("'Web/index.html'", guard, StringComparison.Ordinal);
Assert.Contains("'Web/playout-ui.js'", guard, StringComparison.Ordinal);
Assert.Contains("'Web/styles.css'", guard, StringComparison.Ordinal);
Assert.Contains("'Web/workspace-navigation.js'", guard,
StringComparison.Ordinal);
Assert.Contains("Get-StreamSha256Hex $entryStream", guard,
StringComparison.Ordinal);
Assert.Contains("registered Debug AppX is stale", guard,

View File

@@ -81,7 +81,7 @@ public sealed class LegacyPackageLocalStateSmokeContractTests
}
[Fact]
public void Comparison_import_requires_exact_source_one_row_baseline_and_idempotence()
public void Comparison_import_requires_exact_source_known_baseline_and_idempotence()
{
Assert.Contains(
"1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2",
@@ -91,10 +91,27 @@ public sealed class LegacyPackageLocalStateSmokeContractTests
"13C07BA64587549EDA9C1A694F3DFA19875CB1F888F02C9E071F17C47739E087",
Wrapper,
StringComparison.Ordinal);
Assert.Contains("receipt?.addedPairCount === 8", Harness, StringComparison.Ordinal);
Assert.Contains(
"2575C2420800670B0022EBEE819BA3D9640DB14E9552426DF46075D079F1F697",
Wrapper,
StringComparison.Ordinal);
Assert.Contains("baselineMode === \"fresh\" ? 8 : 0", Harness,
StringComparison.Ordinal);
Assert.Contains("baselineMode === \"fresh\" ? 0 : 8", Harness,
StringComparison.Ordinal);
Assert.Contains("receipt?.addedPairCount === expectedFirstAdded", Harness,
StringComparison.Ordinal);
Assert.Contains("receipt?.skippedDuplicateCount === expectedFirstSkipped", Harness,
StringComparison.Ordinal);
Assert.Contains("receipt?.addedPairCount === 0", Harness, StringComparison.Ordinal);
Assert.Contains("receipt?.skippedDuplicateCount === 8", Harness,
StringComparison.Ordinal);
Assert.Contains(
"initialDestinationMode = isFresh ? \"fresh\" : \"alreadyImported\"",
Harness,
StringComparison.Ordinal);
Assert.Contains("$script:DestinationBaselineMode = if ($isFresh)", Wrapper,
StringComparison.Ordinal);
Assert.Equal(
2,
CountOccurrences(

View File

@@ -134,6 +134,46 @@ public sealed class LegacyPlayoutWebContractTests
Assert.Contains("마지막 항목", Script, StringComparison.Ordinal);
}
[Fact]
public void StatusStateUsesFailSafePriorityAndIsReassignedOnEveryRender()
{
var resolver = ExtractFunction(Script, "function statusState(playout, busy)");
foreach (var state in new[]
{
"outcome-unknown",
"busy",
"error",
"program",
"prepared",
"idle"
})
{
Assert.Contains($"\"{state}\"", resolver, StringComparison.Ordinal);
}
Assert.True(
resolver.IndexOf("playout.outcomeUnknown === true", StringComparison.Ordinal) <
resolver.IndexOf("if (busy)", StringComparison.Ordinal),
"OutcomeUnknown must remain visually dominant while a command is busy.");
Assert.True(
resolver.IndexOf("if (busy)", StringComparison.Ordinal) <
resolver.IndexOf("playout.mode === \"disabled\"", StringComparison.Ordinal),
"Busy must win over an unavailable engine after OutcomeUnknown is excluded.");
Assert.True(
resolver.IndexOf("playout.mode === \"disabled\"", StringComparison.Ordinal) <
resolver.IndexOf("playout.phase === \"program\"", StringComparison.Ordinal),
"Engine errors must win over ordinary phase styling.");
Assert.Contains(
"status.dataset.state = statusState(playout, busy);",
Script,
StringComparison.Ordinal);
Assert.Contains(
"status.dataset.state = \"error\";",
Script,
StringComparison.Ordinal);
}
private static int CountOccurrences(string source, string value)
{
var count = 0;
@@ -156,6 +196,28 @@ public sealed class LegacyPlayoutWebContractTests
return source[start..(end + closingTag.Length)];
}
private static string ExtractFunction(string source, string signature)
{
var start = source.IndexOf(signature, StringComparison.Ordinal);
Assert.True(start >= 0, $"Function '{signature}' was not found.");
var bodyStart = source.IndexOf('{', start);
Assert.True(bodyStart >= 0, $"Function '{signature}' has no body.");
var depth = 0;
for (var index = bodyStart; index < source.Length; index++)
{
if (source[index] == '{')
{
depth++;
}
else if (source[index] == '}' && --depth == 0)
{
return source[start..(index + 1)];
}
}
throw new InvalidDataException($"Function '{signature}' is incomplete.");
}
private static string FindRepositoryRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);

View File

@@ -13,6 +13,8 @@ public sealed class LegacyWorkspaceLayoutContractTests
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"));
private static readonly string Playout = File.ReadAllText(
Path.Combine(WebRoot, "playout-ui.js"));
private static readonly string Navigation = File.ReadAllText(
Path.Combine(WebRoot, "workspace-navigation.js"));
@@ -26,6 +28,7 @@ public sealed class LegacyWorkspaceLayoutContractTests
var stockStart = Markup.IndexOf("id=\"stock-workspace\"", StringComparison.Ordinal);
var catalogStart = Markup.IndexOf("id=\"catalog-workspace\"", StringComparison.Ordinal);
var settingsStart = Markup.IndexOf("id=\"settings-workspace\"", StringComparison.Ordinal);
var splitterStart = Markup.IndexOf("id=\"workspace-splitter\"", StringComparison.Ordinal);
var scheduleStart = Markup.IndexOf("class=\"playlist-panel\"", StringComparison.Ordinal);
Assert.True(workspaceStart >= 0);
@@ -35,14 +38,27 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.True(stockStart > settingsTabStart);
Assert.True(catalogStart > stockStart);
Assert.True(settingsStart > catalogStart);
Assert.True(scheduleStart > settingsStart);
Assert.True(splitterStart > settingsStart);
Assert.True(scheduleStart > splitterStart);
Assert.Matches(
@"<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*<section class=""playlist-panel""",
@"<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*" +
@"<div id=""workspace-splitter""[^>]*><\/div>\s*" +
@"<section class=""playlist-panel""",
Markup);
Assert.Matches(
@"id=""workspace-splitter""[^>]*class=""workspace-splitter""" +
@"[^>]*role=""separator""" +
@"[^>]*aria-label=""작업 영역과 송출 스케줄 너비 조절""" +
@"[^>]*aria-orientation=""vertical""" +
@"[^>]*aria-controls=""workspace-region""" +
@"[^>]*aria-valuemin=""34""[^>]*aria-valuemax=""48""" +
@"[^>]*aria-valuenow=""39""[^>]*tabindex=""0""",
Markup);
Assert.Matches("id=\"catalog-workspace\"[^>]*hidden inert", Markup);
Assert.Matches("id=\"settings-workspace\"[^>]*hidden inert", Markup);
Assert.Contains("<small>항상 표시</small><h2>송출 스케줄</h2>", Markup,
Assert.Contains("<div><h2>송출 스케줄</h2></div>", Markup,
StringComparison.Ordinal);
Assert.DoesNotContain("항상 표시", Markup, StringComparison.Ordinal);
}
[Fact]
@@ -68,12 +84,27 @@ public sealed class LegacyWorkspaceLayoutContractTests
}
[Fact]
public void WorkAndScheduleGiveTheAlwaysVisibleScheduleAWiderResponsiveShare()
public void WorkAndScheduleExposeAnAccessibleResizableThreeColumnLayout()
{
Assert.Contains(
"grid-template-columns: minmax(900px, 1.75fr) minmax(600px, 1fr);",
Styles,
Assert.Matches(
@"\.operator-shell\s*\{[^}]*grid-template-columns:\s*" +
@"minmax\(780px,\s*1fr\)\s+var\(--workspace-splitter-width\)\s+" +
@"minmax\(600px,\s*var\(--schedule-pane-width,[^)]+\)\)",
Styles);
Assert.Matches(@"--workspace-splitter-width:\s*8px", Styles);
Assert.Matches(
@"\.workspace-splitter\s*\{[^}]*cursor:\s*col-resize" +
@"[^}]*touch-action:\s*none",
Styles);
Assert.Matches(
@"body\.resizing-workspace\s*\{[^}]*cursor:\s*col-resize" +
@"[^}]*user-select:\s*none",
Styles);
Assert.Contains(".workspace-splitter:focus-visible", Styles,
StringComparison.Ordinal);
Assert.Matches(
@"@media \(forced-colors:\s*active\)[\s\S]*?\.workspace-splitter",
Styles);
Assert.Contains(
"grid-template-rows: 66px minmax(0, 1fr) auto;",
Styles,
@@ -116,12 +147,16 @@ public sealed class LegacyWorkspaceLayoutContractTests
}
[Fact]
public void ModernOperatorThemePreservesAccessibleAndSemanticStates()
public void UnifiedOperatorThemePreservesAccessibleAndSemanticStates()
{
Assert.Contains(
"Modern operator theme: visual-only overrides; geometry and native contracts stay intact",
"Unified operator theme: visual-only rules; geometry and native contracts stay intact.",
Styles,
StringComparison.Ordinal);
Assert.Contains("Compact broadcast-console components.", Styles,
StringComparison.Ordinal);
Assert.Single(
Regex.Matches(Styles, @"(?m)^:root\s*\{").Cast<Match>());
Assert.Contains("--ui-canvas:", Styles, StringComparison.Ordinal);
Assert.Contains("--ui-accent:", Styles, StringComparison.Ordinal);
Assert.Contains("--ui-success:", Styles, StringComparison.Ordinal);
@@ -139,10 +174,6 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.Contains(".playout-actions .take-in:not(:disabled)", Styles, StringComparison.Ordinal);
Assert.Contains(".playout-actions .next:not(:disabled)", Styles, StringComparison.Ordinal);
Assert.Contains(".playout-actions .take-out:not(:disabled)", Styles, StringComparison.Ordinal);
Assert.Contains(
"Contemporary density refinement: keep operator geometry, clarify visual hierarchy",
Styles,
StringComparison.Ordinal);
Assert.Contains(".stock-result.selected", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-heading-count", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-rows:empty::after", Styles, StringComparison.Ordinal);
@@ -188,6 +219,84 @@ public sealed class LegacyWorkspaceLayoutContractTests
StringComparison.Ordinal);
}
[Fact]
public void ScheduleHeaderAndToolbarExposeClearStatesAndExistingShortcuts()
{
Assert.Matches(
@"class=""playlist-state-legend""[^>]*aria-label=""스케줄 상태""[\s\S]*?" +
@"class=""legend-selected""[\s\S]*?>선택</span>[\s\S]*?" +
@"class=""legend-next""[\s\S]*?>다음</span>[\s\S]*?" +
@"class=""legend-on-air""[\s\S]*?>ON AIR</span>",
Markup);
Assert.Contains(".playlist-state-legend .legend-selected", Styles,
StringComparison.Ordinal);
Assert.Contains(".playlist-state-legend .legend-next", Styles,
StringComparison.Ordinal);
Assert.Contains(".playlist-state-legend .legend-on-air", Styles,
StringComparison.Ordinal);
var toolbar = Slice(
Markup,
"<div class=\"playlist-toolbar\">",
"<div id=\"playlist-drop-zone\" class=\"playlist-grid\"");
foreach (var group in new[] { "표시", "순서", "배경", "DB", "삭제" })
{
Assert.Matches(
$"class=\"playlist-toolbar-group[^\"]*\"[^>]*aria-label=\"{group}\"",
toolbar);
}
Assert.Contains("id=\"moving-average-5\"", toolbar, StringComparison.Ordinal);
Assert.Contains("id=\"moving-average-20\"", toolbar, StringComparison.Ordinal);
Assert.Contains("id=\"playlist-move-up\"", toolbar, StringComparison.Ordinal);
Assert.Contains("id=\"playlist-move-down\"", toolbar, StringComparison.Ordinal);
Assert.Matches("id=\"playout-background-none\"[^>]*aria-keyshortcuts=\"F3\"", toolbar);
Assert.Matches("id=\"playout-background-file\"[^>]*aria-keyshortcuts=\"F2\"", toolbar);
Assert.Matches("id=\"playlist-delete-selected\"[^>]*aria-keyshortcuts=\"Delete\"", toolbar);
Assert.Contains("id=\"db-load-button\"", toolbar, StringComparison.Ordinal);
Assert.Contains("id=\"db-save-button\"", toolbar, StringComparison.Ordinal);
Assert.Contains(".playlist-toolbar-group", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-toolbar-background", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-toolbar-danger", Styles, StringComparison.Ordinal);
Assert.Contains("aria-keyshortcuts=\"F8\"", Markup, StringComparison.Ordinal);
Assert.Contains("aria-keyshortcuts=\"Escape\"", Markup, StringComparison.Ordinal);
}
[Fact]
public void SearchBusyAndEmptyFeedbackRemainAccessibleAndNonBlocking()
{
Assert.Matches(
@"id=""stock-search-meta""[^>]*role=""status""[^>]*aria-live=""polite""" +
@"[^>]*aria-atomic=""true""[^>]*hidden",
Markup);
Assert.Contains(".search-meta", Styles, StringComparison.Ordinal);
Assert.Contains(".stock-results:empty[data-empty-label]::after", Styles,
StringComparison.Ordinal);
Assert.Contains(".cut-list:empty[data-empty-label]::after", Styles,
StringComparison.Ordinal);
Assert.Contains("content: attr(data-empty-label);", Styles, StringComparison.Ordinal);
Assert.Contains("body.busy::before", Styles, StringComparison.Ordinal);
Assert.Contains("@keyframes operator-progress", Styles, StringComparison.Ordinal);
Assert.Contains("body.busy .operator-shell { pointer-events: none; }", Styles,
StringComparison.Ordinal);
Assert.Contains("function renderStockSearchMeta(state)", App,
StringComparison.Ordinal);
Assert.Contains("label = \"검색 중\";", App, StringComparison.Ordinal);
Assert.Contains("stockResults.dataset.emptyLabel = \"결과 없음\";", App,
StringComparison.Ordinal);
Assert.Contains("? \"컷 없음\"", App, StringComparison.Ordinal);
Assert.Contains(": \"종목을 선택하세요\"", App, StringComparison.Ordinal);
Assert.Contains("status.textContent = isBusy ? \"처리 중\"", App,
StringComparison.Ordinal);
Assert.Contains("status.dataset.state = statusState(playout, busy);", Playout,
StringComparison.Ordinal);
Assert.Contains("return \"outcome-unknown\";", Playout, StringComparison.Ordinal);
Assert.Contains(".playout-status[data-state=\"outcome-unknown\"]", Styles,
StringComparison.Ordinal);
}
[Fact]
public void WorkspaceHeaderUsesModernSemanticLockupAndStableNativeStatusTarget()
{
@@ -198,23 +307,21 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.Matches(
@"class=""workspace-heading""[\s\S]*?class=""workspace-heading-icon""" +
@"[\s\S]*?<div class=""workspace-heading-copy"">[\s\S]*?" +
@"<small>현재 작업</small>[\s\S]*?id=""workspace-title""",
Markup);
Assert.Matches(
@"class=""playout-connection-summary""[\s\S]*?<small>송출 연결</small>" +
@"[\s\S]*?<strong>Tornado2</strong>[\s\S]*?id=""playout-connection-state""",
@"<h1 id=""workspace-title"">종목·컷</h1>",
Markup);
var header = Slice(Markup, "<header class=\"workspace-header\">", "</header>");
Assert.DoesNotContain("<small>", header, StringComparison.Ordinal);
Assert.DoesNotContain("playout-connection-copy", header, StringComparison.Ordinal);
Assert.DoesNotContain("<strong>Tornado2</strong>", header, StringComparison.Ordinal);
Assert.Matches(
@"id=""playout-connection-state""[^>]*role=""status""" +
@"[^>]*aria-label=""Tornado2 연결 상태""[^>]*aria-live=""polite""" +
@"[^>]*aria-atomic=""true"">미연결</div>",
Markup);
Assert.Contains(".workspace-header::before", Styles, StringComparison.Ordinal);
Assert.Contains(".playlist-heading::before", Styles, StringComparison.Ordinal);
Assert.Contains(".connection-state::before", Styles, StringComparison.Ordinal);
Assert.Matches(
@"\.playout-connection-copy small\s*\{[^}]*color:\s*var\(--ui-text-muted\)",
@"\.workspace-header::before,\s*\.playlist-heading::before\s*\{\s*display:\s*none;\s*\}",
Styles);
Assert.Contains(".connection-state::before", Styles, StringComparison.Ordinal);
Assert.Contains(".connection-state.connected::before", Styles, StringComparison.Ordinal);
Assert.Contains(
".connection-state.dry-run:not(.outcome-unknown)::before",
@@ -222,12 +329,12 @@ public sealed class LegacyWorkspaceLayoutContractTests
StringComparison.Ordinal);
Assert.Contains(
"if (connection.textContent !== nextConnectionText)",
File.ReadAllText(Path.Combine(WebRoot, "playout-ui.js")),
Playout,
StringComparison.Ordinal);
}
[Fact]
public void NavigationChangesPresentationWithoutNativeOrStoredState()
public void NavigationStoresOnlyApprovedLocalPresentationState()
{
Assert.Contains("entry.view.hidden = !isCurrent", Navigation, StringComparison.Ordinal);
Assert.Contains(
@@ -254,8 +361,143 @@ public sealed class LegacyWorkspaceLayoutContractTests
Assert.Contains("LegacyWorkspaceNavigation", Navigation, StringComparison.Ordinal);
Assert.Contains("nav-collapsed", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("postMessage", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("localStorage", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("sessionStorage", Navigation, StringComparison.Ordinal);
var storageKeys = Regex.Matches(
Navigation,
"\"(mbn-stock-webview\\.[^\"]+)\"")
.Select(match => match.Groups[1].Value)
.ToArray();
Assert.Equal(
new[]
{
"mbn-stock-webview.last-workspace.v1",
"mbn-stock-webview.schedule-width.v1"
},
storageKeys);
Assert.Contains("window.localStorage && window.localStorage.getItem(key)", Navigation,
StringComparison.Ordinal);
Assert.Contains("window.localStorage.setItem(key, value)", Navigation,
StringComparison.Ordinal);
}
[Fact]
public void SplitterClampsPointerAndKeyboardResizingAndEscapeRestoresTheGesture()
{
foreach (var declaration in new[]
{
"const scheduleMinimumPercent = 34",
"const scheduleMaximumPercent = 48",
"const workspaceMinimumPixels = 780",
"const scheduleMinimumPixels = 600",
"const splitterPixels = 8"
})
{
Assert.Contains(declaration, Navigation, StringComparison.Ordinal);
}
Assert.Matches(
@"const minimum = Math\.max\([\s\S]*?scheduleMinimumPercent," +
@"[\s\S]*?scheduleMinimumPixels / width \* 100\)",
Navigation);
Assert.Matches(
@"const maximum = Math\.min\([\s\S]*?scheduleMaximumPercent," +
@"[\s\S]*?\(width - workspaceMinimumPixels - splitterPixels\) / width \* 100\)",
Navigation);
Assert.Contains(
"return Math.min(bounds.maximum, Math.max(bounds.minimum, value))",
Navigation,
StringComparison.Ordinal);
Assert.Contains(
"shell.style.setProperty(\"--schedule-pane-width\", serialized)",
Navigation,
StringComparison.Ordinal);
foreach (var attribute in new[]
{
"aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext"
})
{
Assert.Contains($"splitter.setAttribute(\"{attribute}\"", Navigation,
StringComparison.Ordinal);
}
Assert.Contains(
"splitter.setAttribute(\"aria-valuemin\", bounds.minimum.toFixed(1))",
Navigation,
StringComparison.Ordinal);
Assert.Contains(
"splitter.setAttribute(\"aria-valuemax\", bounds.maximum.toFixed(1))",
Navigation,
StringComparison.Ordinal);
Assert.Contains(
"splitter.setAttribute(\"aria-valuenow\", scheduleWidthPercent.toFixed(1))",
Navigation,
StringComparison.Ordinal);
Assert.DoesNotMatch(
@"aria-valuemin""[\s\S]{0,80}Math\.ceil|" +
@"aria-valuemax""[\s\S]{0,80}Math\.floor",
Navigation);
Assert.Contains(
"event.button !== 0 || event.isPrimary === false || splitterGesture",
Navigation,
StringComparison.Ordinal);
Assert.Contains("splitter.setPointerCapture?.(event.pointerId)", Navigation,
StringComparison.Ordinal);
foreach (var eventName in new[]
{
"pointerdown", "pointermove", "pointerup", "pointercancel",
"lostpointercapture"
})
{
Assert.Contains($"splitter.addEventListener(\"{eventName}\"", Navigation,
StringComparison.Ordinal);
}
Assert.Matches(
@"event\.key === ""Escape"" && splitterGesture[\s\S]*?" +
@"event\.stopImmediatePropagation\(\)[\s\S]*?finishSplitterGesture\(true\)",
Navigation);
Assert.Contains("event.key === \"ArrowLeft\") next += 2", Navigation,
StringComparison.Ordinal);
Assert.Contains("event.key === \"ArrowRight\") next -= 2", Navigation,
StringComparison.Ordinal);
Assert.Contains("event.key === \"Home\") next = numericScheduleBounds().minimum",
Navigation,
StringComparison.Ordinal);
Assert.Contains("event.key === \"End\") next = numericScheduleBounds().maximum",
Navigation,
StringComparison.Ordinal);
Assert.Matches(
@"applyScheduleWidth\(\s*restore === true \? gesture\.startPercent" +
@" : scheduleWidthPercent,[\s\S]*?restore !== true\)",
Navigation);
}
[Fact]
public void ExplicitMenuInputWinsOverDeferredLastWorkspaceRestore()
{
Assert.Matches(
@"stockTab\.addEventListener\(""click"", function \(\) \{\s*" +
@"startPreferenceApplied = true;\s*setWorkspace\(""stock""\);\s*\}\)",
Navigation);
Assert.Matches(
@"settingsTab\.addEventListener\(""click"", function \(\) \{\s*" +
@"startPreferenceApplied = true;\s*setWorkspace\(""settings""\);\s*\}\)",
Navigation);
Assert.Matches(
@"marketTabs\.addEventListener\(""click"", function \(event\) \{" +
@"[\s\S]*?if \(!button \|\| !marketTabs\.contains\(button\)\) return;\s*" +
@"startPreferenceApplied = true;\s*syncMarket\(button\.dataset\.tabId\);\s*" +
@"setWorkspace\(""catalog""\);",
Navigation);
}
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()