Add useAppSettings hook for parsing app config day ranges
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { useAppSettings } from "./useAppSettings.js";
|
||||
import type { AppSettingsResponse } from "@/shared/api/appSettings.js";
|
||||
|
||||
const mockGetAppSettings = vi.fn();
|
||||
|
||||
vi.mock("@/shared/api/appSettings.js", () => ({
|
||||
getAppSettings: (...args: unknown[]) => mockGetAppSettings(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/api/provider.js", () => ({
|
||||
useApiClient: () => ({ get: vi.fn() }),
|
||||
}));
|
||||
|
||||
describe("useAppSettings", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("parses searchFrom/searchTo strings into numbers", async () => {
|
||||
const response: AppSettingsResponse = {
|
||||
uiOptions: {
|
||||
filter: {
|
||||
onlineboard: { searchFrom: "2d", searchTo: "14d" },
|
||||
schedule: { searchFrom: "30d", searchTo: "45d" },
|
||||
},
|
||||
},
|
||||
};
|
||||
mockGetAppSettings.mockResolvedValue(response);
|
||||
|
||||
const { result } = renderHook(() => useAppSettings());
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.onlineboardSearchFrom).toBe(2);
|
||||
expect(result.current.onlineboardSearchTo).toBe(14);
|
||||
expect(result.current.scheduleSearchFrom).toBe(30);
|
||||
expect(result.current.scheduleSearchTo).toBe(45);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it("returns defaults when uiOptions.filter is missing", async () => {
|
||||
const response: AppSettingsResponse = {};
|
||||
mockGetAppSettings.mockResolvedValue(response);
|
||||
|
||||
const { result } = renderHook(() => useAppSettings());
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.onlineboardSearchFrom).toBe(2);
|
||||
expect(result.current.onlineboardSearchTo).toBe(14);
|
||||
expect(result.current.scheduleSearchFrom).toBe(30);
|
||||
expect(result.current.scheduleSearchTo).toBe(30);
|
||||
});
|
||||
|
||||
it("returns defaults when a specific value does not match /\\d+d/", async () => {
|
||||
const response: AppSettingsResponse = {
|
||||
uiOptions: {
|
||||
filter: {
|
||||
onlineboard: { searchFrom: "blah", searchTo: "14d" },
|
||||
schedule: { searchFrom: "30d", searchTo: "30d" },
|
||||
},
|
||||
},
|
||||
};
|
||||
mockGetAppSettings.mockResolvedValue(response);
|
||||
|
||||
const { result } = renderHook(() => useAppSettings());
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.onlineboardSearchFrom).toBe(2);
|
||||
expect(result.current.onlineboardSearchTo).toBe(14);
|
||||
});
|
||||
|
||||
it("starts with loading=true and returns error on failure", async () => {
|
||||
const err = new Error("network");
|
||||
mockGetAppSettings.mockRejectedValue(err);
|
||||
|
||||
const { result } = renderHook(() => useAppSettings());
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.error).toBe(err);
|
||||
expect(result.current.onlineboardSearchFrom).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiClient } from "@/shared/api/provider.js";
|
||||
import { getAppSettings } from "@/shared/api/appSettings.js";
|
||||
|
||||
const DAYS_PATTERN = /^(\d+)d$/;
|
||||
|
||||
const DEFAULTS = {
|
||||
onlineboardSearchFrom: 2,
|
||||
onlineboardSearchTo: 14,
|
||||
scheduleSearchFrom: 30,
|
||||
scheduleSearchTo: 30,
|
||||
} as const;
|
||||
|
||||
function parseDays(value: string | undefined, fallback: number): number {
|
||||
if (!value) return fallback;
|
||||
const match = DAYS_PATTERN.exec(value);
|
||||
if (!match) return fallback;
|
||||
return parseInt(match[1]!, 10);
|
||||
}
|
||||
|
||||
export interface UseAppSettingsResult {
|
||||
onlineboardSearchFrom: number;
|
||||
onlineboardSearchTo: number;
|
||||
scheduleSearchFrom: number;
|
||||
scheduleSearchTo: number;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the global app settings and exposes day-range numbers.
|
||||
* On error or parse failure, returns default values (2/14/30/30).
|
||||
*/
|
||||
export function useAppSettings(): UseAppSettingsResult {
|
||||
const client = useApiClient();
|
||||
const [state, setState] = useState<Omit<UseAppSettingsResult, "loading" | "error">>(DEFAULTS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
getAppSettings(client)
|
||||
.then((response) => {
|
||||
if (cancelled) return;
|
||||
const ob = response.uiOptions?.filter?.onlineboard;
|
||||
const sc = response.uiOptions?.filter?.schedule;
|
||||
setState({
|
||||
onlineboardSearchFrom: parseDays(ob?.searchFrom, DEFAULTS.onlineboardSearchFrom),
|
||||
onlineboardSearchTo: parseDays(ob?.searchTo, DEFAULTS.onlineboardSearchTo),
|
||||
scheduleSearchFrom: parseDays(sc?.searchFrom, DEFAULTS.scheduleSearchFrom),
|
||||
scheduleSearchTo: parseDays(sc?.searchTo, DEFAULTS.scheduleSearchTo),
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (cancelled) return;
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
return { ...state, loading, error };
|
||||
}
|
||||
Reference in New Issue
Block a user