Files
homarr/packages/custom-widgets/src/test/workbench-ui.spec.tsx
T

158 lines
5.1 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import type { Root } from "react-dom/client";
import { MantineProvider } from "@mantine/core";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { CustomWidgetCodeEditor, PreviewHeader, PreviewResponsePanel, ResponseTree } from "../workbench";
import type { CustomWidgetEditorMessages } from "../workbench";
vi.mock("../workbench/direct-code-mirror", () => ({
default: ({ id, label, labelledBy }: { id: string; label: string; labelledBy: string }) => (
<textarea id={id} aria-label={label} aria-labelledby={labelledBy} />
),
}));
const editorMessages: CustomWidgetEditorMessages = {
languageJsx: "JSX",
languageJson: "JSON",
undo: "Undo",
redo: "Redo",
components: "Components",
componentSearch: "Search components",
componentEmpty: "No components",
componentCount: (count) => `${count} components`,
insertStarter: "Insert starter",
format: "Format",
copy: "Copy",
copied: "Copied",
schema: "Schema",
schemaTab: "JSON Schema",
minimalTab: "Minimal",
fullTab: "Full",
errors: (count) => `${count} errors`,
warnings: (count) => `${count} warnings`,
ready: "Ready",
position: ({ line, column }) => `${line}:${column}`,
characters: (count) => `${count}`,
diagnosticsTitle: "Diagnostics",
diagnostic: (diagnostic) => diagnostic.code,
};
let root: Root;
let host: HTMLDivElement;
beforeEach(() => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
window.matchMedia = vi.fn().mockImplementation(() => ({
matches: false,
media: "",
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
host = document.createElement("div");
document.body.append(host);
root = createRoot(host);
});
afterEach(async () => {
await act(() => root.unmount());
host.remove();
});
async function render(node: ReactNode) {
await act(async () => root.render(<MantineProvider>{node}</MantineProvider>));
}
describe("Custom Widget workbench UI", () => {
test("associates the visible label with the editable CodeMirror control", async () => {
await render(
<CustomWidgetCodeEditor
id="template-editor"
label="Template"
language="jsx"
value="<Text>Hello</Text>"
messages={editorMessages}
onChange={vi.fn()}
/>,
);
await act(async () => Promise.resolve());
const label = host.querySelector('#template-editor-label[for="template-editor"]');
expect(label).not.toBeNull();
expect(host.querySelector("#template-editor")?.getAttribute("aria-label")).toBe("Template");
expect(host.querySelector("#template-editor")?.getAttribute("aria-labelledby")).toBe("template-editor-label");
}, 15_000);
test("renders response data through the Mantine Tree with accessible path actions", async () => {
await render(
<ResponseTree
value={{ service: { name: "Homarr", online: true } }}
labels={{ copyPath: "Copy path", pathCopied: "Path copied", insertPath: "Insert path" }}
onInsertDataPath={vi.fn()}
/>,
);
expect(host.querySelector('[role="tree"]')).not.toBeNull();
expect(host.textContent).toContain("service");
expect(host.querySelector('button[aria-label="Copy path"]')).not.toBeNull();
expect(host.querySelector('button[aria-label="Insert path"]')).not.toBeNull();
});
test("keeps mutation preview execution disabled without a network-capability badge", async () => {
await render(
<PreviewHeader
method="POST"
url="https://example.com"
isTesting={false}
isSampleStale={false}
onTest={vi.fn()}
messages={{
title: "Preview",
test: "Test",
mutationDisabled: "Mutations are disabled",
staleTitle: "Stale",
staleDescription: "Run again",
}}
/>,
);
const testButton = [...host.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Test");
expect(testButton?.disabled).toBe(true);
expect(host.textContent).toContain("Mutations are disabled");
expect(host.textContent).not.toContain("Interactive");
});
test("shows an empty response state without exposing a raw JSON toggle", async () => {
await render(
<PreviewResponsePanel
value={null}
messages={{
empty: "No response",
sampleHint: "Add sample data",
editSample: "Edit sample",
addSample: "Add sample",
copied: "Copied",
copy: "Copy",
sampleLabel: "Sample",
sampleDescription: "JSON",
invalidSample: "Invalid JSON",
cancelSample: "Cancel",
applySample: "Apply",
copyPath: "Copy path",
pathCopied: "Path copied",
insertPath: "Insert path",
openRaw: "Open raw response",
}}
/>,
);
expect(host.textContent).toContain("No response");
expect(host.textContent).not.toContain("View raw JSON");
});
});