推論工作區前端塊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>
116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
/**
|
||
* 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");
|
||
});
|
||
});
|