- {isOffline && }
+ {isOffline && (
+
+ )}
{toasts.map((toast, i) => (
))}
diff --git a/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.js b/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.js
index 2837fbc1..3748bf88 100644
--- a/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.js
+++ b/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.js
@@ -1,23 +1,55 @@
import { useState, useEffect } from "react";
+import { useApplication } from "../../Application";
export const useOfflineStatus = () => {
- const [isOffline, setIsOffline] = useState(!navigator.onLine);
+ const application = useApplication();
+ const [isOffline, setIsOffline] = useState(!application.isOnline);
const updateOnlineStatus = () => {
- setIsOffline(!navigator.onLine);
+ setIsOffline(!application.isOnline);
};
useEffect(() => {
updateOnlineStatus();
- window.addEventListener("online", updateOnlineStatus);
- window.addEventListener("offline", updateOnlineStatus);
+ const setIsOfflineTrue = () => setIsOffline(true);
+ const setIsOfflineFalse = () => setIsOffline(false);
+
+ application.addEventListener("online", setIsOfflineFalse);
+ application.addEventListener("offline", setIsOfflineTrue);
return () => {
- window.removeEventListener("online", updateOnlineStatus);
- window.removeEventListener("offline", updateOnlineStatus);
+ application.removeEventListener("online", setIsOfflineFalse);
+ application.removeEventListener("offline", setIsOfflineTrue);
};
- }, []);
+ }, [application]);
- return { isOffline };
+ // Method to check a URL with timeout
+ const checkEndpoint = async (url, options = {}) => {
+ try {
+ const { method = 'HEAD', timeout = application._networkTimeout } = options;
+ await application.fetch(url, { method, timeout });
+ return true;
+ } catch (error) {
+ return false;
+ }
+ };
+
+ // Method to check multiple endpoints
+ const checkMultipleEndpoints = async (urls, options = {}) => {
+ const results = {};
+
+ const checks = urls.map(async (url) => {
+ results[url] = await checkEndpoint(url, options);
+ });
+
+ await Promise.all(checks);
+ return results;
+ };
+
+ return {
+ isOffline,
+ checkEndpoint,
+ checkMultipleEndpoints
+ };
};
diff --git a/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.spec.js b/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.spec.js
index 44d3d498..75bb33bc 100644
--- a/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.spec.js
+++ b/packages/react-radfish/hooks/useOfflineStatus/useOfflineStatus.spec.js
@@ -1,23 +1,161 @@
import { renderHook, act } from "@testing-library/react";
import { useOfflineStatus } from "./useOfflineStatus";
+import { useApplication } from "../../Application";
-describe("useToast", () => {
- it("should trigger when naviagator online switches", async () => {
- let dispatchEventSpy = vi.spyOn(window, "dispatchEvent");
- const onLineSpy = vi.spyOn(window.navigator, "onLine", "get");
+// Mock the useApplication hook
+vi.mock("../../Application", () => ({
+ useApplication: vi.fn()
+}));
- const { result, rerender } = renderHook(() => useOfflineStatus());
+describe("useOfflineStatus", () => {
+ let mockApplication;
+ beforeEach(() => {
+ vi.resetAllMocks();
+ const emitter = new EventTarget();
+ mockApplication = {
+ isOnline: false,
+ emitter,
+ _networkTimeout: 5000,
+ fetch: vi.fn(),
+ addEventListener: vi.fn((event, callback) => {
+ emitter.addEventListener(event, callback);
+ }),
+ dispatchEvent: vi.fn((event) => {
+ emitter.dispatchEvent(event);
+ }),
+ removeEventListener: vi.fn((event, callback) => {
+ emitter.removeEventListener(event, callback);
+ })
+ };
+ useApplication.mockReturnValue(mockApplication);
+ });
+
+ it("should initialize with offline status", () => {
+ const { result } = renderHook(() => useOfflineStatus());
+ expect(result.current.isOffline).toBe(true);
+ });
+
+ it('should mount and unmount handlers', () => {
+ const {unmount } = renderHook(() => useOfflineStatus());
- onLineSpy.mockReturnValue(true);
- window.dispatchEvent(new window.Event("online"));
- rerender();
+ expect(mockApplication.addEventListener).toHaveBeenCalledWith(
+ "online",
+ expect.any(Function)
+ );
+ expect(mockApplication.addEventListener).toHaveBeenCalledWith(
+ "offline",
+ expect.any(Function)
+ );
+
+ unmount();
+
+ expect(mockApplication.removeEventListener).toHaveBeenCalledWith(
+ "online",
+ expect.any(Function)
+ );
+ expect(mockApplication.removeEventListener).toHaveBeenCalledWith(
+ "offline",
+ expect.any(Function)
+ );
+
+ });
+
+ it("should trigger when navigator online switches", async () => {
+ const { result } = renderHook(() => useOfflineStatus());
+
+ // Must be wrapped in act callback to allow callstack to resolve
+ // before asserting value
+ act(() => {
+ mockApplication.dispatchEvent(new CustomEvent("online"));
+ });
expect(result.current.isOffline).toBe(false);
- onLineSpy.mockReturnValue(false);
- window.dispatchEvent(new window.Event("offline"));
- rerender();
+ // Must be wrapped in act callback to allow callstack to resolve
+ // before asserting value
+ act(() => {
+ mockApplication.dispatchEvent(new CustomEvent("offline"));
+ });
expect(result.current.isOffline).toBe(true);
+
+ });
+
+ it("should add event listeners for network events", () => {
+ renderHook(() => useOfflineStatus());
+
+ expect(mockApplication.addEventListener).toHaveBeenCalledWith(
+ "online",
+ expect.any(Function)
+ );
+ expect(mockApplication.addEventListener).toHaveBeenCalledWith(
+ "offline",
+ expect.any(Function)
+ );
+ });
+
+ it("should cleanup event listeners on unmount", () => {
+ const { unmount } = renderHook(() => useOfflineStatus());
+ unmount();
+
+ expect(mockApplication.removeEventListener).toHaveBeenCalledWith(
+ "online",
+ expect.any(Function)
+ );
+ expect(mockApplication.removeEventListener).toHaveBeenCalledWith(
+ "offline",
+ expect.any(Function)
+ );
+ });
+
+ it("should correctly check endpoint connectivity", async () => {
+ const { result } = renderHook(() => useOfflineStatus());
+
+ // Test successful fetch
+ mockApplication.fetch.mockResolvedValueOnce({});
+
+ let isReachable = await result.current.checkEndpoint("https://example.com");
+ expect(isReachable).toBe(true);
+ expect(mockApplication.fetch).toHaveBeenCalledWith(
+ "https://example.com",
+ { method: "HEAD", timeout: 5000 }
+ );
+
+ // Test failed fetch
+ mockApplication.fetch.mockRejectedValueOnce(new Error("Network error"));
+
+ isReachable = await result.current.checkEndpoint("https://example.com");
+ expect(isReachable).toBe(false);
+ });
+
+ it("should check multiple endpoints correctly", async () => {
+ const { result } = renderHook(() => useOfflineStatus());
+
+ // Setup different responses for different endpoints
+ mockApplication.fetch
+ .mockImplementationOnce((url) => {
+ if (url === "https://working.example.com") {
+ return Promise.resolve({});
+ }
+ return Promise.reject(new Error("Failed"));
+ })
+ .mockImplementationOnce((url) => {
+ if (url === "https://api.example.com") {
+ return Promise.resolve({});
+ }
+ return Promise.reject(new Error("Failed"));
+ });
+
+ const endpoints = [
+ "https://working.example.com",
+ "https://down.example.com"
+ ];
+
+ const results = await result.current.checkMultipleEndpoints(endpoints);
+
+ expect(results).toEqual({
+ "https://working.example.com": true,
+ "https://down.example.com": false
+ });
});
});
diff --git a/packages/react-radfish/hooks/useToast/useToast.spec.js b/packages/react-radfish/hooks/useToast/useToast.spec.js
index 493df398..5e36d12d 100644
--- a/packages/react-radfish/hooks/useToast/useToast.spec.js
+++ b/packages/react-radfish/hooks/useToast/useToast.spec.js
@@ -1,4 +1,4 @@
-import { renderHook } from "@testing-library/react";
+import { renderHook, act } from "@testing-library/react";
import { useToasts, dispatchToast } from "./useToast";
describe("useToast", () => {
@@ -11,7 +11,9 @@ describe("useToast", () => {
},
} = renderHook(() => useToasts());
- dispatchToast({ message: "Hello", status: "ok" });
+ act(() => {
+ dispatchToast({ message: "Hello", status: "ok" });
+ });
let dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(CustomEvent));
diff --git a/packages/react-radfish/package.json b/packages/react-radfish/package.json
index 0f55e3e1..382f62b4 100644
--- a/packages/react-radfish/package.json
+++ b/packages/react-radfish/package.json
@@ -1,6 +1,6 @@
{
"name": "@nmfs-radfish/react-radfish",
- "version": "1.0.0",
+ "version": "1.1.0-rc.0",
"type": "module",
"main": "./dist/index.umd.js",
"module": "./dist/index.es.js",
@@ -25,20 +25,25 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
- "@tanstack/react-table": "^8.16.0",
- "@trussworks/react-uswds": "^9.0.0"
+ "@tanstack/react-table": "^8.16.0"
},
"peerDependencies": {
"@nmfs-radfish/radfish": "^1.1.0",
+ "@trussworks/react-uswds": "^9.0.0",
"react": "^18.2.0"
},
"devDependencies": {
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "16.0.0",
+ "@trussworks/react-uswds": "^9.0.0",
"@vitejs/plugin-react": "^4.3.1",
+ "focus-trap-react": "^10.3.1",
"jsdom": "25.0.1",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.3.1",
"vite-plugin-css-injected-by-js": "^3.5.1",
"vitest": "2.1.2"
}
-}
\ No newline at end of file
+}