package web
import (
"html/template"
"strings"
"testing"
)
// A device name beside a full IPv6 address is wider than a phone, and
// the wrapper's sideways scroll is one nobody finds inside a card. The
// session table stops being a table below the fold: one block per
// device, each cell under the name it goes by.
func TestSessionTableStacksOnAPhone(t *testing.T) {
for _, want := range []string{".dt-stack thead { display: none; }", `.dt-stack td[data-label]::before`} {
if !strings.Contains(sheet, want) {
t.Errorf("FAIL: the stacked layout is not in the served sheet: missing %q", want)
}
}
// base is CLONED, never executed: executing it would freeze it and
// every page built from it afterwards would fail to clone.
tpl := template.Must(template.Must(base.Clone()).Parse(`{{define "content"}}{{end}}`))
var b strings.Builder
if err := tpl.ExecuteTemplate(&b, "session-table", map[string]any{
"Sessions": []sessionRow{{ID: "a", Agent: "Safari on iPhone", IP: "2a09::1", Created: "2026-09-12T10:00:00Z"}},
"This": "a", "Action": "/me/sessions/end", "ShowAccount": false,
}); err != nil {
t.Fatal(err)
}
got := b.String()
for _, want := range []string{`class="dt dt-stack"`, `data-label="Signed in"`, `data-label="Last seen"`} {
if !strings.Contains(got, want) {
t.Errorf("FAIL: the table cannot stack: missing %s", want)
}
}
t.Log("ok: a phone reads the sessions as blocks, not as a table it must drag sideways")
}
// Stacking is decided by two rules that fight, and CSS resolves the
// fight by specificity rather than by which one reads as the intent.
// Both of these were real and both were invisible to every other test,
// because a stylesheet compiles whatever it says:
//
// - ".dt td.dt-act { width: 1% }" sat AFTER the stacking block at
// equal specificity, so the action cell stayed 1% wide, the button
// was squeezed to its minimum, and "Sign out" broke one letter per
// line down the card.
// - "main .dt-wrap > table { display: table }" outranks ".dt-stack"
// (0,1,1 against 0,1,0), so the table stayed a table while its rows
// became blocks. A display:table box shrink-wraps, so the stacked
// card was as wide as its widest cell and every row separator
// stopped mid-card.
//
// This asserts the two overrides that settle it, and that the stacking
// block is last among the .dt rules so the first cannot come back.
func TestStackedSessionsOutrankTheTableRules(t *testing.T) {
const blockOverride = "main .dt-wrap > table.dt-stack { display: block; }"
if !strings.Contains(sheet, blockOverride) {
t.Error("FAIL: the stacked table does not outrank the .dt-wrap table rule, " +
"so it shrink-wraps and its row separators stop mid-card")
}
if !strings.Contains(sheet, ".dt-stack td.dt-act .btn { overflow-wrap: normal; white-space: nowrap; }") {
t.Error("FAIL: nothing stops a control breaking mid-word inside a cell " +
"that sets overflow-wrap:anywhere for IPv6")
}
// Order matters for the width rule, which is same-specificity: the
// stacking block has to be the LAST thing that speaks about .dt.
stack := strings.Index(sheet, ".dt-stack td.dt-act { text-align: left;")
width := strings.LastIndex(sheet, ".dt td.dt-act, .dt th.dt-act { text-align: right; width: 1%; }")
if stack < 0 || width < 0 {
t.Fatalf("FAIL: the rules this pins are gone (stack=%d width=%d)", stack, width)
}
if width > stack {
t.Error("FAIL: a .dt width rule now comes after the stacking block at equal " +
"specificity, which squeezes the action cell back to 1% on a phone")
}
t.Log("ok: the stacked layout wins on both specificity and order")
}