test: lock approved IoT UI architecture

This commit is contained in:
tian 2026-04-27 10:28:07 +08:00
parent a01597c719
commit ca004e07a7

View File

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