using Nonemm.Contests.Rules; using Nonemm.Core; namespace Nonemm.Session.Tests; public class LogColumnsTests { private static readonly StationInfo Me = new() { Callsign = "DL1ABC", CqZone = 14, ItuZone = 28, Continent = "EU", CountryPrefix = "DL", }; private static IReadOnlyList Contest() => new QsoEditor(new CqWorldWide(ModeCategory.Cw), Me, null).Columns; [Fact] public void AnEmptyChoiceKeepsTheColumnsTheContestOpensWith() => Assert.Equal(Contest(), LogColumns.Chosen(Contest(), [])); [Fact] public void TheChosenColumnsComeBackInTheStoredOrder() { IReadOnlyList chosen = LogColumns.Chosen(Contest(), ["Call", "Time", "Comment"]); Assert.Equal([QsoField.Call, QsoField.Time, QsoField.Comment], chosen.Select(c => c.Field)); } /// The contest names its own exchange columns, so a field it asked for /// keeps that label rather than the fallback one. [Fact] public void AContestColumnKeepsTheLabelTheContestGaveIt() { QsoColumn zone = LogColumns.Chosen(Contest(), ["Zone"]).Single(); Assert.Equal(Contest().Single(c => c.Field == QsoField.Zone).Label, zone.Label); } [Fact] public void ANameThatIsNotAFieldIsDropped() => Assert.Equal( [QsoField.Call], LogColumns.Chosen(Contest(), ["Call", "Nonsense"]).Select(c => c.Field)); [Fact] public void ARepeatedNameOnlyGivesOneColumn() => Assert.Single(LogColumns.Chosen(Contest(), ["Call", "Call"])); /// Every name dropped leaves nothing to show, so the contest's own set is /// used rather than an empty grid. [Fact] public void AListOfNothingUsableFallsBackToTheContest() => Assert.Equal(Contest(), LogColumns.Chosen(Contest(), ["Nonsense"])); [Fact] public void TheMenuOffersTheContestColumnsFirstAndThenTheRest() { IReadOnlyList choices = LogColumns.Choices(Contest()); Assert.Equal(Contest(), choices.Take(Contest().Count)); Assert.Equal(choices.Select(c => c.Field).Distinct(), choices.Select(c => c.Field)); Assert.Contains(QsoField.Comment, choices.Select(c => c.Field)); } [Fact] public void EveryFieldTheLogCanShowReadsBack() => Assert.All(LogColumns.All, c => Assert.NotNull(QsoEditor.Read(Contact(), c.Field))); private static Qso Contact() => new() { Id = "1", TimestampUtc = new DateTime(2026, 5, 30, 12, 34, 56, DateTimeKind.Utc), Call = Callsign.Parse("JA1XYZ"), Frequency = Frequency.FromKilohertz(14_025), Mode = Modes.Cw, ContestName = "CQWW", Points = 3, }; /// Sorting on the text of the cell would put 14,000 kHz before 9,000 and /// serial number 10 before 2. [Fact] public void NumbersSortAsNumbersNotAsTheTextTheLogShows() { Qso low = Contact() with { Frequency = Frequency.FromKilohertz(9_000), ReceivedNumber = 2 }; Qso high = Contact() with { Frequency = Frequency.FromKilohertz(14_000), ReceivedNumber = 10 }; Assert.True(Key(low, QsoField.Frequency).CompareTo(Key(high, QsoField.Frequency)) < 0); Assert.True(Key(low, QsoField.ReceivedNumber).CompareTo(Key(high, QsoField.ReceivedNumber)) < 0); } [Fact] public void TheTimeSortsAsATime() { Qso first = Contact(); Qso second = Contact() with { TimestampUtc = first.TimestampUtc.AddHours(1) }; Assert.True(Key(first, QsoField.Time).CompareTo(Key(second, QsoField.Time)) < 0); } [Fact] public void ATextFieldSortsOnWhatTheLogShows() => Assert.Equal( QsoEditor.Read(Contact(), QsoField.Call), Key(Contact(), QsoField.Call)); [Fact] public void EveryColumnHasASortKey() => Assert.All(LogColumns.All, c => Assert.NotNull(Key(Contact(), c.Field))); private static IComparable Key(Qso qso, QsoField field) => LogColumns.SortKey(qso, field); [Fact] public void StoreAndChosenRoundTrip() { IReadOnlyList chosen = LogColumns.Chosen(Contest(), ["Comment", "Call"]); Assert.Equal(chosen, LogColumns.Chosen(Contest(), LogColumns.Store(chosen))); } }