From 4c3d39709b5a13749deef114f5ecfe0067aaa9e2 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:33:08 -0800 Subject: [PATCH] test: finish utils coverage (contexts, kubernetes helpers, weather maps) --- src/utils/contexts/color.jsx | 9 +- src/utils/contexts/color.test.jsx | 39 ++++++ src/utils/contexts/settings.jsx | 10 +- src/utils/contexts/settings.test.jsx | 33 +++++ src/utils/contexts/tab.jsx | 10 +- src/utils/contexts/tab.test.jsx | 33 +++++ src/utils/contexts/theme.jsx | 9 +- src/utils/contexts/theme.test.jsx | 33 +++++ src/utils/kubernetes/export.test.js | 28 ++++ src/utils/kubernetes/httproute-list.test.js | 77 ++++++++++ src/utils/kubernetes/ingress-list.js | 2 +- src/utils/kubernetes/ingress-list.test.js | 79 +++++++++++ src/utils/kubernetes/resource-helpers.test.js | 131 ++++++++++++++++++ src/utils/kubernetes/traefik-list.test.js | 78 +++++++++++ src/utils/kubernetes/utils.test.js | 22 +++ src/utils/styles/themes.test.js | 17 +++ src/utils/weather/condition-map.test.js | 15 ++ .../weather/openmeteo-condition-map.test.js | 15 ++ src/utils/weather/owm-condition-map.test.js | 15 ++ 19 files changed, 636 insertions(+), 19 deletions(-) create mode 100644 src/utils/contexts/color.test.jsx create mode 100644 src/utils/contexts/settings.test.jsx create mode 100644 src/utils/contexts/tab.test.jsx create mode 100644 src/utils/contexts/theme.test.jsx create mode 100644 src/utils/kubernetes/export.test.js create mode 100644 src/utils/kubernetes/httproute-list.test.js create mode 100644 src/utils/kubernetes/ingress-list.test.js create mode 100644 src/utils/kubernetes/resource-helpers.test.js create mode 100644 src/utils/kubernetes/traefik-list.test.js create mode 100644 src/utils/kubernetes/utils.test.js create mode 100644 src/utils/styles/themes.test.js create mode 100644 src/utils/weather/condition-map.test.js create mode 100644 src/utils/weather/openmeteo-condition-map.test.js create mode 100644 src/utils/weather/owm-condition-map.test.js diff --git a/src/utils/contexts/color.jsx b/src/utils/contexts/color.jsx index 623d31f84..909b8e986 100644 --- a/src/utils/contexts/color.jsx +++ b/src/utils/contexts/color.jsx @@ -17,7 +17,7 @@ const getInitialColor = () => { export const ColorContext = createContext(); export function ColorProvider({ initialTheme, children }) { - const [color, setColor] = useState(getInitialColor); + const [color, setColor] = useState(() => initialTheme ?? getInitialColor()); const rawSetColor = (rawColor) => { const root = window.document.documentElement; @@ -30,9 +30,10 @@ export function ColorProvider({ initialTheme, children }) { lastColor = rawColor; }; - if (initialTheme) { - rawSetColor(initialTheme); - } + useEffect(() => { + if (initialTheme !== undefined) setColor(initialTheme ?? getInitialColor()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialTheme]); useEffect(() => { rawSetColor(color); diff --git a/src/utils/contexts/color.test.jsx b/src/utils/contexts/color.test.jsx new file mode 100644 index 000000000..738cd61c8 --- /dev/null +++ b/src/utils/contexts/color.test.jsx @@ -0,0 +1,39 @@ +// @vitest-environment jsdom + +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { useContext } from "react"; +import { describe, expect, it } from "vitest"; + +import { ColorContext, ColorProvider } from "./color"; + +function Reader() { + const { color, setColor } = useContext(ColorContext); + return ( +
+
{color}
+ +
+ ); +} + +describe("utils/contexts/color", () => { + it("initializes from localStorage and writes theme class + storage on updates", async () => { + localStorage.setItem("theme-color", "blue"); + document.documentElement.className = ""; + + render( + + + , + ); + + expect(screen.getByTestId("value")).toHaveTextContent("blue"); + await waitFor(() => expect(document.documentElement.classList.contains("theme-blue")).toBe(true)); + + fireEvent.click(screen.getByRole("button", { name: "red" })); + await waitFor(() => expect(document.documentElement.classList.contains("theme-red")).toBe(true)); + expect(localStorage.getItem("theme-color")).toBe("red"); + }); +}); diff --git a/src/utils/contexts/settings.jsx b/src/utils/contexts/settings.jsx index 764519530..00699b41a 100644 --- a/src/utils/contexts/settings.jsx +++ b/src/utils/contexts/settings.jsx @@ -1,13 +1,13 @@ -import { createContext, useMemo, useState } from "react"; +import { createContext, useEffect, useMemo, useState } from "react"; export const SettingsContext = createContext(); export function SettingsProvider({ initialSettings, children }) { - const [settings, setSettings] = useState({}); + const [settings, setSettings] = useState(() => initialSettings ?? {}); - if (initialSettings) { - setSettings(initialSettings); - } + useEffect(() => { + if (initialSettings !== undefined) setSettings(initialSettings ?? {}); + }, [initialSettings]); const value = useMemo(() => ({ settings, setSettings }), [settings]); diff --git a/src/utils/contexts/settings.test.jsx b/src/utils/contexts/settings.test.jsx new file mode 100644 index 000000000..2a6abd5bb --- /dev/null +++ b/src/utils/contexts/settings.test.jsx @@ -0,0 +1,33 @@ +// @vitest-environment jsdom + +import { fireEvent, render, screen } from "@testing-library/react"; +import { useContext } from "react"; +import { describe, expect, it } from "vitest"; + +import { SettingsContext, SettingsProvider } from "./settings"; + +function Reader() { + const { settings, setSettings } = useContext(SettingsContext); + return ( +
+
{JSON.stringify(settings)}
+ +
+ ); +} + +describe("utils/contexts/settings", () => { + it("provides initial settings and allows updates", () => { + render( + + + , + ); + + expect(screen.getByTestId("value")).toHaveTextContent('{"a":1}'); + fireEvent.click(screen.getByRole("button", { name: "update" })); + expect(screen.getByTestId("value")).toHaveTextContent('{"updated":true}'); + }); +}); diff --git a/src/utils/contexts/tab.jsx b/src/utils/contexts/tab.jsx index 2a3d34578..c18fc2c94 100644 --- a/src/utils/contexts/tab.jsx +++ b/src/utils/contexts/tab.jsx @@ -1,13 +1,13 @@ -import { createContext, useMemo, useState } from "react"; +import { createContext, useEffect, useMemo, useState } from "react"; export const TabContext = createContext(); export function TabProvider({ initialTab, children }) { - const [activeTab, setActiveTab] = useState(false); + const [activeTab, setActiveTab] = useState(() => initialTab ?? false); - if (initialTab) { - setActiveTab(initialTab); - } + useEffect(() => { + if (initialTab !== undefined) setActiveTab(initialTab ?? false); + }, [initialTab]); const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]); diff --git a/src/utils/contexts/tab.test.jsx b/src/utils/contexts/tab.test.jsx new file mode 100644 index 000000000..5b051edfa --- /dev/null +++ b/src/utils/contexts/tab.test.jsx @@ -0,0 +1,33 @@ +// @vitest-environment jsdom + +import { fireEvent, render, screen } from "@testing-library/react"; +import { useContext } from "react"; +import { describe, expect, it } from "vitest"; + +import { TabContext, TabProvider } from "./tab"; + +function Reader() { + const { activeTab, setActiveTab } = useContext(TabContext); + return ( +
+
{String(activeTab)}
+ +
+ ); +} + +describe("utils/contexts/tab", () => { + it("provides initial tab and allows updates", () => { + render( + + + , + ); + + expect(screen.getByTestId("value")).toHaveTextContent("first"); + fireEvent.click(screen.getByRole("button", { name: "next" })); + expect(screen.getByTestId("value")).toHaveTextContent("next"); + }); +}); diff --git a/src/utils/contexts/theme.jsx b/src/utils/contexts/theme.jsx index 2c48e8347..76dd37800 100644 --- a/src/utils/contexts/theme.jsx +++ b/src/utils/contexts/theme.jsx @@ -19,7 +19,7 @@ const getInitialTheme = () => { export const ThemeContext = createContext(); export function ThemeProvider({ initialTheme, children }) { - const [theme, setTheme] = useState(getInitialTheme); + const [theme, setTheme] = useState(() => initialTheme ?? getInitialTheme()); const rawSetTheme = (rawTheme) => { const root = window.document.documentElement; @@ -31,9 +31,10 @@ export function ThemeProvider({ initialTheme, children }) { localStorage.setItem("theme-mode", rawTheme); }; - if (initialTheme) { - rawSetTheme(initialTheme); - } + useEffect(() => { + if (initialTheme !== undefined) setTheme(initialTheme ?? getInitialTheme()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialTheme]); useEffect(() => { rawSetTheme(theme); diff --git a/src/utils/contexts/theme.test.jsx b/src/utils/contexts/theme.test.jsx new file mode 100644 index 000000000..e4ede3def --- /dev/null +++ b/src/utils/contexts/theme.test.jsx @@ -0,0 +1,33 @@ +// @vitest-environment jsdom + +import { render, screen, waitFor } from "@testing-library/react"; +import { useContext } from "react"; +import { describe, expect, it, vi } from "vitest"; + +import { ThemeContext, ThemeProvider } from "./theme"; + +function Reader() { + const { theme } = useContext(ThemeContext); + return
{theme}
; +} + +describe("utils/contexts/theme", () => { + it("initializes from localStorage and writes html classes", async () => { + // jsdom doesn't implement matchMedia by default; ensure it exists for getInitialTheme. + window.matchMedia = + window.matchMedia || vi.fn(() => ({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })); + + localStorage.setItem("theme-mode", "light"); + document.documentElement.className = ""; + + render( + + + , + ); + + expect(screen.getByTestId("value")).toHaveTextContent("light"); + await waitFor(() => expect(document.documentElement.classList.contains("light")).toBe(true)); + expect(localStorage.getItem("theme-mode")).toBe("light"); + }); +}); diff --git a/src/utils/kubernetes/export.test.js b/src/utils/kubernetes/export.test.js new file mode 100644 index 000000000..142d28357 --- /dev/null +++ b/src/utils/kubernetes/export.test.js @@ -0,0 +1,28 @@ +import { describe, expect, it, vi } from "vitest"; + +const { listIngress, listTraefikIngress, listHttpRoute, isDiscoverable, constructedServiceFromResource } = vi.hoisted( + () => ({ + listIngress: vi.fn(), + listTraefikIngress: vi.fn(), + listHttpRoute: vi.fn(), + isDiscoverable: vi.fn(), + constructedServiceFromResource: vi.fn(), + }), +); + +vi.mock("utils/kubernetes/ingress-list", () => ({ default: listIngress })); +vi.mock("utils/kubernetes/traefik-list", () => ({ default: listTraefikIngress })); +vi.mock("utils/kubernetes/httproute-list", () => ({ default: listHttpRoute })); +vi.mock("utils/kubernetes/resource-helpers", () => ({ isDiscoverable, constructedServiceFromResource })); + +import kubernetes from "./export"; + +describe("utils/kubernetes/export", () => { + it("re-exports kubernetes helper functions", () => { + expect(kubernetes.listIngress).toBe(listIngress); + expect(kubernetes.listTraefikIngress).toBe(listTraefikIngress); + expect(kubernetes.listHttpRoute).toBe(listHttpRoute); + expect(kubernetes.isDiscoverable).toBe(isDiscoverable); + expect(kubernetes.constructedServiceFromResource).toBe(constructedServiceFromResource); + }); +}); diff --git a/src/utils/kubernetes/httproute-list.test.js b/src/utils/kubernetes/httproute-list.test.js new file mode 100644 index 000000000..f8e9eda24 --- /dev/null +++ b/src/utils/kubernetes/httproute-list.test.js @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { state, getKubernetes, getKubeConfig, logger } = vi.hoisted(() => { + const state = { + enabled: true, + namespaces: ["a", "b"], + routesByNs: { + a: [{ metadata: { name: "r1" } }], + b: [{ metadata: { name: "r2" } }], + }, + crd: { + listNamespacedCustomObject: vi.fn(async ({ namespace }) => ({ items: state.routesByNs[namespace] ?? [] })), + }, + core: { + listNamespace: vi.fn(async () => ({ items: state.namespaces.map((n) => ({ metadata: { name: n } })) })), + }, + kc: { + makeApiClient: vi.fn((Api) => (Api.name === "CoreV1Api" ? state.core : state.crd)), + }, + }; + + return { + state, + getKubernetes: vi.fn(() => ({ gateway: state.enabled })), + getKubeConfig: vi.fn(() => state.kc), + logger: { error: vi.fn(), debug: vi.fn() }, + }; +}); + +vi.mock("@kubernetes/client-node", () => ({ + CoreV1Api: class CoreV1Api {}, + CustomObjectsApi: class CustomObjectsApi {}, +})); + +vi.mock("utils/config/kubernetes", () => ({ + getKubeConfig, + getKubernetes, + HTTPROUTE_API_GROUP: "gateway.networking.k8s.io", + HTTPROUTE_API_VERSION: "v1", +})); + +vi.mock("utils/logger", () => ({ + default: () => logger, +})); + +describe("utils/kubernetes/httproute-list", () => { + beforeEach(() => { + vi.clearAllMocks(); + state.enabled = true; + state.namespaces = ["a", "b"]; + state.routesByNs = { + a: [{ metadata: { name: "r1" } }], + b: [{ metadata: { name: "r2" } }], + }; + }); + + it("returns an empty list when gateway discovery is disabled", async () => { + state.enabled = false; + vi.resetModules(); + const listHttpRoute = (await import("./httproute-list")).default; + + const result = await listHttpRoute(); + + expect(result).toEqual([]); + }); + + it("lists namespaces and aggregates httproutes", async () => { + vi.resetModules(); + const listHttpRoute = (await import("./httproute-list")).default; + + const result = await listHttpRoute(); + + expect(result.map((r) => r.metadata.name)).toEqual(["r1", "r2"]); + expect(state.core.listNamespace).toHaveBeenCalled(); + expect(state.crd.listNamespacedCustomObject).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/utils/kubernetes/ingress-list.js b/src/utils/kubernetes/ingress-list.js index 1cd9ca952..0523fefe1 100644 --- a/src/utils/kubernetes/ingress-list.js +++ b/src/utils/kubernetes/ingress-list.js @@ -20,7 +20,7 @@ export default async function listIngress() { logger.debug(error); return null; }); - ingressList = ingressData.items; + ingressList = ingressData?.items ?? []; } return ingressList; } diff --git a/src/utils/kubernetes/ingress-list.test.js b/src/utils/kubernetes/ingress-list.test.js new file mode 100644 index 000000000..4e1cdc76f --- /dev/null +++ b/src/utils/kubernetes/ingress-list.test.js @@ -0,0 +1,79 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { state, getKubernetes, getKubeConfig, logger } = vi.hoisted(() => { + const state = { + ingressEnabled: true, + items: [], + throw: null, + networking: { + listIngressForAllNamespaces: vi.fn(async () => { + if (state.throw) throw state.throw; + return { items: state.items }; + }), + }, + kc: { + makeApiClient: vi.fn(() => state.networking), + }, + }; + + return { + state, + getKubernetes: vi.fn(() => ({ ingress: state.ingressEnabled })), + getKubeConfig: vi.fn(() => state.kc), + logger: { error: vi.fn(), debug: vi.fn() }, + }; +}); + +vi.mock("@kubernetes/client-node", () => ({ + NetworkingV1Api: class NetworkingV1Api {}, +})); + +vi.mock("utils/config/kubernetes", () => ({ + getKubernetes, + getKubeConfig, +})); + +vi.mock("utils/logger", () => ({ + default: () => logger, +})); + +describe("utils/kubernetes/ingress-list", () => { + beforeEach(() => { + vi.clearAllMocks(); + state.ingressEnabled = true; + state.items = []; + state.throw = null; + }); + + it("returns an empty list when ingress discovery is disabled", async () => { + state.ingressEnabled = false; + vi.resetModules(); + const listIngress = (await import("./ingress-list")).default; + + const result = await listIngress(); + + expect(result).toEqual([]); + expect(state.networking.listIngressForAllNamespaces).not.toHaveBeenCalled(); + }); + + it("returns items from listIngressForAllNamespaces", async () => { + state.items = [{ metadata: { name: "i1" } }]; + vi.resetModules(); + const listIngress = (await import("./ingress-list")).default; + + const result = await listIngress(); + + expect(result).toEqual([{ metadata: { name: "i1" } }]); + }); + + it("returns an empty list on errors", async () => { + state.throw = { statusCode: 500, body: "nope", response: "x" }; + vi.resetModules(); + const listIngress = (await import("./ingress-list")).default; + + const result = await listIngress(); + + expect(result).toEqual([]); + expect(logger.error).toHaveBeenCalled(); + }); +}); diff --git a/src/utils/kubernetes/resource-helpers.test.js b/src/utils/kubernetes/resource-helpers.test.js new file mode 100644 index 000000000..0ea12cb33 --- /dev/null +++ b/src/utils/kubernetes/resource-helpers.test.js @@ -0,0 +1,131 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { state, substituteEnvironmentVars, getKubeConfig, logger } = vi.hoisted(() => { + const state = { + gatewayProtocol: "https", + }; + + const substituteEnvironmentVars = vi.fn((raw) => + raw.replaceAll("${DESC}", process.env.DESC ?? "").replaceAll("${ICON}", process.env.ICON ?? ""), + ); + + const crd = { + getNamespacedCustomObject: vi.fn(async () => ({ + spec: { listeners: [{ name: "web", protocol: state.gatewayProtocol.toUpperCase() }] }, + })), + }; + + const kc = { + makeApiClient: vi.fn(() => crd), + }; + + return { + state, + substituteEnvironmentVars, + getKubeConfig: vi.fn(() => kc), + logger: { error: vi.fn(), debug: vi.fn() }, + }; +}); + +vi.mock("@kubernetes/client-node", () => ({ + CustomObjectsApi: class CustomObjectsApi {}, +})); + +vi.mock("utils/config/config", () => ({ + substituteEnvironmentVars, +})); + +vi.mock("utils/config/kubernetes", () => ({ + ANNOTATION_BASE: "gethomepage.dev", + ANNOTATION_WIDGET_BASE: "gethomepage.dev/widget.", + HTTPROUTE_API_GROUP: "gateway.networking.k8s.io", + HTTPROUTE_API_VERSION: "v1", + getKubeConfig, +})); + +vi.mock("utils/logger", () => ({ + default: () => logger, +})); + +import { constructedServiceFromResource, isDiscoverable } from "./resource-helpers"; + +describe("utils/kubernetes/resource-helpers", () => { + beforeEach(() => { + vi.clearAllMocks(); + process.env.DESC = "desc"; + process.env.ICON = "mdi:test"; + state.gatewayProtocol = "https"; + }); + + it("checks discoverability by annotations and instance", () => { + const base = "gethomepage.dev"; + const resource = { metadata: { annotations: { [`${base}/enabled`]: "true" } } }; + + expect(isDiscoverable(resource, "x")).toBe(true); + expect(isDiscoverable({ metadata: { annotations: { [`${base}/enabled`]: "false" } } }, "x")).toBe(false); + expect( + isDiscoverable({ metadata: { annotations: { [`${base}/enabled`]: "true", [`${base}/instance`]: "x" } } }, "x"), + ).toBe(true); + expect( + isDiscoverable({ metadata: { annotations: { [`${base}/enabled`]: "true", [`${base}/instance.y`]: "1" } } }, "y"), + ).toBe(true); + }); + + it("constructs a service from an ingress and applies widget annotations + env substitution", async () => { + const base = "gethomepage.dev"; + const resource = { + kind: "Ingress", + metadata: { + name: "app", + namespace: "ns", + annotations: { + [`${base}/external`]: "TRUE", + [`${base}/description`]: "${DESC}", + [`${base}/icon`]: "${ICON}", + [`${base}/widget.type`]: "kubernetes", + [`${base}/widget.url`]: "http://x", + }, + }, + spec: { + tls: [{}], + rules: [{ host: "example.com", http: { paths: [{ path: "/app" }] } }], + }, + }; + + const service = await constructedServiceFromResource(resource); + + expect(service.href).toBe("https://example.com/app"); + expect(service.external).toBe(true); + expect(service.description).toBe("desc"); + expect(service.icon).toBe("mdi:test"); + expect(service.widget.type).toBe("kubernetes"); + expect(service.widget.url).toBe("http://x"); + expect(substituteEnvironmentVars).toHaveBeenCalled(); + }); + + it("constructs a href from an HTTPRoute using the gateway listener protocol", async () => { + const base = "gethomepage.dev"; + const resource = { + kind: "HTTPRoute", + metadata: { + name: "route", + namespace: "ns", + annotations: { + [`${base}/enabled`]: "true", + }, + }, + spec: { + hostnames: ["example.com"], + parentRefs: [{ namespace: "ns", name: "gw", sectionName: "web" }], + rules: [ + { + matches: [{ path: { type: "PathPrefix", value: "/r" } }], + }, + ], + }, + }; + + const service = await constructedServiceFromResource(resource); + expect(service.href).toBe("https://example.com/r"); + }); +}); diff --git a/src/utils/kubernetes/traefik-list.test.js b/src/utils/kubernetes/traefik-list.test.js new file mode 100644 index 000000000..a7aa04aa8 --- /dev/null +++ b/src/utils/kubernetes/traefik-list.test.js @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { state, getKubernetes, getKubeConfig, checkCRD, logger } = vi.hoisted(() => { + const state = { + enabled: true, + containoItems: [], + ioItems: [], + crd: { + listClusterCustomObject: vi.fn(async ({ group }) => { + if (group === "traefik.containo.us") return { items: state.containoItems }; + if (group === "traefik.io") return { items: state.ioItems }; + return { items: [] }; + }), + }, + kc: { + makeApiClient: vi.fn(() => state.crd), + }, + }; + + return { + state, + getKubernetes: vi.fn(() => ({ traefik: state.enabled })), + getKubeConfig: vi.fn(() => state.kc), + checkCRD: vi.fn(async () => true), + logger: { error: vi.fn(), debug: vi.fn() }, + }; +}); + +vi.mock("@kubernetes/client-node", () => ({ + CustomObjectsApi: class CustomObjectsApi {}, +})); + +vi.mock("utils/config/kubernetes", () => ({ + ANNOTATION_BASE: "gethomepage.dev", + checkCRD, + getKubeConfig, + getKubernetes, +})); + +vi.mock("utils/logger", () => ({ + default: () => logger, +})); + +describe("utils/kubernetes/traefik-list", () => { + beforeEach(() => { + vi.clearAllMocks(); + state.enabled = true; + state.containoItems = []; + state.ioItems = []; + }); + + it("returns an empty list when traefik discovery is disabled", async () => { + state.enabled = false; + vi.resetModules(); + const listTraefikIngress = (await import("./traefik-list")).default; + + const result = await listTraefikIngress(); + + expect(result).toEqual([]); + }); + + it("filters and merges ingressroutes with homepage href annotations", async () => { + state.containoItems = [ + { metadata: { annotations: { "gethomepage.dev/href": "http://a" } } }, + { metadata: { annotations: {} } }, + ]; + state.ioItems = [{ metadata: { annotations: { "gethomepage.dev/href": "http://b" } } }]; + vi.resetModules(); + const listTraefikIngress = (await import("./traefik-list")).default; + + const result = await listTraefikIngress(); + + expect(result).toHaveLength(2); + expect(result[0].metadata.annotations["gethomepage.dev/href"]).toBe("http://a"); + expect(result[1].metadata.annotations["gethomepage.dev/href"]).toBe("http://b"); + expect(checkCRD).toHaveBeenCalled(); + }); +}); diff --git a/src/utils/kubernetes/utils.test.js b/src/utils/kubernetes/utils.test.js new file mode 100644 index 000000000..bea9506f5 --- /dev/null +++ b/src/utils/kubernetes/utils.test.js @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { parseCpu, parseMemory } from "./utils"; + +describe("utils/kubernetes/utils", () => { + it("parses cpu units into core values", () => { + expect(parseCpu("500m")).toBeCloseTo(0.5); + expect(parseCpu("250u")).toBeCloseTo(0.00025); + expect(parseCpu("1000n")).toBeCloseTo(0.000001); + expect(parseCpu("2")).toBe(2); + }); + + it("parses memory units into numeric values", () => { + expect(parseMemory("1Gi")).toBe(1000000000); + expect(parseMemory("1G")).toBe(1024 * 1024 * 1024); + expect(parseMemory("1Mi")).toBe(1000000); + expect(parseMemory("1M")).toBe(1024 * 1024); + expect(parseMemory("1Ki")).toBe(1000); + expect(parseMemory("1K")).toBe(1024); + expect(parseMemory("256")).toBe(256); + }); +}); diff --git a/src/utils/styles/themes.test.js b/src/utils/styles/themes.test.js new file mode 100644 index 000000000..7daacee24 --- /dev/null +++ b/src/utils/styles/themes.test.js @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; + +import themes from "./themes"; + +describe("utils/styles/themes", () => { + it("contains expected theme palettes", () => { + expect(themes).toHaveProperty("slate"); + expect(themes.slate).toEqual( + expect.objectContaining({ + light: expect.stringMatching(/^#[0-9a-f]{6}$/i), + dark: expect.stringMatching(/^#[0-9a-f]{6}$/i), + iconStart: expect.stringMatching(/^#[0-9a-f]{6}$/i), + iconEnd: expect.stringMatching(/^#[0-9a-f]{6}$/i), + }), + ); + }); +}); diff --git a/src/utils/weather/condition-map.test.js b/src/utils/weather/condition-map.test.js new file mode 100644 index 000000000..c74635b9c --- /dev/null +++ b/src/utils/weather/condition-map.test.js @@ -0,0 +1,15 @@ +import * as Icons from "react-icons/wi"; +import { describe, expect, it } from "vitest"; + +import mapIcon from "./condition-map"; + +describe("utils/weather/condition-map", () => { + it("maps known condition codes to day/night icons", () => { + expect(mapIcon(1000, "day")).toBe(Icons.WiDaySunny); + expect(mapIcon(1000, "night")).toBe(Icons.WiNightClear); + }); + + it("falls back to a default icon for unknown codes", () => { + expect(mapIcon(999999, "day")).toBe(Icons.WiDaySunny); + }); +}); diff --git a/src/utils/weather/openmeteo-condition-map.test.js b/src/utils/weather/openmeteo-condition-map.test.js new file mode 100644 index 000000000..40e301e67 --- /dev/null +++ b/src/utils/weather/openmeteo-condition-map.test.js @@ -0,0 +1,15 @@ +import * as Icons from "react-icons/wi"; +import { describe, expect, it } from "vitest"; + +import mapIcon from "./openmeteo-condition-map"; + +describe("utils/weather/openmeteo-condition-map", () => { + it("maps known condition codes to day/night icons", () => { + expect(mapIcon(95, "day")).toBe(Icons.WiDayThunderstorm); + expect(mapIcon(95, "night")).toBe(Icons.WiNightAltThunderstorm); + }); + + it("falls back to a default icon for unknown codes", () => { + expect(mapIcon(999999, "day")).toBe(Icons.WiDaySunny); + }); +}); diff --git a/src/utils/weather/owm-condition-map.test.js b/src/utils/weather/owm-condition-map.test.js new file mode 100644 index 000000000..2094eac0b --- /dev/null +++ b/src/utils/weather/owm-condition-map.test.js @@ -0,0 +1,15 @@ +import * as Icons from "react-icons/wi"; +import { describe, expect, it } from "vitest"; + +import mapIcon from "./owm-condition-map"; + +describe("utils/weather/owm-condition-map", () => { + it("maps known condition codes to day/night icons", () => { + expect(mapIcon(804, "day")).toBe(Icons.WiCloudy); + expect(mapIcon(500, "night")).toBe(Icons.WiNightAltRain); + }); + + it("falls back to a default icon for unknown codes", () => { + expect(mapIcon(999999, "day")).toBe(Icons.WiDaySunny); + }); +});