423b6d3404
gates / gates (push) Successful in 1m5s
Seen on scratch 9202 with the R-906 build: once the real banner warnings showed (instead of an empty overflow line), the link ran 90 px past a 390 px screen and rendered browser-default blue. Phone block: .alert-banner wraps, .alert-message breaks long paths, .alert-link on its own line; .alert-link color: inherit. Pinned in TestR907_PageHeaderWrapsOnAPhone (red against d5f2e47's CSS). No Hungarian string changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
149 lines
6.2 KiB
Go
149 lines
6.2 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, no
|
|
// banner rule, and .alert-link has no colour.
|
|
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")
|
|
}
|
|
// A banner on a phone: the long message breaks and the link wraps inside the banner (seen on 9202 once R-906
|
|
// let the real banners through: the link ran past the screen). And the link has a colour of its own.
|
|
for _, re := range []string{`\.alert-banner \{[^}]*flex-wrap: wrap`, `\.alert-message \{[^}]*min-width: 0[^}]*overflow-wrap: anywhere`, `\.alert-link \{[^}]*flex-basis: 100%`} {
|
|
if !regexp.MustCompile(re).MatchString(block) {
|
|
t.Errorf("the phone block lacks %s", re)
|
|
}
|
|
}
|
|
if r := cssRule(t, string(data), ".alert-link"); !strings.Contains(r, "color: inherit") {
|
|
t.Errorf(".alert-link must take the banner's colour, not the browser's link blue: %s", r)
|
|
}
|
|
// 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")
|
|
}
|