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") } } 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") } }