Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  config.browser.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { render } from "lit";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ThemeMode, ThemeName } from "../theme.ts";
import { renderConfig, resetConfigViewStateForTests, type ConfigProps } 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,
    showModeToggle: true,
    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(),
    onReset: vi.fn(),
    onSave: vi.fn(),
    onApply: vi.fn(),
    onUpdate: vi.fn(),
    onSubsectionChange: vi.fn(),
    version: "2026.3.11",
    theme: "claw" as ThemeName,
    themeMode: "system" as ThemeMode,
    setTheme: vi.fn(),
    setThemeMode: vi.fn(),
    hasCustomTheme: false,
    customThemeLabel: null,
    customThemeSourceUrl: null,
    customThemeImportUrl: "",
    customThemeImportBusy: false,
    customThemeImportMessage: null,
    customThemeImportExpanded: false,
    customThemeImportFocusToken: 0,
    onCustomThemeImportUrlChange: vi.fn(),
    onImportCustomTheme: vi.fn(),
    onClearCustomTheme: vi.fn(),
    onOpenCustomThemeImport: vi.fn(),
    borderRadius: 50,
    setBorderRadius: vi.fn(),
    gatewayUrl: "",
    assistantName: "OpenClaw",
  });

  function findActionButtons(container: HTMLElement): {
    clearButton?: HTMLButtonElement;
    saveButton?: HTMLButtonElement;
    applyButton?: HTMLButtonElement;
  } {
    const buttons = Array.from(container.querySelectorAll("button"));
    return {
      clearButton: buttons.find((btn) => btn.textContent?.trim() === "Clear"),
      saveButton: buttons.find((btn) => btn.textContent?.trim() === "Save"),
      applyButton: buttons.find((btn) => btn.textContent?.trim() === "Apply"),
    };
  }

  function renderConfigView(overrides: Partial<ConfigProps> = {}): {
    container: HTMLElement;
    props: ConfigProps;
  } {
    const container = document.createElement("div");
    const props = {
      ...baseProps(),
      ...overrides,
    };
    const rerender = () =>
      render(
        renderConfig({
          ...props,
          onRequestUpdate: rerender,
        }),
        container,
      );
    rerender();
    return { container, props };
  }

  function normalizedText(container: HTMLElement): string {
    return container.textContent?.replace(/\s+/g, " ").trim() ?? "";
  }

  beforeEach(() => {
    resetConfigViewStateForTests();
  });

  it("updates save/apply disabled state from form safety and raw dirtiness", () => {
    const container = document.createElement("div");

    const renderCase = (overrides: Partial<ConfigProps>) =>
      render(renderConfig({ ...baseProps(), ...overrides }), container);

    renderCase({
      schema: {
        type: "object",
        properties: {
          mixed: {
            anyOf: [{ type: "string" }, { type: "object", properties: {} }],
          },
        },
      },
      schemaLoading: false,
      uiHints: {},
      formMode: "form",
      formValue: { mixed: "x" },
    });
    let { saveButton, applyButton } = findActionButtons(container);
    expect(saveButton).not.toBeUndefined();
    expect(saveButton?.disabled).toBe(false);
    expect(applyButton?.disabled).toBe(false);

    renderCase({
      schema: null,
      formMode: "form",
      formValue: { gateway: { mode: "local" } },
      originalValue: {},
    });
    ({ saveButton, applyButton } = findActionButtons(container));
    expect(saveButton).not.toBeUndefined();
    expect(saveButton?.disabled).toBe(true);
    expect(applyButton?.disabled).toBe(true);

    renderCase({
      formMode: "raw",
      raw: "{\n}\n",
      originalRaw: "{\n}\n",
    });
    let clearButton: HTMLButtonElement | undefined;
    ({ clearButton, saveButton, applyButton } = findActionButtons(container));
    expect(clearButton).not.toBeUndefined();
    expect(saveButton).not.toBeUndefined();
    expect(applyButton).not.toBeUndefined();
    expect(clearButton?.disabled).toBe(true);
    expect(saveButton?.disabled).toBe(true);
    expect(applyButton?.disabled).toBe(true);

    const onReset = vi.fn();
    renderCase({
      formMode: "raw",
      raw: '{\n  gateway: { mode: "local" }\n}\n',
      originalRaw: "{\n}\n",
      onReset,
    });
    ({ clearButton, saveButton, applyButton } = findActionButtons(container));
    expect(saveButton).not.toBeUndefined();
    expect(applyButton).not.toBeUndefined();
    expect(clearButton?.disabled).toBe(false);
    expect(saveButton?.disabled).toBe(false);
    expect(applyButton?.disabled).toBe(false);

    clearButton?.click();
    expect(onReset).toHaveBeenCalledTimes(1);
  });

  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("forces Form mode and disables Raw mode when raw text is unavailable", () => {
    const onFormModeChange = vi.fn();
    const { container } = renderConfigView({
      formMode: "raw",
      rawAvailable: false,
      onFormModeChange,
      schema: {
        type: "object",
        properties: {
          gateway: {
            type: "object",
            properties: {
              mode: { type: "string" },
            },
          },
        },
      },
      formValue: { gateway: { mode: "local" } },
      originalValue: { gateway: { mode: "local" } },
    });

    const formButton = Array.from(container.querySelectorAll("button")).find(
      (btn) => btn.textContent?.trim() === "Form",
    );
    const rawButton = Array.from(container.querySelectorAll("button")).find(
      (btn) => btn.textContent?.trim() === "Raw",
    );
    expect(formButton?.classList.contains("active")).toBe(true);
    expect(rawButton?.disabled).toBe(true);
    const rawNotice = container.querySelector(".config-actions__notice");
    const actionButtons = container.querySelector(".config-actions__buttons");
    expect(rawNotice).not.toBeNull();
    expect(actionButtons).not.toBeNull();
    expect(actionButtons?.textContent).toContain("Reload");
    expect(actionButtons?.textContent).toContain("Update");
    expect(normalizedText(container)).toContain(
      "Raw mode disabled (snapshot cannot safely round-trip raw text).",
    );
    expect(container.querySelector(".config-raw-field")).toBeNull();

    rawButton?.click();
    expect(onFormModeChange).not.toHaveBeenCalled();
  });

  it("renders section tabs and 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 tabs = Array.from(container.querySelectorAll(".config-top-tabs__tab")).map((tab) =>
      tab.textContent?.trim(),
    );
    expect(tabs).toContain("Settings");
    expect(tabs).toContain("Agents");
    expect(tabs).toContain("Gateway");

    const btn = Array.from(container.querySelectorAll("button")).find(
      (b) => b.textContent?.trim() === "Gateway",
    );
    expect(btn).toBeTruthy();
    btn?.click();
    expect(onSectionChange).toHaveBeenCalledWith("gateway");
  });

  it("resets config content scroll when switching top-tab sections", async () => {
    const { container } = renderConfigView({
      activeSection: "channels",
      navRootLabel: "Communication",
      includeSections: ["channels", "messages"],
      schema: {
        type: "object",
        properties: {
          channels: {
            type: "object",
            properties: {
              telegram: { type: "string" },
            },
          },
          messages: {
            type: "object",
            properties: {
              inbox: { type: "string" },
            },
          },
        },
      },
      formValue: {
        channels: { telegram: "on" },
        messages: { inbox: "smart" },
      },
      originalValue: {
        channels: { telegram: "on" },
        messages: { inbox: "smart" },
      },
    });

    const content = container.querySelector<HTMLElement>(".config-content");
    expect(content).toBeTruthy();
    if (!content) {
      return;
    }
    content.scrollTop = 280;
    content.scrollLeft = 24;
    content.scrollTo = vi.fn(({ top, left }: { top?: number; left?: number }) => {
      content.scrollTop = top ?? content.scrollTop;
      content.scrollLeft = left ?? content.scrollLeft;
    }) as typeof content.scrollTo;

    const messagesButton = Array.from(container.querySelectorAll("button")).find(
      (btn) => btn.textContent?.trim() === "Messages",
    );
    expect(messagesButton).toBeTruthy();

    messagesButton?.click();
    await Promise.resolve();

    expect(content.scrollTo).toHaveBeenCalledOnce();
    expect(content.scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: "auto" });
    expect(content.scrollTop).toBe(0);
    expect(content.scrollLeft).toBe(0);
  });

  it("renders and wires the search field controls", () => {
    const container = document.createElement("div");
    const onSearchChange = vi.fn();
    render(
      renderConfig({
        ...baseProps(),
        searchQuery: "gateway",
        onSearchChange,
      }),
      container,
    );

    const icon = container.querySelector<SVGElement>(".config-search__icon");
    expect(icon).not.toBeNull();
    expect(icon?.closest(".config-search__input-row")).not.toBeNull();

    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");

    const clearButton = container.querySelector<HTMLButtonElement>(".config-search__clear");
    expect(clearButton).toBeTruthy();
    clearButton?.click();
    expect(onSearchChange).toHaveBeenCalledWith("");
  });

  it("keeps sensitive raw config hidden until reveal before editing", () => {
    const onRawChange = vi.fn();
    const { container } = renderConfigView({
      formMode: "raw",
      raw: '{\n  "openai": { "apiKey": "supersecret" }\n}\n',
      originalRaw: '{\n  "openai": { "apiKey": "supersecret" }\n}\n',
      formValue: {
        openai: {
          apiKey: "supersecret",
        },
      },
      onRawChange,
    });

    const text = normalizedText(container);
    expect(text).toContain("1 secret redacted");
    expect(text).toContain("Use the reveal button above to edit the raw config.");
    expect(text).not.toContain("supersecret");
    expect(container.querySelector("textarea")).toBeNull();

    const revealButton = container.querySelector<HTMLButtonElement>(".config-raw-toggle");
    expect(revealButton).toBeTruthy();
    revealButton?.click();

    const textarea = container.querySelector<HTMLTextAreaElement>("textarea");
    expect(textarea).not.toBeNull();
    expect(textarea?.value).toContain("supersecret");
    if (!textarea) {
      return;
    }
    textarea.value = textarea.value.replace("supersecret", "updatedsecret");
    textarea.dispatchEvent(new Event("input", { bubbles: true }));
    expect(onRawChange).toHaveBeenCalledWith(textarea.value);
  });

  it("renders structured SecretRef values without stringifying", () => {
    const onFormPatch = vi.fn();
    const secretRefSchema = {
      type: "object" as const,
      properties: {
        channels: {
          type: "object" as const,
          properties: {
            discord: {
              type: "object" as const,
              properties: {
                token: { type: "string" as const },
              },
            },
          },
        },
      },
    };
    const secretRefValue = {
      channels: {
        discord: {
          token: { source: "env", provider: "default", id: "__OPENCLAW_REDACTED__" },
        },
      },
    };
    const secretRefOriginalValue = {
      channels: {
        discord: {
          token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
        },
      },
    };
    const { container } = renderConfigView({
      schema: secretRefSchema,
      uiHints: {
        "channels.discord.token": { sensitive: true },
      },
      formMode: "form",
      formValue: secretRefValue,
      originalValue: secretRefOriginalValue,
      onFormPatch,
    });

    const input = container.querySelector<HTMLInputElement>(".cfg-input");
    expect(input).not.toBeNull();
    expect(input?.readOnly).toBe(true);
    expect(input?.value).toBe("");
    expect(input?.placeholder).toContain("Structured value (SecretRef)");
    expect(container.textContent ?? "").not.toContain("[object Object]");

    if (!input) {
      return;
    }
    input.value = "[object Object]";
    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    expect(onFormPatch).not.toHaveBeenCalled();

    render(
      renderConfig({
        ...baseProps(),
        rawAvailable: false,
        formMode: "raw",
        schema: secretRefSchema,
        uiHints: {
          "channels.discord.token": { sensitive: true },
        },
        formValue: secretRefValue,
        originalValue: secretRefOriginalValue,
      }),
      container,
    );

    const rawUnavailableInput = container.querySelector<HTMLInputElement>(".cfg-input");
    expect(rawUnavailableInput).not.toBeNull();
    expect(rawUnavailableInput?.placeholder).toBe(
      "Structured value (SecretRef) - edit the config file directly",
    );
  });

  it("keeps malformed non-SecretRef object values editable when raw mode is unavailable", () => {
    const onFormPatch = vi.fn();
    const { container } = renderConfigView({
      rawAvailable: false,
      formMode: "raw",
      schema: {
        type: "object",
        properties: {
          gateway: {
            type: "object",
            properties: {
              mode: { type: "string" },
            },
          },
        },
      },
      formValue: {
        gateway: {
          mode: { malformed: true },
        },
      },
      originalValue: {
        gateway: {
          mode: { malformed: true },
        },
      },
      onFormPatch,
    });

    const input = container.querySelector<HTMLInputElement>(".cfg-input");
    expect(input).not.toBeNull();
    expect(input?.readOnly).toBe(false);
    expect(input?.value).toContain("malformed");
    expect(input?.value).not.toBe("[object Object]");
    expect(input?.placeholder).not.toContain("Structured value (SecretRef)");

    if (!input) {
      return;
    }
    input.value = "local";
    input.dispatchEvent(new Event("input", { bubbles: true }));
    expect(onFormPatch).toHaveBeenCalledWith(["gateway", "mode"], "local");
  });

  it("opens the tweakcn importer when custom is clicked without an imported theme", () => {
    const onOpenCustomThemeImport = vi.fn();
    const { container } = renderConfigView({
      activeSection: "__appearance__",
      includeSections: ["__appearance__"],
      onOpenCustomThemeImport,
    });

    const customButton = Array.from(container.querySelectorAll("button")).find(
      (btn) => btn.textContent?.trim() === "Custom",
    );

    expect(customButton?.disabled).toBe(false);
    expect(normalizedText(container)).toContain("Click Custom to import a tweakcn theme");

    customButton?.click();

    expect(onOpenCustomThemeImport).toHaveBeenCalledTimes(1);
  });

  it("shows the tweakcn importer once the custom slot is opened", () => {
    const { container } = renderConfigView({
      activeSection: "__appearance__",
      includeSections: ["__appearance__"],
      customThemeImportExpanded: true,
      customThemeImportFocusToken: 1,
    });

    const importButton = Array.from(container.querySelectorAll("button")).find((btn) =>
      btn.textContent?.includes("Import custom theme"),
    );

    expect(importButton?.disabled).toBe(true);
    expect(container.querySelector(".settings-theme-import__input")).not.toBeNull();
  });

  it("shows custom theme actions once a tweakcn import exists", () => {
    const setTheme = vi.fn();
    const onClearCustomTheme = vi.fn();
    const onImportCustomTheme = vi.fn();
    const onCustomThemeImportUrlChange = vi.fn();
    const { container } = renderConfigView({
      activeSection: "__appearance__",
      includeSections: ["__appearance__"],
      hasCustomTheme: true,
      customThemeLabel: "Light Green",
      customThemeSourceUrl: "https://tweakcn.com/themes/cmlhfpjhw000004l4f4ax3m7z",
      customThemeImportUrl: "https://tweakcn.com/themes/cmlhfpjhw000004l4f4ax3m7z",
      setTheme,
      onClearCustomTheme,
      onImportCustomTheme,
      onCustomThemeImportUrlChange,
    });

    const customButton = Array.from(container.querySelectorAll("button")).find(
      (btn) => btn.textContent?.trim() === "Custom",
    );
    expect(customButton?.disabled).toBe(false);
    customButton?.click();
    expect(setTheme).toHaveBeenCalledWith("custom", expect.any(Object));

    const replaceButton = Array.from(container.querySelectorAll("button")).find((btn) =>
      btn.textContent?.includes("Replace custom theme"),
    );
    const clearButton = Array.from(container.querySelectorAll("button")).find((btn) =>
      btn.textContent?.includes("Clear custom theme"),
    );
    replaceButton?.click();
    clearButton?.click();

    expect(onImportCustomTheme).toHaveBeenCalledTimes(1);
    expect(onClearCustomTheme).toHaveBeenCalledTimes(1);
    expect(normalizedText(container)).toContain("Loaded Light Green");

    const input = container.querySelector(".settings-theme-import__input") as HTMLInputElement;
    input.value = "https://tweakcn.com/themes/custom";
    input.dispatchEvent(new Event("input"));
    expect(onCustomThemeImportUrlChange).toHaveBeenCalledWith("https://tweakcn.com/themes/custom");
  });
});

¤ Dauer der Verarbeitung: 0.23 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge