feat: refine branded operator experience

Add the MBN-branded startup intro, semantic sidebar icons, and refined card-based operator visuals. Strengthen packaged UI and input smoke coverage for the updated experience.
This commit is contained in:
2026-07-24 01:26:13 +09:00
parent f43483533a
commit b050f2f06c
25 changed files with 2316 additions and 164 deletions

View File

@@ -110,9 +110,10 @@ public sealed class LegacyOperatorSettingsWebContractTests
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("<strong>그래픽 목록</strong>", 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("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);

View File

@@ -21,6 +21,10 @@ public sealed class LegacyPackageAppearanceSmokeContractTests
StringComparison.Ordinal);
Assert.Contains("Start-ExactAppearanceApplication $registration 2", Wrapper,
StringComparison.Ordinal);
Assert.Contains(
"Wait-StartupIntroDismissed $application $Registration.Executable",
Wrapper,
StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(Wrapper, "Close-ExactAppearanceApplication $"));
Assert.DoesNotContain("Stop-Process", Wrapper,
StringComparison.OrdinalIgnoreCase);
@@ -141,6 +145,13 @@ public sealed class LegacyPackageAppearanceSmokeContractTests
Assert.Contains("restoration is one-shot and was already attempted", Wrapper,
StringComparison.Ordinal);
Assert.Contains("[IO.File]::Replace(", Wrapper, StringComparison.Ordinal);
Assert.Contains("$replacementBackupPath", Wrapper, StringComparison.Ordinal);
Assert.Contains("[IO.File]::Delete($replacementBackupPath)", Wrapper,
StringComparison.Ordinal);
Assert.DoesNotContain(
"[IO.File]::Replace($temporaryPath, $OperatorSettingsPath, $null",
Wrapper,
StringComparison.Ordinal);
Assert.Contains("[IO.File]::Delete($OperatorSettingsPath)", Wrapper,
StringComparison.Ordinal);
Assert.Contains("exactOriginalBytesRestored = $true", Wrapper,
@@ -154,6 +165,10 @@ public sealed class LegacyPackageAppearanceSmokeContractTests
StringComparison.Ordinal);
Assert.Contains("retried. Preserve the recovery evidence", Wrapper,
StringComparison.Ordinal);
Assert.Contains("[IO.Path]::GetFileNameWithoutExtension($Output)", Wrapper,
StringComparison.Ordinal);
Assert.DoesNotContain("[IO.Path]::ChangeExtension($Output, $null)", Wrapper,
StringComparison.Ordinal);
}
[Fact]

View File

@@ -834,6 +834,11 @@ public sealed class LegacyPackageInputSmokeContractTests
"delete-selected-playlist-rows",
"selections[1].shift !== true",
"shiftRangeDeletedRowIds = [rangeStartRowId, rangeEndRowId]");
Assert.Contains("neutral-pointer guard", request, StringComparison.Ordinal);
Assert.Contains("await moveMouse(neutralPoint)", request,
StringComparison.Ordinal);
Assert.Contains("neutralSignature !== guardSignature", request,
StringComparison.Ordinal);
Assert.DoesNotContain("Input.dispatchMouseEvent", request,
StringComparison.Ordinal);
@@ -934,6 +939,39 @@ public sealed class LegacyPackageInputSmokeContractTests
StringComparison.Ordinal);
}
[Fact]
public void StartupIntroMustBeDismissedBeforeThePhysicalInputHarnessStarts()
{
var wait = Slice(
Runner,
"function Wait-StartupIntroDismissed",
"function Assert-NoExistingContentDialog");
Assert.Contains("[datetime]::UtcNow.AddSeconds(4)", wait,
StringComparison.Ordinal);
Assert.Contains("매일경제TV V-STOCK 시작 화면", wait,
StringComparison.Ordinal);
Assert.Contains("[Windows.Automation.PropertyCondition]::new(", wait,
StringComparison.Ordinal);
Assert.Contains("$intro.Current.IsOffscreen", wait,
StringComparison.Ordinal);
Assert.Contains("Assert-ExactApplicationStillRunning", wait,
StringComparison.Ordinal);
Assert.Contains("Assert-NoTornadoConnection", wait,
StringComparison.Ordinal);
Assert.Contains("remained visible beyond its four-second bound", wait,
StringComparison.Ordinal);
var launch = Slice(
Runner,
"$script:Phase = 'package launch'",
"$harnessRun = Invoke-NodeHarnessUnderTcpWatch");
AssertInOrder(
launch,
"Wait-ExactLoopbackListener",
"Wait-StartupIntroDismissed",
"$script:Phase = 'real input harness'");
}
[Fact]
public void DebugAppXMustMatchTheLatestMsixAndCurrentWebSourceBeforeLaunch()
{
@@ -946,7 +984,10 @@ public sealed class LegacyPackageInputSmokeContractTests
StringComparison.Ordinal);
Assert.Contains("[IO.Compression.ZipFile]::OpenRead($msix)", guard,
StringComparison.Ordinal);
Assert.Contains("'AppxManifest.xml'", guard, StringComparison.Ordinal);
Assert.Contains("'Assets/StartupHero.png'", guard, StringComparison.Ordinal);
Assert.Contains("'Web/app.js'", guard, StringComparison.Ordinal);
Assert.Contains("'Web/Brand/logo.png'", 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);
@@ -956,7 +997,20 @@ public sealed class LegacyPackageInputSmokeContractTests
StringComparison.Ordinal);
Assert.Contains("registered Debug AppX is stale", guard,
StringComparison.Ordinal);
Assert.Contains("Web\\app.js", guard, StringComparison.Ordinal);
foreach (var relativeSource in new[]
{
"Web\\app.js",
"Web\\index.html",
"Web\\styles.css",
"Web\\workspace-navigation.js"
})
{
Assert.Contains(relativeSource, guard, StringComparison.Ordinal);
}
Assert.Contains("$sourcePath = Join-Path $projectRoot $relativeSource", guard,
StringComparison.Ordinal);
Assert.Contains("$installedPath = Join-Path $InstallLocation $relativeSource", guard,
StringComparison.Ordinal);
Assert.Contains(
"Assert-DebugAppXMatchesLatestPackage ([string]$package.InstallLocation)",
Runner,

View File

@@ -107,6 +107,40 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void CustomerWordmarkIsProjectedFromTheAllowlistedLegacyAssetIntoTheTrustedWebRoot()
{
var project = XDocument.Load(ProjectPath);
var brandAsset = Assert.Single(project.Descendants("LegacyBrandAsset"));
Assert.Equal(
"$(LegacyResSourceRoot)\\logo.png",
brandAsset.Attribute("Include")?.Value);
var brandContent = Assert.Single(
project.Descendants("Content"),
element => string.Equals(
element.Attribute("Include")?.Value,
"@(LegacyBrandAsset)",
StringComparison.Ordinal));
Assert.Equal(
"Web\\Brand\\%(Filename)%(Extension)",
brandContent.Element("Link")?.Value);
Assert.Equal(
"Web\\Brand\\%(Filename)%(Extension)",
brandContent.Element("TargetPath")?.Value);
Assert.Equal(
"PreserveNewest",
brandContent.Element("CopyToOutputDirectory")?.Value);
Assert.Equal(
"PreserveNewest",
brandContent.Element("CopyToPublishDirectory")?.Value);
var packagedResNames = project.Descendants("LegacyPackagedResAsset")
.Select(element => Path.GetFileName(element.Attribute("Include")?.Value))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Contains("logo.png", packagedResNames);
}
[Fact]
public void WebViewIgnoresItsPersistentCacheBeforeExactTrustedNavigation()
{
@@ -266,6 +300,7 @@ public sealed class LegacyRuntimeAssetDeploymentContractTests
"$(LegacyCutsSourceRoot)\\%(LegacyRequiredScene.Identity).t2s",
"$(LegacyCutsSourceRoot)\\%(LegacyRequiredBuiltInAsset.Identity)",
"$(LegacyResSourceRoot)\\MmoneyCoder.ini",
"$(LegacyResSourceRoot)\\logo.png",
"$(LegacyResSourceRoot)\\종목.ini",
"$(LegacyResSourceRoot)\\업종_코스피.ini",
"$(LegacyResSourceRoot)\\업종_코스닥.ini",

View File

@@ -0,0 +1,238 @@
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacySidebarIconContractTests
{
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 Navigation = File.ReadAllText(
Path.Combine(WebRoot, "workspace-navigation.js"));
private static readonly string Styles = File.ReadAllText(
Path.Combine(WebRoot, "styles.css"));
private static readonly string PackageInputSmoke = File.ReadAllText(
Path.Combine(RepositoryRoot, "scripts", "Test-LegacyPackageInput.mjs"));
private static readonly string UiOnlySmoke = File.ReadAllText(
Path.Combine(RepositoryRoot, "scripts", "Test-LegacyPackageUiOnlySmoke.mjs"));
[Fact]
public void EverySidebarDestinationUsesOneClosedSemanticSvgIcon()
{
var mappings = new (string Anchor, string Icon)[]
{
("id=\"workspace-stock-tab\"", "stock-cut"),
("data-tab-id=\"overseas\"", "overseas"),
("data-tab-id=\"exchange\"", "exchange"),
("data-tab-id=\"index\"", "index"),
("data-tab-id=\"kospiIndustry\"", "kospi"),
("data-tab-id=\"kosdaqIndustry\"", "kosdaq"),
("data-tab-id=\"comparison\"", "comparison"),
("data-tab-id=\"theme\"", "theme"),
("data-tab-id=\"overseasStocks\"", "overseas-stocks"),
("data-tab-id=\"expert\"", "expert"),
("data-tab-id=\"tradingHalt\"", "trading-halt"),
("id=\"workspace-settings-tab\"", "settings")
};
foreach (var (anchor, icon) in mappings)
{
var button = Slice(Markup, anchor, "</button>");
Assert.Contains($"data-icon=\"{icon}\"", button, StringComparison.Ordinal);
Assert.Contains(
$"href=\"#workspace-icon-{icon}\"",
button,
StringComparison.Ordinal);
Assert.DoesNotContain("<img", button, StringComparison.OrdinalIgnoreCase);
}
var sprite = Slice(
Markup,
"<svg class=\"workspace-icon-sprite\"",
"</svg>");
Assert.DoesNotContain("<text", sprite, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("http://", sprite, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("https://", sprite, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void KospiAndKosdaqUseDistinctFontIndependentMonograms()
{
var kospi = Slice(
Markup,
"<symbol id=\"workspace-icon-kospi\"",
"</symbol>");
var kosdaq = Slice(
Markup,
"<symbol id=\"workspace-icon-kosdaq\"",
"</symbol>");
Assert.Contains(
"M4.5 18V6h4a4 4 0 0 1 0 8h-4",
kospi,
StringComparison.Ordinal);
Assert.Contains(
"M15 6h4M17 6v12M15 18h4",
kospi,
StringComparison.Ordinal);
Assert.Contains(
"M2.3 18V6h1.6c2.8 0 4.3 2.2 4.3 6",
kosdaq,
StringComparison.Ordinal);
Assert.Contains(
"M9 18l2.5-12H13l2.5 12M10 14h4.5",
kosdaq,
StringComparison.Ordinal);
Assert.Contains(
"<ellipse cx=\"19.2\" cy=\"12\" rx=\"3.1\" ry=\"6\" stroke-width=\"1.65\">",
kosdaq,
StringComparison.Ordinal);
Assert.Contains("m19.5 14.5 3.2 4", kosdaq, StringComparison.Ordinal);
Assert.DoesNotContain("<text", kospi + kosdaq, StringComparison.OrdinalIgnoreCase);
Assert.NotEqual(kospi, kosdaq);
}
[Fact]
public void HeadingClonesTheSelectedIconAndThemeStylingStaysVectorNative()
{
Assert.Contains(
"const titleIcon = document.querySelector(\".workspace-heading-icon\")",
Navigation,
StringComparison.Ordinal);
var sync = Slice(
Navigation,
"function syncHeadingIcon(button)",
"function refreshCurrentMenu()");
Assert.Contains("source.cloneNode(true)", sync, StringComparison.Ordinal);
Assert.Contains("titleIcon.replaceChildren(next)", sync, StringComparison.Ordinal);
var workspace = Slice(
Navigation,
"function setWorkspace(name)",
"function applyStartPreference(mode)");
AssertInOrder(
workspace,
"activeWorkspace = name;",
"refreshCurrentMenu();",
"syncHeadingIcon(selectedButton);",
"title.textContent");
Assert.Contains(
".workspace-icon-sprite",
Styles,
StringComparison.Ordinal);
Assert.Contains(
".workspace-nav-icon { shape-rendering: geometricPrecision; }",
Styles,
StringComparison.Ordinal);
Assert.Contains(
".workspace-nav-toggle svg, .workspace-nav-item svg",
Styles,
StringComparison.Ordinal);
Assert.Contains("stroke: currentColor;", Styles, StringComparison.Ordinal);
}
[Fact]
public void PackageSmokesProveIconsArePaintedMappedAndCentered()
{
var mappings = new (string TabId, string Icon)[]
{
("overseas", "overseas"),
("exchange", "exchange"),
("index", "index"),
("kospiIndustry", "kospi"),
("kosdaqIndustry", "kosdaq"),
("comparison", "comparison"),
("theme", "theme"),
("overseasStocks", "overseas-stocks"),
("expert", "expert"),
("tradingHalt", "trading-halt")
};
foreach (var harness in new[] { PackageInputSmoke, UiOnlySmoke })
{
foreach (var (tabId, icon) in mappings)
{
Assert.Contains(
$"{tabId}: \"{icon}\"",
harness,
StringComparison.Ordinal);
}
Assert.Contains("svg?.getBBox?.()", harness, StringComparison.Ordinal);
Assert.Contains("reference:", harness, StringComparison.Ordinal);
Assert.Contains("Number.parseFloat(style.opacity) > 0",
harness,
StringComparison.Ordinal);
Assert.Contains("style.stroke !== \"none\" || style.fill !== \"none\"",
harness,
StringComparison.Ordinal);
Assert.Contains("rendered: Boolean(", harness, StringComparison.Ordinal);
Assert.Contains("expectedHeadingIcon", harness, StringComparison.Ordinal);
}
Assert.Contains("navigationItems:", PackageInputSmoke, StringComparison.Ordinal);
Assert.Contains("iconCenterOffset:", PackageInputSmoke, StringComparison.Ordinal);
Assert.Contains("iconCenterOffsetY:", PackageInputSmoke, StringComparison.Ordinal);
Assert.Contains("item.iconCenterOffset > 1.5", PackageInputSmoke, StringComparison.Ordinal);
Assert.Contains("item.iconCenterOffsetY > 1.5",
PackageInputSmoke,
StringComparison.Ordinal);
Assert.Contains("collapsedIconCenterOffsets:", PackageInputSmoke, StringComparison.Ordinal);
Assert.Contains("tab.icon.key === baseline.marketTabs[index].icon.key",
PackageInputSmoke,
StringComparison.Ordinal);
Assert.Contains("layout.stockTabIcon.rendered === true",
UiOnlySmoke,
StringComparison.Ordinal);
Assert.Contains("layout.settingsTabIcon.rendered === true",
UiOnlySmoke,
StringComparison.Ordinal);
Assert.Contains("tab.icon.rendered === true",
UiOnlySmoke,
StringComparison.Ordinal);
}
private static string Slice(string value, string start, string end)
{
var startIndex = value.IndexOf(start, StringComparison.Ordinal);
Assert.True(startIndex >= 0, $"Required marker was not found: {start}");
var endIndex = value.IndexOf(
end,
startIndex + start.Length,
StringComparison.Ordinal);
Assert.True(endIndex > startIndex, $"Required marker was not found: {end}");
return value[startIndex..(endIndex + end.Length)];
}
private static void AssertInOrder(string value, params string[] needles)
{
var previous = -1;
foreach (var needle in needles)
{
var current = value.IndexOf(needle, previous + 1, StringComparison.Ordinal);
Assert.True(current > previous, $"Required ordered marker was not found: {needle}");
previous = current;
}
}
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 was not found.");
}
}

View File

@@ -0,0 +1,242 @@
using System.Buffers.Binary;
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
public sealed class LegacyStartupIntroNativeContractTests
{
private static readonly string RepositoryRoot = FindRepositoryRoot();
private static readonly string AppRoot = Path.Combine(
RepositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp");
private static readonly string WindowXaml = File.ReadAllText(
Path.Combine(AppRoot, "MainWindow.xaml"));
private static readonly string Window = File.ReadAllText(
Path.Combine(AppRoot, "MainWindow.xaml.cs"));
private static readonly string StartupIntro = File.ReadAllText(
Path.Combine(AppRoot, "MainWindow.StartupIntro.cs"));
private static readonly string Manifest = File.ReadAllText(
Path.Combine(AppRoot, "Package.appxmanifest"));
private static readonly string Project = File.ReadAllText(
Path.Combine(AppRoot, "MBN_STOCK_WEBVIEW.LegacyParityApp.csproj"));
[Fact]
public void IntroUsesOnlyPackagedBrandAssetsAndCompactCopy()
{
var overlay = Slice(
WindowXaml,
"x:Name=\"LoadingOverlay\"",
"<InfoBar");
Assert.Contains(
"ms-appx:///Assets/Square150x150Logo.scale-200.png",
overlay,
StringComparison.Ordinal);
Assert.Contains(
"ms-appx:///Assets/StartupHero.png",
overlay,
StringComparison.Ordinal);
Assert.Contains(
"ms-appx:///Web/Brand/logo.png",
overlay,
StringComparison.Ordinal);
Assert.Contains("V-STOCK", overlay, StringComparison.Ordinal);
Assert.Contains("증권정보 송출 시스템", overlay, StringComparison.Ordinal);
Assert.DoesNotContain("http://", overlay, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("https://", overlay, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("원본 호환 화면을 준비하고 있습니다.", overlay,
StringComparison.Ordinal);
}
[Fact]
public void GeneratedBroadcastHeroIsAProjectPackagedWidePng()
{
const string heroInclude =
"<Content Include=\"..\\..\\Assets\\StartupHero.png\" Link=\"Assets\\StartupHero.png\" />";
Assert.Contains(heroInclude, Project, StringComparison.Ordinal);
var heroPath = Path.Combine(RepositoryRoot, "Assets", "StartupHero.png");
var bytes = File.ReadAllBytes(heroPath);
Assert.True(bytes.Length > 500_000);
Assert.Equal(
new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 },
bytes[..8]);
Assert.True(bytes.Length >= 24);
var width = BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(16, 4));
var height = BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(20, 4));
Assert.True(width >= 1600);
Assert.True(height >= 900);
Assert.True((double)width / height > 1.7);
}
[Fact]
public void IntroUsesTheDeepNavyMbnAccentAndShortNativeMotion()
{
var overlay = Slice(
WindowXaml,
"x:Name=\"LoadingOverlay\"",
"<InfoBar");
Assert.Contains("#FF08182B", WindowXaml, StringComparison.OrdinalIgnoreCase);
Assert.Contains("#FFF47B20", overlay, StringComparison.OrdinalIgnoreCase);
Assert.Contains(
"x:Name=\"StartupIntroEnterStoryboard\"",
WindowXaml,
StringComparison.Ordinal);
Assert.Contains(
"x:Name=\"StartupIntroExitStoryboard\"",
WindowXaml,
StringComparison.Ordinal);
Assert.Contains("IsIndeterminate=\"True\"", overlay, StringComparison.Ordinal);
Assert.Contains(
"AutomationProperties.Name=\"매일경제TV V-STOCK 시작 화면\"",
overlay,
StringComparison.Ordinal);
Assert.Contains(
"AutomationProperties.AccessibilityView=\"Content\"",
overlay,
StringComparison.Ordinal);
}
[Fact]
public void EveryStartupOutcomeKeepsItsCancellableTwoSecondHold()
{
Assert.Contains(
"TimeSpan.FromMilliseconds(2050)",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains(
"TimeSpan.FromMilliseconds(180)",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains("UISettings", StartupIntro, StringComparison.Ordinal);
Assert.Contains(".AnimationsEnabled", StartupIntro, StringComparison.Ordinal);
Assert.Contains(
"Interlocked.CompareExchange(",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains(
"_lifetimeCancellation.Token",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains("Task.Delay(", StartupIntro, StringComparison.Ordinal);
Assert.DoesNotContain(
"skipMinimumDuration",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains(
"if (!_startupIntroAnimationsEnabled)",
StartupIntro,
StringComparison.Ordinal);
}
[Fact]
public void WebReadyPostsTheFirstStateBeforeEveryOutcomeUsesTheSameDismissal()
{
var dispatch = Slice(
Window,
"private async Task ProcessIntentAsync(",
"private static bool IsNamedPlaylistModalIntent(");
var finalPost = RequiredIndex(dispatch, "PostState(state);");
var readyDismiss = RequiredIndex(
dispatch,
"if (intent is LegacyReadyIntent)",
finalPost);
var normalDismiss = RequiredIndex(
dispatch,
"BeginDismissStartupIntro();",
readyDismiss);
Assert.True(finalPost < readyDismiss);
Assert.True(readyDismiss < normalDismiss);
var failurePost = RequiredIndex(dispatch, "PostState(failureState);");
var immediateDismiss = RequiredIndex(
dispatch,
"BeginDismissStartupIntro();",
failurePost);
Assert.True(failurePost < immediateDismiss);
var navigation = Slice(
Window,
"private void OnNavigationCompleted(",
"private static void OnNewWindowRequested(");
Assert.Contains("if (!args.IsSuccess)", navigation, StringComparison.Ordinal);
Assert.Contains(
"BeginDismissStartupIntro();",
navigation,
StringComparison.Ordinal);
Assert.DoesNotContain(
"LoadingOverlay.Visibility = Visibility.Collapsed;",
navigation,
StringComparison.Ordinal);
}
[Fact]
public void IntroBlocksInputUntilItsOneShotDismissalCompletes()
{
var browser = Slice(WindowXaml, "<WebView2", "/>");
Assert.Contains("x:Name=\"Browser\"", browser, StringComparison.Ordinal);
Assert.Contains("IsHitTestVisible=\"False\"", browser, StringComparison.Ordinal);
Assert.Contains(
"LoadingOverlay.Visibility = Visibility.Collapsed;",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains(
"LoadingOverlay.IsHitTestVisible = false;",
StartupIntro,
StringComparison.Ordinal);
Assert.Contains(
"Browser.IsHitTestVisible = true;",
StartupIntro,
StringComparison.Ordinal);
var loaded = Slice(
Window,
"private async void OnRootLoaded(",
"private void ConfigureWindow()");
Assert.Contains("StartStartupIntro();", loaded, StringComparison.Ordinal);
}
[Fact]
public void MsixSystemSplashMatchesTheNativeIntroCanvas()
{
Assert.Contains(
"BackgroundColor=\"#08182B\"",
Manifest,
StringComparison.OrdinalIgnoreCase);
Assert.Contains(
"<uap:SplashScreen Image=\"Assets\\SplashScreen.png\" />",
Manifest,
StringComparison.Ordinal);
}
private static string Slice(string value, string start, string end)
{
var startIndex = RequiredIndex(value, start);
var endIndex = RequiredIndex(value, end, startIndex + start.Length);
return value[startIndex..endIndex];
}
private static int RequiredIndex(string value, string needle, int startIndex = 0)
{
var index = value.IndexOf(needle, startIndex, StringComparison.Ordinal);
Assert.True(index >= 0, $"Required marker was not found: {needle}");
return index;
}
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 was not found.");
}
}

View File

@@ -301,9 +301,15 @@ public sealed class LegacyWorkspaceLayoutContractTests
public void WorkspaceHeaderUsesModernSemanticLockupAndStableNativeStatusTarget()
{
Assert.Matches(
@"class=""brand""[^>]*role=""img""[^>]*aria-label=""매일경제TV VRi""[\s\S]*?" +
@"class=""brand-symbol""[\s\S]*?<svg",
@"class=""brand""[\s\S]*?class=""brand-wordmark-plate""[\s\S]*?" +
@"<img class=""brand-wordmark"" src=""Brand/logo\.png"" width=""176"" height=""43""[\s\S]*?" +
@"alt=""매일경제TV"" draggable=""false"">[\s\S]*?" +
@"class=""brand-divider"" aria-hidden=""true""[\s\S]*?" +
@"class=""brand-product"">V-STOCK</span>",
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]*?<div class=""workspace-heading-copy"">[\s\S]*?" +
@@ -491,6 +497,70 @@ public sealed class LegacyWorkspaceLayoutContractTests
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);