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

View file

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

View file

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

View file

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

View file

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

View file

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