test: harden custom widget AI authoring

This commit is contained in:
ajnart committed 2026-07-30 11:09:17 +02:00
1 parent 2f0c0a07dd
commit 6eff1da08f
14 files changed
+1056 -23

No files matched your search

@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import { customWidgetDefinitionSchema } from "@homarr/custom-widgets/core";
import {
applyDefinition,
applyCustomWidgetAiResponse,
filterSecretsForSourceAuthentication,
getChangedSecrets,
getCustomWidgetPreviewOptionIssues,
@@ -24,9 +24,9 @@ const definition = customWidgetDefinitionSchema.parse({
});
describe("Custom Widget workbench preview options", () => {
it("loads a complete pasted widget template into the JSX editor field", () => {
it("loads one complete AI response into every editor field, including JSX", () => {
const setValues = vi.fn();
applyDefinition(
const result = applyCustomWidgetAiResponse(
{
values: {
name: "",
@@ -40,10 +40,22 @@ describe("Custom Widget workbench preview options", () => {
},
setValues,
} as never,
definition,
`\`\`\`json\n${JSON.stringify(definition)}\n\`\`\``,
);
expect(setValues).toHaveBeenCalledWith(expect.objectContaining({ template: "<Text>Preview</Text>" }));
if (!result.success) throw new Error("Expected the complete AI response to be accepted");
expect(setValues).toHaveBeenCalledOnce();
const values = setValues.mock.calls[0]?.[0];
expect(values).toMatchObject({
name: "Preview options",
description: "",
iconUrl: "",
template: "<Text>Preview</Text>",
secrets: [],
});
expect(JSON.parse(values?.sources as string)).toEqual(result.widget.sources);
expect(JSON.parse(values?.requests as string)).toEqual(result.widget.requests);
expect(JSON.parse(values?.options as string)).toEqual(result.widget.options);
});
it("shows the default source first regardless of manifest key order", () => {
@@ -3,6 +3,7 @@ import {
customWidgetDefinitionSchema,
getCustomWidgetDefaultOptions,
getCustomWidgetRequiredSecretKinds,
parseCustomWidgetAiResponse,
validateCustomWidgetOptions,
} from "@homarr/custom-widgets/core";
import type { CustomWidgetSource, HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
@@ -56,6 +57,12 @@ export function applyDefinition(form: CustomWidgetWorkbenchForm, widget: HomarrC
});
}
export function applyCustomWidgetAiResponse(form: CustomWidgetWorkbenchForm, response: string) {
const result = parseCustomWidgetAiResponse(response);
if (result.success) applyDefinition(form, result.widget);
return result;
}
export function parseSources(value: string): Array<CustomWidgetSource & { id: string }> {
const parsed = parseJson(value);
return isRecord(parsed)
@@ -6,17 +6,13 @@ import { useRouter } from "next/navigation";
import type { UseFormReturnType } from "@mantine/form";
import { clientApi } from "@homarr/api/client";
import {
customWidgetDefinitionSchema,
formatCustomWidgetImportIssues,
parseCustomWidgetAiResponse,
} from "@homarr/custom-widgets/core";
import { formatCustomWidgetImportIssues } from "@homarr/custom-widgets/core";
import type { CustomWidgetFormValues } from "@homarr/custom-widgets/workbench";
import { showErrorNotification, showSuccessNotification, showWarningNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import {
applyDefinition,
applyCustomWidgetAiResponse,
buildDefinition,
getChangedSecrets,
getDefinitionDefaults,
@@ -167,11 +163,10 @@ export function useCustomWidgetFormActions(input: FormActionsInput) {
const pasteAiResponse = async () => {
try {
const result = parseCustomWidgetAiResponse(await navigator.clipboard.readText());
const result = applyCustomWidgetAiResponse(input.form, await navigator.clipboard.readText());
if (!result.success) {
throw new Error(formatCustomWidgetImportIssues(result.issues));
}
applyDefinition(input.form, customWidgetDefinitionSchema.parse(result.widget));
input.setOptionsSnapshot(getDefinitionDefaults(result.widget));
input.setPreview({ data: {}, status: {}, session: null, outcome: "idle" });
showSuccessNotification({