987e915bf2
New /launcher page: a grid of large tappable tiles, one per openable deployed app (subdomain presence is the single openability criterion, shared with the Megnyitás button via the extracted Server.subdomainMap helper). Colored tiles (deterministic slug color or .felhom.yml brand_color), white glyph/monogram fallback, target=_blank links for operational apps, greyed unclickable tiles for stopped ones. First sidebar item; / stays the Vezérlőpult. Universal app placeholder: new AppPlaceholderSVG served at /static/app-placeholder.svg, now the default FallbackIcon on app_list_row so a logo-less app shows a placeholder instead of visibility:hidden. Brand mark is never an app placeholder. New Metadata.BrandColor; new funcmap tileColor/initial. 10 new test functions + 4 red-proofs. No agent coupling; MinAgent unchanged.
183 lines
8.0 KiB
Go
183 lines
8.0 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
|
)
|
|
|
|
// v0.126.0 Part A — the shared app_list_row partial (app_row.html) is the ONE row source on
|
|
// the list surfaces. Per-surface render tests: the partial's marker classes are present, the
|
|
// old hand-rolled structures (storage-path-item rows / stack-card rows) are gone, and the
|
|
// caller-provided action blocks (behavior) are untouched.
|
|
// COMPANION red-proof (recorded in REPORT): paste one pre-0.126.0 storage-path-item row block
|
|
// back into backups_remote.html → scripts/app_row_dedup_gate.py exits 1.
|
|
|
|
func appRowSplitData() map[string]interface{} {
|
|
d := splitTestData()
|
|
d["OffboxApps"] = []OffboxAppRow{
|
|
{Name: "calibre-web", DisplayName: "Calibre-Web", Slug: "calibre-web", Enabled: true},
|
|
{Name: "radarr", DisplayName: "Radarr", Slug: "radarr", Enabled: false},
|
|
}
|
|
return d
|
|
}
|
|
|
|
func TestAppRow_RemoteToggleList(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_remote", appRowSplitData())
|
|
if !strings.Contains(html, `class="app-row"`) || !strings.Contains(html, `app-row-name`) {
|
|
t.Error("remote toggle list must render through the app_list_row partial")
|
|
}
|
|
if strings.Contains(html, "storage-path-item") {
|
|
t.Error("old storage-path-item row structure survives on the remote page")
|
|
}
|
|
// Behavior unchanged: the toggle form + both label variants render as before.
|
|
if !strings.Contains(html, `action="/backup/offbox/toggle"`) {
|
|
t.Error("toggle form missing")
|
|
}
|
|
if !strings.Contains(html, "Távoli mentés kikapcsolása") || !strings.Contains(html, "Távoli mentés bekapcsolása") {
|
|
t.Error("toggle button labels changed")
|
|
}
|
|
}
|
|
|
|
func TestAppRow_RestoreLists(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_restore", appRowSplitData())
|
|
if !strings.Contains(html, `class="app-row"`) {
|
|
t.Error("restore-page lists must render through the app_list_row partial")
|
|
}
|
|
if strings.Contains(html, "storage-path-item") {
|
|
t.Error("old storage-path-item row structure survives on the restore page")
|
|
}
|
|
// v0.154.0 (R-48): the restore row is now a single entry that LINKS to the per-app wizard —
|
|
// the mutation forms moved there. The row itself must still be the one, and only, restore
|
|
// control for the app.
|
|
if !strings.Contains(html, `href="/backups/restore/app?name=calibre-web"`) {
|
|
t.Error("per-app restore wizard entry missing")
|
|
}
|
|
if strings.Contains(html, `action="/backup/offbox/restore"`) ||
|
|
strings.Contains(html, `action="/backup/offbox/place"`) ||
|
|
strings.Contains(html, `action="/backup/offbox/reconstitute"`) {
|
|
t.Error("offsite restore mutation forms must not render on the list page (R-48)")
|
|
}
|
|
if !strings.Contains(html, `fab-dl-btn`) || !strings.Contains(html, "Letöltés (.fab)") {
|
|
t.Error(".fab download action missing")
|
|
}
|
|
// The disabled app (radarr) must NOT appear in the restore-to-verify list (Enabled filter),
|
|
// but MUST appear in the .fab list — the partial must not have changed the filters.
|
|
if strings.Count(html, `value="radarr"`) != 0 {
|
|
t.Error("restore-to-verify must list only offbox-enabled apps")
|
|
}
|
|
if !strings.Contains(html, `data-stack="radarr"`) {
|
|
t.Error(".fab list must list all apps")
|
|
}
|
|
}
|
|
|
|
// Group D (v0.163.0) — the app_list_row now defaults its fallback icon to the generic app
|
|
// placeholder, so a logo-less app shows a placeholder glyph instead of the old visibility:hidden
|
|
// dead-end; an infra caller still overrides it with the server glyph.
|
|
// COMPANION red-proof (REPORT): revert app_row.html's data-fallback to the old
|
|
// `{{with .FallbackIcon}}...{{end}}` (attribute absent when no caller value) → the default-present
|
|
// assertion FAILS.
|
|
func TestAppRow_DefaultPlaceholderFallback(t *testing.T) {
|
|
// No FallbackIcon → the placeholder is the default.
|
|
def := renderPartial(t, "app_list_row", map[string]interface{}{"Slug": "no-logo-app", "Name": "Logótlan"})
|
|
if !strings.Contains(def, `data-fallback="/static/app-placeholder.svg"`) {
|
|
t.Errorf("a row without FallbackIcon must default to the app placeholder, got:\n%s", def)
|
|
}
|
|
// A caller-provided FallbackIcon (infra) still wins.
|
|
infra := renderPartial(t, "app_list_row",
|
|
map[string]interface{}{"Slug": "filebrowser", "Name": "FileBrowser", "FallbackIcon": "/static/infra-logo.svg"})
|
|
if !strings.Contains(infra, `data-fallback="/static/infra-logo.svg"`) {
|
|
t.Errorf("an explicit FallbackIcon must be preserved, got:\n%s", infra)
|
|
}
|
|
if strings.Contains(infra, "/static/app-placeholder.svg") {
|
|
t.Error("an infra row must not also carry the app placeholder")
|
|
}
|
|
}
|
|
|
|
// The placeholder asset is served with the SVG content-type + long cache, like the infra logo.
|
|
func TestAppPlaceholderRoute(t *testing.T) {
|
|
s := testPageServer(t)
|
|
rec := getPage(t, s, "/static/app-placeholder.svg")
|
|
if rec.Code != 200 {
|
|
t.Fatalf("GET /static/app-placeholder.svg = %d", rec.Code)
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); ct != "image/svg+xml" {
|
|
t.Errorf("content-type = %q, want image/svg+xml", ct)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "<svg") {
|
|
t.Error("placeholder body must be an SVG")
|
|
}
|
|
}
|
|
|
|
// renderPartial renders a single {{define}}'d partial (e.g. app_list_row) in isolation.
|
|
func renderPartial(t *testing.T, name string, data map[string]interface{}) string {
|
|
t.Helper()
|
|
return renderBackupPage(t, name, data)
|
|
}
|
|
|
|
func TestAppRow_Dashboard(t *testing.T) {
|
|
data := map[string]interface{}{
|
|
"Page": "dashboard", "Title": "Vezérlőpult",
|
|
"Stacks": []stacks.Stack{
|
|
{Name: "radarr", Deployed: true, State: stacks.StateRunning,
|
|
Meta: stacks.Metadata{Slug: "radarr", DisplayName: "Radarr", Description: "Filmgyűjtemény kezelése"}},
|
|
},
|
|
"MissingStorage": map[string]string{},
|
|
"NetworkWarnings": map[string]string{},
|
|
"NetworkStubs": map[string]string{},
|
|
"Subdomains": map[string]string{"radarr": "radarr"},
|
|
"RunningCount": 1, "StoppedCount": 0, "TotalCount": 1,
|
|
"SystemInfo": system.SystemInfo{},
|
|
"BackupEnabled": false,
|
|
"Domain": "demo-felhom.eu",
|
|
}
|
|
html := renderBackupPage(t, "dashboard", data)
|
|
if !strings.Contains(html, `class="app-row stack-state-run"`) {
|
|
t.Error("dashboard rows must render through app_list_row with the state RowClass")
|
|
}
|
|
if !strings.Contains(html, `data-href="/apps/radarr"`) {
|
|
t.Error("dashboard row lost its data-href")
|
|
}
|
|
if !strings.Contains(html, "Filmgyűjtemény kezelése") {
|
|
t.Error("dashboard row lost the description secondary line")
|
|
}
|
|
if strings.Contains(html, "stack-card") || strings.Contains(html, "stack-info") {
|
|
t.Error("old stack-card row structure survives on the dashboard")
|
|
}
|
|
}
|
|
|
|
// The Alkalmazások collapsed header is ALIGNED to the row grammar (icon + name left,
|
|
// status + chevron right) while keeping its own expander structure.
|
|
func TestAppRow_AppsHeaderAligned(t *testing.T) {
|
|
d := appRowSplitData()
|
|
d["AppBackupRows"] = []AppBackupRow{
|
|
{StackName: "calibre-web", DisplayName: "Calibre-Web", Slug: "calibre-web",
|
|
Status: "green", Tier3State: "active"},
|
|
}
|
|
d["Backup"] = &backup.FullBackupStatus{
|
|
AppDataInfo: []backup.AppBackupInfo{{StackName: "calibre-web", DisplayName: "Calibre-Web"}},
|
|
}
|
|
d["Offbox"] = &settings.OffboxTarget{Enabled: true, Host: "nas.local", LastStatus: "ok", EscrowState: "escrowed"}
|
|
html := renderBackupPage(t, "backups_apps", d)
|
|
|
|
hdr := html[strings.Index(html, `class="app-backup-row-header"`):]
|
|
hdr = hdr[:strings.Index(hdr, "app-backup-row-detail")]
|
|
iIcon := strings.Index(hdr, "app-row-icon")
|
|
iName := strings.Index(hdr, "app-backup-row-name")
|
|
iDot := strings.Index(hdr, "status-dot")
|
|
iChevron := strings.Index(hdr, "expand-icon")
|
|
if iIcon < 0 || iName < 0 || iDot < 0 || iChevron < 0 {
|
|
t.Fatalf("aligned header pieces missing (icon=%d name=%d dot=%d chevron=%d)", iIcon, iName, iDot, iChevron)
|
|
}
|
|
if !(iIcon < iName && iName < iDot && iDot < iChevron) {
|
|
t.Error("header grammar must be icon, name, ..., status, chevron (left→right)")
|
|
}
|
|
if !strings.Contains(hdr, `onclick="toggleBackupDetail(this)"`) {
|
|
t.Error("expander behavior must be untouched")
|
|
}
|
|
}
|