package web import ( "3588AdminBackend/internal/config" "3588AdminBackend/internal/models" "3588AdminBackend/internal/service" "context" "net" "net/http" "net/http/httptest" "net/url" "os" "strconv" "strings" "testing" "github.com/go-chi/chi/v5" ) func TestUI_ActionDevicesBatchAction_RedirectsToTask(t *testing.T) { cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} reg := service.NewRegistryService(cfg, nil) reg.UpdateDevice(&models.Device{DeviceID: "dev1", IP: "127.0.0.1", AgentPort: 9100, Online: true}) reg.UpdateDevice(&models.Device{DeviceID: "dev2", IP: "127.0.0.1", AgentPort: 9100, Online: true}) tasks := service.NewTaskService(cfg, nil, reg) ui, err := NewUI(nil, reg, nil, tasks, nil) if err != nil { t.Fatalf("NewUI: %v", err) } form := url.Values{} form.Set("action", "media_start") form.Add("device_id", "dev1") form.Add("device_id", "dev2") form.Set("config", "cam1") req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-action", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() ui.actionDevicesBatchAction(rr, req) if rr.Code != http.StatusFound { t.Fatalf("expected 302, got %d: %s", rr.Code, rr.Body.String()) } loc := rr.Header().Get("Location") if !strings.HasPrefix(loc, "/ui/tasks/") { t.Fatalf("expected redirect to /ui/tasks/*, got %q", loc) } } func TestUI_DevicePageUsesEdgeVisionConsoleShell(t *testing.T) { cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} reg := service.NewRegistryService(cfg, nil) reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true}) tasks := service.NewTaskService(cfg, nil, reg) ui, err := NewUI(nil, reg, nil, tasks, nil) if err != nil { t.Fatalf("NewUI: %v", err) } req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil) rr := httptest.NewRecorder() ui.pageDevices(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{ "视觉识别边缘控制台", "设备列表", "识别配置", "模型管理", "高级调试", "

设备列表

", } { if !strings.Contains(body, want) { t.Fatalf("expected rendered HTML to contain %q, got:\n%s", want, body) } } } func TestUI_ConsoleTypographyStaysModerate(t *testing.T) { css, err := os.ReadFile("ui/assets/style.css") if err != nil { t.Fatalf("read stylesheet: %v", err) } text := string(css) for _, want := range []string{ ".brand-title{font-weight:600", ".nav-section{font-size:11px", ".side-nav a{color:#dce6f1;display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:8px;font-size:13px;font-weight:500", ".topbar h1{margin:3px 0 0;font-size:18px;letter-spacing:0;font-weight:600", ".crumb{font-size:11px", "input,select,textarea,button,.btn{background:#fff;color:var(--text);border:1px solid var(--border2);border-radius:7px;padding:8px 10px;outline:none;font:inherit;font-size:13px", "button,.btn{cursor:pointer;font-size:13px;font-weight:600", "th{color:#40546a;font-size:13px;font-weight:600", } { if !strings.Contains(text, want) { t.Fatalf("expected stylesheet to contain %q", want) } } } func newTestUI(t *testing.T) *UI { t.Helper() cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} reg := service.NewRegistryService(cfg, nil) reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.0"}) tasks := service.NewTaskService(cfg, nil, reg) ui, err := NewUI(nil, reg, nil, tasks, nil) if err != nil { t.Fatalf("NewUI: %v", err) } return ui } func TestUI_DashboardRendersFleetOverview(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/dashboard", nil) rr := httptest.NewRecorder() ui.pageDashboard(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{"总览", "视觉节点态势", "视频分析概览", "最近任务", "快速入口", "执行中任务", "失败任务", "需要关注的节点", "运维工作流"} { if !strings.Contains(body, want) { t.Fatalf("expected dashboard HTML to contain %q, got:\n%s", want, body) } } } func TestUI_DevicePageIncludesDrawerAndBatchConfirm(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil) rr := httptest.NewRecorder() ui.pageDevices(rr, req) body := rr.Body.String() for _, want := range []string{"device-drawer", "batch-confirm-panel", "openDeviceDrawer", "confirmBatchAction"} { if !strings.Contains(body, want) { t.Fatalf("expected device page HTML to contain %q", want) } } } func TestUI_DeviceDetailIncludesTabs(t *testing.T) { ui := newTestUI(t) routes, err := ui.Routes() if err != nil { t.Fatalf("Routes: %v", err) } rr := httptest.NewRecorder() routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil)) body := rr.Body.String() for _, want := range []string{"device-tabs", "返回设备列表", "节点详情", "概览", "识别配置", "模型管理", "诊断日志", "运行指标", "技术信息"} { if !strings.Contains(body, want) { t.Fatalf("expected device detail HTML to contain %q", want) } } } func TestUI_DeviceDetailIsReadOnly(t *testing.T) { ui := newTestUI(t) routes, err := ui.Routes() if err != nil { t.Fatalf("Routes: %v", err) } rr := httptest.NewRecorder() routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil)) body := rr.Body.String() for _, want := range []string{"视觉识别节点详情", "只读详情", "配置管理", "技术信息", "诊断日志", "运行指标"} { if !strings.Contains(body, want) { t.Fatalf("expected read-only device detail HTML to contain %q", want) } } for _, forbidden := range []string{ `method="post"`, `type="file"`, "部署到设备", "启动视频分析", "重启视频分析", "停止视频分析", "重载识别服务", "回滚识别配置", "上传视频分析配置", } { if strings.Contains(body, forbidden) { t.Fatalf("device detail should be read-only and not contain %q", forbidden) } } } func TestUI_DeviceDetailShowsRunningConfigMetadata(t *testing.T) { agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/config/status" { t.Fatalf("unexpected agent path %s", r.URL.Path) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "ok": true, "config_path": "/opt/rk3588-media-server/etc/media-server.json", "exists": true, "sha256": "8d935c9d366637ff853c69d25e9c6644c2fbff3b8d3aa15ff99ef32847fb947c", "metadata": { "config_id": "local_3588_face_debug", "config_version": "20260419.104457", "template": "workshop_face_shoe_alarm", "profile": "local_3588_test", "overlays": ["face_debug"], "rendered_by": "tools/render_config.py", "rendered_at": "2026-04-19T10:44:58+08:00" }, "media_server": {"running": true, "pid": 1706009} }`)) })) defer agentServer.Close() host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://")) if err != nil { t.Fatalf("parse test server address: %v", err) } port, err := strconv.Atoi(portText) if err != nil { t.Fatalf("parse test server port: %v", err) } cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} agent := service.NewAgentClient(cfg) reg := service.NewRegistryService(cfg, agent) reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true, Version: "1.0.0"}) tasks := service.NewTaskService(cfg, agent, reg) ui, err := NewUI(nil, reg, agent, tasks, nil) if err != nil { t.Fatalf("NewUI: %v", err) } routes, err := ui.Routes() if err != nil { t.Fatalf("Routes: %v", err) } rr := httptest.NewRecorder() routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil)) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{ "当前运行配置", "local_3588_face_debug", "20260419.104457", "workshop_face_shoe_alarm", "local_3588_test", "face_debug", "8d935c9d366637ff853c69d25e9c6644c2fbff3b8d3aa15ff99ef32847fb947c", "/opt/rk3588-media-server/etc/media-server.json", "运行中", } { if !strings.Contains(body, want) { t.Fatalf("expected device detail HTML to contain %q, got:\n%s", want, body) } } } func TestUI_DeviceSubpagesIncludeContextNavigation(t *testing.T) { ui := newTestUI(t) dev := &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true} for _, content := range []string{"device_logs", "device_graphs", "config_ui"} { req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01", nil) rr := httptest.NewRecorder() ui.render(rr, req, content, PageData{Title: "节点子页面", Device: dev}) body := rr.Body.String() for _, want := range []string{"device-tabs", "返回设备列表", "节点详情", "识别配置", "诊断日志", "运行指标"} { if !strings.Contains(body, want) { t.Fatalf("expected %s HTML to contain %q", content, want) } } } } func TestUI_ConfigFriendlyIncludesWizard(t *testing.T) { ui := newTestUI(t) routes, err := ui.Routes() if err != nil { t.Fatalf("Routes: %v", err) } rr := httptest.NewRecorder() routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/config-friendly", nil)) body := rr.Body.String() for _, want := range []string{"config-wizard", "选择节点", "编辑视频通道", "预览变更", "部署结果", "data-wizard-section=\"channels\"", "下一步:编辑视频通道", "前往预览变更", "查看部署结果"} { if !strings.Contains(body, want) { t.Fatalf("expected config page HTML to contain %q", want) } } } func TestUI_ConfigPreviewPageShowsTemplateProfileOverlayForm(t *testing.T) { ui := newTestUI(t) routes, err := ui.Routes() if err != nil { t.Fatalf("Routes: %v", err) } rr := httptest.NewRecorder() routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/config-preview", nil)) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{ "配置预览", "模板", "Profile", "Overlay", "config_id", "config_version", "生成预览", "workshop_face_shoe_alarm", "local_3588_test", "face_debug", "上传为候选配置", "应用候选配置", } { if !strings.Contains(body, want) { t.Fatalf("expected config preview page to contain %q, got:\n%s", want, body) } } } func TestUI_ConfigPreviewPageKeepsApplyActionAfterUploadResult(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil) rr := httptest.NewRecorder() ui.render(rr, req, "config_preview", PageData{ Title: "配置预览", Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100}, ConfigStatus: &ConfigStatusView{ Candidate: &ConfigStatusLastGoodFile{Exists: true, Path: "/opt/rk3588-media-server/etc/media-server.json.candidate.json"}, }, ConfigPreview: &service.ConfigPreviewResult{ JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1"}}`, Metadata: map[string]any{ "config_id": "preview_edge-01", "config_version": "v1", "template": "workshop_face_shoe_alarm", "profile": "local_3588_test", }, Size: 123, }, RawText: `{"ok":true}`, ResultTitle: "应用候选配置结果", }) body := rr.Body.String() for _, want := range []string{ "应用候选配置结果", "上传为候选配置", "应用候选配置", `formaction="/ui/devices/edge-01/config-candidate/apply"`, } { if !strings.Contains(body, want) { t.Fatalf("expected config preview upload result HTML to contain %q, got:\n%s", want, body) } } } func TestUI_ActionDeviceConfigCandidateKeepsPreviewApplyAction(t *testing.T) { agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet && r.URL.Path == "/v1/config/status" { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"ok":true,"candidate":{"exists":true,"path":"/opt/rk3588-media-server/etc/media-server.json.candidate.json"}}`)) return } if r.Method != http.MethodPut || r.URL.Path != "/v1/config/candidate" { t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"ok":true}`)) })) defer agentServer.Close() host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://")) if err != nil { t.Fatalf("parse test server address: %v", err) } port, err := strconv.Atoi(portText) if err != nil { t.Fatalf("parse test server port: %v", err) } cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} agent := service.NewAgentClient(cfg) reg := service.NewRegistryService(cfg, agent) reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true}) tasks := service.NewTaskService(cfg, agent, reg) ui, err := NewUI(nil, reg, agent, tasks, nil) if err != nil { t.Fatalf("NewUI: %v", err) } form := url.Values{} form.Set("json", `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1","template":"workshop_face_shoe_alarm","profile":"local_3588_test"}}`) req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/config-candidate", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetPathValue("id", "edge-01") rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "edge-01") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rr := httptest.NewRecorder() ui.actionDeviceConfigCandidate(rr, req) body := rr.Body.String() for _, want := range []string{ "候选配置结果", "应用候选配置", `formaction="/ui/devices/edge-01/config-candidate/apply"`, } { if !strings.Contains(body, want) { t.Fatalf("expected actionDeviceConfigCandidate HTML to contain %q, got:\n%s", want, body) } } } func TestUI_ConfigPreviewKeepsSelectedOverlayAfterPreview(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil) rr := httptest.NewRecorder() ui.render(rr, req, "config_preview", PageData{ Title: "配置预览", Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100}, ConfigSources: service.ConfigPreviewSources{Templates: []service.ConfigSource{{Name: "workshop_face_shoe_alarm"}}, Profiles: []service.ConfigSource{{Name: "local_3588_test"}}, Overlays: []service.ConfigSource{{Name: "face_debug"}, {Name: "face_test_sensitive"}}}, SelectedTemplate: "workshop_face_shoe_alarm", SelectedProfile: "local_3588_test", SelectedOverlays: []string{"face_test_sensitive"}, SelectedConfigID: "preview_edge-01", ConfigPreview: &service.ConfigPreviewResult{ JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1","template":"workshop_face_shoe_alarm","profile":"local_3588_test","overlays":["face_test_sensitive"]}}`, Metadata: map[string]any{ "config_id": "preview_edge-01", "config_version": "v1", "template": "workshop_face_shoe_alarm", "profile": "local_3588_test", "overlays": []any{"face_test_sensitive"}, }, Size: 123, }, }) body := rr.Body.String() if !strings.Contains(body, `value="face_test_sensitive" checked`) { t.Fatalf("expected selected overlay to remain checked, got:\n%s", body) } if strings.Contains(body, `value="face_debug" checked`) { t.Fatalf("did not expect default overlay to stay checked, got:\n%s", body) } } func TestUI_ConfigPreviewCollapsesJSONAfterActionResult(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil) rr := httptest.NewRecorder() ui.render(rr, req, "config_preview", PageData{ Title: "配置预览", Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100}, ConfigSources: service.ConfigPreviewSources{Templates: []service.ConfigSource{{Name: "workshop_face_shoe_alarm"}}, Profiles: []service.ConfigSource{{Name: "local_3588_test"}}, Overlays: []service.ConfigSource{{Name: "face_debug"}}}, SelectedTemplate: "workshop_face_shoe_alarm", SelectedProfile: "local_3588_test", SelectedOverlays: []string{"face_debug"}, SelectedConfigID: "preview_edge-01", ConfigPreview: &service.ConfigPreviewResult{ JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[]}`, Metadata: map[string]any{"config_id": "preview_edge-01"}, Size: 64, }, ResultTitle: "候选配置结果", RawText: `{"ok":true}`, }) body := rr.Body.String() if !strings.Contains(body, "") { t.Fatalf("expected json panel to be collapsed after action result, got:\n%s", body) } } func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil) rr := httptest.NewRecorder() ui.render(rr, req, "config_preview", PageData{ Title: "配置预览", Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100}, SelectedTemplate: "workshop_face_shoe_alarm", SelectedProfile: "local_3588_test", SelectedOverlays: []string{"face_debug"}, SelectedConfigID: "preview_edge-01", ConfigPreview: &service.ConfigPreviewResult{ JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2"}}`, Metadata: map[string]any{ "config_id": "preview_edge-01", "config_version": "v2", }, Size: 64, }, ConfigStatus: &ConfigStatusView{ OK: true, Metadata: ConfigStatusMetadata{ ConfigID: "preview_edge-01", ConfigVersion: "v2", }, Candidate: &ConfigStatusLastGoodFile{ Exists: false, Path: "/opt/rk3588-media-server/etc/media-server.json.candidate.json", }, LastGood: &ConfigStatusLastGoodFile{ Exists: true, Path: "/opt/rk3588-media-server/etc/media-server.json.last_good.json", Metadata: ConfigStatusMetadata{ ConfigID: "local_3588_face_debug", ConfigVersion: "20260419.120246", }, }, MediaServer: ConfigStatusMediaServer{ Running: true, PID: 1810489, }, }, ResultTitle: "应用候选配置结果", RawText: `{"ok":true}`, }) body := rr.Body.String() for _, want := range []string{ "应用结果摘要", "当前运行", "preview_edge-01 / v2", "last_good", "local_3588_face_debug / 20260419.120246", "candidate", "已清空", "media-server", "运行中", } { if !strings.Contains(body, want) { t.Fatalf("expected apply summary HTML to contain %q, got:\n%s", want, body) } } } func TestUI_ActionDeviceConfigCandidateApplyReloadsStatusAfterApply(t *testing.T) { statusCalls := 0 agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/config/status": statusCalls++ w.Header().Set("Content-Type", "application/json") if statusCalls == 1 { _, _ = w.Write([]byte(`{ "ok": true, "metadata": {"config_id": "preview_edge-01", "config_version": "v2"}, "candidate": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"}, "last_good": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}}, "media_server": {"running": true, "pid": 1810489} }`)) return } _, _ = w.Write([]byte(`{ "ok": true, "metadata": {"config_id": "preview_edge-01", "config_version": "v2"}, "candidate": {"exists": false, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"}, "last_good": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}}, "media_server": {"running": true, "pid": 1810489} }`)) return case r.Method == http.MethodPost && r.URL.Path == "/v1/config/candidate/apply": w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"ok":true}`)) return default: t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) } })) defer agentServer.Close() host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://")) if err != nil { t.Fatalf("parse test server address: %v", err) } port, err := strconv.Atoi(portText) if err != nil { t.Fatalf("parse test server port: %v", err) } cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} agent := service.NewAgentClient(cfg) reg := service.NewRegistryService(cfg, agent) reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true}) tasks := service.NewTaskService(cfg, agent, reg) ui, err := NewUI(nil, reg, agent, tasks, nil) if err != nil { t.Fatalf("NewUI: %v", err) } form := url.Values{} form.Set("json", `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2","template":"workshop_face_shoe_alarm","profile":"local_3588_test"}}`) req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/config-candidate/apply", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetPathValue("id", "edge-01") rctx := chi.NewRouteContext() rctx.URLParams.Add("id", "edge-01") req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) rr := httptest.NewRecorder() ui.actionDeviceConfigCandidateApply(rr, req) body := rr.Body.String() if statusCalls != 2 { t.Fatalf("expected status to be loaded twice, got %d", statusCalls) } for _, want := range []string{ "应用结果摘要", "preview_edge-01 / v2", "local_3588_face_debug / 20260419.120246", "已清空", "运行中", } { if !strings.Contains(body, want) { t.Fatalf("expected apply result HTML to contain %q, got:\n%s", want, body) } } if strings.Contains(body, "仍存在") { t.Fatalf("expected refreshed status after apply, got stale candidate state:\n%s", body) } } func TestUI_ModelDeploymentPageRendersDeviceActions(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/models", nil) rr := httptest.NewRecorder() ui.pageModels(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{"模型管理", "模型管理工作流", "model-summary", "summary-value", "入口识别节点", "上传模型", "查看设备模型", "人脸库"} { if !strings.Contains(body, want) { t.Fatalf("expected model deployment HTML to contain %q", want) } } if strings.Contains(body, `
目标节点`) { t.Fatalf("model summary should not use large stat cards") } } func TestUI_DiagnosticsPageRendersLogAndMetricLinks(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/diagnostics", nil) rr := httptest.NewRecorder() ui.pageDiagnostics(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{"日志分析", "诊断工作台", "入口识别节点", "诊断日志", "运行指标", "高级调试"} { if !strings.Contains(body, want) { t.Fatalf("expected diagnostics HTML to contain %q", want) } } } func TestUI_SidebarMatchesInformationArchitecture(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/dashboard", nil) rr := httptest.NewRecorder() ui.pageDashboard(rr, req) body := rr.Body.String() for _, want := range []string{ "总览", "设备列表", "配置管理", "新增设备", "模型管理", "识别配置", "日志分析", "高级调试", `href="/ui/device-config"`, `href="/ui/recognition"`, `href="/ui/logs"`, } { if !strings.Contains(body, want) { t.Fatalf("expected sidebar to contain %q", want) } } for _, old := range []string{"设备节点", "添加节点", "模型部署", "日志指标", "任务中心"} { if strings.Contains(body, old) { t.Fatalf("sidebar should not contain old label %q", old) } } } func TestUI_DeviceConfigPageDefinesOpsConfigScope(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/device-config", nil) rr := httptest.NewRecorder() ui.pageDeviceConfig(rr, req) body := rr.Body.String() for _, want := range []string{"配置管理", "设备运行配置", "服务状态", "服务管理", "配置查看", "配置上传", "入口识别节点", "启动视频分析", "重启视频分析", "停止视频分析", "重载识别服务", "回滚识别配置"} { if !strings.Contains(body, want) { t.Fatalf("expected device config HTML to contain %q", want) } } if !strings.Contains(body, `method="post"`) { t.Fatalf("expected device config page to contain service operation forms") } } func TestUI_RecognitionPageDefinesAIScope(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/recognition", nil) rr := httptest.NewRecorder() ui.pageRecognition(rr, req) body := rr.Body.String() for _, want := range []string{"识别配置", "识别方案", "视频通道", "模板参数", "预览变更", "入口识别节点"} { if !strings.Contains(body, want) { t.Fatalf("expected recognition HTML to contain %q", want) } } } func TestUI_LogsPageDefinesDiagnosticsScope(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/logs", nil) rr := httptest.NewRecorder() ui.pageLogs(rr, req) body := rr.Body.String() for _, want := range []string{"日志分析", "日志筛选", "诊断日志", "运行指标", "高级调试", "入口识别节点"} { if !strings.Contains(body, want) { t.Fatalf("expected logs HTML to contain %q", want) } } }