diff --git a/src/widgets/omada/component.test.jsx b/src/widgets/omada/component.test.jsx
new file mode 100644
index 000000000..c7aa5447f
--- /dev/null
+++ b/src/widgets/omada/component.test.jsx
@@ -0,0 +1,80 @@
+// @vitest-environment jsdom
+
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { SettingsContext } from "utils/contexts/settings";
+
+const { useWidgetAPI } = vi.hoisted(() => ({
+ useWidgetAPI: vi.fn(),
+}));
+
+vi.mock("next-i18next", () => ({
+ useTranslation: () => ({
+ t: (key, opts) => {
+ if (key === "common.number") return String(opts?.value ?? "");
+ return key;
+ },
+ }),
+}));
+
+vi.mock("../../utils/proxy/use-widget-api", () => ({
+ default: useWidgetAPI,
+}));
+
+import Component from "./component";
+
+function renderWithSettings(ui) {
+ return render(
+ {} }}>{ui},
+ );
+}
+
+describe("widgets/omada/component", () => {
+ it("renders error UI when widget API errors", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
+
+ renderWithSettings();
+
+ expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
+ });
+
+ it("renders placeholders while loading and defaults fields to 4 visible blocks", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const { container } = renderWithSettings();
+
+ // Default fields do not include connectedSwitches, so Container filters it out.
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("omada.connectedAp")).toBeInTheDocument();
+ expect(screen.getByText("omada.activeUser")).toBeInTheDocument();
+ expect(screen.getByText("omada.alerts")).toBeInTheDocument();
+ expect(screen.getByText("omada.connectedGateways")).toBeInTheDocument();
+ expect(screen.queryByText("omada.connectedSwitches")).toBeNull();
+
+ // Values should be placeholders ("-") while loading.
+ expect(screen.getAllByText("-")).toHaveLength(4);
+ });
+
+ it("renders values when loaded (formatted via common.number)", () => {
+ useWidgetAPI.mockReturnValue({
+ data: {
+ connectedAp: 1,
+ activeUser: 2,
+ alerts: 3,
+ connectedGateways: 4,
+ connectedSwitches: 5,
+ },
+ error: undefined,
+ });
+
+ const { container } = renderWithSettings();
+
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ expect(screen.getByText("4")).toBeInTheDocument();
+ expect(screen.queryByText("5")).toBeNull(); // connectedSwitches filtered by default fields
+ });
+});
diff --git a/vitest.config.mjs b/vitest.config.mjs
index 7aeb0ae90..1568484e6 100644
--- a/vitest.config.mjs
+++ b/vitest.config.mjs
@@ -2,6 +2,11 @@ import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vitest/config";
export default defineConfig({
+ // Next.js handles JSX via SWC; Vitest uses Vite/esbuild, so enable the modern JSX runtime
+ // to avoid requiring `import React from "react"` in every JSX file.
+ esbuild: {
+ jsx: "automatic",
+ },
resolve: {
alias: {
components: fileURLToPath(new URL("./src/components", import.meta.url)),