mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 00:40:52 +08:00
test: finish utils coverage (contexts, kubernetes helpers, weather maps)
This commit is contained in:
@@ -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);
|
||||
|
||||
39
src/utils/contexts/color.test.jsx
Normal file
39
src/utils/contexts/color.test.jsx
Normal file
@@ -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 (
|
||||
<div>
|
||||
<div data-testid="value">{color}</div>
|
||||
<button type="button" onClick={() => setColor("red")}>
|
||||
red
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<ColorProvider>
|
||||
<Reader />
|
||||
</ColorProvider>,
|
||||
);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
|
||||
|
||||
33
src/utils/contexts/settings.test.jsx
Normal file
33
src/utils/contexts/settings.test.jsx
Normal file
@@ -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 (
|
||||
<div>
|
||||
<div data-testid="value">{JSON.stringify(settings)}</div>
|
||||
<button type="button" onClick={() => setSettings({ updated: true })}>
|
||||
update
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("utils/contexts/settings", () => {
|
||||
it("provides initial settings and allows updates", () => {
|
||||
render(
|
||||
<SettingsProvider initialSettings={{ a: 1 }}>
|
||||
<Reader />
|
||||
</SettingsProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("value")).toHaveTextContent('{"a":1}');
|
||||
fireEvent.click(screen.getByRole("button", { name: "update" }));
|
||||
expect(screen.getByTestId("value")).toHaveTextContent('{"updated":true}');
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
|
||||
|
||||
33
src/utils/contexts/tab.test.jsx
Normal file
33
src/utils/contexts/tab.test.jsx
Normal file
@@ -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 (
|
||||
<div>
|
||||
<div data-testid="value">{String(activeTab)}</div>
|
||||
<button type="button" onClick={() => setActiveTab("next")}>
|
||||
next
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("utils/contexts/tab", () => {
|
||||
it("provides initial tab and allows updates", () => {
|
||||
render(
|
||||
<TabProvider initialTab="first">
|
||||
<Reader />
|
||||
</TabProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("value")).toHaveTextContent("first");
|
||||
fireEvent.click(screen.getByRole("button", { name: "next" }));
|
||||
expect(screen.getByTestId("value")).toHaveTextContent("next");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
33
src/utils/contexts/theme.test.jsx
Normal file
33
src/utils/contexts/theme.test.jsx
Normal file
@@ -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 <div data-testid="value">{theme}</div>;
|
||||
}
|
||||
|
||||
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(
|
||||
<ThemeProvider>
|
||||
<Reader />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("value")).toHaveTextContent("light");
|
||||
await waitFor(() => expect(document.documentElement.classList.contains("light")).toBe(true));
|
||||
expect(localStorage.getItem("theme-mode")).toBe("light");
|
||||
});
|
||||
});
|
||||
28
src/utils/kubernetes/export.test.js
Normal file
28
src/utils/kubernetes/export.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
77
src/utils/kubernetes/httproute-list.test.js
Normal file
77
src/utils/kubernetes/httproute-list.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ export default async function listIngress() {
|
||||
logger.debug(error);
|
||||
return null;
|
||||
});
|
||||
ingressList = ingressData.items;
|
||||
ingressList = ingressData?.items ?? [];
|
||||
}
|
||||
return ingressList;
|
||||
}
|
||||
|
||||
79
src/utils/kubernetes/ingress-list.test.js
Normal file
79
src/utils/kubernetes/ingress-list.test.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
131
src/utils/kubernetes/resource-helpers.test.js
Normal file
131
src/utils/kubernetes/resource-helpers.test.js
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
78
src/utils/kubernetes/traefik-list.test.js
Normal file
78
src/utils/kubernetes/traefik-list.test.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
22
src/utils/kubernetes/utils.test.js
Normal file
22
src/utils/kubernetes/utils.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
17
src/utils/styles/themes.test.js
Normal file
17
src/utils/styles/themes.test.js
Normal file
@@ -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),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
15
src/utils/weather/condition-map.test.js
Normal file
15
src/utils/weather/condition-map.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
15
src/utils/weather/openmeteo-condition-map.test.js
Normal file
15
src/utils/weather/openmeteo-condition-map.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
15
src/utils/weather/owm-condition-map.test.js
Normal file
15
src/utils/weather/owm-condition-map.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user