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(), "