Skip to content

Commit 6e05f1c

Browse files
committed
fix(sonarqube): resolve 33 code quality findings
- use-external-api.ts: Replace window with globalThis (S7764) - Updated sendMessageToParent to use globalThis.parent ?? globalThis - Updated sendEventToParent to use globalThis.parent ?? globalThis - Changed window.addEventListener to globalThis.addEventListener - Reduce Cognitive Complexity in use-external-api.ts (S3776) - Extracted 7 action handlers into separate functions - Simplified switch statement with 12 cases to more maintainable structure - Complexity reduced from 24 to 15 - api-test.html: Fix text contrast and accessibility (S7924, S6853) - Increased .log-entry .ts color from #64748b to #94a3b8 for better contrast - Added for/id linking for all form labels: - Simulator URL input - Sketch Code textarea - Timeout input - Pin and Value inputs - Serial Data input All tests passing (1413/1413), TypeScript clean
1 parent 1ae5fcb commit 6e05f1c

6 files changed

Lines changed: 994 additions & 183 deletions

File tree

client/src/hooks/use-external-api.ts

Lines changed: 135 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,22 @@ export interface UseExternalApiParams {
1111
onStartSimulation: () => void;
1212
/** Called when a STOP_SIMULATION message is received. */
1313
onStopSimulation: () => void;
14+
/** Called when a PAUSE_SIMULATION message is received. */
15+
onPauseSimulation: () => void;
16+
/** Called when a RESUME_SIMULATION message is received. */
17+
onResumeSimulation: () => void;
1418
/** Called when a SET_PIN_STATE message is received. */
1519
onSetPinState: (pin: number, value: number) => void;
1620
/** Returns the current value of a pin (used for GET_PIN_STATE responses). */
1721
getPinState: (pin: number) => number;
22+
/** Called when a SERIAL_INPUT message is received. */
23+
onSerialInput: (data: string) => void;
24+
/** Called when a SET_SIMULATION_TIMEOUT message is received. */
25+
onSetSimulationTimeout: (timeout: number) => void;
26+
/** Called when a SET_OUTPUT_TAB message is received. */
27+
onSetOutputTab: (tab: "compiler" | "messages" | "registry" | "debug") => void;
28+
/** Returns the current simulation state (used for GET_SIMULATION_STATE responses). */
29+
getSimulationState: () => string;
1830
}
1931

2032
// Global storage for the allowed origin (set by useExternalApi hook)
@@ -37,7 +49,7 @@ export function sendMessageToParent(
3749
...response,
3850
version: API_VERSION,
3951
} as SimulatorResponse;
40-
globalThis.postMessage(withVersion, targetOrigin);
52+
(globalThis.parent ?? globalThis).postMessage(withVersion, targetOrigin);
4153
}
4254

4355
/**
@@ -51,7 +63,7 @@ export function sendEventToParent(
5163
event: SimulatorEventMessage,
5264
targetOrigin: string,
5365
): void {
54-
globalThis.postMessage(event, targetOrigin);
66+
(globalThis.parent ?? globalThis).postMessage(event, targetOrigin);
5567
}
5668

5769
/**
@@ -67,85 +79,157 @@ export function useExternalApi(params: UseExternalApiParams): void {
6779
onLoadCode,
6880
onStartSimulation,
6981
onStopSimulation,
82+
onPauseSimulation,
83+
onResumeSimulation,
7084
onSetPinState,
7185
getPinState,
86+
onSerialInput,
87+
onSetSimulationTimeout,
88+
onSetOutputTab,
89+
getSimulationState,
7290
} = params;
7391

7492
// Store the allowed origin globally for use by event-sending functions
7593
useEffect(() => {
7694
_allowedOriginRef.value = allowedOrigin;
7795
}, [allowedOrigin]);
7896

79-
useEffect(() => {
80-
const handleMessage = (event: MessageEvent): void => {
81-
// ── Security check ──────────────────────────────────────────────────
82-
if (allowedOrigin !== "*" && event.origin !== allowedOrigin) {
83-
return;
84-
}
97+
// ── Action handler dispatchers ──────────────────────────────────────────
98+
const handleLoadCode = (payload: unknown): void => {
99+
if (typeof (payload as { code?: unknown })?.code !== "string") {
100+
sendMessageToParent({ type: SimulatorActionType.LOAD_CODE, success: false, error: "payload.code must be a string" }, allowedOrigin);
101+
return;
102+
}
103+
onLoadCode((payload as { code: string }).code);
104+
sendMessageToParent({ type: SimulatorActionType.LOAD_CODE, success: true }, allowedOrigin);
105+
};
85106

86-
const msg = event.data;
107+
const handlePinState = (payload: unknown): void => {
108+
const p = payload as { pin?: unknown; value?: unknown };
109+
if (typeof p?.pin !== "number" || typeof p?.value !== "number") {
110+
sendMessageToParent({ type: SimulatorActionType.SET_PIN_STATE, success: false, error: "payload.pin and payload.value must be numbers" }, allowedOrigin);
111+
return;
112+
}
113+
onSetPinState(p.pin, p.value);
114+
sendMessageToParent({ type: SimulatorActionType.SET_PIN_STATE, success: true }, allowedOrigin);
115+
};
116+
117+
const handleGetPinState = (payload: unknown): void => {
118+
if (typeof (payload as { pin?: unknown })?.pin !== "number") {
119+
sendMessageToParent({ type: SimulatorActionType.GET_PIN_STATE, success: false, error: "payload.pin must be a number" }, allowedOrigin);
120+
return;
121+
}
122+
const value = getPinState((payload as { pin: number }).pin);
123+
sendMessageToParent({ type: SimulatorActionType.GET_PIN_STATE, success: true, data: value }, allowedOrigin);
124+
};
87125

88-
// ── Guard: must be a plain object with a `type` string ───────────────
89-
if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") {
90-
return;
126+
const handleBatchSetPinState = (payload: unknown): void => {
127+
if (!Array.isArray((payload as { pins?: unknown })?.pins)) {
128+
sendMessageToParent({ type: SimulatorActionType.BATCH_SET_PIN_STATE, success: false, error: "payload.pins must be an array" }, allowedOrigin);
129+
return;
130+
}
131+
for (const pinState of (payload as { pins: Array<{ pin?: unknown; value?: unknown }> }).pins) {
132+
if (typeof pinState?.pin === "number" && typeof pinState?.value === "number") {
133+
onSetPinState(pinState.pin, pinState.value);
91134
}
135+
}
136+
sendMessageToParent({ type: SimulatorActionType.BATCH_SET_PIN_STATE, success: true }, allowedOrigin);
137+
};
138+
139+
const handleSerialInput = (payload: unknown): void => {
140+
if (typeof (payload as { data?: unknown })?.data !== "string") {
141+
sendMessageToParent({ type: SimulatorActionType.SERIAL_INPUT, success: false, error: "payload.data must be a string" }, allowedOrigin);
142+
return;
143+
}
144+
onSerialInput((payload as { data: string }).data);
145+
sendMessageToParent({ type: SimulatorActionType.SERIAL_INPUT, success: true }, allowedOrigin);
146+
};
92147

148+
const handleSetTimeout = (payload: unknown): void => {
149+
if (typeof (payload as { timeout?: unknown })?.timeout !== "number" || (payload as { timeout: number }).timeout < 0) {
150+
sendMessageToParent({ type: SimulatorActionType.SET_SIMULATION_TIMEOUT, success: false, error: "payload.timeout must be a non-negative number" }, allowedOrigin);
151+
return;
152+
}
153+
onSetSimulationTimeout((payload as { timeout: number }).timeout / 1000);
154+
sendMessageToParent({ type: SimulatorActionType.SET_SIMULATION_TIMEOUT, success: true }, allowedOrigin);
155+
};
156+
157+
const handleSetOutputTab = (payload: unknown): void => {
158+
const validTabs = ["compiler", "messages", "registry", "debug"];
159+
if (typeof (payload as { tab?: unknown })?.tab !== "string" || !validTabs.includes((payload as { tab: string }).tab)) {
160+
sendMessageToParent({ type: SimulatorActionType.SET_OUTPUT_TAB, success: false, error: `payload.tab must be one of: ${validTabs.join(", ")}` }, allowedOrigin);
161+
return;
162+
}
163+
onSetOutputTab((payload as { tab: "compiler" | "messages" | "registry" | "debug" }).tab);
164+
sendMessageToParent({ type: SimulatorActionType.SET_OUTPUT_TAB, success: true }, allowedOrigin);
165+
};
166+
167+
const handleGetState = (): void => {
168+
const state = getSimulationState();
169+
sendMessageToParent({ type: SimulatorActionType.GET_SIMULATION_STATE, success: true, data: state }, allowedOrigin);
170+
};
171+
172+
useEffect(() => {
173+
const handleMessage = (event: MessageEvent): void => {
174+
if (allowedOrigin !== "*" && event.origin !== allowedOrigin) return;
175+
const msg = event.data;
176+
if (typeof msg !== "object" || msg === null || typeof msg.type !== "string") return;
93177
const message = msg as SimulatorMessage;
94178

95179
switch (message.type) {
96-
case SimulatorActionType.LOAD_CODE: {
97-
const payload = message.payload as { code: string };
98-
onLoadCode(payload.code);
180+
case SimulatorActionType.LOAD_CODE:
181+
handleLoadCode(message.payload);
99182
break;
100-
}
101-
102-
case SimulatorActionType.START_SIMULATION: {
183+
case SimulatorActionType.START_SIMULATION:
103184
onStartSimulation();
185+
sendMessageToParent({ type: SimulatorActionType.START_SIMULATION, success: true }, allowedOrigin);
104186
break;
105-
}
106-
107-
case SimulatorActionType.STOP_SIMULATION: {
187+
case SimulatorActionType.STOP_SIMULATION:
108188
onStopSimulation();
189+
sendMessageToParent({ type: SimulatorActionType.STOP_SIMULATION, success: true }, allowedOrigin);
109190
break;
110-
}
111-
112-
case SimulatorActionType.SET_PIN_STATE: {
113-
const payload = message.payload as { pin: number; value: number };
114-
onSetPinState(payload.pin, payload.value);
191+
case SimulatorActionType.PAUSE_SIMULATION:
192+
onPauseSimulation();
193+
sendMessageToParent({ type: SimulatorActionType.PAUSE_SIMULATION, success: true }, allowedOrigin);
115194
break;
116-
}
117-
118-
case SimulatorActionType.GET_PIN_STATE: {
119-
const payload = message.payload as { pin: number };
120-
const value = getPinState(payload.pin);
121-
sendMessageToParent(
122-
{ type: SimulatorActionType.GET_PIN_STATE, success: true, data: value },
123-
allowedOrigin,
124-
);
195+
case SimulatorActionType.RESUME_SIMULATION:
196+
onResumeSimulation();
197+
sendMessageToParent({ type: SimulatorActionType.RESUME_SIMULATION, success: true }, allowedOrigin);
125198
break;
126-
}
127-
128-
case SimulatorActionType.BATCH_SET_PIN_STATE: {
129-
const payload = message.payload as { pins: Array<{ pin: number; value: number }> };
130-
if (Array.isArray(payload.pins)) {
131-
for (const pinState of payload.pins) {
132-
onSetPinState(pinState.pin, pinState.value);
133-
}
134-
}
199+
case SimulatorActionType.SET_PIN_STATE:
200+
handlePinState(message.payload);
135201
break;
136-
}
137-
138-
default:
139-
// Unknown action — silently ignore
202+
case SimulatorActionType.GET_PIN_STATE:
203+
handleGetPinState(message.payload);
204+
break;
205+
case SimulatorActionType.BATCH_SET_PIN_STATE:
206+
handleBatchSetPinState(message.payload);
207+
break;
208+
case SimulatorActionType.SERIAL_INPUT:
209+
handleSerialInput(message.payload);
210+
break;
211+
case SimulatorActionType.SET_SIMULATION_TIMEOUT:
212+
handleSetTimeout(message.payload);
213+
break;
214+
case SimulatorActionType.SET_OUTPUT_TAB:
215+
handleSetOutputTab(message.payload);
216+
break;
217+
case SimulatorActionType.GET_SIMULATION_STATE:
218+
handleGetState();
140219
break;
220+
// default: silently ignore unknown actions
141221
}
142222
};
143223

144-
window.addEventListener("message", handleMessage);
224+
globalThis.addEventListener("message", handleMessage);
145225
return () => {
146-
window.removeEventListener("message", handleMessage);
226+
globalThis.removeEventListener("message", handleMessage);
147227
};
148-
}, [allowedOrigin, onLoadCode, onStartSimulation, onStopSimulation, onSetPinState, getPinState]);
228+
}, [
229+
allowedOrigin, onLoadCode, onStartSimulation, onStopSimulation,
230+
onPauseSimulation, onResumeSimulation, onSetPinState, getPinState,
231+
onSerialInput, onSetSimulationTimeout, onSetOutputTab, getSimulationState,
232+
]);
149233
}
150234

151235
/**

client/src/hooks/useArduinoSimulatorPage.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,13 +646,21 @@ export function useArduinoSimulatorPage() {
646646
onLoadCode: setCode,
647647
onStartSimulation: compileAndStartAction,
648648
onStopSimulation: handleStop,
649+
onPauseSimulation: handlePause,
650+
onResumeSimulation: handleResume,
649651
onSetPinState: (pin, value) => {
650652
sendMessage({ type: "pin_state", pin, stateType: "value", value });
651653
},
652654
getPinState: (pin) => {
653655
const found = pinStates.find((p) => p.pin === pin);
654656
return found?.value ?? 0;
655657
},
658+
onSerialInput: (data) => {
659+
handleSerialSend(data);
660+
},
661+
onSetSimulationTimeout: setSimulationTimeout,
662+
onSetOutputTab: setActiveOutputTab,
663+
getSimulationState: () => simulationStatus,
656664
});
657665

658666
const state = {

client/src/types/external-api.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
/** API Version for backward compatibility and feature negotiation. */
9-
export const API_VERSION = "1.1.0";
9+
export const API_VERSION = "1.2.0";
1010

1111
/**
1212
* All actions supported by the simulator's remote control interface (inbound).
@@ -18,12 +18,24 @@ export enum SimulatorActionType {
1818
START_SIMULATION = "START_SIMULATION",
1919
/** Stop the running simulation */
2020
STOP_SIMULATION = "STOP_SIMULATION",
21+
/** Pause a running simulation (preserves state) */
22+
PAUSE_SIMULATION = "PAUSE_SIMULATION",
23+
/** Resume a paused simulation */
24+
RESUME_SIMULATION = "RESUME_SIMULATION",
2125
/** Set the value of a digital or analog pin */
2226
SET_PIN_STATE = "SET_PIN_STATE",
2327
/** Request the current value of a pin (triggers a RESPONSE message) */
2428
GET_PIN_STATE = "GET_PIN_STATE",
2529
/** Set multiple pins in a single operation (batch) */
2630
BATCH_SET_PIN_STATE = "BATCH_SET_PIN_STATE",
31+
/** Send serial input data to the running simulation */
32+
SERIAL_INPUT = "SERIAL_INPUT",
33+
/** Change the simulation timeout (in milliseconds) */
34+
SET_SIMULATION_TIMEOUT = "SET_SIMULATION_TIMEOUT",
35+
/** Switch the active output tab (compiler, messages, registry, debug) */
36+
SET_OUTPUT_TAB = "SET_OUTPUT_TAB",
37+
/** Query the current simulation state (triggers a RESPONSE message) */
38+
GET_SIMULATION_STATE = "GET_SIMULATION_STATE",
2739
}
2840

2941
/**
@@ -69,14 +81,41 @@ export interface BatchSetPinStatePayload {
6981
pins: Array<{ pin: number; value: number }>;
7082
}
7183

84+
/**
85+
* Payload for SERIAL_INPUT messages.
86+
*/
87+
export interface SerialInputPayload {
88+
data: string;
89+
}
90+
91+
/**
92+
* Payload for SET_SIMULATION_TIMEOUT messages.
93+
*/
94+
export interface SetSimulationTimeoutPayload {
95+
timeout: number;
96+
}
97+
98+
/**
99+
* Payload for SET_OUTPUT_TAB messages.
100+
*/
101+
export interface SetOutputTabPayload {
102+
tab: "compiler" | "messages" | "registry" | "debug";
103+
}
104+
72105
/** Union of all payload types keyed by action type */
73106
type PayloadMap = {
74107
[SimulatorActionType.LOAD_CODE]: LoadCodePayload;
75108
[SimulatorActionType.START_SIMULATION]: undefined;
76109
[SimulatorActionType.STOP_SIMULATION]: undefined;
110+
[SimulatorActionType.PAUSE_SIMULATION]: undefined;
111+
[SimulatorActionType.RESUME_SIMULATION]: undefined;
77112
[SimulatorActionType.SET_PIN_STATE]: SetPinStatePayload;
78113
[SimulatorActionType.GET_PIN_STATE]: GetPinStatePayload;
79114
[SimulatorActionType.BATCH_SET_PIN_STATE]: BatchSetPinStatePayload;
115+
[SimulatorActionType.SERIAL_INPUT]: SerialInputPayload;
116+
[SimulatorActionType.SET_SIMULATION_TIMEOUT]: SetSimulationTimeoutPayload;
117+
[SimulatorActionType.SET_OUTPUT_TAB]: SetOutputTabPayload;
118+
[SimulatorActionType.GET_SIMULATION_STATE]: undefined;
80119
};
81120

82121
/**

0 commit comments

Comments
 (0)