467 lines
17 KiB
C#
467 lines
17 KiB
C#
using MMoneyCoderSharp.Data;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
|
|
|
public sealed class LegacyStockCutInteractionParityTests
|
|
{
|
|
[Fact]
|
|
public async Task ThreeOrderedProgramClicksSelectThenAppendExactlyOneSafeTailRow()
|
|
{
|
|
var controller = new LegacyOperatorController(new QueryLookup());
|
|
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
|
|
controller.SelectStock(0);
|
|
controller.CutPointerDown(
|
|
1,
|
|
control: false,
|
|
shift: false,
|
|
x: 2,
|
|
y: 24,
|
|
timestampMilliseconds: 1_000);
|
|
|
|
controller.CutPointerDown(
|
|
0,
|
|
control: false,
|
|
shift: false,
|
|
x: 2,
|
|
y: 1,
|
|
timestampMilliseconds: 10_000);
|
|
controller.CutPointerDown(
|
|
0,
|
|
control: false,
|
|
shift: false,
|
|
x: 2,
|
|
y: 1,
|
|
timestampMilliseconds: 10_046);
|
|
var state = controller.CutPointerDown(
|
|
0,
|
|
control: false,
|
|
shift: false,
|
|
x: 2,
|
|
y: 1,
|
|
timestampMilliseconds: 10_089);
|
|
|
|
var appended = Assert.Single(state.Playlist);
|
|
Assert.Equal(LegacyStockCutCatalog.Rows[0].RawLabel, appended.RawCutLabel);
|
|
Assert.Equal(
|
|
[0],
|
|
state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASecondDragInsideTheLegacyDoubleClickWindowAddsOnceOnPointerDownAndOnceOnDrop()
|
|
{
|
|
var controller = new LegacyOperatorController(new QueryLookup());
|
|
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
|
|
controller.SelectStock(0);
|
|
|
|
controller.CutPointerDown(1, false, false, 2, 24, 1_000);
|
|
controller.DropSelectedCutsOnPlaylist();
|
|
controller.CutPointerDown(1, false, false, 2, 24, 1_313);
|
|
var state = controller.DropSelectedCutsOnPlaylist();
|
|
|
|
Assert.Equal(3, state.Playlist.Count);
|
|
Assert.All(
|
|
state.Playlist,
|
|
row => Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ASecondDragAtTheExactLegacyBoundaryAddsOnlyItsDrop()
|
|
{
|
|
var controller = new LegacyOperatorController(new QueryLookup());
|
|
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
|
|
controller.SelectStock(0);
|
|
|
|
controller.CutPointerDown(1, false, false, 2, 24, 1_000);
|
|
controller.DropSelectedCutsOnPlaylist();
|
|
controller.CutPointerDown(1, false, false, 2, 24, 1_500);
|
|
var state = controller.DropSelectedCutsOnPlaylist();
|
|
|
|
Assert.Equal(2, state.Playlist.Count);
|
|
Assert.All(
|
|
state.Playlist,
|
|
row => Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DropPreservesExistingPrefixAndAppendsSelectedCutsAtTheTail()
|
|
{
|
|
var controller = new LegacyOperatorController(new QueryLookup());
|
|
await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
|
|
controller.SelectStock(0);
|
|
|
|
controller.CutPointerDown(0, false, false, 2, 1, 1_000);
|
|
var initial = controller.CutPointerDown(0, false, false, 2, 1, 1_100);
|
|
var existing = Assert.Single(initial.Playlist);
|
|
|
|
controller.CutPointerDown(1, false, false, 2, 24, 2_000);
|
|
controller.CutPointerDown(2, true, false, 2, 47, 2_100);
|
|
var dropped = controller.DropSelectedCutsOnPlaylist();
|
|
|
|
Assert.Equal(3, dropped.Playlist.Count);
|
|
Assert.Equal(existing, dropped.Playlist[0]);
|
|
Assert.Equal(
|
|
[
|
|
LegacyStockCutCatalog.Rows[1].RawLabel,
|
|
LegacyStockCutCatalog.Rows[2].RawLabel
|
|
],
|
|
dropped.Playlist.Skip(1).Select(row => row.RawCutLabel));
|
|
}
|
|
|
|
[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")]));
|
|
}
|
|
}
|