Files
openclaw/ui/src/ui/views/config.browser.test.ts
2026-03-12 06:15:47 -05:00

234 lines
6.3 KiB
TypeScript

import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
import { renderConfig } from "./config.ts";
describe("config view", () => {
const baseProps = () => ({
raw: "{\n}\n",
originalRaw: "{\n}\n",
valid: true,
issues: [],
loading: false,
saving: false,
applying: false,
updating: false,
connected: true,
schema: {
type: "object",
properties: {},
},
schemaLoading: false,
uiHints: {},
formMode: "form" as const,
formValue: {},
originalValue: {},
searchQuery: "",
activeSection: null,
activeSubsection: null,
onRawChange: vi.fn(),
onFormModeChange: vi.fn(),
onFormPatch: vi.fn(),
onSearchChange: vi.fn(),
onSectionChange: vi.fn(),
onReload: vi.fn(),
onSave: vi.fn(),
onApply: vi.fn(),
onUpdate: vi.fn(),
onSubsectionChange: vi.fn(),
});
function findActionButtons(container: HTMLElement): {
saveButton?: HTMLButtonElement;
applyButton?: HTMLButtonElement;
} {
const buttons = Array.from(container.querySelectorAll("button"));
return {
saveButton: buttons.find((btn) => btn.textContent?.trim() === "Save"),
applyButton: buttons.find((btn) => btn.textContent?.trim() === "Apply"),
};
}
it("allows save when form is unsafe", () => {
const container = document.createElement("div");
render(
renderConfig({
...baseProps(),
schema: {
type: "object",
properties: {
mixed: {
anyOf: [{ type: "string" }, { type: "object", properties: {} }],
},
},
},
schemaLoading: false,
uiHints: {},
formMode: "form",
formValue: { mixed: "x" },
}),
container,
);
const saveButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Save",
);
expect(saveButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(false);
});
it("disables save when schema is missing", () => {
const container = document.createElement("div");
render(
renderConfig({
...baseProps(),
schema: null,
formMode: "form",
formValue: { gateway: { mode: "local" } },
originalValue: {},
}),
container,
);
const saveButton = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.textContent?.trim() === "Save",
);
expect(saveButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(true);
});
it("disables save and apply when raw is unchanged", () => {
const container = document.createElement("div");
render(
renderConfig({
...baseProps(),
formMode: "raw",
raw: "{\n}\n",
originalRaw: "{\n}\n",
}),
container,
);
const { saveButton, applyButton } = findActionButtons(container);
expect(saveButton).not.toBeUndefined();
expect(applyButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(true);
expect(applyButton?.disabled).toBe(true);
});
it("enables save and apply when raw changes", () => {
const container = document.createElement("div");
render(
renderConfig({
...baseProps(),
formMode: "raw",
raw: '{\n gateway: { mode: "local" }\n}\n',
originalRaw: "{\n}\n",
}),
container,
);
const { saveButton, applyButton } = findActionButtons(container);
expect(saveButton).not.toBeUndefined();
expect(applyButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(false);
expect(applyButton?.disabled).toBe(false);
});
it("switches mode via the sidebar toggle", () => {
const container = document.createElement("div");
const onFormModeChange = vi.fn();
render(
renderConfig({
...baseProps(),
onFormModeChange,
}),
container,
);
const btn = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "Raw",
);
expect(btn).toBeTruthy();
btn?.click();
expect(onFormModeChange).toHaveBeenCalledWith("raw");
});
it("switches sections from the sidebar", () => {
const container = document.createElement("div");
const onSectionChange = vi.fn();
render(
renderConfig({
...baseProps(),
onSectionChange,
schema: {
type: "object",
properties: {
gateway: { type: "object", properties: {} },
agents: { type: "object", properties: {} },
},
},
}),
container,
);
const btn = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "Gateway",
);
expect(btn).toBeTruthy();
btn?.click();
expect(onSectionChange).toHaveBeenCalledWith("gateway");
});
it("wires search input to onSearchChange", () => {
const container = document.createElement("div");
const onSearchChange = vi.fn();
render(
renderConfig({
...baseProps(),
onSearchChange,
}),
container,
);
const input = container.querySelector(".config-search__input");
expect(input).not.toBeNull();
if (!input) {
return;
}
(input as HTMLInputElement).value = "gateway";
input.dispatchEvent(new Event("input", { bubbles: true }));
expect(onSearchChange).toHaveBeenCalledWith("gateway");
});
it("shows all tag options in compact tag picker", () => {
const container = document.createElement("div");
render(renderConfig(baseProps()), container);
const options = Array.from(container.querySelectorAll(".config-search__tag-option")).map(
(option) => option.textContent?.trim(),
);
expect(options).toContain("tag:security");
expect(options).toContain("tag:advanced");
expect(options).toHaveLength(15);
});
it("updates search query when toggling a tag option", () => {
const container = document.createElement("div");
const onSearchChange = vi.fn();
render(
renderConfig({
...baseProps(),
onSearchChange,
}),
container,
);
const option = container.querySelector<HTMLButtonElement>(
'.config-search__tag-option[data-tag="security"]',
);
expect(option).toBeTruthy();
option?.click();
expect(onSearchChange).toHaveBeenCalledWith("tag:security");
});
});