Files
mrleo1nidandClaude Opus 5.5 83832fc734
CI / test (push) Successful in 1m19s
Release / release (push) Successful in 1m18s
Веб-морда: поиск, фильтры, сортировка и пагинация клиентов; раскладка настроек
- список клиентов: поиск по домену и названию, фильтр по статусу (чипы
  со счётчиками), сортировка по любой колонке, пагинация 10/25/50/100;
  состояние в URL — отфильтрованный вид можно сохранить ссылкой
- размеры последних бэкапов одним запросом вместо запроса на клиента
- шапка выровнена по колонке контента; настройки на всю ширину:
  ntfy и Telegram двумя карточками, на узком экране — друг под другом

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:42:32 +03:00

88 lines
3.1 KiB
Go

package server
import (
"fmt"
"net/http/httptest"
"strings"
"testing"
"time"
)
func testClients() ([]*Client, map[int64]int64) {
now := time.Now().Unix()
var cs []*Client
sizes := map[int64]int64{}
for i := 1; i <= 30; i++ {
c := &Client{ID: int64(i), Domain: fmt.Sprintf("srv%02d.example.com", i), Name: fmt.Sprintf("srv%02d", i),
StaleHours: 26, LastBackupAt: now - int64(i)*60, CreatedAt: now}
sizes[c.ID] = int64(i) * 1000
cs = append(cs, c)
}
cs[0].Name = "Нидерланды" // srv01
cs[1].LastBackupAt = 0 // srv02 — never
cs[2].LastErrorAt = now // srv03 — fail
cs[3].Disabled = true // srv04 — off
cs[4].LastBackupAt = now - 100*3600 // srv05 — stale
return cs, sizes
}
func view(t *testing.T, rawQuery string) *clientsView {
t.Helper()
cs, sizes := testClients()
r := httptest.NewRequest("GET", "/?"+rawQuery, nil)
return buildClientsView(parseClientsQuery(r), cs, sizes)
}
func TestClientsSearchFilter(t *testing.T) {
v := view(t, "q=нидер")
if v.Found != 1 || v.Rows[0].Domain != "srv01.example.com" {
t.Fatalf("поиск по названию: %d", v.Found)
}
if v := view(t, "q=SRV1"); v.Found != 10 { // srv10..srv19
t.Fatalf("поиск без учёта регистра: %d", v.Found)
}
v = view(t, "status=fail")
if v.Found != 1 || v.Rows[0].Domain != "srv03.example.com" {
t.Fatalf("фильтр fail: %+v", v.Found)
}
chips := map[string]int{}
for _, c := range v.Chips {
chips[c.Key] = c.Count
}
if chips[""] != 30 || chips["never"] != 1 || chips["off"] != 1 || chips["stale"] != 1 || chips["ok"] != 26 {
t.Fatalf("счётчики: %v", chips)
}
}
func TestClientsSortAndPaging(t *testing.T) {
v := view(t, "")
if v.Found != 30 || len(v.Rows) != 25 || v.Rows[0].Domain != "srv01.example.com" || v.Next == "" {
t.Fatalf("первая страница: found=%d rows=%d", v.Found, len(v.Rows))
}
v = view(t, "page=2")
if len(v.Rows) != 5 || v.From != 26 || v.To != 30 || v.Prev == "" || v.Next != "" {
t.Fatalf("вторая страница: %+v", v)
}
if v := view(t, "page=99"); v.Query.Page != 2 {
t.Fatalf("страница за пределами: %d", v.Query.Page)
}
v = view(t, "sort=size&per=10")
if v.Rows[0].Domain != "srv30.example.com" || len(v.Rows) != 10 {
t.Fatalf("размер по убыванию по умолчанию: %s", v.Rows[0].Domain)
}
if v := view(t, "sort=size&dir=asc"); v.Rows[0].Domain != "srv01.example.com" {
t.Fatalf("размер по возрастанию: %s", v.Rows[0].Domain)
}
if v := view(t, "sort=status"); v.Rows[0].StatusClass != "fail" {
t.Fatalf("сортировка по статусу: %s", v.Rows[0].StatusClass)
}
// повторный клик по текущей колонке меняет направление
if u := view(t, "sort=size").Query.SortURL("size"); !strings.Contains(u, "dir=asc") {
t.Fatalf("SortURL: %s", u)
}
// фильтр сохраняется в ссылках пагинации
if u := view(t, "q=srv&status=ok").Query.PageURL(2); !strings.Contains(u, "q=srv") || !strings.Contains(u, "status=ok") {
t.Fatalf("PageURL: %s", u)
}
}