fix: Resolve TypeScript strict mode errors in hook tests

- Add type assertions to useErrorPopoverState test rerender calls
- Use non-null assertions for callback invocations in useBluetoothDeviceListener tests
- Fix type inference issues with union types (number | undefined, string | null)
- All 91 tests passing with proper TypeScript compliance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-12-27 12:48:49 +01:00
parent f2b01c59e1
commit eff8e15179
6 changed files with 132 additions and 80 deletions

View file

@ -30,13 +30,13 @@ describe("useErrorPopoverState", () => {
pyodideError: null, pyodideError: null,
hasError, hasError,
}), }),
{ initialProps: { machineError: undefined } }, { initialProps: { machineError: undefined as number | undefined } },
); );
expect(result.current.isOpen).toBe(false); expect(result.current.isOpen).toBe(false);
// Error appears // Error appears
rerender({ machineError: 1 }); rerender({ machineError: 1 as number | undefined });
expect(result.current.isOpen).toBe(true); expect(result.current.isOpen).toBe(true);
}); });
@ -49,12 +49,12 @@ describe("useErrorPopoverState", () => {
pyodideError: null, pyodideError: null,
hasError, hasError,
}), }),
{ initialProps: { machineErrorMessage: null } }, { initialProps: { machineErrorMessage: null as string | null } },
); );
expect(result.current.isOpen).toBe(false); expect(result.current.isOpen).toBe(false);
rerender({ machineErrorMessage: "Error occurred" }); rerender({ machineErrorMessage: "Error occurred" as string | null });
expect(result.current.isOpen).toBe(true); expect(result.current.isOpen).toBe(true);
}); });
@ -67,12 +67,12 @@ describe("useErrorPopoverState", () => {
pyodideError, pyodideError,
hasError, hasError,
}), }),
{ initialProps: { pyodideError: null } }, { initialProps: { pyodideError: null as string | null } },
); );
expect(result.current.isOpen).toBe(false); expect(result.current.isOpen).toBe(false);
rerender({ pyodideError: "Pyodide error" }); rerender({ pyodideError: "Pyodide error" as string | null });
expect(result.current.isOpen).toBe(true); expect(result.current.isOpen).toBe(true);
}); });
@ -98,7 +98,7 @@ describe("useErrorPopoverState", () => {
}); });
it("should track manual dismissal", async () => { it("should track manual dismissal", async () => {
const { result, rerender } = renderHook( const { result } = renderHook(
({ machineError }) => ({ machineError }) =>
useErrorPopoverState({ useErrorPopoverState({
machineError, machineError,
@ -218,7 +218,15 @@ describe("useErrorPopoverState", () => {
it("should handle multiple error sources", () => { it("should handle multiple error sources", () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(
({ machineError, machineErrorMessage, pyodideError }) => ({
machineError,
machineErrorMessage,
pyodideError,
}: {
machineError: number | undefined;
machineErrorMessage: string | null;
pyodideError: string | null;
}) =>
useErrorPopoverState({ useErrorPopoverState({
machineError, machineError,
machineErrorMessage, machineErrorMessage,
@ -227,9 +235,9 @@ describe("useErrorPopoverState", () => {
}), }),
{ {
initialProps: { initialProps: {
machineError: undefined, machineError: undefined as number | undefined,
machineErrorMessage: null, machineErrorMessage: null as string | null,
pyodideError: null, pyodideError: null as string | null,
}, },
}, },
); );
@ -238,34 +246,34 @@ describe("useErrorPopoverState", () => {
// Machine error appears // Machine error appears
rerender({ rerender({
machineError: 1, machineError: 1 as number | undefined,
machineErrorMessage: null, machineErrorMessage: null as string | null,
pyodideError: null, pyodideError: null as string | null,
}); });
expect(result.current.isOpen).toBe(true); expect(result.current.isOpen).toBe(true);
// Additional pyodide error // Additional pyodide error
rerender({ rerender({
machineError: 1, machineError: 1 as number | undefined,
machineErrorMessage: null, machineErrorMessage: null as string | null,
pyodideError: "Pyodide error", pyodideError: "Pyodide error" as string | null,
}); });
expect(result.current.isOpen).toBe(true); expect(result.current.isOpen).toBe(true);
// Clear machine error but pyodide error remains // Clear machine error but pyodide error remains
rerender({ rerender({
machineError: 0, machineError: 0 as number | undefined,
machineErrorMessage: null, machineErrorMessage: null as string | null,
pyodideError: "Pyodide error", pyodideError: "Pyodide error" as string | null,
}); });
// Should stay open because pyodide error still exists // Should stay open because pyodide error still exists
expect(result.current.isOpen).toBe(true); expect(result.current.isOpen).toBe(true);
// Clear all errors // Clear all errors
rerender({ rerender({
machineError: 0, machineError: 0 as number | undefined,
machineErrorMessage: null, machineErrorMessage: null as string | null,
pyodideError: null, pyodideError: null as string | null,
}); });
expect(result.current.isOpen).toBe(false); expect(result.current.isOpen).toBe(false);
}); });

View file

@ -16,7 +16,7 @@ describe("useMachinePolling", () => {
const { result } = renderHook(() => const { result } = renderHook(() =>
useMachinePolling({ useMachinePolling({
machineStatus: MachineStatus.READY, machineStatus: MachineStatus.IDLE,
patternInfo: null, patternInfo: null,
onStatusRefresh, onStatusRefresh,
onProgressRefresh, onProgressRefresh,
@ -45,7 +45,7 @@ describe("useMachinePolling", () => {
const { result } = renderHook(() => const { result } = renderHook(() =>
useMachinePolling({ useMachinePolling({
machineStatus: MachineStatus.READY, machineStatus: MachineStatus.IDLE,
patternInfo: null, patternInfo: null,
onStatusRefresh, onStatusRefresh,
onProgressRefresh, onProgressRefresh,
@ -139,7 +139,7 @@ describe("useMachinePolling", () => {
const mocks2 = createMocks(); const mocks2 = createMocks();
const { result: result2 } = renderHook(() => const { result: result2 } = renderHook(() =>
useMachinePolling({ useMachinePolling({
machineStatus: MachineStatus.READY, machineStatus: MachineStatus.IDLE,
patternInfo: null, patternInfo: null,
...mocks2, ...mocks2,
shouldCheckResumablePattern: () => false, shouldCheckResumablePattern: () => false,

View file

@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor } from "@testing-library/react"; import { renderHook, waitFor, act } from "@testing-library/react";
import { useBluetoothDeviceListener } from "./useBluetoothDeviceListener"; import { useBluetoothDeviceListener } from "./useBluetoothDeviceListener";
import type { BluetoothDevice } from "../../types/electron"; import type { BluetoothDevice } from "../../types/electron";
@ -23,7 +23,11 @@ describe("useBluetoothDeviceListener", () => {
it("should return isSupported=true when Electron API is available", () => { it("should return isSupported=true when Electron API is available", () => {
// Mock Electron API // Mock Electron API
(window as { electronAPI: { onBluetoothDeviceList: () => void } }).electronAPI = { (
window as unknown as {
electronAPI: { onBluetoothDeviceList: () => void };
}
).electronAPI = {
onBluetoothDeviceList: vi.fn(), onBluetoothDeviceList: vi.fn(),
}; };
@ -34,7 +38,11 @@ describe("useBluetoothDeviceListener", () => {
it("should register IPC listener when Electron API is available", () => { it("should register IPC listener when Electron API is available", () => {
const mockListener = vi.fn(); const mockListener = vi.fn();
(window as { electronAPI: { onBluetoothDeviceList: typeof mockListener } }).electronAPI = { (
window as unknown as {
electronAPI: { onBluetoothDeviceList: typeof mockListener };
}
).electronAPI = {
onBluetoothDeviceList: mockListener, onBluetoothDeviceList: mockListener,
}; };
@ -45,13 +53,20 @@ describe("useBluetoothDeviceListener", () => {
}); });
it("should update devices when listener receives data", async () => { it("should update devices when listener receives data", async () => {
let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null = null; let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null =
null;
const mockListener = vi.fn((callback: (devices: BluetoothDevice[]) => void) => { const mockListener = vi.fn(
(callback: (devices: BluetoothDevice[]) => void) => {
deviceListCallback = callback; deviceListCallback = callback;
}); },
);
(window as { electronAPI: { onBluetoothDeviceList: typeof mockListener } }).electronAPI = { (
window as unknown as {
electronAPI: { onBluetoothDeviceList: typeof mockListener };
}
).electronAPI = {
onBluetoothDeviceList: mockListener, onBluetoothDeviceList: mockListener,
}; };
@ -61,11 +76,14 @@ describe("useBluetoothDeviceListener", () => {
// Simulate device list update // Simulate device list update
const mockDevices: BluetoothDevice[] = [ const mockDevices: BluetoothDevice[] = [
{ id: "device1", name: "Device 1", address: "00:11:22:33:44:55", paired: false }, { deviceId: "device1", deviceName: "Device 1" },
{ id: "device2", name: "Device 2", address: "AA:BB:CC:DD:EE:FF", paired: true }, { deviceId: "device2", deviceName: "Device 2" },
]; ];
deviceListCallback?.(mockDevices); // Trigger the callback
act(() => {
deviceListCallback!(mockDevices);
});
await waitFor(() => { await waitFor(() => {
expect(result.current.devices).toEqual(mockDevices); expect(result.current.devices).toEqual(mockDevices);
@ -73,20 +91,29 @@ describe("useBluetoothDeviceListener", () => {
}); });
it("should set isScanning=true when empty device list received", async () => { it("should set isScanning=true when empty device list received", async () => {
let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null = null; let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null =
null;
const mockListener = vi.fn((callback: (devices: BluetoothDevice[]) => void) => { const mockListener = vi.fn(
(callback: (devices: BluetoothDevice[]) => void) => {
deviceListCallback = callback; deviceListCallback = callback;
}); },
);
(window as { electronAPI: { onBluetoothDeviceList: typeof mockListener } }).electronAPI = { (
window as unknown as {
electronAPI: { onBluetoothDeviceList: typeof mockListener };
}
).electronAPI = {
onBluetoothDeviceList: mockListener, onBluetoothDeviceList: mockListener,
}; };
const { result } = renderHook(() => useBluetoothDeviceListener()); const { result } = renderHook(() => useBluetoothDeviceListener());
// Simulate empty device list (scanning in progress) // Simulate empty device list (scanning in progress)
deviceListCallback?.([]); act(() => {
deviceListCallback!([]);
});
await waitFor(() => { await waitFor(() => {
expect(result.current.isScanning).toBe(true); expect(result.current.isScanning).toBe(true);
@ -95,29 +122,40 @@ describe("useBluetoothDeviceListener", () => {
}); });
it("should set isScanning=false when devices are received", async () => { it("should set isScanning=false when devices are received", async () => {
let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null = null; let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null =
null;
const mockListener = vi.fn((callback: (devices: BluetoothDevice[]) => void) => { const mockListener = vi.fn(
(callback: (devices: BluetoothDevice[]) => void) => {
deviceListCallback = callback; deviceListCallback = callback;
}); },
);
(window as { electronAPI: { onBluetoothDeviceList: typeof mockListener } }).electronAPI = { (
window as unknown as {
electronAPI: { onBluetoothDeviceList: typeof mockListener };
}
).electronAPI = {
onBluetoothDeviceList: mockListener, onBluetoothDeviceList: mockListener,
}; };
const { result } = renderHook(() => useBluetoothDeviceListener()); const { result } = renderHook(() => useBluetoothDeviceListener());
// First update: empty list (scanning) // First update: empty list (scanning)
deviceListCallback?.([]); act(() => {
deviceListCallback!([]);
});
await waitFor(() => { await waitFor(() => {
expect(result.current.isScanning).toBe(true); expect(result.current.isScanning).toBe(true);
}); });
// Second update: devices found (stop scanning indicator) // Second update: devices found (stop scanning indicator)
const mockDevices: BluetoothDevice[] = [ const mockDevices: BluetoothDevice[] = [
{ id: "device1", name: "Device 1", address: "00:11:22:33:44:55", paired: false }, { deviceId: "device1", deviceName: "Device 1" },
]; ];
deviceListCallback?.(mockDevices); act(() => {
deviceListCallback!(mockDevices);
});
await waitFor(() => { await waitFor(() => {
expect(result.current.isScanning).toBe(false); expect(result.current.isScanning).toBe(false);
@ -125,14 +163,21 @@ describe("useBluetoothDeviceListener", () => {
expect(result.current.devices).toEqual(mockDevices); expect(result.current.devices).toEqual(mockDevices);
}); });
it("should call optional callback when devices change", () => { it("should call optional callback when devices change", async () => {
let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null = null; let deviceListCallback: ((devices: BluetoothDevice[]) => void) | null =
null;
const mockListener = vi.fn((callback: (devices: BluetoothDevice[]) => void) => { const mockListener = vi.fn(
(callback: (devices: BluetoothDevice[]) => void) => {
deviceListCallback = callback; deviceListCallback = callback;
}); },
);
(window as { electronAPI: { onBluetoothDeviceList: typeof mockListener } }).electronAPI = { (
window as unknown as {
electronAPI: { onBluetoothDeviceList: typeof mockListener };
}
).electronAPI = {
onBluetoothDeviceList: mockListener, onBluetoothDeviceList: mockListener,
}; };
@ -140,11 +185,15 @@ describe("useBluetoothDeviceListener", () => {
renderHook(() => useBluetoothDeviceListener(onDevicesChanged)); renderHook(() => useBluetoothDeviceListener(onDevicesChanged));
const mockDevices: BluetoothDevice[] = [ const mockDevices: BluetoothDevice[] = [
{ id: "device1", name: "Device 1", address: "00:11:22:33:44:55", paired: false }, { deviceId: "device1", deviceName: "Device 1" },
]; ];
deviceListCallback?.(mockDevices); act(() => {
deviceListCallback!(mockDevices);
});
await waitFor(() => {
expect(onDevicesChanged).toHaveBeenCalledWith(mockDevices); expect(onDevicesChanged).toHaveBeenCalledWith(mockDevices);
}); });
});
}); });

View file

@ -18,10 +18,9 @@ describe("useAutoScroll", () => {
const scrollIntoViewMock = vi.fn(); const scrollIntoViewMock = vi.fn();
mockElement.scrollIntoView = scrollIntoViewMock; mockElement.scrollIntoView = scrollIntoViewMock;
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ dep }) => useAutoScroll(dep), {
({ dep }) => useAutoScroll(dep), initialProps: { dep: 0 },
{ initialProps: { dep: 0 } }, });
);
// Attach mock element to ref // Attach mock element to ref
(result.current as { current: HTMLElement }).current = mockElement; (result.current as { current: HTMLElement }).current = mockElement;

View file

@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderHook } from "@testing-library/react"; import { renderHook } from "@testing-library/react";
import { useClickOutside } from "./useClickOutside"; import { useClickOutside } from "./useClickOutside";
import { useRef } from "react"; import { useRef, type RefObject } from "react";
describe("useClickOutside", () => { describe("useClickOutside", () => {
it("should call handler when clicking outside element", () => { it("should call handler when clicking outside element", () => {
@ -91,8 +91,10 @@ describe("useClickOutside", () => {
const handler = vi.fn(); const handler = vi.fn();
const { result } = renderHook(() => { const { result } = renderHook(() => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const excludeRef = useRef<HTMLButtonElement>(null); const excludeRef = useRef<HTMLElement>(null);
useClickOutside(ref, handler, { excludeRefs: [excludeRef] }); useClickOutside(ref, handler, {
excludeRefs: [excludeRef as unknown as RefObject<HTMLElement>],
});
return { ref, excludeRef }; return { ref, excludeRef };
}); });
@ -102,7 +104,7 @@ describe("useClickOutside", () => {
document.body.appendChild(excludedElement); document.body.appendChild(excludedElement);
(result.current.ref as { current: HTMLDivElement }).current = element; (result.current.ref as { current: HTMLDivElement }).current = element;
(result.current.excludeRef as { current: HTMLButtonElement }).current = (result.current.excludeRef as { current: HTMLElement }).current =
excludedElement; excludedElement;
// Click on excluded element // Click on excluded element

View file

@ -23,12 +23,9 @@ describe("usePrevious", () => {
}); });
it("should handle different types of values", () => { it("should handle different types of values", () => {
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value }) => usePrevious(value), {
({ value }) => usePrevious(value),
{
initialProps: { value: "hello" as string | number | null }, initialProps: { value: "hello" as string | number | null },
}, });
);
expect(result.current).toBeUndefined(); expect(result.current).toBeUndefined();
@ -43,12 +40,9 @@ describe("usePrevious", () => {
const obj1 = { name: "first" }; const obj1 = { name: "first" };
const obj2 = { name: "second" }; const obj2 = { name: "second" };
const { result, rerender } = renderHook( const { result, rerender } = renderHook(({ value }) => usePrevious(value), {
({ value }) => usePrevious(value),
{
initialProps: { value: obj1 }, initialProps: { value: obj1 },
}, });
);
expect(result.current).toBeUndefined(); expect(result.current).toBeUndefined();