d5f2e47ea1
- Apps card: title row (logo top-aligned, name, address cut with an ellipsis and full in title) over a wrapping .stack-tags row; the tags no longer share a space-between row with the name. - Phone (768px block): .page-header wraps; the share button drops to its own line, text whole. - GetBannerAlerts(page, lang): the layout's filter before the cap; baseData and /monitoring use it. - Tests: TestStackCardHeaderLayout, TestR907_PageHeaderWrapsOnAPhone, TestR906_* (all red against 05e12921). - No Hungarian string changed. The three stacks parity fixtures are re-captured in the next commit (this commit alone fails TestI18nParity on them; both are pushed together). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
138 lines
5.5 KiB
Go
138 lines
5.5 KiB
Go
package web
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Dashboard layout fixes, 2026-10-08 (REPORT-dashboard-layout.md). A Go test cannot measure pixels; these pin the
|
|
// markup and the rules the layout stands on, and the screenshots (felhom.eu/documentation/audits/
|
|
// dashboard-layout-2026-10-08/) show the consequence.
|
|
|
|
// cssRule returns the body of the top-level rule whose selector line is exactly `selector {`.
|
|
func cssRule(t *testing.T, css, selector string) string {
|
|
t.Helper()
|
|
i := strings.Index(css, "\n"+selector+" {")
|
|
if i < 0 {
|
|
t.Fatalf("style.css has no top-level %q rule", selector)
|
|
}
|
|
body := css[i+len(selector)+3:]
|
|
return body[:strings.IndexByte(body, '}')]
|
|
}
|
|
|
|
// The Apps card header. Before: the title row and every tag were siblings in ONE space-between flex row; with two
|
|
// or more tags (any installed app) the one-word name ran under them, and the logo was centred on the name block,
|
|
// so a card with an address carried it lower than one without.
|
|
//
|
|
// RED-PROOF (REPORT): against 05e12921's stacks.html and style.css this fails — no .stack-tags, a tag before it,
|
|
// no address title, and the header rule still `justify-content: space-between`.
|
|
func TestStackCardHeaderLayout(t *testing.T) {
|
|
s := i18nTestServer(t)
|
|
var full i18nCase
|
|
for _, c := range i18nCases() {
|
|
if c.name == "stacks_full" {
|
|
full = c
|
|
}
|
|
}
|
|
if full.name == "" {
|
|
t.Fatal("no stacks_full parity case")
|
|
}
|
|
addrRe := regexp.MustCompile(`<a class="subdomain-link" href="https://([^"]+)" target="_blank" title="([^"]*)">\s*<span class="subdomain-text">([^<]*)</span> ↗\s*</a>`)
|
|
for _, lang := range []string{"hu", "en"} {
|
|
page := renderI18nCase(t, s, lang, full)
|
|
cards := strings.Split(page, `<div class="stack-detail-header">`)[1:]
|
|
if len(cards) != 8 {
|
|
t.Fatalf("%s: %d card headers, want 8", lang, len(cards))
|
|
}
|
|
addresses := 0
|
|
for i, c := range cards {
|
|
head := c[:strings.Index(c, `<div class="metarows">`)]
|
|
title := strings.Index(head, `<div class="stack-title-row">`)
|
|
tags := strings.Index(head, `<div class="stack-tags">`)
|
|
if title < 0 || tags < 0 || tags < title {
|
|
t.Errorf("%s card %d: want the title row, then a .stack-tags row (title %d, tags %d)", lang, i, title, tags)
|
|
continue
|
|
}
|
|
if first := strings.Index(head, `class="tag `); first < tags {
|
|
t.Errorf("%s card %d: a tag outside the tags row (at %d, row at %d)", lang, i, first, tags)
|
|
}
|
|
if !strings.Contains(head[title:tags], `<div class="stack-title-text">`) {
|
|
t.Errorf("%s card %d: the name block has no .stack-title-text (it carries min-width: 0)", lang, i)
|
|
}
|
|
if strings.Contains(head, "subdomain-link") {
|
|
m := addrRe.FindStringSubmatch(head)
|
|
if m == nil || m[1] != m[2] || m[2] != m[3] {
|
|
t.Errorf("%s card %d: the address must be one ellipsis span with the full address in title: %q", lang, i, m)
|
|
}
|
|
addresses++
|
|
}
|
|
}
|
|
if addresses != 2 {
|
|
t.Errorf("%s: %d addresses, want 2 (filebrowser, privatebin)", lang, addresses)
|
|
}
|
|
}
|
|
|
|
data, err := StyleCSS()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
css := string(data)
|
|
if h := cssRule(t, css, ".stack-detail-header"); strings.Contains(h, "space-between") || !strings.Contains(h, "flex-direction: column") {
|
|
t.Errorf(".stack-detail-header must stack the title row over the tags, not spread them on one row: %s", h)
|
|
}
|
|
if r := cssRule(t, css, ".stack-title-row"); !strings.Contains(r, "align-items: flex-start") {
|
|
t.Errorf(".stack-title-row must align the logo to the top of the name: %s", r)
|
|
}
|
|
if r := cssRule(t, css, ".stack-title-text"); !strings.Contains(r, "min-width: 0") {
|
|
t.Errorf(".stack-title-text needs min-width: 0, or a long address cannot shrink: %s", r)
|
|
}
|
|
if r := cssRule(t, css, ".stack-tags"); !strings.Contains(r, "flex-wrap: wrap") {
|
|
t.Errorf(".stack-tags must wrap (English tags are longer): %s", r)
|
|
}
|
|
if r := cssRule(t, css, ".subdomain-text"); !strings.Contains(r, "text-overflow: ellipsis") || !strings.Contains(r, "overflow: hidden") {
|
|
t.Errorf(".subdomain-text must cut a long address with an ellipsis: %s", r)
|
|
}
|
|
if r := cssRule(t, css, ".subdomain-link"); !strings.Contains(r, "white-space: nowrap") {
|
|
t.Errorf(".subdomain-link must keep the arrow on the address line: %s", r)
|
|
}
|
|
}
|
|
|
|
// R-907 — on a phone the launcher's title, domain chip and share button sat on one row that does not wrap; the
|
|
// button ran off the right edge (390 px, demo-hp 9201, 2026-10-08).
|
|
//
|
|
// RED-PROOF (REPORT): against 05e12921's style.css this fails — the 768px block has no .page-header rule.
|
|
func TestR907_PageHeaderWrapsOnAPhone(t *testing.T) {
|
|
data, err := StyleCSS()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
block := mediaBlock768(t, string(data))
|
|
if !regexp.MustCompile(`\.page-header \{[^}]*flex-wrap: wrap`).MatchString(block) {
|
|
t.Error("the phone block must let .page-header wrap")
|
|
}
|
|
if !regexp.MustCompile(`\.share-open-btn \{[^}]*white-space: nowrap`).MatchString(block) {
|
|
t.Error("the share button's text must stay whole when it drops to its own line")
|
|
}
|
|
// The button is still in the header it wraps inside, in both languages.
|
|
s := i18nTestServer(t)
|
|
for _, c := range i18nCases() {
|
|
if c.name != "launcher_full" {
|
|
continue
|
|
}
|
|
for _, lang := range []string{"hu", "en"} {
|
|
page := renderI18nCase(t, s, lang, c)
|
|
i := strings.Index(page, `<div class="page-header">`)
|
|
if i < 0 {
|
|
t.Fatalf("%s: launcher has no .page-header", lang)
|
|
}
|
|
head := page[i : i+strings.Index(page[i:], "</div>")]
|
|
if !strings.Contains(head, "share-open-btn") {
|
|
t.Errorf("%s: the share button left the launcher header", lang)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
t.Fatal("no launcher_full parity case")
|
|
}
|