import { describe, expect, it } from "vitest";
import {
analyzeJsxTemplate,
analyzeCustomWidgetAccessibility,
analyzeRequestManifest,
appendDataPath,
createResponseTreeNode,
CUSTOM_JSX_TEMPLATE_LIMIT,
DEFAULT_CUSTOM_WIDGET_FORM_VALUES,
customWidgetFormSchema,
renameCustomWidgetRequest,
} from "../workbench";
import { CUSTOM_WIDGET_STARTER } from "../core/examples";
describe("Custom Widget workbench contracts", () => {
it("always provides a valid starter template", () => {
expect(DEFAULT_CUSTOM_WIDGET_FORM_VALUES.template.trim()).not.toBe("");
expect(
customWidgetFormSchema.safeParse({
...DEFAULT_CUSTOM_WIDGET_FORM_VALUES,
name: "Example",
url: "https://example.com/api",
}).success,
).toBe(true);
});
it("accepts 50,000 template characters and rejects 50,001", () => {
expect(analyzeJsxTemplate("x".repeat(CUSTOM_JSX_TEMPLATE_LIMIT))).not.toEqual(
expect.arrayContaining([expect.objectContaining({ code: "templateTooLong" })]),
);
expect(analyzeJsxTemplate("x".repeat(CUSTOM_JSX_TEMPLATE_LIMIT + 1))).toEqual(
expect.arrayContaining([expect.objectContaining({ code: "templateTooLong", severity: "error" })]),
);
});
it("validates full request manifests rather than accepting any JSON array", () => {
expect(analyzeRequestManifest("[]")).toEqual([]);
expect(analyzeRequestManifest('[{"id":"broken"}]')).toEqual(
expect.arrayContaining([expect.objectContaining({ code: "invalidRequestManifest", severity: "error" })]),
);
});
it("validates named request references and rejects inline network capabilities", () => {
expect(analyzeJsxTemplate('', { requestIds: ["known"] })).toEqual([]);
expect(analyzeJsxTemplate('', { requestIds: ["known"] })).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "inlineRequestProps" }),
expect.objectContaining({ code: "unknownRequest", value: "missing" }),
]),
);
});
it("diagnoses dynamic and unknown local-state bindings", () => {
expect(analyzeJsxTemplate('', { stateKeys: ["selectedDate"] })).toEqual([]);
expect(analyzeJsxTemplate('', { stateKeys: ["selectedDate"] })).toEqual([
expect.objectContaining({ code: "unknownStateBinding", value: "missing" }),
]);
expect(analyzeJsxTemplate("", { stateKeys: ["selectedDate"] })).toEqual(
expect.arrayContaining([expect.objectContaining({ code: "dynamicStateBinding" })]),
);
});
it("uses the AST security policy for computed reflective access while accepting benign fetch text", () => {
expect(analyzeJsxTemplate("{data['con' + 'structor']}")).toEqual(
expect.arrayContaining([expect.objectContaining({ code: "ast", severity: "error" })]),
);
expect(analyzeJsxTemplate("fetch the latest status")).toEqual([]);
});
it("builds safe response-tree paths for identifiers, arrays, and arbitrary object keys", () => {
expect(appendDataPath("data", "items", false)).toBe("data.items");
expect(appendDataPath("data.items", "0", true)).toBe("data.items[0]");
expect(appendDataPath("data", "first-name", false)).toBe('data["first-name"]');
const tree = createResponseTreeNode({ items: [{ name: "one" }] }, "data", "data");
expect(tree.children?.[0]?.children?.[0]?.children?.[0]?.value).toBe("data.items[0].name");
});
it("renames a request and every canonical reference atomically", () => {
const renamed = renameCustomWidgetRequest(
{
...CUSTOM_WIDGET_STARTER,
requests: [
{
id: "items",
sourceId: "default",
kind: "query",
method: "GET",
pathTemplate: "/items",
parameters: {},
auth: "none",
minimumBoardPermission: "view",
trigger: "manual",
},
{
id: "refresh",
sourceId: "default",
kind: "action",
method: "POST",
pathTemplate: "/refresh",
parameters: {},
auth: "none",
minimumBoardPermission: "modify",
trigger: "manual",
invalidates: ["items"],
},
],
optionsSchema: {
type: "object",
properties: {
item: {
type: "string",
"x-homarr": {
control: "select",
optionsSource: { requestId: "items", valuePath: "$.id", labelPath: "$.name" },
},
},
},
additionalProperties: false,
},
template: '{data["items"]}{status.items.ok}',
},
"items",
"inventory",
);
expect(renamed.requests[0]?.id).toBe("inventory");
expect(renamed.requests[1]?.invalidates).toEqual(["inventory"]);
expect(JSON.stringify(renamed.optionsSchema)).toContain('"requestId":"inventory"');
expect(renamed.template).toBe(
'{data["inventory"]}{status.inventory.ok}',
);
});
it("reports common accessibility omissions in preview", () => {
expect(
analyzeCustomWidgetAccessibility(''),
).toEqual(["imageAlt", "actionIconLabel", "inputLabel"]);
expect(
analyzeCustomWidgetAccessibility(
'',
),
).toEqual([]);
});
});