web-ui: карточка и страница обновляются, пока задачу может двигать фон
- условие самообновления — доменный предикат store.State.IsObservable() вместо фазы catched; один поллер на поверхность, интервалы 5 с и 15 с - отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели вместо 404/500, который htmx не свопит - заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел «Живой поллинг» в конвенции веб-UI
This commit is contained in:
@@ -2,6 +2,7 @@ package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -337,8 +338,9 @@ func TestSourceSwapUpdatesActionBarOOB(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestRetryListShowsProgress: retry из списка → карточка downloading с
|
||||
// прогресс-поллером.
|
||||
// TestRetryListShowsProgress: retry из списка → карточка downloading с живым
|
||||
// прогрессом и самообновлением карточки (опрашивает себя карточка, а не
|
||||
// вложенный блок прогресса).
|
||||
func TestRetryListShowsProgress(t *testing.T) {
|
||||
dl := dlState(store.StateDownloading)
|
||||
lv := stubLive{m: map[string]worker.Live{"ihswap": {Progress: 0.42, DlSpeed: 6400000, ETA: 720}}}
|
||||
@@ -348,8 +350,11 @@ func TestRetryListShowsProgress(t *testing.T) {
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("retry (htmx) = %d, want 200", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "/fragments/downloads/"+testULID+"/progress") {
|
||||
t.Errorf("карточка downloading без прогресс-поллера: %s", rr.Body.String())
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{"width:42%", "/fragments/downloads/" + testULID + "/card"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("карточка downloading без %q: %s", want, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,3 +412,32 @@ func TestActionBarNamesReasonWithoutPreview(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestActionSwapErrorKeepsSwapRoot: действие человека, упавшее на чтении задачи,
|
||||
// отвечает 200 и фрагментом с корнем своей поверхности — иначе своп унёс бы
|
||||
// якорь (#card-{id} у списка, #download-main у страницы) и следующие действия
|
||||
// целились бы в несуществующий узел. Ретрая у действия нет, поэтому уровень лога
|
||||
// здесь ERROR, а не WARN, как у повторяющегося тика.
|
||||
func TestActionSwapErrorKeepsSwapRoot(t *testing.T) {
|
||||
cases := []struct{ surface, root string }{
|
||||
{"list", `id="card-` + testULID + `"`},
|
||||
{"download", `id="download-main"`},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rd := stubReader{getErr: errors.New("db is gone")}
|
||||
h := testRouterAction(t, rd, actionReviewer{}, stubCommander{}, stubLive{})
|
||||
|
||||
rr := post(t, h, "/ui/downloads/"+testULID+"/cancel", url.Values{"surface": {c.surface}}, true)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("surface=%s: status = %d, want 200", c.surface, rr.Code)
|
||||
continue
|
||||
}
|
||||
body := rr.Body.String()
|
||||
if !strings.Contains(body, c.root) {
|
||||
t.Errorf("surface=%s: фрагмент отказа без корня %s:\n%s", c.surface, c.root, body)
|
||||
}
|
||||
if strings.Contains(body, "hx-trigger") {
|
||||
t.Errorf("surface=%s: фрагмент отказа не самозавершается:\n%s", c.surface, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user