Files
Nonemm/tests/Nonemm.Session.Tests/LoggingSessionTests.cs
ericek111 b355b9b6bf Add the edit contact dialog
Ctrl+Y from the entry window, or Enter on a row in the log, opens the whole
contact in one dialog. It reaches the fields the log has no column for: QSX
frequency, name, QTH, comment, grid, power, radio number, run position, and the
country and WPX prefixes. Previous and Next walk the log without closing and
offer to save first.

The dialog checks values through the same QsoEditor the log columns use, so a
zone the contest will not take is refused the same way in both places.
QsoEdit now names the field that was refused, so the dialog can put the cursor
back in the right box, and QsoEditor.ApplyAll changes several fields at once.

Points and the multiplier flags are shown but not editable. They are worked out
from the rules every time the log changes, so a typed value would be lost on
the next edit. N1MM lets you type them; we do not, rather than offering a box
that does nothing.

A contact's own country prefix now wins over the country file when scoring.
Without this, correcting the country in the dialog would change what the log
shows and what Cabrillo says, but not the score.

LoggingSession.Update now broadcasts the change like Edit does, so a contact
saved from the dialog reaches the other stations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 15:38:11 +00:00

255 lines
7.8 KiB
C#

using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Country;
using Nonemm.Storage;
namespace Nonemm.Session.Tests;
public class LoggingSessionTests
{
private const string Countries = """
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
DL,DK,DJ;
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
JA,JH,JR;
""";
private static readonly StationInfo Me = new()
{
Callsign = "DL1ABC",
CqZone = 14,
ItuZone = 28,
Continent = "EU",
CountryPrefix = "DL",
};
private static LoggingSession Session(Contest? contest = null)
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = "CQWW",
});
return new LoggingSession(
store,
contest ?? new CqWorldWide(ModeCategory.Cw),
instance,
Me,
CountryFile.Parse(Countries));
}
private static void Type(LoggingSession session, string call, string zone)
{
session.Entry.Call = call;
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.Zone, zone);
}
[Fact]
public void LoggingAContactPutsItInTheLogAndClearsTheEntry()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal("JA1XYZ", logged.Call.Text);
Assert.Equal(25, logged.Zone);
Assert.Equal(3, logged.Points);
Assert.Equal("", session.Entry.Call);
Assert.Single(session.Log.Qsos);
}
[Fact]
public void CountryAndPrefixAreFilledInFromTheCountryFile()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal("JA", logged.CountryPrefix);
Assert.Equal("AS", logged.Continent);
Assert.Equal("JA1", logged.WpxPrefix);
}
[Fact]
public void AnIncompleteExchangeIsNotLogged()
{
LoggingSession session = Session();
session.Entry.Call = "JA1XYZ";
Assert.Throws<InvalidOperationException>(() => session.LogContact());
}
[Fact]
public void TheSameStationOnTheSameBandReadsAsADupe()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "JA1XYZ", "25");
Assert.True(session.Verdict()?.IsDupe);
}
[Fact]
public void MovingBandClearsTheDupe()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
session.Tune(Frequency.FromKilohertz(7_025));
Type(session, "JA1XYZ", "25");
Assert.False(session.Verdict()?.IsDupe);
}
[Fact]
public void SerialNumbersCountUp()
{
LoggingSession session = Session(new CqWpx(ModeCategory.Cw));
Assert.Equal(1, session.SentNumber);
session.Entry.Call = "JA1XYZ";
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
session.LogContact();
Assert.Equal(2, session.SentNumber);
}
[Fact]
public void SpaceMovesThroughTheBoxesAndBackToTheCall()
{
LoggingSession session = Session();
Assert.Equal(0, session.Entry.Focus);
session.Entry.Advance();
Assert.Equal(1, session.Entry.Focus);
session.Entry.Advance();
session.Entry.Advance();
Assert.Equal(0, session.Entry.Focus);
}
[Theory]
[InlineData("14025", 14_025_000)]
[InlineData("14.025", 14_025_000)]
[InlineData("7025.5", 7_025_500)]
public void AFrequencyTypedIntoTheCallBoxIsRecognised(string typed, long hertz)
{
LoggingSession session = Session();
session.Entry.Call = typed;
Assert.Equal(hertz, session.PendingQsy()?.Hertz);
}
[Theory]
[InlineData("JA1XYZ")]
[InlineData("12000")]
public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed)
{
LoggingSession session = Session();
session.Entry.Call = typed;
Assert.Null(session.PendingQsy());
}
[Fact]
public void AContactDeletedFromTheLogGivesItsMultiplierBack()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso first = session.LogContact();
Type(session, "JA2XYZ", "25");
session.LogContact();
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
session.Delete(first.Id);
Assert.Single(session.Log.Qsos);
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
Assert.True(session.Log.Qsos.Single().IsMultiplier1);
}
[Fact]
public void EditingAContactRescoresTheLog()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(3, session.Log.Tally.Points);
QsoEdit edit = session.Edit(logged.Id, QsoField.Call, "DL2XYZ");
Assert.True(edit.IsAccepted);
Assert.Equal("DL2XYZ", session.Log.Qsos.Single().Call.Text);
Assert.Equal(0, session.Log.Tally.Points);
}
[Fact]
public void ARefusedEditLeavesTheLogAlone()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
QsoEdit edit = session.Edit(logged.Id, QsoField.Zone, "41");
Assert.False(edit.IsAccepted);
Assert.Equal(25, session.Log.Qsos.Single().Zone);
}
/// The other stations need the call and time the contact had before, because
/// that is how they find their own copy of it.
[Fact]
public void AnEditReportsWhatTheContactUsedToBe()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
QsoChange? change = null;
session.Edited += (_, c) => change = c;
session.Edit(logged.Id, QsoField.Call, "JA2XYZ");
Assert.Equal("JA1XYZ", change?.OldCall);
Assert.Equal(logged.TimestampUtc, change?.OldTimestampUtc);
Assert.Equal("JA2XYZ", change?.Qso.Call.Text);
}
/// The country file gets a call wrong now and then. Correcting the country
/// by hand has to change the score, or the correction is decoration.
[Fact]
public void CorrectingTheCountryChangesTheScore()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(3, session.Log.Tally.Points);
session.Edit(logged.Id, QsoField.CountryPrefix, "DL");
Assert.Equal(0, session.Log.Tally.Points);
}
[Fact]
public void ADeleteReportsTheContactThatWent()
{
LoggingSession session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Qso? gone = null;
session.Deleted += (_, q) => gone = q;
session.Delete(logged.Id);
Assert.Equal("JA1XYZ", gone?.Call.Text);
}
[Fact]
public void StoredContactsAreReadBackWhenTheSessionStarts()
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance { ContestNumber = 0, ContestName = "CQWW" });
LoggingSession first = new(store, new CqWorldWide(ModeCategory.Cw), instance, Me, CountryFile.Parse(Countries));
Type(first, "JA1XYZ", "25");
first.LogContact();
LoggingSession second = new(store, new CqWorldWide(ModeCategory.Cw), instance, Me, CountryFile.Parse(Countries));
Assert.Single(second.Log.Qsos);
Assert.Equal(3, second.Log.Tally.Points);
}
}