Add frontend debug mode gate
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
23
src/config/debugMode.ts
Normal 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
4
src/vite-env.d.ts
vendored
@@ -1 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_DEBUG_MODE?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user