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:
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user