package console import ( "strings" "testing" ) // The three legibility failures #9 caught in a browser that the render tests // could not see, because each of them is a fact about pixels rather than // about strings. // TestFlagshipBoxNamesSurviveTheirAlarms is the truncation, pinned with the // exact strings it was caught on. // // The four flagship boxes are `carlos-flagship-au/ie/uk/us` — 18 characters, // differing only in the last two. #322 stopped text overflowing its box by // making the title stop before the alarm marks, which reserved // len(Alarms)*14+5 pixels of the title line; with the two alarms every box on // the flagship has carried since 2026-08-28 that leaves 133px, and 133px at // 13px and the 0.58 glyph ratio is 17 characters. Every box truncated exactly // one character before the character that tells it apart from the other // three, which is the worst available place to run out of room. func TestFlagshipBoxNamesSurviveTheirAlarms(t *testing.T) { two := []NodeAlarm{ {Name: "carlos-flagship-ie-mem-low", State: "OK"}, {Name: "carlos-flagship-ie-reachability", State: "OK"}, } // ArchBand, not ArchView: #387 split the page into two estates drawn one // above the other and #390 split each estate into one band per region, // and layout/RenderArchSVG each take one band now. These pins are about // a box's own pixels, so they belong to a band and are otherwise // unchanged. var v ArchBand v.Nodes = []Node{ {ID: "box:ie", Kind: "box", Title: "carlos-flagship-ie", Subtitle: "t4g.medium running", Alarms: two}, {ID: "box:au", Kind: "box", Title: "carlos-flagship-au", Subtitle: "t4g.small running", Alarms: two}, } layout(&v) out := string(RenderArchSVG(v)) for _, want := range []string{"carlos-flagship-ie", "carlos-flagship-au"} { if !strings.Contains(out, ">"+want+"<") { t.Errorf("box name %q was truncated — the region is the character that tells these boxes apart:\n%s", want, out) } } // The subtitle keeps the full width too: moving the marks down must not // pay for the title out of the line below it. for _, want := range []string{"t4g.medium running", "t4g.small running"} { if !strings.Contains(out, ">"+want+"<") { t.Errorf("subtitle %q was truncated:\n%s", want, out) } } } // TestAlarmMarksClearEveryTextLine: the marks moved off the title's line, so // they have to sit below the LAST baseline rather than merely somewhere else // — otherwise the fix trades a truncated title for a mark drawn over the // words — and inside the rect they belong to. Since #390 a box card carries // Extra lines (its apps, its instance counts) under the subtitle, each // nodeLineH further down, so the last baseline is not the subtitle's and the // check runs at every line count a card can have. func TestAlarmMarksClearEveryTextLine(t *testing.T) { for _, extra := range [][]string{nil, {"console, kass"}, {"console, kass", "static 3 · resident 4"}} { n := Node{ID: "box:ie", Kind: "box", Title: "carlos-flagship-ie", Y: 24, Extra: extra, Alarms: []NodeAlarm{{Name: "a", State: "OK"}}} n.H = nodeHeight(n) // Where RenderArchSVG draws the last .archsub: the subtitle at +44, // then one nodeLineH per Extra line. The mark's TOP edge has to clear // the descenders below that baseline, not just its centre. last := 44 + len(extra)*nodeLineH const descender = 3 if top := alarmRow(n) - alarmR - n.Y; top <= last+descender { t.Errorf("%d extra lines: alarm top at +%d, want below the last baseline (+%d) and its descenders", len(extra), top, last) } if alarmRow(n)+alarmR > n.Y+n.H { t.Errorf("%d extra lines: alarm at +%d (r=%d) overflows a node %d tall", len(extra), alarmRow(n)-n.Y, alarmR, n.H) } } } // TestFiringAlarmsAreSummarisedForTheHeader: the marks ride their nodes, // which is right, but on a drawing wider than the window they are off-screen // and the Alarms table is thousands of pixels down. A page that knows // something is firing has to be able to say so above the fold. func TestFiringAlarmsAreSummarisedForTheHeader(t *testing.T) { nodes := []Node{ {ID: "box:ie", Title: "carlos-flagship-ie", Alarms: []NodeAlarm{ {Name: "mem-low", State: "ALARM", Firing: true}, {Name: "reachability", State: "OK"}, }}, {ID: "box:au", Title: "carlos-flagship-au", Alarms: []NodeAlarm{ {Name: "reachability", State: "OK"}, }}, } got := firingAlarms(nodes) if len(got) != 1 { t.Fatalf("firingAlarms = %v, want exactly the firing one", got) } // It names the alarm AND the thing it watches: "mem-low" on its own sends // a reader to four near-identical box names. if !strings.Contains(got[0], "mem-low") || !strings.Contains(got[0], "carlos-flagship-ie") { t.Errorf("summary %q must name both the alarm and the node it watches", got[0]) } if len(firingAlarms(nil)) != 0 { t.Error("no nodes must summarise to nothing, not to an empty-but-present banner") } }