test: lock approved IoT UI architecture
This commit is contained in:
parent
a01597c719
commit
ca004e07a7
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user