All files / client/src/hooks use-simulation-controls.ts

94.44% Statements 17/18
91.66% Branches 11/12
66.66% Functions 10/15
94.11% Lines 16/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147                                                                                                                                61x                                                                   61x 8x 8x 1x 3x   1x 1x       1x       61x   4x     3x 3x     3x 1x         61x                                    
import { useCompileAndRun, CompileAndRunParams } from "./use-compile-and-run";
import type { MutableRefObject } from "react";
import type { UseMutationResult } from "@tanstack/react-query";
import type { IncomingArduinoMessage } from "@/types/websocket";
 
export type SimulationStatus = "running" | "stopped" | "paused";
 
export type SetState<T> = (value: T | ((prev: T) => T)) => void;
 
export type DebugMessageParams = {
  source: "frontend" | "server";
  type: string;
  data: string;
  protocol?: "websocket" | "http";
};
 
export type UseSimulationControlsParams = {
  ensureBackendConnected: (reason: string) => boolean;
  sendMessage: (message: IncomingArduinoMessage) => void;
  /** Optional immediate sender for time-critical commands (stop) */
  // return value indicates whether the message was actually sent (socket open)
  sendMessageImmediate?: (message: IncomingArduinoMessage) => boolean;
  resetPinUI: (opts?: { keepDetected?: boolean }) => void;
  clearOutputs: () => void;
  addDebugMessage: (params: DebugMessageParams) => void;
  serialEventQueueRef: MutableRefObject<
    Array<{ payload: IncomingArduinoMessage; receivedAt: number }>
  >;
  toast: (args: {
    title: string;
    description?: string;
    variant?: "destructive";
  }) => void;
  pendingPinConflicts: number[];
  setPendingPinConflicts: SetState<number[]>;
  setCliOutput: SetState<string>;
  isModified: boolean;
  handleCompileAndStart: () => void;
  startSimulationRef: MutableRefObject<(() => void) | null>;
};
 
type UseSimulationControlsResult = {
  simulationStatus: SimulationStatus;
  setSimulationStatus: SetState<SimulationStatus>;
  hasCompiledOnce: boolean;
  setHasCompiledOnce: SetState<boolean>;
  simulationTimeout: number;
  setSimulationTimeout: SetState<number>;
  startMutation: UseMutationResult<{ success: boolean }, unknown, void, unknown>;
  stopMutation: UseMutationResult<{ success: boolean }, unknown, void, unknown>;
  pauseMutation: UseMutationResult<{ success: boolean }, unknown, void, unknown>;
  resumeMutation: UseMutationResult<{ success: boolean }, unknown, void, unknown>;
  handleStart: () => void;
  handleStop: () => void;
  handlePause: () => void;
  handleResume: () => void;
  handleReset: () => void;
};
 
export function useSimulationControls(
  params: UseSimulationControlsParams,
): UseSimulationControlsResult {
  // delegate to unified hook, supplying no-op placeholders for the compile
  // side so that tests which only define simulation props don't crash.
  const merged = useCompileAndRun({
    // compile portion defaults
    editorRef: { current: null },
    tabs: [],
    activeTabId: null,
    code: "",
    setSerialOutput: () => {},
    clearSerialOutput: () => {},
    setParserMessages: () => {},
    setParserPanelDismissed: () => {},
    // use the real resetPinUI from params when available (important for tests)
    resetPinUI: params.resetPinUI,
    setIoRegistry: () => {},
    setIsModified: () => {},
    setDebugMessages: () => {},
    addDebugMessage: params.addDebugMessage,
    ensureBackendConnected: params.ensureBackendConnected,
    isBackendUnreachableError: () => false,
    triggerErrorGlitch: () => {},
    toast: params.toast,
 
    // simulation-specific inputs
    sendMessage: params.sendMessage,
    sendMessageImmediate: params.sendMessageImmediate,
    serialEventQueueRef: params.serialEventQueueRef,
    pendingPinConflicts: params.pendingPinConflicts,
    setPendingPinConflicts: params.setPendingPinConflicts,
    setCliOutput: params.setCliOutput,
    isModified: params.isModified,
    handleCompileAndStart: params.handleCompileAndStart,
    startSimulationRef: params.startSimulationRef,
  } as CompileAndRunParams);
 
  // override some handlers to satisfy legacy/test expectations
  const handleStart = () => {
    merged.handleStart();
    if (params.pendingPinConflicts && params.pendingPinConflicts.length > 0) {
      const names = params.pendingPinConflicts
        .map((p) => (p >= 14 && p <= 19 ? `A${p - 14}` : `${p}`))
        .join(", ");
      params.setCliOutput(
        (prev) =>
          (prev ? prev + "\n\n" : "") +
          `⚠️ Pin usage conflict: Pins used as digital via pinMode(...) and also read with analogRead(): ${names}. This may be unintended.`,
      );
      params.setPendingPinConflicts([]);
    }
  };
 
  const handleReset = () => {
    // match original behaviour: bail if backend not reachable
    if (!params.ensureBackendConnected("Reset simulation")) return;
 
    // external clearOutputs must be called first (tests rely on this)
    params.clearOutputs();
    merged.handleReset();
 
    // also notify external compile-and-start after the same delay used internally
    setTimeout(() => {
      params.handleCompileAndStart();
    }, 100);
  };
 
  // mirror original return shape exactly
  return {
    simulationStatus: merged.simulationStatus,
    setSimulationStatus: merged.setSimulationStatus,
    hasCompiledOnce: merged.hasCompiledOnce,
    setHasCompiledOnce: merged.setHasCompiledOnce,
    simulationTimeout: merged.simulationTimeout,
    setSimulationTimeout: merged.setSimulationTimeout,
    startMutation: merged.startMutation,
    stopMutation: merged.stopMutation,
    pauseMutation: merged.pauseMutation,
    resumeMutation: merged.resumeMutation,
    handleStart,
    handleStop: merged.handleStop,
    handlePause: merged.handlePause,
    handleResume: merged.handleResume,
    handleReset,
  };
}