A DXpeditions tab holding the two feeds the DX world announces itself on: NG3K's ADXO (structured — dates, entity, calls, bands, modes, QSL route) and DX-World's headlines. What a logger can say that a news reader cannot is whether the operation is worth chasing, so every announcement is put through the SAME verdict the cluster paints on a spot — one badge, strongest wins, dimmed when the need is only a missing QSL — with an 'only what I need' filter. One click watches every callsign of an operation; the news headlines are mined for callsigns so they can be watched the same way. Parsers ported from DXHunter's and pinned with table tests against real feed text: the 'as PJ2/W2APF' mining, the DXCC-prefix-first normalisation without which no spot ever matches, both ADXO date forms, and the '160-6m' span whose unit is written once (DXHunter read that as 6m alone and lost the low end). Watchlist membership now announces itself app-wide, on the same card as a new version: the bindings emit watchlist:changed, so a call added from the cluster or this tab is confirmed where the operator is actually looking — the panel's own inline message never was.
87 lines
3.0 KiB
Go
87 lines
3.0 KiB
Go
package dxped
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Pinned against REAL feed text: the parser reads a fixed sentence, and a
|
|
// wrong split silently empties a DXpedition list rather than failing loudly.
|
|
func TestParseActivation(t *testing.T) {
|
|
desc := "Aug 24-31, 2026 -- St Kitts and Nevis -- V47JA -- QSL: LoTW -- " +
|
|
"Source: W5JON (Aug 1, 2026) -- By W5JON as V47JA fm Calypso Bay; 160-6m; SSB FT8; yagi, verticals"
|
|
a := parseActivation(desc, "https://www.qrz.com/lookup/v47ja")
|
|
if a == nil {
|
|
t.Fatal("parseActivation returned nil on a real ADXO description")
|
|
}
|
|
if a.DXCC != "St Kitts and Nevis" {
|
|
t.Errorf("DXCC = %q", a.DXCC)
|
|
}
|
|
if a.Callsign != "V47JA" {
|
|
t.Errorf("Callsign = %q, want V47JA (mined from 'as')", a.Callsign)
|
|
}
|
|
if a.QSL != "LoTW" {
|
|
t.Errorf("QSL = %q", a.QSL)
|
|
}
|
|
if a.StartDate != "Aug 24, 2026" || a.EndDate != "Aug 31, 2026" {
|
|
t.Errorf("dates = %q..%q", a.StartDate, a.EndDate)
|
|
}
|
|
if want := []string{"160m", "6m"}; !reflect.DeepEqual(a.Bands, want) {
|
|
t.Errorf("Bands = %v, want %v", a.Bands, want)
|
|
}
|
|
if want := []string{"SSB", "FT8"}; !reflect.DeepEqual(a.Modes, want) {
|
|
t.Errorf("Modes = %v, want %v", a.Modes, want)
|
|
}
|
|
}
|
|
|
|
// The callsign column often holds only the prefix; the operators prose holds
|
|
// the real thing, and a slashed call must lead with the DXCC prefix or no spot
|
|
// will ever match it.
|
|
func TestCallsAfterAsAndNormalise(t *testing.T) {
|
|
if got := callsAfterAs("SQ2RAD as VP2EAD, M0PLX as VP2ELX"); !reflect.DeepEqual(got, []string{"VP2EAD", "VP2ELX"}) {
|
|
t.Errorf("callsAfterAs = %v", got)
|
|
}
|
|
if got := normalizeCall("JG8NQJ/JD1", "JD1"); got != "JD1/JG8NQJ" {
|
|
t.Errorf("normalizeCall = %q, want JD1/JG8NQJ", got)
|
|
}
|
|
if got := normalizeCall("PJ2/W2APF", "PJ2"); got != "PJ2/W2APF" {
|
|
t.Errorf("normalizeCall rewrote an already-correct call: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestActivationStatus(t *testing.T) {
|
|
now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
|
|
cases := []struct{ start, end, want string }{
|
|
{"Aug 24, 2026", "Aug 31, 2026", "active"},
|
|
{"Sep 10, 2026", "Sep 20, 2026", "upcoming"},
|
|
{"Aug 1, 2026", "Aug 10, 2026", "ended"},
|
|
{"Aug 24, 2026", "Aug 27, 2026", "active"}, // ends TODAY: still on
|
|
{"garbage", "garbage", "upcoming"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := activationStatus(c.start, c.end, now); got != c.want {
|
|
t.Errorf("activationStatus(%q,%q) = %q, want %q", c.start, c.end, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mining a headline must find calls without inventing them out of jargon.
|
|
func TestCallsInHeadline(t *testing.T) {
|
|
cases := []struct {
|
|
title string
|
|
want []string
|
|
}{
|
|
{"3B7M, St Brandon", []string{"3B7M"}},
|
|
{"TX5S team lands on Clipperton", []string{"TX5S"}},
|
|
{"FT8 activity on 160m in 2026", nil},
|
|
{"VP6D QSL via OQRS, LoTW", []string{"VP6D"}},
|
|
{"JD1/JG8NQJ from Minami Torishima", []string{"JD1/JG8NQJ"}},
|
|
}
|
|
for _, c := range cases {
|
|
if got := callsInHeadline(c.title); !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("callsInHeadline(%q) = %v, want %v", c.title, got, c.want)
|
|
}
|
|
}
|
|
}
|