From ca004e07a7703d2ecd14b94d3898ce31a59a6aa6 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 27 Apr 2026 10:28:07 +0800 Subject: [PATCH] test: lock approved IoT UI architecture --- internal/web/ui_test.go | 244 ++++++++++++++++++++++++++++++++-------- 1 file changed, 194 insertions(+), 50 deletions(-) diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index b2021ef..3894db8 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -174,6 +174,24 @@ func newTestUI(t *testing.T) *UI { return ui } +func renderPage(t *testing.T, ui *UI, path string) string { + t.Helper() + router, err := ui.Routes() + if err != nil { + t.Fatalf("build routes: %v", err) + } + if strings.HasPrefix(path, "/ui/") { + path = strings.TrimPrefix(path, "/ui") + } + req := httptest.NewRequest(http.MethodGet, path, nil) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("expected 200 for %s, got %d: %s", path, rr.Code, rr.Body.String()) + } + return rr.Body.String() +} + func TestUI_DeviceOverviewHidesBatchBarWithoutSelection(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil) @@ -884,12 +902,12 @@ func TestUI_DeviceDetailIncludesTabs(t *testing.T) { routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil)) body := rr.Body.String() - for _, want := range []string{"device-tabs", "设备详情", "设备控制"} { + for _, want := range []string{"设备详情", "当前设备", "最近状态摘要"} { if !strings.Contains(body, want) { t.Fatalf("expected device detail HTML to contain %q", want) } } - for _, forbidden := range []string{"返回设备总览"} { + for _, forbidden := range []string{"device-tabs", "设备控制", "返回设备总览", "进入管理"} { if strings.Contains(body, forbidden) { t.Fatalf("device detail should not contain redundant nav %q", forbidden) } @@ -912,15 +930,13 @@ func TestUI_DeviceDetailIsReadOnly(t *testing.T) { t.Fatalf("expected read-only device detail HTML to contain %q", want) } } - if strings.Contains(body, "去设备控制") { - t.Fatalf("device detail should not contain redundant cross-link to control page") - } for _, forbidden := range []string{"只读查看页", "权威摘要位置", "当前框架版"} { if strings.Contains(body, forbidden) { t.Fatalf("device detail should not contain placeholder copy %q", forbidden) } } for _, forbidden := range []string{ + `action="/ui/devices/edge-01/alias"`, `type="file"`, "部署到设备", "启动视频分析", @@ -934,9 +950,6 @@ func TestUI_DeviceDetailIsReadOnly(t *testing.T) { t.Fatalf("device detail should be read-only and not contain %q", forbidden) } } - if !strings.Contains(body, `action="/ui/devices/edge-01/alias"`) { - t.Fatalf("device detail should include the device alias edit form") - } } func TestUI_DeviceDetailShowsRunningConfigMetadata(t *testing.T) { @@ -1056,7 +1069,7 @@ func TestUI_DeviceDetailDoesNotUseChannelDisplayNameAsDeviceName(t *testing.T) { routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01", nil)) body := rr.Body.String() - for _, want := range []string{"设备别名", "备用盒子-01", "当前业务配置", "A厂区视觉识别", "通道名", "cam1"} { + for _, want := range []string{"备用盒子-01", "当前业务配置", "A厂区视觉识别", "通道名", "cam1"} { if !strings.Contains(body, want) { t.Fatalf("expected device detail to contain %q, got:\n%s", want, body) } @@ -1071,33 +1084,6 @@ func TestUI_DeviceDetailDoesNotUseChannelDisplayNameAsDeviceName(t *testing.T) { } } -func TestUI_ActionDeviceAliasSaveUpdatesRegistry(t *testing.T) { - ui := newTestUI(t) - routes, err := ui.Routes() - if err != nil { - t.Fatalf("Routes: %v", err) - } - form := url.Values{} - form.Set("device_alias", "备用盒子-02") - req := httptest.NewRequest(http.MethodPost, "/devices/edge-01/alias", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - rr := httptest.NewRecorder() - - routes.ServeHTTP(rr, req) - - if rr.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) - } - body := rr.Body.String() - if !strings.Contains(body, "设备别名已保存") || !strings.Contains(body, "备用盒子-02") { - t.Fatalf("expected alias save result in HTML, got:\n%s", body) - } - devices := ui.registry.GetDevices() - if len(devices) == 0 || devices[0].DeviceAlias != "备用盒子-02" { - t.Fatalf("expected registry alias to update, got %#v", devices) - } -} - 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} @@ -1106,11 +1092,14 @@ func TestUI_DeviceSubpagesIncludeContextNavigation(t *testing.T) { rr := httptest.NewRecorder() ui.render(rr, req, content, PageData{Title: "节点子页面", Device: dev}) body := rr.Body.String() - for _, want := range []string{"device-tabs", "设备详情", "设备控制"} { + for _, want := range []string{"当前设备"} { if !strings.Contains(body, want) { t.Fatalf("expected %s HTML to contain %q", content, want) } } + if strings.Contains(body, "device-tabs") { + t.Fatalf("expected %s HTML to avoid device cross-page tabs", content) + } } } @@ -1652,9 +1641,11 @@ func TestUI_SidebarMatchesInformationArchitecture(t *testing.T) { for _, want := range []string{ "设备", "配置管理", + "识别配置", "操作审计", "系统", `href="/ui/devices"`, + `href="/ui/device-config"`, `href="/ui/assets"`, `href="/ui/audit"`, `href="/ui/system"`, @@ -1663,25 +1654,33 @@ func TestUI_SidebarMatchesInformationArchitecture(t *testing.T) { t.Fatalf("expected sidebar to contain %q", want) } } - for _, old := range []string{"配置资产", "识别配置", "模型管理", "高级调试", "日志分析"} { + for _, old := range []string{"模型管理", "高级调试", "日志分析"} { if strings.Contains(body, old) { t.Fatalf("sidebar should not contain old label %q", old) } } } -func TestUI_LegacyDeviceConfigRedirectsToAssets(t *testing.T) { +func TestUI_DeviceConfigPageShowsDeviceSelector(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/device-config", nil) rr := httptest.NewRecorder() ui.pageDeviceConfig(rr, req) - if rr.Code != http.StatusFound { - t.Fatalf("expected redirect, got %d", rr.Code) + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } - if got := rr.Header().Get("Location"); got != "/ui/assets" { - t.Fatalf("expected redirect to /ui/assets, got %q", got) + body := rr.Body.String() + for _, want := range []string{"配置管理", "选择设备", "edge-01", "入口识别节点", `href="/ui/device-config/edge-01"`} { + if !strings.Contains(body, want) { + t.Fatalf("expected config selector page to contain %q, got:\n%s", want, body) + } + } + for _, forbidden := range []string{"模板", "业务配置", "叠加项", "配置资产"} { + if strings.Contains(body, forbidden) { + t.Fatalf("expected config selector page to avoid asset-library copy %q", forbidden) + } } } @@ -1693,10 +1692,11 @@ func TestUI_DeviceControlPageShowsLiveActions(t *testing.T) { } rr := httptest.NewRecorder() - routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/control", nil)) + routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/device-config/edge-01", nil)) body := rr.Body.String() for _, want := range []string{ - "设备控制", + "配置管理", + "单设备配置", "配置预览", "配置应用", "服务控制", @@ -1711,6 +1711,9 @@ func TestUI_DeviceControlPageShowsLiveActions(t *testing.T) { t.Fatalf("expected device control HTML to contain %q", want) } } + if strings.Contains(body, "设备详情") { + t.Fatalf("device config workspace should not cross-link back into device detail") + } if strings.Contains(body, "重载服务") { t.Fatalf("device control page should not contain reload action") } @@ -1762,7 +1765,7 @@ func TestUI_ActionDeviceActionCanRenderControlPage(t *testing.T) { form := url.Values{} form.Set("action", "reload") - form.Set("return_to", "control") + form.Set("return_to", "config") req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/action", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rctx := chi.NewRouteContext() @@ -1773,7 +1776,7 @@ func TestUI_ActionDeviceActionCanRenderControlPage(t *testing.T) { ui.actionDeviceAction(rr, req) body := rr.Body.String() - for _, want := range []string{"设备控制", "POST /v1/media-server/reload", "执行结果摘要"} { + for _, want := range []string{"配置管理", "POST /v1/media-server/reload", "执行结果摘要"} { if !strings.Contains(body, want) { t.Fatalf("expected control page result HTML to contain %q, got:\n%s", want, body) } @@ -1823,7 +1826,7 @@ func TestUI_ActionDeviceConfigCandidateApplyCanRenderControlPage(t *testing.T) { } form := url.Values{} - form.Set("return_to", "control") + form.Set("return_to", "config") 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") rctx := chi.NewRouteContext() @@ -1837,7 +1840,7 @@ func TestUI_ActionDeviceConfigCandidateApplyCanRenderControlPage(t *testing.T) { t.Fatalf("expected status to be refreshed for control page, got %d calls", statusCalls) } body := rr.Body.String() - for _, want := range []string{"设备控制", "应用候选配置结果", "preview_edge-01", "local_3588_face_debug"} { + for _, want := range []string{"配置管理", "应用候选配置结果", "preview_edge-01", "local_3588_face_debug"} { if !strings.Contains(body, want) { t.Fatalf("expected control page apply result HTML to contain %q, got:\n%s", want, body) } @@ -1857,7 +1860,7 @@ func TestUI_AssetsPageDefinesConfigAssetScope(t *testing.T) { ui.pageAssets(rr, req) body := rr.Body.String() - for _, want := range []string{"配置管理", "总览", "模板", "业务配置", "叠加项", "local_3588_test", "workshop_face_shoe_alarm", "face_debug"} { + for _, want := range []string{"识别配置", "总览", "模板", "业务配置", "叠加项", "local_3588_test", "workshop_face_shoe_alarm", "face_debug"} { if !strings.Contains(body, want) { t.Fatalf("expected assets HTML to contain %q", want) } @@ -1915,6 +1918,84 @@ func TestUI_ProfileAssetPageShowsInstanceSummary(t *testing.T) { } } +func TestUI_TemplateAssetsPageShowsListAndSelectedDetail(t *testing.T) { + ui := newTestUI(t) + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "configs", "templates", "workshop_face_shoe_alarm.json"), `{"name":"workshop_face_shoe_alarm","description":"template","params":{"minio_endpoint":"http://10.0.0.49:9000"},"template":{"nodes":[{},{}],"edges":[[]]}}`) + writeTestFile(t, filepath.Join(root, "configs", "templates", "helmet.json"), `{"name":"helmet","description":"helmet template","template":{"nodes":[{}],"edges":[]}}`) + writeTestFile(t, filepath.Join(root, "configs", "profiles", "local_3588_test.json"), `{"name":"local_3588_test","business_name":"A厂区视觉识别","instances":[{"name":"cam1","template":"workshop_face_shoe_alarm","params":{"display_name":"东门入口"}}]}`) + writeTestFile(t, filepath.Join(root, "configs", "overlays", "face_debug.json"), `{"description":"debug","instance_overrides":{"*":{"override":{}}}}`) + ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: root}) + + req := httptest.NewRequest(http.MethodGet, "/ui/assets/templates?name=helmet", nil) + rr := httptest.NewRecorder() + + ui.pageAssetTemplates(rr, req) + + body := rr.Body.String() + for _, want := range []string{"模板列表", "workshop_face_shoe_alarm", "helmet", "模板详情", "helmet template"} { + if !strings.Contains(body, want) { + t.Fatalf("expected template assets page to contain %q, got:\n%s", want, body) + } + } +} + +func TestUI_OverlayAssetsPageShowsListAndSelectedDetail(t *testing.T) { + ui := newTestUI(t) + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "configs", "templates", "workshop_face_shoe_alarm.json"), `{"name":"workshop_face_shoe_alarm","template":{"nodes":[{}],"edges":[]}}`) + writeTestFile(t, filepath.Join(root, "configs", "profiles", "local_3588_test.json"), `{"name":"local_3588_test","instances":[{"name":"cam1","template":"workshop_face_shoe_alarm","params":{"display_name":"东门入口"}}]}`) + writeTestFile(t, filepath.Join(root, "configs", "overlays", "face_debug.json"), `{"description":"debug","instance_overrides":{"*":{"override":{}}}}`) + writeTestFile(t, filepath.Join(root, "configs", "overlays", "night_relaxed.json"), `{"description":"relaxed","instance_overrides":{"cam2":{"override":{}}}}`) + ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: root}) + + req := httptest.NewRequest(http.MethodGet, "/ui/assets/overlays?name=night_relaxed", nil) + rr := httptest.NewRecorder() + + ui.pageAssetOverlays(rr, req) + + body := rr.Body.String() + for _, want := range []string{"配置叠加项列表", "face_debug", "night_relaxed", "relaxed", "cam2"} { + if !strings.Contains(body, want) { + t.Fatalf("expected overlay assets page to contain %q, got:\n%s", want, body) + } + } +} + +func TestUI_ProfileAssetsPageShowsListAndSelectedEditor(t *testing.T) { + ui := newTestUI(t) + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "configs", "templates", "workshop_face_shoe_alarm.json"), `{"name":"workshop_face_shoe_alarm","template":{"nodes":[{}],"edges":[]}}`) + writeTestFile(t, filepath.Join(root, "configs", "profiles", "local_3588_test.json"), `{ + "name":"local_3588_test", + "business_name":"A厂区视觉识别", + "description":"test profile", + "queue":{"size":8,"strategy":"drop_oldest"}, + "instances":[{"name":"cam1","template":"workshop_face_shoe_alarm","params":{"display_name":"东门入口","rtsp_url":"rtsp://10.0.0.1/live"}}] +}`) + writeTestFile(t, filepath.Join(root, "configs", "profiles", "night_shift.json"), `{ + "name":"night_shift", + "business_name":"夜班巡检", + "description":"night profile", + "queue":{"size":4,"strategy":"drop_oldest"}, + "instances":[{"name":"cam9","template":"workshop_face_shoe_alarm","params":{"display_name":"西门","rtsp_url":"rtsp://10.0.0.9/live"}}] +}`) + writeTestFile(t, filepath.Join(root, "configs", "overlays", "face_debug.json"), `{"description":"debug","instance_overrides":{"*":{"override":{}}}}`) + ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: root}) + + req := httptest.NewRequest(http.MethodGet, "/ui/assets/profiles?name=night_shift", nil) + rr := httptest.NewRecorder() + + ui.pageAssetProfiles(rr, req) + + body := rr.Body.String() + for _, want := range []string{"业务配置列表", "local_3588_test", "night_shift", "业务配置", "夜班巡检", "night profile", "西门", "rtsp://10.0.0.9/live"} { + if !strings.Contains(body, want) { + t.Fatalf("expected profile assets page to contain %q, got:\n%s", want, body) + } + } +} + func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) { cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000} reg := service.NewRegistryService(cfg, nil) @@ -2011,3 +2092,66 @@ func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) { } } } + +func TestUI_SidebarMatchesApprovedIoTArchitecture(t *testing.T) { + ui := newTestUI(t) + html := renderPage(t, ui, "/ui/devices") + + for _, label := range []string{"总览", "设备", "识别配置", "任务", "诊断"} { + if !strings.Contains(html, label) { + t.Fatalf("expected sidebar label %q in html: %s", label, html) + } + } + for _, removed := range []string{"配置管理", "系统状态", "操作审计"} { + if strings.Contains(html, removed) { + t.Fatalf("did not expect legacy top-level label %q in html: %s", removed, html) + } + } +} + +func TestUI_DashboardShowsGlobalOperationsSummary(t *testing.T) { + ui := newTestUI(t) + html := renderPage(t, ui, "/ui/dashboard") + + for _, text := range []string{"全局 KPI", "在线率", "最近任务", "异常设备"} { + if !strings.Contains(html, text) { + t.Fatalf("expected dashboard text %q in html: %s", text, html) + } + } +} + +func TestUI_TasksPageOwnsBatchExecutionDomain(t *testing.T) { + ui := newTestUI(t) + html := renderPage(t, ui, "/ui/tasks") + + for _, text := range []string{"批量下发", "批量重启", "批量回滚", "执行历史"} { + if !strings.Contains(html, text) { + t.Fatalf("expected tasks text %q in html: %s", text, html) + } + } +} + +func TestUI_DiagnosticsPageOwnsLogsSystemAndAudit(t *testing.T) { + ui := newTestUI(t) + html := renderPage(t, ui, "/ui/diagnostics") + + for _, text := range []string{"日志", "系统状态", "审计"} { + if !strings.Contains(html, text) { + t.Fatalf("expected diagnostics text %q in html: %s", text, html) + } + } +} + +func TestUI_DeviceDetailActsAsSingleDeviceWorkspace(t *testing.T) { + ui := newTestUI(t) + html := renderPage(t, ui, "/ui/devices/edge-01") + + for _, text := range []string{"概览", "运行与服务", "设备配置", "日志与指标"} { + if !strings.Contains(html, text) { + t.Fatalf("expected device workspace text %q in html: %s", text, html) + } + } + if strings.Contains(html, "进入配置管理") { + t.Fatalf("device detail should not contain cross-module config shortcut: %s", html) + } +}