d37bb1eb6a
.sidebar-logo was left-aligned; add auto side-margins so the 140px logo centers in the header (desktop sidebar + mobile drawer share the element). Adds TestSidebarLogo_Centered.
216 lines
8.4 KiB
Go
216 lines
8.4 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// v0.166.0 mobile navigation rework. The old @media(max-width:768px) block predated the v0.146.0
|
|
// accordion: it flattened .nav-links into a horizontal, overflow-x strip, which clipped the
|
|
// accordion's nested sub-lists (they share the .nav-links class). This suite pins the replacement:
|
|
// a hamburger + off-canvas drawer that REUSES the existing vertical sidebar, plus a no-JS static
|
|
// fallback so navigation never dead-ends. Desktop (>768px) is untouched — the accordion invariants
|
|
// stay in nav_accordion_test.go and must pass unchanged.
|
|
|
|
// mediaBlock768 returns the full brace-balanced body of the @media(max-width: 768px) rule so CSS
|
|
// pins are scoped to the mobile block rather than matching an unrelated rule elsewhere in the sheet.
|
|
func mediaBlock768(t *testing.T, css string) string {
|
|
t.Helper()
|
|
marker := "@media(max-width: 768px)"
|
|
i := strings.Index(css, marker)
|
|
if i < 0 {
|
|
t.Fatalf("no %q media block found — the mobile breakpoint moved or was renamed", marker)
|
|
}
|
|
open := strings.IndexByte(css[i:], '{')
|
|
if open < 0 {
|
|
t.Fatal("no opening brace after the 768px media marker")
|
|
}
|
|
start := i + open
|
|
depth := 0
|
|
for j := start; j < len(css); j++ {
|
|
switch css[j] {
|
|
case '{':
|
|
depth++
|
|
case '}':
|
|
depth--
|
|
if depth == 0 {
|
|
return css[start : j+1]
|
|
}
|
|
}
|
|
}
|
|
t.Fatal("unbalanced braces in the 768px media block")
|
|
return ""
|
|
}
|
|
|
|
// Group B — CSS pins (Scenario A/B/C).
|
|
//
|
|
// Red-proof: run against the pre-edit style.css and the strip-absence assertions FAIL — the old
|
|
// block still carries `.nav-links { display:flex; ... overflow-x:auto }`.
|
|
func TestMobileCSS_StripRemovedDrawerPresent(t *testing.T) {
|
|
data, err := StyleCSS()
|
|
if err != nil {
|
|
t.Fatalf("StyleCSS(): %v", err)
|
|
}
|
|
css := string(data)
|
|
block := mediaBlock768(t, css)
|
|
|
|
// The horizontal strip that clipped the accordion is gone from the mobile block.
|
|
if strings.Contains(block, "overflow-x") {
|
|
t.Error("768px block still contains an overflow-x rule — the horizontal .nav-links strip was not removed")
|
|
}
|
|
if strings.Contains(block, ".nav-links {") || strings.Contains(block, ".nav-links{") {
|
|
t.Error("768px block still flattens .nav-links — the strip pattern must be deleted, not patched")
|
|
}
|
|
|
|
// The off-canvas drawer + no-JS fallback are present in the mobile block.
|
|
for _, want := range []string{
|
|
".js .sidebar", // JS path: sidebar becomes an off-canvas drawer
|
|
"translateX(-100%)", // parked off-canvas until opened
|
|
".no-js .sidebar", // no-JS path: sidebar renders static inline
|
|
"body.nav-open", // scroll lock while the drawer is open
|
|
".mobile-topbar", // the sticky top bar is styled inside the mobile block
|
|
} {
|
|
if !strings.Contains(block, want) {
|
|
t.Errorf("768px block missing %q — the drawer/fallback rules are incomplete", want)
|
|
}
|
|
}
|
|
|
|
// The desktop defaults that keep >768px pixel-identical live OUTSIDE the media query.
|
|
for _, want := range []string{".mobile-topbar", ".nav-backdrop", ".nav-burger"} {
|
|
if !strings.Contains(css, want) {
|
|
t.Errorf("stylesheet missing base rule for %q", want)
|
|
}
|
|
}
|
|
if !strings.Contains(css, ".mobile-topbar { display: none") &&
|
|
!strings.Contains(css, ".mobile-topbar{display:none") {
|
|
t.Error("no desktop-default `.mobile-topbar { display:none }` — the top bar would leak onto desktop")
|
|
}
|
|
}
|
|
|
|
// The sidebar logo is horizontally centered in its header (desktop sidebar + mobile drawer share
|
|
// the element). Red-proof: revert to `margin-bottom: 0.25rem` (no auto side-margins) → this FAILS.
|
|
func TestSidebarLogo_Centered(t *testing.T) {
|
|
data, err := StyleCSS()
|
|
if err != nil {
|
|
t.Fatalf("StyleCSS(): %v", err)
|
|
}
|
|
css := string(data)
|
|
i := strings.Index(css, ".sidebar-logo {")
|
|
if i < 0 {
|
|
t.Fatal(".sidebar-logo rule not found")
|
|
}
|
|
rule := css[i : i+strings.Index(css[i:], "}")]
|
|
if !strings.Contains(rule, "margin: 0 auto") {
|
|
t.Errorf(".sidebar-logo is not centered (no `margin: 0 auto`); rule was:\n%s", rule)
|
|
}
|
|
}
|
|
|
|
// Group A — drawer markup (Scenario A/C), asserted through the real layout render.
|
|
//
|
|
// Red-proof: remove aria-controls from the burger button in layout.html → the aria-controls
|
|
// assertion FAILS. (Recorded in REPORT.)
|
|
func TestMobileNav_TopbarAndDrawerMarkup(t *testing.T) {
|
|
html := renderNavFor(t, "dashboard")
|
|
|
|
// Progressive-enhancement hook: the html element starts as no-js and the head script swaps it.
|
|
if !strings.Contains(html, `<html lang="hu" class="no-js">`) {
|
|
t.Error("<html> element does not carry class=\"no-js\" — the no-JS CSS fallback would never engage")
|
|
}
|
|
if !strings.Contains(html, `replace('no-js','js')`) {
|
|
t.Error("head script does not swap no-js → js")
|
|
}
|
|
|
|
// Sticky top bar with a single burger toggle wired to the sidebar.
|
|
if !strings.Contains(html, `class="mobile-topbar"`) {
|
|
t.Error("no .mobile-topbar element rendered")
|
|
}
|
|
if !strings.Contains(html, `class="nav-burger"`) {
|
|
t.Error("no .nav-burger button rendered")
|
|
}
|
|
if !strings.Contains(html, `aria-expanded="false"`) {
|
|
t.Error("burger missing aria-expanded=\"false\" initial state")
|
|
}
|
|
if !strings.Contains(html, `aria-controls="sidebar"`) {
|
|
t.Error("burger missing aria-controls=\"sidebar\" — the button is not associated with the drawer")
|
|
}
|
|
|
|
// The sidebar is the drawer target, and the backdrop starts hidden.
|
|
if !strings.Contains(html, `id="sidebar"`) {
|
|
t.Error("sidebar has no id=\"sidebar\" — aria-controls points at nothing")
|
|
}
|
|
if !strings.Contains(html, `class="nav-backdrop"`) {
|
|
t.Error("no .nav-backdrop element rendered")
|
|
}
|
|
// The backdrop must default hidden (both the attribute and its element must be present together).
|
|
if !strings.Contains(html, `class="nav-backdrop" hidden`) {
|
|
t.Error("nav-backdrop is not initially hidden")
|
|
}
|
|
}
|
|
|
|
// Group D — customer name (Scenario D).
|
|
//
|
|
// Red-proof: re-add <span class="customer-name">{{.CustomerName}}</span> to the sidebar header →
|
|
// TestSidebar_NoCustomerName FAILS.
|
|
func TestSidebar_NoCustomerName(t *testing.T) {
|
|
html := renderNavFor(t, "dashboard")
|
|
if strings.Contains(html, "customer-name") {
|
|
t.Error("sidebar still renders the customer-name element — it must live only on the login page")
|
|
}
|
|
}
|
|
|
|
func TestLogin_CustomerNameKept(t *testing.T) {
|
|
s := testServer(t)
|
|
s.loadTemplates()
|
|
var buf bytes.Buffer
|
|
data := map[string]interface{}{
|
|
"Title": "Bejelentkezés",
|
|
"CustomerName": "Teszt Ügyfél",
|
|
"Version": "9.9.9",
|
|
}
|
|
if err := s.tmpl.ExecuteTemplate(&buf, "login", data); err != nil {
|
|
t.Fatalf("render login: %v", err)
|
|
}
|
|
out := buf.String()
|
|
// The login page still identifies whose box this is.
|
|
if !strings.Contains(out, "Teszt Ügyfél") {
|
|
t.Error("login page dropped the CustomerName subtitle — it identifies the box owner and must stay")
|
|
}
|
|
if !strings.Contains(out, `class="login-subtitle"`) {
|
|
t.Error("login-subtitle element missing")
|
|
}
|
|
}
|
|
|
|
// Group E — logo constants carry NO live text (Scenario E). Under `<img>` secure static mode only
|
|
// locally-installed fonts resolve, so a `font-family` in the SVG renders a fallback font everywhere;
|
|
// the wordmark must be outlined paths. Shipped in v0.167.0 once the outlined master landed on
|
|
// felhom.eu main and the vestigial empty text nodes were stripped.
|
|
//
|
|
// Red-proof (recorded 2026-07-24): run against the pre-v0.167.0 constants → FAILS, both constants
|
|
// contained `<text` + `font-family`.
|
|
func TestLogoSVG_NoLiveText(t *testing.T) {
|
|
for name, s := range map[string]string{"FelhomLogoSVG": FelhomLogoSVG, "FelhomFaviconSVG": FelhomFaviconSVG} {
|
|
if strings.Contains(s, "<text") {
|
|
t.Errorf("%s contains a <text element — the wordmark must be outlined paths, not live text", name)
|
|
}
|
|
if strings.Contains(s, "font-family") {
|
|
t.Errorf("%s contains font-family — it renders in a fallback font under <img> secure static mode", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Group E — versioned asset URLs (Scenario E).
|
|
//
|
|
// Red-proof: run against the pre-edit layout.html → both assertions FAIL (the /static/ URLs lack
|
|
// the ?v= cache-bust). Cloudflare edge-caches /static/* for 4h, so an unversioned logo/favicon keeps
|
|
// serving the previous release after a deploy (same failure mode as the 0.126.1 CSS incident).
|
|
func TestLayout_VersionedAssetURLs(t *testing.T) {
|
|
html := renderNavFor(t, "dashboard")
|
|
if !strings.Contains(html, "/static/felhom-logo.svg?v=") {
|
|
t.Error("sidebar logo URL is not cache-busted with ?v= — a deploy would keep serving the stale edge copy")
|
|
}
|
|
if !strings.Contains(html, "/static/favicon.svg?v=") {
|
|
t.Error("favicon URL is not cache-busted with ?v=")
|
|
}
|
|
}
|