diff --git a/.changeset/famous-memes-add.md b/.changeset/famous-memes-add.md new file mode 100644 index 0000000..ab0cfcc --- /dev/null +++ b/.changeset/famous-memes-add.md @@ -0,0 +1,5 @@ +--- +"tiny-hooks": minor +--- + +Add new `useConnectionType` hook diff --git a/src/index.ts b/src/index.ts index 968c635..8199e90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { useBrowserCapabilities } from "./useBrowserCapabilities"; export { useClickAnywhere } from "./useClickAnywhere"; export { useClickOutside } from "./useClickOutside"; export { useClipboard } from "./useClipboard"; +export { useConnectionType } from "./useConnectionType"; export { useCookie } from "./useCookie"; export { useCounter } from "./useCounter"; export { useDebounce } from "./useDebounce"; diff --git a/src/useConnectionType/index.ts b/src/useConnectionType/index.ts new file mode 100644 index 0000000..1b192e5 --- /dev/null +++ b/src/useConnectionType/index.ts @@ -0,0 +1,2 @@ +export type { ConnectionInfo } from "./types"; +export { useConnectionType } from "./useConnectionType"; diff --git a/src/useConnectionType/types.ts b/src/useConnectionType/types.ts new file mode 100644 index 0000000..68f8589 --- /dev/null +++ b/src/useConnectionType/types.ts @@ -0,0 +1,9 @@ +export interface ConnectionInfo { + supported: boolean; + type?: string; + effectiveType?: string; + downlink?: number; + rtt?: number; + saveData?: boolean; + speedCategory?: "slow" | "medium" | "fast"; +} diff --git a/src/useConnectionType/useConnectionType.test.ts b/src/useConnectionType/useConnectionType.test.ts new file mode 100644 index 0000000..c878778 --- /dev/null +++ b/src/useConnectionType/useConnectionType.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from "bun:test"; +import { renderHook, waitFor } from "@testing-library/react"; +import { useConnectionType } from "./useConnectionType"; + +const createMockConnection = (overrides = {}) => ({ + type: "wifi", + effectiveType: "4g", + downlink: 45, + rtt: 20, + saveData: false, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + ...overrides, +}); + +describe("useConnectionType", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("should return supported=false if Network Information API not available", async () => { + const originalNavigator = global.navigator; + // @ts-expect-error + globalThis.navigator = {}; + + const { result } = renderHook(() => useConnectionType()); + + await waitFor(() => { + expect(result.current.supported).toBe(false); + }); + + globalThis.navigator = originalNavigator; + }); + + it("should return connection info and fast speedCategory when API is supported", async () => { + // @ts-expect-error + global.navigator.connection = createMockConnection({ + downlink: 10, + rtt: 50, + }); + + const { result } = renderHook(() => useConnectionType()); + + await waitFor(() => { + expect(result.current.supported).toBe(true); + expect(result.current.type).toBe("wifi"); + expect(result.current.effectiveType).toBe("4g"); + expect(result.current.downlink).toBe(10); + expect(result.current.rtt).toBe(50); + expect(result.current.saveData).toBe(false); + expect(result.current.speedCategory).toBe("fast"); + }); + }); + + it("should correctly calculate medium speedCategory", async () => { + // @ts-expect-error + global.navigator.connection = createMockConnection({ + downlink: 3, + rtt: 150, + }); + + const { result } = renderHook(() => useConnectionType()); + + await waitFor(() => { + expect(result.current.speedCategory).toBe("medium"); + }); + }); + + it("should correctly calculate slow speedCategory", async () => { + // @ts-expect-error + global.navigator.connection = createMockConnection({ + downlink: 1, + rtt: 400, + }); + + const { result } = renderHook(() => useConnectionType()); + + await waitFor(() => { + expect(result.current.speedCategory).toBe("slow"); + }); + }); + + it("should update when connection change event fires", async () => { + let changeHandler: (() => void) | undefined; + + const mockConnection = createMockConnection({ + addEventListener: vi.fn((event, handler) => { + if (event === "change") changeHandler = handler; + }), + }); + + // @ts-expect-error + global.navigator.connection = mockConnection; + + const { result } = renderHook(() => useConnectionType()); + + await waitFor(() => expect(result.current.speedCategory).toBe("fast")); + + mockConnection.downlink = 0.5; + mockConnection.rtt = 500; + + changeHandler?.(); + + await waitFor(() => { + expect(result.current.speedCategory).toBe("slow"); + }); + + mockConnection.downlink = 10; + mockConnection.rtt = 50; + + changeHandler?.(); + + await waitFor(() => { + expect(result.current.speedCategory).toBe("fast"); + }); + }); +}); diff --git a/src/useConnectionType/useConnectionType.ts b/src/useConnectionType/useConnectionType.ts new file mode 100644 index 0000000..6ac326d --- /dev/null +++ b/src/useConnectionType/useConnectionType.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import type { ConnectionInfo } from "./types.ts"; + +export function useConnectionType(): ConnectionInfo { + const [connectionInfo, setConnectionInfo] = useState({ + supported: true, + }); + + useEffect(() => { + // biome-ignore lint/suspicious/noExplicitAny: TS is dumb + const nav = navigator as any; + const connection = + nav.connection || nav.mozConnection || nav.webkitConnection; + + if (!connection) { + setConnectionInfo({ supported: false }); + return; + } + + const calculateSpeedCategory = (rtt?: number, downlink?: number) => { + if (!rtt || !downlink) return "medium"; + if (rtt > 300 || downlink < 1.5) return "slow"; + if (rtt > 100 || downlink < 5) return "medium"; + return "fast"; + }; + + const updateConnection = () => { + setConnectionInfo({ + supported: true, + type: connection.type, + effectiveType: connection.effectiveType, + downlink: connection.downlink, + rtt: connection.rtt, + saveData: connection.saveData, + speedCategory: calculateSpeedCategory( + connection.rtt, + connection.downlink, + ), + }); + }; + + updateConnection(); + + connection.addEventListener("change", updateConnection); + return () => connection.removeEventListener("change", updateConnection); + }, []); + + return connectionInfo; +}