feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -457,6 +457,46 @@ public sealed class LegacyBridgeProtocolTests
Assert.IsType<LegacyActivateTradingHaltActionIntent>(haltAction);
}
[Fact]
public void ExpertProjection_PreservesNullBuyAmountAndAvailableCompositionAction()
{
var expert = new LegacyExpertWorkflowSnapshot(
1,
new LegacyExpertSearchSnapshot(
string.Empty,
LegacyExpertRequestStatus.Ready,
[],
false,
string.Empty),
null,
new LegacyExpertPreviewSnapshot(
LegacyExpertRequestStatus.Ready,
[new LegacyExpertRecommendationView(0, "005930", "Samsung", null)],
false,
5,
1,
20,
string.Empty),
[new LegacyExpertActionView(
LegacyExpertWorkflowController.ActionId,
"Expert",
true)],
string.Empty,
LegacyOperatorStatusKind.Warning);
var snapshot = new LegacyOperatorSnapshot(
1, string.Empty, [], null, [], [], null, string.Empty,
LegacyOperatorStatusKind.Warning, Expert: expert);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var projected = document.RootElement.GetProperty("payload").GetProperty("expert");
var preview = Assert.Single(
projected.GetProperty("preview").GetProperty("items").EnumerateArray());
Assert.Equal(JsonValueKind.Null, preview.GetProperty("buyAmount").ValueKind);
Assert.True(Assert.Single(projected.GetProperty("actions").EnumerateArray())
.GetProperty("isAvailable").GetBoolean());
}
[Fact]
public void ParseExpertInitialConsonantQuery_PreservesCompatibilityJamo()
{

View File

@@ -1,3 +1,4 @@
using System.Text.Json;
using MBN_STOCK_WEBVIEW.LegacyApplication;
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
@@ -16,6 +17,7 @@ public sealed class LegacyComparisonBridgeTests
[InlineData("swap-comparison-targets", "{}", typeof(LegacySwapComparisonTargetsIntent))]
[InlineData("clear-comparison-targets", "{}", typeof(LegacyClearComparisonTargetsIntent))]
[InlineData("add-comparison-pair", "{}", typeof(LegacyAddComparisonPairIntent))]
[InlineData("import-legacy-comparison-pairs", "{}", typeof(LegacyImportComparisonPairsIntent))]
[InlineData("select-comparison-pair", "{\"rowId\":\"comparison-pair-00000001\"}", typeof(LegacySelectComparisonPairIntent))]
[InlineData("move-comparison-pair", "{\"direction\":\"up\"}", typeof(LegacyMoveComparisonPairIntent))]
[InlineData("delete-selected-comparison-pair", "{}", typeof(LegacyDeleteSelectedComparisonPairIntent))]
@@ -41,5 +43,99 @@ public sealed class LegacyComparisonBridgeTests
"{\"type\":\"move-comparison-pair\",\"payload\":{\"direction\":\"sideways\"}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"import-legacy-comparison-pairs\",\"payload\":{\"path\":\"C:\\\\untrusted.dat\"}}",
out _,
out _));
}
[Fact]
public void State_projects_truncation_and_isolated_row_diagnostics()
{
var domestic = new LegacyComparisonSearchSnapshot(
string.Empty,
LegacyComparisonSearchStatus.Ready,
Array.Empty<LegacyComparisonTargetView>(),
null,
0,
string.Empty);
var world = new LegacyComparisonSearchSnapshot(
string.Empty,
LegacyComparisonSearchStatus.Ready,
[new LegacyComparisonTargetView(
"comparison-world-0001-0001",
"ACME, INC",
"US · ACME",
LegacyComparisonTargetKind.WorldStock)
{
CanSelect = false,
UnavailableReason = "쉼표 이름은 선택할 수 없습니다."
}],
null,
1_474,
string.Empty)
{
IsTruncated = true,
IsolatedInvalidRowCount = 16,
DeferredUnsafeRowCount = 1
};
var comparison = new LegacyComparisonWorkflowSnapshot(
1,
false,
domestic,
world,
Array.Empty<LegacyComparisonTargetView>(),
null,
null,
null,
Array.Empty<LegacyComparisonSavedPairView>(),
null,
Array.Empty<LegacyComparisonActionView>(),
string.Empty,
LegacyOperatorStatusKind.Neutral)
{
CanImportLegacyPairs = true,
LastImport = new LegacyComparisonImportReceipt(
new string('A', 64),
8,
7,
1,
8,
DateTimeOffset.Parse("2026-07-22T12:00:00+09:00"))
};
var snapshot = new LegacyOperatorSnapshot(
1,
string.Empty,
Array.Empty<LegacyStockResultRow>(),
null,
Array.Empty<LegacyCutViewRow>(),
Array.Empty<LegacyOperatorPlaylistRow>(),
null,
string.Empty,
LegacyOperatorStatusKind.Neutral,
Comparison: comparison);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var projected = document.RootElement
.GetProperty("payload")
.GetProperty("comparison")
.GetProperty("worldSearch");
Assert.True(projected.GetProperty("isTruncated").GetBoolean());
Assert.Equal(16, projected.GetProperty("isolatedInvalidRowCount").GetInt32());
Assert.Equal(1, projected.GetProperty("deferredUnsafeRowCount").GetInt32());
Assert.Equal(1_474, projected.GetProperty("totalRowCount").GetInt32());
var deferred = projected.GetProperty("results")[0];
Assert.False(deferred.GetProperty("canSelect").GetBoolean());
var comparisonWire = document.RootElement
.GetProperty("payload")
.GetProperty("comparison");
Assert.True(comparisonWire.GetProperty("canImportLegacyPairs").GetBoolean());
Assert.Equal(7, comparisonWire.GetProperty("lastImport")
.GetProperty("addedPairCount").GetInt32());
Assert.Contains(
"쉼표",
deferred.GetProperty("unavailableReason").GetString(),
StringComparison.Ordinal);
}
}

View File

@@ -11,7 +11,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
{
var json = LegacyBridgeProtocol.SerializeState(
CreateSnapshot(),
interactionMetrics: new LegacyInteractionMetrics(7, 9, 96, 1.5));
interactionMetrics: new LegacyInteractionMetrics(7, 9, 96, 1.5, 725));
using var document = JsonDocument.Parse(json);
var payload = document.RootElement.GetProperty("payload");
@@ -22,6 +22,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
Assert.Equal("host-dip", metrics.GetProperty("coordinateSpace").GetString());
Assert.Equal(96u, metrics.GetProperty("sourceDpi").GetUInt32());
Assert.Equal(1.5d, metrics.GetProperty("hostRasterizationScale").GetDouble());
Assert.Equal(725, metrics.GetProperty("doubleClickTimeMilliseconds").GetInt32());
Assert.False(metrics.TryGetProperty("CutDragWidthDips", out _));
Assert.False(metrics.TryGetProperty("CutDragHeightDips", out _));
}
@@ -54,11 +55,13 @@ public sealed class LegacyInteractionMetricsBridgeTests
Assert.Equal(4, unknownDpi.CutDragWidthDips);
Assert.Equal(4, unknownDpi.CutDragHeightDips);
Assert.Equal(1.5d, unknownDpi.HostRasterizationScale);
Assert.Equal(500, unknownDpi.DoubleClickTimeMilliseconds);
var metrics = LegacyInteractionMetrics.FromPixelsAtDpi(0, -1, 192);
Assert.Equal(4, metrics.CutDragWidthDips);
Assert.Equal(4, metrics.CutDragHeightDips);
Assert.Equal(192u, metrics.SourceDpi);
Assert.Equal(500, metrics.DoubleClickTimeMilliseconds);
}
[Fact]
@@ -66,7 +69,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
{
var json = LegacyBridgeProtocol.SerializeState(
CreateSnapshot(),
interactionMetrics: new LegacyInteractionMetrics(0, -1, 0, double.NaN));
interactionMetrics: new LegacyInteractionMetrics(0, -1, 0, double.NaN, 5_001));
using var document = JsonDocument.Parse(json);
var metrics = document.RootElement
@@ -78,6 +81,7 @@ public sealed class LegacyInteractionMetricsBridgeTests
Assert.Equal("host-dip", metrics.GetProperty("coordinateSpace").GetString());
Assert.Equal(96u, metrics.GetProperty("sourceDpi").GetUInt32());
Assert.Equal(1d, metrics.GetProperty("hostRasterizationScale").GetDouble());
Assert.Equal(500, metrics.GetProperty("doubleClickTimeMilliseconds").GetInt32());
}
[Fact]
@@ -95,6 +99,22 @@ public sealed class LegacyInteractionMetricsBridgeTests
Assert.Equal("host-dip", metrics.GetProperty("coordinateSpace").GetString());
Assert.Equal(96u, metrics.GetProperty("sourceDpi").GetUInt32());
Assert.Equal(1d, metrics.GetProperty("hostRasterizationScale").GetDouble());
Assert.Equal(500, metrics.GetProperty("doubleClickTimeMilliseconds").GetInt32());
}
[Theory]
[InlineData(1)]
[InlineData(350)]
[InlineData(5_000)]
public void FromPixelsAtDpi_PreservesValidWindowsDoubleClickTime(int milliseconds)
{
var metrics = LegacyInteractionMetrics.FromPixelsAtDpi(
4,
4,
96,
doubleClickTimeMilliseconds: milliseconds);
Assert.Equal(milliseconds, metrics.DoubleClickTimeMilliseconds);
}
private static LegacyOperatorSnapshot CreateSnapshot() => new(

View File

@@ -57,6 +57,29 @@ public sealed class LegacyManualFinancialBridgeTests
out _));
}
[Fact]
public void Raw_save_preserves_legacy_strings_but_rejects_any_native_row_locator()
{
const string json =
"{\"type\":\"save-manual-financial-raw\",\"payload\":{\"record\":" +
"{\"screen\":\"sales\",\"stockName\":\"Alpha\",\"storageValues\":" +
"[\"\",\"_\",\"label_value_extra\",\" 12 \",\"\\u0001legacy\",\"\\u0000tail\"]}}}";
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
var raw = Assert.IsType<LegacySaveManualFinancialRawIntent>(parsed).Record;
Assert.Equal(
["", "_", "label_value_extra", " 12 ", "\u0001legacy", "\u0000tail"],
raw.StorageValues);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"save-manual-financial-raw\",\"payload\":{\"record\":" +
"{\"screen\":\"sales\",\"stockName\":\"Alpha\",\"storageValues\":" +
"[\"1Q_1\",\"2Q_2\",\"3Q_3\",\"4Q_4\",\"5Q_5\",\"6Q_6\"]," +
"\"rowHandle\":\"AAAAAAAAAAAAAAAAAA\"}}}",
out _,
out _));
}
[Fact]
public void CRUD_and_action_intents_accept_only_opaque_ids_or_empty_payloads()
{
@@ -97,6 +120,26 @@ public sealed class LegacyManualFinancialBridgeTests
out _));
}
[Fact]
public void Empty_GraphE_stock_search_is_a_valid_quiet_no_op_intent()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"search-manual-financial-stocks\",\"payload\":{\"stockName\":\"\"}}",
out var parsed,
out _));
Assert.Equal(
string.Empty,
Assert.IsType<LegacySearchManualFinancialStocksIntent>(parsed).StockName);
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"select-manual-financial-stock\",\"payload\":{\"resultId\":\"\"}}",
out var selection,
out _));
Assert.Equal(
string.Empty,
Assert.IsType<LegacySelectManualFinancialStockIntent>(selection).ResultId);
}
[Fact]
public void GraphE_find_and_name_sort_require_strict_generation_bound_payloads()
{
@@ -144,8 +187,21 @@ public sealed class LegacyManualFinancialBridgeTests
Assert.Equal("sales", manual.GetProperty("screen").GetString());
Assert.True(manual.GetProperty("generation").GetInt64() > 0);
Assert.Equal(0, manual.GetProperty("listRejectedItemCount").GetInt32());
Assert.True(manual.GetProperty("rows")[0].GetProperty("isDataReadable").GetBoolean());
Assert.True(manual.GetProperty("rows")[0].GetProperty("isPlayoutReady").GetBoolean());
Assert.Equal(
["Alpha", "1Q_1", "2Q_2", "3Q_3", "4Q_4", "5Q_5", "6Q_6"],
manual.GetProperty("rows")[0]
.GetProperty("previewValues")
.EnumerateArray()
.Select(static value => value.GetString()!)
.ToArray());
Assert.False(manual.GetProperty("rows")[0].TryGetProperty("rowVersion", out _));
Assert.False(manual.GetProperty("rows")[0].TryGetProperty("rowHandle", out _));
Assert.Equal("ascending", manual.GetProperty("nameSortDirection").GetString());
Assert.Equal("Alpha", manual.GetProperty("selectedRecord").GetProperty("stockName").GetString());
Assert.Equal(JsonValueKind.Null, manual.GetProperty("selectedRawRecord").ValueKind);
Assert.Equal("sales", manual.GetProperty("action").GetProperty("actionId").GetString());
Assert.False(manual.TryGetProperty("rowVersion", out _));
Assert.False(manual.TryGetProperty("tableName", out _));

View File

@@ -83,6 +83,16 @@ public sealed class LegacyManualListsBridgeTests
Assert.False(Parse("add-manual-vi-result", "{\"code\":\"005930\",\"name\":\"삼성전자\"}"));
}
[Fact]
public void VI_search_accepts_the_original_blank_full_list_request()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"search-manual-vi\",\"payload\":{\"query\":\"\"}}",
out var intent,
out _));
Assert.Equal(string.Empty, Assert.IsType<LegacySearchManualViIntent>(intent).Query);
}
[Fact]
public void State_projects_display_rows_but_not_native_codes_versions_or_paths()
{
@@ -115,7 +125,8 @@ public sealed class LegacyManualListsBridgeTests
true,
true)
{
SelectedSearchResultId = "manual-vi-result-1"
SelectedSearchResultId = "manual-vi-result-1",
SearchIsTruncated = true
};
var snapshot = new LegacyOperatorSnapshot(
1,
@@ -135,6 +146,7 @@ public sealed class LegacyManualListsBridgeTests
Assert.Equal(
"manual-vi-result-1",
projected.GetProperty("selectedSearchResultId").GetString());
Assert.True(projected.GetProperty("searchIsTruncated").GetBoolean());
Assert.True(projected.GetProperty("isOpen").GetBoolean());
Assert.Equal(9, projected.GetProperty("viItems").GetArrayLength());

View File

@@ -29,6 +29,31 @@ public sealed class LegacyOperatorCatalogBridgeTests
out _));
}
[Fact]
public void Stock_search_accepts_explicit_blank_show_all_but_rejects_unsafe_or_overlong_input()
{
var blank = Assert.IsType<LegacySearchOperatorCatalogStocksIntent>(ParseRequired(
"search-operator-catalog-stocks",
"""{"query":""}"""));
Assert.Equal(string.Empty, blank.Query);
foreach (var query in new[]
{
"bad\nvalue",
"bad\0value",
new string('A', LegacyStockSearchService.MaximumQueryLength + 1)
})
{
Assert.False(Parse(
JsonSerializer.Serialize(new
{
type = "search-operator-catalog-stocks",
payload = new { query }
}),
out _));
}
}
[Fact]
public void Catalog_selection_and_draft_changes_accept_only_opaque_ids()
{
@@ -96,6 +121,15 @@ public sealed class LegacyOperatorCatalogBridgeTests
Assert.IsType<LegacyBeginCreateThemeCatalogIntent>(ParseRequired(
"begin-create-theme-catalog",
"""{"market":"nxt"}"""));
var marketChange = Assert.IsType<LegacyChangeCreateThemeMarketIntent>(ParseRequired(
"change-create-theme-market",
"""{"market":"krx","editorName":" "}"""));
Assert.Equal(ThemeMarket.Krx, marketChange.Market);
Assert.Equal("사용자 초안", marketChange.EditorName);
Assert.Equal(string.Empty, Assert.IsType<LegacyChangeCreateThemeMarketIntent>(
ParseRequired(
"change-create-theme-market",
"""{"market":"nxt","editorName":""}""")).EditorName);
Assert.IsType<LegacyBeginCreateExpertCatalogIntent>(ParseRequired(
"begin-create-expert-catalog",
"{}"));
@@ -141,6 +175,32 @@ public sealed class LegacyOperatorCatalogBridgeTests
{"type":"add-operator-catalog-stock","payload":{"buyAmount":1.5}}
""",
out _));
foreach (var payload in new[]
{
"""{"market":"future","editorName":""}""",
"""{"market":"nxt","editorName":" "}""",
"""{"market":"nxt","editorName":"bad\nvalue"}""",
"""{"market":"nxt"}""",
"""{"market":"nxt","editorName":"","themeCode":"00000001"}"""
})
{
Assert.False(Parse(
"{\"type\":\"change-create-theme-market\",\"payload\":" + payload + "}",
out _));
}
Assert.False(Parse(
JsonSerializer.Serialize(new
{
type = "change-create-theme-market",
payload = new
{
market = "krx",
editorName = new string('A', 129)
}
}),
out _));
}
[Fact]

View File

@@ -83,6 +83,9 @@ public sealed class LegacyOverseasBridgeTests
Assert.Equal(
"overseas-stock-00000001-0001",
overseas.GetProperty("stock").GetProperty("selectedId").GetString());
Assert.Equal(
0,
overseas.GetProperty("stock").GetProperty("isolatedInvalidRowCount").GetInt32());
Assert.Equal(12, overseas.GetProperty("actions").GetArrayLength());
Assert.All(
overseas.GetProperty("actions").EnumerateArray(),
@@ -143,6 +146,26 @@ public sealed class LegacyOverseasBridgeTests
Assert.DoesNotContain("LNS@UNSUPPORTED", document.RootElement.GetRawText(), StringComparison.Ordinal);
}
[Fact]
public async Task ProjectionReportsRowsIsolatedByTheReadBoundary()
{
var workflow = new LegacyOverseasSelectionWorkflow(
new StaticIndustryService(),
new StaticStockService(isolatedInvalidRowCount: 16));
await workflow.SearchStocksAsync(string.Empty);
var snapshot = new LegacyOperatorSnapshot(
7, string.Empty, [], null, [], [], null, string.Empty,
LegacyOperatorStatusKind.Neutral, Overseas: workflow.Current);
using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(snapshot));
var stock = document.RootElement.GetProperty("payload")
.GetProperty("overseas").GetProperty("stock");
Assert.Equal(16, stock.GetProperty("isolatedInvalidRowCount").GetInt32());
Assert.True(stock.GetProperty("isTruncated").GetBoolean());
Assert.Single(stock.GetProperty("results").EnumerateArray());
}
private sealed class StaticIndustryService : IOverseasIndustryIndexSearchService
{
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
@@ -157,7 +180,8 @@ public sealed class LegacyOverseasBridgeTests
}
private sealed class StaticStockService(
IReadOnlyList<WorldStockSearchItem>? items = null) : IOverseasStockSearchService
IReadOnlyList<WorldStockSearchItem>? items = null,
int isolatedInvalidRowCount = 0) : IOverseasStockSearchService
{
public Task<WorldStockSearchResult> SearchAsync(
string query,
@@ -167,6 +191,7 @@ public sealed class LegacyOverseasBridgeTests
query,
DateTimeOffset.Parse("2026-07-15T10:00:00+09:00"),
items ?? [new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
IsTruncated: false));
IsTruncated: isolatedInvalidRowCount > 0,
IsolatedInvalidRowCount: isolatedInvalidRowCount));
}
}

View File

@@ -50,6 +50,51 @@ public sealed class LegacyRowDoubleClickBridgeTests
out _));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Named_playlist_save_carries_only_the_success_dialog_choice(bool showSavedAlert)
{
var json = JsonSerializer.Serialize(new
{
type = "save-current-named-playlist-to",
payload = new
{
definitionId = "named-definition-AAAAAAAAAAAB",
showSavedAlert
}
});
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
var save = Assert.IsType<LegacySaveCurrentNamedPlaylistToIntent>(parsed);
Assert.Equal(showSavedAlert, save.ShowSavedAlert);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"save-current-named-playlist-to\",\"payload\":{" +
"\"definitionId\":\"named-definition-AAAAAAAAAAAB\"," +
"\"showSavedAlert\":\"yes\"}}",
out _,
out _));
}
[Fact]
public void Named_playlist_load_rejects_save_dialog_metadata()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"load-named-playlist-by-id\",\"payload\":{" +
"\"definitionId\":\"named-definition-AAAAAAAAAAEF\"}}",
out var parsed,
out _));
Assert.IsType<LegacyLoadNamedPlaylistByIdIntent>(parsed);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"load-named-playlist-by-id\",\"payload\":{" +
"\"definitionId\":\"named-definition-AAAAAAAAAAEF\"," +
"\"showSavedAlert\":false}}",
out _,
out _));
}
[Fact]
public void Projection_exposes_only_closed_command_receipt_and_opaque_target()
{

View File

@@ -0,0 +1,163 @@
using System.Globalization;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests;
public sealed class LegacyThemeBridgeTests
{
[Fact]
public async Task ProjectionPreservesFiveThousandRowsAndExplicitTruncationWarningState()
{
var workflow = new LegacyThemeWorkflow(new TruncatedThemeService());
var theme = await workflow.SearchAsync(
workflow.CreateInitial(),
string.Empty,
ThemeSession.PreMarket);
var snapshot = new LegacyOperatorSnapshot(
1,
string.Empty,
[],
null,
[],
[],
null,
"테마 검색 결과가 최대 5,000건으로 제한되었습니다.",
LegacyOperatorStatusKind.Warning,
Theme: theme);
using var document = JsonDocument.Parse(
LegacyBridgeProtocol.SerializeState(snapshot));
var payload = document.RootElement.GetProperty("payload");
var projected = payload.GetProperty("theme");
Assert.True(projected.GetProperty("searchIsTruncated").GetBoolean());
Assert.Equal(5_000, projected.GetProperty("searchResults").GetArrayLength());
Assert.Equal("warning", payload.GetProperty("statusKind").GetString());
Assert.Contains("5,000", payload.GetProperty("statusMessage").GetString());
Assert.DoesNotContain("themeCode", projected.GetRawText(), StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ProjectionPreservesRawLegacyTitlesExplicitMarketAndEmptyPreviewGuards()
{
var workflow = new LegacyThemeWorkflow(new CompatibilityThemeService());
var search = await workflow.SearchAsync(
workflow.CreateInitial(),
string.Empty,
ThemeSession.PreMarket);
var theme = await workflow.SelectAsync(search, "theme-result-005");
var snapshot = new LegacyOperatorSnapshot(
1,
string.Empty,
[],
null,
[],
[],
null,
"Theme compatibility",
LegacyOperatorStatusKind.Information,
Theme: theme);
using var document = JsonDocument.Parse(
LegacyBridgeProtocol.SerializeState(snapshot));
var projected = document.RootElement.GetProperty("payload").GetProperty("theme");
var rows = projected.GetProperty("searchResults").EnumerateArray().ToArray();
Assert.Equal(5, rows.Length);
Assert.Equal(" Legacy ", rows[0].GetProperty("displayTitle").GetString());
Assert.Equal("Same", rows[1].GetProperty("displayTitle").GetString());
Assert.Equal("Same", rows[2].GetProperty("displayTitle").GetString());
Assert.Equal("Desk(NXT)", rows[3].GetProperty("displayTitle").GetString());
Assert.Equal("krx", rows[3].GetProperty("market").GetString());
Assert.Equal(" Empty NXT(NXT)", rows[4].GetProperty("displayTitle").GetString());
Assert.Equal("nxt", rows[4].GetProperty("market").GetString());
Assert.Equal(0, projected.GetProperty("previewItems").GetArrayLength());
Assert.All(projected.GetProperty("actions").EnumerateArray(), action =>
{
Assert.False(action.GetProperty("isAvailable").GetBoolean());
Assert.Equal(
"theme-preview-is-empty",
action.GetProperty("unavailableReason").GetString());
});
Assert.DoesNotContain("themeCode", projected.GetRawText(), StringComparison.OrdinalIgnoreCase);
}
private sealed class TruncatedThemeService : IThemeSelectionService
{
public Task<ThemeSearchResult> SearchAsync(
string query,
ThemeSession nxtSession,
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default)
{
Assert.Equal(LegacyThemeWorkflow.MaximumSearchResults, maximumResults);
var rows = Enumerable.Range(0, maximumResults)
.Select(index => new ThemeSelectorItem(
new ThemeSelectionIdentity(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
index.ToString("D8", CultureInfo.InvariantCulture),
$"Theme {index}"),
DataSourceKind.Oracle))
.ToArray();
return Task.FromResult(new ThemeSearchResult(
query,
nxtSession,
DateTimeOffset.UtcNow,
rows,
IsTruncated: true));
}
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) =>
Task.FromResult(new ThemePreviewResult(
identity,
DateTimeOffset.UtcNow,
[],
IsTruncated: false));
}
private sealed class CompatibilityThemeService : IThemeSelectionService
{
public Task<ThemeSearchResult> SearchAsync(
string query,
ThemeSession nxtSession,
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromResult(new ThemeSearchResult(
query,
nxtSession,
DateTimeOffset.UtcNow,
[
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000001", " Legacy "),
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000002", "Same"),
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000003", "Same"),
Item(ThemeMarket.Krx, ThemeSession.NotApplicable, "00000004", "Desk(NXT)"),
Item(ThemeMarket.Nxt, nxtSession, "00000005", " Empty NXT")
],
IsTruncated: false));
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) =>
Task.FromResult(new ThemePreviewResult(
identity,
DateTimeOffset.UtcNow,
[],
IsTruncated: false));
private static ThemeSelectorItem Item(
ThemeMarket market,
ThemeSession session,
string code,
string title) =>
new(
new ThemeSelectionIdentity(market, session, code, title),
market == ThemeMarket.Krx ? DataSourceKind.Oracle : DataSourceKind.MariaDb);
}
}