Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/components/features/parser-output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
? <span className="text-yellow-400">~</span>
: null;
}
Expand Down
6 changes: 3 additions & 3 deletions client/src/hooks/usePinPollingEngine.ts
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand Down Expand Up @@ -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)`;
}
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions shared/parsers/hardware-compatibility-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StaticIOOperation>([
"digitalRead",
"digitalWrite",
Expand Down Expand Up @@ -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),
}));
Expand Down
13 changes: 13 additions & 0 deletions shared/utils/arduino-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,16 @@ const PIN_MODE_NAMES: Record<number, PinMode> = {
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<number> = new Set(UNO_PWM_PINS);

export function isUnoPwmPin(pin: number): boolean {
return UNO_PWM_PIN_SET.has(pin);
}
10 changes: 4 additions & 6 deletions tests/client/arduino-simulator-codechange.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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
Expand Down
100 changes: 22 additions & 78 deletions tests/client/font-scale-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
2 changes: 1 addition & 1 deletion tests/client/hooks/use-backend-health.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
10 changes: 5 additions & 5 deletions tests/client/hooks/use-toast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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");
});

Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -255,6 +255,6 @@ describe("use-toast", () => {
vi.advanceTimersByTime(4000);
});

expect(result.current.toasts.length).toBe(0);
expect(result.current.toasts).toHaveLength(0);
});
});
2 changes: 1 addition & 1 deletion tests/client/output-panel-auto-behavior.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
2 changes: 1 addition & 1 deletion tests/client/output-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe("OutputPanel — callback reference stability", () => {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
observer.disconnect();
expect(records.length).toBe(0);
expect(records).toHaveLength(0);
resolve();
});
});
Expand Down
Loading
Loading