250 lines
12 KiB
C#
250 lines
12 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
|
|
|
public sealed class LegacyWorkspaceLayoutContractTests
|
|
{
|
|
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
|
private static readonly string WebRoot = Path.Combine(
|
|
RepositoryRoot,
|
|
"src",
|
|
"MBN_STOCK_WEBVIEW.LegacyParityApp",
|
|
"Web");
|
|
private static readonly string Markup = File.ReadAllText(Path.Combine(WebRoot, "index.html"));
|
|
private static readonly string Styles = File.ReadAllText(Path.Combine(WebRoot, "styles.css"));
|
|
private static readonly string App = File.ReadAllText(Path.Combine(WebRoot, "app.js"));
|
|
private static readonly string Navigation = File.ReadAllText(
|
|
Path.Combine(WebRoot, "workspace-navigation.js"));
|
|
|
|
[Fact]
|
|
public void ShellUsesSidebarScreensBesidePersistentSchedule()
|
|
{
|
|
var workspaceStart = Markup.IndexOf("id=\"workspace-region\"", StringComparison.Ordinal);
|
|
var navigationStart = Markup.IndexOf("id=\"workspace-navigation\"", StringComparison.Ordinal);
|
|
var marketStart = Markup.IndexOf("id=\"market-tabs\"", StringComparison.Ordinal);
|
|
var settingsTabStart = Markup.IndexOf("id=\"workspace-settings-tab\"", StringComparison.Ordinal);
|
|
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 scheduleStart = Markup.IndexOf("class=\"playlist-panel\"", StringComparison.Ordinal);
|
|
|
|
Assert.True(workspaceStart >= 0);
|
|
Assert.True(navigationStart > workspaceStart);
|
|
Assert.True(marketStart > navigationStart);
|
|
Assert.True(settingsTabStart > marketStart);
|
|
Assert.True(stockStart > settingsTabStart);
|
|
Assert.True(catalogStart > stockStart);
|
|
Assert.True(settingsStart > catalogStart);
|
|
Assert.True(scheduleStart > settingsStart);
|
|
Assert.Matches(
|
|
@"<\/section>\s*<\/div>\s*<\/section>\s*<\/section>\s*<section class=""playlist-panel""",
|
|
Markup);
|
|
Assert.Matches("id=\"catalog-workspace\"[^>]*hidden inert", Markup);
|
|
Assert.Matches("id=\"settings-workspace\"[^>]*hidden inert", Markup);
|
|
Assert.Contains("<small>항상 표시</small><h2>송출 스케줄</h2>", Markup,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void SidebarOwnsEachNativeMarketButtonAndKeepsSettingsOutsideTheirOrder()
|
|
{
|
|
var marketStart = Markup.IndexOf("<div id=\"market-tabs\"", StringComparison.Ordinal);
|
|
var marketEnd = Markup.IndexOf("</div>", marketStart, StringComparison.Ordinal);
|
|
Assert.True(marketStart >= 0 && marketEnd > marketStart);
|
|
var marketMarkup = Markup[marketStart..marketEnd];
|
|
var expected = new[]
|
|
{
|
|
"overseas", "exchange", "index", "kospiIndustry", "kosdaqIndustry",
|
|
"comparison", "theme", "overseasStocks", "expert", "tradingHalt"
|
|
};
|
|
var actual = Regex.Matches(marketMarkup, "data-tab-id=\"([^\"]+)\"")
|
|
.Select(match => match.Groups[1].Value)
|
|
.ToArray();
|
|
|
|
Assert.Equal(expected, actual);
|
|
Assert.Equal(expected.Length, Regex.Matches(marketMarkup, "<button\\b").Count);
|
|
Assert.DoesNotContain("workspace-settings-tab", marketMarkup, StringComparison.Ordinal);
|
|
Assert.Contains("workspace-nav-label", marketMarkup, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void WorkAndScheduleUseResponsiveTwoToOneColumns()
|
|
{
|
|
Assert.Contains(
|
|
"grid-template-columns: minmax(900px, 2fr) minmax(600px, 1fr);",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"grid-template-rows: 66px minmax(0, 1fr) auto;",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(".workspace-nav-footer", Styles, StringComparison.Ordinal);
|
|
Assert.Contains(".workspace-navigation .market-tabs button", Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Matches(
|
|
new Regex(
|
|
@"\.playlist-toolbar\s*\{[^}]*flex-wrap:\s*wrap",
|
|
RegexOptions.CultureInvariant),
|
|
Styles);
|
|
Assert.Matches(
|
|
new Regex(
|
|
@"\.playlist-row\s*\{[^}]*min-width:\s*600px",
|
|
RegexOptions.CultureInvariant),
|
|
Styles);
|
|
}
|
|
|
|
[Fact]
|
|
public void SidebarIsOneLabelledAndFullyControlledNavigationRegion()
|
|
{
|
|
Assert.Matches(
|
|
"id=\"workspace-navigation\"[^>]*role=\"navigation\"[^>]*aria-label=",
|
|
Markup);
|
|
Assert.Matches(
|
|
"id=\"workspace-nav-toggle\"[^>]*aria-expanded=\"true\"[\\s\\S]*?" +
|
|
"aria-controls=\"workspace-nav-items workspace-nav-footer\"",
|
|
Markup);
|
|
Assert.Matches(
|
|
"id=\"workspace-nav-items\"[^>]*role=\"group\"[^>]*aria-label=",
|
|
Markup);
|
|
Assert.Matches(
|
|
"id=\"market-tabs\"[^>]*role=\"group\"[^>]*aria-label=",
|
|
Markup);
|
|
Assert.Contains(
|
|
"id=\"workspace-nav-footer\" class=\"workspace-nav-footer\"",
|
|
Markup,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ModernOperatorThemePreservesAccessibleAndSemanticStates()
|
|
{
|
|
Assert.Contains(
|
|
"Modern operator theme: visual-only overrides; geometry and native contracts stay intact",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("--ui-canvas:", Styles, StringComparison.Ordinal);
|
|
Assert.Contains("--ui-accent:", Styles, StringComparison.Ordinal);
|
|
Assert.Contains("--ui-success:", Styles, StringComparison.Ordinal);
|
|
Assert.Contains("--ui-danger:", Styles, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"font-family: \"Segoe UI Variable\", \"Segoe UI\", \"Noto Sans KR\"",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Matches(
|
|
new Regex(
|
|
@"button:focus-visible,[\s\S]*?outline:\s*2px solid var\(--ui-accent\)",
|
|
RegexOptions.CultureInvariant),
|
|
Styles);
|
|
Assert.Contains(".playlist-data-row.on-air.selected", Styles, StringComparison.Ordinal);
|
|
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(
|
|
".playlist-data-row[data-active=\"true\"]:not(.on-air)",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
".playlist-data-row[aria-selected=\"true\"]:not(.on-air)",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("@media (forced-colors: active)", Styles, StringComparison.Ordinal);
|
|
Assert.Contains("scrollbar-gutter: stable", Styles, StringComparison.Ordinal);
|
|
Assert.Contains("@media (prefers-reduced-motion: reduce)", Styles, StringComparison.Ordinal);
|
|
Assert.Contains("aria-keyshortcuts=\"F8\"", Markup, StringComparison.Ordinal);
|
|
Assert.Contains("aria-keyshortcuts=\"Escape\"", Markup, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
".connection-state.dry-run:not(.outcome-unknown)",
|
|
Styles,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("accessibleStates.push(\"현재 PGM\")", App, StringComparison.Ordinal);
|
|
Assert.Contains("accessibleStates.push(\"송출 후보\")", App, StringComparison.Ordinal);
|
|
Assert.Contains("accessibleStates.push(\"삭제 선택됨\")", App, StringComparison.Ordinal);
|
|
Assert.Matches(
|
|
new Regex(
|
|
@"id=""stock-search-button""[\s\S]*?<svg[^>]*viewBox=""0 0 24 24""",
|
|
RegexOptions.CultureInvariant),
|
|
Markup);
|
|
Assert.Matches(
|
|
new Regex(
|
|
@"class=""playlist-heading-icon""[\s\S]*?<svg[^>]*viewBox=""0 0 24 24""",
|
|
RegexOptions.CultureInvariant),
|
|
Markup);
|
|
}
|
|
|
|
[Fact]
|
|
public void WorkspaceHeaderUsesModernSemanticLockupAndStableNativeStatusTarget()
|
|
{
|
|
Assert.Matches(
|
|
@"class=""brand""[^>]*role=""img""[^>]*aria-label=""매일경제TV VRi""[\s\S]*?" +
|
|
@"class=""brand-symbol""[\s\S]*?<svg",
|
|
Markup);
|
|
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""",
|
|
Markup);
|
|
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\)",
|
|
Styles);
|
|
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)",
|
|
File.ReadAllText(Path.Combine(WebRoot, "playout-ui.js")),
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void NavigationChangesPresentationWithoutNativeOrStoredState()
|
|
{
|
|
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("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);
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|