package main import ( "strings" "testing" ) // A colour the Flex refuses is a spot that never appears, and the command error // goes to a log nobody is reading. Anything that is not #AARRGGBB must be // dropped here rather than sent. func TestHexARGB(t *testing.T) { for _, ok := range []string{"#FFFF3B30", "#00000000", "#ffabcdef", "#40FF6B22"} { if !hexARGB(ok) { t.Errorf("%q should be accepted", ok) } } for _, bad := range []string{ "", "#FFF", "#FF3B30", "FFFF3B30", "#FFFF3B3", "#FFFF3B300", "#GGFF3B30", "#FFFF3B3G", "red", "rgb(255,0,0)", } { if hexARGB(bad) { t.Errorf("%q should be refused", bad) } } } // The stored palette must survive a round trip, and must not carry a status the // resolver does not know — an unknown key would be a colour nothing can ever use // and a field nobody can remove from the panel. func TestNormSpotColors(t *testing.T) { in := SpotColors{Enabled: true, Colors: map[string]SpotColor{ "new": {Text: "#FFFF3B30", Bg: "#40FF3B30"}, "new-band": {Text: "not a colour", Bg: "#40FFCC00"}, // half valid "worked": {Text: "", Bg: ""}, // nothing at all "invented": {Text: "#FF000000"}, // not a status }} out := normSpotColors(in) if got := out.Colors["new"].Text; got != "#FFFF3B30" { t.Errorf("valid pair lost: %q", got) } if c := out.Colors["new-band"]; c.Text != "" || c.Bg != "#40FFCC00" { t.Errorf("half-valid pair = %+v, want the background kept and the text dropped", c) } if _, ok := out.Colors["worked"]; ok { t.Errorf("an entry with no colour at all must not be stored") } if _, ok := out.Colors["invented"]; ok { t.Errorf("an unknown status must not be stored") } } func TestSpotComment(t *testing.T) { // What the panadapter shows: the cluster's words, who spotted it, the entity, // then what it is worth — DXHunter's order, so an operator reads one // convention and not two. got := spotComment("CQ up 2", "F4BPO", "Franz Josef Land", "new-slot") want := "CQ up 2 [F4BPO] [Franz Josef Land] [New Slot]" if plain(got) != want { t.Fatalf("got %q, want %q", plain(got), want) } // EVERY space is non-breaking, the ones inside the cluster comment included: // SmartSDR splits its command line on spaces, so a real one truncates the // comment at the first word. if strings.Contains(got, " ") { t.Fatalf("an ordinary space survived: %q", got) } if !strings.Contains(got, " ") { t.Fatal("no non-breaking space in the comment") } // Missing parts leave no empty brackets and no double separators. if p := plain(spotComment("", "", "", "new")); p != "[New DXCC]" { t.Fatalf("got %q", p) } if p := plain(spotComment("CQ", "", "Spain", "")); p != "CQ [Spain]" { t.Fatalf("got %q", p) } // A status with nothing to announce contributes no tag. if p := plain(spotComment("loud", "DL1ABC", "Germany", "none")); p != "loud [DL1ABC] [Germany]" { t.Fatalf("got %q", p) } // Every colourable status except the unresolved one is labelled, or the // palette would say something the text does not. for _, st := range spotColorOrder { if st == "none" { continue } if spotStatusTag(st) == "" { t.Fatalf("status %q has a colour but no tag", st) } } } // plain puts the ordinary spaces back, for readable assertions. func plain(s string) string { return strings.ReplaceAll(s, " ", " ") } // The per-status switch must survive a save with no colours on that row: it is // an instruction in its own right, and dropping it would quietly turn the status // back on. And a palette written before the switch existed must keep sending // everything — "no field" is not "the operator turned this off". func TestSpotColorHideSurvivesNormalisation(t *testing.T) { in := SpotColors{Enabled: true, Colors: map[string]SpotColor{ "worked": {Hide: true}, // no colours, just the switch "new": {Text: "#FFFF3B30", Hide: true}, // both "none": {Text: "not a colour"}, // neither survives "new-band": {Text: "#FFFFCC00"}, // colour only }} out := normSpotColors(in) if c, ok := out.Colors["worked"]; !ok || !c.Hide { t.Error("a status hidden with no colour was dropped") } if c := out.Colors["new"]; !c.Hide || c.Text != "#FFFF3B30" { t.Errorf("hide and colour did not both survive: %+v", c) } if _, ok := out.Colors["none"]; ok { t.Error("an entry with nothing valid in it was kept") } if c := out.Colors["new-band"]; c.Hide { t.Error("a colour-only entry came back hidden") } // An older palette says nothing about hiding, and must send everything. old := normSpotColors(SpotColors{Enabled: true, Colors: map[string]SpotColor{"new": {Text: "#FFFF3B30"}}}) if old.Colors["new"].Hide { t.Error("a palette from before the switch existed came back hidden") } } // SmartSDR cuts a long comment mid-word — one came back from the radio as // "…[European Russia", losing its closing bracket and its status. What the // operator would keep must therefore survive: the spotter, the entity and the // status, with the cluster's own words trimmed to fit. func TestSpotCommentFitsTheRadio(t *testing.T) { long := "FT8 5 dB LN14 CQ DX FROM A VERY TALKATIVE SKIMMER NODE INDEED" got := plain(spotComment(long, "OH6BG-#", "European Russia", "new-band")) if len(got) > panCommentMax { t.Fatalf("comment is %d chars, over the %d the radio keeps: %q", len(got), panCommentMax, got) } for _, must := range []string{"[OH6BG-#]", "[European Russia]", "[New Band]"} { if !strings.Contains(got, must) { t.Errorf("%s was trimmed away — the cluster text should go first: %q", must, got) } } // A short comment is left exactly as it is. short := plain(spotComment("CQ", "K1ABC", "Spain", "new")) if short != "CQ [K1ABC] [Spain] [New DXCC]" { t.Fatalf("got %q", short) } // No room at all for the cluster's words: they go, the brackets stay. tight := plain(spotComment("CQ DX UP 2 LISTENING", "VERYLONGSPOTTER-#", "Democratic Republic of the Congo", "new")) if strings.Contains(tight, "LISTENING") { t.Errorf("kept the cluster text at the expense of the tags: %q", tight) } } // RBN lines are column-aligned, so a comment arrives as "FT8 5 dB LN14 // CQ". Preserved as non-breaking spaces, those runs rendered on the panadapter // as a gap wide enough to push the rest of the comment off the screen. func TestSpotCommentCollapsesClusterPadding(t *testing.T) { got := plain(spotComment("FT8 -13 dB CQ", "WC2L-#", "United States", "new")) want := "FT8 -13 dB CQ [WC2L-#] [United States] [New DXCC]" if got != want { t.Fatalf("got %q\nwant %q", got, want) } // Tabs and newlines are padding too. if p := plain(spotComment("CQ\t\tDX\n", "K1ABC", "", "")); p != "CQ DX [K1ABC]" { t.Fatalf("got %q", p) } }