| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // The three legibility failures #9 caught in a browser that the render tests |
| 9 | // could not see, because each of them is a fact about pixels rather than |
| 10 | // about strings. |
| 11 | |
| 12 | // TestFlagshipBoxNamesSurviveTheirAlarms is the truncation, pinned with the |
| 13 | // exact strings it was caught on. |
| 14 | // |
| 15 | // The four flagship boxes are `carlos-flagship-au/ie/uk/us` — 18 characters, |
| 16 | // differing only in the last two. #322 stopped text overflowing its box by |
| 17 | // making the title stop before the alarm marks, which reserved |
| 18 | // len(Alarms)*14+5 pixels of the title line; with the two alarms every box on |
| 19 | // the flagship has carried since 2026-08-28 that leaves 133px, and 133px at |
| 20 | // 13px and the 0.58 glyph ratio is 17 characters. Every box truncated exactly |
| 21 | // one character before the character that tells it apart from the other |
| 22 | // three, which is the worst available place to run out of room. |
| 23 | func TestFlagshipBoxNamesSurviveTheirAlarms(t *testing.T) { |
| 24 | two := []NodeAlarm{ |
| 25 | {Name: "carlos-flagship-ie-mem-low", State: "OK"}, |
| 26 | {Name: "carlos-flagship-ie-reachability", State: "OK"}, |
| 27 | } |
| 28 | // ArchBand, not ArchView: #387 split the page into two estates drawn one |
| 29 | // above the other and #390 split each estate into one band per region, |
| 30 | // and layout/RenderArchSVG each take one band now. These pins are about |
| 31 | // a box's own pixels, so they belong to a band and are otherwise |
| 32 | // unchanged. |
| 33 | var v ArchBand |
| 34 | v.Nodes = []Node{ |
| 35 | {ID: "box:ie", Kind: "box", Title: "carlos-flagship-ie", |
| 36 | Subtitle: "t4g.medium running", Alarms: two}, |
| 37 | {ID: "box:au", Kind: "box", Title: "carlos-flagship-au", |
| 38 | Subtitle: "t4g.small running", Alarms: two}, |
| 39 | } |
| 40 | layout(&v) |
| 41 | out := string(RenderArchSVG(v)) |
| 42 | for _, want := range []string{"carlos-flagship-ie", "carlos-flagship-au"} { |
| 43 | if !strings.Contains(out, ">"+want+"<") { |
| 44 | t.Errorf("box name %q was truncated — the region is the character that tells these boxes apart:\n%s", want, out) |
| 45 | } |
| 46 | } |
| 47 | // The subtitle keeps the full width too: moving the marks down must not |
| 48 | // pay for the title out of the line below it. |
| 49 | for _, want := range []string{"t4g.medium running", "t4g.small running"} { |
| 50 | if !strings.Contains(out, ">"+want+"<") { |
| 51 | t.Errorf("subtitle %q was truncated:\n%s", want, out) |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // TestAlarmMarksClearEveryTextLine: the marks moved off the title's line, so |
| 57 | // they have to sit below the LAST baseline rather than merely somewhere else |
| 58 | // — otherwise the fix trades a truncated title for a mark drawn over the |
| 59 | // words — and inside the rect they belong to. Since #390 a box card carries |
| 60 | // Extra lines (its apps, its instance counts) under the subtitle, each |
| 61 | // nodeLineH further down, so the last baseline is not the subtitle's and the |
| 62 | // check runs at every line count a card can have. |
| 63 | func TestAlarmMarksClearEveryTextLine(t *testing.T) { |
| 64 | for _, extra := range [][]string{nil, {"console, kass"}, {"console, kass", "static 3 · resident 4"}} { |
| 65 | n := Node{ID: "box:ie", Kind: "box", Title: "carlos-flagship-ie", Y: 24, |
| 66 | Extra: extra, Alarms: []NodeAlarm{{Name: "a", State: "OK"}}} |
| 67 | n.H = nodeHeight(n) |
| 68 | // Where RenderArchSVG draws the last .archsub: the subtitle at +44, |
| 69 | // then one nodeLineH per Extra line. The mark's TOP edge has to clear |
| 70 | // the descenders below that baseline, not just its centre. |
| 71 | last := 44 + len(extra)*nodeLineH |
| 72 | const descender = 3 |
| 73 | if top := alarmRow(n) - alarmR - n.Y; top <= last+descender { |
| 74 | t.Errorf("%d extra lines: alarm top at +%d, want below the last baseline (+%d) and its descenders", len(extra), top, last) |
| 75 | } |
| 76 | if alarmRow(n)+alarmR > n.Y+n.H { |
| 77 | t.Errorf("%d extra lines: alarm at +%d (r=%d) overflows a node %d tall", len(extra), alarmRow(n)-n.Y, alarmR, n.H) |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // TestFiringAlarmsAreSummarisedForTheHeader: the marks ride their nodes, |
| 83 | // which is right, but on a drawing wider than the window they are off-screen |
| 84 | // and the Alarms table is thousands of pixels down. A page that knows |
| 85 | // something is firing has to be able to say so above the fold. |
| 86 | func TestFiringAlarmsAreSummarisedForTheHeader(t *testing.T) { |
| 87 | nodes := []Node{ |
| 88 | {ID: "box:ie", Title: "carlos-flagship-ie", Alarms: []NodeAlarm{ |
| 89 | {Name: "mem-low", State: "ALARM", Firing: true}, |
| 90 | {Name: "reachability", State: "OK"}, |
| 91 | }}, |
| 92 | {ID: "box:au", Title: "carlos-flagship-au", Alarms: []NodeAlarm{ |
| 93 | {Name: "reachability", State: "OK"}, |
| 94 | }}, |
| 95 | } |
| 96 | got := firingAlarms(nodes) |
| 97 | if len(got) != 1 { |
| 98 | t.Fatalf("firingAlarms = %v, want exactly the firing one", got) |
| 99 | } |
| 100 | // It names the alarm AND the thing it watches: "mem-low" on its own sends |
| 101 | // a reader to four near-identical box names. |
| 102 | if !strings.Contains(got[0], "mem-low") || !strings.Contains(got[0], "carlos-flagship-ie") { |
| 103 | t.Errorf("summary %q must name both the alarm and the node it watches", got[0]) |
| 104 | } |
| 105 | if len(firingAlarms(nil)) != 0 { |
| 106 | t.Error("no nodes must summarise to nothing, not to an empty-but-present banner") |
| 107 | } |
| 108 | } |
| 109 | |