diff --git a/client/src/components/features/parser-output.tsx b/client/src/components/features/parser-output.tsx index d22c04fdf..2caf42de7 100644 --- a/client/src/components/features/parser-output.tsx +++ b/client/src/components/features/parser-output.tsx @@ -17,6 +17,7 @@ import { useState } from "react"; import * as React from "react"; import type { SeverityLevel } from "@shared/reserved-names-validator"; import type { SourceLocation } from "@shared/source-project"; +import { isUnoPwmPin } from "@shared/utils/arduino-utils"; import { formatSourceNavigationTarget, isSourceLocation, @@ -28,7 +29,6 @@ import { TabBar } from "@/components/ui/tab-bar"; import { getSeverityStatus, getStatusColor, getStatusTextClass } from "@/lib/status-semantics"; // Module-level constants (not re-created on every render) -const PWM_PINS = new Set([3, 5, 6, 9, 10, 11]); const ALL_PIN_RECORDS: IOPinRecord[] = Array.from({ length: 20 }, (_, pinId) => ({ pin: pinId >= 14 ? `A${pinId - 14}` : String(pinId), pinId, @@ -80,7 +80,7 @@ function getRxTxBadge(pin: string): JSX.Element | null { /** Returns a PWM tilde element for PWM-capable pins, or null. Fixes S1940 IIFE anti-pattern. */ function getPwmTilde(pin: string): JSX.Element | null { const n = Number.parseInt(pin, 10); - return !Number.isNaN(n) && PWM_PINS.has(n) + return !Number.isNaN(n) && isUnoPwmPin(n) ? ~ : null; } diff --git a/client/src/hooks/usePinPollingEngine.ts b/client/src/hooks/usePinPollingEngine.ts index 20e00ba80..12670a315 100644 --- a/client/src/hooks/usePinPollingEngine.ts +++ b/client/src/hooks/usePinPollingEngine.ts @@ -1,9 +1,9 @@ import { useEffect } from "react"; import type { PinState } from "@/components/features/arduino-board"; +import { UNO_PWM_PINS, isUnoPwmPin } from "@shared/utils/arduino-utils"; // Constants const VIEWBOX_HEIGHT = 209; -const PWM_PINS = [3, 5, 6, 9, 10, 11]; const FADE_OUT_MS = 200; /** @@ -240,7 +240,7 @@ function pinColorForState(state: PinState, pin: number, brightness: number): str if (brightness <= 0) return "var(--color-black)"; const intensity = Math.round(brightness * 255); if (state.type === "digital") return `rgb(${intensity}, 0, 0)`; - if (PWM_PINS.includes(pin)) { + if (isUnoPwmPin(pin)) { const pwmIntensity = Math.round((state.value / 255) * intensity); return `rgb(${pwmIntensity}, 0, 0)`; } @@ -391,7 +391,7 @@ export function usePinPollingEngine({ return; } - for (const pin of PWM_PINS) { + for (const pin of UNO_PWM_PINS) { const valStr = getPinState(pin)?.value?.toString() ?? ""; updateLabelForPin(svgEl, `pin-${pin}-state`, `pin-${pin}-frame`, `pin-${pin}-val`, valStr, showPWMValues); } diff --git a/shared/parsers/hardware-compatibility-parser.ts b/shared/parsers/hardware-compatibility-parser.ts index b371253de..97ea727b3 100644 --- a/shared/parsers/hardware-compatibility-parser.ts +++ b/shared/parsers/hardware-compatibility-parser.ts @@ -6,8 +6,8 @@ import { type StaticIOOperation, type UnresolvedStaticIOCall, } from "@shared/io-registry-parser"; +import { UNO_PWM_PINS, isUnoPwmPin } from "@shared/utils/arduino-utils"; -const PWM_PINS = new Set([3, 5, 6, 9, 10, 11]); const DIGITAL_OPERATIONS = new Set([ "digitalRead", "digitalWrite", @@ -93,13 +93,13 @@ export class HardwareCompatibilityParser { private checkAnalogWritePWM(): ParserMessage[] { return resolvedCalls(this.analysis, "analogWrite") - .filter(({ pinId }) => !PWM_PINS.has(pinId)) + .filter(({ pinId }) => !isUnoPwmPin(pinId)) .map((call) => ({ id: randomUUID(), type: "warning" as const, category: "hardware" as const, severity: 2 as const, - message: `analogWrite(${call.sourceExpression}, ...) used on pin ${call.pinId}, which doesn't support PWM on Arduino UNO. PWM pins: 3, 5, 6, 9, 10, 11.`, + message: `analogWrite(${call.sourceExpression}, ...) used on pin ${call.pinId}, which doesn't support PWM on Arduino UNO. PWM pins: ${UNO_PWM_PINS.join(", ")}.`, suggestion: "// Use PWM pin instead: analogWrite(3, value);", ...sourceLocation(call), })); diff --git a/shared/utils/arduino-utils.ts b/shared/utils/arduino-utils.ts index f79c1b51a..d80103fc7 100644 --- a/shared/utils/arduino-utils.ts +++ b/shared/utils/arduino-utils.ts @@ -9,3 +9,16 @@ const PIN_MODE_NAMES: Record = { export function pinModeToString(mode: number): PinMode { return PIN_MODE_NAMES[mode] ?? "INPUT"; // Default to INPUT for invalid modes } + +/** + * PWM-capable digital pins of the Arduino UNO (the pins marked "~"), in board + * order. The single definition of this hardware property: UI, polling engine + * and hardware-compatibility checks all derive from it. + */ +export const UNO_PWM_PINS: readonly number[] = [3, 5, 6, 9, 10, 11]; + +const UNO_PWM_PIN_SET: ReadonlySet = new Set(UNO_PWM_PINS); + +export function isUnoPwmPin(pin: number): boolean { + return UNO_PWM_PIN_SET.has(pin); +} diff --git a/tests/client/arduino-simulator-codechange.test.tsx b/tests/client/arduino-simulator-codechange.test.tsx index dec66014d..496f60896 100644 --- a/tests/client/arduino-simulator-codechange.test.tsx +++ b/tests/client/arduino-simulator-codechange.test.tsx @@ -76,7 +76,7 @@ test("handles simulation_status message", async () => { // Sanity-check: hook exposes empty queue initially const wsMock = (await import("@/hooks/use-websocket")).useWebSocket(); - expect(wsMock.messageQueue.length).toBe(0); + expect(wsMock.messageQueue).toHaveLength(0); // Push message AFTER mount and trigger re-render in act() await act(async () => { @@ -98,11 +98,9 @@ test("handles simulation_status message", async () => { vi.runOnlyPendingTimers(); }); - // Test the assertion - await act(async () => { - await waitFor(() => { - expect(document.querySelector('[data-testid="sim-status"]')?.textContent).toBe("running"); - }); + // waitFor flushes its own updates; no surrounding act() needed + await waitFor(() => { + expect(document.querySelector('[data-testid="sim-status"]')?.textContent).toBe("running"); }); // Final timer flush diff --git a/tests/client/font-scale-utils.test.ts b/tests/client/font-scale-utils.test.ts index 35cd7b89f..ba1fc0615 100644 --- a/tests/client/font-scale-utils.test.ts +++ b/tests/client/font-scale-utils.test.ts @@ -110,90 +110,34 @@ describe("Font Scale Utils", () => { }); describe("increaseFontScale", () => { - it("should increase from S to M", () => { - setFontScale(0.875); // S + it.each([ + { title: "should increase from S to M", from: 0.875, changed: true, to: 1.0 }, + { title: "should increase from M to L", from: 1.0, changed: true, to: 1.125 }, + { title: "should increase from L to XL", from: 1.125, changed: true, to: 1.25 }, + { title: "should increase from XL to XXL", from: 1.25, changed: true, to: 1.5 }, + { title: "should not increase beyond XXL", from: 1.5, changed: false, to: 1.5 }, + { title: "should handle custom scale values (0.9 jumps to M)", from: 0.9, changed: true, to: 1.0 }, + ])("$title", ({ from, changed, to }) => { + setFontScale(from); const result = increaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.0); // M - }); - - it("should increase from M to L", () => { - setFontScale(1.0); // M - const result = increaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.125); // L - }); - - it("should increase from L to XL", () => { - setFontScale(1.125); // L - const result = increaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.25); // XL - }); - - it("should increase from XL to XXL", () => { - setFontScale(1.25); // XL - const result = increaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.5); // XXL - }); - - it("should not increase beyond XXL", () => { - setFontScale(1.5); // XXL - const result = increaseFontScale(); - expect(result).toBe(false); - expect(getCurrentFontScale()).toBe(1.5); // Still XXL - }); - - it("should handle custom scale values", () => { - setFontScale(0.9); // Between S and M - const result = increaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.0); // Jumps to M + expect(result).toBe(changed); + expect(getCurrentFontScale()).toBe(to); }); }); describe("decreaseFontScale", () => { - it("should decrease from XXL to XL", () => { - setFontScale(1.5); // XXL - const result = decreaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.25); // XL - }); - - it("should decrease from XL to L", () => { - setFontScale(1.25); // XL - const result = decreaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.125); // L - }); - - it("should decrease from L to M", () => { - setFontScale(1.125); // L - const result = decreaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.0); // M - }); - - it("should decrease from M to S", () => { - setFontScale(1.0); // M - const result = decreaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(0.875); // S - }); - - it("should not decrease below S", () => { - setFontScale(0.875); // S - const result = decreaseFontScale(); - expect(result).toBe(false); - expect(getCurrentFontScale()).toBe(0.875); // Still S - }); - - it("should handle custom scale values", () => { - setFontScale(1.1); // Between M and L + it.each([ + { title: "should decrease from XXL to XL", from: 1.5, changed: true, to: 1.25 }, + { title: "should decrease from XL to L", from: 1.25, changed: true, to: 1.125 }, + { title: "should decrease from L to M", from: 1.125, changed: true, to: 1.0 }, + { title: "should decrease from M to S", from: 1.0, changed: true, to: 0.875 }, + { title: "should not decrease below S", from: 0.875, changed: false, to: 0.875 }, + { title: "should handle custom scale values (1.1 jumps to M)", from: 1.1, changed: true, to: 1.0 }, + ])("$title", ({ from, changed, to }) => { + setFontScale(from); const result = decreaseFontScale(); - expect(result).toBe(true); - expect(getCurrentFontScale()).toBe(1.0); // Jumps to M + expect(result).toBe(changed); + expect(getCurrentFontScale()).toBe(to); }); }); diff --git a/tests/client/hooks/use-backend-health.test.tsx b/tests/client/hooks/use-backend-health.test.tsx index a1ac5002e..6da059621 100644 --- a/tests/client/hooks/use-backend-health.test.tsx +++ b/tests/client/hooks/use-backend-health.test.tsx @@ -451,7 +451,7 @@ describe("useBackendHealth", () => { }); // No new calls after unmount - expect(fetchSpy.mock.calls.length).toBe(callsBefore); + expect(fetchSpy.mock.calls).toHaveLength(callsBefore); }); it("should not update state after unmount", async () => { diff --git a/tests/client/hooks/use-toast.test.ts b/tests/client/hooks/use-toast.test.ts index c3966aa2d..e0cfdb502 100644 --- a/tests/client/hooks/use-toast.test.ts +++ b/tests/client/hooks/use-toast.test.ts @@ -57,7 +57,7 @@ describe("use-toast", () => { expect(t).toHaveProperty("update"); }); - expect(result.current.toasts.length).toBe(1); + expect(result.current.toasts).toHaveLength(1); expect(result.current.toasts[0].title).toBe("Hello"); expect(result.current.toasts[0].open).toBe(true); }); @@ -87,7 +87,7 @@ describe("use-toast", () => { }); // TOAST_LIMIT = 1, so only the newest toast should be present - expect(result.current.toasts.length).toBe(1); + expect(result.current.toasts).toHaveLength(1); expect(result.current.toasts[0].title).toBe("Second"); }); @@ -138,7 +138,7 @@ describe("use-toast", () => { vi.advanceTimersByTime(4000); }); - expect(result.current.toasts.length).toBe(0); + expect(result.current.toasts).toHaveLength(0); }); it("dismiss without toastId dismisses all toasts", () => { @@ -173,7 +173,7 @@ describe("use-toast", () => { vi.advanceTimersByTime(4000); }); - expect(result.current.toasts.length).toBe(0); + expect(result.current.toasts).toHaveLength(0); }); it("onOpenChange(false) triggers dismiss", () => { @@ -255,6 +255,6 @@ describe("use-toast", () => { vi.advanceTimersByTime(4000); }); - expect(result.current.toasts.length).toBe(0); + expect(result.current.toasts).toHaveLength(0); }); }); diff --git a/tests/client/output-panel-auto-behavior.test.tsx b/tests/client/output-panel-auto-behavior.test.tsx index 8f08ad133..a91e6be15 100644 --- a/tests/client/output-panel-auto-behavior.test.tsx +++ b/tests/client/output-panel-auto-behavior.test.tsx @@ -380,7 +380,7 @@ error: missing semicolon on line 5`; // Verify the condition logic expect(successCondition.lastCompilationResult).toBe("success"); expect(successCondition.hasCompilationErrors).toBe(false); - expect(successCondition.parserMessages.length).toBe(0); + expect(successCondition.parserMessages).toHaveLength(0); // Panel should be minimized to 3% const shouldMinimize = diff --git a/tests/client/output-panel.test.tsx b/tests/client/output-panel.test.tsx index a29d85e82..e677d623c 100644 --- a/tests/client/output-panel.test.tsx +++ b/tests/client/output-panel.test.tsx @@ -103,7 +103,7 @@ describe("OutputPanel — callback reference stability", () => { return new Promise((resolve) => { requestAnimationFrame(() => { observer.disconnect(); - expect(records.length).toBe(0); + expect(records).toHaveLength(0); resolve(); }); }); diff --git a/tests/client/parser-output-pinmode.test.tsx b/tests/client/parser-output-pinmode.test.tsx index cea0065f4..de35b2ac6 100644 --- a/tests/client/parser-output-pinmode.test.tsx +++ b/tests/client/parser-output-pinmode.test.tsx @@ -5,189 +5,69 @@ import type { SourceLocation } from "@shared/source-project"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; -// Helper function to extract pinMode data (extracted from parser-output.tsx) -const PIN_MODE_LABELS: Record = { - 0: "INPUT", - 1: "OUTPUT", - 2: "INPUT_PULLUP", -}; - -function extractPinModeData( - operations: Array<{ line: number; operation: string }>, -) { - const pinModes = operations - .filter((u) => u.operation.includes("pinMode")) - .map((u) => { - const match = /pinMode:(\d+)/.exec(u.operation); - const mode = match ? Number.parseInt(match[1]) : -1; - return PIN_MODE_LABELS[mode] ?? "UNKNOWN"; - }); - - const uniqueModes = [...new Set(pinModes)]; - const hasMultipleModes = uniqueModes.length > 1; - - return { pinModes, uniqueModes, hasMultipleModes }; +// Registry records of the legacy format carry only `usedAt` operations such as +// "pinMode:1"; the pin mode is derived from them when `pinModeModes` is absent. +function legacyRecord(pin: string, operations: string[]): IOPinRecord { + return { pin, defined: true, usedAt: operations.map((operation) => ({ line: 0, operation })) }; } -describe("ParserOutput - pinMode Detection", () => { - describe("extractPinModeData", () => { - it("should parse single pinMode:0 as INPUT", () => { - const operations = [{ line: 0, operation: "pinMode:0" }]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["INPUT"]); - expect(result.uniqueModes).toEqual(["INPUT"]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should parse single pinMode:1 as OUTPUT", () => { - const operations = [{ line: 0, operation: "pinMode:1" }]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["OUTPUT"]); - expect(result.uniqueModes).toEqual(["OUTPUT"]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should parse single pinMode:2 as INPUT_PULLUP", () => { - const operations = [{ line: 0, operation: "pinMode:2" }]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["INPUT_PULLUP"]); - expect(result.uniqueModes).toEqual(["INPUT_PULLUP"]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should detect multiple different modes (conflict)", () => { - const operations = [ - { line: 0, operation: "pinMode:1" }, - { line: 0, operation: "pinMode:0" }, - ]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["OUTPUT", "INPUT"]); - expect(result.uniqueModes).toEqual(["OUTPUT", "INPUT"]); - expect(result.hasMultipleModes).toBe(true); - }); - - it("should detect same mode repeated", () => { - const operations = [ - { line: 0, operation: "pinMode:0" }, - { line: 0, operation: "pinMode:0" }, - { line: 0, operation: "pinMode:0" }, - ]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["INPUT", "INPUT", "INPUT"]); - expect(result.uniqueModes).toEqual(["INPUT"]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should handle mixed operations and extract only pinMode", () => { - const operations = [ - { line: 5, operation: "digitalWrite" }, - { line: 0, operation: "pinMode:1" }, - { line: 8, operation: "digitalRead" }, - { line: 0, operation: "pinMode:0" }, - ]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["OUTPUT", "INPUT"]); - expect(result.uniqueModes).toEqual(["OUTPUT", "INPUT"]); - expect(result.hasMultipleModes).toBe(true); - }); - - it("should return UNKNOWN for invalid mode numbers", () => { - const operations = [{ line: 0, operation: "pinMode:99" }]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["UNKNOWN"]); - expect(result.uniqueModes).toEqual(["UNKNOWN"]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should return UNKNOWN for malformed pinMode operation", () => { - const operations = [{ line: 0, operation: "pinMode" }]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["UNKNOWN"]); - expect(result.uniqueModes).toEqual(["UNKNOWN"]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should handle empty operations array", () => { - const operations: Array<{ line: number; operation: string }> = []; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual([]); - expect(result.uniqueModes).toEqual([]); - expect(result.hasMultipleModes).toBe(false); - }); - - it("should handle complex scenario with conflicts and repeats", () => { - const operations = [ - { line: 0, operation: "pinMode:1" }, - { line: 0, operation: "pinMode:1" }, - { line: 0, operation: "pinMode:0" }, - { line: 0, operation: "pinMode:1" }, - ]; - const result = extractPinModeData(operations); - - expect(result.pinModes).toEqual(["OUTPUT", "OUTPUT", "INPUT", "OUTPUT"]); - expect(result.uniqueModes).toEqual(["OUTPUT", "INPUT"]); - expect(result.hasMultipleModes).toBe(true); - }); - }); - - describe("pinMode count logic", () => { - it("should correctly count occurrences of each mode", () => { - const pinModes = ["INPUT", "INPUT", "OUTPUT", "INPUT"]; - const uniqueModes = [...new Set(pinModes)]; +function renderRegistry(...records: IOPinRecord[]) { + return render( + , + ); +} - const inputCount = pinModes.filter((m) => m === "INPUT").length; - const outputCount = pinModes.filter((m) => m === "OUTPUT").length; +describe("ParserOutput - pinMode derived from legacy usedAt operations", () => { + it.each([ + ["pinMode:0", "INPUT"], + ["pinMode:1", "OUTPUT"], + ["pinMode:2", "INPUT_PULLUP"], + ["pinMode:99", "UNKNOWN"], + ["pinMode", "UNKNOWN"], + ])("shows %s as %s without a conflict marker", (operation, label) => { + renderRegistry(legacyRecord("7", [operation])); - expect(uniqueModes).toEqual(["INPUT", "OUTPUT"]); - expect(inputCount).toBe(3); - expect(outputCount).toBe(1); - }); + expect(screen.getAllByText(label)).toHaveLength(1); + expect(screen.queryByText("!")).toBeNull(); }); -}); -describe("I/O Registry - pinMode Operation Format", () => { - it("should match pinMode:0 format", () => { - const operation = "pinMode:0"; - const match = /pinMode:(\d+)/.exec(operation); + it("shows every distinct mode and flags a conflict when modes differ", () => { + renderRegistry(legacyRecord("7", ["pinMode:1", "pinMode:0"])); - expect(match?.[1]).toBe("0"); + expect(screen.getAllByText("OUTPUT")).toHaveLength(1); + expect(screen.getAllByText("INPUT")).toHaveLength(1); + expect(screen.getAllByText("!")).toHaveLength(2); }); - it("should match pinMode:1 format", () => { - const operation = "pinMode:1"; - const match = /pinMode:(\d+)/.exec(operation); + it("shows a repeated mode once without a conflict marker", () => { + renderRegistry(legacyRecord("7", ["pinMode:0", "pinMode:0", "pinMode:0"])); - expect(match?.[1]).toBe("1"); + expect(screen.getAllByText("INPUT")).toHaveLength(1); + expect(screen.queryByText("!")).toBeNull(); }); - it("should match pinMode:2 format", () => { - const operation = "pinMode:2"; - const match = /pinMode:(\d+)/.exec(operation); + it("derives modes only from pinMode operations", () => { + renderRegistry(legacyRecord("7", ["digitalWrite", "pinMode:1", "digitalRead", "pinMode:0"])); - expect(match?.[1]).toBe("2"); + expect(screen.getAllByText("OUTPUT")).toHaveLength(1); + expect(screen.getAllByText("INPUT")).toHaveLength(1); }); - it("should not match plain pinMode without colon", () => { - const operation = "pinMode"; - const match = /pinMode:(\d+)/.exec(operation); + it("marks only PWM-capable pins with a tilde", () => { + renderRegistry( + legacyRecord("3", ["pinMode:1"]), + legacyRecord("4", ["pinMode:1"]), + legacyRecord("A0", ["pinMode:0"]), + ); - expect(match).toBeNull(); + expect(screen.getAllByText("~")).toHaveLength(1); }); - it("should not match pinMode with non-numeric mode", () => { - const operation = "pinMode:INPUT"; - const match = /pinMode:(\d+)/.exec(operation); + it("prefers pinModeModes over the legacy operations", () => { + renderRegistry({ ...legacyRecord("7", ["pinMode:1"]), pinModeModes: ["INPUT_PULLUP"] }); - expect(match).toBeNull(); + expect(screen.getAllByText("INPUT_PULLUP")).toHaveLength(1); + expect(screen.queryByText("OUTPUT")).toBeNull(); }); }); diff --git a/tests/client/serial-monitor.ui.test.tsx b/tests/client/serial-monitor.ui.test.tsx index a40f1a978..6dc6fc2fd 100644 --- a/tests/client/serial-monitor.ui.test.tsx +++ b/tests/client/serial-monitor.ui.test.tsx @@ -417,7 +417,7 @@ describe("applyBackspaceAcrossLines", () => { const lines: Array<{ text: string; incomplete: boolean }> = []; const result = applyBackspaceAcrossLines(lines, "\b\bHello", true); expect(result).toBe("\b\bHello"); - expect(lines.length).toBe(0); + expect(lines).toHaveLength(0); }); it("processes backspaces in middle of text via processAnsiCodes", () => { diff --git a/tests/client/utils/serial-character-renderer.test.ts b/tests/client/utils/serial-character-renderer.test.ts index 8d710e322..be482a3e0 100644 --- a/tests/client/utils/serial-character-renderer.test.ts +++ b/tests/client/utils/serial-character-renderer.test.ts @@ -232,7 +232,7 @@ describe("SerialCharacterRenderer - Unit Tests", () => { vi.runAllTimers(); // Should render all due to high baudrate batching - expect(capturedOutput.join("").length).toBe(10000); + expect(capturedOutput.join("")).toHaveLength(10000); }); it("should not crash when cleared multiple times", () => { diff --git a/tests/client/websocket-manager.test.ts b/tests/client/websocket-manager.test.ts index d3a670b72..8dd0266cd 100644 --- a/tests/client/websocket-manager.test.ts +++ b/tests/client/websocket-manager.test.ts @@ -125,7 +125,7 @@ describe("WebSocketManager", () => { it("creates a WebSocket on connect()", () => { const mgr = getWebSocketManager(); mgr.connect(); - expect(wsInstances.length).toBe(1); + expect(wsInstances).toHaveLength(1); expect(wsInstances[0].url).toBe("ws://localhost:3000/ws"); expect(mgr.getState()).toBe("connecting"); }); @@ -135,7 +135,7 @@ describe("WebSocketManager", () => { mgr.connect(); wsInstances[0]._simulateOpen(); mgr.connect(); // should be no-op - expect(wsInstances.length).toBe(1); + expect(wsInstances).toHaveLength(1); }); it("handles successful connection", () => { @@ -163,11 +163,11 @@ describe("WebSocketManager", () => { expect(result).toBe(true); // Not sent yet (buffered) - expect(wsInstances[0].sentMessages.length).toBe(0); + expect(wsInstances[0].sentMessages).toHaveLength(0); // Advance past buffer flush interval (30ms) vi.advanceTimersByTime(35); - expect(wsInstances[0].sentMessages.length).toBe(1); + expect(wsInstances[0].sentMessages).toHaveLength(1); expect(JSON.parse(wsInstances[0].sentMessages[0])).toEqual(msg); }); @@ -182,7 +182,7 @@ describe("WebSocketManager", () => { } // Should be flushed immediately - expect(wsInstances[0].sentMessages.length).toBe(100); + expect(wsInstances[0].sentMessages).toHaveLength(100); }); it("returns false when WebSocket is not open", () => { @@ -202,7 +202,7 @@ describe("WebSocketManager", () => { const msg = { type: "serial_input" as const, data: "urgent" }; const result = mgr.sendImmediate(msg); expect(result).toBe(true); - expect(wsInstances[0].sentMessages.length).toBe(1); + expect(wsInstances[0].sentMessages).toHaveLength(1); }); it("sendImmediate returns false when not connected", () => { @@ -221,7 +221,7 @@ describe("WebSocketManager", () => { wsInstances[0]._simulateOpen(); wsInstances[0]._simulateMessage({ type: "serial_output", data: "hello" }); - expect(received.length).toBe(1); + expect(received).toHaveLength(1); expect((received[0] as any).type).toBe("serial_output"); }); @@ -292,7 +292,7 @@ describe("WebSocketManager", () => { // Advance timers to trigger reconnect vi.advanceTimersByTime(2000); - expect(wsInstances.length).toBe(2); // new WS created + expect(wsInstances).toHaveLength(2); // new WS created }); it("does not reconnect after explicit disconnect", () => { @@ -304,7 +304,7 @@ describe("WebSocketManager", () => { // Close should not trigger reconnect expect(mgr.getState()).toBe("disconnected"); vi.advanceTimersByTime(60000); - expect(wsInstances.length).toBe(1); // no new WS + expect(wsInstances).toHaveLength(1); // no new WS }); it("retries indefinitely – does not give up after many reconnect failures", () => { @@ -361,7 +361,7 @@ describe("WebSocketManager", () => { mgr.disconnect(); vi.advanceTimersByTime(50); // No error thrown, messages dropped - expect(wsInstances[0].sentMessages.length).toBe(0); + expect(wsInstances[0].sentMessages).toHaveLength(0); }); // ---- Error in event handler ---- @@ -385,7 +385,7 @@ describe("WebSocketManager", () => { mgr.send({ type: "serial_input" as const, data: "c" }); vi.advanceTimersByTime(35); - expect(wsInstances[0].sentMessages.length).toBe(3); + expect(wsInstances[0].sentMessages).toHaveLength(3); }); // ---- sendImmediate error handling ---- diff --git a/tests/core/sandbox-stress.test.ts b/tests/core/sandbox-stress.test.ts index 9c37899cf..efb0bc15d 100644 --- a/tests/core/sandbox-stress.test.ts +++ b/tests/core/sandbox-stress.test.ts @@ -365,7 +365,7 @@ void loop() { // State errors acceptable in local mode due to timing differences if (dockerAvailable) { - expect(stateErrors.length).toBe(0); // No state transition errors + expect(stateErrors).toHaveLength(0); // No state transition errors expect(successfulPauses).toBeGreaterThan(15); // Most pauses should succeed expect(successfulResumes).toBeGreaterThan(15); // Most resumes should succeed expect(tickCount).toBeGreaterThan(10); // Some execution happened @@ -594,7 +594,7 @@ void loop() { return !e.endsWith(".cleanup") && existsSync(join(fullPath, "sketch.ino")); }); - expect(remainingSketchDirs.length).toBe(0); // All sketch dirs should be cleaned/renamed + expect(remainingSketchDirs).toHaveLength(0); // All sketch dirs should be cleaned/renamed }, 120000); // Needs time for 2 real Arduino compilations in parallel it("should cleanup temp directory after rapid start/stop cycles", async () => { diff --git a/tests/server/cache-optimization.test.ts b/tests/server/cache-optimization.test.ts index e5ec99572..adf87fd52 100644 --- a/tests/server/cache-optimization.test.ts +++ b/tests/server/cache-optimization.test.ts @@ -203,7 +203,7 @@ void loop() { delay(100); } expect(resultDiff.cached).toBe(false); // Different code → miss // Validate cache hits were returned consistently - expect(cacheHitTimes.length).toBe(5); + expect(cacheHitTimes).toHaveLength(5); }, 10000); it("should cache properly with identical headers", async () => { diff --git a/tests/server/cli-label-isolation.test.ts b/tests/server/cli-label-isolation.test.ts index 822f91c38..de30d9c44 100644 --- a/tests/server/cli-label-isolation.test.ts +++ b/tests/server/cli-label-isolation.test.ts @@ -145,8 +145,8 @@ describe("CLI Label Session Isolation", () => { const session1Hashes = sessionCompilations.get("session-1") || []; const session2Hashes = sessionCompilations.get("session-2") || []; - expect(session1Hashes.length).toBe(1); - expect(session2Hashes.length).toBe(1); + expect(session1Hashes).toHaveLength(1); + expect(session2Hashes).toHaveLength(1); expect(session1Hashes[0]).not.toBe(session2Hashes[0]); expect(result1.cached).toBe(false); diff --git a/tests/server/control-characters.test.ts b/tests/server/control-characters.test.ts index 73c8cf0cf..bcd9b7d95 100644 --- a/tests/server/control-characters.test.ts +++ b/tests/server/control-characters.test.ts @@ -132,7 +132,7 @@ describe("Control Characters Examples and Handling", () => { // After first chunk, X is still there expect(snapshots[0]).toBe("X"); // Simulate "waiting" - snapshots don't change without new chunks - expect(snapshots.length).toBe(1); + expect(snapshots).toHaveLength(1); }); it("Phase 3: backspace+Y replaces X with Y at same position", () => { diff --git a/tests/server/load-suite.test.ts b/tests/server/load-suite.test.ts index e73f21708..e9b9dab55 100644 --- a/tests/server/load-suite.test.ts +++ b/tests/server/load-suite.test.ts @@ -431,7 +431,7 @@ function createLoadTestSuite( testResults.push(stats); } - expect(testSizes.length).toBe(5); + expect(testSizes).toHaveLength(5); }, 240000, ); diff --git a/tests/server/routes/serial-output-batching.test.ts b/tests/server/routes/serial-output-batching.test.ts index 52af9d1c1..b08a47451 100644 --- a/tests/server/routes/serial-output-batching.test.ts +++ b/tests/server/routes/serial-output-batching.test.ts @@ -61,11 +61,11 @@ describe("serial_output WebSocket Batching (WsOutputBuffer)", () => { send("World", true); send("Test", true); - expect(messages.length).toBe(0); + expect(messages).toHaveLength(0); vi.advanceTimersByTime(50); // All lines have newlines after them (newline AFTER every complete line) - expect(messages.length).toBe(1); + expect(messages).toHaveLength(1); expect(messages[0].type).toBe("serial_output"); expect(messages[0].data).toBe("Hello\nWorld\nTest\n"); expect(messages[0].isComplete).toBe(true); @@ -78,14 +78,14 @@ describe("serial_output WebSocket Batching (WsOutputBuffer)", () => { send("Line2", true); vi.advanceTimersByTime(50); - expect(messages.length).toBe(1); + expect(messages).toHaveLength(1); expect(messages[0].data).toBe("Line1\nLine2\n"); expect(messages[0].isComplete).toBe(true); send("Line3", true); vi.advanceTimersByTime(50); - expect(messages.length).toBe(2); + expect(messages).toHaveLength(2); expect(messages[1].data).toBe("Line3\n"); expect(messages[1].isComplete).toBe(true); }); @@ -100,7 +100,7 @@ describe("serial_output WebSocket Batching (WsOutputBuffer)", () => { vi.advanceTimersByTime(50); // Incomplete lines don't get newlines, complete ones do - expect(messages.length).toBe(1); + expect(messages).toHaveLength(1); expect(messages[0].data).toBe("Partial Output\n"); expect(messages[0].isComplete).toBe(true); }); @@ -113,7 +113,7 @@ describe("serial_output WebSocket Batching (WsOutputBuffer)", () => { vi.advanceTimersByTime(50); // Incomplete line has no newline - expect(messages.length).toBe(1); + expect(messages).toHaveLength(1); expect(messages[0].data).toBe("Still typing"); expect(messages[0].isComplete).toBe(false); }); @@ -127,7 +127,7 @@ describe("serial_output WebSocket Batching (WsOutputBuffer)", () => { vi.advanceTimersByTime(50); - expect(messages.length).toBe(1); + expect(messages).toHaveLength(1); let expectedData = ""; for (let i = 0; i < 10; i++) { expectedData += `Line${i}\n`; diff --git a/tests/server/services/arduino-compiler-parser-messages.test.ts b/tests/server/services/arduino-compiler-parser-messages.test.ts index de80a4129..36855ca62 100644 --- a/tests/server/services/arduino-compiler-parser-messages.test.ts +++ b/tests/server/services/arduino-compiler-parser-messages.test.ts @@ -188,7 +188,7 @@ describe("ArduinoCompiler - Parser Messages Tests", () => { const serialMessages = (result.parserMessages ?? []).filter( (m) => m.category === "serial", ); - expect(serialMessages.length).toBe(0); + expect(serialMessages).toHaveLength(0); // Output is clean (only compiler info) expect(result.output).toContain("Board: Arduino UNO"); @@ -273,7 +273,7 @@ describe("ArduinoCompiler - Parser Messages Tests", () => { const serialMessages = (result.parserMessages ?? []).filter( (m) => m.category === "serial", ); - expect(serialMessages.length).toBe(0); + expect(serialMessages).toHaveLength(0); // Output is clean expect(result.output).toContain("Board: Arduino UNO"); diff --git a/tests/server/services/arduino-output-parser.test.ts b/tests/server/services/arduino-output-parser.test.ts index 3a05c7ebd..d318badd1 100644 --- a/tests/server/services/arduino-output-parser.test.ts +++ b/tests/server/services/arduino-output-parser.test.ts @@ -194,23 +194,13 @@ describe("ArduinoOutputParser", () => { }); describe("protocol fragment handling", () => { - it("should ignore standalone closing brackets ']]'", () => { - const result = parser.parseStderrLine("]]", processStartTime); - expect(result.type).toBe("ignored"); - }); - - it("should ignore standalone opening brackets '[['", () => { - const result = parser.parseStderrLine("[[", processStartTime); - expect(result.type).toBe("ignored"); - }); - - it("should ignore partial protocol header without closing", () => { - const result = parser.parseStderrLine("[[SERIAL_EVENT:", processStartTime); - expect(result.type).toBe("ignored"); - }); - - it("should ignore short base64 tail with closing brackets", () => { - const result = parser.parseStderrLine("SGVsbG8=]]", processStartTime); + it.each([ + { title: "standalone closing brackets ']]'", line: "]]" }, + { title: "standalone opening brackets '[['", line: "[[" }, + { title: "partial protocol header without closing", line: "[[SERIAL_EVENT:" }, + { title: "short base64 tail with closing brackets", line: "SGVsbG8=]]" }, + ])("should ignore $title", ({ line }) => { + const result = parser.parseStderrLine(line, processStartTime); expect(result.type).toBe("ignored"); }); diff --git a/tests/server/services/parser-messages-integration.test.ts b/tests/server/services/parser-messages-integration.test.ts index 13d059948..168718764 100644 --- a/tests/server/services/parser-messages-integration.test.ts +++ b/tests/server/services/parser-messages-integration.test.ts @@ -140,7 +140,7 @@ describe("Parser Messages Integration", () => { // Should have NO Serial messages const messages = result.parserMessages as ParserMessage[]; const serialMessages = messages.filter((m) => m.category === "serial"); - expect(serialMessages.length).toBe(0); + expect(serialMessages).toHaveLength(0); // Should NOT be in output expect(result.output).not.toContain("Serial.begin"); @@ -208,7 +208,7 @@ describe("Parser Messages Integration", () => { // Should have NO Serial warnings const messages = result.parserMessages as ParserMessage[]; const serialMessages = messages.filter((m) => m.category === "serial"); - expect(serialMessages.length).toBe(0); + expect(serialMessages).toHaveLength(0); // Should NOT be in output expect(result.output).not.toContain("Serial.begin"); diff --git a/tests/server/services/registry-manager-telemetry.test.ts b/tests/server/services/registry-manager-telemetry.test.ts index eb1272e27..416423e08 100644 --- a/tests/server/services/registry-manager-telemetry.test.ts +++ b/tests/server/services/registry-manager-telemetry.test.ts @@ -283,7 +283,7 @@ describe("RegistryManager - Telemetry Metrics", () => { vi.advanceTimersByTime(3000); - expect(callbacks.length).toBe(0); + expect(callbacks).toHaveLength(0); localManager.destroy(); }); @@ -306,7 +306,7 @@ describe("RegistryManager - Telemetry Metrics", () => { localManager.pauseTelemetry(); vi.advanceTimersByTime(2000); - expect(callbacks.length).toBe(beforePauseCount); + expect(callbacks).toHaveLength(beforePauseCount); localManager.destroy(); }); diff --git a/tests/server/services/serial-backpressure.test.ts b/tests/server/services/serial-backpressure.test.ts index dd0a005a6..fc0336776 100644 --- a/tests/server/services/serial-backpressure.test.ts +++ b/tests/server/services/serial-backpressure.test.ts @@ -101,7 +101,7 @@ void loop() { const lineMatches = output.match(/Line \d{6}/g) || []; log(`[T-BP-01] Lines received: ${lineMatches.length}`); - expect(lineMatches.length).toBe(10); + expect(lineMatches).toHaveLength(10); // Extract timing deltas const deltas: number[] = []; diff --git a/tests/server/services/serial-output-batcher.test.ts b/tests/server/services/serial-output-batcher.test.ts index f86c7a7c7..8a4f30e54 100644 --- a/tests/server/services/serial-output-batcher.test.ts +++ b/tests/server/services/serial-output-batcher.test.ts @@ -82,7 +82,7 @@ describe("SerialOutputBatcher", () => { expect(chunks).toHaveLength(1); expect(chunks[0]).toBe(largeData); - expect(chunks[0].length).toBe(2001); + expect(chunks[0]).toHaveLength(2001); }); it("T04: Pause stoppt Timer, Resume startet Timer neu", () => { @@ -615,7 +615,7 @@ describe("SerialOutputBatcher", () => { // Collect all chunks and verify total output const totalOutput = chunks.join(""); expect(totalOutput).toBe(message); - expect(totalOutput.length).toBe(13); + expect(totalOutput).toHaveLength(13); }); it("T25: Multiple consecutive messages at low baudrate should all arrive", () => { diff --git a/tests/server/services/serial-stop-pause-resume.test.ts b/tests/server/services/serial-stop-pause-resume.test.ts index 2c167a1c3..a20452d12 100644 --- a/tests/server/services/serial-stop-pause-resume.test.ts +++ b/tests/server/services/serial-stop-pause-resume.test.ts @@ -55,7 +55,7 @@ describe('Serial Output Stop/Pause/Resume', () => { // After destroy, no more data should arrive even after many ticks vi.advanceTimersByTime(5000); - expect(chunks.length).toBe(chunksBeforeStop); + expect(chunks).toHaveLength(chunksBeforeStop); }); it('T-SPR-02: destroy() completes in <1ms (no flush delay)', () => { @@ -79,7 +79,7 @@ describe('Serial Output Stop/Pause/Resume', () => { vi.advanceTimersByTime(10000); const chunksAfter = chunks.length; vi.advanceTimersByTime(10000); - expect(chunks.length).toBe(chunksAfter); + expect(chunks).toHaveLength(chunksAfter); }); it('T-SPR-03: stop() flushes remaining data (natural completion)', () => { @@ -111,7 +111,7 @@ describe('Serial Output Stop/Pause/Resume', () => { // Enqueue after destroy should not cause any output batcher.enqueue('Should not appear\n'); vi.advanceTimersByTime(1000); - expect(chunks.length).toBe(0); + expect(chunks).toHaveLength(0); }); }); @@ -140,7 +140,7 @@ describe('Serial Output Stop/Pause/Resume', () => { // No more output after pause, even after many ticks vi.advanceTimersByTime(5000); - expect(chunks.length).toBe(chunksBeforePause); + expect(chunks).toHaveLength(chunksBeforePause); // Buffer should still hold remaining data const telemetry = batcher.getTelemetryAndReset(); @@ -218,7 +218,7 @@ describe('Serial Output Stop/Pause/Resume', () => { // No output during pause vi.advanceTimersByTime(500); - expect(chunks.length).toBe(0); + expect(chunks).toHaveLength(0); // Resume batcher.resume(); @@ -244,7 +244,7 @@ describe('Serial Output Stop/Pause/Resume', () => { batcher.enqueue('During pause\n'); vi.advanceTimersByTime(500); - expect(chunks.length).toBe(0); // nothing sent during pause + expect(chunks).toHaveLength(0); // nothing sent during pause // Resume batcher.resume(); diff --git a/tests/server/services/unified-gatekeeper-performance.test.ts b/tests/server/services/unified-gatekeeper-performance.test.ts index 65e457b3f..3a968eb91 100644 --- a/tests/server/services/unified-gatekeeper-performance.test.ts +++ b/tests/server/services/unified-gatekeeper-performance.test.ts @@ -45,7 +45,7 @@ describe("UnifiedGatekeeper - Performance Benchmarks", () => { // With event-driven approach, this should complete in ~100-500ms // With polling (25ms intervals), this would take much longer expect(elapsedMs).toBeLessThan(1000); - expect(readers.length).toBe(numWaiters); + expect(readers).toHaveLength(numWaiters); // Cleanup for (const reader of readers) { @@ -90,7 +90,7 @@ describe("UnifiedGatekeeper - Performance Benchmarks", () => { await write1(); const readers1 = await Promise.all(waitersKey1); - expect(readers1.length).toBe(50); + expect(readers1).toHaveLength(50); // key2 waiters should still be waiting let key2Resolved = false; @@ -104,7 +104,7 @@ describe("UnifiedGatekeeper - Performance Benchmarks", () => { // Release key2 await write2(); const readers2 = await Promise.all(waitersKey2); - expect(readers2.length).toBe(50); + expect(readers2).toHaveLength(50); // Cleanup for (const r of readers1) await r(); @@ -228,7 +228,7 @@ describe("UnifiedGatekeeper - Performance Benchmarks", () => { // Event-driven notification should be nearly instant // Polling would require multiple 25ms cycles expect(notificationTime).toBeLessThan(1000); - expect(readers.length).toBe(500); + expect(readers).toHaveLength(500); // Cleanup for (const r of readers) await r(); diff --git a/tests/server/storage.test.ts b/tests/server/storage.test.ts index 018901f5c..07572af95 100644 --- a/tests/server/storage.test.ts +++ b/tests/server/storage.test.ts @@ -11,7 +11,7 @@ describe("DefaultSketchStore", () => { describe("constructor", () => { it("should initialize with a default sketch", async () => { const sketches = await storage.getAllSketches(); - expect(sketches.length).toBe(1); + expect(sketches).toHaveLength(1); expect(sketches[0].name).toBe("sketch.ino"); expect(sketches[0].content).toContain("void setup()"); expect(sketches[0].content).toContain("void loop()"); diff --git a/tests/server/telemetry-integration.test.ts b/tests/server/telemetry-integration.test.ts index 40a30db44..78113f128 100644 --- a/tests/server/telemetry-integration.test.ts +++ b/tests/server/telemetry-integration.test.ts @@ -327,7 +327,7 @@ describe("Telemetry - E2E Integration Pipeline", () => { } const history = client.getHistory(); - expect(history.length).toBe(3); + expect(history).toHaveLength(3); // Verify timestamps are increasing for (let i = 1; i < history.length; i++) { @@ -476,7 +476,7 @@ describe("Telemetry - E2E Integration Pipeline", () => { // Verify history captured all bursts const history = client.getHistory(); - expect(history.length).toBe(3); + expect(history).toHaveLength(3); expect(history[0].pinChangesPerSecond).toBe(30); expect(history[0].serialOutputPerSecond).toBe(20); expect(history[1].pinChangesPerSecond).toBe(2); diff --git a/tests/server/websocket-telemetry-format.test.ts b/tests/server/websocket-telemetry-format.test.ts index a3bc9a585..641111234 100644 --- a/tests/server/websocket-telemetry-format.test.ts +++ b/tests/server/websocket-telemetry-format.test.ts @@ -76,7 +76,7 @@ describe('WebSocket Telemetry Message Format', () => { pushTelemetry(incomingMessage.metrics); // Verify frontend state would update - expect(telemetryHistory.length).toBe(1); + expect(telemetryHistory).toHaveLength(1); expect(lastHeartbeatAt).toBeDefined(); expect(lastHeartbeatAt && lastHeartbeatAt > 0).toBe(true); diff --git a/tests/shared/logger.test.ts b/tests/shared/logger.test.ts index a7e6c620d..3e3501e0e 100644 --- a/tests/shared/logger.test.ts +++ b/tests/shared/logger.test.ts @@ -104,97 +104,63 @@ describe("Logger", () => { }); describe("Logger - Sanitization (sanitize() coverage)", () => { - it("should redact credential key-value pairs in log messages", () => { + function logSanitized(input: string): void { setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("login failed: password=redact-me"); // NOSONAR S2068 - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[REDACTED]"), - ); - expect(logSpy).not.toHaveBeenCalledWith( - expect.stringContaining("redact-me"), - ); - }); - - it("should redact abbreviated credential key-value pairs in log messages", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("login: pwd=redact-me"); // NOSONAR S2068 - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[REDACTED]"), - ); - expect(logSpy).not.toHaveBeenCalledWith( - expect.stringContaining("redact-me"), - ); - }); - - it("should redact email addresses in log messages", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("User logged in: user@example.com"); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[EMAIL_REDACTED]"), - ); - expect(logSpy).not.toHaveBeenCalledWith( - expect.stringContaining("user@example.com"), - ); - }); - - it("should redact bearer tokens in log messages", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc"); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("bearer [REDACTED_TOKEN]"), - ); - }); - - it("should not redact normal log messages", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("Simulation started successfully"); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("Simulation started successfully"), - ); - }); - - it("should redact JSON sensitive fields (password/token/secret in JSON objects)", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info('Request body: {"password": "hunter2", "user": "admin"}'); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[REDACTED]"), - ); - expect(logSpy).not.toHaveBeenCalledWith( - expect.stringContaining("hunter2"), - ); - }); - - it("should redact SSN patterns (XXX-XX-XXXX)", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("Customer SSN: 123-45-6789"); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[SSN_REDACTED]"), - ); - }); - - it("should redact phone numbers", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("Call us at 555-123-4567"); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[PHONE_REDACTED]"), - ); - }); - - it("should redact credit card numbers", () => { - setLogLevel("INFO"); - const sanitizeLogger = new Logger("SanitizeTest"); - sanitizeLogger.info("Card: 1234 5678 9012 3456"); - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining("[CARD_REDACTED]"), - ); + new Logger("SanitizeTest").info(input); + } + + // The plaintext secret must be replaced by the marker and must not be logged. + it.each([ + { + title: "should redact credential key-value pairs in log messages", + input: "login failed: password=redact-me", // NOSONAR S2068 + marker: "[REDACTED]", + leaked: "redact-me", + }, + { + title: "should redact abbreviated credential key-value pairs in log messages", + input: "login: pwd=redact-me", // NOSONAR S2068 + marker: "[REDACTED]", + leaked: "redact-me", + }, + { + title: "should redact email addresses in log messages", + input: "User logged in: user@example.com", + marker: "[EMAIL_REDACTED]", + leaked: "user@example.com", + }, + { + title: "should redact JSON sensitive fields (password/token/secret in JSON objects)", + input: 'Request body: {"password": "hunter2", "user": "admin"}', + marker: "[REDACTED]", + leaked: "hunter2", + }, + ])("$title", ({ input, marker, leaked }) => { + logSanitized(input); + + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(marker)); + expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining(leaked)); + }); + + // Patterns whose replacement marker is the whole contract; ordinary text passes through. + it.each([ + { + title: "should redact bearer tokens in log messages", + input: "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc", + marker: "bearer [REDACTED_TOKEN]", + }, + { + title: "should not redact normal log messages", + input: "Simulation started successfully", + marker: "Simulation started successfully", + }, + { title: "should redact SSN patterns (XXX-XX-XXXX)", input: "Customer SSN: 123-45-6789", marker: "[SSN_REDACTED]" }, + { title: "should redact phone numbers", input: "Call us at 555-123-4567", marker: "[PHONE_REDACTED]" }, + { title: "should redact credit card numbers", input: "Card: 1234 5678 9012 3456", marker: "[CARD_REDACTED]" }, + ])("$title", ({ input, marker }) => { + logSanitized(input); + + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(marker)); }); }); diff --git a/tests/shared/reserved-names-validator.test.ts b/tests/shared/reserved-names-validator.test.ts index ee737d5f4..2e7b3d26a 100644 --- a/tests/shared/reserved-names-validator.test.ts +++ b/tests/shared/reserved-names-validator.test.ts @@ -99,7 +99,7 @@ void setup() { void loop() { }`; const messages = reservedNamesValidator.validateReservedNames(code); - expect(messages.length).toBe(0); + expect(messages).toHaveLength(0); }); it("should allow setup and loop function names", () => { @@ -266,7 +266,7 @@ void loop() { it("should handle empty code", () => { const code = ""; const messages = reservedNamesValidator.validateReservedNames(code); - expect(messages.length).toBe(0); + expect(messages).toHaveLength(0); }); it("should handle code with only comments", () => { @@ -275,7 +275,7 @@ void loop() { /* and another pause */ `; const messages = reservedNamesValidator.validateReservedNames(code); - expect(messages.length).toBe(0); + expect(messages).toHaveLength(0); }); it("should handle multiline block comments", () => { @@ -292,7 +292,7 @@ void setup() { void loop() { }`; const messages = reservedNamesValidator.validateReservedNames(code); - expect(messages.length).toBe(0); + expect(messages).toHaveLength(0); }); it("should handle unterminated block comment gracefully", () => { @@ -305,7 +305,7 @@ void loop() { }`; const messages = reservedNamesValidator.validateReservedNames(code); // Everything after /* is treated as comment, so no reserved name detected - expect(messages.length).toBe(0); + expect(messages).toHaveLength(0); }); }); }); diff --git a/tests/shared/uno-pwm-pins.test.ts b/tests/shared/uno-pwm-pins.test.ts new file mode 100644 index 000000000..cb492e723 --- /dev/null +++ b/tests/shared/uno-pwm-pins.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { UNO_PWM_PINS, isUnoPwmPin } from "@shared/utils/arduino-utils"; + +describe("Arduino UNO PWM pins", () => { + it("lists the PWM-capable pins in board order", () => { + expect([...UNO_PWM_PINS]).toEqual([3, 5, 6, 9, 10, 11]); + }); + + it.each([3, 5, 6, 9, 10, 11])("treats pin %i as PWM-capable", (pin) => { + expect(isUnoPwmPin(pin)).toBe(true); + }); + + it.each([0, 1, 2, 4, 7, 8, 12, 13, 14, 19, 20, -1, 3.5])("treats pin %s as not PWM-capable", (pin) => { + expect(isUnoPwmPin(pin)).toBe(false); + }); + + it("agrees with the list for every pin number the UNO exposes", () => { + for (let pin = 0; pin <= 19; pin += 1) { + expect(isUnoPwmPin(pin), `pin ${pin}`).toBe(UNO_PWM_PINS.includes(pin)); + } + }); +});