Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/LegacyStockCutInteractionParityTests.cs

363 lines
13 KiB
C#

using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyStockCutInteractionParityTests
{
[Fact]
public async Task EmptySearchButtonPreservesResultsStockAndCutSelection()
{
var lookup = new QueryLookup();
var controller = new LegacyOperatorController(lookup);
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24,
timestampMilliseconds: 1_000);
var state = await controller.SearchStocksAsync(
string.Empty,
LegacySearchTrigger.Button);
Assert.Equal(1, lookup.CallCount);
Assert.Equal(string.Empty, state.SearchText);
Assert.Equal("First stock", Assert.Single(state.SearchResults).DisplayName);
Assert.Equal(0, state.SelectedStockIndex);
Assert.Equal(
[1],
state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
Assert.Empty(state.Playlist);
}
[Fact]
public async Task EmptyEnterQueriesAndClearsOnlyTheStockResultSelection()
{
var lookup = new QueryLookup();
var controller = new LegacyOperatorController(lookup);
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24,
timestampMilliseconds: 1_000);
var state = await controller.SearchStocksAsync(
string.Empty,
LegacySearchTrigger.Enter);
Assert.Equal(2, lookup.CallCount);
Assert.Null(state.SelectedStockIndex);
Assert.Equal("All stock", Assert.Single(state.SearchResults).DisplayName);
Assert.Equal(
[1],
state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
}
[Fact]
public async Task RetainedCutSelectionUsesTheNewlySelectedStockAtDropTime()
{
var lookup = new QueryLookup();
var controller = new LegacyOperatorController(lookup);
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24,
timestampMilliseconds: 1_000);
await controller.SearchStocksAsync("second", LegacySearchTrigger.Button);
controller.SelectStock(0);
var state = controller.DropSelectedCutsOnPlaylist();
var row = Assert.Single(state.Playlist);
Assert.Equal("Second stock", row.StockName);
Assert.Equal("000002", row.StockCode);
Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel);
}
[Fact]
public async Task OrdinaryMarketSeparatorIsIgnoredWithoutOpeningTheNxtWarning()
{
var controller = new LegacyOperatorController(new QueryLookup());
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(3, control: false, shift: false, x: 2, y: 70,
timestampMilliseconds: 1_000);
var state = controller.DropSelectedCutsOnPlaylist();
Assert.Empty(state.Playlist);
Assert.Null(state.Dialog);
}
[Fact]
public async Task InjectedRuntimeMenuIsCopiedAndDrivesViewSelectionAndDropByPhysicalOrder()
{
var injected = RuntimeMenu();
var expectedFirstLabel = injected[0].RawLabel;
var expectedLastLabel = injected[2].RawLabel;
var controller = new LegacyOperatorController(
new QueryLookup(),
cutRows: injected);
injected[0] = injected[0] with
{
RawLabel = LegacyStockCutCatalog.Rows[2].RawLabel
};
Assert.Equal(
[expectedFirstLabel, string.Empty, expectedLastLabel],
controller.Current.CutRows.Select(row => row.RawLabel));
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(0, control: false, shift: false, x: 1, y: 1,
timestampMilliseconds: 1_000);
controller.CutPointerDown(2, control: true, shift: false, x: 1, y: 47,
timestampMilliseconds: 2_000);
var dropped = controller.DropSelectedCutsOnPlaylist();
Assert.Equal(
[0, 2],
dropped.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
Assert.Equal(
[expectedFirstLabel, expectedLastLabel],
dropped.Playlist.Select(row => row.RawCutLabel));
var revision = dropped.Revision;
var invalidIndex = controller.CutPointerDown(
3,
control: false,
shift: false,
x: 1,
y: 70,
timestampMilliseconds: 3_000);
Assert.Equal(revision, invalidIndex.Revision);
}
[Fact]
public async Task InjectedRuntimeMenuPreservesTheNxtCurrentLabelRuleAfterReorder()
{
var menu = RuntimeMenu();
var controller = new LegacyOperatorController(
new NxtLookup(),
cutRows: menu);
await controller.SearchStocksAsync("nxt", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(2, control: false, shift: false, x: 1, y: 47,
timestampMilliseconds: 1_000);
var warning = controller.DropSelectedCutsOnPlaylist();
Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message);
Assert.Empty(warning.Playlist);
controller.DismissDialog();
controller.CutPointerDown(0, control: false, shift: false, x: 1, y: 1,
timestampMilliseconds: 2_000);
var accepted = controller.DropSelectedCutsOnPlaylist();
Assert.Equal("N5001", Assert.Single(accepted.Playlist).ToPlayoutEntry().CutCode);
}
[Fact]
public async Task FiftyRowRuntimeMenuCanSelectAndDropItsLastPhysicalRow()
{
var rows = Enumerable.Range(0, LegacyRuntimeStockCutMenuLoader.MaximumRows)
.Select(index => new LegacyCutCatalogRow(
index,
index + 1,
LegacyStockCutCatalog.Rows[1].RawLabel,
IsSeparator: false))
.ToArray();
var controller = new LegacyOperatorController(new QueryLookup(), cutRows: rows);
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(
LegacyRuntimeStockCutMenuLoader.MaximumRows - 1,
control: false,
shift: false,
x: 1,
y: 1,
timestampMilliseconds: 1_000);
var state = controller.DropSelectedCutsOnPlaylist();
Assert.Equal(
LegacyRuntimeStockCutMenuLoader.MaximumRows - 1,
Assert.Single(state.CutRows, row => row.IsSelected).PhysicalIndex);
Assert.Equal(
LegacyStockCutCatalog.Rows[1].RawLabel,
Assert.Single(state.Playlist).RawCutLabel);
}
[Fact]
public async Task ConsecutiveShiftGesturesKeepTheOriginalNativeListAnchor()
{
var controller = new LegacyOperatorController(new QueryLookup());
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(0, control: false, shift: false, 1, 1, 1_000);
var first = controller.CutPointerDown(
2, control: false, shift: true, 1, 47, 2_000);
var second = controller.CutPointerDown(
4, control: false, shift: true, 1, 93, 3_000);
Assert.Equal(
[0, 1, 2],
first.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
Assert.Equal(
[0, 1, 2, 3, 4],
second.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
}
[Fact]
public async Task EmptyCutListAreaClearsSelectionSoAStaleDropAddsNothing()
{
var controller = new LegacyOperatorController(new QueryLookup());
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
var cleared = controller.ClearCutSelection();
var dropped = controller.DropSelectedCutsOnPlaylist();
Assert.DoesNotContain(cleared.CutRows, row => row.IsSelected);
Assert.Empty(dropped.Playlist);
}
[Theory]
[InlineData(0, 2, 0, 1, 2)]
[InlineData(3, 1, 1, 2, 3)]
public async Task EmptyCutListAreaPreservesNativeAnchorForFollowingShift(
int anchor,
int target,
params int[] expected)
{
var controller = new LegacyOperatorController(new QueryLookup());
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(anchor, false, false, 1, 1, 1_000);
controller.ClearCutSelection();
var state = controller.CutPointerDown(target, false, true, 2, 2, 2_000);
Assert.Equal(
expected,
state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
}
[Fact]
public void EmptyCutListAreaIsIgnoredUntilAStockIsSelected()
{
var controller = new LegacyOperatorController(new QueryLookup());
var revision = controller.Current.Revision;
var state = controller.ClearCutSelection();
Assert.Equal(revision, state.Revision);
}
[Fact]
public async Task BlankClickResetsLegacyAnchorEvenAfterControlClearsTheLastRow()
{
var controller = new LegacyOperatorController(new QueryLookup());
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
controller.CutPointerDown(1, true, false, 1, 24, 2_000);
controller.ClearCutSelection();
controller.CutPointerDown(2, false, true, 2, 47, 3_000);
var state = controller.CutPointerDown(2, false, false, 2, 47, 3_400);
Assert.Empty(state.Playlist);
Assert.Equal(
[2],
state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
}
[Theory]
[InlineData("physical-index")]
[InlineData("display-ordinal")]
[InlineData("unknown-label")]
[InlineData("separator-shape")]
[InlineData("too-many")]
public void InjectedRuntimeMenuRejectsInvalidAuthority(string invalidCase)
{
var baseline = RuntimeMenu();
IReadOnlyList<LegacyCutCatalogRow> rows = invalidCase switch
{
"physical-index" =>
[baseline[0] with { PhysicalIndex = 1 }],
"display-ordinal" =>
[baseline[0] with { DisplayOrdinal = 2 }],
"unknown-label" =>
[baseline[0] with { RawLabel = "지원하지않는_컷" }],
"separator-shape" =>
[new LegacyCutCatalogRow(0, 1, "-", IsSeparator: true)],
"too-many" => Enumerable.Range(
0,
LegacyRuntimeStockCutMenuLoader.MaximumRows + 1)
.Select(index => new LegacyCutCatalogRow(
index,
index + 1,
LegacyStockCutCatalog.Rows[0].RawLabel,
IsSeparator: false))
.ToArray(),
_ => throw new ArgumentOutOfRangeException(nameof(invalidCase))
};
Assert.Throws<ArgumentException>(() => new LegacyOperatorController(
new QueryLookup(),
cutRows: rows));
}
private static LegacyCutCatalogRow[] RuntimeMenu() =>
[
new LegacyCutCatalogRow(
0,
1,
LegacyStockCutCatalog.Rows[1].RawLabel,
IsSeparator: false),
new LegacyCutCatalogRow(1, null, string.Empty, IsSeparator: true),
new LegacyCutCatalogRow(
2,
2,
LegacyStockCutCatalog.Rows[0].RawLabel,
IsSeparator: false)
];
private sealed class QueryLookup : ILegacyStockLookup
{
public int CallCount { get; private set; }
public Task<LegacyStockSearchData> SearchAsync(
string text,
CancellationToken cancellationToken = default)
{
CallCount++;
var (name, code) = text switch
{
"first" => ("First stock", "000001"),
"second" => ("Second stock", "000002"),
_ => ("All stock", "000003")
};
return Task.FromResult(new LegacyStockSearchData(
text,
[name],
[new LegacyStockIdentity(
LegacyDomesticStockMarket.Kospi,
name,
code)]));
}
}
private sealed class NxtLookup : ILegacyStockLookup
{
public Task<LegacyStockSearchData> SearchAsync(
string text,
CancellationToken cancellationToken = default) =>
Task.FromResult(new LegacyStockSearchData(
text,
["Next stock(NXT)"],
[new LegacyStockIdentity(
LegacyDomesticStockMarket.NxtKospi,
"Next stock(NXT)",
"A00001")]));
}
}