fix: restore legacy playout parity and live startup
This commit is contained in:
@@ -1,9 +1,104 @@
|
||||
using MMoneyCoderSharp.Data;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyStockCutInteractionParityTests
|
||||
{
|
||||
public static TheoryData<
|
||||
LegacyDomesticStockMarket,
|
||||
int,
|
||||
string,
|
||||
string> DomesticStockCandleCases
|
||||
{
|
||||
get
|
||||
{
|
||||
var cases = new TheoryData<
|
||||
LegacyDomesticStockMarket,
|
||||
int,
|
||||
string,
|
||||
string>();
|
||||
var cuts = new (int PhysicalIndex, string Alias, string Mode)[]
|
||||
{
|
||||
(14, "8035", "PRICE"),
|
||||
(15, "8061", "PRICE"),
|
||||
(16, "8040", "PRICE"),
|
||||
(17, "8046", "PRICE"),
|
||||
(18, "8051", "PRICE"),
|
||||
(19, "8056", "PRICE"),
|
||||
(20, "8035", "VOLUME"),
|
||||
(21, "8061", "VOLUME"),
|
||||
(22, "8040", "VOLUME"),
|
||||
(23, "8046", "VOLUME"),
|
||||
(24, "8051", "VOLUME"),
|
||||
(25, "8056", "VOLUME"),
|
||||
(26, "8061", "EXPECTED"),
|
||||
(27, "8040", "EXPECTED"),
|
||||
(28, "8046", "EXPECTED"),
|
||||
(29, "8051", "EXPECTED"),
|
||||
(30, "8056", "EXPECTED")
|
||||
};
|
||||
|
||||
foreach (var market in new[]
|
||||
{
|
||||
LegacyDomesticStockMarket.Kospi,
|
||||
LegacyDomesticStockMarket.Kosdaq
|
||||
})
|
||||
{
|
||||
foreach (var cut in cuts)
|
||||
{
|
||||
cases.Add(market, cut.PhysicalIndex, cut.Alias, cut.Mode);
|
||||
}
|
||||
}
|
||||
|
||||
return cases;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DomesticStockCandleCases))]
|
||||
public async Task DomesticStockCandlePointerDropCreatesCanonicalPlayoutSelection(
|
||||
LegacyDomesticStockMarket market,
|
||||
int physicalIndex,
|
||||
string expectedAlias,
|
||||
string expectedMode)
|
||||
{
|
||||
var stockName = market == LegacyDomesticStockMarket.Kospi
|
||||
? "Samsung Electronics"
|
||||
: "Kakao Games";
|
||||
var stockCode = market == LegacyDomesticStockMarket.Kospi
|
||||
? "005930"
|
||||
: "293490";
|
||||
var controller = new LegacyOperatorController(
|
||||
new SingleStockLookup(market, stockName, stockCode));
|
||||
await controller.SearchStocksAsync(stockName, LegacySearchTrigger.Button);
|
||||
controller.SelectStock(0);
|
||||
controller.CutPointerDown(
|
||||
physicalIndex,
|
||||
control: false,
|
||||
shift: false,
|
||||
x: 2,
|
||||
y: physicalIndex * 23,
|
||||
timestampMilliseconds: 1_000);
|
||||
|
||||
var state = controller.DropSelectedCutsOnPlaylist();
|
||||
|
||||
var row = Assert.Single(state.Playlist);
|
||||
var expectedSelection = new LegacySceneSelection(
|
||||
market == LegacyDomesticStockMarket.Kospi
|
||||
? "KOSPI_STOCK"
|
||||
: "KOSDAQ_STOCK",
|
||||
stockName,
|
||||
string.Empty,
|
||||
expectedMode,
|
||||
stockCode);
|
||||
Assert.Equal(expectedSelection, row.PlayoutSelection);
|
||||
|
||||
var entry = row.ToPlayoutEntry();
|
||||
Assert.Equal(expectedAlias, entry.CutCode);
|
||||
Assert.Equal(expectedSelection, entry.Selection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThreeOrderedProgramClicksSelectThenAppendExactlyOneSafeTailRow()
|
||||
{
|
||||
@@ -463,4 +558,25 @@ public sealed class LegacyStockCutInteractionParityTests
|
||||
"Next stock(NXT)",
|
||||
"A00001")]));
|
||||
}
|
||||
|
||||
private sealed class SingleStockLookup : ILegacyStockLookup
|
||||
{
|
||||
private readonly LegacyStockIdentity _stock;
|
||||
|
||||
public SingleStockLookup(
|
||||
LegacyDomesticStockMarket market,
|
||||
string displayName,
|
||||
string stockCode)
|
||||
{
|
||||
_stock = new LegacyStockIdentity(market, displayName, stockCode);
|
||||
}
|
||||
|
||||
public Task<LegacyStockSearchData> SearchAsync(
|
||||
string text,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(
|
||||
text,
|
||||
[_stock.DisplayName],
|
||||
[_stock]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,119 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
Assert.Equal(new NamedPlaylistPageState(1, 1), stored.Page);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(
|
||||
LegacyDomesticStockMarket.Kospi,
|
||||
"코스피",
|
||||
"삼성전자",
|
||||
"005930",
|
||||
15,
|
||||
"캔들그래프",
|
||||
"PRICE")]
|
||||
[InlineData(
|
||||
LegacyDomesticStockMarket.Kosdaq,
|
||||
"코스닥",
|
||||
"카카오게임즈",
|
||||
"293490",
|
||||
15,
|
||||
"캔들그래프",
|
||||
"PRICE")]
|
||||
[InlineData(
|
||||
LegacyDomesticStockMarket.Kospi,
|
||||
"코스피",
|
||||
"삼성전자",
|
||||
"005930",
|
||||
21,
|
||||
"캔들그래프(거래량)",
|
||||
"VOLUME")]
|
||||
[InlineData(
|
||||
LegacyDomesticStockMarket.Kosdaq,
|
||||
"코스닥",
|
||||
"카카오게임즈",
|
||||
"293490",
|
||||
21,
|
||||
"캔들그래프(거래량)",
|
||||
"VOLUME")]
|
||||
[InlineData(
|
||||
LegacyDomesticStockMarket.Kospi,
|
||||
"코스피",
|
||||
"삼성전자",
|
||||
"005930",
|
||||
26,
|
||||
"캔들그래프(예상체결가)",
|
||||
"EXPECTED")]
|
||||
[InlineData(
|
||||
LegacyDomesticStockMarket.Kosdaq,
|
||||
"코스닥",
|
||||
"카카오게임즈",
|
||||
"293490",
|
||||
26,
|
||||
"캔들그래프(예상체결가)",
|
||||
"EXPECTED")]
|
||||
public async Task DirectDomesticStockCandleSaveAndLoadRestoresCanonicalPlayoutSelection(
|
||||
LegacyDomesticStockMarket market,
|
||||
string storedMarket,
|
||||
string stockName,
|
||||
string stockCode,
|
||||
int physicalIndex,
|
||||
string visibleGraphicType,
|
||||
string runtimeMode)
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = new LegacyOperatorController(
|
||||
new FixedStockLookup(market, stockName, stockCode),
|
||||
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence));
|
||||
controller.SetMovingAverages(ma5: false, ma20: false);
|
||||
await controller.SearchStocksAsync(stockName, LegacySearchTrigger.Button);
|
||||
controller.SelectStock(0);
|
||||
controller.CutPointerDown(
|
||||
physicalIndex,
|
||||
control: false,
|
||||
shift: false,
|
||||
x: 2,
|
||||
y: physicalIndex * 23,
|
||||
timestampMilliseconds: 1_000);
|
||||
var added = controller.DropSelectedCutsOnPlaylist();
|
||||
var expectedCanonical = new LegacySceneSelection(
|
||||
market == LegacyDomesticStockMarket.Kospi
|
||||
? "KOSPI_STOCK"
|
||||
: "KOSDAQ_STOCK",
|
||||
stockName,
|
||||
string.Empty,
|
||||
runtimeMode,
|
||||
stockCode);
|
||||
Assert.Equal(expectedCanonical, Assert.Single(added.Playlist).PlayoutSelection);
|
||||
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
var definitionId = Assert.Single(
|
||||
listed.NamedPlaylist!.Definitions).DefinitionId;
|
||||
controller.SelectNamedPlaylist(definitionId);
|
||||
await controller.SaveCurrentNamedPlaylistAsync();
|
||||
|
||||
var stored = Assert.Single(persistence.LastReplaceItems!);
|
||||
Assert.Equal(storedMarket, stored.Selection.GroupCode);
|
||||
Assert.Equal(stockName, stored.Selection.Subject);
|
||||
Assert.Equal(visibleGraphicType, stored.Selection.GraphicType);
|
||||
Assert.Equal("5일", stored.Selection.Subtype);
|
||||
Assert.Equal(stockCode, stored.Selection.DataCode);
|
||||
Assert.Equal(new NamedPlaylistPageState(1, 1), stored.Page);
|
||||
|
||||
var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);
|
||||
|
||||
Assert.True(loaded.CommandResult?.Succeeded);
|
||||
var loadedRow = Assert.Single(loaded.Playlist);
|
||||
Assert.Equal(storedMarket, loadedRow.MarketText);
|
||||
Assert.Equal(stockName, loadedRow.StockName);
|
||||
Assert.Equal(visibleGraphicType, loadedRow.GraphicType);
|
||||
Assert.Equal("5일", loadedRow.Subtype);
|
||||
Assert.Equal(stockCode, loadedRow.StockCode);
|
||||
Assert.Equal("8061", loadedRow.SceneAlias);
|
||||
Assert.Equal(expectedCanonical, loadedRow.PlayoutSelection);
|
||||
var runtimeEntry = Assert.Single(controller.CreatePlayoutRequest().Playlist);
|
||||
Assert.Equal("8061", runtimeEntry.CutCode);
|
||||
Assert.Equal(expectedCanonical, runtimeEntry.Selection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveFixedPagedActionPersistsInitialDatabasePageNBeforePlayout()
|
||||
{
|
||||
@@ -635,6 +748,31 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
saved.NamedPlaylist?.LastMutationOutcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveCurrent_PersistsTheConsumedPageProjection()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = CreateController(persistence);
|
||||
var action = LegacyFixedActionCatalog.Default
|
||||
.GetActions(LegacyFixedMarket.Overseas)
|
||||
.First(candidate => candidate.Available);
|
||||
var added = controller.ActivateFixedAction(action.Id);
|
||||
var row = Assert.Single(added.Playlist);
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
controller.SelectNamedPlaylist(
|
||||
listed.NamedPlaylist!.Definitions.Single().DefinitionId);
|
||||
controller.ReflectSuccessfulPlayout(
|
||||
row.RowId,
|
||||
pageIndexZeroBased: 1,
|
||||
pageCount: 3,
|
||||
synchronizeOperatorSelection: false);
|
||||
|
||||
await controller.SaveCurrentNamedPlaylistAsync();
|
||||
|
||||
var stored = Assert.Single(persistence.LastReplaceItems!);
|
||||
Assert.Equal("2/3", stored.Page?.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ThemeMarket.Krx, " Edge Theme ", " Edge Theme ")]
|
||||
[InlineData(ThemeMarket.Nxt, "Robotics(NXT)", "Robotics(NXT)(NXT)")]
|
||||
@@ -955,6 +1093,27 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
Task.FromResult(new LegacyStockSearchData(query, [], []));
|
||||
}
|
||||
|
||||
private sealed class FixedStockLookup : ILegacyStockLookup
|
||||
{
|
||||
private readonly LegacyStockIdentity _stock;
|
||||
|
||||
public FixedStockLookup(
|
||||
LegacyDomesticStockMarket market,
|
||||
string displayName,
|
||||
string stockCode)
|
||||
{
|
||||
_stock = new LegacyStockIdentity(market, displayName, stockCode);
|
||||
}
|
||||
|
||||
public Task<LegacyStockSearchData> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(
|
||||
query,
|
||||
[_stock.DisplayName],
|
||||
[_stock]));
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider(DateTimeOffset localNow) : TimeProvider
|
||||
{
|
||||
private static readonly TimeZoneInfo KoreaStandardTime = TimeZoneInfo.CreateCustomTimeZone(
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyNamedPlaylistPagedPlaceholderFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Raw5074PlaceholdersBeforeSelectedIndexCandleReproduceWholeListPreflightHazard()
|
||||
{
|
||||
var persistence = new ReadOnlyNamedPlaylistPersistence(
|
||||
[
|
||||
new NamedPlaylistStoredItem(
|
||||
0,
|
||||
true,
|
||||
new LegacySceneSelection(
|
||||
"테마",
|
||||
"=========================",
|
||||
"5단 표그래프",
|
||||
"테마-현재가",
|
||||
string.Empty),
|
||||
new NamedPlaylistPageState(1, 1)),
|
||||
new NamedPlaylistStoredItem(
|
||||
1,
|
||||
true,
|
||||
new LegacySceneSelection(
|
||||
"테마",
|
||||
"=========================",
|
||||
"5단 표그래프",
|
||||
"테마-현재가",
|
||||
string.Empty),
|
||||
new NamedPlaylistPageState(1, 1)),
|
||||
new NamedPlaylistStoredItem(
|
||||
2,
|
||||
true,
|
||||
new LegacySceneSelection(
|
||||
"코스피",
|
||||
"지수",
|
||||
"캔들그래프(거래량)",
|
||||
"5일",
|
||||
string.Empty),
|
||||
new NamedPlaylistPageState(1, 1))
|
||||
]);
|
||||
var controller = new LegacyOperatorController(
|
||||
new UnusedStockLookup(),
|
||||
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence));
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
|
||||
var loaded = await controller.LoadNamedPlaylistByIdAsync(
|
||||
Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
|
||||
var candle = loaded.Playlist[2];
|
||||
controller.SetMovingAverages(ma5: false, ma20: false);
|
||||
controller.SelectPlaylistRow(candle.RowId, control: false, shift: false);
|
||||
var request = controller.CreatePlayoutRequest();
|
||||
|
||||
Assert.All(loaded.Playlist, row => Assert.True(row.IsSceneResolved));
|
||||
Assert.All(request.Playlist.Take(2), entry =>
|
||||
{
|
||||
Assert.Equal("5074", entry.CutCode);
|
||||
Assert.Equal("테마", entry.Selection?.GroupCode);
|
||||
Assert.Equal(ScenePageSize.Five, entry.PageNavigation?.PageSize);
|
||||
});
|
||||
Assert.Equal("8061", request.Playlist[2].CutCode);
|
||||
Assert.Equal(
|
||||
new LegacySceneSelection(
|
||||
"KOSPI_INDEX",
|
||||
"INDEX",
|
||||
string.Empty,
|
||||
"VOLUME",
|
||||
string.Empty),
|
||||
request.Playlist[2].Selection);
|
||||
Assert.False(request.Playlist[2].PageNavigation?.IsEnabled);
|
||||
Assert.Equal(2, request.SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
private sealed class UnusedStockLookup : ILegacyStockLookup
|
||||
{
|
||||
public Task<LegacyStockSearchData> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(query, [], []));
|
||||
}
|
||||
|
||||
private sealed class ReadOnlyNamedPlaylistPersistence(
|
||||
IReadOnlyList<NamedPlaylistStoredItem> items)
|
||||
: INamedPlaylistPersistenceService
|
||||
{
|
||||
private const string ProgramCode = "00000004";
|
||||
|
||||
public bool CanMutate => false;
|
||||
|
||||
public Task<NamedPlaylistListResult> ListAsync(
|
||||
int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new NamedPlaylistListResult(
|
||||
DateTimeOffset.UtcNow,
|
||||
[new NamedPlaylistSummary(ProgramCode, "생쇼")],
|
||||
false));
|
||||
|
||||
public Task<string> SuggestNextProgramCodeAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult("00000005");
|
||||
|
||||
public Task<NamedPlaylistDocument> LoadAsync(
|
||||
string programCode,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new NamedPlaylistDocument(
|
||||
new NamedPlaylistSummary(ProgramCode, "생쇼"),
|
||||
DateTimeOffset.UtcNow,
|
||||
items));
|
||||
|
||||
public Task CreateDefinitionAsync(
|
||||
string programCode,
|
||||
string title,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task ReplaceItemsAsync(
|
||||
string programCode,
|
||||
IReadOnlyList<NamedPlaylistStoredItem> replacement,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task DeleteAsync(
|
||||
string programCode,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user