Add frontend debug mode gate

This commit is contained in:
2026-05-11 18:00:36 +08:00
parent 928acb4302
commit 7cea41c911
7 changed files with 137 additions and 15 deletions

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import { beforeEach, expect, test, vi } from 'vitest';
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import { AuthUiContext } from '../auth/AuthUiContext';
@@ -31,6 +31,15 @@ const mocapMock = vi.hoisted(() => ({
y: 0.58,
}));
const debugModeMock = vi.hoisted(() => ({
enabled: true,
}));
vi.mock('../../config/debugMode', () => ({
IS_DEBUG_MODE: debugModeMock.enabled,
isDebugMode: () => debugModeMock.enabled,
}));
vi.mock('../../services/useMocapInput', () => ({
useMocapInput: () => ({
status: 'connected',
@@ -44,6 +53,13 @@ vi.mock('../../services/useMocapInput', () => ({
}),
}));
beforeEach(() => {
debugModeMock.enabled = true;
mocapMock.state = 'grab';
mocapMock.x = 0.42;
mocapMock.y = 0.58;
});
function createAuthValue() {
return {
user: null,
@@ -157,7 +173,7 @@ const clearedRun: PuzzleRunSnapshot = {
},
};
test('拼图界面示 mocap 连接状态最近动作调试信息', () => {
test('调试模式下拼图界面折叠展示 mocap 连接状态,展开后显示最近动作调试信息', () => {
renderPuzzleRuntime(
<PuzzleRuntimeShell
run={{
@@ -176,12 +192,42 @@ test('拼图界面显示 mocap 连接状态和最近动作调试信息', () => {
const debugPanel = screen.getByTestId('puzzle-mocap-debug');
expect(within(debugPanel).getByText('mocap: connected')).toBeTruthy();
const toggleButton = within(debugPanel).getByRole('button', {
name: 'mocap: connected',
});
expect(toggleButton.getAttribute('aria-expanded')).toBe('false');
expect(within(debugPanel).queryByText('动作: grab')).toBeNull();
fireEvent.click(toggleButton);
expect(toggleButton.getAttribute('aria-expanded')).toBe('true');
expect(within(debugPanel).getByText('动作: grab')).toBeTruthy();
expect(within(debugPanel).getByText('手势: grab @ 0.42, 0.58')).toBeTruthy();
expect(within(debugPanel).getByText('解析: 无')).toBeTruthy();
expect(within(debugPanel).getByText(/原始:/)).toBeTruthy();
});
test('非调试模式下拼图界面不渲染 mocap 调试面板', () => {
debugModeMock.enabled = false;
renderPuzzleRuntime(
<PuzzleRuntimeShell
run={{
...clearedRun,
currentLevel: {
...clearedRun.currentLevel!,
status: 'playing',
},
}}
onBack={vi.fn()}
onSwapPieces={vi.fn()}
onDragPiece={vi.fn()}
onAdvanceNextLevel={vi.fn()}
/>,
);
expect(screen.queryByTestId('puzzle-mocap-debug')).toBeNull();
});
test('拼图界面在 mocap open_palm 时显示体感光标', () => {
mocapMock.state = 'open_palm';
mocapMock.x = 0.42;

View File

@@ -1,6 +1,8 @@
import {
ArrowLeft,
ArrowRight,
ChevronDown,
ChevronUp,
Clock,
Eye,
Lightbulb,
@@ -22,6 +24,7 @@ import type {
PuzzleRuntimePropKind,
SwapPuzzlePiecesRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import { isDebugMode } from '../../config/debugMode';
import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl';
import {
createRuntimeDragInputController,
@@ -361,6 +364,7 @@ export function PuzzleRuntimeShell({
const [isFreezeEffectVisible, setIsFreezeEffectVisible] = useState(false);
const [isPropConfirming, setIsPropConfirming] = useState(false);
const [propConfirmError, setPropConfirmError] = useState<string | null>(null);
const [isMocapDebugExpanded, setIsMocapDebugExpanded] = useState(false);
const [hintDemo, setHintDemo] = useState<PuzzleHintDemoState | null>(null);
const [mergeFlash, setMergeFlash] = useState<PuzzleMergeFlashState | null>(
null,
@@ -462,6 +466,7 @@ export function PuzzleRuntimeShell({
? mocapInput.latestCommand.parseWarnings.join('')
: '无';
const mocapRawPacketLabel = mocapInput.rawPacketPreview?.text ?? '未收到';
const shouldShowMocapDebugPanel = isDebugMode();
useEffect(() => {
currentLevelRef.current = currentLevel;
@@ -1744,19 +1749,45 @@ export function PuzzleRuntimeShell({
</div>
) : null}
<div
data-testid="puzzle-mocap-debug"
className="w-[min(92vw,34rem)] rounded-[0.9rem] border border-white/20 bg-slate-950/70 px-3 py-2 font-mono text-[10px] leading-4 text-white shadow-[0_12px_32px_rgba(15,23,42,0.25)] backdrop-blur"
>
<div>mocap: {mocapInput.status}</div>
<div>: {mocapActionsLabel}</div>
<div>: {mocapHandLabel}</div>
<div>: {mocapParseWarningLabel}</div>
<div className="max-h-20 overflow-auto break-all text-white/75">
: {mocapRawPacketLabel}
</div>
{mocapInput.error ? <div>: {mocapInput.error}</div> : null}
</div>
{shouldShowMocapDebugPanel ? (
<section
data-testid="puzzle-mocap-debug"
className="w-[min(92vw,34rem)] overflow-hidden rounded-[0.9rem] border border-white/20 bg-slate-950/70 font-mono text-[10px] leading-4 text-white shadow-[0_12px_32px_rgba(15,23,42,0.25)] backdrop-blur"
>
<button
type="button"
aria-expanded={isMocapDebugExpanded}
aria-controls="puzzle-mocap-debug-content"
onClick={() => {
setIsMocapDebugExpanded((current) => !current);
}}
className="flex min-h-9 w-full items-center justify-between gap-3 px-3 py-2 text-left transition hover:bg-white/10"
>
<span className="min-w-0 truncate">
mocap: {mocapInput.status}
</span>
{isMocapDebugExpanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
) : (
<ChevronUp className="h-3.5 w-3.5 shrink-0" />
)}
</button>
{isMocapDebugExpanded ? (
<div
id="puzzle-mocap-debug-content"
className="border-t border-white/10 px-3 pb-2 pt-2"
>
<div>: {mocapActionsLabel}</div>
<div>: {mocapHandLabel}</div>
<div>: {mocapParseWarningLabel}</div>
<div className="max-h-20 overflow-auto break-all text-white/75">
: {mocapRawPacketLabel}
</div>
{mocapInput.error ? <div>: {mocapInput.error}</div> : null}
</div>
) : null}
</section>
) : null}
{canShowNextAction ? (
<button
type="button"

23
src/config/debugMode.ts Normal file
View File

@@ -0,0 +1,23 @@
const DEBUG_MODE_TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
const DEBUG_MODE_FALSE_VALUES = new Set(['0', 'false', 'no', 'off']);
function parseOptionalBoolean(value: string | undefined) {
const normalizedValue = value?.trim().toLowerCase();
if (!normalizedValue) {
return null;
}
if (DEBUG_MODE_TRUE_VALUES.has(normalizedValue)) {
return true;
}
if (DEBUG_MODE_FALSE_VALUES.has(normalizedValue)) {
return false;
}
return null;
}
export const IS_DEBUG_MODE =
parseOptionalBoolean(import.meta.env.VITE_DEBUG_MODE) ?? import.meta.env.DEV;
export function isDebugMode() {
return IS_DEBUG_MODE;
}

4
src/vite-env.d.ts vendored
View File

@@ -1 +1,5 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DEBUG_MODE?: string;
}