Going down N1MM's published list, the ones a contest operator would miss:
| | |
|---|---|
| `{LASTEXCH}` | what the last station sent, built from the boxes the contest asks for |
| `{ROVERQTH}` | where a rover is now |
| `{TIME}`, `{DATE}`, `{TIMEGMT}`, `{DATEGMT}` | the clock and the calendar |
| `{ESC}`, `{CTRL-A}` to `{CTRL-Z}` | the control characters a TNC reads as commands |
| `{CTRLHOME}` | the cursor back in the callsign box |
| `{F1}` to `{F12}` | another of this radio's keys, where `{CTRLF1}` is the other radio's |
| `{CQ}` | makes any key a CQ key: it starts running and carries alternating CQ on |
| `{DIGQTCS}`, `{DIGQTCR}` | opens the WAE QTC window the way asked for |
| `{CLEARRIT}` | the receive offset back to zero, which is rigctld's `J 0` |
| `{FORCELOG}` | logs what is in the boxes, holes and all |
`{FORCELOG}` is the one that gives something up. `LogContact` refuses an entry
that is short or holds a value the contest will not take, so that a caller who
has not checked cannot log nonsense; `LogContactAnyway` is the way past it,
because a contact in the log with a hole in it can be filled in afterwards and
one that was never logged cannot. It still needs a callsign, and the status
line says the row will need fixing before the log goes in.
A key that names itself would send for ever, so a key sending another key
ignores the macro a second time down.
What is still passed over is written down in the README and unfinished.md.
Advanced voicing is the one that matters: N1MM says a callsign or a serial by
playing a recording per letter out of `LettersFiles`, which is why `!` and `#`
say nothing on phone here and why F5 is empty in N1MM's own SSB file too. The
three packet BBS date formats are passed over because there is nothing here to
check a guess at them against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
812 lines
25 KiB
C#
812 lines
25 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",
|
|
};
|
|
|
|
/// N1MM's `{FORCELOG}`: a contact the checks refuse still goes in the log,
|
|
/// because one that was never logged cannot be fixed afterwards.
|
|
[Fact]
|
|
public void AForcedContactIsLoggedWithTheExchangeShort()
|
|
{
|
|
OperatingPosition session = Session();
|
|
session.Entry.Call = "JA1XYZ";
|
|
|
|
Assert.False(session.IsEntryComplete);
|
|
Assert.Equal("JA1XYZ", session.LogContactAnyway().Call.Text);
|
|
Assert.Single(session.Log.Qsos);
|
|
}
|
|
|
|
[Fact]
|
|
public void AForcedContactStillNeedsACallsign()
|
|
{
|
|
OperatingPosition session = Session();
|
|
|
|
Assert.Throws<InvalidOperationException>(() => session.LogContactAnyway());
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// N1MM shows what this station sends beside what it copies, keeps both
|
|
/// filled in and steps over them.
|
|
[Fact]
|
|
public void TheSentBoxesAreFilledInAndSteppedOver()
|
|
{
|
|
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
|
|
|
|
Assert.Equal("599", session.Entry.ValueOf(ExchangeSlot.SentReport));
|
|
Assert.Equal("1", session.Entry.ValueOf(ExchangeSlot.SentNumber));
|
|
|
|
// the boxes are the call, the two reports, the sent serial and the
|
|
// serial being copied
|
|
session.Entry.Advance();
|
|
Assert.Equal(4, session.Entry.Focus);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheSentSerialGoesUpWhenAContactIsLogged()
|
|
{
|
|
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
|
|
session.Entry.Call = "JA1XYZ";
|
|
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
|
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
|
|
|
|
Assert.Equal(1, session.LogContact().SentNumber);
|
|
Assert.Equal("2", session.Entry.ValueOf(ExchangeSlot.SentNumber));
|
|
}
|
|
|
|
/// The BARTG contest sends the time of the exchange, and logs the time it
|
|
/// sent rather than the time the contact was logged.
|
|
[Fact]
|
|
public void TheTimeTheExchangeWentOutIsLoggedForBartg()
|
|
{
|
|
OperatingPosition session = Session(new BartgSprint());
|
|
session.Entry.Call = "JA1XYZ";
|
|
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
|
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
|
|
session.Entry.Set(ExchangeSlot.Exchange1, "1646");
|
|
session.StampSentTime();
|
|
string stamped = session.SentTime;
|
|
|
|
Qso logged = session.LogContact();
|
|
|
|
Assert.Equal(4, stamped.Length);
|
|
Assert.Equal(stamped, logged.MiscText);
|
|
Assert.Equal("", session.SentTime);
|
|
}
|
|
|
|
/// A contest that does not exchange a time keeps the column empty.
|
|
[Fact]
|
|
public void NothingIsStampedForAContestThatSendsNoTime()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
session.StampSentTime();
|
|
|
|
Assert.Equal("", session.SentTime);
|
|
Assert.Equal("", session.LogContact().MiscText);
|
|
}
|
|
|
|
[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 two reports and the zone
|
|
OperatingPosition session = Session();
|
|
|
|
Assert.Equal(0, session.Entry.Focus);
|
|
session.Entry.Advance();
|
|
Assert.Equal(3, session.Entry.Focus);
|
|
session.Entry.Advance();
|
|
Assert.Equal(0, session.Entry.Focus);
|
|
}
|
|
|
|
[Fact]
|
|
public void GoingBackStepsOverTheReportToo()
|
|
{
|
|
OperatingPosition session = Session();
|
|
session.Entry.FocusOn(3);
|
|
|
|
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("56MDC", session.Entry.ValueOf(ExchangeSlot.Exchange1));
|
|
}
|
|
|
|
/// 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.Exchange1, "72A 74EPA");
|
|
|
|
session.FillFromHistory();
|
|
|
|
Assert.Equal("72A 74EPA", session.Entry.ValueOf(ExchangeSlot.Exchange1));
|
|
}
|
|
|
|
[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.Exchange1));
|
|
}
|
|
|
|
/// 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("00CT", session.Entry.ValueOf(ExchangeSlot.Exchange1));
|
|
}
|
|
|
|
[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 two reports, the zone and the state
|
|
session.MoveFocus(forward: true);
|
|
Assert.Equal(3, 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(3, session.Entry.Focus);
|
|
session.MoveFocus(forward: true);
|
|
Assert.Equal(4, 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));
|
|
}
|
|
|
|
/// The entry boxes and the log window take the same values. A zone that
|
|
/// does not exist is refused in both, and the box holding it is named so
|
|
/// the entry window can put the cursor back in it.
|
|
[Fact]
|
|
public void AZoneThatDoesNotExistCannotBeLogged()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "41");
|
|
|
|
EntryProblem? problem = session.EntryProblem();
|
|
|
|
Assert.Equal("CQ zones run from 1 to 40", problem!.Message);
|
|
Assert.Equal("41", session.Entry[problem.Field]);
|
|
Assert.Throws<InvalidOperationException>(session.LogContact);
|
|
}
|
|
|
|
[Fact]
|
|
public void ArbitraryTextInTheReportBoxCannotBeLogged()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
session.Entry.Set(ExchangeSlot.ReceivedReport, "good copy");
|
|
|
|
Assert.Contains("signal report", session.EntryProblem()!.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ACallThatIsNotShapedLikeOneCannotBeLogged()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "12345", "25");
|
|
|
|
Assert.Equal(0, session.EntryProblem()!.Field);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnExchangeTheContestTakesGivesNoProblem()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "ja1xyz", "25");
|
|
|
|
Assert.Null(session.EntryProblem());
|
|
}
|
|
|
|
/// Nothing is typed yet when the operator is between contacts, and that is
|
|
/// not something to complain about.
|
|
[Fact]
|
|
public void EmptyBoxesGiveNoProblem() =>
|
|
Assert.Null(Session().EntryProblem());
|
|
|
|
/// N1MM's Alt+W goes both ways: it wipes the boxes, and pressed again on
|
|
/// empty boxes it puts back what it wiped.
|
|
[Fact]
|
|
public void AltWWipesAndThenPutsTheBoxesBack()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
|
|
session.WipeOrRestore();
|
|
Assert.Equal("", session.Entry.Call);
|
|
|
|
session.WipeOrRestore();
|
|
Assert.Equal("JA1XYZ", session.Entry.Call);
|
|
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
|
|
}
|
|
|
|
/// A wipe keeps what was in the boxes and Alt+W puts it back.
|
|
[Fact]
|
|
public void WhatAWipeClearedComesBack()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
session.Comment = "loud";
|
|
|
|
session.Wipe();
|
|
Assert.Equal("", session.Entry.Call);
|
|
|
|
Assert.True(session.RestoreWiped());
|
|
Assert.Equal("JA1XYZ", session.Entry.Call);
|
|
Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone));
|
|
Assert.Equal("loud", session.Comment);
|
|
}
|
|
|
|
[Fact]
|
|
public void ThereIsNothingToPutBackBeforeAWipe() =>
|
|
Assert.False(Session().RestoreWiped());
|
|
|
|
/// Wiping empty boxes would otherwise throw away what the wipe before it
|
|
/// kept.
|
|
[Fact]
|
|
public void ASecondWipeKeepsTheFirstOne()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
|
|
session.Wipe();
|
|
session.Wipe();
|
|
|
|
Assert.True(session.RestoreWiped());
|
|
Assert.Equal("JA1XYZ", session.Entry.Call);
|
|
}
|
|
|
|
/// N1MM puts the boxes back only when they are empty, so typing is never
|
|
/// overwritten by a restore.
|
|
[Fact]
|
|
public void NothingIsPutBackOverSomethingTyped()
|
|
{
|
|
OperatingPosition session = Session();
|
|
Type(session, "JA1XYZ", "25");
|
|
session.Wipe();
|
|
session.Entry.Call = "DL2XYZ";
|
|
|
|
Assert.False(session.RestoreWiped());
|
|
Assert.Equal("DL2XYZ", session.Entry.Call);
|
|
}
|
|
|
|
/// A call put on the stack has not gone anywhere, so there is nothing to
|
|
/// put back and Alt+W would only make a second copy of it.
|
|
[Fact]
|
|
public void AStackedCallIsNotWaitingToBePutBack()
|
|
{
|
|
OperatingPosition session = Session();
|
|
session.IsRunning = true;
|
|
Type(session, "JA1XYZ", "25");
|
|
|
|
Assert.True(session.StackAnother());
|
|
Assert.False(session.RestoreWiped());
|
|
}
|
|
|
|
/// N1MM takes the serial number at the first character of the callsign, not
|
|
/// at logging, so the number is on the network while the contact is still
|
|
/// being worked. A second position takes the next one.
|
|
[Fact]
|
|
public void TypingACallTakesTheSerialNumber()
|
|
{
|
|
ContestSession shared = Shared(new FakeLogStore(), new CqWpx(ModeCategory.Cw));
|
|
OperatingPosition run = new(shared, 1);
|
|
OperatingPosition mult = new(shared, 2);
|
|
|
|
run.Entry.Call = "J";
|
|
|
|
Assert.Equal(1, run.SentNumber);
|
|
Assert.Equal(2, mult.SentNumber);
|
|
}
|
|
|
|
/// Without the pool both positions showed the same number until one of them
|
|
/// logged. The number the other station takes counts here as well.
|
|
[Fact]
|
|
public void ANumberAnotherStationTookIsNotSentHere()
|
|
{
|
|
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
|
|
|
|
session.Session.Numbers.Note(5, Bands.Band20M);
|
|
|
|
Assert.Equal(6, session.SentNumber);
|
|
}
|
|
|
|
/// The number is held until the contact is logged, so a wipe keeps it and
|
|
/// the next contact takes a fresh one.
|
|
[Fact]
|
|
public void TheNumberIsHeldUntilTheContactIsLogged()
|
|
{
|
|
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
|
|
session.Entry.Call = "JA1XYZ";
|
|
Assert.Equal(1, session.SentNumber);
|
|
|
|
session.Wipe();
|
|
Assert.Equal(1, session.SentNumber);
|
|
|
|
session.Entry.Call = "JA2XYZ";
|
|
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
|
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
|
|
Assert.Equal(1, session.LogContact().SentNumber);
|
|
|
|
session.Entry.Call = "JA3XYZ";
|
|
Assert.Equal(2, session.SentNumber);
|
|
}
|
|
|
|
/// A contest without serial numbers has nothing to take.
|
|
[Fact]
|
|
public void NothingIsTakenInAContestThatSendsNoNumbers()
|
|
{
|
|
OperatingPosition session = Session();
|
|
|
|
session.Entry.Call = "JA1XYZ";
|
|
|
|
Assert.Equal(1, session.Session.Numbers.Next(Bands.Band20M));
|
|
}
|
|
}
|