Files
Nonemm/tests/Nonemm.Session.Tests/OperatingPositionTests.cs
ericek111 1c118f9ce4 Lay the menu bar out the way N1MM lays it out, and fill in the Edit menu
The menu tree comes from N1MM's EntryWindow.Designer.cs: File, Edit, View,
Tools, Config, Window, with the items under the names N1MM gives them. The
window list moved out of View into Window; the call history file moved from
Config into File > Import. Ctrl+W, Ctrl+L and Ctrl+M now work, so the keys
printed in the menu are real. Two commands that had no menu item before:
the CW and SSB function key definitions, and the QTC setup area.

The rest of the Edit menu:

- Ctrl+N asks for a note and puts it in the comment of the contact in the
  boxes, or of the last logged contact when nothing is typed.
- Edit Current Contact opens the Edit Contact form over what is typed but
  not logged. Update hands the values back to the entry boxes.
- Ctrl+Q and Ctrl+A load a logged contact into the boxes and paint them
  pale yellow. Enter writes the change back, Esc puts back what was being
  typed, and stepping forward past the newest contact leaves quick edit.
- Ctrl+U raises the received serial, or a numeric exchange box when the
  contest has no serial.
- Ctrl+F shows the call being typed in the log window, then the next
  contact with that call.

RadioPosition is now OperatingPosition: it is the operator at one radio,
not a place on a band.

Looked at under Xvfb on a 3775-contact log: quick edit back and forward,
Esc, the note dialog, find, and Edit Current Contact putting a zone back
into the entry boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:18:37 +00:00

543 lines
16 KiB
C#

using Nonemm.Contests;
using Nonemm.Contests.Rules;
using Nonemm.Core;
using Nonemm.Core.Calls;
using Nonemm.Core.Country;
using Nonemm.Storage;
namespace Nonemm.Session.Tests;
public class OperatingPositionTests
{
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 OperatingPosition Session(Contest? contest = null) =>
new(Shared(new FakeLogStore(), contest));
private static ContestSession Shared(FakeLogStore store, Contest? contest = null)
{
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = "CQWW",
});
return new ContestSession(
store,
contest ?? new CqWorldWide(ModeCategory.Cw),
instance,
Me,
CountryFile.Parse(Countries));
}
private static void Type(OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition 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()
{
OperatingPosition session = Session();
session.Entry.Call = "JA1XYZ";
Assert.Throws<InvalidOperationException>(() => session.LogContact());
}
[Fact]
public void TheSameStationOnTheSameBandReadsAsADupe()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "JA1XYZ", "25");
Assert.True(session.Verdict()?.IsDupe);
}
[Fact]
public void MovingBandClearsTheDupe()
{
OperatingPosition 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()
{
OperatingPosition 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 SpaceStepsOverTheReportAndComesBackToTheCall()
{
// CQ WW's boxes are the call, the report and the zone
OperatingPosition session = Session();
Assert.Equal(0, session.Entry.Focus);
session.Entry.Advance();
Assert.Equal(2, session.Entry.Focus);
session.Entry.Advance();
Assert.Equal(0, session.Entry.Focus);
}
[Fact]
public void GoingBackStepsOverTheReportToo()
{
OperatingPosition session = Session();
session.Entry.FocusOn(2);
session.Entry.Retreat();
Assert.Equal(0, session.Entry.Focus);
}
[Fact]
public void TheReportBoxesAreFilledInWithWhatWouldBeSentAnyway()
{
OperatingPosition session = Session();
Assert.True(session.FillReports());
Assert.Equal("599", session.Entry.ValueOf(ExchangeSlot.ReceivedReport));
Assert.False(session.FillReports());
}
[Fact]
public void AReportTheOperatorTypedIsLeftAlone()
{
OperatingPosition session = Session();
session.Entry.Set(ExchangeSlot.ReceivedReport, "559");
Assert.False(session.FillReports());
Assert.Equal("559", session.Entry.ValueOf(ExchangeSlot.ReceivedReport));
}
[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)
{
OperatingPosition session = Session();
session.Entry.Call = typed;
Assert.Equal(hertz, session.PendingQsy()?.Hertz);
}
[Theory]
[InlineData("JA1XYZ")]
[InlineData("12000")]
public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed)
{
OperatingPosition session = Session();
session.Entry.Call = typed;
Assert.Null(session.PendingQsy());
}
[Fact]
public void AContactDeletedFromTheLogGivesItsMultiplierBack()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso first = session.LogContact();
Type(session, "JA2XYZ", "25");
session.LogContact();
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
session.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 AContactWorkedSplitRecordsWhereWeTransmitted()
{
OperatingPosition session = Session();
session.Tune(
Frequency.FromKilohertz(14_025),
Modes.Cw,
Frequency.FromKilohertz(14_200));
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(14_025_000, logged.Frequency.Hertz);
Assert.Equal(14_200_000, logged.QsxFrequency.Hertz);
}
[Fact]
public void AContactRecordsWhichRadioMadeIt()
{
OperatingPosition session = new(Shared(new FakeLogStore()), 2);
Type(session, "JA1XYZ", "25");
Assert.Equal(2, session.LogContact().RadioNumber);
}
/// Tuning without saying anything about split leaves it where it was, so a
/// frequency typed by hand does not clear what the radio reported.
[Fact]
public void TuningWithoutASplitLeavesTheOneAlreadySet()
{
OperatingPosition session = Session();
session.Tune(Frequency.FromKilohertz(14_025), null, Frequency.FromKilohertz(14_200));
session.Tune(Frequency.FromKilohertz(14_030));
Assert.Equal(14_200_000, session.TransmitFrequency.Hertz);
}
[Fact]
public void EditingAContactRescoresTheLog()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(3, session.Log.Tally.Points);
QsoEdit edit = session.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()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
QsoEdit edit = session.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()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
QsoChange? change = null;
session.Session.Edited += (_, c) => change = c;
session.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()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Assert.Equal(3, session.Log.Tally.Points);
session.Session.Edit(logged.Id, QsoField.CountryPrefix, "DL");
Assert.Equal(0, session.Log.Tally.Points);
}
[Fact]
public void ADeleteReportsTheContactThatWent()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Qso? gone = null;
session.Session.Deleted += (_, q) => gone = q;
session.Session.Delete(logged.Id);
Assert.Equal("JA1XYZ", gone?.Call.Text);
}
[Fact]
public void StoredContactsAreReadBackWhenTheSessionStarts()
{
FakeLogStore store = new();
OperatingPosition first = new(Shared(store));
Type(first, "JA1XYZ", "25");
first.LogContact();
OperatingPosition second = new(new ContestSession(
store,
new CqWorldWide(ModeCategory.Cw),
first.Instance,
Me,
CountryFile.Parse(Countries)));
Assert.Single(second.Log.Qsos);
Assert.Equal(3, second.Log.Tally.Points);
}
private static OperatingPosition WithHistory(Contest contest, string file)
{
FakeLogStore store = new();
ContestInstance instance = store.AddContest(new ContestInstance
{
ContestNumber = 0,
ContestName = contest.Name,
});
return new OperatingPosition(new ContestSession(
store,
contest,
instance,
Me,
CountryFile.Parse(Countries),
CallHistory.Parse(file)));
}
[Fact]
public void TheCallHistoryFillsTheExchangeIn()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect,CK\nW3LPL,MDC,56");
session.Entry.Call = "W3LPL";
Assert.True(session.FillFromHistory());
Assert.Equal("MDC", session.Entry.ValueOf(ExchangeSlot.Section));
Assert.Equal("56", session.Entry.ValueOf(ExchangeSlot.Check));
}
/// What the other station actually sends beats what was published, so a box
/// with something in it is left alone.
[Fact]
public void WhatIsAlreadyTypedIsNotOverwritten()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect\nW3LPL,MDC");
session.Entry.Call = "W3LPL";
session.Entry.Set(ExchangeSlot.Section, "EPA");
session.FillFromHistory();
Assert.Equal("EPA", session.Entry.ValueOf(ExchangeSlot.Section));
}
[Fact]
public void ACallTheHistoryDoesNotHoldFillsNothing()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect\nW3LPL,MDC");
session.Entry.Call = "JA1XYZ";
Assert.False(session.FillFromHistory());
Assert.Equal("", session.Entry.ValueOf(ExchangeSlot.Section));
}
/// CQ WW exchanges the CQ zone, so the CQ zone column is the one to use.
[Fact]
public void TheZoneComesFromTheColumnTheContestAsksFor()
{
OperatingPosition session = WithHistory(
new CqWorldWide(ModeCategory.Cw),
"!!Order!!,Call,CqZone,ITUZone\nJA1XYZ,25,45");
session.Entry.Call = "JA1XYZ";
session.FillFromHistory();
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
}
/// A file for a section contest often carries the state instead.
[Fact]
public void TheStateFillsASectionWhenTheFileHasNoSection()
{
OperatingPosition session = WithHistory(
new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect,State\nK1TTT,,CT");
session.Entry.Call = "K1TTT";
session.FillFromHistory();
Assert.Equal("CT", session.Entry.ValueOf(ExchangeSlot.Section));
}
[Fact]
public void TheNameAndCommentBoxesReachTheLog()
{
OperatingPosition session = Session();
session.Entry.Call = "JA1XYZ";
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.Zone, "25");
session.OtherName = "Ken";
session.Comment = "up 2";
Qso logged = session.LogContact();
Assert.Equal("Ken", logged.Name);
Assert.Equal("up 2", logged.Comment);
Assert.Equal("", session.OtherName);
Assert.Equal("", session.Comment);
}
[Fact]
public void WipingClearsTheNameAndTheComment()
{
OperatingPosition session = Session();
session.OtherName = "Ken";
session.Comment = "up 2";
session.Wipe();
Assert.Equal("", session.OtherName);
Assert.Equal("", session.Comment);
}
[Fact]
public void CqWwRttyStepsOverTheStateBoxForAStationOutsideTheUsAndCanada()
{
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
session.Entry.Call = "JA1XYZ";
// the boxes are the call, the report, the zone and the state
session.MoveFocus(forward: true);
Assert.Equal(2, session.Entry.Focus);
session.MoveFocus(forward: true);
Assert.Equal(0, session.Entry.Focus);
}
[Fact]
public void CqWwRttyStopsOnTheStateBoxForAUsStation()
{
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
session.Entry.Call = "K1ABC";
session.MoveFocus(forward: true);
Assert.Equal(2, session.Entry.Focus);
session.MoveFocus(forward: true);
Assert.Equal(3, session.Entry.Focus);
}
[Fact]
public void QuickEditLoadsTheLastContactAndSavesTheChangeBack()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
Qso logged = session.LogContact();
Type(session, "DL9ZZZ", "14");
Assert.True(session.QuickEdit(forward: false));
Assert.Equal(logged.Id, session.Editing?.Id);
Assert.Equal("JA1XYZ", session.Entry.Call);
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
session.Entry.Set(ExchangeSlot.Zone, "24");
Qso? saved = session.SaveQuickEdit();
Assert.Equal(24, saved?.Zone);
Assert.Equal(24, session.Log.Qsos[0].Zone);
Assert.Null(session.Editing);
}
[Fact]
public void LeavingQuickEditPutsBackWhatWasBeingTyped()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "DL9ZZZ", "14");
session.QuickEdit(forward: false);
session.LeaveQuickEdit();
Assert.Null(session.Editing);
Assert.Equal("DL9ZZZ", session.Entry.Call);
Assert.Equal("14", session.Entry.ValueOf(ExchangeSlot.Zone));
}
[Fact]
public void QuickEditForwardPastTheNewestContactLeavesQuickEdit()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "DL9ZZZ", "14");
session.QuickEdit(forward: false);
Assert.True(session.QuickEdit(forward: true));
Assert.Null(session.Editing);
Assert.Equal("DL9ZZZ", session.Entry.Call);
}
[Fact]
public void QuickEditStepsBackThroughTheLog()
{
OperatingPosition session = Session();
Type(session, "JA1XYZ", "25");
session.LogContact();
Type(session, "DL9ZZZ", "14");
session.LogContact();
session.QuickEdit(forward: false);
Assert.Equal("DL9ZZZ", session.Entry.Call);
session.QuickEdit(forward: false);
Assert.Equal("JA1XYZ", session.Entry.Call);
Assert.False(session.QuickEdit(forward: false));
}
}