]*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()
{
Assert.Matches(
@"class=""brand""[\s\S]*?class=""brand-wordmark-plate""[\s\S]*?" +
@"
![]()
[\s\S]*?" +
@"class=""brand-divider"" aria-hidden=""true""[\s\S]*?" +
@"class=""brand-product"">V-STOCK",
Markup);
Assert.DoesNotContain("class=\"brand-symbol\"", Markup, StringComparison.Ordinal);
Assert.DoesNotContain("class=\"brand-lockup\"", Markup, StringComparison.Ordinal);
Assert.DoesNotContain(">VRi<", Markup, StringComparison.Ordinal);
Assert.Matches(
@"class=""workspace-heading""[\s\S]*?class=""workspace-heading-icon""" +
@"[\s\S]*?
[\s\S]*?" +
@"
종목·컷
",
Markup);
var header = Slice(Markup, "");
Assert.DoesNotContain("", header, StringComparison.Ordinal);
Assert.DoesNotContain("playout-connection-copy", header, StringComparison.Ordinal);
Assert.DoesNotContain("Tornado2", header, StringComparison.Ordinal);
Assert.Matches(
@"id=""playout-connection-state""[^>]*role=""status""" +
@"[^>]*aria-label=""Tornado2 연결 상태""[^>]*aria-live=""polite""" +
@"[^>]*aria-atomic=""true"">미연결",
Markup);
Assert.Matches(
@"\.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",
Styles,
StringComparison.Ordinal);
Assert.Contains(
"if (connection.textContent !== nextConnectionText)",
Playout,
StringComparison.Ordinal);
}
[Fact]
public void NavigationStoresOnlyApprovedLocalPresentationState()
{
Assert.Contains("entry.view.hidden = !isCurrent", Navigation, StringComparison.Ordinal);
Assert.Contains(
"entry.view.toggleAttribute(\"inert\", !isCurrent)",
Navigation,
StringComparison.Ordinal);
Assert.Contains("settingsTab.addEventListener(\"click\"", Navigation,
StringComparison.Ordinal);
Assert.Contains("syncMarket(button.dataset.tabId)", Navigation,
StringComparison.Ordinal);
Assert.Contains("menuButton.click()", Navigation, StringComparison.Ordinal);
Assert.Contains("pendingKeyboardMarketTabId", Navigation, StringComparison.Ordinal);
Assert.Contains("pendingButton.focus()", Navigation, StringComparison.Ordinal);
Assert.Contains("window.requestAnimationFrame(function", Navigation,
StringComparison.Ordinal);
Assert.Contains("options.authoritativeNativeRender === true", Navigation,
StringComparison.Ordinal);
Assert.Contains("hasPendingKeyboardMarketFocus", Navigation,
StringComparison.Ordinal);
Assert.Contains("document.addEventListener(\"pointerdown\"", Navigation,
StringComparison.Ordinal);
Assert.DoesNotContain("catalogWorkspace.contains(active)", Navigation,
StringComparison.Ordinal);
Assert.Contains("LegacyWorkspaceNavigation", Navigation, StringComparison.Ordinal);
Assert.Contains("nav-collapsed", Navigation, StringComparison.Ordinal);
Assert.DoesNotContain("postMessage", 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);
}
[Fact]
public void VisualCardModeReflowsOnlyGraphicActionsAndKeepsNativeIntentButtons()
{
var viewModes = Slice(
Styles,
"/* View modes affect graphic actions only.",
"/* End view-mode presentation. */");
Assert.Contains("[data-visual-card-list=\"true\"]", viewModes,
StringComparison.Ordinal);
Assert.Contains(
"@container operator-workspace (min-width: 520px)",
viewModes,
StringComparison.Ordinal);
Assert.Contains("grid-template-columns: repeat(2, minmax(0, 1fr))", viewModes,
StringComparison.Ordinal);
Assert.Contains("@container operator-workspace (min-width: 820px)", viewModes,
StringComparison.Ordinal);
Assert.Contains("grid-template-columns: repeat(3, minmax(0, 1fr))", viewModes,
StringComparison.Ordinal);
Assert.Contains("button.visual-card", viewModes, StringComparison.Ordinal);
Assert.Contains("min-height: 62px", viewModes, StringComparison.Ordinal);
Assert.DoesNotContain("grid-auto-rows: minmax(62px", viewModes,
StringComparison.Ordinal);
Assert.DoesNotContain(".visual-card-preview", viewModes,
StringComparison.Ordinal);
Assert.DoesNotContain(".visual-card-kicker", viewModes,
StringComparison.Ordinal);
Assert.DoesNotContain(".visual-card-state", viewModes,
StringComparison.Ordinal);
Assert.Matches(@"\.comparison-actions\s*\{\s*display:\s*block;", viewModes);
Assert.Contains(
"summary[data-tree-root].tree-node-selected",
viewModes,
StringComparison.Ordinal);
Assert.Contains("@container operator-workspace (max-width: 759px)", viewModes,
StringComparison.Ordinal);
Assert.DoesNotContain(".stock-result", viewModes, StringComparison.Ordinal);
Assert.DoesNotContain(".cut-row", viewModes, StringComparison.Ordinal);
Assert.DoesNotContain(".playlist-data-row", viewModes, StringComparison.Ordinal);
Assert.DoesNotContain(".operator-catalog-draft", viewModes, StringComparison.Ordinal);
Assert.DoesNotContain(".overseas-period-actions", viewModes, StringComparison.Ordinal);
Assert.Matches(@"\.industry-action-tree\s*\{\s*overflow:\s*auto;", viewModes);
Assert.Contains("function setVisualCardContent(button, options)", App,
StringComparison.Ordinal);
Assert.Contains("button.dataset.actionId = action.id", App,
StringComparison.Ordinal);
Assert.Contains("button.dataset.industryActionId = action.actionId", App,
StringComparison.Ordinal);
Assert.Contains("button.dataset.comparisonActionId = action.actionId", App,
StringComparison.Ordinal);
Assert.Contains("button.dataset.themeActionId = action.actionId", App,
StringComparison.Ordinal);
Assert.Contains("button.dataset.expertActionId = action.actionId", App,
StringComparison.Ordinal);
Assert.Contains("button.dataset.haltActionId = action.actionId", App,
StringComparison.Ordinal);
Assert.Matches(
@"button\.dataset\.overseasActionId = action\.actionId;\s*" +
@"button\.textContent = label;",
App);
}
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.");
}
}