visionA/visionA-frontend/src/components/workspace/inference-panel.test.tsx
jim800121chen 0010cc35c3 feat(workspace): canvas overlay 疊 bbox + WS 即時推論面板(前端塊2)
推論工作區前端塊2:canvas overlay 疊在 MJPEG <img> 上、接 WS
inference:<deviceId> 推的 raw InferenceResult、即時繪 bbox+label+信心度;
右側 Inference panel 從 Phase 1 佔位換成 WS 即時結果清單 + FPS/延遲。
照 edge-ai-platform POC 移植(唯讀參考)。

- camera-overlay.tsx:normalized bbox→canvas 像素換算、label 框
- inference-panel.tsx:指標 + 清單 + aria-live 無障礙
- inference-store.ts:fps/avgLatency、MAX_RESULTS=100 上限
- use-inference-stream.ts:用既有 useWebSocket(same-origin cookie、無 token URL)
- workspace-client:overlay 塞 CameraFeed slot、訂閱 WS(僅 isRunning && isOnline)

canvas 顏色讀設計 token(--chart-*/--background,getComputedStyle 快取、
主題切換才重讀)、跟隨深淺主題、不裸色值。

Reviewer 2 輪通過(0C/0M)。修正:
- M1 overlay 座標錯位:feedSize 初值 null、未拿真實顯示尺寸不繪(MJPEG 非
  4:3 不錯位)
- M2 效能 jank:palette 快取 useState + MutationObserver 主題切換才重讀
  (不再每 frame getComputedStyle)
- m2 label fallback 色改實際色(canvas 不認 currentColor)
- m3 isValidBBox guard:壞 payload 略過不 crash

+6 新測試(M1 640×360→height=360、M2 呼叫數不增、m3 NaN/undefined 略過)。
tsc/eslint clean、塊2 範圍 37 test 綠。(全套 11 failed 全屬 conversion-store
既有 time-based flaky、未觸碰)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 05:03:35 +08:00

116 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* InferencePanel 單元測試(塊 2
*
* 驗證:
* - isRunning=false → 顯示等待提示idle
* - isRunning=true 但無結果 → 顯示 waitingResults
* - 有 classifications → 顯示 label + 信心度百分比
* - 有 detections → 顯示 label + 信心度
* - 低於 confidenceThreshold 的結果被過濾
* - 效能指標 fps / latency 呈現
*/
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it } from "vitest";
import { LocaleProvider } from "@/lib/i18n/context";
import { useInferenceStore } from "@/stores/inference-store";
import { InferencePanel } from "./inference-panel";
function resetStore() {
useInferenceStore.setState({
result: null,
results: [],
fps: 0,
avgLatency: 0,
batchResults: {},
confidenceThreshold: 0.5,
});
}
function renderPanel(isRunning: boolean) {
return render(
<LocaleProvider>
<InferencePanel isRunning={isRunning} />
</LocaleProvider>,
);
}
describe("<InferencePanel />", () => {
beforeEach(resetStore);
it("未推論時顯示 idle 等待提示", () => {
renderPanel(false);
expect(screen.getByTestId("inference-panel-idle")).toBeInTheDocument();
expect(screen.queryByTestId("inference-panel")).not.toBeInTheDocument();
});
it("推論中但無結果 → 顯示 waitingResults", () => {
renderPanel(true);
expect(screen.getByTestId("inference-panel")).toBeInTheDocument();
expect(screen.getByText("等待第一筆結果…")).toBeInTheDocument();
});
it("顯示 classification 結果的 label + 信心度", () => {
useInferenceStore.setState({
result: {
taskType: "classification",
timestamp: Date.now(),
latencyMs: 15,
classifications: [
{ label: "dog", confidence: 0.92 },
{ label: "cat", confidence: 0.61 },
],
},
fps: 12,
avgLatency: 15,
});
renderPanel(true);
expect(screen.getByText("dog")).toBeInTheDocument();
expect(screen.getByText("92%")).toBeInTheDocument();
expect(screen.getByText("cat")).toBeInTheDocument();
expect(screen.getByText("61%")).toBeInTheDocument();
});
it("過濾低於 confidenceThreshold 的結果", () => {
useInferenceStore.setState({
confidenceThreshold: 0.7,
result: {
taskType: "classification",
timestamp: Date.now(),
latencyMs: 15,
classifications: [
{ label: "high", confidence: 0.8 },
{ label: "low", confidence: 0.4 },
],
},
});
renderPanel(true);
expect(screen.getByText("high")).toBeInTheDocument();
expect(screen.queryByText("low")).not.toBeInTheDocument();
});
it("顯示 detection 結果", () => {
useInferenceStore.setState({
result: {
taskType: "detection",
timestamp: Date.now(),
latencyMs: 20,
detections: [
{ label: "person", confidence: 0.88, bbox: { x: 0, y: 0, width: 0.3, height: 0.6 } },
],
},
});
renderPanel(true);
expect(screen.getByText("person")).toBeInTheDocument();
expect(screen.getByText("88%")).toBeInTheDocument();
});
it("呈現 fps 與 latency 指標", () => {
useInferenceStore.setState({ fps: 24, avgLatency: 33.7 });
renderPanel(true);
expect(screen.getByTestId("metric-fps")).toHaveTextContent("24");
expect(screen.getByTestId("metric-latency")).toHaveTextContent("34 ms");
});
});